From 1d78bb5a4f69a58d1fe4ea12c9414856a5da18a3 Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Mon, 26 Feb 2024 19:13:58 -0500 Subject: [PATCH] [token-detection-controller] Apply recent `DetectTokensController` updates (#3923) ## Explanation As a preparatory step for fully replacing the extension `DetectTokensController` with the consolidated core repo `TokenDetectionController`, `TokenDetectionController` needs to be updated with changes made to the extension `DetectTokensController` since #1813 was closed. #### Diff of `DetectTokensController` state - [MetaMask/metamask-extension@`5d285f7be5f7be981995dfa725aad97d81cc990a..85cd1c89039e900b452edb704ec37e9ccbd3e76a`#diff-323d0cf464](https://github.com/MetaMask/metamask-extension/compare/5d285f7be5f7be981995dfa725aad97d81cc990a..85cd1c89039e900b452edb704ec37e9ccbd3e76a#diff-323d0cf46498be3850b971474905354ea5ccf7fa13745ad1e6eba59c5b586830) ### Differences from extension `DetectTokensController` - Refactors logic for retrieving `chainId`, `networkClientId` into `this.#getCorrectChainIdAndNetworkClientId` - Uses `getNetworkConfigurationByNetworkClientId` action instead of `getNetworkClientById` to retrieve `chainId`. - If `networkClientId` is not supplied to the method, or it's supplied but `getNetworkConfigurationByNetworkClientId` returns `undefined`, finds `chainId` from `providerConfig`. - `detectTokens` replaces `detectNewTokens` - `detectTokens` accepts options object `{ selectedAddress, networkClientId }` instead of `{ selectedAddress, chainId, networkClientId }`. - Does not throw error if `getBalancesInSingleCall` fails. Also does not exit early -- continues looping. - Passes lists of full `Token` types to `TokensController:addDetectedTokens` instead of objects containing only `{ address, decimals, symbol }`. - `#trackMetaMetricsEvents` is a private method instead of protected. - Passes string literals instead of extension shared constants into `_trackMetaMetricsEvent`. ## References - Partially implements #3916 - Blocking #3918 - Changes adopted from: - https://github.com/MetaMask/metamask-extension/pull/22898 - https://github.com/MetaMask/metamask-extension/pull/22814 - https://github.com/MetaMask/core/pull/3914 - https://github.com/MetaMask/metamask-extension/pull/21437 - Blocking (Followed by) #3938 ## Changelog ### [`@metamask/assets-controllers`](https://github.com/MetaMask/core/pull/3923/files#diff-ee47d03d53776b8dd530799a8047f5e32e36e35765620aeb50b294adc3339fab) ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've highlighted breaking changes using the "BREAKING" category above as appropriate --------- Co-authored-by: jiexi --- packages/assets-controllers/CHANGELOG.md | 6 +- packages/assets-controllers/jest.config.js | 6 +- .../src/TokenDetectionController.test.ts | 96 ++++++-- .../src/TokenDetectionController.ts | 219 ++++++++++-------- 4 files changed, 210 insertions(+), 117 deletions(-) diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index 9c4e2b57c0..2c6d29df14 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -10,17 +10,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - **BREAKING:** Adds `@metamask/accounts-controller` ^8.0.0 and `@metamask/keyring-controller` ^12.0.0 as dependencies and peer dependencies. ([#3775](https://github.com/MetaMask/core/pull/3775/)). -- **BREAKING:** `TokenDetectionController` newly subscribes to the `PreferencesController:stateChange`, `AccountsController:selectedAccountChange`, `KeyringController:lock`, `KeyringController:unlock` events, and allows the `PreferencesController:getState` messenger action. ([#3775](https://github.com/MetaMask/core/pull/3775/)) +- **BREAKING:** `TokenDetectionController` newly subscribes to the `PreferencesController:stateChange`, `AccountsController:selectedAccountChange`, `KeyringController:lock`, `KeyringController:unlock` events, and allows messenger actions `AccountsController:getSelectedAccount`, `NetworkController:findNetworkClientIdByChainId`, `NetworkController:getNetworkConfigurationByNetworkClientId`, `NetworkController:getProviderConfig`, `KeyringController:getState`, `PreferencesController:getState`, `TokenListController:getState`, `TokensController:getState`, `TokensController:addDetectedTokens`. ([#3775](https://github.com/MetaMask/core/pull/3775/)), ([#3923](https://github.com/MetaMask/core/pull/3923/)) - `TokensController` now exports `TokensControllerActions`, `TokensControllerGetStateAction`, `TokensControllerAddDetectedTokensAction`, `TokensControllerEvents`, `TokensControllerStateChangeEvent`. ([#3690](https://github.com/MetaMask/core/pull/3690/)) ### Changed -- **BREAKING:** `TokenDetectionController` is merged with `DetectTokensController` from the `metamask-extension` repo. ([#3775](https://github.com/MetaMask/core/pull/3775/)) +- **BREAKING:** `TokenDetectionController` is merged with `DetectTokensController` from the `metamask-extension` repo. ([#3775](https://github.com/MetaMask/core/pull/3775/), [#3923](https://github.com/MetaMask/core/pull/3923)), ([#3938](https://github.com/MetaMask/core/pull/3938)) - **BREAKING:** `TokenDetectionController` now resets its polling interval to the default value of 3 minutes when token detection is triggered by external controller events `KeyringController:unlock`, `TokenListController:stateChange`, `PreferencesController:stateChange`, `AccountsController:selectedAccountChange`. - **BREAKING:** `TokenDetectionController` now refetches tokens on `NetworkController:networkDidChange` if the `networkClientId` is changed instead of `chainId`. - **BREAKING:** `TokenDetectionController` cannot initiate polling or token detection if `KeyringController` state is locked. + - **BREAKING:** The `detectTokens` method input option `accountAddress` has been renamed to `selectedAddress`. - **BREAKING:** The `detectTokens` method now excludes tokens that are already included in the `TokensController`'s `detectedTokens` list from the batch of incoming tokens it sends to the `TokensController` `addDetectedTokens` method. - **BREAKING:** The constructor for `TokenDetectionController` expects a new required proprerty `trackMetaMetricsEvent`, which defines the callback that is called in the `detectTokens` method. + - The constructor option `selectedAddress` no longer defaults to `''` if omitted. Instead, the correct address is assigned using the `AccountsController:getSelectedAccount` messenger action. - **BREAKING:** In Mainnet, even if the `PreferenceController`'s `useTokenDetection` option is set to false, automatic token detection is performed on the legacy token list (token data from the contract-metadata repo). - **BREAKING:** The `TokensState` type is now defined as a type alias rather than an interface. ([#3690](https://github.com/MetaMask/core/pull/3690/)) - This is breaking because it could affect how this type is used with other types, such as `Json`, which does not support TypeScript interfaces. diff --git a/packages/assets-controllers/jest.config.js b/packages/assets-controllers/jest.config.js index 761ed600dd..ecc2975e25 100644 --- a/packages/assets-controllers/jest.config.js +++ b/packages/assets-controllers/jest.config.js @@ -17,9 +17,9 @@ module.exports = merge(baseConfig, { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 88.8, - functions: 96.71, - lines: 97.34, + branches: 88.58, + functions: 96.98, + lines: 97.35, statements: 97.4, }, }, diff --git a/packages/assets-controllers/src/TokenDetectionController.test.ts b/packages/assets-controllers/src/TokenDetectionController.test.ts index f2e8f653f1..81d82ce848 100644 --- a/packages/assets-controllers/src/TokenDetectionController.test.ts +++ b/packages/assets-controllers/src/TokenDetectionController.test.ts @@ -8,12 +8,14 @@ import { } from '@metamask/controller-utils'; import type { InternalAccount } from '@metamask/keyring-api'; import type { KeyringControllerState } from '@metamask/keyring-controller'; -import { - defaultState as defaultNetworkState, - type NetworkState, - type NetworkConfiguration, - type NetworkController, +import type { + NetworkState, + NetworkConfiguration, + NetworkController, + ProviderConfig, + NetworkClientId, } from '@metamask/network-controller'; +import { defaultState as defaultNetworkState } from '@metamask/network-controller'; import { getDefaultPreferencesState, type PreferencesState, @@ -138,8 +140,11 @@ function buildTokenDetectionControllerMessenger( return controllerMessenger.getRestricted({ name: controllerName, allowedActions: [ + 'AccountsController:getSelectedAccount', 'KeyringController:getState', + 'NetworkController:findNetworkClientIdByChainId', 'NetworkController:getNetworkConfigurationByNetworkClientId', + 'NetworkController:getProviderConfig', 'TokensController:getState', 'TokensController:addDetectedTokens', 'TokenListController:getState', @@ -338,7 +343,16 @@ describe('TokenDetectionController', () => { selectedAddress, }, }, - async ({ controller, mockTokenListGetState, callActionSpy }) => { + async ({ + controller, + mockGetProviderConfig, + mockTokenListGetState, + callActionSpy, + }) => { + mockGetProviderConfig({ + chainId: '0x89', + } as unknown as ProviderConfig); + mockTokenListGetState({ ...getDefaultTokenListState(), tokensChainsCache: { @@ -1748,19 +1762,19 @@ describe('TokenDetectionController', () => { await advanceTime({ clock, duration: 0 }); expect(spy.mock.calls).toMatchObject([ - [{ networkClientId: 'mainnet', accountAddress: '0x1' }], - [{ networkClientId: 'sepolia', accountAddress: '0xdeadbeef' }], - [{ networkClientId: 'goerli', accountAddress: '0x3' }], + [{ networkClientId: 'mainnet', selectedAddress: '0x1' }], + [{ networkClientId: 'sepolia', selectedAddress: '0xdeadbeef' }], + [{ networkClientId: 'goerli', selectedAddress: '0x3' }], ]); await advanceTime({ clock, duration: DEFAULT_INTERVAL }); expect(spy.mock.calls).toMatchObject([ - [{ networkClientId: 'mainnet', accountAddress: '0x1' }], - [{ networkClientId: 'sepolia', accountAddress: '0xdeadbeef' }], - [{ networkClientId: 'goerli', accountAddress: '0x3' }], - [{ networkClientId: 'mainnet', accountAddress: '0x1' }], - [{ networkClientId: 'sepolia', accountAddress: '0xdeadbeef' }], - [{ networkClientId: 'goerli', accountAddress: '0x3' }], + [{ networkClientId: 'mainnet', selectedAddress: '0x1' }], + [{ networkClientId: 'sepolia', selectedAddress: '0xdeadbeef' }], + [{ networkClientId: 'goerli', selectedAddress: '0x3' }], + [{ networkClientId: 'mainnet', selectedAddress: '0x1' }], + [{ networkClientId: 'sepolia', selectedAddress: '0xdeadbeef' }], + [{ networkClientId: 'goerli', selectedAddress: '0x3' }], ]); }, ); @@ -1793,7 +1807,7 @@ describe('TokenDetectionController', () => { }); await controller.detectTokens({ networkClientId: NetworkType.goerli, - accountAddress: selectedAddress, + selectedAddress, }); expect(callActionSpy).not.toHaveBeenCalledWith( 'TokensController:addDetectedTokens', @@ -1833,7 +1847,7 @@ describe('TokenDetectionController', () => { }); await controller.detectTokens({ networkClientId: NetworkType.mainnet, - accountAddress: selectedAddress, + selectedAddress, }); expect(callActionSpy).toHaveBeenLastCalledWith( 'TokensController:addDetectedTokens', @@ -1896,7 +1910,7 @@ describe('TokenDetectionController', () => { await controller.detectTokens({ networkClientId: NetworkType.mainnet, - accountAddress: selectedAddress, + selectedAddress, }); expect(callActionSpy).toHaveBeenCalledWith( @@ -1951,7 +1965,7 @@ describe('TokenDetectionController', () => { await controller.detectTokens({ networkClientId: NetworkType.mainnet, - accountAddress: selectedAddress, + selectedAddress, }); expect(mockTrackMetaMetricsEvent).toHaveBeenCalledWith({ @@ -1983,10 +1997,14 @@ function getTokensPath(chainId: Hex) { type WithControllerCallback = ({ controller, + mockGetSelectedAccount, mockKeyringGetState, mockTokensGetState, mockTokenListGetState, mockPreferencesGetState, + mockFindNetworkClientIdByChainId, + mockGetNetworkConfigurationByNetworkClientId, + mockGetProviderConfig, callActionSpy, triggerKeyringUnlock, triggerKeyringLock, @@ -1996,13 +2014,18 @@ type WithControllerCallback = ({ triggerNetworkDidChange, }: { controller: TokenDetectionController; + mockGetSelectedAccount: (address: string) => void; mockKeyringGetState: (state: KeyringControllerState) => void; mockTokensGetState: (state: TokensState) => void; mockTokenListGetState: (state: TokenListState) => void; mockPreferencesGetState: (state: PreferencesState) => void; + mockFindNetworkClientIdByChainId: ( + handler: (chainId: Hex) => NetworkClientId, + ) => void; mockGetNetworkConfigurationByNetworkClientId: ( handler: (networkClientId: string) => NetworkConfiguration, ) => void; + mockGetProviderConfig: (config: ProviderConfig) => void; callActionSpy: jest.SpyInstance; triggerKeyringUnlock: () => void; triggerKeyringLock: () => void; @@ -2039,6 +2062,13 @@ async function withController( const controllerMessenger = messenger ?? new ControllerMessenger(); + const mockGetSelectedAccount = jest.fn(); + controllerMessenger.registerActionHandler( + 'AccountsController:getSelectedAccount', + mockGetSelectedAccount.mockReturnValue({ + address: '0x1', + } as InternalAccount), + ); const mockKeyringState = jest.fn(); controllerMessenger.registerActionHandler( 'KeyringController:getState', @@ -2046,6 +2076,11 @@ async function withController( isUnlocked: isKeyringUnlocked ?? true, } as KeyringControllerState), ); + const mockFindNetworkClientIdByChainId = jest.fn(); + controllerMessenger.registerActionHandler( + 'NetworkController:findNetworkClientIdByChainId', + mockFindNetworkClientIdByChainId.mockReturnValue(NetworkType.mainnet), + ); const mockGetNetworkConfigurationByNetworkClientId = jest.fn< ReturnType, Parameters @@ -2053,11 +2088,19 @@ async function withController( controllerMessenger.registerActionHandler( 'NetworkController:getNetworkConfigurationByNetworkClientId', mockGetNetworkConfigurationByNetworkClientId.mockImplementation( - (networkClientId: string) => { + (networkClientId: NetworkClientId) => { return mockNetworkConfigurations[networkClientId]; }, ), ); + const mockGetProviderConfig = jest.fn(); + controllerMessenger.registerActionHandler( + 'NetworkController:getProviderConfig', + mockGetProviderConfig.mockReturnValue({ + type: NetworkType.mainnet, + chainId: '0x1', + } as unknown as ProviderConfig), + ); const mockTokensState = jest.fn(); controllerMessenger.registerActionHandler( 'TokensController:getState', @@ -2096,6 +2139,9 @@ async function withController( try { return await fn({ controller, + mockGetSelectedAccount: (address: string) => { + mockGetSelectedAccount.mockReturnValue({ address } as InternalAccount); + }, mockKeyringGetState: (state: KeyringControllerState) => { mockKeyringState.mockReturnValue(state); }, @@ -2108,13 +2154,21 @@ async function withController( mockTokenListGetState: (state: TokenListState) => { mockTokenListState.mockReturnValue(state); }, + mockFindNetworkClientIdByChainId: ( + handler: (chainId: Hex) => NetworkClientId, + ) => { + mockFindNetworkClientIdByChainId.mockImplementation(handler); + }, mockGetNetworkConfigurationByNetworkClientId: ( - handler: (networkClientId: string) => NetworkConfiguration, + handler: (networkClientId: NetworkClientId) => NetworkConfiguration, ) => { mockGetNetworkConfigurationByNetworkClientId.mockImplementation( handler, ); }, + mockGetProviderConfig: (config: ProviderConfig) => { + mockGetProviderConfig.mockReturnValue(config); + }, callActionSpy, triggerKeyringUnlock: () => { controllerMessenger.publish('KeyringController:unlock'); diff --git a/packages/assets-controllers/src/TokenDetectionController.ts b/packages/assets-controllers/src/TokenDetectionController.ts index adb7503a3c..d4d1cd8d2c 100644 --- a/packages/assets-controllers/src/TokenDetectionController.ts +++ b/packages/assets-controllers/src/TokenDetectionController.ts @@ -1,15 +1,14 @@ -import type { AccountsControllerSelectedAccountChangeEvent } from '@metamask/accounts-controller'; +import type { + AccountsControllerGetSelectedAccountAction, + AccountsControllerSelectedAccountChangeEvent, +} from '@metamask/accounts-controller'; import type { RestrictedControllerMessenger, ControllerGetStateAction, ControllerStateChangeEvent, } from '@metamask/base-controller'; import contractMap from '@metamask/contract-metadata'; -import { - ChainId, - safelyExecute, - toChecksumHexAddress, -} from '@metamask/controller-utils'; +import { ChainId, safelyExecute } from '@metamask/controller-utils'; import type { KeyringControllerGetStateAction, KeyringControllerLockEvent, @@ -17,8 +16,10 @@ import type { } from '@metamask/keyring-controller'; import type { NetworkClientId, - NetworkControllerNetworkDidChangeEvent, + NetworkControllerFindNetworkClientIdByChainIdAction, NetworkControllerGetNetworkConfigurationByNetworkClientId, + NetworkControllerGetProviderConfigAction, + NetworkControllerNetworkDidChangeEvent, } from '@metamask/network-controller'; import { StaticIntervalPollingController } from '@metamask/polling-controller'; import type { @@ -43,13 +44,23 @@ import type { const DEFAULT_INTERVAL = 180000; /** - * Finds a case insensitive match in an array of strings - * @param source - An array of strings to search. - * @param target - The target string to search for. - * @returns The first match that is found. + * Compare 2 given strings and return boolean + * eg: "foo" and "FOO" => true + * eg: "foo" and "bar" => false + * eg: "foo" and 123 => false + * + * @param value1 - first string to compare + * @param value2 - first string to compare + * @returns true if 2 strings are identical when they are lowercase */ -function findCaseInsensitiveMatch(source: string[], target: string) { - return source.find((e: string) => e.toLowerCase() === target.toLowerCase()); +export function isEqualCaseInsensitive( + value1: string, + value2: string, +): boolean { + if (typeof value1 !== 'string' || typeof value2 !== 'string') { + return false; + } + return value1.toLowerCase() === value2.toLowerCase(); } type LegacyToken = Omit< @@ -95,7 +106,10 @@ export type TokenDetectionControllerActions = TokenDetectionControllerGetStateAction; export type AllowedActions = + | AccountsControllerGetSelectedAccountAction + | NetworkControllerFindNetworkClientIdByChainIdAction | NetworkControllerGetNetworkConfigurationByNetworkClientId + | NetworkControllerGetProviderConfigAction | GetTokenListState | KeyringControllerGetStateAction | PreferencesControllerGetStateAction @@ -182,7 +196,7 @@ export class TokenDetectionController extends StaticIntervalPollingController< */ constructor({ networkClientId, - selectedAddress = '', + selectedAddress, interval = DEFAULT_INTERVAL, disabled = true, getBalancesInSingleCall, @@ -216,8 +230,13 @@ export class TokenDetectionController extends StaticIntervalPollingController< this.setIntervalLength(interval); this.#networkClientId = networkClientId; - this.#selectedAddress = selectedAddress; - this.#chainId = this.#getCorrectChainId(networkClientId); + this.#selectedAddress = + selectedAddress ?? + this.messagingSystem.call('AccountsController:getSelectedAccount') + .address; + const { chainId } = + this.#getCorrectChainIdAndNetworkClientId(networkClientId); + this.#chainId = chainId; const { useTokenDetection: defaultUseTokenDetection } = this.messagingSystem.call('PreferencesController:getState'); @@ -308,7 +327,9 @@ export class TokenDetectionController extends StaticIntervalPollingController< const isNetworkClientIdChanged = this.#networkClientId !== selectedNetworkClientId; - const newChainId = this.#getCorrectChainId(selectedNetworkClientId); + const { chainId: newChainId } = + this.#getCorrectChainIdAndNetworkClientId(selectedNetworkClientId); + this.#chainId = newChainId; this.#isDetectionEnabledForNetwork = isTokenDetectionSupportedForNetwork(newChainId); @@ -381,13 +402,33 @@ export class TokenDetectionController extends StaticIntervalPollingController< }, this.getIntervalLength()); } - #getCorrectChainId(networkClientId?: NetworkClientId) { - const { chainId } = - this.messagingSystem.call( + #getCorrectChainIdAndNetworkClientId(networkClientId?: NetworkClientId): { + chainId: Hex; + networkClientId: NetworkClientId; + } { + if (networkClientId) { + const networkConfiguration = this.messagingSystem.call( 'NetworkController:getNetworkConfigurationByNetworkClientId', - networkClientId ?? this.#networkClientId, - ) ?? {}; - return chainId ?? this.#chainId; + networkClientId, + ); + if (networkConfiguration) { + return { + chainId: networkConfiguration.chainId, + networkClientId, + }; + } + } + const { chainId } = this.messagingSystem.call( + 'NetworkController:getProviderConfig', + ); + const newNetworkClientId = this.messagingSystem.call( + 'NetworkController:findNetworkClientIdByChainId', + chainId, + ); + return { + chainId, + networkClientId: newNetworkClientId, + }; } async _executePoll( @@ -399,7 +440,7 @@ export class TokenDetectionController extends StaticIntervalPollingController< } await this.detectTokens({ networkClientId, - accountAddress: options.address, + selectedAddress: options.address, }); } @@ -417,7 +458,7 @@ export class TokenDetectionController extends StaticIntervalPollingController< }: { selectedAddress?: string; networkClientId?: string } = {}) { await this.detectTokens({ networkClientId, - accountAddress: selectedAddress, + selectedAddress, }); this.setIntervalLength(DEFAULT_INTERVAL); } @@ -428,114 +469,110 @@ export class TokenDetectionController extends StaticIntervalPollingController< * * @param options - Options for token detection. * @param options.networkClientId - The ID of the network client to use. - * @param options.accountAddress - the selectedAddress against which to detect for token balances. + * @param options.selectedAddress - the selectedAddress against which to detect for token balances. */ async detectTokens({ networkClientId, - accountAddress, + selectedAddress, }: { networkClientId?: NetworkClientId; - accountAddress?: string; + selectedAddress?: string; } = {}): Promise { - if (!this.isActive || !this.#isDetectionEnabledForNetwork) { + if (!this.isActive) { + return; + } + + const addressAgainstWhichToDetect = + selectedAddress ?? this.#selectedAddress; + const { + chainId: chainIdAgainstWhichToDetect, + networkClientId: networkClientIdAgainstWhichToDetect, + } = this.#getCorrectChainIdAndNetworkClientId(networkClientId); + + if (!isTokenDetectionSupportedForNetwork(chainIdAgainstWhichToDetect)) { return; } - const selectedAddress = accountAddress ?? this.#selectedAddress; - const chainId = this.#getCorrectChainId(networkClientId); if ( !this.#isDetectionEnabledFromPreferences && - chainId !== ChainId.mainnet + chainIdAgainstWhichToDetect !== ChainId.mainnet ) { return; } const isTokenDetectionInactiveInMainnet = - !this.#isDetectionEnabledFromPreferences && chainId === ChainId.mainnet; + !this.#isDetectionEnabledFromPreferences && + chainIdAgainstWhichToDetect === ChainId.mainnet; const { tokensChainsCache } = this.messagingSystem.call( 'TokenListController:getState', ); - const tokenList = tokensChainsCache[chainId]?.data ?? {}; - + const tokenList = + tokensChainsCache[chainIdAgainstWhichToDetect]?.data ?? {}; const tokenListUsed = isTokenDetectionInactiveInMainnet ? STATIC_MAINNET_TOKEN_LIST : tokenList; const { allTokens, allDetectedTokens, allIgnoredTokens } = this.messagingSystem.call('TokensController:getState'); - const tokens = allTokens[chainId]?.[selectedAddress] ?? []; - const detectedTokens = allDetectedTokens[chainId]?.[selectedAddress] ?? []; - const ignoredTokens = allIgnoredTokens[chainId]?.[selectedAddress] ?? []; - + const [tokensAddresses, detectedTokensAddresses, ignoredTokensAddresses] = [ + allTokens, + allDetectedTokens, + allIgnoredTokens, + ].map((tokens) => + ( + tokens[chainIdAgainstWhichToDetect]?.[addressAgainstWhichToDetect] ?? [] + ).map((value) => (typeof value === 'string' ? value : value.address)), + ); const tokensToDetect: string[] = []; for (const tokenAddress of Object.keys(tokenListUsed)) { if ( - !findCaseInsensitiveMatch( - tokens.map(({ address }) => address), - tokenAddress, - ) && - !findCaseInsensitiveMatch( - detectedTokens.map(({ address }) => address), - tokenAddress, + [ + tokensAddresses, + detectedTokensAddresses, + ignoredTokensAddresses, + ].every( + (addresses) => + !addresses.find((address) => + isEqualCaseInsensitive(address, tokenAddress), + ), ) ) { tokensToDetect.push(tokenAddress); } } - const sliceOfTokensToDetect = []; - sliceOfTokensToDetect[0] = tokensToDetect.slice(0, 1000); - sliceOfTokensToDetect[1] = tokensToDetect.slice( + const slicesOfTokensToDetect = []; + slicesOfTokensToDetect[0] = tokensToDetect.slice(0, 1000); + slicesOfTokensToDetect[1] = tokensToDetect.slice( 1000, tokensToDetect.length - 1, ); - - /* istanbul ignore else */ - if (!selectedAddress) { - return; - } - - for (const tokensSlice of sliceOfTokensToDetect) { + for (const tokensSlice of slicesOfTokensToDetect) { if (tokensSlice.length === 0) { break; } await safelyExecute(async () => { const balances = await this.#getBalancesInSingleCall( - selectedAddress, + addressAgainstWhichToDetect, tokensSlice, + networkClientIdAgainstWhichToDetect, ); - const tokensToAdd: Token[] = []; + const tokensWithBalance: Token[] = []; const eventTokensDetails: string[] = []; - let ignored; - for (const tokenAddress of Object.keys(balances)) { - if (ignoredTokens.length) { - ignored = ignoredTokens.find( - (ignoredTokenAddress) => - ignoredTokenAddress === toChecksumHexAddress(tokenAddress), - ); - } - const caseInsensitiveTokenKey = - findCaseInsensitiveMatch( - Object.keys(tokenListUsed), - tokenAddress, - ) ?? ''; - - if (ignored === undefined) { - const { decimals, symbol, aggregators, iconUrl, name } = - tokenListUsed[caseInsensitiveTokenKey]; - eventTokensDetails.push(`${symbol} - ${tokenAddress}`); - tokensToAdd.push({ - address: tokenAddress, - decimals, - symbol, - aggregators, - image: iconUrl, - isERC721: false, - name, - }); - } + for (const nonZeroTokenAddress of Object.keys(balances)) { + const { decimals, symbol, aggregators, iconUrl, name } = + tokenListUsed[nonZeroTokenAddress]; + eventTokensDetails.push(`${symbol} - ${nonZeroTokenAddress}`); + tokensWithBalance.push({ + address: nonZeroTokenAddress, + decimals, + symbol, + aggregators, + image: iconUrl, + isERC721: false, + name, + }); } - - if (tokensToAdd.length) { + if (tokensWithBalance.length) { this.#trackMetaMetricsEvent({ event: 'Token Detected', category: 'Wallet', @@ -547,10 +584,10 @@ export class TokenDetectionController extends StaticIntervalPollingController< }); await this.messagingSystem.call( 'TokensController:addDetectedTokens', - tokensToAdd, + tokensWithBalance, { - selectedAddress, - chainId, + selectedAddress: addressAgainstWhichToDetect, + chainId: chainIdAgainstWhichToDetect, }, ); }