diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index c062bd8d5..5402d0d02 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -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?" }, "decimal": { - "message": "Decimals of Precision" + "message": "Token Decimal" }, "decimalsMustZerotoTen": { "message": "Decimals must be at least 0, and not over 36." @@ -2221,6 +2221,9 @@ "tokenContractAddress": { "message": "Token Contract Address" }, + "tokenDecimalFetchFailed": { + "message": "Token decimal required." + }, "tokenSymbol": { "message": "Token Symbol" }, @@ -2352,6 +2355,10 @@ "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": { "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\"" diff --git a/package.json b/package.json index 5509077ca..1ddc3009c 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "@metamask/controllers": "^8.0.0", "@metamask/eth-ledger-bridge-keyring": "^0.5.0", "@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/logo": "^2.5.0", "@metamask/obs-store": "^5.0.0", diff --git a/ui/helpers/utils/token-util.js b/ui/helpers/utils/token-util.js index 8811bec5d..4b6d5dd08 100644 --- a/ui/helpers/utils/token-util.js +++ b/ui/helpers/utils/token-util.js @@ -13,7 +13,6 @@ const casedContractMap = Object.keys(contractMap).reduce((acc, base) => { }, {}); const DEFAULT_SYMBOL = ''; -const DEFAULT_DECIMALS = '0'; async function getSymbolFromContract(tokenAddress) { const token = util.getContractAtAddress(tokenAddress); @@ -78,25 +77,6 @@ async function getDecimals(tokenAddress) { 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 = []) { const existingToken = existingTokens.find( ({ address }) => tokenAddress === address, @@ -123,7 +103,7 @@ export async function getSymbolAndDecimals(tokenAddress, existingTokens = []) { return { symbol: symbol || DEFAULT_SYMBOL, - decimals: decimals || DEFAULT_DECIMALS, + decimals, }; } diff --git a/ui/pages/add-token/add-token.component.js b/ui/pages/add-token/add-token.component.js index f5c7d2077..d5d02312e 100644 --- a/ui/pages/add-token/add-token.component.js +++ b/ui/pages/add-token/add-token.component.js @@ -1,5 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import { getTokenTrackerLink } from '@metamask/etherscan-link'; import { checkExistingAddresses } from '../../helpers/utils/util'; import { tokenInfoGetter } from '../../helpers/utils/token-util'; 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 { isValidHexAddress } from '../../../shared/modules/hexstring-utils'; 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 TokenSearch from './token-search'; @@ -30,6 +35,8 @@ class AddToken extends Component { identities: PropTypes.object, showSearchTab: PropTypes.bool.isRequired, mostRecentOverviewPage: PropTypes.string.isRequired, + chainId: PropTypes.string, + rpcPrefs: PropTypes.object, }; state = { @@ -42,8 +49,9 @@ class AddToken extends Component { customAddressError: null, customSymbolError: null, customDecimalsError: null, - autoFilled: false, forceEditSymbol: false, + symbolAutoFilled: false, + decimalAutoFilled: false, }; componentDidMount() { @@ -148,10 +156,11 @@ class AddToken extends Component { } async attemptToAutoFillTokenParams(address) { - const { symbol = '', decimals = 0 } = await this.tokenInfoGetter(address); + const { symbol = '', decimals } = await this.tokenInfoGetter(address); - const autoFilled = Boolean(symbol && decimals); - this.setState({ autoFilled }); + const symbolAutoFilled = Boolean(symbol); + const decimalAutoFilled = Boolean(decimals); + this.setState({ symbolAutoFilled, decimalAutoFilled }); this.handleCustomSymbolChange(symbol || ''); this.handleCustomDecimalsChange(decimals); } @@ -162,7 +171,8 @@ class AddToken extends Component { customAddress, customAddressError: null, tokenSelectorError: null, - autoFilled: false, + symbolAutoFilled: false, + decimalAutoFilled: false, }); const addressIsValid = isValidHexAddress(customAddress, { @@ -213,16 +223,18 @@ class AddToken extends Component { } handleCustomDecimalsChange(value) { - const customDecimals = value.trim(); - const validDecimals = - customDecimals !== null && - customDecimals !== '' && - customDecimals >= MIN_DECIMAL_VALUE && - customDecimals <= MAX_DECIMAL_VALUE; + let customDecimals; let customDecimalsError = null; - if (!validDecimals) { - customDecimalsError = this.context.t('decimalsMustZerotoTen'); + if (value) { + 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 }); @@ -236,10 +248,23 @@ class AddToken extends Component { customAddressError, customSymbolError, customDecimalsError, - autoFilled, forceEditSymbol, + symbolAutoFilled, + decimalAutoFilled, } = 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 (
{this.context.t('tokenSymbol')} - {autoFilled && !forceEditSymbol && ( + {symbolAutoFilled && !forceEditSymbol && (
this.setState({ forceEditSymbol: true })} @@ -276,7 +301,7 @@ class AddToken extends Component { error={customSymbolError} fullWidth margin="normal" - disabled={autoFilled && !forceEditSymbol} + disabled={symbolAutoFilled && !forceEditSymbol} /> this.handleCustomDecimalsChange(e.target.value)} - error={customDecimalsError} + error={customDecimals ? customDecimalsError : null} fullWidth margin="normal" - disabled={autoFilled} + disabled={decimalAutoFilled} min={MIN_DECIMAL_VALUE} max={MAX_DECIMAL_VALUE} /> + {customDecimals === '' && ( + + + {this.context.t('tokenDecimalFetchFailed')} + + + {this.context.t('verifyThisTokenDecimalOn', [ + , + ])} + + + } + type="warning" + withRightButton + className="add-token__decimal-warning" + /> + )}
); } diff --git a/ui/pages/add-token/add-token.container.js b/ui/pages/add-token/add-token.container.js index ab8bf24c8..08f6505e3 100644 --- a/ui/pages/add-token/add-token.container.js +++ b/ui/pages/add-token/add-token.container.js @@ -2,12 +2,20 @@ import { connect } from 'react-redux'; import { setPendingTokens, clearPendingTokens } from '../../store/actions'; import { getMostRecentOverviewPage } from '../../ducks/history/history'; -import { getIsMainnet } from '../../selectors/selectors'; +import { + getIsMainnet, + getRpcPrefsForCurrentProvider, +} from '../../selectors/selectors'; import AddToken from './add-token.component'; const mapStateToProps = (state) => { const { - metamask: { identities, tokens, pendingTokens }, + metamask: { + identities, + tokens, + pendingTokens, + provider: { chainId }, + }, } = state; return { identities, @@ -15,6 +23,8 @@ const mapStateToProps = (state) => { tokens, pendingTokens, showSearchTab: getIsMainnet(state) || process.env.IN_TEST === 'true', + chainId, + rpcPrefs: getRpcPrefsForCurrentProvider(state), }; }; diff --git a/ui/pages/add-token/add-token.test.js b/ui/pages/add-token/add-token.test.js index b94e06b60..4b36e7f39 100644 --- a/ui/pages/add-token/add-token.test.js +++ b/ui/pages/add-token/add-token.test.js @@ -82,7 +82,7 @@ describe('Add Token', () => { expect( wrapper.find('AddToken').instance().state.customDecimals, - ).toStrictEqual(tokenPrecision); + ).toStrictEqual(Number(tokenPrecision)); }); it('next', () => { diff --git a/ui/pages/add-token/index.scss b/ui/pages/add-token/index.scss index 5060a6348..581832ca4 100644 --- a/ui/pages/add-token/index.scss +++ b/ui/pages/add-token/index.scss @@ -1,6 +1,8 @@ @import 'token-list/index'; .add-token { + $self: &; + &__custom-token-form { padding: 8px 16px 16px; @@ -13,6 +15,9 @@ -webkit-appearance: none; display: none; } + & #{$self}__decimal-warning { + margin-top: 5px; + } } &__search-token { @@ -41,4 +46,12 @@ cursor: pointer; } } + + &__link { + @include H7; + + display: inline; + color: $primary-blue; + padding-left: 0; + } } diff --git a/ui/store/actions.js b/ui/store/actions.js index c5c55c383..9f89baa9c 100644 --- a/ui/store/actions.js +++ b/ui/store/actions.js @@ -9,7 +9,7 @@ import { loadRelativeTimeFormatLocaleData, } from '../helpers/utils/i18n-helper'; 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 { ENVIRONMENT_TYPE_NOTIFICATION } from '../../shared/constants/app'; 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 { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; import { LISTED_CONTRACT_ADDRESSES } from '../../shared/constants/tokens'; -import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; import * as actionConstants from './actionConstants'; let background = null; @@ -2252,7 +2251,7 @@ export function setPendingTokens(pendingTokens) { const { customToken = {}, selectedTokens = {} } = pendingTokens; const { address, symbol, decimals } = customToken; const tokens = - address && symbol && decimals + address && symbol && decimals >= 0 <= 36 ? { ...selectedTokens, [address]: { @@ -2654,12 +2653,10 @@ export function getTokenParams(tokenAddress) { dispatch(loadingTokenParamsStarted()); log.debug(`loadingTokenParams`); - return fetchSymbolAndDecimals(tokenAddress, existingTokens).then( - ({ symbol, decimals }) => { - dispatch(addToken(tokenAddress, symbol, Number(decimals))); - dispatch(loadingTokenParamsFinished()); - }, - ); + return getSymbolAndDecimals(tokenAddress).then(({ symbol, decimals }) => { + dispatch(addToken(tokenAddress, symbol, Number(decimals))); + dispatch(loadingTokenParamsFinished()); + }); }; } diff --git a/yarn.lock b/yarn.lock index add2e81a7..8d51b3c5f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2739,10 +2739,10 @@ human-standard-token-abi "^1.0.2" safe-event-emitter "^1.0.1" -"@metamask/etherscan-link@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@metamask/etherscan-link/-/etherscan-link-2.0.0.tgz#89035736515a39532ba1142d87b9a8c2b4f920f1" - integrity sha512-/YS32hS2UTTxs0KyUmAgaDj1w4dzAvOrT+p4TJtpICeH3E/k51r2FO0Or7WJJI/mpzTqNKgcH5yyS2oCtupGiA== +"@metamask/etherscan-link@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@metamask/etherscan-link/-/etherscan-link-2.1.0.tgz#c0be8e68445b7b83cf85bcc03a56cdf8e256c973" + integrity sha512-ADuWlTUkFfN2vXlz81Bg/0BA+XRor+CdK1055p6k7H6BLIPoDKn9SBOFld9haQFuR9cKh/JYHcnlSIv5R4fUEw== "@metamask/forwarder@^1.1.0": version "1.1.0"