diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 23b12c646..378eec13c 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -2066,6 +2066,9 @@
"message": "Nonce is higher than suggested nonce of $1",
"description": "The next nonce according to MetaMask's internal logic"
},
+ "nft": {
+ "message": "NFT"
+ },
"nftTokenIdPlaceholder": {
"message": "Enter the Token ID"
},
diff --git a/ui/hooks/useAssetDetails.js b/ui/hooks/useAssetDetails.js
index ce9ef30ba..ce8d00253 100644
--- a/ui/hooks/useAssetDetails.js
+++ b/ui/hooks/useAssetDetails.js
@@ -1,7 +1,7 @@
import { useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { parseStandardTokenTransactionData } from '../../shared/modules/transaction.utils';
-import { getCollectibles, getTokens } from '../ducks/metamask/metamask';
+import { getCollectibles } from '../ducks/metamask/metamask';
import { ERC1155, ERC721, ERC20 } from '../helpers/constants/common';
import {
calcTokenAmount,
@@ -9,16 +9,13 @@ import {
getTokenAddressParam,
getTokenValueParam,
} from '../helpers/utils/token-util';
-import { getTokenList } from '../selectors';
import { hideLoadingIndication, showLoadingIndication } from '../store/actions';
import { usePrevious } from './usePrevious';
export function useAssetDetails(tokenAddress, userAddress, transactionData) {
const dispatch = useDispatch();
// state selectors
- const tokens = useSelector(getTokens);
const collectibles = useSelector(getCollectibles);
- const tokenList = useSelector(getTokenList);
// in-hook state
const [currentAsset, setCurrentAsset] = useState(null);
@@ -36,8 +33,6 @@ export function useAssetDetails(tokenAddress, userAddress, transactionData) {
userAddress,
transactionData,
collectibles,
- tokens,
- tokenList,
);
setCurrentAsset(assetDetails);
dispatch(hideLoadingIndication());
@@ -58,8 +53,6 @@ export function useAssetDetails(tokenAddress, userAddress, transactionData) {
userAddress,
transactionData,
collectibles,
- tokens,
- tokenList,
]);
let assetStandard,
@@ -83,11 +76,13 @@ export function useAssetDetails(tokenAddress, userAddress, transactionData) {
balance,
decimals: currentAssetDecimals,
} = currentAsset;
+
const tokenData = parseStandardTokenTransactionData(transactionData);
assetStandard = standard;
assetAddress = tokenAddress;
- tokenSymbol = symbol;
+ tokenSymbol = symbol ?? '';
tokenImage = image;
+
toAddress = getTokenAddressParam(tokenData);
if (assetStandard === ERC721 || assetStandard === ERC1155) {
assetName = name;
@@ -101,6 +96,7 @@ export function useAssetDetails(tokenAddress, userAddress, transactionData) {
calcTokenAmount(getTokenValueParam(tokenData), decimals).toString(10);
}
}
+
return {
assetStandard,
assetName,
diff --git a/ui/hooks/useAssetDetails.test.js b/ui/hooks/useAssetDetails.test.js
new file mode 100644
index 000000000..f580a550d
--- /dev/null
+++ b/ui/hooks/useAssetDetails.test.js
@@ -0,0 +1,197 @@
+import React from 'react';
+import { Provider } from 'react-redux';
+import { renderHook } from '@testing-library/react-hooks';
+
+import configureStore from '../store/store';
+import * as tokenUtils from '../helpers/utils/token-util';
+import { ERC1155, ERC20, ERC721 } from '../helpers/constants/common';
+import { useAssetDetails } from './useAssetDetails';
+
+const renderUseAssetDetails = ({
+ tokenAddress,
+ userAddress,
+ transactionData,
+}) => {
+ const mockState = {
+ metamask: {
+ provider: {
+ type: 'test',
+ chainId: '0x3',
+ },
+ tokenList: {},
+ },
+ };
+
+ const wrapper = ({ children }) => (
+ {children}
+ );
+
+ return renderHook(
+ () => useAssetDetails(tokenAddress, userAddress, transactionData),
+ { wrapper },
+ );
+};
+
+describe('useAssetDetails', () => {
+ let getAssetDetailsStub;
+ beforeEach(() => {
+ getAssetDetailsStub = jest
+ .spyOn(tokenUtils, 'getAssetDetails')
+ .mockImplementation(() => Promise.resolve({}));
+ });
+ it('should return object with tokenSymbol set to and empty string, when getAssetDetails returns and empty object', async () => {
+ const toAddress = '000000000000000000000000000000000000dead';
+ const tokenAddress = '0x1';
+
+ const transactionData = `0xa9059cbb000000000000000000000000${toAddress}000000000000000000000000000000000000000000000000016345785d8a0000`;
+
+ const { result, waitForNextUpdate } = renderUseAssetDetails({
+ tokenAddress,
+ userAddress: '0x111',
+ transactionData,
+ });
+
+ await waitForNextUpdate();
+
+ expect(result.current).toStrictEqual({
+ assetAddress: tokenAddress,
+ assetName: undefined,
+ assetStandard: undefined,
+ decimals: undefined,
+ toAddress: `0x${toAddress}`,
+ tokenAmount: undefined,
+ tokenId: undefined,
+ tokenImage: undefined,
+ tokenSymbol: '',
+ tokenValue: undefined,
+ userBalance: undefined,
+ });
+ });
+
+ it('should return object with correct tokenValues for an ERC20 token', async () => {
+ const userAddress = '0xf04a5cc80b1e94c69b48f5ee68a08cd2f09a7c3e';
+ const tokenAddress = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';
+ const toAddress = '000000000000000000000000000000000000dead';
+ const transactionData = `0xa9059cbb000000000000000000000000${toAddress}00000000000000000000000000000000000000000000000000000000000001f4`;
+
+ const standard = ERC20;
+ const symbol = 'WETH';
+ const balance = '1';
+ const decimals = 18;
+
+ getAssetDetailsStub.mockImplementation(() =>
+ Promise.resolve({
+ standard,
+ symbol,
+ balance,
+ decimals,
+ }),
+ );
+
+ const { result, waitForNextUpdate } = renderUseAssetDetails({
+ tokenAddress,
+ userAddress,
+ transactionData,
+ });
+
+ await waitForNextUpdate();
+
+ expect(result.current).toStrictEqual({
+ assetAddress: tokenAddress,
+ assetName: undefined,
+ assetStandard: standard,
+ decimals,
+ toAddress: `0x${toAddress}`,
+ tokenAmount: '0.0000000000000005',
+ tokenId: undefined,
+ tokenImage: undefined,
+ tokenSymbol: symbol,
+ tokenValue: undefined,
+ userBalance: balance,
+ });
+ });
+
+ it('should return object with correct tokenValues for an ERC721 token', async () => {
+ const tokenAddress = '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D';
+ const toAddress = '000000000000000000000000000000000000dead';
+ const transactionData = `0x23b872dd000000000000000000000000a544eebe103733f22ef62af556023bc918b73d36000000000000000000000000${toAddress}000000000000000000000000000000000000000000000000000000000000000c`;
+
+ const symbol = 'BAYC';
+ const tokenId = '12';
+ const name = 'BoredApeYachtClub';
+ const image =
+ 'https://bafybeihw3gvmthmvrenfmcvagtais5tv7r4nmiezgsv7nyknjubxw4lite.ipfs.dweb.link';
+ const standard = ERC721;
+
+ getAssetDetailsStub.mockImplementation(() =>
+ Promise.resolve({
+ standard,
+ symbol,
+ name,
+ tokenId,
+ image,
+ }),
+ );
+
+ const { result, waitForNextUpdate } = renderUseAssetDetails({
+ tokenAddress,
+ transactionData,
+ });
+
+ await waitForNextUpdate();
+
+ expect(result.current).toStrictEqual({
+ assetAddress: tokenAddress,
+ assetName: name,
+ assetStandard: standard,
+ decimals: undefined,
+ toAddress: `0x${toAddress}`,
+ tokenId,
+ tokenImage: image,
+ tokenSymbol: symbol,
+ tokenValue: undefined,
+ userBalance: undefined,
+ tokenAmount: undefined,
+ });
+ });
+
+ it('should return object with correct tokenValues for an ERC1155 token', async () => {
+ const tokenAddress = '0x76BE3b62873462d2142405439777e971754E8E77';
+ const toAddress = '000000000000000000000000000000000000dead';
+ const transactionData = `0xf242432a000000000000000000000000a544eebe103733f22ef62af556023bc918b73d36000000000000000000000000000000000000000000000000000000000000dead0000000000000000000000000000000000000000000000000000000000000322000000000000000000000000000000000000000000000000000000000000009c00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000`;
+
+ const tokenId = '121';
+ const image =
+ 'https://bafybeihw3gvmthmvrenfmcvagtais5tv7r4nmiezgsv7nyknjubxw4lite.ipfs.dweb.link';
+ const standard = ERC1155;
+
+ getAssetDetailsStub.mockImplementation(() =>
+ Promise.resolve({
+ standard,
+ tokenId,
+ image,
+ }),
+ );
+
+ const { result, waitForNextUpdate } = renderUseAssetDetails({
+ tokenAddress,
+ transactionData,
+ });
+
+ await waitForNextUpdate();
+
+ expect(result.current).toStrictEqual({
+ assetAddress: tokenAddress,
+ assetName: undefined,
+ assetStandard: standard,
+ decimals: undefined,
+ toAddress: `0x${toAddress}`,
+ tokenId: undefined,
+ tokenImage: image,
+ tokenSymbol: '',
+ tokenValue: undefined,
+ userBalance: undefined,
+ tokenAmount: undefined,
+ });
+ });
+});
diff --git a/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js b/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js
index 7487e594b..7a6d32cce 100644
--- a/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js
+++ b/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js
@@ -62,6 +62,7 @@ export default class ConfirmApproveContent extends Component {
txData: PropTypes.object,
fromAddressIsLedger: PropTypes.bool,
chainId: PropTypes.string,
+ tokenAddress: PropTypes.string,
rpcPrefs: PropTypes.object,
isContract: PropTypes.bool,
hexTransactionTotal: PropTypes.string,
@@ -183,7 +184,9 @@ export default class ConfirmApproveContent extends Component {
renderERC721OrERC1155PermissionContent() {
const { t } = this.context;
- const { origin, toAddress, isContract, assetName, tokenId } = this.props;
+ const { origin, toAddress, isContract } = this.props;
+
+ const titleTokenDescription = this.getTitleTokenDescription();
const displayedAddress = isContract
? `${t('contract')} (${addressSummary(toAddress)})`
@@ -198,7 +201,7 @@ export default class ConfirmApproveContent extends Component {
{t('approvedAsset')}:
- {`${assetName} #${tokenId}`}
+ {titleTokenDescription}
@@ -430,6 +433,82 @@ export default class ConfirmApproveContent extends Component {
);
}
+ getTitleTokenDescription() {
+ const {
+ tokenId,
+ assetName,
+ tokenAddress,
+ rpcPrefs,
+ chainId,
+ assetStandard,
+ tokenSymbol,
+ } = this.props;
+ const { t } = this.context;
+ let titleTokenDescription = t('token');
+ if (rpcPrefs?.blockExplorerUrl || chainId) {
+ const unknownTokenBlockExplorerLink = getTokenTrackerLink(
+ tokenAddress,
+ chainId,
+ null,
+ {
+ blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null,
+ },
+ );
+
+ const unknownTokenLink = (
+
+ {t('token')}
+
+ );
+ titleTokenDescription = unknownTokenLink;
+ }
+
+ if (assetStandard === ERC20 || (tokenSymbol && !tokenId)) {
+ titleTokenDescription = tokenSymbol;
+ } else if (
+ assetStandard === ERC721 ||
+ assetStandard === ERC1155 ||
+ // if we don't have an asset standard but we do have either both an assetname and a tokenID or both a tokenSymbol and tokenId we assume its an NFT
+ (assetName && tokenId) ||
+ (tokenSymbol && tokenId)
+ ) {
+ const tokenIdWrapped = tokenId ? ` (#${tokenId})` : null;
+ if (assetName || tokenSymbol) {
+ titleTokenDescription = `${assetName ?? tokenSymbol} ${tokenIdWrapped}`;
+ } else {
+ const unknownNFTBlockExplorerLink = getTokenTrackerLink(
+ tokenAddress,
+ chainId,
+ null,
+ {
+ blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null,
+ },
+ );
+ const unknownNFTLink = (
+ <>
+
+ {t('nft')}
+
+ {tokenIdWrapped &&
{tokenIdWrapped}}
+ >
+ );
+ titleTokenDescription = unknownNFTLink;
+ }
+ }
+
+ return titleTokenDescription;
+ }
+
render() {
const { t } = this.context;
const {
@@ -452,11 +531,11 @@ export default class ConfirmApproveContent extends Component {
rpcPrefs,
isContract,
assetStandard,
- tokenId,
- assetName,
} = this.props;
const { showFullTxDetails } = this.state;
+ const titleTokenDescription = this.getTitleTokenDescription();
+
return (
- {t('allowSpendToken', [
- assetStandard === ERC20
- ? tokenSymbol
- : `${assetName} (#${tokenId})`,
- ])}
+ {t('allowSpendToken', [titleTokenDescription])}
{t('trustSiteApprovePermission', [
@@ -554,7 +629,9 @@ export default class ConfirmApproveContent extends Component {
: getAccountLink(
toAddress,
chainId,
- { blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null },
+ {
+ blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null,
+ },
null,
);
global.platform.openTab({
diff --git a/ui/pages/confirm-approve/confirm-approve-content/index.scss b/ui/pages/confirm-approve/confirm-approve-content/index.scss
index 0f2593ee4..473be2f7b 100644
--- a/ui/pages/confirm-approve/confirm-approve-content/index.scss
+++ b/ui/pages/confirm-approve/confirm-approve-content/index.scss
@@ -9,6 +9,10 @@
padding: 0 24px 16px 24px;
}
+ &__unknown-asset {
+ color: var(--color-primary-default);
+ }
+
&__icon-display-content {
display: flex;
height: 51px;
diff --git a/ui/pages/confirm-approve/confirm-approve.js b/ui/pages/confirm-approve/confirm-approve.js
index 246baca77..6dca97d1b 100644
--- a/ui/pages/confirm-approve/confirm-approve.js
+++ b/ui/pages/confirm-approve/confirm-approve.js
@@ -52,6 +52,7 @@ export default function ConfirmApprove({
tokenId,
userAddress,
toAddress,
+ tokenAddress,
transaction,
ethTransactionTotal,
fiatTransactionTotal,
@@ -173,6 +174,7 @@ export default function ConfirmApprove({
tokenId={tokenId}
assetName={assetName}
assetStandard={assetStandard}
+ tokenAddress={tokenAddress}
showCustomizeGasModal={approveTransaction}
showEditApprovalPermissionModal={({
/* eslint-disable no-shadow */
@@ -268,6 +270,7 @@ export default function ConfirmApprove({
ConfirmApprove.propTypes = {
assetStandard: PropTypes.string,
assetName: PropTypes.string,
+ tokenAddress: PropTypes.string,
userBalance: PropTypes.string,
tokenSymbol: PropTypes.string,
decimals: PropTypes.string,