Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
5be1186
test(assets-controller) - add integration test to prove scam token hole.
Prithpal-Sooriya Sep 10, 2026
c5d9fd5
fix(assets-controller): add corrected scam asset deletion logic in To…
Prithpal-Sooriya Sep 10, 2026
b03222b
refactor: test cleanup
Prithpal-Sooriya Sep 10, 2026
2ad2edc
refactor: cleanup tests
Prithpal-Sooriya Sep 10, 2026
7505cff
refactor: cleanup more tests
Prithpal-Sooriya Sep 10, 2026
241cd49
refactor: test cleanup
Prithpal-Sooriya Sep 10, 2026
7eee877
refactor: streamline integration test by removing unused functions
Prithpal-Sooriya Sep 10, 2026
c72e2f2
Merge branch 'main' of github.com:MetaMask/core into fix-assets-contr…
Prithpal-Sooriya Sep 11, 2026
67f1874
refactor: format API responses (match linter)
Prithpal-Sooriya Sep 11, 2026
28e5735
refactor: test messenger cleanup.
Prithpal-Sooriya Sep 11, 2026
1db1ce1
refactor: remove bad messenger internal mock to use internal getState.
Prithpal-Sooriya Sep 11, 2026
bfc8864
test: add integration tests to the AssetsController too :)
Prithpal-Sooriya Sep 11, 2026
cbe2f52
test: cleanUp UTs
Prithpal-Sooriya Sep 11, 2026
8a5230c
refactor: remove comments
Prithpal-Sooriya Sep 11, 2026
422d5df
chore: update README and TypeScript configurations to include eth_jso…
Prithpal-Sooriya Sep 11, 2026
aef4a58
Merge branch 'main' of github.com:MetaMask/core into fix-assets-contr…
Prithpal-Sooriya Sep 11, 2026
01e0cbb
Merge branch 'main' of github.com:MetaMask/core into fix-assets-contr…
Prithpal-Sooriya Sep 11, 2026
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ linkStyle default opacity:0.5
assets_controller --> remote_feature_flag_controller;
assets_controller --> transaction_controller;
assets_controller --> utils;
assets_controller --> eth_json_rpc_provider;
assets_controllers --> account_tree_controller;
assets_controllers --> accounts_controller;
assets_controllers --> approval_controller;
Expand Down
4 changes: 4 additions & 0 deletions packages/assets-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `AccountsApiDataSource` now reads Accounts API `/v2/supportedNetworks` as CAIP-2 `fullSupport` and `partialSupport` string arrays, matching the current API payload
- Bump `@metamask/utils` from `^11.12.0` to `^12.0.0` ([#10192](https://github.com/MetaMask/core/pull/10192))

### Fixed

- Remove spam assets from balances, metadata, and detected assets regardless of asset ID casing, so filtered spam tokens no longer appear in state ([#10172](https://github.com/MetaMask/core/pull/10172))

## [16.0.0]

### Changed
Expand Down
1 change: 1 addition & 0 deletions packages/assets-controller/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
},
"devDependencies": {
"@metamask/auto-changelog": "^6.1.0",
"@metamask/eth-json-rpc-provider": "^7.0.0",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dev dependency only, aligned to best practice on RPC mocking.

"@types/jest": "^30.0.0",
"@types/lodash-es": "^4.17.12",
"@typescript/native": "npm:typescript@^7.0.2",
Expand Down

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also created an integration test on the AssetsController too 🎉

Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import type { ApiPlatformClient } from '@metamask/core-backend';
import { cleanAll } from 'nock';

import { mockBscSpamApis } from './__fixtures__/bsc-spam-token/api-responses/index.js';
import {
buildBscSpamAccount,
buildEmptyAssetsState,
getIgnoringCase,
} from './__fixtures__/bsc-spam-token/bscSpamWallet.js';
import { registerBscSpamControllerActions } from './__fixtures__/bsc-spam-token/messenger.js';
import {
BNB_ASSET_ID,
BSC_CHAIN_ID,
BSC_SPAM_ACCOUNT_ID,
CDOGE_ASSET_ID_CHECKSUM,
CDOGE_ASSET_ID_LOWERCASE,
} from './__fixtures__/bsc-spam-token/wallet.js';
import { createMockMessengers } from './__fixtures__/MockAssetControllerMessenger.js';
import type { MockRootMessenger } from './__fixtures__/MockAssetControllerMessenger.js';
import { createTestApiClient } from './__fixtures__/mockTokenApi.js';
import { waitFor, waitUntilStable } from './__fixtures__/test-utils.js';
import { AssetsController } from './AssetsController.js';
import type { AssetsControllerState } from './AssetsController.js';

/**
* Integration coverage for `AssetsController` against the BNB Chain wallet
* from the `$$$DOGECHAIN` (`CDOGE`) spam-token report.
*
* Boots the real controller, answers the same captured APIs as
* `buildFastFetchSources.bsc-spam-token-filtering.integration.test.ts`, and
* asserts CDOGE never lands in persisted state.
*
* Integration Expectation - CDOGE is correctly filtered out of controller state.
*/

type StateSurface = {
surface: string;
lookUp: (state: AssetsControllerState, assetId: string) => unknown;
};

const BALANCES: StateSurface = {
surface: 'balances',
lookUp: (state, assetId) =>
getIgnoringCase(state.assetsBalance[BSC_SPAM_ACCOUNT_ID] ?? {}, assetId),
};

const METADATA: StateSurface = {
surface: 'metadata',
lookUp: (state, assetId) => getIgnoringCase(state.assetsInfo, assetId),
};

const PRICES: StateSurface = {
surface: 'prices',
lookUp: (state, assetId) => getIgnoringCase(state.assetsPrice, assetId),
};

type WithControllerCallback<ReturnValue> = (args: {
controller: AssetsController;
messenger: MockRootMessenger;
}) => Promise<ReturnValue>;

async function withController<ReturnValue>(
{
state = buildEmptyAssetsState(),
queryApiClient = createTestApiClient(),
}: {
state?: Partial<AssetsControllerState>;
queryApiClient?: ApiPlatformClient;
},
fn: WithControllerCallback<ReturnValue>,
): Promise<ReturnValue> {
const { rootMessenger, assetsControllerMessenger } = createMockMessengers({
registerCustomRootActions: registerBscSpamControllerActions,
});

const controller = new AssetsController({
messenger: assetsControllerMessenger,
state,
queryApiClient,
isBasicFunctionality: (): boolean => true,
});

try {
return await fn({ controller, messenger: rootMessenger });
} finally {
controller.destroy();
queryApiClient.clear();
}
}

async function fetchWallet(
state: Partial<AssetsControllerState> = buildEmptyAssetsState(),
): Promise<AssetsControllerState> {
const { accountsSupportedNetworks } = mockBscSpamApis();

return await withController({ state }, async ({ controller }) => {
// wait for `AccountsApiDataSource` to ask `/v2/supportedNetworks` to indicate the fast-lane is ready
await waitFor(() => expect(accountsSupportedNetworks.isDone()).toBe(true));

await controller.getAssets([buildBscSpamAccount()], {
chainIds: [BSC_CHAIN_ID],
forceUpdate: true,
});

// `getAssets` awaits the fast lane only; the slow lane is fire-and-forget
// and can still be writing. Let state settle so the assertions about CDOGE
// being absent cannot pass just because nothing has landed yet.
await waitUntilStable(() => controller.state);

return controller.state;
});
}

const WALLET_PASSES = [
{
pass: 'first pass over a fresh wallet',
run: (): Promise<AssetsControllerState> => fetchWallet(),
},
{
pass: 'second pass over the wallet the first pass left behind',
run: async (): Promise<AssetsControllerState> => {
const firstPass = await fetchWallet();
cleanAll();

const secondPass = await fetchWallet(
buildEmptyAssetsState({
assetsBalance: firstPass.assetsBalance,
assetsInfo: firstPass.assetsInfo,
assetsPrice: firstPass.assetsPrice,
}),
);
return secondPass;
},
},
];

describe('AssetsController: BNB Chain spam token (CDOGE)', () => {
afterEach(() => {
cleanAll();
});

describe.each(WALLET_PASSES)('$pass', ({ run }) => {
let state: AssetsControllerState;

beforeAll(async () => {
state = await run();
});

it.each([BALANCES, METADATA])(
'$surface - filter out the spam token',
({ lookUp }) => {
expect(lookUp(state, CDOGE_ASSET_ID_LOWERCASE)).toBeUndefined();
expect(lookUp(state, CDOGE_ASSET_ID_CHECKSUM)).toBeUndefined();
},
);

it.each([BALANCES, METADATA])(
'$surface - keeps the native BNB asset despite low occurrences',
({ lookUp }) => {
expect(lookUp(state, BNB_ASSET_ID)).toBeDefined();
},
);

// Same gap as the pipeline suite: prices are not occurrence-filtered.
// Unlock cleanup eventually strips them; this flags the hole.
it.failing('keeps the spam token out of prices', () => {
expect(PRICES.lookUp(state, CDOGE_ASSET_ID_LOWERCASE)).toBeUndefined();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import type { InternalAccount } from '@metamask/keyring-internal-api';
import type { FeatureFlags } from '@metamask/remote-feature-flag-controller';

import {
createMockAssetControllerMessenger,
createMockInternalAccount,
createMockMessengers,
registerAssetsControllerActions,
} from './__fixtures__/MockAssetControllerMessenger.js';
import type { MockRootMessenger } from './__fixtures__/MockAssetControllerMessenger.js';
Expand Down Expand Up @@ -67,9 +67,6 @@ async function withController<ReturnValue>(
}: WithControllerOptions,
fn: WithControllerCallback<ReturnValue>,
): Promise<ReturnValue> {
const { rootMessenger, assetsControllerMessenger } =
createMockAssetControllerMessenger({ delegateGetState: false });

// Every account the wallet tracks balances for: the synthetic catch-all
// account plus the real custom-asset owner.
const accounts = [
Expand All @@ -87,11 +84,14 @@ async function withController<ReturnValue>(
),
];

registerAssetsControllerActions(rootMessenger, {
accounts,
enabledNetworkMap: { eip155: { '1': true, '10': true, '8453': true } },
nativeAssetIdentifiers: SCAM_WALLET_NATIVE_ASSET_IDENTIFIERS,
remoteFeatureFlags,
const { rootMessenger, assetsControllerMessenger } = createMockMessengers({
registerCustomRootActions: (messenger) =>
registerAssetsControllerActions(messenger, {
accounts,
enabledNetworkMap: { eip155: { '1': true, '10': true, '8453': true } },
nativeAssetIdentifiers: SCAM_WALLET_NATIVE_ASSET_IDENTIFIERS,
remoteFeatureFlags,
}),
Comment on lines +87 to +94

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Messenger mock reformatting.

});

const controller = new AssetsController({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import type { InternalAccount } from '@metamask/keyring-internal-api';
import type { FeatureFlags } from '@metamask/remote-feature-flag-controller';

import {
createMockAssetControllerMessenger,
createMockInternalAccount,
createMockMessengers,
registerAssetsControllerActions,
} from './__fixtures__/MockAssetControllerMessenger.js';
import type { MockRootMessenger } from './__fixtures__/MockAssetControllerMessenger.js';
Expand Down Expand Up @@ -89,8 +89,6 @@ async function withController<ReturnValue>(
}: WithControllerOptions,
fn: WithControllerCallback<ReturnValue>,
): Promise<ReturnValue> {
const { rootMessenger, assetsControllerMessenger } =
createMockAssetControllerMessenger({ delegateGetState: false });
const accounts = [
createMockInternalAccount({
id: ACCOUNT_ONE_ID,
Expand All @@ -104,11 +102,14 @@ async function withController<ReturnValue>(
}),
];

registerAssetsControllerActions(rootMessenger, {
accounts,
enabledNetworkMap: { eip155: { '1': true, '10': true } },
nativeAssetIdentifiers: { 'eip155:1': MAINNET_NATIVE },
remoteFeatureFlags,
const { rootMessenger, assetsControllerMessenger } = createMockMessengers({
registerCustomRootActions: (messenger) =>
registerAssetsControllerActions(messenger, {
accounts,
enabledNetworkMap: { eip155: { '1': true, '10': true } },
nativeAssetIdentifiers: { 'eip155:1': MAINNET_NATIVE },
remoteFeatureFlags,
}),
Comment on lines +105 to +112

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Messenger mock reformatting.

});

const controller = new AssetsController({
Expand Down
Loading