diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index 1d683a92c09..9d25dbb9f2f 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 `TokenDetectionController` constructor ([#9362](https://github.com/MetaMask/core/pull/9362)) + - When `isDeprecated()` returns `true`, no network requests are sent and entry points bail early at `start`, `detectTokens`, `_executePoll`, `addDetectedTokensViaWs`, and `addDetectedTokensViaPolling`, so no token detection work runs while the controller is disabled. + - The function is re-evaluated on each entry point so it can be toggled at runtime without reconstructing the controller. - Add `isDeprecated` option to `MultichainAssetsController` constructor ([#9310](https://github.com/MetaMask/core/pull/9310)) - When `isDeprecated()` returns `true`, no Snap requests are issued and `accountsAssets`, `assetsMetadata`, and `allIgnoredAssets` are reset to `{}` at construction and at every entry point (`addAssets`, `ignoreAssets`, `_executePoll`, `AccountsController:accountAdded`, `AccountsController:accountRemoved`, and `AccountsController:accountAssetListUpdated`), so no stale asset data remains in state. - The function is re-evaluated on each entry point so it can be toggled at runtime without reconstructing the controller. diff --git a/packages/assets-controllers/src/TokenDetectionController.test.ts b/packages/assets-controllers/src/TokenDetectionController.test.ts index 14f987f8e3f..d6947ccf626 100644 --- a/packages/assets-controllers/src/TokenDetectionController.test.ts +++ b/packages/assets-controllers/src/TokenDetectionController.test.ts @@ -4067,6 +4067,276 @@ describe('TokenDetectionController', () => { ); }); }); + + describe('isDeprecated', () => { + it('does not throw at construction when isDeprecated() is true', async () => { + await withController( + { options: { isDeprecated: () => true } }, + ({ controller }) => { + expect(controller.state).toStrictEqual({}); + }, + ); + }); + + it('does not make any network calls when isDeprecated() returns true from construction', async () => { + const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({}); + await withController( + { + options: { + isDeprecated: () => true, + disabled: false, + getBalancesInSingleCall: mockGetBalancesInSingleCall, + }, + mocks: { + getSelectedAccount: defaultSelectedAccount, + }, + }, + async ({ + controller, + mockTokenListGetState, + mockGetNetworkClientById, + }) => { + mockTokenListGetState({ + ...getDefaultTokenListState(), + tokensChainsCache: { + '0xa86a': { + timestamp: 0, + data: { + [sampleTokenA.address]: { + name: sampleTokenA.name, + symbol: sampleTokenA.symbol, + decimals: sampleTokenA.decimals, + address: sampleTokenA.address, + aggregators: [], + iconUrl: '', + occurrences: 11, + }, + }, + }, + }, + }); + mockGetNetworkClientById( + () => + ({ + configuration: { chainId: '0xa86a' }, + }) as unknown as AutoManagedNetworkClient, + ); + + await controller.detectTokens(); + + expect(mockGetBalancesInSingleCall).not.toHaveBeenCalled(); + expect(controller.state).toStrictEqual({}); + }, + ); + }); + + it('does not detect tokens when isDeprecated toggles to true at runtime via detectTokens', async () => { + let deprecated = false; + const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({}); + await withController( + { + options: { + isDeprecated: () => deprecated, + disabled: false, + getBalancesInSingleCall: mockGetBalancesInSingleCall, + }, + mocks: { + getSelectedAccount: defaultSelectedAccount, + }, + }, + async ({ controller }) => { + deprecated = true; + + await controller.detectTokens(); + + expect(mockGetBalancesInSingleCall).not.toHaveBeenCalled(); + expect(controller.state).toStrictEqual({}); + }, + ); + }); + + it('does not start polling when isDeprecated toggles to true at runtime via start', async () => { + let deprecated = false; + const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({}); + await withController( + { + options: { + isDeprecated: () => deprecated, + disabled: false, + getBalancesInSingleCall: mockGetBalancesInSingleCall, + }, + mocks: { + getSelectedAccount: defaultSelectedAccount, + }, + }, + async ({ controller }) => { + const mockDetectTokens = jest + .spyOn(controller, 'detectTokens') + .mockImplementation(); + + deprecated = true; + + await controller.start(); + + expect(mockDetectTokens).not.toHaveBeenCalled(); + }, + ); + }); + + it('does not detect tokens when isDeprecated toggles to true at runtime via _executePoll', async () => { + let deprecated = false; + const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({}); + await withController( + { + options: { + isDeprecated: () => deprecated, + disabled: false, + getBalancesInSingleCall: mockGetBalancesInSingleCall, + }, + }, + async ({ controller }) => { + deprecated = true; + + await controller._executePoll({ + chainIds: ['0xa86a'], + address: '0x1', + }); + + expect(mockGetBalancesInSingleCall).not.toHaveBeenCalled(); + expect(controller.state).toStrictEqual({}); + }, + ); + }); + + it('does not add tokens when isDeprecated toggles to true at runtime via addDetectedTokensViaWs', async () => { + let deprecated = false; + const mockTokenAddress = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'; + const chainId = '0xa86a'; + + await withController( + { + options: { + isDeprecated: () => deprecated, + disabled: false, + }, + mockTokenListState: { + tokensChainsCache: { + [chainId]: { + timestamp: 0, + data: { + [mockTokenAddress]: { + name: 'USD Coin', + symbol: 'USDC', + decimals: 6, + address: mockTokenAddress, + aggregators: [], + iconUrl: 'https://example.com/usdc.png', + occurrences: 11, + }, + }, + }, + }, + }, + }, + async ({ controller, callActionSpy }) => { + deprecated = true; + + await controller.addDetectedTokensViaWs({ + tokensSlice: [mockTokenAddress], + chainId: chainId as Hex, + }); + + expect(callActionSpy).not.toHaveBeenCalledWith( + 'TokensController:addTokens', + expect.anything(), + expect.anything(), + ); + }, + ); + }); + + it('does not add tokens when isDeprecated toggles to true at runtime via addDetectedTokensViaPolling', async () => { + let deprecated = false; + const mockTokenAddress = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'; + const chainId = '0xa86a'; + + await withController( + { + options: { + isDeprecated: () => deprecated, + disabled: false, + }, + mockTokenListState: { + tokensChainsCache: { + [chainId]: { + timestamp: 0, + data: { + [mockTokenAddress]: { + name: 'USD Coin', + symbol: 'USDC', + decimals: 6, + address: mockTokenAddress, + aggregators: [], + iconUrl: 'https://example.com/usdc.png', + occurrences: 11, + }, + }, + }, + }, + }, + }, + async ({ controller, callActionSpy }) => { + deprecated = true; + + await controller.addDetectedTokensViaPolling({ + tokensSlice: [mockTokenAddress], + chainId: chainId as Hex, + }); + + expect(callActionSpy).not.toHaveBeenCalledWith( + 'TokensController:addTokens', + expect.anything(), + expect.anything(), + ); + }, + ); + }); + + it('keeps polling but bails early when isDeprecated toggles to true at runtime', async () => { + jest.useFakeTimers(); + let deprecated = false; + const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({}); + await withController( + { + options: { + isDeprecated: () => deprecated, + disabled: false, + getBalancesInSingleCall: mockGetBalancesInSingleCall, + }, + mocks: { + getSelectedAccount: defaultSelectedAccount, + }, + }, + async ({ controller }) => { + const detectTokensSpy = jest.spyOn(controller, 'detectTokens'); + + controller.setIntervalLength(10); + await controller.start(); + expect(detectTokensSpy).toHaveBeenCalledTimes(1); + + deprecated = true; + await controller.detectTokens(); + mockGetBalancesInSingleCall.mockClear(); + + detectTokensSpy.mockClear(); + await jestAdvanceTime({ duration: 15 }); + expect(detectTokensSpy).toHaveBeenCalled(); + expect(mockGetBalancesInSingleCall).not.toHaveBeenCalled(); + }, + ); + jest.useRealTimers(); + }); + }); }); /** diff --git a/packages/assets-controllers/src/TokenDetectionController.ts b/packages/assets-controllers/src/TokenDetectionController.ts index 0baf8a33311..a499e36a0ff 100644 --- a/packages/assets-controllers/src/TokenDetectionController.ts +++ b/packages/assets-controllers/src/TokenDetectionController.ts @@ -204,6 +204,8 @@ export class TokenDetectionController extends StaticIntervalPollingController boolean; + readonly #isDeprecated: () => boolean; + readonly #getBalancesInSingleCall: AssetsContractController['getBalancesInSingleCall']; readonly #trackMetaMetricsEvent: (options: { @@ -230,6 +232,7 @@ export class TokenDetectionController extends StaticIntervalPollingController true, useExternalServices = (): boolean => true, + isDeprecated = (): boolean => false, }: { interval?: number; disabled?: boolean; @@ -259,6 +263,7 @@ export class TokenDetectionController extends StaticIntervalPollingController boolean; useExternalServices?: () => boolean; + isDeprecated?: () => boolean; }) { super({ name: controllerName, @@ -290,10 +295,22 @@ export class TokenDetectionController extends StaticIntervalPollingController ({})); + } + /** * Constructor helper for registering this controller's messenger subscriptions to controller events. */ @@ -390,6 +407,10 @@ export class TokenDetectionController extends StaticIntervalPollingController { + if (this.#isDeprecated()) { + this.#enforceDisabledState(); + return; + } this.enable(); await this.#startPolling(); } @@ -459,6 +480,10 @@ export class TokenDetectionController extends StaticIntervalPollingController { + if (this.#isDeprecated()) { + this.#enforceDisabledState(); + return; + } if (!this.isActive) { return; } @@ -574,6 +599,10 @@ export class TokenDetectionController extends StaticIntervalPollingController { + if (this.#isDeprecated()) { + this.#enforceDisabledState(); + return; + } if (!this.isActive) { return; } @@ -886,6 +915,10 @@ export class TokenDetectionController extends StaticIntervalPollingController { + if (this.#isDeprecated()) { + this.#enforceDisabledState(); + return; + } // Check if token detection is enabled via preferences if (!this.#useTokenDetection()) { return; @@ -992,6 +1025,10 @@ export class TokenDetectionController extends StaticIntervalPollingController { + if (this.#isDeprecated()) { + this.#enforceDisabledState(); + return; + } // Check if token detection is enabled via preferences if (!this.#useTokenDetection()) { return;