diff --git a/app/components/UI/AddressInputs/index.test.jsx b/app/components/UI/AddressInputs/index.test.jsx index a32e4577c0d..cc3a6d2bfc5 100644 --- a/app/components/UI/AddressInputs/index.test.jsx +++ b/app/components/UI/AddressInputs/index.test.jsx @@ -23,6 +23,7 @@ const initialState = { name: 'Account 2', }, }, + useTokenDetection: false, }, AddressBookController: { addressBook: { @@ -61,7 +62,7 @@ describe('AddressInputs', () => { fromAccountBalance="0x5" fromAccountName="DUMMY_ACCOUNT" />, - {}, + { state: initialState }, ); expect(container).toMatchSnapshot(); }); @@ -74,7 +75,7 @@ describe('AddressInputs', () => { fromAccountName="DUMMY_ACCOUNT" layout="vertical" />, - {}, + { state: initialState }, ); expect(container).toMatchSnapshot(); }); diff --git a/app/components/UI/Identicon/__snapshots__/index.test.tsx.snap b/app/components/UI/Identicon/__snapshots__/index.test.tsx.snap index 18c2cca7567..9707813bf99 100644 --- a/app/components/UI/Identicon/__snapshots__/index.test.tsx.snap +++ b/app/components/UI/Identicon/__snapshots__/index.test.tsx.snap @@ -1,5 +1,25 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Identicon should render correctly when provided address found in tokenList and iconUrl is available 1`] = ` + +`; + exports[`Identicon should render correctly when useBlockieIcon is false 1`] = ` { const mockStore = configureMockStore(); + const mockUseTokenList = jest + .mocked(useTokenList) + .mockImplementation(() => ({})); + + it('should render correctly when provided address found in tokenList and iconUrl is available', () => { + const addressMock = '0x0439e60f02a8900a951603950d8d4527f400c3f1'; + mockUseTokenList.mockImplementation(() => [ + { + address: addressMock, + iconUrl: 'https://example.com/icon.png', + }, + ]); + + const initialState = { + settings: { useBlockieIcon: true }, + }; + const store = mockStore(initialState); + + const wrapper = render( + + + , + ); + expect(wrapper).toMatchSnapshot(); + }); it('should render correctly when useBlockieIcon is true', () => { const initialState = { settings: { useBlockieIcon: true }, diff --git a/app/components/UI/Identicon/index.tsx b/app/components/UI/Identicon/index.tsx index 945419c69b5..aaac02b3ef4 100644 --- a/app/components/UI/Identicon/index.tsx +++ b/app/components/UI/Identicon/index.tsx @@ -6,6 +6,8 @@ import FadeIn from 'react-native-fade-in-image'; import Jazzicon from 'react-native-jazzicon'; import { connect } from 'react-redux'; import { useTheme } from '../../../util/theme'; +import { useTokenListEntry } from '../../../components/hooks/DisplayName/useTokenListEntry'; +import { NameType } from '../../UI/Name/Name.types'; interface IdenticonProps { /** @@ -43,23 +45,35 @@ const Identicon: React.FC = ({ useBlockieIcon = true, }) => { const { colors } = useTheme(); + const tokenListIcon = useTokenListEntry( + address as string, + NameType.EthereumAddress, + )?.iconUrl; if (!address) return null; const uri = useBlockieIcon && toDataUrl(address); + const styleForBlockieAndTokenIcon = [ + { + height: diameter, + width: diameter, + borderRadius: diameter / 2, + }, + customStyle, + ]; + + if (tokenListIcon) { + return ( + + ); + } + const image = useBlockieIcon ? ( - + ) : ( diff --git a/app/components/UI/Name/Name.test.tsx b/app/components/UI/Name/Name.test.tsx index b927c680ba8..a233e535f09 100644 --- a/app/components/UI/Name/Name.test.tsx +++ b/app/components/UI/Name/Name.test.tsx @@ -14,6 +14,11 @@ jest.mock('../../hooks/DisplayName/useDisplayName', () => ({ default: jest.fn(), })); +jest.mock('../Identicon', () => ({ + __esModule: true, + default: () => 'Identicon', +})); + const UNKNOWN_ADDRESS_CHECKSUMMED = '0x299007B3F9E23B8d432D5f545F8a4a2B3E9A5B4e'; const EXPECTED_UNKNOWN_ADDRESS_CHECKSUMMED = '0x29900...A5B4e'; diff --git a/app/components/UI/Name/__snapshots__/Name.test.tsx.snap b/app/components/UI/Name/__snapshots__/Name.test.tsx.snap index 75d9eb71ec6..2d61fb1f825 100644 --- a/app/components/UI/Name/__snapshots__/Name.test.tsx.snap +++ b/app/components/UI/Name/__snapshots__/Name.test.tsx.snap @@ -16,64 +16,7 @@ exports[`Name recognized address should return name 1`] = ` } } > - - - - - - + Identicon -`; \ No newline at end of file +`; diff --git a/app/components/Views/confirmations/Send/index.test.tsx b/app/components/Views/confirmations/Send/index.test.tsx index 6432727b813..309da7b89ec 100644 --- a/app/components/Views/confirmations/Send/index.test.tsx +++ b/app/components/Views/confirmations/Send/index.test.tsx @@ -50,6 +50,9 @@ const initialState = { TokenBalancesController: { contractBalances: {}, }, + TokenListController: { + tokenList: [], + }, PreferencesController: { featureFlags: {}, identities: { diff --git a/app/components/hooks/DisplayName/useTokenList.test.ts b/app/components/hooks/DisplayName/useTokenList.test.ts index 856ed1c042d..acd96d4e973 100644 --- a/app/components/hooks/DisplayName/useTokenList.test.ts +++ b/app/components/hooks/DisplayName/useTokenList.test.ts @@ -1,19 +1,19 @@ import React from 'react'; -import { type TokenListMap } from '@metamask/assets-controllers'; +import { type TokenListToken } from '@metamask/assets-controllers'; import { selectChainId } from '../../../selectors/networkController'; import { selectUseTokenDetection } from '../../../selectors/preferencesController'; -import { selectTokenList } from '../../../selectors/tokenListController'; +import { selectTokenListArray } from '../../../selectors/tokenListController'; import { isMainnetByChainId } from '../../../util/networks'; import useTokenList from './useTokenList'; const MAINNET_TOKEN_ADDRESS_MOCK = '0xdAC17F958D2ee523a2206206994597C13D831ec7'; const MAINNET_TOKEN_NAME_MOCK = 'Tether USD'; -const normalizedMainnetTokenListMock = { - [MAINNET_TOKEN_ADDRESS_MOCK.toLowerCase()]: { +const normalizedMainnetTokenListMock = [ + { name: MAINNET_TOKEN_NAME_MOCK, }, -}; +]; jest.mock('@metamask/contract-metadata', () => ({ __esModule: true, default: { @@ -38,7 +38,7 @@ jest.mock('../../../selectors/preferencesController', () => ({ })); jest.mock('../../../selectors/tokenListController', () => ({ - selectTokenList: jest.fn(), + selectTokenListArray: jest.fn(), })); jest.mock('../../../util/networks', () => ({ @@ -48,27 +48,29 @@ jest.mock('../../../util/networks', () => ({ const CHAIN_ID_MOCK = '0x1'; const TOKEN_NAME_MOCK = 'MetaMask Token'; const TOKEN_ADDRESS_MOCK = '0x0439e60F02a8900a951603950d8D4527f400C3f1'; -const TOKEN_LIST_MOCK = { - [TOKEN_ADDRESS_MOCK]: { +const TOKEN_LIST_ARRAY_MOCK = [ + { name: TOKEN_NAME_MOCK, + address: TOKEN_ADDRESS_MOCK, }, -} as unknown as TokenListMap; -const normalizedTokenListMock = { - [TOKEN_ADDRESS_MOCK.toLowerCase()]: { +] as unknown as TokenListToken[]; +const normalizedTokenListMock = [ + { + address: TOKEN_ADDRESS_MOCK, name: TOKEN_NAME_MOCK, }, -}; +]; describe('useTokenList', () => { const selectChainIdMock = jest.mocked(selectChainId); const selectUseTokenDetectionMock = jest.mocked(selectUseTokenDetection); - const selectTokenListMock = jest.mocked(selectTokenList); + const selectTokenListArrayMock = jest.mocked(selectTokenListArray); const isMainnetByChainIdMock = jest.mocked(isMainnetByChainId); beforeEach(() => { jest.resetAllMocks(); selectChainIdMock.mockReturnValue(CHAIN_ID_MOCK); selectUseTokenDetectionMock.mockReturnValue(true); - selectTokenListMock.mockReturnValue(TOKEN_LIST_MOCK); + selectTokenListArrayMock.mockReturnValue(TOKEN_LIST_ARRAY_MOCK); isMainnetByChainIdMock.mockReturnValue(true); const memoizedValues = new Map(); diff --git a/app/components/hooks/DisplayName/useTokenList.ts b/app/components/hooks/DisplayName/useTokenList.ts index 6726fffbbf3..3e99976658e 100644 --- a/app/components/hooks/DisplayName/useTokenList.ts +++ b/app/components/hooks/DisplayName/useTokenList.ts @@ -1,38 +1,27 @@ import { useMemo } from 'react'; -import { type TokenListMap } from '@metamask/assets-controllers'; import contractMap from '@metamask/contract-metadata'; - +import { TokenListToken } from '@metamask/assets-controllers'; import { useSelector } from 'react-redux'; import { selectChainId } from '../../../selectors/networkController'; import { selectUseTokenDetection } from '../../../selectors/preferencesController'; -import { selectTokenList } from '../../../selectors/tokenListController'; +import { selectTokenListArray } from '../../../selectors/tokenListController'; import { isMainnetByChainId } from '../../../util/networks'; -function normalizeTokenAddresses(tokenMap: TokenListMap) { - return Object.keys(tokenMap).reduce((acc, address) => { - const tokenMetadata = tokenMap[address]; - return { - ...acc, - [address.toLowerCase()]: { - ...tokenMetadata, - }, - }; - }, {}); -} - -const NORMALIZED_MAINNET_TOKEN_LIST = normalizeTokenAddresses(contractMap); +const NORMALIZED_MAINNET_TOKEN_ARRAY = Object.values( + contractMap, +) as TokenListToken[]; -export default function useTokenList(): TokenListMap { +export default function useTokenList(): TokenListToken[] { const chainId = useSelector(selectChainId); const isMainnet = isMainnetByChainId(chainId); const isTokenDetectionEnabled = useSelector(selectUseTokenDetection); - const tokenList = useSelector(selectTokenList); + const tokenListArray = useSelector(selectTokenListArray); const shouldUseStaticList = !isTokenDetectionEnabled && isMainnet; return useMemo(() => { if (shouldUseStaticList) { - return NORMALIZED_MAINNET_TOKEN_LIST; + return NORMALIZED_MAINNET_TOKEN_ARRAY; } - return normalizeTokenAddresses(tokenList); - }, [shouldUseStaticList, tokenList]); + return tokenListArray; + }, [shouldUseStaticList, tokenListArray]); } diff --git a/app/components/hooks/DisplayName/useTokenListEntry.test.ts b/app/components/hooks/DisplayName/useTokenListEntry.test.ts index 7aeb4144129..bd576c0bb28 100644 --- a/app/components/hooks/DisplayName/useTokenListEntry.test.ts +++ b/app/components/hooks/DisplayName/useTokenListEntry.test.ts @@ -1,4 +1,4 @@ -import { TokenListMap } from '@metamask/assets-controllers'; +import { TokenListToken } from '@metamask/assets-controllers'; import { NameType } from '../../UI/Name/Name.types'; import { useTokenListEntry } from './useTokenListEntry'; import useTokenList from './useTokenList'; @@ -18,12 +18,13 @@ describe('useTokenListEntry', () => { beforeEach(() => { jest.resetAllMocks(); - useTokenListMock.mockReturnValue({ - [TOKEN_ADDRESS_MOCK.toLowerCase()]: { + useTokenListMock.mockReturnValue([ + { + address: TOKEN_ADDRESS_MOCK.toLowerCase(), name: TOKEN_NAME_MOCK, symbol: TOKEN_SYMBOL_MOCK, }, - } as TokenListMap); + ] as unknown as TokenListToken[]); }); it('returns undefined if no token found', () => { diff --git a/app/components/hooks/DisplayName/useTokenListEntry.ts b/app/components/hooks/DisplayName/useTokenListEntry.ts index 6fecba6303f..6cfe75a87d1 100644 --- a/app/components/hooks/DisplayName/useTokenListEntry.ts +++ b/app/components/hooks/DisplayName/useTokenListEntry.ts @@ -1,3 +1,4 @@ +import { TokenListToken } from '@metamask/assets-controllers'; import { NameType } from '../../UI/Name/Name.types'; import useTokenList from './useTokenList'; @@ -7,7 +8,7 @@ export interface UseTokenListEntriesRequest { } export function useTokenListEntries(requests: UseTokenListEntriesRequest[]) { - const tokenList = useTokenList(); + const tokenListArray = useTokenList(); return requests.map(({ value, type }) => { if (type !== NameType.EthereumAddress) { @@ -16,7 +17,9 @@ export function useTokenListEntries(requests: UseTokenListEntriesRequest[]) { const normalizedValue = value.toLowerCase(); - return tokenList[normalizedValue]; + return tokenListArray.find( + (token: TokenListToken) => token.address === normalizedValue, + ); }); } diff --git a/app/selectors/tokenListController.ts b/app/selectors/tokenListController.ts index 4cdee2c6586..294c3a6ed3e 100644 --- a/app/selectors/tokenListController.ts +++ b/app/selectors/tokenListController.ts @@ -2,6 +2,7 @@ import { createSelector } from 'reselect'; import { TokenListState } from '@metamask/assets-controllers'; import { RootState } from '../reducers'; import { tokenListToArray } from '../util/tokens'; +import { createDeepEqualSelector } from '../selectors/util'; const selectTokenLIstConstrollerState = (state: RootState) => state.engine.backgroundState.TokenListController; @@ -20,7 +21,7 @@ export const selectTokenList = createSelector( * Return token list array from TokenListController. * Can pass directly into useSelector. */ -export const selectTokenListArray = createSelector( +export const selectTokenListArray = createDeepEqualSelector( selectTokenList, tokenListToArray, );