Skip to content

Commit

Permalink
refactor: memoize fetchErc20Decimals, relocate to utils/token, and ap…
Browse files Browse the repository at this point in the history
…ply to other instances (#27088)
  • Loading branch information
digiwand authored Sep 19, 2024
1 parent f06b7a0 commit 88664bb
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 41 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useMemo } from 'react';
import { NameType } from '@metamask/name-controller';
import { Hex } from '@metamask/utils';
import { captureException } from '@sentry/browser';
import { getTokenStandardAndDetails } from '../../../../../../../../store/actions';
import { shortenString } from '../../../../../../../../helpers/utils/util';

import { calcTokenAmount } from '../../../../../../../../../shared/lib/transactions-controller-utils';
Expand All @@ -27,23 +27,30 @@ import {
TextAlign,
} from '../../../../../../../../helpers/constants/design-system';
import Name from '../../../../../../../../components/app/name/name';
import { fetchErc20Decimals } from '../../../../../../utils/token';

const getTokenDecimals = async (tokenContract: string) => {
const tokenDetails = await getTokenStandardAndDetails(tokenContract);
const tokenDecimals = tokenDetails?.decimals;
type PermitSimulationValueDisplayParams = {
/** The primaryType of the typed sign message */
primaryType?: string;

return parseInt(tokenDecimals ?? '0', 10);
};
/**
* 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}
*/
tokenContract: Hex | string;

const PermitSimulationValueDisplay: React.FC<{
primaryType?: string;
tokenContract: string;
/** The token amount */
value: number | string;
}> = ({ primaryType, tokenContract, value }) => {
};

const PermitSimulationValueDisplay: React.FC<
PermitSimulationValueDisplayParams
> = ({ primaryType, tokenContract, value }) => {
const exchangeRate = useTokenExchangeRate(tokenContract);

const { value: tokenDecimals } = useAsyncResult(
async () => await getTokenDecimals(tokenContract),
async () => await fetchErc20Decimals(tokenContract),
[tokenContract],
);

Expand Down
14 changes: 6 additions & 8 deletions ui/pages/confirmations/components/confirm/row/dataTree.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Hex } from '@metamask/utils';
import React, { memo } from 'react';

import {
Expand All @@ -6,9 +7,8 @@ import {
PRIMARY_TYPES_PERMIT,
} from '../../../../../../shared/constants/signatures';
import { isValidHexAddress } from '../../../../../../shared/modules/hexstring-utils';
import { sanitizeString } from '../../../../../helpers/utils/util';
import { getTokenStandardAndDetails } from '../../../../../store/actions';

import { sanitizeString } from '../../../../../helpers/utils/util';
import { Box } from '../../../../../components/component-library';
import { BlockSize } from '../../../../../helpers/constants/design-system';
import { useAsyncResult } from '../../../../../hooks/useAsyncResult';
Expand All @@ -19,6 +19,7 @@ import {
ConfirmInfoRowText,
ConfirmInfoRowTextTokenUnits,
} from '../../../../../components/app/confirm/info/row';
import { fetchErc20Decimals } from '../../../utils/token';

type ValueType = string | Record<string, TreeData> | TreeData[];

Expand Down Expand Up @@ -68,15 +69,12 @@ const getTokenDecimalsOfDataTree = async (
}

const tokenContract = (dataTreeData as Record<string, TreeData>).token
?.value as string;
if (!tokenContract) {
?.value as Hex;
if (!tokenContract || !isValidHexAddress(tokenContract)) {
return undefined;
}

const tokenDetails = await getTokenStandardAndDetails(tokenContract);
const tokenDecimals = tokenDetails?.decimals;

return parseInt(tokenDecimals ?? '0', 10);
return await fetchErc20Decimals(tokenContract);
};

export const DataTree = ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +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 { useBalanceChanges } from './useBalanceChanges';
import { FIAT_UNAVAILABLE } from './types';

Expand Down Expand Up @@ -89,6 +90,11 @@ describe('useBalanceChanges', () => {
});
});

afterEach(() => {
/** Reset memoized function for each test */
fetchErc20Decimals?.cache?.clear?.();
});

describe('pending states', () => {
it('returns pending=true if no simulation data', async () => {
const { result, waitForNextUpdate } = renderHook(() =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import {
import { BigNumber } from 'bignumber.js';
import { ContractExchangeRates } from '@metamask/assets-controllers';
import { useAsyncResultOrThrow } from '../../../../hooks/useAsyncResult';
import { getTokenStandardAndDetails } from '../../../../store/actions';
import { TokenStandard } from '../../../../../shared/constants/transaction';
import { getConversionRate } from '../../../../ducks/metamask/metamask';
import { getCurrentChainId, getCurrentCurrency } from '../../../../selectors';
import { fetchTokenExchangeRates } from '../../../../helpers/utils/util';
import { ERC20_DEFAULT_DECIMALS, fetchErc20Decimals } from '../../utils/token';

import {
BalanceChange,
FIAT_UNAVAILABLE,
Expand All @@ -23,8 +24,6 @@ import {

const NATIVE_DECIMALS = 18;

const ERC20_DEFAULT_DECIMALS = 18;

// See https://github.com/MikeMcl/bignumber.js/issues/11#issuecomment-23053776
function convertNumberToStringWithPrecisionWarning(value: number): string {
return String(value);
Expand Down Expand Up @@ -57,25 +56,6 @@ function getAssetAmount(
);
}

// Fetches the decimals for the given token address.
async function fetchErc20Decimals(address: Hex): Promise<number> {
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;
}
}
return ERC20_DEFAULT_DECIMALS;
} catch {
return ERC20_DEFAULT_DECIMALS;
}
}

// Fetches token details for all the token addresses in the SimulationTokenBalanceChanges
async function fetchAllErc20Decimals(
addresses: Hex[],
Expand Down
4 changes: 4 additions & 0 deletions ui/pages/confirmations/confirm/confirm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
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 Confirm from './confirm';

jest.mock('react-router-dom', () => ({
Expand All @@ -32,6 +33,9 @@ const middleware = [thunk];
describe('Confirm', () => {
afterEach(() => {
jest.resetAllMocks();

/** Reset memoized function using getTokenStandardAndDetails for each test */
fetchErc20Decimals?.cache?.clear?.();
});

it('should render', () => {
Expand Down
1 change: 1 addition & 0 deletions ui/pages/confirmations/constants/token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const ERC20_DEFAULT_DECIMALS = 18;
50 changes: 50 additions & 0 deletions ui/pages/confirmations/utils/token.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { getTokenStandardAndDetails } from '../../../store/actions';
import { ERC20_DEFAULT_DECIMALS } from '../constants/token';
import { fetchErc20Decimals } from './token';

const MOCK_ADDRESS = '0x514910771af9ca656af840dff83e8264ecf986ca';
const MOCK_DECIMALS = 36;

jest.mock('../../../store/actions', () => ({
getTokenStandardAndDetails: jest.fn(),
}));

describe('fetchErc20Decimals', () => {
afterEach(() => {
jest.clearAllMocks();

/** Reset memoized function using getTokenStandardAndDetails for each test */
fetchErc20Decimals?.cache?.clear?.();
});

it(`should return the default number, ${ERC20_DEFAULT_DECIMALS}, if no decimals were found from details`, async () => {
(getTokenStandardAndDetails as jest.Mock).mockResolvedValue({});
const decimals = await fetchErc20Decimals(MOCK_ADDRESS);

expect(decimals).toBe(ERC20_DEFAULT_DECIMALS);
});

it('should return the decimals for a given token address', async () => {
(getTokenStandardAndDetails as jest.Mock).mockResolvedValue({
decimals: MOCK_DECIMALS,
});
const decimals = await fetchErc20Decimals(MOCK_ADDRESS);

expect(decimals).toBe(MOCK_DECIMALS);
});

it('should memoize the result for the same token addresses', async () => {
(getTokenStandardAndDetails as jest.Mock).mockResolvedValue({
decimals: MOCK_DECIMALS,
});

const firstCallResult = await fetchErc20Decimals(MOCK_ADDRESS);
const secondCallResult = await fetchErc20Decimals(MOCK_ADDRESS);

expect(firstCallResult).toBe(secondCallResult);
expect(getTokenStandardAndDetails).toHaveBeenCalledTimes(1);

await fetchErc20Decimals('0xDifferentAddress');
expect(getTokenStandardAndDetails).toHaveBeenCalledTimes(2);
});
});
32 changes: 32 additions & 0 deletions ui/pages/confirmations/utils/token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { memoize } from 'lodash';
import { Hex } from '@metamask/utils';
import { getTokenStandardAndDetails } from '../../../store/actions';

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<number> => {
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;
}
}
return ERC20_DEFAULT_DECIMALS;
} catch {
return ERC20_DEFAULT_DECIMALS;
}
},
);

0 comments on commit 88664bb

Please sign in to comment.