Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/assets-controllers/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add `isDeprecated` option to `TokenDetectionController` constructor ([#9363](https://github.com/MetaMask/core/pull/9363))
- When `isDeprecated()` returns `true`, no token detection requests are sent and polling is stopped at construction and at every entry point (`detectTokens`, `addDetectedTokensViaWs`, `addDetectedTokensViaPolling`, `start`, `_executePoll`, and event-driven restarts), so the controller is fully inactive.
- The function is re-evaluated on each entry point so it can be toggled at runtime without reconstructing the controller.

## [109.3.0]

### Added
Expand Down
278 changes: 278 additions & 0 deletions packages/assets-controllers/src/TokenDetectionController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4061,6 +4061,284 @@ describe('TokenDetectionController', () => {
);
});
});

describe('isDeprecated', () => {
beforeEach(() => {
jest.useFakeTimers();
});

afterEach(() => {
jest.useRealTimers();
});

it('stops polling at construction when isDeprecated() returns true', async () => {
await withController(
{
isKeyringUnlocked: true,
options: { disabled: false, isDeprecated: () => true },
},
async ({ controller }) => {
const mockDetectTokens = jest
.spyOn(controller, 'detectTokens')
.mockImplementation();

await controller.start();
await jestAdvanceTime({ duration: DEFAULT_INTERVAL * 2 });

expect(mockDetectTokens).not.toHaveBeenCalled();
expect(controller.isActive).toBe(false);
},
);
});

it('does not throw at construction when isDeprecated() returns true', async () => {
await withController(
{
options: { isDeprecated: () => true },
},
({ controller }) => {
expect(controller.state).toStrictEqual({});
},
);
});

it('does not make any detection requests when isDeprecated() returns true from construction', async () => {
const mockGetBalancesInSingleCall = jest.fn();
await withController(
{
isKeyringUnlocked: true,
options: {
disabled: false,
isDeprecated: () => true,
getBalancesInSingleCall: mockGetBalancesInSingleCall,
},
},
async ({ controller }) => {
await controller.detectTokens({ chainIds: ['0xa86a'] });

expect(mockGetBalancesInSingleCall).not.toHaveBeenCalled();
},
);
});

it('does not detect tokens and stops polling when isDeprecated toggles to true at runtime via detectTokens', async () => {
let deprecated = false;
const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({});
const selectedAccount = createMockInternalAccount({
address: '0x0000000000000000000000000000000000000001',
});
await withController(
{
isKeyringUnlocked: true,
options: {
disabled: false,
isDeprecated: () => deprecated,
getBalancesInSingleCall: mockGetBalancesInSingleCall,
},
mocks: {
getSelectedAccount: selectedAccount,
getAccount: selectedAccount,
},
},
async ({ controller, mockTokenListGetState, mockNetworkState }) => {
const defaultState = getDefaultNetworkControllerState();
mockNetworkState({
...defaultState,
networkConfigurationsByChainId: {
...defaultState.networkConfigurationsByChainId,
...mockNetworkConfigurationsByChainId,
},
});
mockTokenListGetState({
...getDefaultTokenListState(),
tokensChainsCache: {
'0xa86a': {
timestamp: 0,
data: {
[sampleTokenA.address]: {
name: sampleTokenA.name,
symbol: sampleTokenA.symbol,
decimals: sampleTokenA.decimals,
address: sampleTokenA.address,
occurrences: 1,
aggregators: sampleTokenA.aggregators,
iconUrl: sampleTokenA.image,
},
},
},
},
});

await controller.detectTokens({
chainIds: ['0xa86a'],
selectedAddress: selectedAccount.address,
});
expect(mockGetBalancesInSingleCall).toHaveBeenCalled();

mockGetBalancesInSingleCall.mockClear();
deprecated = true;

await controller.detectTokens({
chainIds: ['0xa86a'],
selectedAddress: selectedAccount.address,
});

expect(mockGetBalancesInSingleCall).not.toHaveBeenCalled();
expect(controller.isActive).toBe(false);
},
);
});

it('does not poll when isDeprecated toggles to true at runtime via start', async () => {
let deprecated = false;
await withController(
{
isKeyringUnlocked: true,
options: { disabled: false, isDeprecated: () => deprecated },
},
async ({ controller }) => {
const mockDetectTokens = jest
.spyOn(controller, 'detectTokens')
.mockImplementation();

await controller.start();
expect(mockDetectTokens).toHaveBeenCalledTimes(1);

deprecated = true;
mockDetectTokens.mockClear();

await controller.start();
await jestAdvanceTime({ duration: DEFAULT_INTERVAL * 2 });

expect(mockDetectTokens).not.toHaveBeenCalled();
expect(controller.isActive).toBe(false);
},
);
});

it('does not add tokens when isDeprecated toggles to true at runtime via addDetectedTokensViaWs', async () => {
let deprecated = false;
await withController(
{
options: { isDeprecated: () => deprecated },
mockTokenListState: {
tokensChainsCache: {
[ChainId.mainnet]: {
timestamp: Date.now(),
data: {
[tokenAFromList.address.toLowerCase()]: {
...tokenAFromList,
aggregators: formattedSampleAggregators,
iconUrl: '',
},
},
},
},
},
},
async ({
controller,
callActionSpy,
mockFindNetworkClientIdByChainId,
}) => {
mockFindNetworkClientIdByChainId(() => 'mainnet');

deprecated = true;

await controller.addDetectedTokensViaWs({
tokensSlice: [tokenAFromList.address],
chainId: ChainId.mainnet,
});

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;
await withController(
{
options: { isDeprecated: () => deprecated },
mockTokenListState: {
tokensChainsCache: {
[ChainId.mainnet]: {
timestamp: Date.now(),
data: {
[tokenAFromList.address.toLowerCase()]: {
...tokenAFromList,
aggregators: formattedSampleAggregators,
iconUrl: '',
},
},
},
},
},
},
async ({
controller,
callActionSpy,
mockFindNetworkClientIdByChainId,
}) => {
mockFindNetworkClientIdByChainId(() => 'mainnet');

deprecated = true;

await controller.addDetectedTokensViaPolling({
tokensSlice: [tokenAFromList.address],
chainId: ChainId.mainnet,
});

expect(callActionSpy).not.toHaveBeenCalledWith(
'TokensController:addTokens',
expect.anything(),
expect.anything(),
);
},
);
});

it('does not detect tokens on KeyringController:unlock when deprecated', async () => {
await withController(
{
isKeyringUnlocked: false,
options: { disabled: false, isDeprecated: () => true },
},
async ({ controller, triggerKeyringUnlock }) => {
const mockDetectTokens = jest
.spyOn(controller, 'detectTokens')
.mockImplementation();

triggerKeyringUnlock();

expect(mockDetectTokens).not.toHaveBeenCalled();
},
);
});

it('does not detect tokens on TransactionController:transactionConfirmed when deprecated', async () => {
const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({});
await withController(
{
isKeyringUnlocked: true,
options: {
disabled: false,
isDeprecated: () => true,
getBalancesInSingleCall: mockGetBalancesInSingleCall,
},
},
async ({ triggerTransactionConfirmed }) => {
triggerTransactionConfirmed({ chainId: ChainId.mainnet });

expect(mockGetBalancesInSingleCall).not.toHaveBeenCalled();
},
);
});
});
});

/**
Expand Down
Loading