Skip to content

fix(assets-controller): token datasource filtering - #10172

Open
Prithpal-Sooriya wants to merge 17 commits into
mainfrom
fix-assets-controller-token-datasource-spam-bypass
Open

fix(assets-controller): token datasource filtering#10172
Prithpal-Sooriya wants to merge 17 commits into
mainfrom
fix-assets-controller-token-datasource-spam-bypass

Conversation

@Prithpal-Sooriya

@Prithpal-Sooriya Prithpal-Sooriya commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Explanation

Added integration tests to prove hole in scam filtering system.
Added a simple patch to correctly filter out scam tokens.

Demo

https://www.loom.com/share/6c5bd9f2517b42d7920799616231d2fe

Code Walkthrough

https://www.loom.com/share/ef93d877b2d24d6f8d382f34ac7c3a3d

References

N/A

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

Note

Medium Risk
Changes how spam tokens are removed from fetch responses before state is updated; incorrect matching could hide legitimate tokens or leave spam visible, though integration coverage targets the reported BSC case.

Overview
Fixes low-occurrence spam tokens (e.g. BNB Chain CDOGE) slipping into wallet state when asset IDs differ by checksum casing. TokenDataSource now centralizes removal in cleanResponseSpam, stripping filtered IDs from balances, metadata, and detected assets with case-insensitive matching instead of deleting balances by exact key only.

The fast fetch lane is extracted into a pipeline module (buildFastFetchSources, executeAssetsPipeline) so the same stack can run in integration tests without booting AssetsController. New BSC wallet integration tests (pipeline + full controller) lock in the CDOGE behavior; it.failing tests document that prices are still not occurrence-filtered.

Test helpers were refactored (createMockMessengers, BSC fixtures, waitUntilStable) and @metamask/eth-json-rpc-provider was added as a dev dependency for RPC mocks.

Reviewed by Cursor Bugbot for commit 01e0cbb. Bugbot is set up for automated code reviews on this repo. Configure here.

@Prithpal-Sooriya
Prithpal-Sooriya requested a review from a team as a code owner September 10, 2026 16:32

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.

real api data of https://accounts.api.cx.metamask.io/v2/supportedNetworks at the time

@Prithpal-Sooriya Prithpal-Sooriya Sep 10, 2026

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.

real api data of https://accounts.api.cx.metamask.io/v5/multiaccount/balances?accountIds=eip155%3A56%3A0xAAAAA at the time

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.

real data of https://tokens.api.cx.metamask.io/v2/supportedNetworks at the time

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.

real data of roughly https://token.api.cx.metamask.io/assets?assetIds=eip155:56/erc20:0xa7255c85232a42b5c602ed66c319da9af8433bb3&includeOccurrences=true at the time

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.

real data of https://token.api.cx.metamask.io/v1/suggestedOccurrenceFloors at the time

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.

real data of https://price.api.cx.metamask.io/v2/supportedNetworks at the time

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.

read data of https://price.api.cx.metamask.io/v3/spot-prices?assetIds= at the time

@Prithpal-Sooriya
Prithpal-Sooriya requested a review from a team as a code owner September 10, 2026 21:51
@Prithpal-Sooriya Prithpal-Sooriya changed the title Fix assets controller token datasource spam bypass fix(assets-controller) token datasource filtering Sep 10, 2026

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.

This file formulates the nock management.

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.

integration test utils

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.

integration test constants

Comment on lines +388 to +397
// Legitimate failing test, our middleware stack does not filter out spam asset prices!
// This does eventually get cleaned up during unlock cleanup, but worth flagging.
// eslint-disable-next-line jest/no-disabled-tests
it.skip('does not carry a price for the spam token', async () => {
const { response } = await runPipeline(buildEmptyAssetsState());
expect(
getIgnoringCase(response.assetsPrice ?? {}, CDOGE_ASSET_ID_LOWERCASE),
).toBeUndefined();
});
});

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.

Mentioned in the PR recording, we don't prevent spam prices from being added due to our architecture...

Not a dealbreaker since we do end up cleaning during the unlock lifecycle.

use test tables for a cleaner test implementation
This commit cleans up the integration test for the BSC spam token filtering by removing unnecessary functions and simplifying the logic for balance and asset ID retrieval. The changes enhance code readability and maintainability while ensuring the test remains functional.
for (const accountBalances of Object.values(response.assetsBalance)) {
for (const assetId of Object.keys(accountBalances)) {
if (spamLowerIds.has(assetId.toLowerCase())) {
delete (accountBalances as Record<string, unknown>)[assetId];

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.

deletion is now using the exact keys from the object - avoids this assetId possible casing issues.

if (response.assetsInfo) {
for (const assetId of Object.keys(response.assetsInfo)) {
if (spamLowerIds.has(assetId.toLowerCase())) {
delete response.assetsInfo[assetId as Caip19AssetId];

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.

deletion is now using the exact keys from the object - avoids this assetId possible casing issues.

Comment on lines +180 to +182
response.detectedAssets[accountId] = assetIds.filter(
(id) => !spamLowerIds.has(id.toLowerCase()),
);

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.

deletion is now using the exact keys from the object - avoids this assetId possible casing issues.

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.

The main fun integration test 😄

I think we can consider making a skill.

  1. Using extension open network tab and perform your actions.
  2. Export the network .har
  3. Have and agent (+ scripts) to create these mock fixtures for the integration test 🎉

Likewise you can also give the agent the account address, chain, and tokens you want to examine - it can make real API calls to then build fixtures.

Comment on lines +338 to +344
// Legitimate failing test, our middleware stack does not filter out spam
// asset prices! This does eventually get cleaned up during unlock cleanup,
// but worth flagging.
// eslint-disable-next-line jest/no-disabled-tests
it.skip('keeps the spam token out of prices', () => {
expect(PRICES.lookUp(response, CDOGE_ASSET_ID_LOWERCASE)).toBeUndefined();
});

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.

Discussed in the PR walkthrough.

Because our prices are ran in parallel with TokenDataSource, the PriceDataSource doesn't really have information to do any cleanup.

Not a deal-breaker, we will perform cleanup during unlock.

Our messenger mocks were poor:
- improper messenger registrations on global vs scoped messengers
- hacks on overwriting messenger publish
- mock subscriptions were out of order.

I've cleaned up the messenger to correctly manage rootMessenger vs scopedMessenger actions and events.

Also cleaned up the spam token integration messenger tests -- makes the integration test itself cleaner.
We have a code-smell, there should be no reason for our internal logic to call its own messenger to get state...

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.

sets up the messenger for these integration tests

Comment on lines -35 to -41
type MessengerWithPublish = {
publish: (event: string, ...args: unknown[]) => void;
registerActionHandler: (
action: string,
handler: (...args: unknown[]) => unknown,
) => void;
};

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.

Fixed test smell. Instead of hacking & injecting the messenger, use the events/actions on the global root messenger.

rootMessenger: MockRootMessenger;
assetsControllerMessenger: AssetsControllerMessenger;
} {
const { delegateGetState = true } = options ?? {};

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.

Fixed test smell: none of our code should be referencing internal actions. Added a comment in the RPCDataSource test.

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.

Revamped our internal test utils.

Instead of test code smells (like promise flushing), please use the waitFor or waitUntilStable utilities.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit bfc8864. Configure here.

Comment on lines +167 to +173
// TODO - code smell, why is our internal logic trying to call its own methods via messenger?
registerAssetsControllerStateMock(
assetsControllerMessenger,
actionHandlerOverrides?.['AssetsController:getState'] as
| (() => AssetsControllerState)
| undefined,
);

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.

Cleaned up test messenger.

As commented above, we shouldn't need to use the internal messenger to call the controller itself. Just use a callback and get state directly.

Comment on lines +124 to 130
const { rootMessenger, assetsControllerMessenger } = createMockMessengers({
registerCustomRootActions: (messenger) =>
registerStakedMessengerActions(messenger, {
enabledNetworkMap,
mockProvider,
}),
});

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 cleanup.

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.

Main fix is in this file 🎉

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.

Gut logic out of AssetsController so we can simplify the controller and also test in isolation!

This builds the fast lane, we can move other pipelines to this folder too as a fast follow.

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.

Gutted pipeline execution from the AssetsController. The AssetsController should not have owned this responsibility.

This code for is a 1:1.

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 🎉

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

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.

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

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 result = await chain({
request,
response: initialResponse,
return executeAssetsPipeline({

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.

Gutted this execution logic to executeAssetsPipeline.ts

Comment on lines +1544 to +1556
const fastSources = buildFastFetchSources(
{
accountsApiDataSource: this.#accountsApiDataSource,
stakedBalanceDataSource: this.#stakedBalanceDataSource,
customAssetGraduationMiddleware:
this.#customAssetGraduationMiddleware,
this.#rpcFallbackMiddleware,
this.#detectionMiddleware,
createParallelMiddleware([
this.#tokenDataSource,
this.#priceDataSource,
]),
]
: [this.#stakedBalanceDataSource, this.#detectionMiddleware];
rpcFallbackMiddleware: this.#rpcFallbackMiddleware,
detectionMiddleware: this.#detectionMiddleware,
tokenDataSource: this.#tokenDataSource,
priceDataSource: this.#priceDataSource,
},
{ isBasicFunctionality: this.#isBasicFunctionality() },
);

@Prithpal-Sooriya Prithpal-Sooriya Sep 11, 2026

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.

Gutted the fast lane pipeline generation to buildFastFetchSources.ts

This allows us better isolation for testing, debugging, visibility, etc.

@Prithpal-Sooriya Prithpal-Sooriya Sep 11, 2026

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.

We can do the other pipelines later, this PR is already too big.

},
"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.

…n_rpc_provider

- Added eth_json_rpc_provider to the assets_controller diagram in README.md.
- Updated tsconfig.build.json and tsconfig.json to include path for eth-json-rpc-provider.
@Prithpal-Sooriya Prithpal-Sooriya changed the title fix(assets-controller) token datasource filtering fix(assets-controller): token datasource filtering Sep 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant