From 5f3610d04b5c00d80aee556cf83cc3012686a338 Mon Sep 17 00:00:00 2001 From: juanmigdr Date: Thu, 18 Jun 2026 13:15:30 +0200 Subject: [PATCH 1/4] chore: deprecate TokensController --- .../src/TokensController.test.ts | 252 ++++++++++++++++++ .../src/TokensController.ts | 91 ++++++- 2 files changed, 340 insertions(+), 3 deletions(-) diff --git a/packages/assets-controllers/src/TokensController.test.ts b/packages/assets-controllers/src/TokensController.test.ts index db3bf37c7a3..6cfdd416ad1 100644 --- a/packages/assets-controllers/src/TokensController.test.ts +++ b/packages/assets-controllers/src/TokensController.test.ts @@ -3740,6 +3740,258 @@ describe('TokensController', () => { }); }); + describe('isDeprecated', () => { + const initialState: TokensControllerState = { + allTokens: { + [ChainId.mainnet]: { + '0x0001': [ + { + address: '0x03', + symbol: 'barC', + decimals: 2, + aggregators: [], + image: undefined, + name: undefined, + }, + ], + }, + }, + allIgnoredTokens: { + [ChainId.mainnet]: { + '0x0001': ['0x03'], + }, + }, + allDetectedTokens: { + [ChainId.mainnet]: { + '0x0001': [ + { + address: '0x01', + symbol: 'barA', + decimals: 2, + aggregators: [], + image: undefined, + name: undefined, + }, + ], + }, + }, + }; + + const emptyState: TokensControllerState = { + allTokens: {}, + allIgnoredTokens: {}, + allDetectedTokens: {}, + }; + + it('clears all persisted state at construction when isDeprecated() returns true', async () => { + await withController( + { options: { state: initialState, isDeprecated: () => true } }, + ({ controller }) => { + expect(controller.state).toStrictEqual(emptyState); + }, + ); + }); + + it('preserves persisted state at construction when isDeprecated() returns false', async () => { + await withController( + { options: { state: initialState, isDeprecated: () => false } }, + ({ controller }) => { + expect(controller.state).toStrictEqual(initialState); + }, + ); + }); + + it('does not throw at construction when isDeprecated() is true and state is already empty', async () => { + await withController( + { options: { isDeprecated: () => true } }, + ({ controller }) => { + expect(controller.state).toStrictEqual(emptyState); + }, + ); + }); + + it('does not call tokenListService.fetchTokensByChainId at construction when isDeprecated() returns true', async () => { + await withController( + { options: { state: initialState, isDeprecated: () => true } }, + async ({ controller }) => { + // Give any async init work a chance to settle + await new Promise(process.nextTick); + + // The tokenListService mock is accessed via the controller factory; + // we verify by checking that state was not modified by enrichment + expect(controller.state).toStrictEqual(emptyState); + }, + ); + }); + + it('does not add tokens and clears stale state when isDeprecated toggles to true at runtime via addToken', async () => { + let deprecated = false; + await withController( + { options: { state: initialState, isDeprecated: () => deprecated } }, + async ({ controller }) => { + expect(controller.state).toStrictEqual(initialState); + + deprecated = true; + + const result = await controller.addToken({ + address: '0x05', + symbol: 'NEW', + decimals: 18, + networkClientId: 'mainnet', + }); + + expect(result).toStrictEqual([]); + expect(controller.state).toStrictEqual(emptyState); + }, + ); + }); + + it('does not add tokens and clears stale state when isDeprecated toggles to true at runtime via addTokens', async () => { + let deprecated = false; + await withController( + { options: { state: initialState, isDeprecated: () => deprecated } }, + async ({ controller }) => { + expect(controller.state).toStrictEqual(initialState); + + deprecated = true; + + await controller.addTokens( + [{ address: '0x05', symbol: 'NEW', decimals: 18 }], + 'mainnet', + ); + + expect(controller.state).toStrictEqual(emptyState); + }, + ); + }); + + it('does not ignore tokens and clears stale state when isDeprecated toggles to true at runtime via ignoreTokens', async () => { + let deprecated = false; + await withController( + { options: { state: initialState, isDeprecated: () => deprecated } }, + ({ controller }) => { + expect(controller.state).toStrictEqual(initialState); + + deprecated = true; + + controller.ignoreTokens(['0x03'], 'mainnet'); + + expect(controller.state).toStrictEqual(emptyState); + }, + ); + }); + + it('does not add detected tokens and clears stale state when isDeprecated toggles to true at runtime via addDetectedTokens', async () => { + let deprecated = false; + await withController( + { options: { state: initialState, isDeprecated: () => deprecated } }, + async ({ controller }) => { + expect(controller.state).toStrictEqual(initialState); + + deprecated = true; + + await controller.addDetectedTokens( + [{ address: '0x05', symbol: 'NEW', decimals: 18 }], + { chainId: ChainId.mainnet }, + ); + + expect(controller.state).toStrictEqual(emptyState); + }, + ); + }); + + it('throws and clears stale state when isDeprecated toggles to true at runtime via updateTokenType', async () => { + let deprecated = false; + await withController( + { options: { state: initialState, isDeprecated: () => deprecated } }, + async ({ controller }) => { + expect(controller.state).toStrictEqual(initialState); + + deprecated = true; + + await expect( + controller.updateTokenType('0x03', 'mainnet'), + ).rejects.toThrow('TokensController is deprecated'); + + expect(controller.state).toStrictEqual(emptyState); + }, + ); + }); + + it('does not process watchAsset and clears stale state when isDeprecated toggles to true at runtime via watchAsset', async () => { + let deprecated = false; + await withController( + { options: { state: initialState, isDeprecated: () => deprecated } }, + async ({ controller }) => { + expect(controller.state).toStrictEqual(initialState); + + deprecated = true; + + await controller.watchAsset({ + asset: { address: '0x05', symbol: 'NEW', decimals: 18 }, + type: 'ERC20', + networkClientId: 'mainnet', + }); + + expect(controller.state).toStrictEqual(emptyState); + }, + ); + }); + + it('clears all stale state when isDeprecated toggles to true at runtime via clearIgnoredTokens', async () => { + let deprecated = false; + await withController( + { options: { state: initialState, isDeprecated: () => deprecated } }, + ({ controller }) => { + expect(controller.state).toStrictEqual(initialState); + + deprecated = true; + + controller.clearIgnoredTokens(); + + expect(controller.state).toStrictEqual(emptyState); + }, + ); + }); + + it('clears stale state on NetworkController:stateChange when isDeprecated toggles to true at runtime', async () => { + let deprecated = false; + await withController( + { options: { state: initialState, isDeprecated: () => deprecated } }, + ({ controller, triggerNetworkStateChange }) => { + expect(controller.state).toStrictEqual(initialState); + + deprecated = true; + + triggerNetworkStateChange({} as NetworkState, [ + { + op: 'remove', + path: ['networkConfigurationsByChainId', ChainId.mainnet], + }, + ]); + + expect(controller.state).toStrictEqual(emptyState); + }, + ); + }); + + it('clears stale state on KeyringController:accountRemoved when isDeprecated toggles to true at runtime', async () => { + let deprecated = false; + await withController( + { options: { state: initialState, isDeprecated: () => deprecated } }, + ({ controller, triggerAccountRemoved }) => { + expect(controller.state).toStrictEqual(initialState); + + deprecated = true; + + triggerAccountRemoved('0x0001'); + + expect(controller.state).toStrictEqual(emptyState); + }, + ); + }); + }); + describe('metadata', () => { it('includes expected state in debug snapshots', async () => { await withController(({ controller }) => { diff --git a/packages/assets-controllers/src/TokensController.ts b/packages/assets-controllers/src/TokensController.ts index c386f4e4f12..f528a12b52b 100644 --- a/packages/assets-controllers/src/TokensController.ts +++ b/packages/assets-controllers/src/TokensController.ts @@ -206,6 +206,8 @@ export class TokensController extends BaseController< readonly #abortController: AbortController; + readonly #isDeprecated: () => boolean; + /** * Tokens controller options * @@ -215,18 +217,27 @@ export class TokensController extends BaseController< * @param options.state - Initial state to set on this controller. * @param options.messenger - The messenger. * @param options.tokenListService - Shared service for fetching token metadata per chain. + * @param options.isDeprecated - Optional function that returns true to completely + * disable this controller (no requests, no state updates). When it returns + * `true`, `allTokens`, `allIgnoredTokens`, and `allDetectedTokens` are reset to + * `{}` at construction and at every entry point, so no stale token data remains + * in state. The function is evaluated dynamically on each entry point so it can + * be toggled at runtime. Intended for use when a higher-level controller + * (e.g. AssetsController) supersedes this one. */ constructor({ provider, state, messenger, tokenListService, + isDeprecated = (): boolean => false, }: { chainId: Hex; provider: Provider; state?: Partial; messenger: TokensControllerMessenger; tokenListService: TokenListService; + isDeprecated?: () => boolean; }) { super({ name: controllerName, @@ -239,6 +250,7 @@ export class TokensController extends BaseController< }); this.#provider = provider; + this.#isDeprecated = isDeprecated; this.#selectedAccountId = this.#getSelectedAccount().id; @@ -261,9 +273,37 @@ export class TokensController extends BaseController< (accountAddress: string) => this.#handleOnAccountRemoved(accountAddress), ); - // Enrich persisted tokens with name/rwaData from the token list once at init. - this.#enrichTokensFromTokenList(tokenListService).catch(() => { - // Tokens remain usable without metadata enrichment + if (this.#isDeprecated()) { + this.#enforceDisabledState(); + } else { + // Enrich persisted tokens with name/rwaData from the token list once at init. + this.#enrichTokensFromTokenList(tokenListService).catch(() => { + // Tokens remain usable without metadata enrichment + }); + } + } + + /** + * Clears all persisted token state so that no stale data remains. + * + * Called from every entry point when `isDeprecated()` is true so that a + * runtime toggle propagates to state immediately, even if the controller was + * originally constructed while it was enabled. The update is skipped when + * all three maps are already empty to avoid emitting redundant state changes. + */ + #enforceDisabledState(): void { + const { allTokens, allIgnoredTokens, allDetectedTokens } = this.state; + if ( + Object.keys(allTokens).length === 0 && + Object.keys(allIgnoredTokens).length === 0 && + Object.keys(allDetectedTokens).length === 0 + ) { + return; + } + this.update((state) => { + state.allTokens = {}; + state.allIgnoredTokens = {}; + state.allDetectedTokens = {}; }); } @@ -322,6 +362,11 @@ export class TokensController extends BaseController< } #handleOnAccountRemoved(accountAddress: string) { + if (this.#isDeprecated()) { + this.#enforceDisabledState(); + return; + } + const isEthAddress = isStrictHexString(accountAddress.toLowerCase()) && isValidHexAddress(accountAddress); @@ -367,6 +412,11 @@ export class TokensController extends BaseController< * @param patches - An array of patch operations performed on the network state. */ #onNetworkStateChange(_: NetworkState, patches: Patch[]) { + if (this.#isDeprecated()) { + this.#enforceDisabledState(); + return; + } + // Remove state for deleted networks for (const patch of patches) { if ( @@ -455,6 +505,11 @@ export class TokensController extends BaseController< networkClientId: NetworkClientId; rwaData?: TokenRwaData; }): Promise { + if (this.#isDeprecated()) { + this.#enforceDisabledState(); + return []; + } + const releaseLock = await this.#mutex.acquire(); const { allTokens, allIgnoredTokens, allDetectedTokens } = this.state; @@ -542,6 +597,11 @@ export class TokensController extends BaseController< * @param networkClientId - Optional network client ID used to determine interacting chain ID. */ async addTokens(tokensToImport: Token[], networkClientId: NetworkClientId) { + if (this.#isDeprecated()) { + this.#enforceDisabledState(); + return; + } + const releaseLock = await this.#mutex.acquire(); const { allTokens, allIgnoredTokens, allDetectedTokens } = this.state; const importedTokensMap: { [key: string]: true } = {}; @@ -622,6 +682,11 @@ export class TokensController extends BaseController< tokenAddressesToIgnore: string[], networkClientId: NetworkClientId, ) { + if (this.#isDeprecated()) { + this.#enforceDisabledState(); + return; + } + const interactingChainId = this.messenger.call( 'NetworkController:getNetworkClientById', networkClientId, @@ -679,6 +744,11 @@ export class TokensController extends BaseController< incomingDetectedTokens: Token[], detectionDetails: { selectedAddress?: string; chainId: Hex }, ) { + if (this.#isDeprecated()) { + this.#enforceDisabledState(); + return; + } + const releaseLock = await this.#mutex.acquire(); const { chainId } = detectionDetails; @@ -782,6 +852,11 @@ export class TokensController extends BaseController< tokenAddress: string, networkClientId: NetworkClientId, ): Promise { + if (this.#isDeprecated()) { + this.#enforceDisabledState(); + throw new Error('TokensController is deprecated'); + } + const chainIdToUse = this.messenger.call( 'NetworkController:getNetworkClientById', networkClientId, @@ -894,6 +969,11 @@ export class TokensController extends BaseController< pageMeta?: Record; requestMetadata?: WatchAssetRequestMetadata; }): Promise { + if (this.#isDeprecated()) { + this.#enforceDisabledState(); + return; + } + if (type !== ERC20) { throw new Error(`Asset of type ${type} not supported`); } @@ -1121,6 +1201,11 @@ export class TokensController extends BaseController< * Removes all tokens from the ignored list. */ clearIgnoredTokens() { + if (this.#isDeprecated()) { + this.#enforceDisabledState(); + return; + } + this.update((state) => { state.allIgnoredTokens = {}; }); From 9bfd5a0a9ee96cd5f16063425de45c72862404f3 Mon Sep 17 00:00:00 2001 From: juanmigdr Date: Thu, 18 Jun 2026 13:17:52 +0200 Subject: [PATCH 2/4] chore: update changelog --- packages/assets-controllers/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index e14f1c4e313..738204cd2c3 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Add `isDeprecated` option to `TokensController` constructor ([#9186](https://github.com/MetaMask/core/pull/9186)) + - When `isDeprecated()` returns `true`, no token list enrichment runs and `allTokens`, `allIgnoredTokens`, and `allDetectedTokens` are reset to `{}` at construction and at every entry point (`addToken`, `addTokens`, `ignoreTokens`, `addDetectedTokens`, `updateTokenType`, `watchAsset`, `clearIgnoredTokens`, `NetworkController:stateChange`, and `KeyringController:accountRemoved`), so no stale token data remains in state. + - The function is re-evaluated on each entry point so it can be toggled at runtime without reconstructing the controller. - Add `isDeprecated` option to `MultichainAssetsRatesController` constructor ([#9044](https://github.com/MetaMask/core/pull/9044)) - When `isDeprecated()` returns `true`, no Snap requests are sent and `conversionRates` and `historicalPrices` are reset to `{}` at construction and at every entry point (`updateAssetsRates`, `fetchHistoricalPricesForAsset`, `_executePoll`, `CurrencyRateController:stateChange`, and `MultichainAssetsController:accountAssetListUpdated`), so no stale rates remain in state. - The function is re-evaluated on each entry point so it can be toggled at runtime without reconstructing the controller. @@ -3213,7 +3216,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Initial release + - As a result of converting our shared controllers repo into a monorepo ([#831](https://github.com/MetaMask/core/pull/831)), we've created this package from select parts of [`@metamask/controllers` v33.0.0](https://github.com/MetaMask/core/tree/v33.0.0), namely: + - Everything in `src/assets` - Asset-related functions from `src/util.ts` and accompanying tests From cc39f22a4d7df35bb9f789bd8d57959c7296ea88 Mon Sep 17 00:00:00 2001 From: juanmigdr Date: Fri, 19 Jun 2026 09:25:25 +0200 Subject: [PATCH 3/4] chore: minor change --- packages/assets-controllers/CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index e27c819df35..95e95a83b01 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -3218,9 +3218,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Initial release - - As a result of converting our shared controllers repo into a monorepo ([#831](https://github.com/MetaMask/core/pull/831)), we've created this package from select parts of [`@metamask/controllers` v33.0.0](https://github.com/MetaMask/core/tree/v33.0.0), namely: - - Everything in `src/assets` - Asset-related functions from `src/util.ts` and accompanying tests From 9a1bc87a0399df9cd47b1ea2eb32964fb6378c51 Mon Sep 17 00:00:00 2001 From: juanmigdr Date: Fri, 19 Jun 2026 10:03:05 +0200 Subject: [PATCH 4/4] fix: isues --- packages/assets-controllers/src/TokensController.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/assets-controllers/src/TokensController.test.ts b/packages/assets-controllers/src/TokensController.test.ts index 6cfdd416ad1..e88f0eb588e 100644 --- a/packages/assets-controllers/src/TokensController.test.ts +++ b/packages/assets-controllers/src/TokensController.test.ts @@ -3815,7 +3815,7 @@ describe('TokensController', () => { { options: { state: initialState, isDeprecated: () => true } }, async ({ controller }) => { // Give any async init work a chance to settle - await new Promise(process.nextTick); + await new Promise((resolve) => process.nextTick(resolve)); // The tokenListService mock is accessed via the controller factory; // we verify by checking that state was not modified by enrichment