Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ app/core/DeeplinkManager/handlers/legacy/handlePredictUrl.ts @MetaMask/predict
app/components/hooks/useIsOriginalNativeTokenSymbol @MetaMask/metamask-assets
app/components/hooks/useTokenBalancesController @MetaMask/metamask-assets
app/components/hooks/useTokenBalance.tsx @MetaMask/metamask-assets
app/components/hooks/useTokensData @MetaMask/metamask-assets
app/components/hooks/useSafeChains.ts @MetaMask/metamask-assets
app/components/UI/Assets @MetaMask/metamask-assets
app/components/UI/AssetOverview @MetaMask/metamask-assets
Expand Down
210 changes: 90 additions & 120 deletions app/components/hooks/DisplayName/useERC20Tokens.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,137 +6,107 @@ import { renderHookWithProvider } from '../../../util/test/renderWithProvider';
const TOKEN_NAME_MOCK = 'Test Token';
const TOKEN_SYMBOL_MOCK = 'TT';
const TOKEN_ICON_URL_MOCK = 'https://example.com/icon.png';
const TOKEN_ADDRESS_MOCK = '0x0439e60F02a8900a951603950d8D4527f400C3f1';
const UNKNOWN_ADDRESS_MOCK = '0xabc123';

const STATE_MOCK = {
engine: {
backgroundState: {
TokenListController: {
tokensChainsCache: {
[CHAIN_IDS.MAINNET]: {
data: {
[TOKEN_ADDRESS_MOCK.toLowerCase()]: {
name: TOKEN_NAME_MOCK,
symbol: TOKEN_SYMBOL_MOCK,
iconUrl: TOKEN_ICON_URL_MOCK,
},
},
},
},
},
},
},
};
const TOKEN_ADDRESS_MOCK = '0x0439e60f02a8900a951603950d8d4527f400c3f1';
const CHAIN_ID_MOCK = CHAIN_IDS.MAINNET;
const ASSET_ID_MOCK = `eip155:1/erc20:${TOKEN_ADDRESS_MOCK}`;

jest.mock('../useTokensData/useTokensData', () => ({
useTokensData: jest.fn(),
}));

import { useTokensData } from '../useTokensData/useTokensData';

const mockUseTokensData = useTokensData as jest.Mock;

function renderHook(requests: Parameters<typeof useERC20Tokens>[0]) {
return renderHookWithProvider(() => useERC20Tokens(requests), { state: {} });
}

describe('useERC20Tokens', () => {
it('returns undefined if no token found', () => {
const {
result: { current },
} = renderHookWithProvider(
() =>
useERC20Tokens([
{
type: NameType.EthereumAddress,
value: UNKNOWN_ADDRESS_MOCK,
variation: CHAIN_IDS.MAINNET,
},
]),
{ state: STATE_MOCK },
);

expect(current[0]?.name).toBe(undefined);
beforeEach(() => {
jest.clearAllMocks();
mockUseTokensData.mockReturnValue({
[ASSET_ID_MOCK]: {
assetId: ASSET_ID_MOCK,
name: TOKEN_NAME_MOCK,
symbol: TOKEN_SYMBOL_MOCK,
iconUrl: TOKEN_ICON_URL_MOCK,
},
});
});

it('returns name if found', () => {
const {
result: { current },
} = renderHookWithProvider(
() =>
useERC20Tokens([
{
type: NameType.EthereumAddress,
value: TOKEN_ADDRESS_MOCK,
variation: CHAIN_IDS.MAINNET,
},
]),
{ state: STATE_MOCK },
);

expect(current[0]?.name).toBe(TOKEN_NAME_MOCK);
it('returns undefined if type is not EthereumAddress', () => {
const { result } = renderHook([
{
type: 'alternateType' as NameType,
value: TOKEN_ADDRESS_MOCK,
variation: CHAIN_ID_MOCK,
},
]);

expect(result.current[0]).toBeUndefined();
});

it('returns symbol if preferred', () => {
const {
result: { current },
} = renderHookWithProvider(
() =>
useERC20Tokens([
{
preferContractSymbol: true,
type: NameType.EthereumAddress,
value: TOKEN_ADDRESS_MOCK,
variation: CHAIN_IDS.MAINNET,
},
]),
{ state: STATE_MOCK },
);

expect(current[0]?.name).toBe(TOKEN_SYMBOL_MOCK);
it('returns name when token is found', () => {
const { result } = renderHook([
{
type: NameType.EthereumAddress,
value: TOKEN_ADDRESS_MOCK,
variation: CHAIN_ID_MOCK,
},
]);

expect(result.current[0]?.name).toBe(TOKEN_NAME_MOCK);
});

it('returns image', () => {
const {
result: { current },
} = renderHookWithProvider(
() =>
useERC20Tokens([
{
preferContractSymbol: true,
type: NameType.EthereumAddress,
value: TOKEN_ADDRESS_MOCK,
variation: CHAIN_IDS.MAINNET,
},
]),
{ state: STATE_MOCK },
);

expect(current[0]?.image).toBe(TOKEN_ICON_URL_MOCK);
it('returns symbol when preferContractSymbol is true', () => {
const { result } = renderHook([
{
preferContractSymbol: true,
type: NameType.EthereumAddress,
value: TOKEN_ADDRESS_MOCK,
variation: CHAIN_ID_MOCK,
},
]);

expect(result.current[0]?.name).toBe(TOKEN_SYMBOL_MOCK);
});

it('returns image when token is found', () => {
const { result } = renderHook([
{
type: NameType.EthereumAddress,
value: TOKEN_ADDRESS_MOCK,
variation: CHAIN_ID_MOCK,
},
]);

expect(result.current[0]?.image).toBe(TOKEN_ICON_URL_MOCK);
});

it('returns null if type is not address', () => {
const {
result: { current },
} = renderHookWithProvider(
() =>
useERC20Tokens([
{
type: 'alternateType' as NameType,
value: TOKEN_ADDRESS_MOCK,
variation: CHAIN_IDS.MAINNET,
},
]),
{ state: STATE_MOCK },
);

expect(current[0]?.name).toBeUndefined();
it('returns name and image as undefined when token is not found', () => {
mockUseTokensData.mockReturnValue({});

const { result } = renderHook([
{
type: NameType.EthereumAddress,
value: TOKEN_ADDRESS_MOCK,
variation: CHAIN_ID_MOCK,
},
]);

expect(result.current[0]).toEqual({ name: undefined, image: undefined });
});

it('normalizes addresses to lowercase', () => {
const {
result: { current },
} = renderHookWithProvider(
() =>
useERC20Tokens([
{
type: NameType.EthereumAddress,
value: TOKEN_ADDRESS_MOCK.toUpperCase(),
variation: CHAIN_IDS.MAINNET,
},
]),
{ state: STATE_MOCK },
);

expect(current[0]?.name).toBe(TOKEN_NAME_MOCK);
it('normalizes addresses to lowercase when building the asset ID', () => {
const { result } = renderHook([
{
type: NameType.EthereumAddress,
value: TOKEN_ADDRESS_MOCK.toUpperCase(),
variation: CHAIN_ID_MOCK,
},
]);

expect(result.current[0]?.name).toBe(TOKEN_NAME_MOCK);
});
});
28 changes: 14 additions & 14 deletions app/components/hooks/DisplayName/useERC20Tokens.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
import { NameType } from '../../UI/Name/Name.types';
import { UseDisplayNameRequest } from './useDisplayName';
import { selectERC20TokensByChain } from '../../../selectors/tokenListController';
import { useSelector } from 'react-redux';
import { Hex } from '@metamask/utils';
import { useTokensData } from '../useTokensData/useTokensData';
import { buildEvmCaip19AssetId } from '../../../util/multichain/buildEvmCaip19AssetId';

export function useERC20Tokens(requests: UseDisplayNameRequest[]) {
const erc20TokensByChain = useSelector(selectERC20TokensByChain);
const assetIds = requests
.filter(({ type, value }) => type === NameType.EthereumAddress && value)
.map(({ value, variation }) =>
buildEvmCaip19AssetId(value as string, variation as Hex),

Check warning on line 11 in app/components/hooks/DisplayName/useERC20Tokens.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This assertion is unnecessary since it does not change the type of the expression.

See more on https://sonarcloud.io/project/issues?id=metamask-mobile&issues=AZ0acYhBpX3LlBtpJsVS&open=AZ0acYhBpX3LlBtpJsVS&pullRequest=27611
);

const tokensByAssetId = useTokensData(assetIds);

return requests.map(({ preferContractSymbol, type, value, variation }) => {
if (type !== NameType.EthereumAddress || !value) {
return undefined;
}

const contractAddress = value.toLowerCase();
const chainId = variation as Hex;

const {
name: tokenName,
symbol,
iconUrl: image,
} = erc20TokensByChain[chainId]?.data?.[contractAddress] ?? {};

const name = preferContractSymbol && symbol ? symbol : tokenName;
const token =
tokensByAssetId[buildEvmCaip19AssetId(value as string, variation as Hex)];

Check warning on line 22 in app/components/hooks/DisplayName/useERC20Tokens.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This assertion is unnecessary since it does not change the type of the expression.

See more on https://sonarcloud.io/project/issues?id=metamask-mobile&issues=AZ0acYhBpX3LlBtpJsVT&open=AZ0acYhBpX3LlBtpJsVT&pullRequest=27611
const name =
preferContractSymbol && token?.symbol ? token.symbol : token?.name;

return { name, image };
return { name, image: token?.iconUrl };
});
}
Loading
Loading