1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-26 04:20:53 +01:00

Handling custom token decimal fetch failure due to network error (#10956)

This commit is contained in:
Niranjana Binoy 2021-05-18 13:23:54 -04:00 committed by Dan Miller
parent 6a73b7c998
commit d4cb403d51
9 changed files with 123 additions and 57 deletions

View File

@ -486,7 +486,7 @@
"message": "Some of your account data was backed up during a previous installation of MetaMask. This could include your settings, contacts, and tokens. Would you like to restore this data now?" "message": "Some of your account data was backed up during a previous installation of MetaMask. This could include your settings, contacts, and tokens. Would you like to restore this data now?"
}, },
"decimal": { "decimal": {
"message": "Decimals of Precision" "message": "Token Decimal"
}, },
"decimalsMustZerotoTen": { "decimalsMustZerotoTen": {
"message": "Decimals must be at least 0, and not over 36." "message": "Decimals must be at least 0, and not over 36."
@ -2221,6 +2221,9 @@
"tokenContractAddress": { "tokenContractAddress": {
"message": "Token Contract Address" "message": "Token Contract Address"
}, },
"tokenDecimalFetchFailed": {
"message": "Token decimal required."
},
"tokenSymbol": { "tokenSymbol": {
"message": "Token Symbol" "message": "Token Symbol"
}, },
@ -2352,6 +2355,10 @@
"userName": { "userName": {
"message": "Username" "message": "Username"
}, },
"verifyThisTokenDecimalOn": {
"message": "Token decimal can be found on $1",
"description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\""
},
"verifyThisTokenOn": { "verifyThisTokenOn": {
"message": "Verify this token on $1", "message": "Verify this token on $1",
"description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\""

View File

@ -100,7 +100,7 @@
"@metamask/controllers": "^8.0.0", "@metamask/controllers": "^8.0.0",
"@metamask/eth-ledger-bridge-keyring": "^0.5.0", "@metamask/eth-ledger-bridge-keyring": "^0.5.0",
"@metamask/eth-token-tracker": "^3.0.1", "@metamask/eth-token-tracker": "^3.0.1",
"@metamask/etherscan-link": "^2.0.0", "@metamask/etherscan-link": "^2.1.0",
"@metamask/jazzicon": "^2.0.0", "@metamask/jazzicon": "^2.0.0",
"@metamask/logo": "^2.5.0", "@metamask/logo": "^2.5.0",
"@metamask/obs-store": "^5.0.0", "@metamask/obs-store": "^5.0.0",

View File

@ -13,7 +13,6 @@ const casedContractMap = Object.keys(contractMap).reduce((acc, base) => {
}, {}); }, {});
const DEFAULT_SYMBOL = ''; const DEFAULT_SYMBOL = '';
const DEFAULT_DECIMALS = '0';
async function getSymbolFromContract(tokenAddress) { async function getSymbolFromContract(tokenAddress) {
const token = util.getContractAtAddress(tokenAddress); const token = util.getContractAtAddress(tokenAddress);
@ -78,25 +77,6 @@ async function getDecimals(tokenAddress) {
return decimals; return decimals;
} }
export async function fetchSymbolAndDecimals(tokenAddress) {
let symbol, decimals;
try {
symbol = await getSymbol(tokenAddress);
decimals = await getDecimals(tokenAddress);
} catch (error) {
log.warn(
`symbol() and decimal() calls for token at address ${tokenAddress} resulted in error:`,
error,
);
}
return {
symbol: symbol || DEFAULT_SYMBOL,
decimals: decimals || DEFAULT_DECIMALS,
};
}
export async function getSymbolAndDecimals(tokenAddress, existingTokens = []) { export async function getSymbolAndDecimals(tokenAddress, existingTokens = []) {
const existingToken = existingTokens.find( const existingToken = existingTokens.find(
({ address }) => tokenAddress === address, ({ address }) => tokenAddress === address,
@ -123,7 +103,7 @@ export async function getSymbolAndDecimals(tokenAddress, existingTokens = []) {
return { return {
symbol: symbol || DEFAULT_SYMBOL, symbol: symbol || DEFAULT_SYMBOL,
decimals: decimals || DEFAULT_DECIMALS, decimals,
}; };
} }

View File

@ -1,5 +1,6 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { getTokenTrackerLink } from '@metamask/etherscan-link';
import { checkExistingAddresses } from '../../helpers/utils/util'; import { checkExistingAddresses } from '../../helpers/utils/util';
import { tokenInfoGetter } from '../../helpers/utils/token-util'; import { tokenInfoGetter } from '../../helpers/utils/token-util';
import { CONFIRM_ADD_TOKEN_ROUTE } from '../../helpers/constants/routes'; import { CONFIRM_ADD_TOKEN_ROUTE } from '../../helpers/constants/routes';
@ -8,6 +9,10 @@ import PageContainer from '../../components/ui/page-container';
import { Tabs, Tab } from '../../components/ui/tabs'; import { Tabs, Tab } from '../../components/ui/tabs';
import { isValidHexAddress } from '../../../shared/modules/hexstring-utils'; import { isValidHexAddress } from '../../../shared/modules/hexstring-utils';
import { addHexPrefix } from '../../../app/scripts/lib/util'; import { addHexPrefix } from '../../../app/scripts/lib/util';
import ActionableMessage from '../swaps/actionable-message';
import Typography from '../../components/ui/typography';
import { TYPOGRAPHY, FONT_WEIGHT } from '../../helpers/constants/design-system';
import Button from '../../components/ui/button';
import TokenList from './token-list'; import TokenList from './token-list';
import TokenSearch from './token-search'; import TokenSearch from './token-search';
@ -30,6 +35,8 @@ class AddToken extends Component {
identities: PropTypes.object, identities: PropTypes.object,
showSearchTab: PropTypes.bool.isRequired, showSearchTab: PropTypes.bool.isRequired,
mostRecentOverviewPage: PropTypes.string.isRequired, mostRecentOverviewPage: PropTypes.string.isRequired,
chainId: PropTypes.string,
rpcPrefs: PropTypes.object,
}; };
state = { state = {
@ -42,8 +49,9 @@ class AddToken extends Component {
customAddressError: null, customAddressError: null,
customSymbolError: null, customSymbolError: null,
customDecimalsError: null, customDecimalsError: null,
autoFilled: false,
forceEditSymbol: false, forceEditSymbol: false,
symbolAutoFilled: false,
decimalAutoFilled: false,
}; };
componentDidMount() { componentDidMount() {
@ -148,10 +156,11 @@ class AddToken extends Component {
} }
async attemptToAutoFillTokenParams(address) { async attemptToAutoFillTokenParams(address) {
const { symbol = '', decimals = 0 } = await this.tokenInfoGetter(address); const { symbol = '', decimals } = await this.tokenInfoGetter(address);
const autoFilled = Boolean(symbol && decimals); const symbolAutoFilled = Boolean(symbol);
this.setState({ autoFilled }); const decimalAutoFilled = Boolean(decimals);
this.setState({ symbolAutoFilled, decimalAutoFilled });
this.handleCustomSymbolChange(symbol || ''); this.handleCustomSymbolChange(symbol || '');
this.handleCustomDecimalsChange(decimals); this.handleCustomDecimalsChange(decimals);
} }
@ -162,7 +171,8 @@ class AddToken extends Component {
customAddress, customAddress,
customAddressError: null, customAddressError: null,
tokenSelectorError: null, tokenSelectorError: null,
autoFilled: false, symbolAutoFilled: false,
decimalAutoFilled: false,
}); });
const addressIsValid = isValidHexAddress(customAddress, { const addressIsValid = isValidHexAddress(customAddress, {
@ -213,16 +223,18 @@ class AddToken extends Component {
} }
handleCustomDecimalsChange(value) { handleCustomDecimalsChange(value) {
const customDecimals = value.trim(); let customDecimals;
const validDecimals =
customDecimals !== null &&
customDecimals !== '' &&
customDecimals >= MIN_DECIMAL_VALUE &&
customDecimals <= MAX_DECIMAL_VALUE;
let customDecimalsError = null; let customDecimalsError = null;
if (!validDecimals) { if (value) {
customDecimalsError = this.context.t('decimalsMustZerotoTen'); customDecimals = Number(value.trim());
customDecimalsError =
value < MIN_DECIMAL_VALUE || value > MAX_DECIMAL_VALUE
? this.context.t('decimalsMustZerotoTen')
: null;
} else {
customDecimals = '';
customDecimalsError = this.context.t('tokenDecimalFetchFailed');
} }
this.setState({ customDecimals, customDecimalsError }); this.setState({ customDecimals, customDecimalsError });
@ -236,10 +248,23 @@ class AddToken extends Component {
customAddressError, customAddressError,
customSymbolError, customSymbolError,
customDecimalsError, customDecimalsError,
autoFilled,
forceEditSymbol, forceEditSymbol,
symbolAutoFilled,
decimalAutoFilled,
} = this.state; } = this.state;
const { chainId, rpcPrefs } = this.props;
const blockExplorerTokenLink = getTokenTrackerLink(
customAddress,
chainId,
null,
null,
{ blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null },
);
const blockExplorerLabel = rpcPrefs?.blockExplorerUrl
? new URL(blockExplorerTokenLink).hostname
: this.context.t('etherscan');
return ( return (
<div className="add-token__custom-token-form"> <div className="add-token__custom-token-form">
<TextField <TextField
@ -260,7 +285,7 @@ class AddToken extends Component {
<span className="add-token__custom-symbol__label"> <span className="add-token__custom-symbol__label">
{this.context.t('tokenSymbol')} {this.context.t('tokenSymbol')}
</span> </span>
{autoFilled && !forceEditSymbol && ( {symbolAutoFilled && !forceEditSymbol && (
<div <div
className="add-token__custom-symbol__edit" className="add-token__custom-symbol__edit"
onClick={() => this.setState({ forceEditSymbol: true })} onClick={() => this.setState({ forceEditSymbol: true })}
@ -276,7 +301,7 @@ class AddToken extends Component {
error={customSymbolError} error={customSymbolError}
fullWidth fullWidth
margin="normal" margin="normal"
disabled={autoFilled && !forceEditSymbol} disabled={symbolAutoFilled && !forceEditSymbol}
/> />
<TextField <TextField
id="custom-decimals" id="custom-decimals"
@ -284,13 +309,47 @@ class AddToken extends Component {
type="number" type="number"
value={customDecimals} value={customDecimals}
onChange={(e) => this.handleCustomDecimalsChange(e.target.value)} onChange={(e) => this.handleCustomDecimalsChange(e.target.value)}
error={customDecimalsError} error={customDecimals ? customDecimalsError : null}
fullWidth fullWidth
margin="normal" margin="normal"
disabled={autoFilled} disabled={decimalAutoFilled}
min={MIN_DECIMAL_VALUE} min={MIN_DECIMAL_VALUE}
max={MAX_DECIMAL_VALUE} max={MAX_DECIMAL_VALUE}
/> />
{customDecimals === '' && (
<ActionableMessage
message={
<>
<Typography
variant={TYPOGRAPHY.H7}
fontWeight={FONT_WEIGHT.BOLD}
>
{this.context.t('tokenDecimalFetchFailed')}
</Typography>
<Typography
variant={TYPOGRAPHY.H7}
fontWeight={FONT_WEIGHT.NORMAL}
>
{this.context.t('verifyThisTokenDecimalOn', [
<Button
type="link"
key="add-token-verify-token-decimal"
className="add-token__link"
rel="noopener noreferrer"
target="_blank"
href={blockExplorerTokenLink}
>
{blockExplorerLabel}
</Button>,
])}
</Typography>
</>
}
type="warning"
withRightButton
className="add-token__decimal-warning"
/>
)}
</div> </div>
); );
} }

View File

@ -2,12 +2,20 @@ import { connect } from 'react-redux';
import { setPendingTokens, clearPendingTokens } from '../../store/actions'; import { setPendingTokens, clearPendingTokens } from '../../store/actions';
import { getMostRecentOverviewPage } from '../../ducks/history/history'; import { getMostRecentOverviewPage } from '../../ducks/history/history';
import { getIsMainnet } from '../../selectors/selectors'; import {
getIsMainnet,
getRpcPrefsForCurrentProvider,
} from '../../selectors/selectors';
import AddToken from './add-token.component'; import AddToken from './add-token.component';
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
const { const {
metamask: { identities, tokens, pendingTokens }, metamask: {
identities,
tokens,
pendingTokens,
provider: { chainId },
},
} = state; } = state;
return { return {
identities, identities,
@ -15,6 +23,8 @@ const mapStateToProps = (state) => {
tokens, tokens,
pendingTokens, pendingTokens,
showSearchTab: getIsMainnet(state) || process.env.IN_TEST === 'true', showSearchTab: getIsMainnet(state) || process.env.IN_TEST === 'true',
chainId,
rpcPrefs: getRpcPrefsForCurrentProvider(state),
}; };
}; };

View File

@ -82,7 +82,7 @@ describe('Add Token', () => {
expect( expect(
wrapper.find('AddToken').instance().state.customDecimals, wrapper.find('AddToken').instance().state.customDecimals,
).toStrictEqual(tokenPrecision); ).toStrictEqual(Number(tokenPrecision));
}); });
it('next', () => { it('next', () => {

View File

@ -1,6 +1,8 @@
@import 'token-list/index'; @import 'token-list/index';
.add-token { .add-token {
$self: &;
&__custom-token-form { &__custom-token-form {
padding: 8px 16px 16px; padding: 8px 16px 16px;
@ -13,6 +15,9 @@
-webkit-appearance: none; -webkit-appearance: none;
display: none; display: none;
} }
& #{$self}__decimal-warning {
margin-top: 5px;
}
} }
&__search-token { &__search-token {
@ -41,4 +46,12 @@
cursor: pointer; cursor: pointer;
} }
} }
&__link {
@include H7;
display: inline;
color: $primary-blue;
padding-left: 0;
}
} }

View File

@ -9,7 +9,7 @@ import {
loadRelativeTimeFormatLocaleData, loadRelativeTimeFormatLocaleData,
} from '../helpers/utils/i18n-helper'; } from '../helpers/utils/i18n-helper';
import { getMethodDataAsync } from '../helpers/utils/transactions.util'; import { getMethodDataAsync } from '../helpers/utils/transactions.util';
import { fetchSymbolAndDecimals } from '../helpers/utils/token-util'; import { getSymbolAndDecimals } from '../helpers/utils/token-util';
import switchDirection from '../helpers/utils/switch-direction'; import switchDirection from '../helpers/utils/switch-direction';
import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../shared/constants/app'; import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../shared/constants/app';
import { hasUnconfirmedTransactions } from '../helpers/utils/confirm-tx.util'; import { hasUnconfirmedTransactions } from '../helpers/utils/confirm-tx.util';
@ -24,7 +24,6 @@ import { switchedToUnconnectedAccount } from '../ducks/alerts/unconnected-accoun
import { getUnconnectedAccountAlertEnabledness } from '../ducks/metamask/metamask'; import { getUnconnectedAccountAlertEnabledness } from '../ducks/metamask/metamask';
import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils';
import { LISTED_CONTRACT_ADDRESSES } from '../../shared/constants/tokens'; import { LISTED_CONTRACT_ADDRESSES } from '../../shared/constants/tokens';
import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils';
import * as actionConstants from './actionConstants'; import * as actionConstants from './actionConstants';
let background = null; let background = null;
@ -2252,7 +2251,7 @@ export function setPendingTokens(pendingTokens) {
const { customToken = {}, selectedTokens = {} } = pendingTokens; const { customToken = {}, selectedTokens = {} } = pendingTokens;
const { address, symbol, decimals } = customToken; const { address, symbol, decimals } = customToken;
const tokens = const tokens =
address && symbol && decimals address && symbol && decimals >= 0 <= 36
? { ? {
...selectedTokens, ...selectedTokens,
[address]: { [address]: {
@ -2654,12 +2653,10 @@ export function getTokenParams(tokenAddress) {
dispatch(loadingTokenParamsStarted()); dispatch(loadingTokenParamsStarted());
log.debug(`loadingTokenParams`); log.debug(`loadingTokenParams`);
return fetchSymbolAndDecimals(tokenAddress, existingTokens).then( return getSymbolAndDecimals(tokenAddress).then(({ symbol, decimals }) => {
({ symbol, decimals }) => { dispatch(addToken(tokenAddress, symbol, Number(decimals)));
dispatch(addToken(tokenAddress, symbol, Number(decimals))); dispatch(loadingTokenParamsFinished());
dispatch(loadingTokenParamsFinished()); });
},
);
}; };
} }

View File

@ -2739,10 +2739,10 @@
human-standard-token-abi "^1.0.2" human-standard-token-abi "^1.0.2"
safe-event-emitter "^1.0.1" safe-event-emitter "^1.0.1"
"@metamask/etherscan-link@^2.0.0": "@metamask/etherscan-link@^2.1.0":
version "2.0.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/@metamask/etherscan-link/-/etherscan-link-2.0.0.tgz#89035736515a39532ba1142d87b9a8c2b4f920f1" resolved "https://registry.yarnpkg.com/@metamask/etherscan-link/-/etherscan-link-2.1.0.tgz#c0be8e68445b7b83cf85bcc03a56cdf8e256c973"
integrity sha512-/YS32hS2UTTxs0KyUmAgaDj1w4dzAvOrT+p4TJtpICeH3E/k51r2FO0Or7WJJI/mpzTqNKgcH5yyS2oCtupGiA== integrity sha512-ADuWlTUkFfN2vXlz81Bg/0BA+XRor+CdK1055p6k7H6BLIPoDKn9SBOFld9haQFuR9cKh/JYHcnlSIv5R4fUEw==
"@metamask/forwarder@^1.1.0": "@metamask/forwarder@^1.1.0":
version "1.1.0" version "1.1.0"