1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Fix to infinite loading on approve screen (#14756)

This commit is contained in:
Alex Donesky 2022-06-11 12:55:35 -05:00 committed by Dan J Miller
parent 2c6236ed4f
commit f561aeeef6
6 changed files with 299 additions and 19 deletions

View File

@ -2066,6 +2066,9 @@
"message": "Nonce is higher than suggested nonce of $1", "message": "Nonce is higher than suggested nonce of $1",
"description": "The next nonce according to MetaMask's internal logic" "description": "The next nonce according to MetaMask's internal logic"
}, },
"nft": {
"message": "NFT"
},
"nftTokenIdPlaceholder": { "nftTokenIdPlaceholder": {
"message": "Enter the Token ID" "message": "Enter the Token ID"
}, },

View File

@ -1,7 +1,7 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { parseStandardTokenTransactionData } from '../../shared/modules/transaction.utils'; 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 { ERC1155, ERC721, ERC20 } from '../helpers/constants/common';
import { import {
calcTokenAmount, calcTokenAmount,
@ -9,16 +9,13 @@ import {
getTokenAddressParam, getTokenAddressParam,
getTokenValueParam, getTokenValueParam,
} from '../helpers/utils/token-util'; } from '../helpers/utils/token-util';
import { getTokenList } from '../selectors';
import { hideLoadingIndication, showLoadingIndication } from '../store/actions'; import { hideLoadingIndication, showLoadingIndication } from '../store/actions';
import { usePrevious } from './usePrevious'; import { usePrevious } from './usePrevious';
export function useAssetDetails(tokenAddress, userAddress, transactionData) { export function useAssetDetails(tokenAddress, userAddress, transactionData) {
const dispatch = useDispatch(); const dispatch = useDispatch();
// state selectors // state selectors
const tokens = useSelector(getTokens);
const collectibles = useSelector(getCollectibles); const collectibles = useSelector(getCollectibles);
const tokenList = useSelector(getTokenList);
// in-hook state // in-hook state
const [currentAsset, setCurrentAsset] = useState(null); const [currentAsset, setCurrentAsset] = useState(null);
@ -36,8 +33,6 @@ export function useAssetDetails(tokenAddress, userAddress, transactionData) {
userAddress, userAddress,
transactionData, transactionData,
collectibles, collectibles,
tokens,
tokenList,
); );
setCurrentAsset(assetDetails); setCurrentAsset(assetDetails);
dispatch(hideLoadingIndication()); dispatch(hideLoadingIndication());
@ -58,8 +53,6 @@ export function useAssetDetails(tokenAddress, userAddress, transactionData) {
userAddress, userAddress,
transactionData, transactionData,
collectibles, collectibles,
tokens,
tokenList,
]); ]);
let assetStandard, let assetStandard,
@ -83,11 +76,13 @@ export function useAssetDetails(tokenAddress, userAddress, transactionData) {
balance, balance,
decimals: currentAssetDecimals, decimals: currentAssetDecimals,
} = currentAsset; } = currentAsset;
const tokenData = parseStandardTokenTransactionData(transactionData); const tokenData = parseStandardTokenTransactionData(transactionData);
assetStandard = standard; assetStandard = standard;
assetAddress = tokenAddress; assetAddress = tokenAddress;
tokenSymbol = symbol; tokenSymbol = symbol ?? '';
tokenImage = image; tokenImage = image;
toAddress = getTokenAddressParam(tokenData); toAddress = getTokenAddressParam(tokenData);
if (assetStandard === ERC721 || assetStandard === ERC1155) { if (assetStandard === ERC721 || assetStandard === ERC1155) {
assetName = name; assetName = name;
@ -101,6 +96,7 @@ export function useAssetDetails(tokenAddress, userAddress, transactionData) {
calcTokenAmount(getTokenValueParam(tokenData), decimals).toString(10); calcTokenAmount(getTokenValueParam(tokenData), decimals).toString(10);
} }
} }
return { return {
assetStandard, assetStandard,
assetName, assetName,

View File

@ -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 }) => (
<Provider store={configureStore(mockState)}>{children}</Provider>
);
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,
});
});
});

View File

@ -62,6 +62,7 @@ export default class ConfirmApproveContent extends Component {
txData: PropTypes.object, txData: PropTypes.object,
fromAddressIsLedger: PropTypes.bool, fromAddressIsLedger: PropTypes.bool,
chainId: PropTypes.string, chainId: PropTypes.string,
tokenAddress: PropTypes.string,
rpcPrefs: PropTypes.object, rpcPrefs: PropTypes.object,
isContract: PropTypes.bool, isContract: PropTypes.bool,
hexTransactionTotal: PropTypes.string, hexTransactionTotal: PropTypes.string,
@ -183,7 +184,9 @@ export default class ConfirmApproveContent extends Component {
renderERC721OrERC1155PermissionContent() { renderERC721OrERC1155PermissionContent() {
const { t } = this.context; 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 const displayedAddress = isContract
? `${t('contract')} (${addressSummary(toAddress)})` ? `${t('contract')} (${addressSummary(toAddress)})`
@ -198,7 +201,7 @@ export default class ConfirmApproveContent extends Component {
{t('approvedAsset')}: {t('approvedAsset')}:
</div> </div>
<div className="confirm-approve-content__medium-text"> <div className="confirm-approve-content__medium-text">
{`${assetName} #${tokenId}`} {titleTokenDescription}
</div> </div>
</div> </div>
<div className="flex-row"> <div className="flex-row">
@ -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 = (
<a
href={unknownTokenBlockExplorerLink}
target="_blank"
rel="noopener noreferrer"
className="confirm-approve-content__unknown-asset"
>
{t('token')}
</a>
);
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 = (
<>
<a
href={unknownNFTBlockExplorerLink}
target="_blank"
rel="noopener noreferrer"
className="confirm-approve-content__unknown-asset"
>
{t('nft')}
</a>
{tokenIdWrapped && <span>{tokenIdWrapped}</span>}
</>
);
titleTokenDescription = unknownNFTLink;
}
}
return titleTokenDescription;
}
render() { render() {
const { t } = this.context; const { t } = this.context;
const { const {
@ -452,11 +531,11 @@ export default class ConfirmApproveContent extends Component {
rpcPrefs, rpcPrefs,
isContract, isContract,
assetStandard, assetStandard,
tokenId,
assetName,
} = this.props; } = this.props;
const { showFullTxDetails } = this.state; const { showFullTxDetails } = this.state;
const titleTokenDescription = this.getTitleTokenDescription();
return ( return (
<div <div
className={classnames('confirm-approve-content', { className={classnames('confirm-approve-content', {
@ -496,11 +575,7 @@ export default class ConfirmApproveContent extends Component {
</Box> </Box>
</Box> </Box>
<div className="confirm-approve-content__title"> <div className="confirm-approve-content__title">
{t('allowSpendToken', [ {t('allowSpendToken', [titleTokenDescription])}
assetStandard === ERC20
? tokenSymbol
: `${assetName} (#${tokenId})`,
])}
</div> </div>
<div className="confirm-approve-content__description"> <div className="confirm-approve-content__description">
{t('trustSiteApprovePermission', [ {t('trustSiteApprovePermission', [
@ -554,7 +629,9 @@ export default class ConfirmApproveContent extends Component {
: getAccountLink( : getAccountLink(
toAddress, toAddress,
chainId, chainId,
{ blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null }, {
blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null,
},
null, null,
); );
global.platform.openTab({ global.platform.openTab({

View File

@ -9,6 +9,10 @@
padding: 0 24px 16px 24px; padding: 0 24px 16px 24px;
} }
&__unknown-asset {
color: var(--color-primary-default);
}
&__icon-display-content { &__icon-display-content {
display: flex; display: flex;
height: 51px; height: 51px;

View File

@ -52,6 +52,7 @@ export default function ConfirmApprove({
tokenId, tokenId,
userAddress, userAddress,
toAddress, toAddress,
tokenAddress,
transaction, transaction,
ethTransactionTotal, ethTransactionTotal,
fiatTransactionTotal, fiatTransactionTotal,
@ -173,6 +174,7 @@ export default function ConfirmApprove({
tokenId={tokenId} tokenId={tokenId}
assetName={assetName} assetName={assetName}
assetStandard={assetStandard} assetStandard={assetStandard}
tokenAddress={tokenAddress}
showCustomizeGasModal={approveTransaction} showCustomizeGasModal={approveTransaction}
showEditApprovalPermissionModal={({ showEditApprovalPermissionModal={({
/* eslint-disable no-shadow */ /* eslint-disable no-shadow */
@ -268,6 +270,7 @@ export default function ConfirmApprove({
ConfirmApprove.propTypes = { ConfirmApprove.propTypes = {
assetStandard: PropTypes.string, assetStandard: PropTypes.string,
assetName: PropTypes.string, assetName: PropTypes.string,
tokenAddress: PropTypes.string,
userBalance: PropTypes.string, userBalance: PropTypes.string,
tokenSymbol: PropTypes.string, tokenSymbol: PropTypes.string,
decimals: PropTypes.string, decimals: PropTypes.string,