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`] = `
-
-
-
-
-
-
-
-
- 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;
+ }
+};