Skip to content
Merged
3 changes: 3 additions & 0 deletions packages/assets-controllers/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
270 changes: 270 additions & 0 deletions packages/assets-controllers/src/TokenDetectionController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CustomNetworkClientConfiguration>,
);

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();
});
});
});

/**
Expand Down
37 changes: 37 additions & 0 deletions packages/assets-controllers/src/TokenDetectionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ export class TokenDetectionController extends StaticIntervalPollingController<To

readonly #useExternalServices: () => boolean;

readonly #isDeprecated: () => boolean;

readonly #getBalancesInSingleCall: AssetsContractController['getBalancesInSingleCall'];

readonly #trackMetaMetricsEvent: (options: {
Expand All @@ -230,6 +232,7 @@ export class TokenDetectionController extends StaticIntervalPollingController<To
* @param options.trackMetaMetricsEvent - Sets options for MetaMetrics event tracking.
* @param options.useTokenDetection - Feature Switch for using token detection (default: true)
* @param options.useExternalServices - Feature Switch for using external services (default: false)
* @param options.isDeprecated - Optional callback that disables token detection when it returns true.
*/
constructor({
interval = DEFAULT_INTERVAL,
Expand All @@ -240,6 +243,7 @@ export class TokenDetectionController extends StaticIntervalPollingController<To
tokenListService,
useTokenDetection = (): boolean => true,
useExternalServices = (): boolean => true,
isDeprecated = (): boolean => false,
}: {
interval?: number;
disabled?: boolean;
Expand All @@ -259,6 +263,7 @@ export class TokenDetectionController extends StaticIntervalPollingController<To
tokenListService: TokenListService;
useTokenDetection?: () => boolean;
useExternalServices?: () => boolean;
isDeprecated?: () => boolean;
}) {
super({
name: controllerName,
Expand Down Expand Up @@ -290,10 +295,22 @@ export class TokenDetectionController extends StaticIntervalPollingController<To

this.#useTokenDetection = useTokenDetection;
this.#useExternalServices = useExternalServices;
this.#isDeprecated = isDeprecated;

if (this.#isDeprecated()) {
this.#enforceDisabledState();
}

this.#registerEventListeners();
}

#enforceDisabledState(): void {
if (Object.keys(this.state).length === 0) {
return;
}
this.update(() => ({}));
}
Comment thread
Prithpal-Sooriya marked this conversation as resolved.

/**
* Constructor helper for registering this controller's messenger subscriptions to controller events.
*/
Expand Down Expand Up @@ -390,6 +407,10 @@ export class TokenDetectionController extends StaticIntervalPollingController<To
* Start polling for detected tokens.
*/
async start(): Promise<void> {
if (this.#isDeprecated()) {
this.#enforceDisabledState();
return;
}
Comment thread
Prithpal-Sooriya marked this conversation as resolved.
this.enable();
await this.#startPolling();
}
Expand Down Expand Up @@ -459,6 +480,10 @@ export class TokenDetectionController extends StaticIntervalPollingController<To
chainIds,
address,
}: TokenDetectionPollingInput): Promise<void> {
if (this.#isDeprecated()) {
this.#enforceDisabledState();
return;
}
if (!this.isActive) {
return;
}
Expand Down Expand Up @@ -574,6 +599,10 @@ export class TokenDetectionController extends StaticIntervalPollingController<To
selectedAddress?: string;
forceRpc?: boolean;
} = {}): Promise<void> {
if (this.#isDeprecated()) {
this.#enforceDisabledState();
return;
}
if (!this.isActive) {
return;
}
Expand Down Expand Up @@ -886,6 +915,10 @@ export class TokenDetectionController extends StaticIntervalPollingController<To
tokensSlice: string[];
chainId: Hex;
}): Promise<void> {
if (this.#isDeprecated()) {
this.#enforceDisabledState();
return;
}
// Check if token detection is enabled via preferences
if (!this.#useTokenDetection()) {
return;
Expand Down Expand Up @@ -992,6 +1025,10 @@ export class TokenDetectionController extends StaticIntervalPollingController<To
tokensSlice: string[];
chainId: Hex;
}): Promise<void> {
if (this.#isDeprecated()) {
this.#enforceDisabledState();
return;
}
// Check if token detection is enabled via preferences
if (!this.#useTokenDetection()) {
return;
Expand Down
Loading