@metamask/assets-controller 3.3.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/dist/AssetsController.cjs +69 -28
  3. package/dist/AssetsController.cjs.map +1 -1
  4. package/dist/AssetsController.d.cts +4 -3
  5. package/dist/AssetsController.d.cts.map +1 -1
  6. package/dist/AssetsController.d.mts +4 -3
  7. package/dist/AssetsController.d.mts.map +1 -1
  8. package/dist/AssetsController.mjs +69 -28
  9. package/dist/AssetsController.mjs.map +1 -1
  10. package/dist/data-sources/PriceDataSource.cjs +63 -38
  11. package/dist/data-sources/PriceDataSource.cjs.map +1 -1
  12. package/dist/data-sources/PriceDataSource.d.cts.map +1 -1
  13. package/dist/data-sources/PriceDataSource.d.mts.map +1 -1
  14. package/dist/data-sources/PriceDataSource.mjs +63 -38
  15. package/dist/data-sources/PriceDataSource.mjs.map +1 -1
  16. package/dist/data-sources/RpcDataSource.cjs +26 -59
  17. package/dist/data-sources/RpcDataSource.cjs.map +1 -1
  18. package/dist/data-sources/RpcDataSource.d.cts +1 -2
  19. package/dist/data-sources/RpcDataSource.d.cts.map +1 -1
  20. package/dist/data-sources/RpcDataSource.d.mts +1 -2
  21. package/dist/data-sources/RpcDataSource.d.mts.map +1 -1
  22. package/dist/data-sources/RpcDataSource.mjs +28 -61
  23. package/dist/data-sources/RpcDataSource.mjs.map +1 -1
  24. package/dist/data-sources/StakedBalanceDataSource.cjs +9 -2
  25. package/dist/data-sources/StakedBalanceDataSource.cjs.map +1 -1
  26. package/dist/data-sources/StakedBalanceDataSource.d.cts.map +1 -1
  27. package/dist/data-sources/StakedBalanceDataSource.d.mts.map +1 -1
  28. package/dist/data-sources/StakedBalanceDataSource.mjs +9 -2
  29. package/dist/data-sources/StakedBalanceDataSource.mjs.map +1 -1
  30. package/dist/data-sources/TokenDataSource.cjs +138 -12
  31. package/dist/data-sources/TokenDataSource.cjs.map +1 -1
  32. package/dist/data-sources/TokenDataSource.d.cts +11 -7
  33. package/dist/data-sources/TokenDataSource.d.cts.map +1 -1
  34. package/dist/data-sources/TokenDataSource.d.mts +11 -7
  35. package/dist/data-sources/TokenDataSource.d.mts.map +1 -1
  36. package/dist/data-sources/TokenDataSource.mjs +140 -14
  37. package/dist/data-sources/TokenDataSource.mjs.map +1 -1
  38. package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.cjs +67 -0
  39. package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.cjs.map +1 -0
  40. package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.d.cts +23 -0
  41. package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.d.cts.map +1 -0
  42. package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.d.mts +23 -0
  43. package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.d.mts.map +1 -0
  44. package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.mjs +63 -0
  45. package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.mjs.map +1 -0
  46. package/dist/data-sources/evm-rpc-services/clients/index.cjs +3 -1
  47. package/dist/data-sources/evm-rpc-services/clients/index.cjs.map +1 -1
  48. package/dist/data-sources/evm-rpc-services/clients/index.d.cts +1 -0
  49. package/dist/data-sources/evm-rpc-services/clients/index.d.cts.map +1 -1
  50. package/dist/data-sources/evm-rpc-services/clients/index.d.mts +1 -0
  51. package/dist/data-sources/evm-rpc-services/clients/index.d.mts.map +1 -1
  52. package/dist/data-sources/evm-rpc-services/clients/index.mjs +1 -0
  53. package/dist/data-sources/evm-rpc-services/clients/index.mjs.map +1 -1
  54. package/dist/data-sources/evm-rpc-services/index.cjs +2 -1
  55. package/dist/data-sources/evm-rpc-services/index.cjs.map +1 -1
  56. package/dist/data-sources/evm-rpc-services/index.d.cts +1 -1
  57. package/dist/data-sources/evm-rpc-services/index.d.cts.map +1 -1
  58. package/dist/data-sources/evm-rpc-services/index.d.mts +1 -1
  59. package/dist/data-sources/evm-rpc-services/index.d.mts.map +1 -1
  60. package/dist/data-sources/evm-rpc-services/index.mjs +1 -1
  61. package/dist/data-sources/evm-rpc-services/index.mjs.map +1 -1
  62. package/dist/data-sources/evm-rpc-services/services/TokenDetector.cjs +47 -50
  63. package/dist/data-sources/evm-rpc-services/services/TokenDetector.cjs.map +1 -1
  64. package/dist/data-sources/evm-rpc-services/services/TokenDetector.d.cts +12 -9
  65. package/dist/data-sources/evm-rpc-services/services/TokenDetector.d.cts.map +1 -1
  66. package/dist/data-sources/evm-rpc-services/services/TokenDetector.d.mts +12 -9
  67. package/dist/data-sources/evm-rpc-services/services/TokenDetector.d.mts.map +1 -1
  68. package/dist/data-sources/evm-rpc-services/services/TokenDetector.mjs +47 -50
  69. package/dist/data-sources/evm-rpc-services/services/TokenDetector.mjs.map +1 -1
  70. package/dist/data-sources/evm-rpc-services/services/index.cjs.map +1 -1
  71. package/dist/data-sources/evm-rpc-services/services/index.d.cts +1 -1
  72. package/dist/data-sources/evm-rpc-services/services/index.d.cts.map +1 -1
  73. package/dist/data-sources/evm-rpc-services/services/index.d.mts +1 -1
  74. package/dist/data-sources/evm-rpc-services/services/index.d.mts.map +1 -1
  75. package/dist/data-sources/evm-rpc-services/services/index.mjs.map +1 -1
  76. package/dist/index.cjs.map +1 -1
  77. package/dist/index.d.cts +6 -6
  78. package/dist/index.d.cts.map +1 -1
  79. package/dist/index.d.mts +6 -6
  80. package/dist/index.d.mts.map +1 -1
  81. package/dist/index.mjs.map +1 -1
  82. package/package.json +9 -7
@@ -10,10 +10,11 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
10
10
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
11
11
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
12
12
  };
13
- var _TokenDataSource_instances, _TokenDataSource_apiClient, _TokenDataSource_getNativeAssetIds, _TokenDataSource_getSupportedNetworks, _TokenDataSource_filterAssetsByNetwork;
13
+ var _TokenDataSource_instances, _TokenDataSource_apiClient, _TokenDataSource_getNativeAssetIds, _TokenDataSource_messenger, _TokenDataSource_getSupportedNetworks, _TokenDataSource_filterAssetsByNetwork, _TokenDataSource_filterBlockaidSpamTokens;
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.TokenDataSource = void 0;
16
16
  const core_backend_1 = require("@metamask/core-backend");
17
+ const phishing_controller_1 = require("@metamask/phishing-controller");
17
18
  const utils_1 = require("@metamask/utils");
18
19
  const evm_rpc_services_1 = require("./evm-rpc-services/index.cjs");
19
20
  const logger_1 = require("../logger.cjs");
@@ -22,8 +23,23 @@ const types_1 = require("../types.cjs");
22
23
  // CONSTANTS
23
24
  // ============================================================================
24
25
  const CONTROLLER_NAME = 'TokenDataSource';
25
- const MIN_TOKEN_OCCURRENCES = 3;
26
26
  const log = (0, logger_1.createModuleLogger)(logger_1.projectLogger, CONTROLLER_NAME);
27
+ /** Max asset IDs per tokens API request. */
28
+ const TOKENS_API_BATCH_SIZE = 50;
29
+ /** Max tokens per PhishingController:bulkScanTokens request (see PhishingController). */
30
+ const BULK_SCAN_BATCH_SIZE = 100;
31
+ /**
32
+ * Minimum number of aggregator occurrences required for an EVM ERC-20 token to
33
+ * pass the spam filter. Non-EVM tokens are filtered via Blockaid bulk scan instead.
34
+ */
35
+ const MIN_TOKEN_OCCURRENCES = 3;
36
+ /** CAIP-19 `assetNamespace` segments used across filtering logic. */
37
+ var CaipAssetNamespace;
38
+ (function (CaipAssetNamespace) {
39
+ CaipAssetNamespace["Slip44"] = "slip44";
40
+ CaipAssetNamespace["Erc20"] = "erc20";
41
+ CaipAssetNamespace["Token"] = "token";
42
+ })(CaipAssetNamespace || (CaipAssetNamespace = {}));
27
43
  // ============================================================================
28
44
  // HELPER FUNCTIONS
29
45
  // ============================================================================
@@ -81,19 +97,23 @@ function transformV3AssetResponseToMetadata(assetId, assetData) {
81
97
  * - Fetches metadata from Tokens API v3 for assets needing enrichment
82
98
  * - Merges fetched metadata into the response
83
99
  *
84
- * Usage: Create with queryApiClient and use assetsMiddleware; no messenger required.
100
+ * Pass the same {@link AssetsControllerMessenger} as other data sources for Blockaid
101
+ * token scans.
85
102
  */
86
103
  class TokenDataSource {
87
104
  getName() {
88
105
  return this.name;
89
106
  }
90
- constructor(options) {
107
+ constructor(messenger, options) {
91
108
  _TokenDataSource_instances.add(this);
92
109
  this.name = CONTROLLER_NAME;
93
110
  /** ApiPlatformClient for cached API calls */
94
111
  _TokenDataSource_apiClient.set(this, void 0);
95
112
  /** Returns CAIP-19 native asset IDs from NetworkEnablementController state */
96
113
  _TokenDataSource_getNativeAssetIds.set(this, void 0);
114
+ /** Shared controller messenger — used for `PhishingController:bulkScanTokens`. */
115
+ _TokenDataSource_messenger.set(this, void 0);
116
+ __classPrivateFieldSet(this, _TokenDataSource_messenger, messenger, "f");
97
117
  __classPrivateFieldSet(this, _TokenDataSource_apiClient, options.queryApiClient, "f");
98
118
  __classPrivateFieldSet(this, _TokenDataSource_getNativeAssetIds, options.getNativeAssetIds, "f");
99
119
  }
@@ -150,9 +170,7 @@ class TokenDataSource {
150
170
  return next(ctx);
151
171
  }
152
172
  try {
153
- // Use ApiPlatformClient for fetching asset metadata
154
- // API returns an array with assetId as a property on each item
155
- const metadataResponse = await __classPrivateFieldGet(this, _TokenDataSource_apiClient, "f").tokens.fetchV3Assets(supportedAssetIds, {
173
+ const fetchOptions = {
156
174
  includeIconUrl: true,
157
175
  includeMarketData: true,
158
176
  includeMetadata: true,
@@ -160,14 +178,58 @@ class TokenDataSource {
160
178
  includeRwaData: true,
161
179
  includeAggregators: true,
162
180
  includeOccurrences: true,
181
+ };
182
+ const metadataResponse = await (0, evm_rpc_services_1.reduceInBatchesSerially)({
183
+ values: supportedAssetIds,
184
+ batchSize: TOKENS_API_BATCH_SIZE,
185
+ eachBatch: async (workingResult, batch) => {
186
+ const batchResponse = await __classPrivateFieldGet(this, _TokenDataSource_apiClient, "f").tokens.fetchV3Assets(batch, fetchOptions);
187
+ return [...workingResult, ...batchResponse];
188
+ },
189
+ initialResult: [],
163
190
  });
191
+ // Split assets by chain type: EVM uses occurrence-count filtering;
192
+ // non-EVM non-native uses Blockaid; native (slip44) is always allowed.
193
+ const occurrencesByAssetId = new Map(metadataResponse.map((a) => [a.assetId, a.occurrences]));
194
+ const evmErc20Ids = [];
195
+ const nonEvmTokenIds = [];
196
+ for (const assetData of metadataResponse) {
197
+ const { assetNamespace, chain } = (0, utils_1.parseCaipAssetType)(assetData.assetId);
198
+ if (assetNamespace === CaipAssetNamespace.Slip44) {
199
+ // Native assets are always kept — no filtering.
200
+ }
201
+ else if (assetNamespace === CaipAssetNamespace.Erc20 &&
202
+ chain.namespace === utils_1.KnownCaipNamespace.Eip155) {
203
+ evmErc20Ids.push(assetData.assetId);
204
+ }
205
+ else if (assetNamespace === CaipAssetNamespace.Token) {
206
+ nonEvmTokenIds.push(assetData.assetId);
207
+ }
208
+ }
209
+ // EVM: require minimum occurrence count to suppress low-signal tokens.
210
+ // Tokens with no occurrence data (undefined) are treated the same as
211
+ // zero occurrences and filtered out.
212
+ const allowedEvmIds = new Set(evmErc20Ids.filter((id) => (occurrencesByAssetId.get(id) ?? 0) >= MIN_TOKEN_OCCURRENCES));
213
+ // Non-EVM: Blockaid bulk scan.
214
+ const allowedNonEvmIds = new Set(await __classPrivateFieldGet(this, _TokenDataSource_instances, "m", _TokenDataSource_filterBlockaidSpamTokens).call(this, nonEvmTokenIds));
215
+ // Start with every asset the API returned; only remove those that
216
+ // fail their respective filter (EVM occurrences / non-EVM Blockaid).
217
+ // Native (slip44) and unrecognised namespaces are kept (fail open).
218
+ const allowedAssetIds = new Set(metadataResponse.map((a) => a.assetId));
219
+ for (const id of evmErc20Ids) {
220
+ if (!allowedEvmIds.has(id)) {
221
+ allowedAssetIds.delete(id);
222
+ }
223
+ }
224
+ for (const id of nonEvmTokenIds) {
225
+ if (!allowedNonEvmIds.has(id)) {
226
+ allowedAssetIds.delete(id);
227
+ }
228
+ }
164
229
  response.assetsInfo ?? (response.assetsInfo = {});
165
230
  const filteredOutAssets = new Set();
166
231
  for (const assetData of metadataResponse) {
167
- const parsed = (0, utils_1.parseCaipAssetType)(assetData.assetId);
168
- const isNative = parsed.assetNamespace === 'slip44';
169
- if (!isNative &&
170
- (assetData.occurrences ?? 0) < MIN_TOKEN_OCCURRENCES) {
232
+ if (!allowedAssetIds.has(assetData.assetId)) {
171
233
  filteredOutAssets.add(assetData.assetId);
172
234
  continue;
173
235
  }
@@ -198,7 +260,7 @@ class TokenDataSource {
198
260
  }
199
261
  }
200
262
  exports.TokenDataSource = TokenDataSource;
201
- _TokenDataSource_apiClient = new WeakMap(), _TokenDataSource_getNativeAssetIds = new WeakMap(), _TokenDataSource_instances = new WeakSet(), _TokenDataSource_getSupportedNetworks =
263
+ _TokenDataSource_apiClient = new WeakMap(), _TokenDataSource_getNativeAssetIds = new WeakMap(), _TokenDataSource_messenger = new WeakMap(), _TokenDataSource_instances = new WeakSet(), _TokenDataSource_getSupportedNetworks =
202
264
  /**
203
265
  * Gets the supported networks from the API.
204
266
  * Caching is handled by ApiPlatformClient.
@@ -232,5 +294,69 @@ async function _TokenDataSource_getSupportedNetworks() {
232
294
  return false;
233
295
  }
234
296
  });
297
+ }, _TokenDataSource_filterBlockaidSpamTokens =
298
+ /**
299
+ * Filters non-EVM fungible `token` assets flagged as malicious by Blockaid
300
+ * via `PhishingController:bulkScanTokens`. Only the `token` namespace (e.g.
301
+ * Solana mints) is scanned; native (`slip44`) and EVM assets are not handled
302
+ * here (EVM uses occurrence-count filtering instead). Fails open on error.
303
+ *
304
+ * @param assets - CAIP-19 asset IDs to filter (non-EVM only).
305
+ * @returns Asset IDs with malicious tokens removed.
306
+ */
307
+ async function _TokenDataSource_filterBlockaidSpamTokens(assets) {
308
+ if (assets.length === 0) {
309
+ return assets;
310
+ }
311
+ const tokensByChain = {};
312
+ for (const asset of assets) {
313
+ try {
314
+ const { assetNamespace, assetReference, chain } = (0, utils_1.parseCaipAssetType)(asset);
315
+ if (assetNamespace === CaipAssetNamespace.Token) {
316
+ const chainName = chain.namespace;
317
+ if (!tokensByChain[chainName]) {
318
+ tokensByChain[chainName] = [];
319
+ }
320
+ tokensByChain[chainName].push({ asset, address: assetReference });
321
+ }
322
+ }
323
+ catch {
324
+ // Malformed or unsupported for bulk scan — keep asset (fail open)
325
+ }
326
+ }
327
+ if (Object.keys(tokensByChain).length === 0) {
328
+ return assets;
329
+ }
330
+ const rejectedAssets = new Set();
331
+ try {
332
+ for (const [chainId, tokenEntries] of Object.entries(tokensByChain)) {
333
+ const addresses = tokenEntries.map((entry) => entry.address);
334
+ const batches = [];
335
+ for (let i = 0; i < addresses.length; i += BULK_SCAN_BATCH_SIZE) {
336
+ batches.push(addresses.slice(i, i + BULK_SCAN_BATCH_SIZE));
337
+ }
338
+ const batchResults = await Promise.allSettled(batches.map((batch) => __classPrivateFieldGet(this, _TokenDataSource_messenger, "f").call('PhishingController:bulkScanTokens', {
339
+ chainId,
340
+ tokens: batch,
341
+ })));
342
+ const scanResponse = {};
343
+ for (const result of batchResults) {
344
+ if (result.status === 'fulfilled') {
345
+ Object.assign(scanResponse, result.value);
346
+ }
347
+ }
348
+ for (const entry of tokenEntries) {
349
+ const result = scanResponse[entry.address];
350
+ if (result?.result_type === phishing_controller_1.TokenScanResultType.Malicious) {
351
+ rejectedAssets.add(entry.asset);
352
+ }
353
+ }
354
+ }
355
+ }
356
+ catch (error) {
357
+ log('Blockaid bulk token scan failed; keeping all tokens', { error });
358
+ return assets;
359
+ }
360
+ return assets.filter((asset) => !rejectedAssets.has(asset));
235
361
  };
236
362
  //# sourceMappingURL=TokenDataSource.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"TokenDataSource.cjs","sourceRoot":"","sources":["../../src/data-sources/TokenDataSource.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AACA,yDAA2D;AAC3D,2CAAqD;AAGrD,mEAA8D;AAC9D,0CAA8D;AAC9D,wCAAwC;AAQxC,+EAA+E;AAC/E,YAAY;AACZ,+EAA+E;AAE/E,MAAM,eAAe,GAAG,iBAAiB,CAAC;AAE1C,MAAM,qBAAqB,GAAG,CAAC,CAAC;AAEhC,MAAM,GAAG,GAAG,IAAA,2BAAkB,EAAC,sBAAa,EAAE,eAAe,CAAC,CAAC;AAuB/D,+EAA+E;AAC/E,mBAAmB;AACnB,+EAA+E;AAE/E;;;;;;;;;;;GAWG;AACH,SAAS,kCAAkC,CACzC,OAAe,EACf,SAA0B;IAE1B,MAAM,MAAM,GAAG,IAAA,0BAAkB,EAAC,OAAwB,CAAC,CAAC;IAC5D,IAAI,SAAS,GAA+B,OAAO,CAAC;IAEpD,IAAI,MAAM,CAAC,cAAc,KAAK,QAAQ,EAAE,CAAC;QACvC,SAAS,GAAG,QAAQ,CAAC;IACvB,CAAC;SAAM,IAAI,MAAM,CAAC,cAAc,KAAK,KAAK,EAAE,CAAC;QAC3C,SAAS,GAAG,KAAK,CAAC;IACpB,CAAC;IAED,MAAM,QAAQ,GAA0B;QACtC,4BAA4B;QAC5B,IAAI,EAAE,SAAS;QACf,2BAA2B;QAC3B,IAAI,EAAE,SAAS,CAAC,IAAI;QACpB,MAAM,EAAE,SAAS,CAAC,MAAM;QACxB,QAAQ,EAAE,SAAS,CAAC,QAAQ;QAC5B,KAAK,EAAE,SAAS,CAAC,OAAO;QACxB,wBAAwB;QACxB,WAAW,EAAE,SAAS,CAAC,WAAW;QAClC,WAAW,EAAE,SAAS,CAAC,WAAW;QAClC,WAAW,EAAE,SAAS,CAAC,WAAW;QAClC,MAAM,EAAE,SAAS,CAAC,MAAM;QACxB,WAAW,EAAE,SAAS,CAAC,WAAW;QAClC,IAAI,EAAE,SAAS,CAAC,IAAI;QACpB,cAAc,EAAE,SAAS,CAAC,cAAc;QACxC,OAAO,EAAE,SAAS,CAAC,OAAO;QAC1B,kBAAkB,EAAE,SAAS,CAAC,kBAAkB;QAChD,WAAW,EAAE,SAAS,CAAC,WAAW;KACnC,CAAC;IAEF,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,+EAA+E;AAC/E,oBAAoB;AACpB,+EAA+E;AAE/E;;;;;;;;;GASG;AACH,MAAa,eAAe;IAG1B,OAAO;QACL,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC;IAQD,YAAY,OAA+B;;QAZlC,SAAI,GAAG,eAAe,CAAC;QAMhC,6CAA6C;QACpC,6CAA8B;QAEvC,8EAA8E;QACrE,qDAAmC;QAG1C,uBAAA,IAAI,8BAAc,OAAO,CAAC,cAAc,MAAA,CAAC;QACzC,uBAAA,IAAI,sCAAsB,OAAO,CAAC,iBAAiB,MAAA,CAAC;IACtD,CAAC;IAkDD;;;;;;;;;;OAUG;IACH,IAAI,gBAAgB;QAClB,OAAO,IAAA,oBAAY,EAAC,CAAC,UAAU,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;YACpD,gCAAgC;YAChC,MAAM,EAAE,QAAQ,EAAE,GAAG,GAAG,CAAC;YAEzB,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,GAAG,GAAG,CAAC,cAAc,EAAE,CAAC;YAC3D,MAAM,uBAAuB,GAAG,IAAI,GAAG,EAAU,CAAC;YAElD,mEAAmE;YACnE,KAAK,MAAM,aAAa,IAAI,uBAAA,IAAI,0CAAmB,MAAvB,IAAI,CAAqB,EAAE,CAAC;gBACtD,uBAAuB,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;YAC7C,CAAC;YAED,8DAA8D;YAC9D,IAAI,QAAQ,CAAC,cAAc,EAAE,CAAC;gBAC5B,KAAK,MAAM,WAAW,IAAI,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;oBACjE,KAAK,MAAM,OAAO,IAAI,WAAW,EAAE,CAAC;wBAClC,mDAAmD;wBACnD,MAAM,gBAAgB,GAAG,QAAQ,CAAC,UAAU,EAAE,CAAC,OAAO,CAAC,CAAC;wBACxD,IAAI,gBAAgB,EAAE,KAAK,EAAE,CAAC;4BAC5B,SAAS;wBACX,CAAC;wBAED,gDAAgD;wBAChD,MAAM,gBAAgB,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;wBAChD,IAAI,gBAAgB,EAAE,KAAK,EAAE,CAAC;4BAC5B,SAAS;wBACX,CAAC;wBAED,wFAAwF;wBACxF,IAAI,IAAA,2CAAwB,EAAC,OAAO,CAAC,EAAE,CAAC;4BACtC,SAAS;wBACX,CAAC;wBAED,uBAAuB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;oBACvC,CAAC;gBACH,CAAC;YACH,CAAC;YAED,IAAI,uBAAuB,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;gBACvC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;YACnB,CAAC;YAED,sDAAsD;YACtD,MAAM,iBAAiB,GAAG,MAAM,uBAAA,IAAI,yEAAsB,MAA1B,IAAI,CAAwB,CAAC;YAC7D,MAAM,iBAAiB,GAAG,uBAAA,IAAI,0EAAuB,MAA3B,IAAI,EAC5B,CAAC,GAAG,uBAAuB,CAAC,EAC5B,iBAAiB,CAClB,CAAC;YAEF,IAAI,iBAAiB,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACnC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;YACnB,CAAC;YAED,IAAI,CAAC;gBACH,oDAAoD;gBACpD,+DAA+D;gBAC/D,MAAM,gBAAgB,GAAG,MAAM,uBAAA,IAAI,kCAAW,CAAC,MAAM,CAAC,aAAa,CACjE,iBAAiB,EACjB;oBACE,cAAc,EAAE,IAAI;oBACpB,iBAAiB,EAAE,IAAI;oBACvB,eAAe,EAAE,IAAI;oBACrB,aAAa,EAAE,IAAI;oBACnB,cAAc,EAAE,IAAI;oBACpB,kBAAkB,EAAE,IAAI;oBACxB,kBAAkB,EAAE,IAAI;iBACzB,CACF,CAAC;gBAEF,QAAQ,CAAC,UAAU,KAAnB,QAAQ,CAAC,UAAU,GAAK,EAAE,EAAC;gBAE3B,MAAM,iBAAiB,GAAG,IAAI,GAAG,EAAU,CAAC;gBAE5C,KAAK,MAAM,SAAS,IAAI,gBAAgB,EAAE,CAAC;oBACzC,MAAM,MAAM,GAAG,IAAA,0BAAkB,EAAC,SAAS,CAAC,OAAwB,CAAC,CAAC;oBACtE,MAAM,QAAQ,GAAG,MAAM,CAAC,cAAc,KAAK,QAAQ,CAAC;oBAEpD,IACE,CAAC,QAAQ;wBACT,CAAC,SAAS,CAAC,WAAW,IAAI,CAAC,CAAC,GAAG,qBAAqB,EACpD,CAAC;wBACD,iBAAiB,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;wBACzC,SAAS;oBACX,CAAC;oBAED,MAAM,WAAW,GAAG,SAAS,CAAC,OAAwB,CAAC;oBACvD,QAAQ,CAAC,UAAU,CAAC,WAAW,CAAC,GAAG,kCAAkC,CACnE,SAAS,CAAC,OAAO,EACjB,SAAS,CACV,CAAC;gBACJ,CAAC;gBAED,IAAI,iBAAiB,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;oBAC/B,IAAI,QAAQ,CAAC,aAAa,EAAE,CAAC;wBAC3B,KAAK,MAAM,eAAe,IAAI,MAAM,CAAC,MAAM,CACzC,QAAQ,CAAC,aAAa,CACvB,EAAE,CAAC;4BACF,KAAK,MAAM,OAAO,IAAI,iBAAiB,EAAE,CAAC;gCACxC,OAAQ,eAA2C,CAAC,OAAO,CAAC,CAAC;4BAC/D,CAAC;wBACH,CAAC;oBACH,CAAC;oBAED,IAAI,QAAQ,CAAC,cAAc,EAAE,CAAC;wBAC5B,KAAK,MAAM,CAAC,SAAS,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAChD,QAAQ,CAAC,cAAc,CACxB,EAAE,CAAC;4BACF,QAAQ,CAAC,cAAc,CAAC,SAAS,CAAC,GAAG,QAAQ,CAAC,MAAM,CAClD,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC,CACnC,CAAC;wBACJ,CAAC;oBACH,CAAC;gBACH,CAAC;YACH,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,GAAG,CAAC,0BAA0B,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;YAC7C,CAAC;YAED,0DAA0D;YAC1D,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;QACnB,CAAC,CAAC,CAAC;IACL,CAAC;CACF;AAvMD,0CAuMC;;AArLC;;;;;GAKG;AACH,KAAK;IACH,IAAI,CAAC;QACH,wDAAwD;QACxD,oCAAoC;QACpC,MAAM,QAAQ,GACZ,MAAM,uBAAA,IAAI,kCAAW,CAAC,MAAM,CAAC,6BAA6B,EAAE,CAAC;QAE/D,4CAA4C;QAC5C,MAAM,WAAW,GAAG,CAAC,GAAG,QAAQ,CAAC,WAAW,EAAE,GAAG,QAAQ,CAAC,cAAc,CAAC,CAAC;QAE1E,OAAO,IAAI,GAAG,CAAC,WAAW,CAAC,CAAC;IAC9B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,GAAG,CAAC,oCAAoC,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QACrD,OAAO,IAAI,GAAG,EAAE,CAAC;IACnB,CAAC;AACH,CAAC,2FAUC,QAAkB,EAClB,iBAA8B;IAE9B,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE;QACjC,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAA,0BAAkB,EAAC,OAAwB,CAAC,CAAC;YAC5D,sDAAsD;YACtD,sDAAsD;YACtD,MAAM,OAAO,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,SAAS,IAAI,MAAM,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC;YACtE,OAAO,iBAAiB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACxC,CAAC;QAAC,MAAM,CAAC;YACP,gDAAgD;YAChD,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["import type { V3AssetResponse } from '@metamask/core-backend';\nimport { ApiPlatformClient } from '@metamask/core-backend';\nimport { parseCaipAssetType } from '@metamask/utils';\nimport type { CaipAssetType } from '@metamask/utils';\n\nimport { isStakingContractAssetId } from './evm-rpc-services';\nimport { projectLogger, createModuleLogger } from '../logger';\nimport { forDataTypes } from '../types';\nimport type {\n Caip19AssetId,\n AssetMetadata,\n Middleware,\n FungibleAssetMetadata,\n} from '../types';\n\n// ============================================================================\n// CONSTANTS\n// ============================================================================\n\nconst CONTROLLER_NAME = 'TokenDataSource';\n\nconst MIN_TOKEN_OCCURRENCES = 3;\n\nconst log = createModuleLogger(projectLogger, CONTROLLER_NAME);\n\n// ============================================================================\n// MESSENGER TYPES\n// ============================================================================\n\n/**\n * TokenDataSource does not call external messenger actions.\n * It uses ApiPlatformClient directly.\n */\nexport type TokenDataSourceAllowedActions = never;\n\n// ============================================================================\n// OPTIONS\n// ============================================================================\n\nexport type TokenDataSourceOptions = {\n /** ApiPlatformClient for API calls with caching */\n queryApiClient: ApiPlatformClient;\n /** Returns CAIP-19 native asset IDs from NetworkEnablementController state */\n getNativeAssetIds: () => string[];\n};\n\n// ============================================================================\n// HELPER FUNCTIONS\n// ============================================================================\n\n/**\n * Transform V3 API response to FungibleAssetMetadata for state storage.\n *\n * Mapping:\n * - assetId → used to derive `type` (native/erc20/spl)\n * - iconUrl → image\n * - All other fields map directly\n *\n * @param assetId - CAIP-19 asset ID used to derive token type.\n * @param assetData - V3 API response data.\n * @returns FungibleAssetMetadata for state storage.\n */\nfunction transformV3AssetResponseToMetadata(\n assetId: string,\n assetData: V3AssetResponse,\n): AssetMetadata {\n const parsed = parseCaipAssetType(assetId as CaipAssetType);\n let tokenType: 'native' | 'erc20' | 'spl' = 'erc20';\n\n if (parsed.assetNamespace === 'slip44') {\n tokenType = 'native';\n } else if (parsed.assetNamespace === 'spl') {\n tokenType = 'spl';\n }\n\n const metadata: FungibleAssetMetadata = {\n // Type derived from assetId\n type: tokenType,\n // BaseAssetMetadata fields\n name: assetData.name,\n symbol: assetData.symbol,\n decimals: assetData.decimals,\n image: assetData.iconUrl,\n // Direct mapping fields\n coingeckoId: assetData.coingeckoId,\n occurrences: assetData.occurrences,\n aggregators: assetData.aggregators,\n labels: assetData.labels,\n erc20Permit: assetData.erc20Permit,\n fees: assetData.fees,\n honeypotStatus: assetData.honeypotStatus,\n storage: assetData.storage,\n isContractVerified: assetData.isContractVerified,\n description: assetData.description,\n };\n\n return metadata;\n}\n\n// ============================================================================\n// TOKEN DATA SOURCE\n// ============================================================================\n\n/**\n * TokenDataSource enriches responses with token metadata from the Tokens API.\n *\n * This middleware-based data source:\n * - Checks detected assets for missing metadata/images\n * - Fetches metadata from Tokens API v3 for assets needing enrichment\n * - Merges fetched metadata into the response\n *\n * Usage: Create with queryApiClient and use assetsMiddleware; no messenger required.\n */\nexport class TokenDataSource {\n readonly name = CONTROLLER_NAME;\n\n getName(): string {\n return this.name;\n }\n\n /** ApiPlatformClient for cached API calls */\n readonly #apiClient: ApiPlatformClient;\n\n /** Returns CAIP-19 native asset IDs from NetworkEnablementController state */\n readonly #getNativeAssetIds: () => string[];\n\n constructor(options: TokenDataSourceOptions) {\n this.#apiClient = options.queryApiClient;\n this.#getNativeAssetIds = options.getNativeAssetIds;\n }\n\n /**\n * Gets the supported networks from the API.\n * Caching is handled by ApiPlatformClient.\n *\n * @returns Set of supported chain IDs in CAIP format\n */\n async #getSupportedNetworks(): Promise<Set<string>> {\n try {\n // Use v2/supportedNetworks which returns CAIP chain IDs\n // ApiPlatformClient handles caching\n const response =\n await this.#apiClient.tokens.fetchTokenV2SupportedNetworks();\n\n // Combine full and partial support networks\n const allNetworks = [...response.fullSupport, ...response.partialSupport];\n\n return new Set(allNetworks);\n } catch (error) {\n log('Failed to fetch supported networks', { error });\n return new Set();\n }\n }\n\n /**\n * Filters asset IDs to only include those from supported networks.\n *\n * @param assetIds - Array of CAIP-19 asset IDs\n * @param supportedNetworks - Set of supported chain IDs\n * @returns Array of asset IDs from supported networks\n */\n #filterAssetsByNetwork(\n assetIds: string[],\n supportedNetworks: Set<string>,\n ): string[] {\n return assetIds.filter((assetId) => {\n try {\n const parsed = parseCaipAssetType(assetId as CaipAssetType);\n // chainId is in format \"eip155:1\" or \"tron:728126428\"\n // parsed.chain has namespace and reference properties\n const chainId = `${parsed.chain.namespace}:${parsed.chain.reference}`;\n return supportedNetworks.has(chainId);\n } catch {\n // If we can't parse the asset ID, filter it out\n return false;\n }\n });\n }\n\n /**\n * Get the middleware for enriching responses with token metadata.\n *\n * This middleware:\n * 1. Extracts the response from context\n * 2. Fetches metadata for detected assets (assets without metadata)\n * 3. Enriches the response with fetched metadata\n * 4. Calls next() at the end to continue the middleware chain\n *\n * @returns The middleware function for the assets pipeline.\n */\n get assetsMiddleware(): Middleware {\n return forDataTypes(['metadata'], async (ctx, next) => {\n // Extract response from context\n const { response } = ctx;\n\n const { assetsInfo: stateMetadata } = ctx.getAssetsState();\n const assetIdsNeedingMetadata = new Set<string>();\n\n // Always include native asset IDs from NetworkEnablementController\n for (const nativeAssetId of this.#getNativeAssetIds()) {\n assetIdsNeedingMetadata.add(nativeAssetId);\n }\n\n // Also fetch metadata for detected assets that are missing it\n if (response.detectedAssets) {\n for (const detectedIds of Object.values(response.detectedAssets)) {\n for (const assetId of detectedIds) {\n // Skip if response already has metadata with image\n const responseMetadata = response.assetsInfo?.[assetId];\n if (responseMetadata?.image) {\n continue;\n }\n\n // Skip if state already has metadata with image\n const existingMetadata = stateMetadata[assetId];\n if (existingMetadata?.image) {\n continue;\n }\n\n // Skip staking contracts; we use built-in metadata and do not fetch from the tokens API\n if (isStakingContractAssetId(assetId)) {\n continue;\n }\n\n assetIdsNeedingMetadata.add(assetId);\n }\n }\n }\n\n if (assetIdsNeedingMetadata.size === 0) {\n return next(ctx);\n }\n\n // Filter asset IDs to only include supported networks\n const supportedNetworks = await this.#getSupportedNetworks();\n const supportedAssetIds = this.#filterAssetsByNetwork(\n [...assetIdsNeedingMetadata],\n supportedNetworks,\n );\n\n if (supportedAssetIds.length === 0) {\n return next(ctx);\n }\n\n try {\n // Use ApiPlatformClient for fetching asset metadata\n // API returns an array with assetId as a property on each item\n const metadataResponse = await this.#apiClient.tokens.fetchV3Assets(\n supportedAssetIds,\n {\n includeIconUrl: true,\n includeMarketData: true,\n includeMetadata: true,\n includeLabels: true,\n includeRwaData: true,\n includeAggregators: true,\n includeOccurrences: true,\n },\n );\n\n response.assetsInfo ??= {};\n\n const filteredOutAssets = new Set<string>();\n\n for (const assetData of metadataResponse) {\n const parsed = parseCaipAssetType(assetData.assetId as CaipAssetType);\n const isNative = parsed.assetNamespace === 'slip44';\n\n if (\n !isNative &&\n (assetData.occurrences ?? 0) < MIN_TOKEN_OCCURRENCES\n ) {\n filteredOutAssets.add(assetData.assetId);\n continue;\n }\n\n const caipAssetId = assetData.assetId as Caip19AssetId;\n response.assetsInfo[caipAssetId] = transformV3AssetResponseToMetadata(\n assetData.assetId,\n assetData,\n );\n }\n\n if (filteredOutAssets.size > 0) {\n if (response.assetsBalance) {\n for (const accountBalances of Object.values(\n response.assetsBalance,\n )) {\n for (const assetId of filteredOutAssets) {\n delete (accountBalances as Record<string, unknown>)[assetId];\n }\n }\n }\n\n if (response.detectedAssets) {\n for (const [accountId, assetIds] of Object.entries(\n response.detectedAssets,\n )) {\n response.detectedAssets[accountId] = assetIds.filter(\n (id) => !filteredOutAssets.has(id),\n );\n }\n }\n }\n } catch (error) {\n log('Failed to fetch metadata', { error });\n }\n\n // Call next() at the end to continue the middleware chain\n return next(ctx);\n });\n }\n}\n"]}
1
+ {"version":3,"file":"TokenDataSource.cjs","sourceRoot":"","sources":["../../src/data-sources/TokenDataSource.ts"],"names":[],"mappings":"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","sourcesContent":["import type { V3AssetResponse } from '@metamask/core-backend';\nimport { ApiPlatformClient } from '@metamask/core-backend';\nimport type {\n BulkTokenScanResponse,\n PhishingControllerBulkScanTokensAction,\n} from '@metamask/phishing-controller';\nimport { TokenScanResultType } from '@metamask/phishing-controller';\nimport { KnownCaipNamespace, parseCaipAssetType } from '@metamask/utils';\nimport type { CaipAssetType } from '@metamask/utils';\n\nimport {\n isStakingContractAssetId,\n reduceInBatchesSerially,\n} from './evm-rpc-services';\nimport type { AssetsControllerMessenger } from '../AssetsController';\nimport { projectLogger, createModuleLogger } from '../logger';\nimport { forDataTypes } from '../types';\nimport type {\n Caip19AssetId,\n AssetMetadata,\n Middleware,\n FungibleAssetMetadata,\n} from '../types';\n\n// ============================================================================\n// CONSTANTS\n// ============================================================================\n\nconst CONTROLLER_NAME = 'TokenDataSource';\n\nconst log = createModuleLogger(projectLogger, CONTROLLER_NAME);\n\n/** Max asset IDs per tokens API request. */\nconst TOKENS_API_BATCH_SIZE = 50;\n\n/** Max tokens per PhishingController:bulkScanTokens request (see PhishingController). */\nconst BULK_SCAN_BATCH_SIZE = 100;\n\n/**\n * Minimum number of aggregator occurrences required for an EVM ERC-20 token to\n * pass the spam filter. Non-EVM tokens are filtered via Blockaid bulk scan instead.\n */\nconst MIN_TOKEN_OCCURRENCES = 3;\n\n/** CAIP-19 `assetNamespace` segments used across filtering logic. */\nenum CaipAssetNamespace {\n Slip44 = 'slip44',\n Erc20 = 'erc20',\n Token = 'token',\n}\n\n// ============================================================================\n// OPTIONS\n// ============================================================================\n\nexport type TokenDataSourceOptions = {\n /** ApiPlatformClient for API calls with caching */\n queryApiClient: ApiPlatformClient;\n /** Returns CAIP-19 native asset IDs from NetworkEnablementController state */\n getNativeAssetIds: () => string[];\n};\n\n/**\n * Messenger actions `TokenDataSource` may invoke (via {@link AssetsControllerMessenger}).\n * Not re-exported from the package public `index` (repo ESLint); import from this module when\n * typing a messenger in the same package or tests.\n */\nexport type TokenDataSourceAllowedActions =\n PhishingControllerBulkScanTokensAction;\n\n// ============================================================================\n// HELPER FUNCTIONS\n// ============================================================================\n\n/**\n * Transform V3 API response to FungibleAssetMetadata for state storage.\n *\n * Mapping:\n * - assetId → used to derive `type` (native/erc20/spl)\n * - iconUrl → image\n * - All other fields map directly\n *\n * @param assetId - CAIP-19 asset ID used to derive token type.\n * @param assetData - V3 API response data.\n * @returns FungibleAssetMetadata for state storage.\n */\nfunction transformV3AssetResponseToMetadata(\n assetId: string,\n assetData: V3AssetResponse,\n): AssetMetadata {\n const parsed = parseCaipAssetType(assetId as CaipAssetType);\n let tokenType: 'native' | 'erc20' | 'spl' = 'erc20';\n\n if (parsed.assetNamespace === 'slip44') {\n tokenType = 'native';\n } else if (parsed.assetNamespace === 'spl') {\n tokenType = 'spl';\n }\n\n const metadata: FungibleAssetMetadata = {\n // Type derived from assetId\n type: tokenType,\n // BaseAssetMetadata fields\n name: assetData.name,\n symbol: assetData.symbol,\n decimals: assetData.decimals,\n image: assetData.iconUrl,\n // Direct mapping fields\n coingeckoId: assetData.coingeckoId,\n occurrences: assetData.occurrences,\n aggregators: assetData.aggregators,\n labels: assetData.labels,\n erc20Permit: assetData.erc20Permit,\n fees: assetData.fees,\n honeypotStatus: assetData.honeypotStatus,\n storage: assetData.storage,\n isContractVerified: assetData.isContractVerified,\n description: assetData.description,\n };\n\n return metadata;\n}\n\n// ============================================================================\n// TOKEN DATA SOURCE\n// ============================================================================\n\n/**\n * TokenDataSource enriches responses with token metadata from the Tokens API.\n *\n * This middleware-based data source:\n * - Checks detected assets for missing metadata/images\n * - Fetches metadata from Tokens API v3 for assets needing enrichment\n * - Merges fetched metadata into the response\n *\n * Pass the same {@link AssetsControllerMessenger} as other data sources for Blockaid\n * token scans.\n */\nexport class TokenDataSource {\n readonly name = CONTROLLER_NAME;\n\n getName(): string {\n return this.name;\n }\n\n /** ApiPlatformClient for cached API calls */\n readonly #apiClient: ApiPlatformClient;\n\n /** Returns CAIP-19 native asset IDs from NetworkEnablementController state */\n readonly #getNativeAssetIds: () => string[];\n\n /** Shared controller messenger — used for `PhishingController:bulkScanTokens`. */\n readonly #messenger: AssetsControllerMessenger;\n\n constructor(\n messenger: AssetsControllerMessenger,\n options: TokenDataSourceOptions,\n ) {\n this.#messenger = messenger;\n this.#apiClient = options.queryApiClient;\n this.#getNativeAssetIds = options.getNativeAssetIds;\n }\n\n /**\n * Gets the supported networks from the API.\n * Caching is handled by ApiPlatformClient.\n *\n * @returns Set of supported chain IDs in CAIP format\n */\n async #getSupportedNetworks(): Promise<Set<string>> {\n try {\n // Use v2/supportedNetworks which returns CAIP chain IDs\n // ApiPlatformClient handles caching\n const response =\n await this.#apiClient.tokens.fetchTokenV2SupportedNetworks();\n\n // Combine full and partial support networks\n const allNetworks = [...response.fullSupport, ...response.partialSupport];\n\n return new Set(allNetworks);\n } catch (error) {\n log('Failed to fetch supported networks', { error });\n return new Set();\n }\n }\n\n /**\n * Filters asset IDs to only include those from supported networks.\n *\n * @param assetIds - Array of CAIP-19 asset IDs\n * @param supportedNetworks - Set of supported chain IDs\n * @returns Array of asset IDs from supported networks\n */\n #filterAssetsByNetwork(\n assetIds: string[],\n supportedNetworks: Set<string>,\n ): string[] {\n return assetIds.filter((assetId) => {\n try {\n const parsed = parseCaipAssetType(assetId as CaipAssetType);\n // chainId is in format \"eip155:1\" or \"tron:728126428\"\n // parsed.chain has namespace and reference properties\n const chainId = `${parsed.chain.namespace}:${parsed.chain.reference}`;\n return supportedNetworks.has(chainId);\n } catch {\n // If we can't parse the asset ID, filter it out\n return false;\n }\n });\n }\n\n /**\n * Filters non-EVM fungible `token` assets flagged as malicious by Blockaid\n * via `PhishingController:bulkScanTokens`. Only the `token` namespace (e.g.\n * Solana mints) is scanned; native (`slip44`) and EVM assets are not handled\n * here (EVM uses occurrence-count filtering instead). Fails open on error.\n *\n * @param assets - CAIP-19 asset IDs to filter (non-EVM only).\n * @returns Asset IDs with malicious tokens removed.\n */\n async #filterBlockaidSpamTokens(assets: string[]): Promise<string[]> {\n if (assets.length === 0) {\n return assets;\n }\n\n const tokensByChain: Record<string, { asset: string; address: string }[]> =\n {};\n\n for (const asset of assets) {\n try {\n const { assetNamespace, assetReference, chain } = parseCaipAssetType(\n asset as CaipAssetType,\n );\n\n if (assetNamespace === CaipAssetNamespace.Token) {\n const chainName = chain.namespace;\n if (!tokensByChain[chainName]) {\n tokensByChain[chainName] = [];\n }\n tokensByChain[chainName].push({ asset, address: assetReference });\n }\n } catch {\n // Malformed or unsupported for bulk scan — keep asset (fail open)\n }\n }\n\n if (Object.keys(tokensByChain).length === 0) {\n return assets;\n }\n\n const rejectedAssets = new Set<string>();\n\n try {\n for (const [chainId, tokenEntries] of Object.entries(tokensByChain)) {\n const addresses = tokenEntries.map((entry) => entry.address);\n const batches: string[][] = [];\n for (let i = 0; i < addresses.length; i += BULK_SCAN_BATCH_SIZE) {\n batches.push(addresses.slice(i, i + BULK_SCAN_BATCH_SIZE));\n }\n\n const batchResults = await Promise.allSettled(\n batches.map((batch) =>\n this.#messenger.call('PhishingController:bulkScanTokens', {\n chainId,\n tokens: batch,\n }),\n ),\n );\n\n const scanResponse: BulkTokenScanResponse = {};\n for (const result of batchResults) {\n if (result.status === 'fulfilled') {\n Object.assign(scanResponse, result.value);\n }\n }\n\n for (const entry of tokenEntries) {\n const result = scanResponse[entry.address];\n if (result?.result_type === TokenScanResultType.Malicious) {\n rejectedAssets.add(entry.asset);\n }\n }\n }\n } catch (error) {\n log('Blockaid bulk token scan failed; keeping all tokens', { error });\n return assets;\n }\n\n return assets.filter((asset) => !rejectedAssets.has(asset));\n }\n\n /**\n * Get the middleware for enriching responses with token metadata.\n *\n * This middleware:\n * 1. Extracts the response from context\n * 2. Fetches metadata for detected assets (assets without metadata)\n * 3. Enriches the response with fetched metadata\n * 4. Calls next() at the end to continue the middleware chain\n *\n * @returns The middleware function for the assets pipeline.\n */\n get assetsMiddleware(): Middleware {\n return forDataTypes(['metadata'], async (ctx, next) => {\n // Extract response from context\n const { response } = ctx;\n\n const { assetsInfo: stateMetadata } = ctx.getAssetsState();\n const assetIdsNeedingMetadata = new Set<string>();\n\n // Always include native asset IDs from NetworkEnablementController\n for (const nativeAssetId of this.#getNativeAssetIds()) {\n assetIdsNeedingMetadata.add(nativeAssetId);\n }\n\n // Also fetch metadata for detected assets that are missing it\n if (response.detectedAssets) {\n for (const detectedIds of Object.values(response.detectedAssets)) {\n for (const assetId of detectedIds) {\n // Skip if response already has metadata with image\n const responseMetadata = response.assetsInfo?.[assetId];\n if (responseMetadata?.image) {\n continue;\n }\n\n // Skip if state already has metadata with image\n const existingMetadata = stateMetadata[assetId];\n if (existingMetadata?.image) {\n continue;\n }\n\n // Skip staking contracts; we use built-in metadata and do not fetch from the tokens API\n if (isStakingContractAssetId(assetId)) {\n continue;\n }\n\n assetIdsNeedingMetadata.add(assetId);\n }\n }\n }\n\n if (assetIdsNeedingMetadata.size === 0) {\n return next(ctx);\n }\n\n // Filter asset IDs to only include supported networks\n const supportedNetworks = await this.#getSupportedNetworks();\n const supportedAssetIds = this.#filterAssetsByNetwork(\n [...assetIdsNeedingMetadata],\n supportedNetworks,\n );\n\n if (supportedAssetIds.length === 0) {\n return next(ctx);\n }\n\n try {\n const fetchOptions = {\n includeIconUrl: true,\n includeMarketData: true,\n includeMetadata: true,\n includeLabels: true,\n includeRwaData: true,\n includeAggregators: true,\n includeOccurrences: true,\n };\n\n const metadataResponse = await reduceInBatchesSerially<\n string,\n V3AssetResponse[]\n >({\n values: supportedAssetIds,\n batchSize: TOKENS_API_BATCH_SIZE,\n eachBatch: async (workingResult, batch) => {\n const batchResponse = await this.#apiClient.tokens.fetchV3Assets(\n batch,\n fetchOptions,\n );\n return [...(workingResult as V3AssetResponse[]), ...batchResponse];\n },\n initialResult: [],\n });\n\n // Split assets by chain type: EVM uses occurrence-count filtering;\n // non-EVM non-native uses Blockaid; native (slip44) is always allowed.\n const occurrencesByAssetId = new Map(\n metadataResponse.map((a) => [a.assetId, a.occurrences]),\n );\n\n const evmErc20Ids: string[] = [];\n const nonEvmTokenIds: string[] = [];\n\n for (const assetData of metadataResponse) {\n const { assetNamespace, chain } = parseCaipAssetType(\n assetData.assetId as CaipAssetType,\n );\n if (assetNamespace === CaipAssetNamespace.Slip44) {\n // Native assets are always kept — no filtering.\n } else if (\n assetNamespace === CaipAssetNamespace.Erc20 &&\n chain.namespace === KnownCaipNamespace.Eip155\n ) {\n evmErc20Ids.push(assetData.assetId);\n } else if (assetNamespace === CaipAssetNamespace.Token) {\n nonEvmTokenIds.push(assetData.assetId);\n }\n }\n\n // EVM: require minimum occurrence count to suppress low-signal tokens.\n // Tokens with no occurrence data (undefined) are treated the same as\n // zero occurrences and filtered out.\n const allowedEvmIds = new Set(\n evmErc20Ids.filter(\n (id) =>\n (occurrencesByAssetId.get(id) ?? 0) >= MIN_TOKEN_OCCURRENCES,\n ),\n );\n\n // Non-EVM: Blockaid bulk scan.\n const allowedNonEvmIds = new Set(\n await this.#filterBlockaidSpamTokens(nonEvmTokenIds),\n );\n\n // Start with every asset the API returned; only remove those that\n // fail their respective filter (EVM occurrences / non-EVM Blockaid).\n // Native (slip44) and unrecognised namespaces are kept (fail open).\n const allowedAssetIds = new Set(metadataResponse.map((a) => a.assetId));\n\n for (const id of evmErc20Ids) {\n if (!allowedEvmIds.has(id)) {\n allowedAssetIds.delete(id);\n }\n }\n for (const id of nonEvmTokenIds) {\n if (!allowedNonEvmIds.has(id)) {\n allowedAssetIds.delete(id);\n }\n }\n\n response.assetsInfo ??= {};\n\n const filteredOutAssets = new Set<string>();\n\n for (const assetData of metadataResponse) {\n if (!allowedAssetIds.has(assetData.assetId)) {\n filteredOutAssets.add(assetData.assetId);\n continue;\n }\n\n const caipAssetId = assetData.assetId as Caip19AssetId;\n response.assetsInfo[caipAssetId] = transformV3AssetResponseToMetadata(\n assetData.assetId,\n assetData,\n );\n }\n\n if (filteredOutAssets.size > 0) {\n if (response.assetsBalance) {\n for (const accountBalances of Object.values(\n response.assetsBalance,\n )) {\n for (const assetId of filteredOutAssets) {\n delete (accountBalances as Record<string, unknown>)[assetId];\n }\n }\n }\n\n if (response.detectedAssets) {\n for (const [accountId, assetIds] of Object.entries(\n response.detectedAssets,\n )) {\n response.detectedAssets[accountId] = assetIds.filter(\n (id) => !filteredOutAssets.has(id),\n );\n }\n }\n }\n } catch (error) {\n log('Failed to fetch metadata', { error });\n }\n\n // Call next() at the end to continue the middleware chain\n return next(ctx);\n });\n }\n}\n"]}
@@ -1,16 +1,19 @@
1
1
  import { ApiPlatformClient } from "@metamask/core-backend";
2
+ import type { PhishingControllerBulkScanTokensAction } from "@metamask/phishing-controller";
3
+ import type { AssetsControllerMessenger } from "../AssetsController.cjs";
2
4
  import type { Middleware } from "../types.cjs";
3
- /**
4
- * TokenDataSource does not call external messenger actions.
5
- * It uses ApiPlatformClient directly.
6
- */
7
- export type TokenDataSourceAllowedActions = never;
8
5
  export type TokenDataSourceOptions = {
9
6
  /** ApiPlatformClient for API calls with caching */
10
7
  queryApiClient: ApiPlatformClient;
11
8
  /** Returns CAIP-19 native asset IDs from NetworkEnablementController state */
12
9
  getNativeAssetIds: () => string[];
13
10
  };
11
+ /**
12
+ * Messenger actions `TokenDataSource` may invoke (via {@link AssetsControllerMessenger}).
13
+ * Not re-exported from the package public `index` (repo ESLint); import from this module when
14
+ * typing a messenger in the same package or tests.
15
+ */
16
+ export type TokenDataSourceAllowedActions = PhishingControllerBulkScanTokensAction;
14
17
  /**
15
18
  * TokenDataSource enriches responses with token metadata from the Tokens API.
16
19
  *
@@ -19,13 +22,14 @@ export type TokenDataSourceOptions = {
19
22
  * - Fetches metadata from Tokens API v3 for assets needing enrichment
20
23
  * - Merges fetched metadata into the response
21
24
  *
22
- * Usage: Create with queryApiClient and use assetsMiddleware; no messenger required.
25
+ * Pass the same {@link AssetsControllerMessenger} as other data sources for Blockaid
26
+ * token scans.
23
27
  */
24
28
  export declare class TokenDataSource {
25
29
  #private;
26
30
  readonly name = "TokenDataSource";
27
31
  getName(): string;
28
- constructor(options: TokenDataSourceOptions);
32
+ constructor(messenger: AssetsControllerMessenger, options: TokenDataSourceOptions);
29
33
  /**
30
34
  * Get the middleware for enriching responses with token metadata.
31
35
  *
@@ -1 +1 @@
1
- {"version":3,"file":"TokenDataSource.d.cts","sourceRoot":"","sources":["../../src/data-sources/TokenDataSource.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,+BAA+B;AAO3D,OAAO,KAAK,EAGV,UAAU,EAEX,qBAAiB;AAgBlB;;;GAGG;AACH,MAAM,MAAM,6BAA6B,GAAG,KAAK,CAAC;AAMlD,MAAM,MAAM,sBAAsB,GAAG;IACnC,mDAAmD;IACnD,cAAc,EAAE,iBAAiB,CAAC;IAClC,8EAA8E;IAC9E,iBAAiB,EAAE,MAAM,MAAM,EAAE,CAAC;CACnC,CAAC;AA2DF;;;;;;;;;GASG;AACH,qBAAa,eAAe;;IAC1B,QAAQ,CAAC,IAAI,qBAAmB;IAEhC,OAAO,IAAI,MAAM;gBAUL,OAAO,EAAE,sBAAsB;IAqD3C;;;;;;;;;;OAUG;IACH,IAAI,gBAAgB,IAAI,UAAU,CAyHjC;CACF"}
1
+ {"version":3,"file":"TokenDataSource.d.cts","sourceRoot":"","sources":["../../src/data-sources/TokenDataSource.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,+BAA+B;AAC3D,OAAO,KAAK,EAEV,sCAAsC,EACvC,sCAAsC;AASvC,OAAO,KAAK,EAAE,yBAAyB,EAAE,gCAA4B;AAGrE,OAAO,KAAK,EAGV,UAAU,EAEX,qBAAiB;AAiClB,MAAM,MAAM,sBAAsB,GAAG;IACnC,mDAAmD;IACnD,cAAc,EAAE,iBAAiB,CAAC;IAClC,8EAA8E;IAC9E,iBAAiB,EAAE,MAAM,MAAM,EAAE,CAAC;CACnC,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,6BAA6B,GACvC,sCAAsC,CAAC;AA2DzC;;;;;;;;;;GAUG;AACH,qBAAa,eAAe;;IAC1B,QAAQ,CAAC,IAAI,qBAAmB;IAEhC,OAAO,IAAI,MAAM;gBAcf,SAAS,EAAE,yBAAyB,EACpC,OAAO,EAAE,sBAAsB;IAuIjC;;;;;;;;;;OAUG;IACH,IAAI,gBAAgB,IAAI,UAAU,CAsLjC;CACF"}
@@ -1,16 +1,19 @@
1
1
  import { ApiPlatformClient } from "@metamask/core-backend";
2
+ import type { PhishingControllerBulkScanTokensAction } from "@metamask/phishing-controller";
3
+ import type { AssetsControllerMessenger } from "../AssetsController.mjs";
2
4
  import type { Middleware } from "../types.mjs";
3
- /**
4
- * TokenDataSource does not call external messenger actions.
5
- * It uses ApiPlatformClient directly.
6
- */
7
- export type TokenDataSourceAllowedActions = never;
8
5
  export type TokenDataSourceOptions = {
9
6
  /** ApiPlatformClient for API calls with caching */
10
7
  queryApiClient: ApiPlatformClient;
11
8
  /** Returns CAIP-19 native asset IDs from NetworkEnablementController state */
12
9
  getNativeAssetIds: () => string[];
13
10
  };
11
+ /**
12
+ * Messenger actions `TokenDataSource` may invoke (via {@link AssetsControllerMessenger}).
13
+ * Not re-exported from the package public `index` (repo ESLint); import from this module when
14
+ * typing a messenger in the same package or tests.
15
+ */
16
+ export type TokenDataSourceAllowedActions = PhishingControllerBulkScanTokensAction;
14
17
  /**
15
18
  * TokenDataSource enriches responses with token metadata from the Tokens API.
16
19
  *
@@ -19,13 +22,14 @@ export type TokenDataSourceOptions = {
19
22
  * - Fetches metadata from Tokens API v3 for assets needing enrichment
20
23
  * - Merges fetched metadata into the response
21
24
  *
22
- * Usage: Create with queryApiClient and use assetsMiddleware; no messenger required.
25
+ * Pass the same {@link AssetsControllerMessenger} as other data sources for Blockaid
26
+ * token scans.
23
27
  */
24
28
  export declare class TokenDataSource {
25
29
  #private;
26
30
  readonly name = "TokenDataSource";
27
31
  getName(): string;
28
- constructor(options: TokenDataSourceOptions);
32
+ constructor(messenger: AssetsControllerMessenger, options: TokenDataSourceOptions);
29
33
  /**
30
34
  * Get the middleware for enriching responses with token metadata.
31
35
  *
@@ -1 +1 @@
1
- {"version":3,"file":"TokenDataSource.d.mts","sourceRoot":"","sources":["../../src/data-sources/TokenDataSource.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,+BAA+B;AAO3D,OAAO,KAAK,EAGV,UAAU,EAEX,qBAAiB;AAgBlB;;;GAGG;AACH,MAAM,MAAM,6BAA6B,GAAG,KAAK,CAAC;AAMlD,MAAM,MAAM,sBAAsB,GAAG;IACnC,mDAAmD;IACnD,cAAc,EAAE,iBAAiB,CAAC;IAClC,8EAA8E;IAC9E,iBAAiB,EAAE,MAAM,MAAM,EAAE,CAAC;CACnC,CAAC;AA2DF;;;;;;;;;GASG;AACH,qBAAa,eAAe;;IAC1B,QAAQ,CAAC,IAAI,qBAAmB;IAEhC,OAAO,IAAI,MAAM;gBAUL,OAAO,EAAE,sBAAsB;IAqD3C;;;;;;;;;;OAUG;IACH,IAAI,gBAAgB,IAAI,UAAU,CAyHjC;CACF"}
1
+ {"version":3,"file":"TokenDataSource.d.mts","sourceRoot":"","sources":["../../src/data-sources/TokenDataSource.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,+BAA+B;AAC3D,OAAO,KAAK,EAEV,sCAAsC,EACvC,sCAAsC;AASvC,OAAO,KAAK,EAAE,yBAAyB,EAAE,gCAA4B;AAGrE,OAAO,KAAK,EAGV,UAAU,EAEX,qBAAiB;AAiClB,MAAM,MAAM,sBAAsB,GAAG;IACnC,mDAAmD;IACnD,cAAc,EAAE,iBAAiB,CAAC;IAClC,8EAA8E;IAC9E,iBAAiB,EAAE,MAAM,MAAM,EAAE,CAAC;CACnC,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,6BAA6B,GACvC,sCAAsC,CAAC;AA2DzC;;;;;;;;;;GAUG;AACH,qBAAa,eAAe;;IAC1B,QAAQ,CAAC,IAAI,qBAAmB;IAEhC,OAAO,IAAI,MAAM;gBAcf,SAAS,EAAE,yBAAyB,EACpC,OAAO,EAAE,sBAAsB;IAuIjC;;;;;;;;;;OAUG;IACH,IAAI,gBAAgB,IAAI,UAAU,CAsLjC;CACF"}
@@ -9,18 +9,34 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
9
9
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
10
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
11
  };
12
- var _TokenDataSource_instances, _TokenDataSource_apiClient, _TokenDataSource_getNativeAssetIds, _TokenDataSource_getSupportedNetworks, _TokenDataSource_filterAssetsByNetwork;
12
+ var _TokenDataSource_instances, _TokenDataSource_apiClient, _TokenDataSource_getNativeAssetIds, _TokenDataSource_messenger, _TokenDataSource_getSupportedNetworks, _TokenDataSource_filterAssetsByNetwork, _TokenDataSource_filterBlockaidSpamTokens;
13
13
  import { ApiPlatformClient } from "@metamask/core-backend";
14
- import { parseCaipAssetType } from "@metamask/utils";
15
- import { isStakingContractAssetId } from "./evm-rpc-services/index.mjs";
14
+ import { TokenScanResultType } from "@metamask/phishing-controller";
15
+ import { KnownCaipNamespace, parseCaipAssetType } from "@metamask/utils";
16
+ import { isStakingContractAssetId, reduceInBatchesSerially } from "./evm-rpc-services/index.mjs";
16
17
  import { projectLogger, createModuleLogger } from "../logger.mjs";
17
18
  import { forDataTypes } from "../types.mjs";
18
19
  // ============================================================================
19
20
  // CONSTANTS
20
21
  // ============================================================================
21
22
  const CONTROLLER_NAME = 'TokenDataSource';
22
- const MIN_TOKEN_OCCURRENCES = 3;
23
23
  const log = createModuleLogger(projectLogger, CONTROLLER_NAME);
24
+ /** Max asset IDs per tokens API request. */
25
+ const TOKENS_API_BATCH_SIZE = 50;
26
+ /** Max tokens per PhishingController:bulkScanTokens request (see PhishingController). */
27
+ const BULK_SCAN_BATCH_SIZE = 100;
28
+ /**
29
+ * Minimum number of aggregator occurrences required for an EVM ERC-20 token to
30
+ * pass the spam filter. Non-EVM tokens are filtered via Blockaid bulk scan instead.
31
+ */
32
+ const MIN_TOKEN_OCCURRENCES = 3;
33
+ /** CAIP-19 `assetNamespace` segments used across filtering logic. */
34
+ var CaipAssetNamespace;
35
+ (function (CaipAssetNamespace) {
36
+ CaipAssetNamespace["Slip44"] = "slip44";
37
+ CaipAssetNamespace["Erc20"] = "erc20";
38
+ CaipAssetNamespace["Token"] = "token";
39
+ })(CaipAssetNamespace || (CaipAssetNamespace = {}));
24
40
  // ============================================================================
25
41
  // HELPER FUNCTIONS
26
42
  // ============================================================================
@@ -78,19 +94,23 @@ function transformV3AssetResponseToMetadata(assetId, assetData) {
78
94
  * - Fetches metadata from Tokens API v3 for assets needing enrichment
79
95
  * - Merges fetched metadata into the response
80
96
  *
81
- * Usage: Create with queryApiClient and use assetsMiddleware; no messenger required.
97
+ * Pass the same {@link AssetsControllerMessenger} as other data sources for Blockaid
98
+ * token scans.
82
99
  */
83
100
  export class TokenDataSource {
84
101
  getName() {
85
102
  return this.name;
86
103
  }
87
- constructor(options) {
104
+ constructor(messenger, options) {
88
105
  _TokenDataSource_instances.add(this);
89
106
  this.name = CONTROLLER_NAME;
90
107
  /** ApiPlatformClient for cached API calls */
91
108
  _TokenDataSource_apiClient.set(this, void 0);
92
109
  /** Returns CAIP-19 native asset IDs from NetworkEnablementController state */
93
110
  _TokenDataSource_getNativeAssetIds.set(this, void 0);
111
+ /** Shared controller messenger — used for `PhishingController:bulkScanTokens`. */
112
+ _TokenDataSource_messenger.set(this, void 0);
113
+ __classPrivateFieldSet(this, _TokenDataSource_messenger, messenger, "f");
94
114
  __classPrivateFieldSet(this, _TokenDataSource_apiClient, options.queryApiClient, "f");
95
115
  __classPrivateFieldSet(this, _TokenDataSource_getNativeAssetIds, options.getNativeAssetIds, "f");
96
116
  }
@@ -147,9 +167,7 @@ export class TokenDataSource {
147
167
  return next(ctx);
148
168
  }
149
169
  try {
150
- // Use ApiPlatformClient for fetching asset metadata
151
- // API returns an array with assetId as a property on each item
152
- const metadataResponse = await __classPrivateFieldGet(this, _TokenDataSource_apiClient, "f").tokens.fetchV3Assets(supportedAssetIds, {
170
+ const fetchOptions = {
153
171
  includeIconUrl: true,
154
172
  includeMarketData: true,
155
173
  includeMetadata: true,
@@ -157,14 +175,58 @@ export class TokenDataSource {
157
175
  includeRwaData: true,
158
176
  includeAggregators: true,
159
177
  includeOccurrences: true,
178
+ };
179
+ const metadataResponse = await reduceInBatchesSerially({
180
+ values: supportedAssetIds,
181
+ batchSize: TOKENS_API_BATCH_SIZE,
182
+ eachBatch: async (workingResult, batch) => {
183
+ const batchResponse = await __classPrivateFieldGet(this, _TokenDataSource_apiClient, "f").tokens.fetchV3Assets(batch, fetchOptions);
184
+ return [...workingResult, ...batchResponse];
185
+ },
186
+ initialResult: [],
160
187
  });
188
+ // Split assets by chain type: EVM uses occurrence-count filtering;
189
+ // non-EVM non-native uses Blockaid; native (slip44) is always allowed.
190
+ const occurrencesByAssetId = new Map(metadataResponse.map((a) => [a.assetId, a.occurrences]));
191
+ const evmErc20Ids = [];
192
+ const nonEvmTokenIds = [];
193
+ for (const assetData of metadataResponse) {
194
+ const { assetNamespace, chain } = parseCaipAssetType(assetData.assetId);
195
+ if (assetNamespace === CaipAssetNamespace.Slip44) {
196
+ // Native assets are always kept — no filtering.
197
+ }
198
+ else if (assetNamespace === CaipAssetNamespace.Erc20 &&
199
+ chain.namespace === KnownCaipNamespace.Eip155) {
200
+ evmErc20Ids.push(assetData.assetId);
201
+ }
202
+ else if (assetNamespace === CaipAssetNamespace.Token) {
203
+ nonEvmTokenIds.push(assetData.assetId);
204
+ }
205
+ }
206
+ // EVM: require minimum occurrence count to suppress low-signal tokens.
207
+ // Tokens with no occurrence data (undefined) are treated the same as
208
+ // zero occurrences and filtered out.
209
+ const allowedEvmIds = new Set(evmErc20Ids.filter((id) => (occurrencesByAssetId.get(id) ?? 0) >= MIN_TOKEN_OCCURRENCES));
210
+ // Non-EVM: Blockaid bulk scan.
211
+ const allowedNonEvmIds = new Set(await __classPrivateFieldGet(this, _TokenDataSource_instances, "m", _TokenDataSource_filterBlockaidSpamTokens).call(this, nonEvmTokenIds));
212
+ // Start with every asset the API returned; only remove those that
213
+ // fail their respective filter (EVM occurrences / non-EVM Blockaid).
214
+ // Native (slip44) and unrecognised namespaces are kept (fail open).
215
+ const allowedAssetIds = new Set(metadataResponse.map((a) => a.assetId));
216
+ for (const id of evmErc20Ids) {
217
+ if (!allowedEvmIds.has(id)) {
218
+ allowedAssetIds.delete(id);
219
+ }
220
+ }
221
+ for (const id of nonEvmTokenIds) {
222
+ if (!allowedNonEvmIds.has(id)) {
223
+ allowedAssetIds.delete(id);
224
+ }
225
+ }
161
226
  response.assetsInfo ?? (response.assetsInfo = {});
162
227
  const filteredOutAssets = new Set();
163
228
  for (const assetData of metadataResponse) {
164
- const parsed = parseCaipAssetType(assetData.assetId);
165
- const isNative = parsed.assetNamespace === 'slip44';
166
- if (!isNative &&
167
- (assetData.occurrences ?? 0) < MIN_TOKEN_OCCURRENCES) {
229
+ if (!allowedAssetIds.has(assetData.assetId)) {
168
230
  filteredOutAssets.add(assetData.assetId);
169
231
  continue;
170
232
  }
@@ -194,7 +256,7 @@ export class TokenDataSource {
194
256
  });
195
257
  }
196
258
  }
197
- _TokenDataSource_apiClient = new WeakMap(), _TokenDataSource_getNativeAssetIds = new WeakMap(), _TokenDataSource_instances = new WeakSet(), _TokenDataSource_getSupportedNetworks =
259
+ _TokenDataSource_apiClient = new WeakMap(), _TokenDataSource_getNativeAssetIds = new WeakMap(), _TokenDataSource_messenger = new WeakMap(), _TokenDataSource_instances = new WeakSet(), _TokenDataSource_getSupportedNetworks =
198
260
  /**
199
261
  * Gets the supported networks from the API.
200
262
  * Caching is handled by ApiPlatformClient.
@@ -228,5 +290,69 @@ async function _TokenDataSource_getSupportedNetworks() {
228
290
  return false;
229
291
  }
230
292
  });
293
+ }, _TokenDataSource_filterBlockaidSpamTokens =
294
+ /**
295
+ * Filters non-EVM fungible `token` assets flagged as malicious by Blockaid
296
+ * via `PhishingController:bulkScanTokens`. Only the `token` namespace (e.g.
297
+ * Solana mints) is scanned; native (`slip44`) and EVM assets are not handled
298
+ * here (EVM uses occurrence-count filtering instead). Fails open on error.
299
+ *
300
+ * @param assets - CAIP-19 asset IDs to filter (non-EVM only).
301
+ * @returns Asset IDs with malicious tokens removed.
302
+ */
303
+ async function _TokenDataSource_filterBlockaidSpamTokens(assets) {
304
+ if (assets.length === 0) {
305
+ return assets;
306
+ }
307
+ const tokensByChain = {};
308
+ for (const asset of assets) {
309
+ try {
310
+ const { assetNamespace, assetReference, chain } = parseCaipAssetType(asset);
311
+ if (assetNamespace === CaipAssetNamespace.Token) {
312
+ const chainName = chain.namespace;
313
+ if (!tokensByChain[chainName]) {
314
+ tokensByChain[chainName] = [];
315
+ }
316
+ tokensByChain[chainName].push({ asset, address: assetReference });
317
+ }
318
+ }
319
+ catch {
320
+ // Malformed or unsupported for bulk scan — keep asset (fail open)
321
+ }
322
+ }
323
+ if (Object.keys(tokensByChain).length === 0) {
324
+ return assets;
325
+ }
326
+ const rejectedAssets = new Set();
327
+ try {
328
+ for (const [chainId, tokenEntries] of Object.entries(tokensByChain)) {
329
+ const addresses = tokenEntries.map((entry) => entry.address);
330
+ const batches = [];
331
+ for (let i = 0; i < addresses.length; i += BULK_SCAN_BATCH_SIZE) {
332
+ batches.push(addresses.slice(i, i + BULK_SCAN_BATCH_SIZE));
333
+ }
334
+ const batchResults = await Promise.allSettled(batches.map((batch) => __classPrivateFieldGet(this, _TokenDataSource_messenger, "f").call('PhishingController:bulkScanTokens', {
335
+ chainId,
336
+ tokens: batch,
337
+ })));
338
+ const scanResponse = {};
339
+ for (const result of batchResults) {
340
+ if (result.status === 'fulfilled') {
341
+ Object.assign(scanResponse, result.value);
342
+ }
343
+ }
344
+ for (const entry of tokenEntries) {
345
+ const result = scanResponse[entry.address];
346
+ if (result?.result_type === TokenScanResultType.Malicious) {
347
+ rejectedAssets.add(entry.asset);
348
+ }
349
+ }
350
+ }
351
+ }
352
+ catch (error) {
353
+ log('Blockaid bulk token scan failed; keeping all tokens', { error });
354
+ return assets;
355
+ }
356
+ return assets.filter((asset) => !rejectedAssets.has(asset));
231
357
  };
232
358
  //# sourceMappingURL=TokenDataSource.mjs.map