Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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);
});
});
31 changes: 17 additions & 14 deletions app/components/hooks/DisplayName/useERC20Tokens.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,31 @@
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';

function buildAssetId(value: string, variation: Hex): string {
Comment thread
juanmigdr marked this conversation as resolved.
Outdated
return `eip155:${parseInt(variation, 16)}/erc20:${value.toLowerCase()}`;
}

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

Check warning on line 14 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=AZ0BPTlpQYS7SbNaKPla&open=AZ0BPTlpQYS7SbNaKPla&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[buildAssetId(value as string, variation as Hex)];

Check warning on line 25 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=AZ0BPTlpQYS7SbNaKPlc&open=AZ0BPTlpQYS7SbNaKPlc&pullRequest=27611
const name =
preferContractSymbol && token?.symbol ? token.symbol : token?.name;

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