From 34bf8f08fc48aacc12ee6a4478ad04f92032de53 Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Mon, 4 Mar 2024 12:21:29 -0500 Subject: [PATCH] [token-detection-controller] Refactor `detectTokens` method (#3938) ## Explanation Adds refactors and cosmetic, typing fixes to #3923. - Extracts three methods from `TokenDetectionController`'s `detectTokens` method: - `#getCorrectChainIdAndNetworkClientId` - `#getTokenListAndSlicesOfTokensToDetect` - `#addDetectedTokens` - Maintains distinction between class fields `#selectedAddress`, `#networkClientId` and corresponding parameters used in `detectTokens` and its helper methods, so that `detectTokens` method can be used independently of polling/passive detection. - [Refactor `#getCorrectChainIdAndNetworkClientId`](https://github.com/MetaMask/core/pull/3938/commits/c75fb3bd37ddf2715ed250bdd2c2331a54008b72) to remove `findNetworkClientIdByChainId` which might return inconsistent/unexpected results, and replace it with `getState`, `getNetworkClientById` - [Add missing method return types](https://github.com/MetaMask/core/pull/3938/commits/5c2e8879fd4dea4a99125d0ffe9a29b4aef0cd21) - [Type networkClientId as `NetworkClientId`](https://github.com/MetaMask/core/pull/3938/commits/05be5d2e842c516217871f62cfd7f66de0590fa1) - [Fix excess properties from legacy token list, define `LegacyToken`, `TokenDetectionMap`](https://github.com/MetaMask/core/pull/3938/commits/ea4c5b883b4663a3369d0f76222abbe55baaca55) - Removes `#chainId` class field. ## References - Closes https://github.com/MetaMask/core/issues/1614 - Fixes https://github.com/MetaMask/core/issues/3661 - Blocked by (Follows from) #3923 - Blocking #3916 - Blocking #3918 ## Changelog ## 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: Brian Bergeron Co-authored-by: Elliot Winkler --- packages/assets-controllers/CHANGELOG.md | 7 +- packages/assets-controllers/jest.config.js | 8 +- .../src/TokenDetectionController.test.ts | 130 +++++----- .../src/TokenDetectionController.ts | 245 ++++++++++-------- 4 files changed, 203 insertions(+), 187 deletions(-) diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index 2c6d29df14a..916278a4258 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -10,7 +10,7 @@ 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 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/)) +- **BREAKING:** `TokenDetectionController` newly subscribes to the `PreferencesController:stateChange`, `AccountsController:selectedAccountChange`, `KeyringController:lock`, `KeyringController:unlock` events, and allows messenger actions `AccountsController:getSelectedAccount`, `NetworkController:getNetworkClientById`, `NetworkController:getNetworkConfigurationByNetworkClientId`, `NetworkController:getState`, `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/), [#3938](https://github.com/MetaMask/core/pull/3938)) - `TokensController` now exports `TokensControllerActions`, `TokensControllerGetStateAction`, `TokensControllerAddDetectedTokensAction`, `TokensControllerEvents`, `TokensControllerStateChangeEvent`. ([#3690](https://github.com/MetaMask/core/pull/3690/)) ### Changed @@ -25,11 +25,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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. + - `TokensState` now extends the `Record` types, and it has an index signature of `string`, making it compatible with the `BaseControllerV2` state object constraint of `Record`. + - The `detectTokens` method can now process an arbitrary number of tokens in batches of 1000. ([#3938](https://github.com/MetaMask/core/pull/3938)) ### Removed -- **BREAKING:** `TokenDetectionController` constructor no longer accepts options `onPreferencesStateChange`, `getPreferencesState`, `getTokensState`, `addDetectedTokens`. ([#3690](https://github.com/MetaMask/core/pull/3690/), [#3775](https://github.com/MetaMask/core/pull/3775/)) +- **BREAKING:** `TokenDetectionController` constructor no longer accepts options `networkClientId`, `onPreferencesStateChange`, `getPreferencesState`, `getTokensState`, `addDetectedTokens`. ([#3690](https://github.com/MetaMask/core/pull/3690/), [#3775](https://github.com/MetaMask/core/pull/3775/), [#3938](https://github.com/MetaMask/core/pull/3938)) - **BREAKING:** `TokenDetectionController` no longer allows the `NetworkController:stateChange` event. The `NetworkController:networkDidChange` event can be used instead. ([#3775](https://github.com/MetaMask/core/pull/3775/)) - **BREAKING:** `TokensController` constructor no longer accepts options `onPreferencesStateChange`, `onNetworkDidChange`, `onTokenListStateChange`, `getNetworkClientById`. ([#3690](https://github.com/MetaMask/core/pull/3690/)) - **BREAKING:** `TokenBalancesController` constructor no longer accepts options `onTokensStateChange`, `getSelectedAddress`. ([#3690](https://github.com/MetaMask/core/pull/3690/)) diff --git a/packages/assets-controllers/jest.config.js b/packages/assets-controllers/jest.config.js index ecc2975e254..e927d0100dd 100644 --- a/packages/assets-controllers/jest.config.js +++ b/packages/assets-controllers/jest.config.js @@ -17,10 +17,10 @@ module.exports = merge(baseConfig, { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 88.58, - functions: 96.98, - lines: 97.35, - statements: 97.4, + branches: 88.67, + functions: 97, + lines: 97.36, + statements: 97.41, }, }, diff --git a/packages/assets-controllers/src/TokenDetectionController.test.ts b/packages/assets-controllers/src/TokenDetectionController.test.ts index 81d82ce8484..b30e755c876 100644 --- a/packages/assets-controllers/src/TokenDetectionController.test.ts +++ b/packages/assets-controllers/src/TokenDetectionController.test.ts @@ -12,10 +12,11 @@ import type { NetworkState, NetworkConfiguration, NetworkController, - ProviderConfig, NetworkClientId, } from '@metamask/network-controller'; import { defaultState as defaultNetworkState } from '@metamask/network-controller'; +import type { AutoManagedNetworkClient } from '@metamask/network-controller/src/create-auto-managed-network-client'; +import type { CustomNetworkClientConfiguration } from '@metamask/network-controller/src/types'; import { getDefaultPreferencesState, type PreferencesState, @@ -142,9 +143,9 @@ function buildTokenDetectionControllerMessenger( allowedActions: [ 'AccountsController:getSelectedAccount', 'KeyringController:getState', - 'NetworkController:findNetworkClientIdByChainId', + 'NetworkController:getNetworkClientById', 'NetworkController:getNetworkConfigurationByNetworkClientId', - 'NetworkController:getProviderConfig', + 'NetworkController:getState', 'TokensController:getState', 'TokensController:addDetectedTokens', 'TokenListController:getState', @@ -271,10 +272,13 @@ describe('TokenDetectionController', () => { { options: { getBalancesInSingleCall: mockGetBalancesInSingleCall, - networkClientId: NetworkType.goerli, }, }, - async ({ controller }) => { + async ({ controller, mockNetworkState }) => { + mockNetworkState({ + ...defaultNetworkState, + selectedNetworkClientId: NetworkType.goerli, + }); await controller.start(); expect(mockGetBalancesInSingleCall).not.toHaveBeenCalled(); @@ -291,7 +295,6 @@ describe('TokenDetectionController', () => { { options: { getBalancesInSingleCall: mockGetBalancesInSingleCall, - networkClientId: NetworkType.mainnet, selectedAddress, }, }, @@ -339,19 +342,26 @@ describe('TokenDetectionController', () => { { options: { getBalancesInSingleCall: mockGetBalancesInSingleCall, - networkClientId: 'polygon', selectedAddress, }, }, async ({ controller, - mockGetProviderConfig, mockTokenListGetState, + mockNetworkState, + mockGetNetworkClientById, callActionSpy, }) => { - mockGetProviderConfig({ - chainId: '0x89', - } as unknown as ProviderConfig); + mockNetworkState({ + ...defaultNetworkState, + selectedNetworkClientId: 'polygon', + }); + mockGetNetworkClientById( + () => + ({ + configuration: { chainId: '0x89' }, + } as unknown as AutoManagedNetworkClient), + ); mockTokenListGetState({ ...getDefaultTokenListState(), @@ -399,7 +409,6 @@ describe('TokenDetectionController', () => { options: { getBalancesInSingleCall: mockGetBalancesInSingleCall, interval, - networkClientId: NetworkType.mainnet, selectedAddress, }, }, @@ -459,7 +468,6 @@ describe('TokenDetectionController', () => { { options: { getBalancesInSingleCall: mockGetBalancesInSingleCall, - networkClientId: NetworkType.mainnet, selectedAddress, }, }, @@ -510,8 +518,6 @@ describe('TokenDetectionController', () => { { options: { getBalancesInSingleCall: mockGetBalancesInSingleCall, - networkClientId: NetworkType.mainnet, - selectedAddress: '', }, }, async ({ controller, mockTokenListGetState, callActionSpy }) => { @@ -569,7 +575,6 @@ describe('TokenDetectionController', () => { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - networkClientId: NetworkType.mainnet, selectedAddress: firstSelectedAddress, }, }, @@ -625,7 +630,6 @@ describe('TokenDetectionController', () => { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - networkClientId: NetworkType.mainnet, selectedAddress, }, }, @@ -680,7 +684,6 @@ describe('TokenDetectionController', () => { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - networkClientId: NetworkType.mainnet, selectedAddress: firstSelectedAddress, }, isKeyringUnlocked: false, @@ -738,7 +741,6 @@ describe('TokenDetectionController', () => { options: { disabled: true, getBalancesInSingleCall: mockGetBalancesInSingleCall, - networkClientId: NetworkType.mainnet, selectedAddress: firstSelectedAddress, }, }, @@ -805,7 +807,6 @@ describe('TokenDetectionController', () => { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - networkClientId: NetworkType.mainnet, selectedAddress: firstSelectedAddress, }, }, @@ -863,7 +864,6 @@ describe('TokenDetectionController', () => { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - networkClientId: NetworkType.mainnet, selectedAddress, }, }, @@ -931,7 +931,6 @@ describe('TokenDetectionController', () => { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - networkClientId: NetworkType.mainnet, selectedAddress: firstSelectedAddress, }, }, @@ -979,7 +978,6 @@ describe('TokenDetectionController', () => { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - networkClientId: NetworkType.mainnet, selectedAddress, }, }, @@ -1031,7 +1029,6 @@ describe('TokenDetectionController', () => { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - networkClientId: NetworkType.mainnet, selectedAddress: firstSelectedAddress, }, isKeyringUnlocked: false, @@ -1080,7 +1077,6 @@ describe('TokenDetectionController', () => { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - networkClientId: NetworkType.mainnet, selectedAddress, }, isKeyringUnlocked: false, @@ -1142,7 +1138,6 @@ describe('TokenDetectionController', () => { options: { disabled: true, getBalancesInSingleCall: mockGetBalancesInSingleCall, - networkClientId: NetworkType.mainnet, selectedAddress: firstSelectedAddress, }, }, @@ -1190,7 +1185,6 @@ describe('TokenDetectionController', () => { options: { disabled: true, getBalancesInSingleCall: mockGetBalancesInSingleCall, - networkClientId: NetworkType.mainnet, selectedAddress, }, }, @@ -1258,7 +1252,6 @@ describe('TokenDetectionController', () => { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - networkClientId: NetworkType.mainnet, selectedAddress, }, }, @@ -1315,7 +1308,6 @@ describe('TokenDetectionController', () => { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - networkClientId: NetworkType.mainnet, selectedAddress, }, }, @@ -1367,7 +1359,6 @@ describe('TokenDetectionController', () => { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - networkClientId: NetworkType.mainnet, selectedAddress, }, }, @@ -1415,7 +1406,6 @@ describe('TokenDetectionController', () => { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - networkClientId: NetworkType.mainnet, selectedAddress, }, isKeyringUnlocked: false, @@ -1466,7 +1456,6 @@ describe('TokenDetectionController', () => { options: { disabled: true, getBalancesInSingleCall: mockGetBalancesInSingleCall, - networkClientId: NetworkType.mainnet, selectedAddress, }, }, @@ -1526,7 +1515,6 @@ describe('TokenDetectionController', () => { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - networkClientId: NetworkType.mainnet, selectedAddress, }, }, @@ -1583,7 +1571,6 @@ describe('TokenDetectionController', () => { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - networkClientId: NetworkType.mainnet, selectedAddress, }, }, @@ -1619,7 +1606,6 @@ describe('TokenDetectionController', () => { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - networkClientId: NetworkType.mainnet, selectedAddress, }, isKeyringUnlocked: false, @@ -1668,7 +1654,6 @@ describe('TokenDetectionController', () => { options: { disabled: true, getBalancesInSingleCall: mockGetBalancesInSingleCall, - networkClientId: NetworkType.mainnet, selectedAddress, }, }, @@ -1725,7 +1710,6 @@ describe('TokenDetectionController', () => { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - networkClientId: NetworkType.mainnet, selectedAddress, }, }, @@ -1792,15 +1776,19 @@ describe('TokenDetectionController', () => { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - networkClientId: NetworkType.goerli, selectedAddress, }, }, async ({ controller, + mockNetworkState, triggerPreferencesStateChange, callActionSpy, }) => { + mockNetworkState({ + ...defaultNetworkState, + selectedNetworkClientId: NetworkType.goerli, + }); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), useTokenDetection: false, @@ -1832,7 +1820,6 @@ describe('TokenDetectionController', () => { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - networkClientId: NetworkType.mainnet, selectedAddress, }, }, @@ -1852,17 +1839,12 @@ describe('TokenDetectionController', () => { expect(callActionSpy).toHaveBeenLastCalledWith( 'TokensController:addDetectedTokens', Object.values(STATIC_MAINNET_TOKEN_LIST).map((token) => { - const newToken = { - ...token, + const { iconUrl, ...tokenMetadata } = token; + return { + ...tokenMetadata, image: token.iconUrl, isERC721: false, }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - delete (newToken as any).erc20; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - delete (newToken as any).erc721; - delete newToken.iconUrl; - return newToken; }), { selectedAddress, @@ -1883,7 +1865,6 @@ describe('TokenDetectionController', () => { options: { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, - networkClientId: NetworkType.mainnet, selectedAddress, }, }, @@ -1938,7 +1919,6 @@ describe('TokenDetectionController', () => { disabled: false, getBalancesInSingleCall: mockGetBalancesInSingleCall, trackMetaMetricsEvent: mockTrackMetaMetricsEvent, - networkClientId: NetworkType.mainnet, selectedAddress, }, }, @@ -2002,9 +1982,9 @@ type WithControllerCallback = ({ mockTokensGetState, mockTokenListGetState, mockPreferencesGetState, - mockFindNetworkClientIdByChainId, + mockGetNetworkClientById, mockGetNetworkConfigurationByNetworkClientId, - mockGetProviderConfig, + mockNetworkState, callActionSpy, triggerKeyringUnlock, triggerKeyringLock, @@ -2019,13 +1999,15 @@ type WithControllerCallback = ({ mockTokensGetState: (state: TokensState) => void; mockTokenListGetState: (state: TokenListState) => void; mockPreferencesGetState: (state: PreferencesState) => void; - mockFindNetworkClientIdByChainId: ( - handler: (chainId: Hex) => NetworkClientId, + mockGetNetworkClientById: ( + handler: ( + networkClientId: NetworkClientId, + ) => AutoManagedNetworkClient, ) => void; mockGetNetworkConfigurationByNetworkClientId: ( - handler: (networkClientId: string) => NetworkConfiguration, + handler: (networkClientId: NetworkClientId) => NetworkConfiguration, ) => void; - mockGetProviderConfig: (config: ProviderConfig) => void; + mockNetworkState: (state: NetworkState) => void; callActionSpy: jest.SpyInstance; triggerKeyringUnlock: () => void; triggerKeyringLock: () => void; @@ -2076,10 +2058,20 @@ async function withController( isUnlocked: isKeyringUnlocked ?? true, } as KeyringControllerState), ); - const mockFindNetworkClientIdByChainId = jest.fn(); + const mockGetNetworkClientById = jest.fn< + ReturnType, + Parameters + >(); controllerMessenger.registerActionHandler( - 'NetworkController:findNetworkClientIdByChainId', - mockFindNetworkClientIdByChainId.mockReturnValue(NetworkType.mainnet), + 'NetworkController:getNetworkClientById', + mockGetNetworkClientById.mockImplementation(() => { + return { + configuration: { chainId: '0x1' }, + provider: {}, + destroy: {}, + blockTracker: {}, + } as unknown as AutoManagedNetworkClient; + }), ); const mockGetNetworkConfigurationByNetworkClientId = jest.fn< ReturnType, @@ -2093,13 +2085,10 @@ async function withController( }, ), ); - const mockGetProviderConfig = jest.fn(); + const mockNetworkState = jest.fn(); controllerMessenger.registerActionHandler( - 'NetworkController:getProviderConfig', - mockGetProviderConfig.mockReturnValue({ - type: NetworkType.mainnet, - chainId: '0x1', - } as unknown as ProviderConfig), + 'NetworkController:getState', + mockNetworkState.mockReturnValue({ ...defaultNetworkState }), ); const mockTokensState = jest.fn(); controllerMessenger.registerActionHandler( @@ -2130,7 +2119,6 @@ async function withController( const callActionSpy = jest.spyOn(controllerMessenger, 'call'); const controller = new TokenDetectionController({ - networkClientId: NetworkType.mainnet, getBalancesInSingleCall: jest.fn(), trackMetaMetricsEvent: jest.fn(), messenger: buildTokenDetectionControllerMessenger(controllerMessenger), @@ -2154,10 +2142,12 @@ async function withController( mockTokenListGetState: (state: TokenListState) => { mockTokenListState.mockReturnValue(state); }, - mockFindNetworkClientIdByChainId: ( - handler: (chainId: Hex) => NetworkClientId, + mockGetNetworkClientById: ( + handler: ( + networkClientId: NetworkClientId, + ) => AutoManagedNetworkClient, ) => { - mockFindNetworkClientIdByChainId.mockImplementation(handler); + mockGetNetworkClientById.mockImplementation(handler); }, mockGetNetworkConfigurationByNetworkClientId: ( handler: (networkClientId: NetworkClientId) => NetworkConfiguration, @@ -2166,8 +2156,8 @@ async function withController( handler, ); }, - mockGetProviderConfig: (config: ProviderConfig) => { - mockGetProviderConfig.mockReturnValue(config); + mockNetworkState: (state: NetworkState) => { + mockNetworkState.mockReturnValue(state); }, callActionSpy, triggerKeyringUnlock: () => { diff --git a/packages/assets-controllers/src/TokenDetectionController.ts b/packages/assets-controllers/src/TokenDetectionController.ts index d4d1cd8d2ca..bda6be6f5f6 100644 --- a/packages/assets-controllers/src/TokenDetectionController.ts +++ b/packages/assets-controllers/src/TokenDetectionController.ts @@ -16,9 +16,9 @@ import type { } from '@metamask/keyring-controller'; import type { NetworkClientId, - NetworkControllerFindNetworkClientIdByChainIdAction, + NetworkControllerGetNetworkClientByIdAction, NetworkControllerGetNetworkConfigurationByNetworkClientId, - NetworkControllerGetProviderConfigAction, + NetworkControllerGetStateAction, NetworkControllerNetworkDidChangeEvent, } from '@metamask/network-controller'; import { StaticIntervalPollingController } from '@metamask/polling-controller'; @@ -32,8 +32,8 @@ import type { AssetsContractController } from './AssetsContractController'; import { isTokenDetectionSupportedForNetwork } from './assetsUtil'; import type { GetTokenListState, + TokenListMap, TokenListStateChange, - TokenListToken, } from './TokenListController'; import type { Token } from './TokenRatesController'; import type { @@ -63,25 +63,23 @@ export function isEqualCaseInsensitive( return value1.toLowerCase() === value2.toLowerCase(); } -type LegacyToken = Omit< - Token, - 'aggregators' | 'image' | 'balanceError' | 'isERC721' -> & { +type LegacyToken = { name: string; - logo: string; + logo: `${string}.svg`; + symbol: string; + decimals: number; erc20?: boolean; erc721?: boolean; }; +type TokenDetectionMap = { + [P in keyof TokenListMap]: Omit; +}; + export const STATIC_MAINNET_TOKEN_LIST = Object.entries( contractMap, -).reduce< - Record< - string, - Partial & Pick - > ->((acc, [base, contract]) => { - const { logo, ...tokenMetadata } = contract; +).reduce((acc, [base, contract]) => { + const { logo, erc20, erc721, ...tokenMetadata } = contract; return { ...acc, [base.toLowerCase()]: { @@ -107,9 +105,9 @@ export type TokenDetectionControllerActions = export type AllowedActions = | AccountsControllerGetSelectedAccountAction - | NetworkControllerFindNetworkClientIdByChainIdAction + | NetworkControllerGetNetworkClientByIdAction | NetworkControllerGetNetworkConfigurationByNetworkClientId - | NetworkControllerGetProviderConfigAction + | NetworkControllerGetStateAction | GetTokenListState | KeyringControllerGetStateAction | PreferencesControllerGetStateAction @@ -141,7 +139,6 @@ export type TokenDetectionControllerMessenger = RestrictedControllerMessenger< /** * Controller that passively polls on a set interval for Tokens auto detection * @property intervalId - Polling interval used to fetch new token rates - * @property chainId - The chain ID of the current network * @property selectedAddress - Vault selected address * @property networkClientId - The network client ID of the current selected network * @property disabled - Boolean to track if network requests are blocked @@ -156,12 +153,12 @@ export class TokenDetectionController extends StaticIntervalPollingController< > { #intervalId?: ReturnType; - #chainId: Hex; - #selectedAddress: string; #networkClientId: NetworkClientId; + #tokenList: TokenDetectionMap = {}; + #disabled: boolean; #isUnlocked: boolean; @@ -189,13 +186,11 @@ export class TokenDetectionController extends StaticIntervalPollingController< * @param options.messenger - The controller messaging system. * @param options.disabled - If set to true, all network requests are blocked. * @param options.interval - Polling interval used to fetch new token rates - * @param options.networkClientId - The selected network client ID of the current network * @param options.selectedAddress - Vault selected address * @param options.getBalancesInSingleCall - Gets the balances of a list of tokens for the given address. * @param options.trackMetaMetricsEvent - Sets options for MetaMetrics event tracking. */ constructor({ - networkClientId, selectedAddress, interval = DEFAULT_INTERVAL, disabled = true, @@ -203,7 +198,6 @@ export class TokenDetectionController extends StaticIntervalPollingController< trackMetaMetricsEvent, messenger, }: { - networkClientId: NetworkClientId; selectedAddress?: string; interval?: number; disabled?: boolean; @@ -229,21 +223,20 @@ export class TokenDetectionController extends StaticIntervalPollingController< this.#disabled = disabled; this.setIntervalLength(interval); - this.#networkClientId = networkClientId; this.#selectedAddress = selectedAddress ?? this.messagingSystem.call('AccountsController:getSelectedAccount') .address; - const { chainId } = - this.#getCorrectChainIdAndNetworkClientId(networkClientId); - this.#chainId = chainId; + + const { chainId, networkClientId } = + this.#getCorrectChainIdAndNetworkClientId(); + this.#networkClientId = networkClientId; const { useTokenDetection: defaultUseTokenDetection } = this.messagingSystem.call('PreferencesController:getState'); this.#isDetectionEnabledFromPreferences = defaultUseTokenDetection; - this.#isDetectionEnabledForNetwork = isTokenDetectionSupportedForNetwork( - this.#chainId, - ); + this.#isDetectionEnabledForNetwork = + isTokenDetectionSupportedForNetwork(chainId); this.#getBalancesInSingleCall = getBalancesInSingleCall; @@ -329,7 +322,6 @@ export class TokenDetectionController extends StaticIntervalPollingController< const { chainId: newChainId } = this.#getCorrectChainIdAndNetworkClientId(selectedNetworkClientId); - this.#chainId = newChainId; this.#isDetectionEnabledForNetwork = isTokenDetectionSupportedForNetwork(newChainId); @@ -346,30 +338,29 @@ export class TokenDetectionController extends StaticIntervalPollingController< /** * Allows controller to make active and passive polling requests */ - enable() { + enable(): void { this.#disabled = false; } /** * Blocks controller from making network calls */ - disable() { + disable(): void { this.#disabled = true; } /** * Internal isActive state - * - * @type {object} + * @type {boolean} */ - get isActive() { + get isActive(): boolean { return !this.#disabled && this.#isUnlocked; } /** * Start polling for detected tokens. */ - async start() { + async start(): Promise { this.enable(); await this.#startPolling(); } @@ -377,12 +368,12 @@ export class TokenDetectionController extends StaticIntervalPollingController< /** * Stop polling for detected tokens. */ - stop() { + stop(): void { this.disable(); this.#stopPolling(); } - #stopPolling() { + #stopPolling(): void { if (this.#intervalId) { clearInterval(this.#intervalId); } @@ -418,21 +409,23 @@ export class TokenDetectionController extends StaticIntervalPollingController< }; } } - const { chainId } = this.messagingSystem.call( - 'NetworkController:getProviderConfig', + const { selectedNetworkClientId } = this.messagingSystem.call( + 'NetworkController:getState', ); - const newNetworkClientId = this.messagingSystem.call( - 'NetworkController:findNetworkClientIdByChainId', - chainId, + const { + configuration: { chainId }, + } = this.messagingSystem.call( + 'NetworkController:getNetworkClientById', + selectedNetworkClientId, ); return { chainId, - networkClientId: newNetworkClientId, + networkClientId: selectedNetworkClientId, }; } async _executePoll( - networkClientId: string, + networkClientId: NetworkClientId, options: { address: string }, ): Promise { if (!this.isActive) { @@ -455,7 +448,10 @@ export class TokenDetectionController extends StaticIntervalPollingController< async #restartTokenDetection({ selectedAddress, networkClientId, - }: { selectedAddress?: string; networkClientId?: string } = {}) { + }: { + selectedAddress?: string; + networkClientId?: NetworkClientId; + } = {}): Promise { await this.detectTokens({ networkClientId, selectedAddress, @@ -484,15 +480,14 @@ export class TokenDetectionController extends StaticIntervalPollingController< const addressAgainstWhichToDetect = selectedAddress ?? this.#selectedAddress; - const { - chainId: chainIdAgainstWhichToDetect, - networkClientId: networkClientIdAgainstWhichToDetect, - } = this.#getCorrectChainIdAndNetworkClientId(networkClientId); + const { chainId, networkClientId: selectedNetworkClientId } = + this.#getCorrectChainIdAndNetworkClientId(networkClientId); + const chainIdAgainstWhichToDetect = chainId; + const networkClientIdAgainstWhichToDetect = selectedNetworkClientId; if (!isTokenDetectionSupportedForNetwork(chainIdAgainstWhichToDetect)) { return; } - if ( !this.#isDetectionEnabledFromPreferences && chainIdAgainstWhichToDetect !== ChainId.mainnet @@ -505,12 +500,30 @@ export class TokenDetectionController extends StaticIntervalPollingController< const { tokensChainsCache } = this.messagingSystem.call( 'TokenListController:getState', ); - const tokenList = - tokensChainsCache[chainIdAgainstWhichToDetect]?.data ?? {}; - const tokenListUsed = isTokenDetectionInactiveInMainnet + this.#tokenList = isTokenDetectionInactiveInMainnet ? STATIC_MAINNET_TOKEN_LIST - : tokenList; + : tokensChainsCache[chainIdAgainstWhichToDetect]?.data ?? {}; + + for (const tokensSlice of this.#getSlicesOfTokensToDetect({ + chainId: chainIdAgainstWhichToDetect, + selectedAddress: addressAgainstWhichToDetect, + })) { + await this.#addDetectedTokens({ + tokensSlice, + selectedAddress: addressAgainstWhichToDetect, + networkClientId: networkClientIdAgainstWhichToDetect, + chainId: chainIdAgainstWhichToDetect, + }); + } + } + #getSlicesOfTokensToDetect({ + chainId, + selectedAddress, + }: { + chainId: Hex; + selectedAddress: string; + }): string[][] { const { allTokens, allDetectedTokens, allIgnoredTokens } = this.messagingSystem.call('TokensController:getState'); const [tokensAddresses, detectedTokensAddresses, ignoredTokensAddresses] = [ @@ -518,12 +531,13 @@ export class TokenDetectionController extends StaticIntervalPollingController< allDetectedTokens, allIgnoredTokens, ].map((tokens) => - ( - tokens[chainIdAgainstWhichToDetect]?.[addressAgainstWhichToDetect] ?? [] - ).map((value) => (typeof value === 'string' ? value : value.address)), + (tokens[chainId]?.[selectedAddress] ?? []).map((value) => + typeof value === 'string' ? value : value.address, + ), ); + const tokensToDetect: string[] = []; - for (const tokenAddress of Object.keys(tokenListUsed)) { + for (const tokenAddress of Object.keys(this.#tokenList)) { if ( [ tokensAddresses, @@ -539,60 +553,71 @@ export class TokenDetectionController extends StaticIntervalPollingController< tokensToDetect.push(tokenAddress); } } + const slicesOfTokensToDetect = []; - slicesOfTokensToDetect[0] = tokensToDetect.slice(0, 1000); - slicesOfTokensToDetect[1] = tokensToDetect.slice( - 1000, - tokensToDetect.length - 1, - ); - for (const tokensSlice of slicesOfTokensToDetect) { - if (tokensSlice.length === 0) { - break; + for (let i = 0, size = 1000; i < tokensToDetect.length; i += size) { + slicesOfTokensToDetect.push(tokensToDetect.slice(i, i + size)); + } + + return slicesOfTokensToDetect; + } + + async #addDetectedTokens({ + tokensSlice, + selectedAddress, + networkClientId, + chainId, + }: { + tokensSlice: string[]; + selectedAddress: string; + networkClientId: NetworkClientId; + chainId: Hex; + }): Promise { + await safelyExecute(async () => { + const balances = await this.#getBalancesInSingleCall( + selectedAddress, + tokensSlice, + networkClientId, + ); + + const tokensWithBalance: Token[] = []; + const eventTokensDetails: string[] = []; + for (const nonZeroTokenAddress of Object.keys(balances)) { + const { decimals, symbol, aggregators, iconUrl, name } = + this.#tokenList[nonZeroTokenAddress]; + eventTokensDetails.push(`${symbol} - ${nonZeroTokenAddress}`); + tokensWithBalance.push({ + address: nonZeroTokenAddress, + decimals, + symbol, + aggregators, + image: iconUrl, + isERC721: false, + name, + }); } - await safelyExecute(async () => { - const balances = await this.#getBalancesInSingleCall( - addressAgainstWhichToDetect, - tokensSlice, - networkClientIdAgainstWhichToDetect, + if (tokensWithBalance.length) { + this.#trackMetaMetricsEvent({ + event: 'Token Detected', + category: 'Wallet', + properties: { + tokens: eventTokensDetails, + token_standard: 'ERC20', + asset_type: 'TOKEN', + }, + }); + + await this.messagingSystem.call( + 'TokensController:addDetectedTokens', + tokensWithBalance, + { + selectedAddress, + chainId, + }, ); - const tokensWithBalance: Token[] = []; - const eventTokensDetails: string[] = []; - 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 (tokensWithBalance.length) { - this.#trackMetaMetricsEvent({ - event: 'Token Detected', - category: 'Wallet', - properties: { - tokens: eventTokensDetails, - token_standard: 'ERC20', - asset_type: 'TOKEN', - }, - }); - await this.messagingSystem.call( - 'TokensController:addDetectedTokens', - tokensWithBalance, - { - selectedAddress: addressAgainstWhichToDetect, - chainId: chainIdAgainstWhichToDetect, - }, - ); - } - }); - } + } + }); } }