diff --git a/test/e2e/tests/confirmations/signatures/permit.spec.ts b/test/e2e/tests/confirmations/signatures/permit.spec.ts index 5c52d1f029ee..8da5e411a2f4 100644 --- a/test/e2e/tests/confirmations/signatures/permit.spec.ts +++ b/test/e2e/tests/confirmations/signatures/permit.spec.ts @@ -126,7 +126,7 @@ async function assertInfoValues(driver: Driver) { css: '.name__value', text: '0x5B38D...eddC4', }); - const value = driver.findElement({ text: '<0.000001' }); + const value = driver.findElement({ text: '3,000' }); const nonce = driver.findElement({ text: '0' }); const deadline = driver.findElement({ text: '09 June 3554, 16:53' }); diff --git a/test/integration/confirmations/signatures/permit.test.tsx b/test/integration/confirmations/signatures/permit.test.tsx index e11f206d1996..8e9c979562f2 100644 --- a/test/integration/confirmations/signatures/permit.test.tsx +++ b/test/integration/confirmations/signatures/permit.test.tsx @@ -73,7 +73,7 @@ describe('Permit Confirmation', () => { jest.resetAllMocks(); mockedBackgroundConnection.submitRequestToBackground.mockImplementation( createMockImplementation({ - getTokenStandardAndDetails: { decimals: '2' }, + getTokenStandardAndDetails: { decimals: '2', standard: 'ERC20' }, }), ); }); diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.test.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.test.tsx index e89efb3c0dc1..0d67715867d9 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.test.tsx @@ -8,15 +8,25 @@ import { permitNFTSignatureMsg, permitSignatureMsg, } from '../../../../../../../../test/data/confirmations/typed_sign'; +import { memoizedGetTokenStandardAndDetails } from '../../../../../utils/token'; import PermitSimulation from './permit-simulation'; jest.mock('../../../../../../../store/actions', () => { return { - getTokenStandardAndDetails: jest.fn().mockResolvedValue({ decimals: 2 }), + getTokenStandardAndDetails: jest + .fn() + .mockResolvedValue({ decimals: 2, standard: 'ERC20' }), }; }); describe('PermitSimulation', () => { + afterEach(() => { + jest.clearAllMocks(); + + /** Reset memoized function using getTokenStandardAndDetails for each test */ + memoizedGetTokenStandardAndDetails?.cache?.clear?.(); + }); + it('renders component correctly', async () => { const state = getMockTypedSignConfirmStateForRequest(permitSignatureMsg); const mockStore = configureMockStore([])(state); diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/__snapshots__/value-display.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/__snapshots__/value-display.test.tsx.snap index 9c4134aa1b2d..26def806c6fa 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/__snapshots__/value-display.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/__snapshots__/value-display.test.tsx.snap @@ -56,49 +56,3 @@ exports[`PermitSimulationValueDisplay renders component correctly 1`] = ` `; - -exports[`PermitSimulationValueDisplay renders component correctly for NFT token 1`] = ` -
-
-
-
-
-

- #4321 -

-
-
-
-
- -

- 0xA0b86...6eB48 -

-
-
-
-
-
-
-`; diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.test.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.test.tsx index da86d497aac1..e8e48c1ca6f9 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.test.tsx @@ -4,14 +4,24 @@ import configureMockStore from 'redux-mock-store'; import mockState from '../../../../../../../../../test/data/mock-state.json'; import { renderWithProvider } from '../../../../../../../../../test/lib/render-helpers'; +import useTrackERC20WithoutDecimalInformation from '../../../../../../hooks/useTrackERC20WithoutDecimalInformation'; import PermitSimulationValueDisplay from './value-display'; jest.mock('../../../../../../../../store/actions', () => { return { - getTokenStandardAndDetails: jest.fn().mockResolvedValue({ decimals: 4 }), + getTokenStandardAndDetails: jest + .fn() + .mockResolvedValue({ decimals: 4, standard: 'ERC20' }), }; }); +jest.mock( + '../../../../../../hooks/useTrackERC20WithoutDecimalInformation', + () => { + return jest.fn(); + }, +); + describe('PermitSimulationValueDisplay', () => { it('renders component correctly', async () => { const mockStore = configureMockStore([])(mockState); @@ -30,20 +40,19 @@ describe('PermitSimulationValueDisplay', () => { }); }); - it('renders component correctly for NFT token', async () => { + it('should invoke method to track missing decimal information for ERC20 tokens', async () => { const mockStore = configureMockStore([])(mockState); await act(async () => { - const { container, findByText } = renderWithProvider( + renderWithProvider( , mockStore, ); - expect(await findByText('#4321')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); + expect(useTrackERC20WithoutDecimalInformation).toHaveBeenCalled(); }); }); }); diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.tsx index 360559493596..e95edc03087b 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.tsx @@ -2,8 +2,9 @@ import React, { useMemo } from 'react'; import { NameType } from '@metamask/name-controller'; import { Hex } from '@metamask/utils'; import { captureException } from '@sentry/browser'; -import { shortenString } from '../../../../../../../../helpers/utils/util'; +import { MetaMetricsEventLocation } from '../../../../../../../../../shared/constants/metametrics'; +import { shortenString } from '../../../../../../../../helpers/utils/util'; import { calcTokenAmount } from '../../../../../../../../../shared/lib/transactions-controller-utils'; import useTokenExchangeRate from '../../../../../../../../components/app/currency-input/hooks/useTokenExchangeRate'; import { IndividualFiatDisplay } from '../../../../../simulation-details/fiat-display'; @@ -11,7 +12,8 @@ import { formatAmount, formatAmountMaxPrecision, } from '../../../../../simulation-details/formatAmount'; -import { useAsyncResult } from '../../../../../../../../hooks/useAsyncResult'; +import { useGetTokenStandardAndDetails } from '../../../../../../hooks/useGetTokenStandardAndDetails'; +import useTrackERC20WithoutDecimalInformation from '../../../../../../hooks/useTrackERC20WithoutDecimalInformation'; import { Box, @@ -27,7 +29,7 @@ import { TextAlign, } from '../../../../../../../../helpers/constants/design-system'; import Name from '../../../../../../../../components/app/name/name'; -import { fetchErc20Decimals } from '../../../../../../utils/token'; +import { TokenDetailsERC20 } from '../../../../../../utils/token'; type PermitSimulationValueDisplayParams = { /** The primaryType of the typed sign message */ @@ -52,12 +54,13 @@ const PermitSimulationValueDisplay: React.FC< > = ({ primaryType, tokenContract, value, tokenId }) => { const exchangeRate = useTokenExchangeRate(tokenContract); - const { value: tokenDecimals } = useAsyncResult(async () => { - if (tokenId) { - return undefined; - } - return await fetchErc20Decimals(tokenContract); - }, [tokenContract]); + const tokenDetails = useGetTokenStandardAndDetails(tokenContract); + useTrackERC20WithoutDecimalInformation( + tokenContract, + tokenDetails as TokenDetailsERC20, + MetaMetricsEventLocation.SignatureConfirmation, + ); + const { decimalsNumber: tokenDecimals } = tokenDetails; const fiatValue = useMemo(() => { if (exchangeRate && value && !tokenId) { diff --git a/ui/pages/confirmations/components/confirm/row/dataTree.tsx b/ui/pages/confirmations/components/confirm/row/dataTree.tsx index 26c91baed3a6..b295f337deb4 100644 --- a/ui/pages/confirmations/components/confirm/row/dataTree.tsx +++ b/ui/pages/confirmations/components/confirm/row/dataTree.tsx @@ -11,7 +11,6 @@ import { isValidHexAddress } from '../../../../../../shared/modules/hexstring-ut import { sanitizeString } from '../../../../../helpers/utils/util'; import { Box } from '../../../../../components/component-library'; import { BlockSize } from '../../../../../helpers/constants/design-system'; -import { useAsyncResult } from '../../../../../hooks/useAsyncResult'; import { useI18nContext } from '../../../../../hooks/useI18nContext'; import { ConfirmInfoRow, @@ -20,7 +19,7 @@ import { ConfirmInfoRowText, ConfirmInfoRowTextTokenUnits, } from '../../../../../components/app/confirm/info/row'; -import { fetchErc20Decimals } from '../../../utils/token'; +import { useGetTokenStandardAndDetails } from '../../../hooks/useGetTokenStandardAndDetails'; type ValueType = string | Record | TreeData[]; @@ -78,9 +77,9 @@ const NONE_DATE_VALUE = -1; * * @param dataTreeData */ -const getTokenDecimalsOfDataTree = async ( +const getTokenContractInDataTree = ( dataTreeData: Record | TreeData[], -): Promise => { +): Hex | undefined => { if (Array.isArray(dataTreeData)) { return undefined; } @@ -91,7 +90,7 @@ const getTokenDecimalsOfDataTree = async ( return undefined; } - return await fetchErc20Decimals(tokenContract); + return tokenContract; }; export const DataTree = ({ @@ -103,13 +102,10 @@ export const DataTree = ({ primaryType?: PrimaryType; tokenDecimals?: number; }) => { - const { value: decimalsResponse } = useAsyncResult( - async () => await getTokenDecimalsOfDataTree(data), - [data], - ); - + const tokenContract = getTokenContractInDataTree(data); + const { decimalsNumber } = useGetTokenStandardAndDetails(tokenContract); const tokenDecimals = - typeof decimalsResponse === 'number' ? decimalsResponse : tokenDecimalsProp; + typeof decimalsNumber === 'number' ? decimalsNumber : tokenDecimalsProp; return ( diff --git a/ui/pages/confirmations/components/confirm/row/typed-sign-data-v1/typedSignDataV1.test.tsx b/ui/pages/confirmations/components/confirm/row/typed-sign-data-v1/typedSignDataV1.test.tsx index 9563b5523f39..ecf55e3b574d 100644 --- a/ui/pages/confirmations/components/confirm/row/typed-sign-data-v1/typedSignDataV1.test.tsx +++ b/ui/pages/confirmations/components/confirm/row/typed-sign-data-v1/typedSignDataV1.test.tsx @@ -1,22 +1,28 @@ import React from 'react'; -import { render } from '@testing-library/react'; +import configureMockStore from 'redux-mock-store'; +import mockState from '../../../../../../../test/data/mock-state.json'; +import { renderWithProvider } from '../../../../../../../test/lib/render-helpers'; import { unapprovedTypedSignMsgV1 } from '../../../../../../../test/data/confirmations/typed_sign'; import { TypedSignDataV1Type } from '../../../../types/confirm'; import { ConfirmInfoRowTypedSignDataV1 } from './typedSignDataV1'; +const mockStore = configureMockStore([])(mockState); + describe('ConfirmInfoRowTypedSignData', () => { it('should match snapshot', () => { - const { container } = render( + const { container } = renderWithProvider( , + mockStore, ); expect(container).toMatchSnapshot(); }); it('should return null if data is not defined', () => { - const { container } = render( + const { container } = renderWithProvider( , + mockStore, ); expect(container).toBeEmptyDOMElement(); }); diff --git a/ui/pages/confirmations/components/simulation-details/useBalanceChanges.test.ts b/ui/pages/confirmations/components/simulation-details/useBalanceChanges.test.ts index 10e4cca518b7..5dc0be870538 100644 --- a/ui/pages/confirmations/components/simulation-details/useBalanceChanges.test.ts +++ b/ui/pages/confirmations/components/simulation-details/useBalanceChanges.test.ts @@ -9,7 +9,7 @@ import { TokenStandard } from '../../../../../shared/constants/transaction'; import { getConversionRate } from '../../../../ducks/metamask/metamask'; import { getTokenStandardAndDetails } from '../../../../store/actions'; import { fetchTokenExchangeRates } from '../../../../helpers/utils/util'; -import { fetchErc20Decimals } from '../../utils/token'; +import { memoizedGetTokenStandardAndDetails } from '../../utils/token'; import { useBalanceChanges } from './useBalanceChanges'; import { FIAT_UNAVAILABLE } from './types'; @@ -92,7 +92,7 @@ describe('useBalanceChanges', () => { afterEach(() => { /** Reset memoized function for each test */ - fetchErc20Decimals?.cache?.clear?.(); + memoizedGetTokenStandardAndDetails?.cache?.clear?.(); }); describe('pending states', () => { diff --git a/ui/pages/confirmations/confirm/confirm.test.tsx b/ui/pages/confirmations/confirm/confirm.test.tsx index d6b2dd704fb8..939ca8768afe 100644 --- a/ui/pages/confirmations/confirm/confirm.test.tsx +++ b/ui/pages/confirmations/confirm/confirm.test.tsx @@ -17,7 +17,7 @@ import mockState from '../../../../test/data/mock-state.json'; import { renderWithConfirmContextProvider } from '../../../../test/lib/confirmations/render-helpers'; import * as actions from '../../../store/actions'; import { SignatureRequestType } from '../types/confirm'; -import { fetchErc20Decimals } from '../utils/token'; +import { memoizedGetTokenStandardAndDetails } from '../utils/token'; import Confirm from './confirm'; jest.mock('react-router-dom', () => ({ @@ -34,7 +34,7 @@ describe('Confirm', () => { jest.resetAllMocks(); /** Reset memoized function using getTokenStandardAndDetails for each test */ - fetchErc20Decimals?.cache?.clear?.(); + memoizedGetTokenStandardAndDetails?.cache?.clear?.(); }); it('should render', () => { @@ -59,7 +59,7 @@ describe('Confirm', () => { jest.spyOn(actions, 'getTokenStandardAndDetails').mockResolvedValue({ decimals: '2', - standard: 'erc20', + standard: 'ERC20', }); const mockStore = configureMockStore(middleware)(mockStateTypedSign); @@ -103,7 +103,7 @@ describe('Confirm', () => { jest.spyOn(actions, 'getTokenStandardAndDetails').mockResolvedValue({ decimals: '2', - standard: 'erc20', + standard: 'ERC20', }); const mockStore = configureMockStore(middleware)(mockStateTypedSign); @@ -146,7 +146,7 @@ describe('Confirm', () => { jest.spyOn(actions, 'getTokenStandardAndDetails').mockResolvedValue({ decimals: '2', - standard: 'erc20', + standard: 'ERC20', }); await act(async () => { @@ -170,7 +170,7 @@ describe('Confirm', () => { jest.spyOn(actions, 'getTokenStandardAndDetails').mockResolvedValue({ decimals: '2', - standard: 'erc20', + standard: 'ERC20', }); await act(async () => { diff --git a/ui/pages/confirmations/hooks/useGetTokenStandardAndDetails.test.ts b/ui/pages/confirmations/hooks/useGetTokenStandardAndDetails.test.ts new file mode 100644 index 000000000000..7cd217db3a85 --- /dev/null +++ b/ui/pages/confirmations/hooks/useGetTokenStandardAndDetails.test.ts @@ -0,0 +1,50 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { waitFor } from '@testing-library/react'; + +import * as TokenActions from '../utils/token'; +import { useGetTokenStandardAndDetails } from './useGetTokenStandardAndDetails'; + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: () => 0x1, +})); + +jest.mock('react', () => ({ + ...jest.requireActual('react'), + useContext: jest.fn(), +})); + +jest.mock('../../../store/actions', () => { + return { + getTokenStandardAndDetails: jest + .fn() + .mockResolvedValue({ decimals: 2, standard: 'ERC20' }), + }; +}); + +describe('useGetTokenStandardAndDetails', () => { + it('should return token details', () => { + const { result } = renderHook(() => useGetTokenStandardAndDetails('0x5')); + expect(result.current).toEqual({ decimalsNumber: undefined }); + }); + + it('should return token details obtained from getTokenStandardAndDetails action', async () => { + jest + .spyOn(TokenActions, 'memoizedGetTokenStandardAndDetails') + .mockResolvedValue({ + standard: 'ERC20', + } as TokenActions.TokenDetailsERC20); + const { result, rerender } = renderHook(() => + useGetTokenStandardAndDetails('0x5'), + ); + + rerender(); + + await waitFor(() => { + expect(result.current).toEqual({ + decimalsNumber: 18, + standard: 'ERC20', + }); + }); + }); +}); diff --git a/ui/pages/confirmations/hooks/useGetTokenStandardAndDetails.ts b/ui/pages/confirmations/hooks/useGetTokenStandardAndDetails.ts new file mode 100644 index 000000000000..88dfb0a12b9d --- /dev/null +++ b/ui/pages/confirmations/hooks/useGetTokenStandardAndDetails.ts @@ -0,0 +1,42 @@ +import { Hex } from '@metamask/utils'; + +import { TokenStandard } from '../../../../shared/constants/transaction'; +import { useAsyncResult } from '../../../hooks/useAsyncResult'; +import { + ERC20_DEFAULT_DECIMALS, + parseTokenDetailDecimals, + memoizedGetTokenStandardAndDetails, + TokenDetailsERC20, +} from '../utils/token'; + +/** + * Returns token details for a given token contract + * + * @param tokenAddress + * @returns + */ +export const useGetTokenStandardAndDetails = ( + tokenAddress: Hex | string | undefined, +) => { + const { value: details } = useAsyncResult( + async () => + (await memoizedGetTokenStandardAndDetails( + tokenAddress, + )) as TokenDetailsERC20, + [tokenAddress], + ); + + if (!details) { + return { decimalsNumber: undefined }; + } + + const { decimals, standard } = details || {}; + + if (standard === TokenStandard.ERC20) { + const parsedDecimals = + parseTokenDetailDecimals(decimals) ?? ERC20_DEFAULT_DECIMALS; + details.decimalsNumber = parsedDecimals; + } + + return details; +}; diff --git a/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.test.ts b/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.test.ts new file mode 100644 index 000000000000..dff0103fbe21 --- /dev/null +++ b/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.test.ts @@ -0,0 +1,40 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { useContext } from 'react'; + +import { TokenStandard } from '../../../../shared/constants/transaction'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { TokenDetailsERC20 } from '../utils/token'; +import useTrackERC20WithoutDecimalInformation from './useTrackERC20WithoutDecimalInformation'; + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: () => 0x1, +})); + +jest.mock('react', () => ({ + ...jest.requireActual('react'), + useContext: jest.fn(), +})); + +describe('useTrackERC20WithoutDecimalInformation', () => { + const useContextMock = jest.mocked(useContext); + + const trackEventMock = jest.fn(); + + it('should invoke trackEvent method', () => { + useContextMock.mockImplementation((context) => { + if (context === MetaMetricsContext) { + return trackEventMock; + } + return undefined; + }); + + renderHook(() => + useTrackERC20WithoutDecimalInformation('0x5', { + standard: TokenStandard.ERC20, + } as TokenDetailsERC20), + ); + + expect(trackEventMock).toHaveBeenCalled(); + }); +}); diff --git a/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.ts b/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.ts new file mode 100644 index 000000000000..fa6a5e620fc4 --- /dev/null +++ b/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.ts @@ -0,0 +1,58 @@ +import { useSelector } from 'react-redux'; +import { useContext, useEffect } from 'react'; +import { Hex } from '@metamask/utils'; + +import { + MetaMetricsEventCategory, + MetaMetricsEventLocation, + MetaMetricsEventName, + MetaMetricsEventUiCustomization, +} from '../../../../shared/constants/metametrics'; +import { TokenStandard } from '../../../../shared/constants/transaction'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { getCurrentChainId } from '../../../selectors'; +import { parseTokenDetailDecimals, TokenDetailsERC20 } from '../utils/token'; + +/** + * Track event that number of decimals in ERC20 is not obtained + * + * @param tokenAddress + * @param tokenDetails + * @param metricLocation + */ +const useTrackERC20WithoutDecimalInformation = ( + tokenAddress: Hex | string | undefined, + tokenDetails?: TokenDetailsERC20, + metricLocation = MetaMetricsEventLocation.SignatureConfirmation, +) => { + const trackEvent = useContext(MetaMetricsContext); + const chainId = useSelector(getCurrentChainId); + + useEffect(() => { + if (chainId === undefined || tokenDetails === undefined) { + return; + } + const { decimals, standard } = tokenDetails || {}; + if (standard === TokenStandard.ERC20) { + const parsedDecimals = parseTokenDetailDecimals(decimals); + if (parsedDecimals === undefined) { + trackEvent({ + event: MetaMetricsEventName.SimulationIncompleteAssetDisplayed, + category: MetaMetricsEventCategory.Confirmations, + properties: { + token_decimals_available: false, + asset_address: tokenAddress, + asset_type: TokenStandard.ERC20, + chain_id: chainId, + location: metricLocation, + ui_customizations: [ + MetaMetricsEventUiCustomization.RedesignedConfirmation, + ], + }, + }); + } + } + }, [tokenDetails, chainId, tokenAddress, trackEvent]); +}; + +export default useTrackERC20WithoutDecimalInformation; diff --git a/ui/pages/confirmations/utils/token.test.ts b/ui/pages/confirmations/utils/token.test.ts index e71813713d79..250bff90c07c 100644 --- a/ui/pages/confirmations/utils/token.test.ts +++ b/ui/pages/confirmations/utils/token.test.ts @@ -1,6 +1,9 @@ import { getTokenStandardAndDetails } from '../../../store/actions'; import { ERC20_DEFAULT_DECIMALS } from '../constants/token'; -import { fetchErc20Decimals } from './token'; +import { + fetchErc20Decimals, + memoizedGetTokenStandardAndDetails, +} from './token'; const MOCK_ADDRESS = '0x514910771af9ca656af840dff83e8264ecf986ca'; const MOCK_DECIMALS = 36; @@ -14,7 +17,7 @@ describe('fetchErc20Decimals', () => { jest.clearAllMocks(); /** Reset memoized function using getTokenStandardAndDetails for each test */ - fetchErc20Decimals?.cache?.clear?.(); + memoizedGetTokenStandardAndDetails?.cache?.clear?.(); }); it(`should return the default number, ${ERC20_DEFAULT_DECIMALS}, if no decimals were found from details`, async () => { diff --git a/ui/pages/confirmations/utils/token.ts b/ui/pages/confirmations/utils/token.ts index 1f94280129a9..3a8c3a2a671e 100644 --- a/ui/pages/confirmations/utils/token.ts +++ b/ui/pages/confirmations/utils/token.ts @@ -1,32 +1,89 @@ import { memoize } from 'lodash'; import { Hex } from '@metamask/utils'; +import { AssetsContractController } from '@metamask/assets-controllers'; import { getTokenStandardAndDetails } from '../../../store/actions'; +export type TokenDetailsERC20 = Awaited< + ReturnType< + ReturnType['getDetails'] + > +> & { decimalsNumber: number }; + +export type TokenDetailsERC721 = Awaited< + ReturnType< + ReturnType['getDetails'] + > +>; + +export type TokenDetailsERC1155 = Awaited< + ReturnType< + ReturnType['getDetails'] + > +>; + +export type TokenDetails = + | TokenDetailsERC20 + | TokenDetailsERC721 + | TokenDetailsERC1155; + export const ERC20_DEFAULT_DECIMALS = 18; -/** - * Fetches the decimals for the given token address. - * - * @param {Hex | string} address - The ethereum token contract address. It is expected to be in hex format. - * We currently accept strings since we have a patch that accepts a custom string - * {@see .yarn/patches/@metamask-eth-json-rpc-middleware-npm-14.0.1-b6c2ccbe8c.patch} - */ -export const fetchErc20Decimals = memoize( - async (address: Hex | string): Promise => { +export const parseTokenDetailDecimals = ( + decStr?: string, +): number | undefined => { + if (!decStr) { + return undefined; + } + + for (const radix of [10, 16]) { + const parsedDec = parseInt(decStr, radix); + if (isFinite(parsedDec)) { + return parsedDec; + } + } + return undefined; +}; + +export const memoizedGetTokenStandardAndDetails = memoize( + async ( + tokenAddress?: Hex | string, + userAddress?: string, + tokenId?: string, + ): Promise> => { try { - const { decimals: decStr } = await getTokenStandardAndDetails(address); - if (!decStr) { - return ERC20_DEFAULT_DECIMALS; - } - for (const radix of [10, 16]) { - const parsedDec = parseInt(decStr, radix); - if (isFinite(parsedDec)) { - return parsedDec; - } + if (!tokenAddress) { + return {}; } - return ERC20_DEFAULT_DECIMALS; + + return (await getTokenStandardAndDetails( + tokenAddress, + userAddress, + tokenId, + )) as TokenDetails; } catch { - return ERC20_DEFAULT_DECIMALS; + return {}; } }, ); + +/** + * Fetches the decimals for the given token address. + * + * @param address - The ethereum token contract address. It is expected to be in hex format. + * We currently accept strings since we have a patch that accepts a custom string + * {@see .yarn/patches/@metamask-eth-json-rpc-middleware-npm-14.0.1-b6c2ccbe8c.patch} + */ +export const fetchErc20Decimals = async ( + address: Hex | string, +): Promise => { + try { + const { decimals: decStr } = (await memoizedGetTokenStandardAndDetails( + address, + )) as TokenDetailsERC20; + const decimals = parseTokenDetailDecimals(decStr); + + return decimals ?? ERC20_DEFAULT_DECIMALS; + } catch { + return ERC20_DEFAULT_DECIMALS; + } +};