From 3a50d58478dfabdd32047015f3d98cc66b4f77bc Mon Sep 17 00:00:00 2001 From: salimtb Date: Sat, 28 Mar 2026 19:20:31 +0100 Subject: [PATCH 1/5] feat: require messenger and Blockaid bulk scan in TokenDataSource --- eslint-suppressions.json | 7 +- packages/assets-controller/package.json | 1 + .../assets-controller/src/AssetsController.ts | 7 +- .../src/data-sources/TokenDataSource.test.ts | 141 ++++++++++++---- .../src/data-sources/TokenDataSource.ts | 150 +++++++++++++++--- packages/assets-controller/src/index.ts | 9 -- yarn.lock | 1 + 7 files changed, 246 insertions(+), 70 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index b5f5675bede..685aa296ca5 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -142,11 +142,6 @@ "count": 1 } }, - "packages/assets-controller/src/index.ts": { - "no-restricted-syntax": { - "count": 9 - } - }, "packages/assets-controllers/jest.environment.js": { "n/prefer-global/text-decoder": { "count": 1 @@ -1835,4 +1830,4 @@ "count": 1 } } -} +} \ No newline at end of file diff --git a/packages/assets-controller/package.json b/packages/assets-controller/package.json index 6706714c713..50fb5aaaf93 100644 --- a/packages/assets-controller/package.json +++ b/packages/assets-controller/package.json @@ -65,6 +65,7 @@ "@metamask/network-controller": "^30.0.1", "@metamask/network-enablement-controller": "^5.0.1", "@metamask/permission-controller": "^12.3.0", + "@metamask/phishing-controller": "^17.1.0", "@metamask/polling-controller": "^16.0.4", "@metamask/preferences-controller": "^23.1.0", "@metamask/snaps-controllers": "^19.0.0", diff --git a/packages/assets-controller/src/AssetsController.ts b/packages/assets-controller/src/AssetsController.ts index 32b15259a5c..16caca6df08 100644 --- a/packages/assets-controller/src/AssetsController.ts +++ b/packages/assets-controller/src/AssetsController.ts @@ -38,6 +38,7 @@ import type { GetPermissions, PermissionControllerStateChange, } from '@metamask/permission-controller'; +import { PhishingControllerBulkScanTokensAction } from '@metamask/phishing-controller'; import type { PreferencesControllerStateChangeEvent } from '@metamask/preferences-controller'; import type { SnapControllerGetRunnableSnapsAction, @@ -275,7 +276,9 @@ type AllowedActions = | SnapControllerHandleRequestAction | GetPermissions // BackendWebsocketDataSource - | BackendWebSocketServiceActions; + | BackendWebSocketServiceActions + // PhishingController + | PhishingControllerBulkScanTokensAction; type AllowedEvents = // AssetsController @@ -737,7 +740,7 @@ export class AssetsController extends BaseController< onActiveChainsUpdated: this.#onActiveChainsUpdated, ...stakedBalanceDataSourceConfig, }); - this.#tokenDataSource = new TokenDataSource({ + this.#tokenDataSource = new TokenDataSource(this.messenger, { queryApiClient, getNativeAssetIds: (): string[] => { const { nativeAssetIdentifiers } = this.messenger.call( diff --git a/packages/assets-controller/src/data-sources/TokenDataSource.test.ts b/packages/assets-controller/src/data-sources/TokenDataSource.test.ts index be1bb8fe3b2..8bf05d7e987 100644 --- a/packages/assets-controller/src/data-sources/TokenDataSource.test.ts +++ b/packages/assets-controller/src/data-sources/TokenDataSource.test.ts @@ -1,14 +1,19 @@ import type { V3AssetResponse } from '@metamask/core-backend'; import { Messenger, MOCK_ANY_NAMESPACE } from '@metamask/messenger'; import type { MockAnyNamespace } from '@metamask/messenger'; +import type { + BulkTokenScanResponse, + PhishingControllerBulkScanTokensAction, +} from '@metamask/phishing-controller'; +import { TokenScanResultType } from '@metamask/phishing-controller'; import type { TokenDataSourceOptions } from './TokenDataSource'; import { TokenDataSource } from './TokenDataSource'; +import type { AssetsControllerMessenger } from '../AssetsController'; import type { Context, DataRequest, Caip19AssetId, ChainId } from '../types'; -type AllActions = never; +type AllActions = PhishingControllerBulkScanTokensAction; type AllEvents = never; -type RootMessenger = Messenger; const CHAIN_MAINNET = 'eip155:1' as ChainId; const MOCK_ADDRESS = '0x1234567890123456789012345678901234567890'; @@ -27,10 +32,23 @@ type MockApiClient = { type SetupResult = { controller: TokenDataSource; - messenger: RootMessenger; + messenger: AssetsControllerMessenger; apiClient: MockApiClient; }; +function createTestMessenger( + bulkScanTokens: PhishingControllerBulkScanTokensAction['handler'] = async (): Promise => ({}), +): AssetsControllerMessenger { + const rootMessenger = new Messenger({ + namespace: MOCK_ANY_NAMESPACE, + }); + rootMessenger.registerActionHandler( + 'PhishingController:bulkScanTokens', + bulkScanTokens, + ); + return rootMessenger as unknown as AssetsControllerMessenger; +} + function createMockApiClient( supportedNetworks: string[] = ['eip155:1'], assetsResponse: V3AssetResponse[] = [], @@ -103,26 +121,22 @@ function createMiddlewareContext(overrides?: Partial): Context { }; } -function setupController( - options: { - supportedNetworks?: string[]; - assetsResponse?: V3AssetResponse[]; - nativeAssetIds?: string[]; - } = {}, -): SetupResult { +function setupController(options: { + messenger: AssetsControllerMessenger; + supportedNetworks?: string[]; + assetsResponse?: V3AssetResponse[]; + nativeAssetIds?: string[]; +}): SetupResult { const { + messenger, supportedNetworks = ['eip155:1'], assetsResponse = [], nativeAssetIds = [], } = options; - const rootMessenger = new Messenger({ - namespace: MOCK_ANY_NAMESPACE, - }); - const apiClient = createMockApiClient(supportedNetworks, assetsResponse); - const controller = new TokenDataSource({ + const controller = new TokenDataSource(messenger, { queryApiClient: apiClient as unknown as TokenDataSourceOptions['queryApiClient'], getNativeAssetIds: (): string[] => nativeAssetIds, @@ -130,7 +144,7 @@ function setupController( return { controller, - messenger: rootMessenger, + messenger, apiClient, }; } @@ -141,12 +155,16 @@ describe('TokenDataSource', () => { }); it('initializes with correct name', () => { - const { controller } = setupController(); + const { controller } = setupController({ + messenger: createTestMessenger(), + }); expect(controller.name).toBe('TokenDataSource'); }); it('exposes assetsMiddleware on instance', () => { - const { controller } = setupController(); + const { controller } = setupController({ + messenger: createTestMessenger(), + }); const middleware = controller.assetsMiddleware; expect(middleware).toBeDefined(); @@ -154,7 +172,9 @@ describe('TokenDataSource', () => { }); it('middleware passes to next when no detected assets', async () => { - const { controller } = setupController(); + const { controller } = setupController({ + messenger: createTestMessenger(), + }); const next = jest.fn().mockResolvedValue(undefined); const context = createMiddlewareContext({ @@ -167,7 +187,9 @@ describe('TokenDataSource', () => { }); it('middleware passes to next when detected assets is empty', async () => { - const { controller } = setupController(); + const { controller } = setupController({ + messenger: createTestMessenger(), + }); const next = jest.fn().mockResolvedValue(undefined); const context = createMiddlewareContext({ @@ -181,6 +203,7 @@ describe('TokenDataSource', () => { it('middleware fetches metadata for detected assets', async () => { const { controller, apiClient } = setupController({ + messenger: createTestMessenger(), supportedNetworks: ['eip155:1'], assetsResponse: [createMockAssetResponse(MOCK_TOKEN_ASSET)], }); @@ -230,6 +253,7 @@ describe('TokenDataSource', () => { it('middleware skips assets with existing metadata containing image in response', async () => { const { controller, apiClient } = setupController({ + messenger: createTestMessenger(), supportedNetworks: ['eip155:1'], }); @@ -259,6 +283,7 @@ describe('TokenDataSource', () => { it('middleware skips assets with existing metadata containing image in state', async () => { const { controller, apiClient } = setupController({ + messenger: createTestMessenger(), supportedNetworks: ['eip155:1'], }); @@ -290,6 +315,7 @@ describe('TokenDataSource', () => { it('middleware fetches metadata for assets without image in existing metadata', async () => { const { controller, apiClient } = setupController({ + messenger: createTestMessenger(), supportedNetworks: ['eip155:1'], assetsResponse: [createMockAssetResponse(MOCK_TOKEN_ASSET)], }); @@ -332,6 +358,7 @@ describe('TokenDataSource', () => { 'eip155:137/erc20:0x0000000000000000000000000000000000001010' as Caip19AssetId; const { controller, apiClient } = setupController({ + messenger: createTestMessenger(), supportedNetworks: ['eip155:1'], assetsResponse: [createMockAssetResponse(MOCK_TOKEN_ASSET)], }); @@ -366,6 +393,7 @@ describe('TokenDataSource', () => { 'eip155:137/erc20:0x0000000000000000000000000000000000001010' as Caip19AssetId; const { controller, apiClient } = setupController({ + messenger: createTestMessenger(), supportedNetworks: ['eip155:1'], }); @@ -385,7 +413,9 @@ describe('TokenDataSource', () => { }); it('middleware handles getSupportedNetworks error gracefully', async () => { - const { controller, apiClient } = setupController(); + const { controller, apiClient } = setupController({ + messenger: createTestMessenger(), + }); apiClient.tokens.fetchTokenV2SupportedNetworks.mockRejectedValueOnce( new Error('Network Error'), @@ -408,6 +438,7 @@ describe('TokenDataSource', () => { it('middleware handles fetchV3Assets error gracefully', async () => { const { controller, apiClient } = setupController({ + messenger: createTestMessenger(), supportedNetworks: ['eip155:1'], }); @@ -431,6 +462,7 @@ describe('TokenDataSource', () => { it('middleware transforms native asset type correctly', async () => { const { controller } = setupController({ + messenger: createTestMessenger(), supportedNetworks: ['eip155:1'], assetsResponse: [ createMockAssetResponse(MOCK_NATIVE_ASSET, { @@ -458,6 +490,7 @@ describe('TokenDataSource', () => { it('middleware transforms SPL token type correctly', async () => { const { controller } = setupController({ + messenger: createTestMessenger(), supportedNetworks: ['solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'], assetsResponse: [ createMockAssetResponse(MOCK_SPL_ASSET, { @@ -487,6 +520,7 @@ describe('TokenDataSource', () => { 'eip155:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f' as Caip19AssetId; const { controller } = setupController({ + messenger: createTestMessenger(), supportedNetworks: ['eip155:1'], assetsResponse: [createMockAssetResponse(MOCK_TOKEN_ASSET)], }); @@ -520,6 +554,7 @@ describe('TokenDataSource', () => { 'eip155:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f' as Caip19AssetId; const { controller, apiClient } = setupController({ + messenger: createTestMessenger(), supportedNetworks: ['eip155:1'], assetsResponse: [ createMockAssetResponse(MOCK_TOKEN_ASSET), @@ -560,6 +595,7 @@ describe('TokenDataSource', () => { it('middleware deduplicates assets across accounts', async () => { const { controller, apiClient } = setupController({ + messenger: createTestMessenger(), supportedNetworks: ['eip155:1'], assetsResponse: [createMockAssetResponse(MOCK_TOKEN_ASSET)], }); @@ -591,7 +627,9 @@ describe('TokenDataSource', () => { }); it('middleware includes partial support networks', async () => { - const { controller, apiClient } = setupController(); + const { controller, apiClient } = setupController({ + messenger: createTestMessenger(), + }); apiClient.tokens.fetchTokenV2SupportedNetworks.mockResolvedValueOnce({ fullSupport: [], @@ -628,6 +666,7 @@ describe('TokenDataSource', () => { it('middleware filters out invalid CAIP asset IDs', async () => { const { controller, apiClient } = setupController({ + messenger: createTestMessenger(), supportedNetworks: ['eip155:1'], assetsResponse: [createMockAssetResponse(MOCK_TOKEN_ASSET)], }); @@ -660,15 +699,35 @@ describe('TokenDataSource', () => { ); }); - it('middleware filters out non-native assets with occurrences < 3', async () => { - const lowOccurrenceAsset = + it('middleware filters out erc20 assets flagged malicious by Blockaid', async () => { + const maliciousAsset = 'eip155:1/erc20:0x1111111111111111111111111111111111111111' as Caip19AssetId; const { controller } = setupController({ + messenger: createTestMessenger(async ({ chainId, tokens }) => { + expect(chainId).toBe('0x1'); + const out: BulkTokenScanResponse = {}; + for (const addr of tokens) { + const lower = addr.toLowerCase(); + out[lower] = + lower === '0x1111111111111111111111111111111111111111' + ? { + result_type: TokenScanResultType.Malicious, + chain: chainId, + address: lower, + } + : { + result_type: TokenScanResultType.Benign, + chain: chainId, + address: lower, + }; + } + return out; + }), supportedNetworks: ['eip155:1'], assetsResponse: [ createMockAssetResponse(MOCK_TOKEN_ASSET, { occurrences: 5 }), - createMockAssetResponse(lowOccurrenceAsset, { occurrences: 2 }), + createMockAssetResponse(maliciousAsset, { occurrences: 99 }), ], }); @@ -676,12 +735,12 @@ describe('TokenDataSource', () => { const context = createMiddlewareContext({ response: { detectedAssets: { - 'mock-account-id': [MOCK_TOKEN_ASSET, lowOccurrenceAsset], + 'mock-account-id': [MOCK_TOKEN_ASSET, maliciousAsset], }, assetsBalance: { 'mock-account-id': { [MOCK_TOKEN_ASSET]: { amount: '100' }, - [lowOccurrenceAsset]: { amount: '50' }, + [maliciousAsset]: { amount: '50' }, }, }, }, @@ -690,27 +749,39 @@ describe('TokenDataSource', () => { await controller.assetsMiddleware(context, next); expect(context.response.assetsInfo?.[MOCK_TOKEN_ASSET]).toBeDefined(); - expect(context.response.assetsInfo?.[lowOccurrenceAsset]).toBeUndefined(); + expect(context.response.assetsInfo?.[maliciousAsset]).toBeUndefined(); const accountBalances = context.response.assetsBalance?.[ 'mock-account-id' ] as Record | undefined; expect(accountBalances?.[MOCK_TOKEN_ASSET]).toBeDefined(); - expect(accountBalances?.[lowOccurrenceAsset]).toBeUndefined(); + expect(accountBalances?.[maliciousAsset]).toBeUndefined(); expect(context.response.detectedAssets?.['mock-account-id']).toContain( MOCK_TOKEN_ASSET, ); expect(context.response.detectedAssets?.['mock-account-id']).not.toContain( - lowOccurrenceAsset, + maliciousAsset, ); }); - it('middleware filters out non-native assets with undefined occurrences', async () => { + it('middleware keeps non-native assets with undefined occurrences when Blockaid reports benign', async () => { const noOccurrenceAsset = 'eip155:1/erc20:0x2222222222222222222222222222222222222222' as Caip19AssetId; const { controller } = setupController({ + messenger: createTestMessenger(async ({ tokens }) => { + const out: BulkTokenScanResponse = {}; + for (const addr of tokens) { + const lower = addr.toLowerCase(); + out[lower] = { + result_type: TokenScanResultType.Benign, + chain: '0x1', + address: lower, + }; + } + return out; + }), supportedNetworks: ['eip155:1'], assetsResponse: [ createMockAssetResponse(noOccurrenceAsset, { occurrences: undefined }), @@ -728,11 +799,12 @@ describe('TokenDataSource', () => { await controller.assetsMiddleware(context, next); - expect(context.response.assetsInfo?.[noOccurrenceAsset]).toBeUndefined(); + expect(context.response.assetsInfo?.[noOccurrenceAsset]).toBeDefined(); }); it('middleware keeps native assets regardless of occurrences', async () => { const { controller } = setupController({ + messenger: createTestMessenger(), supportedNetworks: ['eip155:1'], nativeAssetIds: [MOCK_NATIVE_ASSET], assetsResponse: [ @@ -759,6 +831,7 @@ describe('TokenDataSource', () => { it('middleware always includes native asset IDs in the fetch', async () => { const { controller, apiClient } = setupController({ + messenger: createTestMessenger(), supportedNetworks: ['eip155:1'], nativeAssetIds: [MOCK_NATIVE_ASSET], assetsResponse: [ @@ -793,6 +866,7 @@ describe('TokenDataSource', () => { it('middleware fetches native asset IDs even when detectedAssets is undefined', async () => { const { controller, apiClient } = setupController({ + messenger: createTestMessenger(), supportedNetworks: ['eip155:1'], nativeAssetIds: [MOCK_NATIVE_ASSET], assetsResponse: [ @@ -821,6 +895,7 @@ describe('TokenDataSource', () => { it('middleware fetches native asset IDs when detectedAssets is an empty object', async () => { const { controller, apiClient } = setupController({ + messenger: createTestMessenger(), supportedNetworks: ['eip155:1'], nativeAssetIds: [MOCK_NATIVE_ASSET], assetsResponse: [ @@ -851,6 +926,7 @@ describe('TokenDataSource', () => { it('middleware deduplicates native asset IDs with detected assets', async () => { const { controller, apiClient } = setupController({ + messenger: createTestMessenger(), supportedNetworks: ['eip155:1'], nativeAssetIds: [MOCK_NATIVE_ASSET], assetsResponse: [ @@ -882,6 +958,7 @@ describe('TokenDataSource', () => { const polygonNativeAsset = 'eip155:137/slip44:966' as Caip19AssetId; const { controller, apiClient } = setupController({ + messenger: createTestMessenger(), supportedNetworks: ['eip155:1', 'eip155:137'], nativeAssetIds: [MOCK_NATIVE_ASSET, polygonNativeAsset], assetsResponse: [ diff --git a/packages/assets-controller/src/data-sources/TokenDataSource.ts b/packages/assets-controller/src/data-sources/TokenDataSource.ts index 8a7a742c7a6..f15d6540a3d 100644 --- a/packages/assets-controller/src/data-sources/TokenDataSource.ts +++ b/packages/assets-controller/src/data-sources/TokenDataSource.ts @@ -1,9 +1,15 @@ import type { V3AssetResponse } from '@metamask/core-backend'; import { ApiPlatformClient } from '@metamask/core-backend'; -import { parseCaipAssetType } from '@metamask/utils'; +import type { + BulkTokenScanResponse, + PhishingControllerBulkScanTokensAction, +} from '@metamask/phishing-controller'; +import { TokenScanResultType } from '@metamask/phishing-controller'; +import { numberToHex, parseCaipAssetType } from '@metamask/utils'; import type { CaipAssetType } from '@metamask/utils'; import { isStakingContractAssetId } from './evm-rpc-services'; +import type { AssetsControllerMessenger } from '../AssetsController'; import { projectLogger, createModuleLogger } from '../logger'; import { forDataTypes } from '../types'; import type { @@ -19,19 +25,10 @@ import type { const CONTROLLER_NAME = 'TokenDataSource'; -const MIN_TOKEN_OCCURRENCES = 3; - const log = createModuleLogger(projectLogger, CONTROLLER_NAME); -// ============================================================================ -// MESSENGER TYPES -// ============================================================================ - -/** - * TokenDataSource does not call external messenger actions. - * It uses ApiPlatformClient directly. - */ -export type TokenDataSourceAllowedActions = never; +/** Max tokens per PhishingController:bulkScanTokens request (see PhishingController). */ +const BULK_SCAN_BATCH_SIZE = 100; // ============================================================================ // OPTIONS @@ -44,6 +41,14 @@ export type TokenDataSourceOptions = { getNativeAssetIds: () => string[]; }; +/** + * Messenger actions `TokenDataSource` may invoke (via {@link AssetsControllerMessenger}). + * Not re-exported from the package public `index` (repo ESLint); import from this module when + * typing a messenger in the same package or tests. + */ +export type TokenDataSourceAllowedActions = + PhishingControllerBulkScanTokensAction; + // ============================================================================ // HELPER FUNCTIONS // ============================================================================ @@ -109,7 +114,8 @@ function transformV3AssetResponseToMetadata( * - Fetches metadata from Tokens API v3 for assets needing enrichment * - Merges fetched metadata into the response * - * Usage: Create with queryApiClient and use assetsMiddleware; no messenger required. + * Pass the same {@link AssetsControllerMessenger} as other data sources for Blockaid + * token scans. */ export class TokenDataSource { readonly name = CONTROLLER_NAME; @@ -124,7 +130,14 @@ export class TokenDataSource { /** Returns CAIP-19 native asset IDs from NetworkEnablementController state */ readonly #getNativeAssetIds: () => string[]; - constructor(options: TokenDataSourceOptions) { + /** Shared controller messenger — used for `PhishingController:bulkScanTokens`. */ + readonly #messenger: AssetsControllerMessenger; + + constructor( + messenger: AssetsControllerMessenger, + options: TokenDataSourceOptions, + ) { + this.#messenger = messenger; this.#apiClient = options.queryApiClient; this.#getNativeAssetIds = options.getNativeAssetIds; } @@ -177,6 +190,102 @@ export class TokenDataSource { }); } + /** + * Filters out tokens flagged as malicious by Blockaid via + * `PhishingController:bulkScanTokens`. EVM ERC-20 assets (`erc20` + `eip155`) + * are scanned with a hex chain ID; non-EVM fungible `token` assets use + * `chain.namespace` (same pattern as MultichainAssetsController). Native + * (`slip44`) and other namespaces are not scanned. If the scan fails, all + * tokens are kept (fail open). + * + * @param assets - CAIP-19 asset IDs to filter. + * @returns Asset IDs with malicious tokens removed. + */ + async #filterBlockaidSpamTokens(assets: string[]): Promise { + if (assets.length === 0) { + return assets; + } + + const tokensByChain: Record = + {}; + + for (const asset of assets) { + try { + const { assetNamespace, assetReference, chain } = parseCaipAssetType( + asset as CaipAssetType, + ); + + if (assetNamespace === 'slip44') { + continue; + } + + if (assetNamespace === 'erc20' && chain.namespace === 'eip155') { + const chainIdHex = numberToHex(parseInt(chain.reference, 10)); + if (!tokensByChain[chainIdHex]) { + tokensByChain[chainIdHex] = []; + } + tokensByChain[chainIdHex].push({ asset, address: assetReference }); + } else if (assetNamespace === 'token') { + const chainName = chain.namespace; + if (!tokensByChain[chainName]) { + tokensByChain[chainName] = []; + } + tokensByChain[chainName].push({ asset, address: assetReference }); + } + } catch { + // Malformed or unsupported for bulk scan — keep asset (fail open) + } + } + + if (Object.keys(tokensByChain).length === 0) { + return assets; + } + + const rejectedAssets = new Set(); + + try { + for (const [chainId, tokenEntries] of Object.entries(tokensByChain)) { + const addresses = tokenEntries.map((entry) => entry.address); + const batches: string[][] = []; + for (let i = 0; i < addresses.length; i += BULK_SCAN_BATCH_SIZE) { + batches.push(addresses.slice(i, i + BULK_SCAN_BATCH_SIZE)); + } + + const batchResults = await Promise.allSettled( + batches.map((batch) => + this.#messenger.call('PhishingController:bulkScanTokens', { + chainId, + tokens: batch, + }), + ), + ); + + const scanResponse: BulkTokenScanResponse = {}; + for (const result of batchResults) { + if (result.status === 'fulfilled') { + Object.assign(scanResponse, result.value); + } + } + + for (const entry of tokenEntries) { + const addressKey = chainId.startsWith('0x') + ? entry.address.toLowerCase() + : entry.address; + const result = + scanResponse[addressKey] ?? scanResponse[entry.address]; + if (result?.result_type === TokenScanResultType.Malicious) { + rejectedAssets.add(entry.asset); + } + } + } + } catch (error) { + log('Blockaid bulk token scan failed; keeping all tokens', { error }); + return assets; + } + + return assets.filter((asset) => !rejectedAssets.has(asset)); + } + /** * Get the middleware for enriching responses with token metadata. * @@ -258,18 +367,17 @@ export class TokenDataSource { }, ); + const assetIdsFromApi = metadataResponse.map((a) => a.assetId); + const allowedAssetIds = new Set( + await this.#filterBlockaidSpamTokens(assetIdsFromApi), + ); + response.assetsInfo ??= {}; const filteredOutAssets = new Set(); for (const assetData of metadataResponse) { - const parsed = parseCaipAssetType(assetData.assetId as CaipAssetType); - const isNative = parsed.assetNamespace === 'slip44'; - - if ( - !isNative && - (assetData.occurrences ?? 0) < MIN_TOKEN_OCCURRENCES - ) { + if (!allowedAssetIds.has(assetData.assetId)) { filteredOutAssets.add(assetData.assetId); continue; } diff --git a/packages/assets-controller/src/index.ts b/packages/assets-controller/src/index.ts index 7be1e0b9e13..ecf90601115 100644 --- a/packages/assets-controller/src/index.ts +++ b/packages/assets-controller/src/index.ts @@ -30,7 +30,6 @@ export type { AssetsControllerUnhideAssetAction, AssetsControllerGetExchangeRatesForBridgeAction, AssetsControllerGetStateForTransactionPayAction, - AssetsControllerMethodActions, } from './AssetsController-method-action-types'; // Core types @@ -99,7 +98,6 @@ export type { AccountsApiDataSourceConfig, AccountsApiDataSourceOptions, AccountsApiDataSourceState, - AccountsApiDataSourceAllowedActions, } from './data-sources'; // Data sources - BackendWebsocket @@ -111,8 +109,6 @@ export { export type { BackendWebsocketDataSourceOptions, BackendWebsocketDataSourceState, - BackendWebsocketDataSourceAllowedActions, - BackendWebsocketDataSourceAllowedEvents, } from './data-sources'; // Data sources - RPC @@ -122,8 +118,6 @@ export type { RpcDataSourceConfig, RpcDataSourceOptions, RpcDataSourceState, - RpcDataSourceAllowedActions, - RpcDataSourceAllowedEvents, ChainStatus, } from './data-sources'; @@ -142,8 +136,6 @@ export { export type { SnapDataSourceState, SnapDataSourceOptions, - SnapDataSourceAllowedActions, - SnapDataSourceAllowedEvents, } from './data-sources'; // Enrichment data sources @@ -151,7 +143,6 @@ export { TokenDataSource, PriceDataSource } from './data-sources'; export type { TokenDataSourceOptions, - TokenDataSourceAllowedActions, PriceDataSourceConfig, PriceDataSourceOptions, } from './data-sources'; diff --git a/yarn.lock b/yarn.lock index f133a906283..db622ea7b02 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2784,6 +2784,7 @@ __metadata: "@metamask/network-controller": "npm:^30.0.1" "@metamask/network-enablement-controller": "npm:^5.0.1" "@metamask/permission-controller": "npm:^12.3.0" + "@metamask/phishing-controller": "npm:^17.1.0" "@metamask/polling-controller": "npm:^16.0.4" "@metamask/preferences-controller": "npm:^23.1.0" "@metamask/snaps-controllers": "npm:^19.0.0" From 8ae2be3db8cabad6def266ab34b85731e2eacb69 Mon Sep 17 00:00:00 2001 From: salimtb Date: Sat, 28 Mar 2026 19:29:41 +0100 Subject: [PATCH 2/5] fix: fix lint --- eslint-suppressions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 685aa296ca5..040bf296a57 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -1830,4 +1830,4 @@ "count": 1 } } -} \ No newline at end of file +} From c03d95169edabc0137b2810f43421f693b8823c4 Mon Sep 17 00:00:00 2001 From: salimtb Date: Sat, 28 Mar 2026 19:33:28 +0100 Subject: [PATCH 3/5] fix: fix lint --- eslint-suppressions.json | 5 +++++ packages/assets-controller/CHANGELOG.md | 6 ++++++ packages/assets-controller/src/index.ts | 1 + 3 files changed, 12 insertions(+) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 040bf296a57..b5f5675bede 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -142,6 +142,11 @@ "count": 1 } }, + "packages/assets-controller/src/index.ts": { + "no-restricted-syntax": { + "count": 9 + } + }, "packages/assets-controllers/jest.environment.js": { "n/prefer-global/text-decoder": { "count": 1 diff --git a/packages/assets-controller/CHANGELOG.md b/packages/assets-controller/CHANGELOG.md index 9e975019878..5b1a1810fe5 100644 --- a/packages/assets-controller/CHANGELOG.md +++ b/packages/assets-controller/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +-`TokenDataSource` constructor now takes `(messenger, options)` instead of `(options)`; `messenger` must be the same `AssetsControllerMessenger` used by `AssetsController` so token metadata enrichment can call `PhishingController:bulkScanTokens` ([#8329](https://github.com/MetaMask/core/pull/8329)) + +- `TokenDataSource` removes tokens flagged malicious by Blockaid (via `PhishingController:bulkScanTokens`) before merging metadata, instead of filtering non-native tokens by a minimum occurrence count ([#8329](https://github.com/MetaMask/core/pull/8329)) + ## [3.2.1] ### Changed diff --git a/packages/assets-controller/src/index.ts b/packages/assets-controller/src/index.ts index ecf90601115..7591f28168f 100644 --- a/packages/assets-controller/src/index.ts +++ b/packages/assets-controller/src/index.ts @@ -30,6 +30,7 @@ export type { AssetsControllerUnhideAssetAction, AssetsControllerGetExchangeRatesForBridgeAction, AssetsControllerGetStateForTransactionPayAction, + AssetsControllerMethodActions, } from './AssetsController-method-action-types'; // Core types From 7af3044daa85ff6512d0b3b9bdb7f1ff5fe315db Mon Sep 17 00:00:00 2001 From: salimtb Date: Sat, 28 Mar 2026 19:36:19 +0100 Subject: [PATCH 4/5] fix: fix changelog --- packages/assets-controller/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/assets-controller/CHANGELOG.md b/packages/assets-controller/CHANGELOG.md index 5b1a1810fe5..7c9cf77b67c 100644 --- a/packages/assets-controller/CHANGELOG.md +++ b/packages/assets-controller/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed --`TokenDataSource` constructor now takes `(messenger, options)` instead of `(options)`; `messenger` must be the same `AssetsControllerMessenger` used by `AssetsController` so token metadata enrichment can call `PhishingController:bulkScanTokens` ([#8329](https://github.com/MetaMask/core/pull/8329)) +- `TokenDataSource` constructor now takes `(messenger, options)` instead of `(options)`; `messenger` must be the same `AssetsControllerMessenger` used by `AssetsController` so token metadata enrichment can call `PhishingController:bulkScanTokens` ([#8329](https://github.com/MetaMask/core/pull/8329)) - `TokenDataSource` removes tokens flagged malicious by Blockaid (via `PhishingController:bulkScanTokens`) before merging metadata, instead of filtering non-native tokens by a minimum occurrence count ([#8329](https://github.com/MetaMask/core/pull/8329)) From 246ad42b8500f36d691653f7dfe3274061954dd1 Mon Sep 17 00:00:00 2001 From: salimtb Date: Sat, 28 Mar 2026 19:50:18 +0100 Subject: [PATCH 5/5] fix: fix linter --- eslint-suppressions.json | 5 ----- packages/assets-controller/src/index.ts | 1 - 2 files changed, 6 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index b5f5675bede..040bf296a57 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -142,11 +142,6 @@ "count": 1 } }, - "packages/assets-controller/src/index.ts": { - "no-restricted-syntax": { - "count": 9 - } - }, "packages/assets-controllers/jest.environment.js": { "n/prefer-global/text-decoder": { "count": 1 diff --git a/packages/assets-controller/src/index.ts b/packages/assets-controller/src/index.ts index 7591f28168f..ecf90601115 100644 --- a/packages/assets-controller/src/index.ts +++ b/packages/assets-controller/src/index.ts @@ -30,7 +30,6 @@ export type { AssetsControllerUnhideAssetAction, AssetsControllerGetExchangeRatesForBridgeAction, AssetsControllerGetStateForTransactionPayAction, - AssetsControllerMethodActions, } from './AssetsController-method-action-types'; // Core types