@metamask/assets-controller 11.0.0 → 11.1.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 (78) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/dist/AssetsController-method-action-types.cjs.map +1 -1
  3. package/dist/AssetsController-method-action-types.d.cts +23 -1
  4. package/dist/AssetsController-method-action-types.d.cts.map +1 -1
  5. package/dist/AssetsController-method-action-types.d.mts +23 -1
  6. package/dist/AssetsController-method-action-types.d.mts.map +1 -1
  7. package/dist/AssetsController-method-action-types.mjs.map +1 -1
  8. package/dist/AssetsController.cjs +99 -51
  9. package/dist/AssetsController.cjs.map +1 -1
  10. package/dist/AssetsController.d.cts +21 -1
  11. package/dist/AssetsController.d.cts.map +1 -1
  12. package/dist/AssetsController.d.mts +21 -1
  13. package/dist/AssetsController.d.mts.map +1 -1
  14. package/dist/AssetsController.mjs +99 -51
  15. package/dist/AssetsController.mjs.map +1 -1
  16. package/dist/data-sources/AccountsApiDataSource.cjs +95 -9
  17. package/dist/data-sources/AccountsApiDataSource.cjs.map +1 -1
  18. package/dist/data-sources/AccountsApiDataSource.d.cts +8 -1
  19. package/dist/data-sources/AccountsApiDataSource.d.cts.map +1 -1
  20. package/dist/data-sources/AccountsApiDataSource.d.mts +8 -1
  21. package/dist/data-sources/AccountsApiDataSource.d.mts.map +1 -1
  22. package/dist/data-sources/AccountsApiDataSource.mjs +95 -9
  23. package/dist/data-sources/AccountsApiDataSource.mjs.map +1 -1
  24. package/dist/data-sources/TokenDataSource.cjs +73 -12
  25. package/dist/data-sources/TokenDataSource.cjs.map +1 -1
  26. package/dist/data-sources/TokenDataSource.d.cts +4 -1
  27. package/dist/data-sources/TokenDataSource.d.cts.map +1 -1
  28. package/dist/data-sources/TokenDataSource.d.mts +4 -1
  29. package/dist/data-sources/TokenDataSource.d.mts.map +1 -1
  30. package/dist/data-sources/TokenDataSource.mjs +73 -12
  31. package/dist/data-sources/TokenDataSource.mjs.map +1 -1
  32. package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.cjs +86 -27
  33. package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.cjs.map +1 -1
  34. package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.d.cts +4 -3
  35. package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.d.cts.map +1 -1
  36. package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.d.mts +4 -3
  37. package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.d.mts.map +1 -1
  38. package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.mjs +86 -27
  39. package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.mjs.map +1 -1
  40. package/dist/index.cjs.map +1 -1
  41. package/dist/index.d.cts +2 -2
  42. package/dist/index.d.cts.map +1 -1
  43. package/dist/index.d.mts +2 -2
  44. package/dist/index.d.mts.map +1 -1
  45. package/dist/index.mjs.map +1 -1
  46. package/dist/utils/formatExchangeRatesForBridge.cjs +36 -7
  47. package/dist/utils/formatExchangeRatesForBridge.cjs.map +1 -1
  48. package/dist/utils/formatExchangeRatesForBridge.d.cts +20 -14
  49. package/dist/utils/formatExchangeRatesForBridge.d.cts.map +1 -1
  50. package/dist/utils/formatExchangeRatesForBridge.d.mts +20 -14
  51. package/dist/utils/formatExchangeRatesForBridge.d.mts.map +1 -1
  52. package/dist/utils/formatExchangeRatesForBridge.mjs +34 -5
  53. package/dist/utils/formatExchangeRatesForBridge.mjs.map +1 -1
  54. package/dist/utils/formatStateForTransactionPay.cjs +56 -9
  55. package/dist/utils/formatStateForTransactionPay.cjs.map +1 -1
  56. package/dist/utils/formatStateForTransactionPay.d.cts +23 -17
  57. package/dist/utils/formatStateForTransactionPay.d.cts.map +1 -1
  58. package/dist/utils/formatStateForTransactionPay.d.mts +23 -17
  59. package/dist/utils/formatStateForTransactionPay.d.mts.map +1 -1
  60. package/dist/utils/formatStateForTransactionPay.mjs +54 -7
  61. package/dist/utils/formatStateForTransactionPay.mjs.map +1 -1
  62. package/dist/utils/index.cjs +4 -1
  63. package/dist/utils/index.cjs.map +1 -1
  64. package/dist/utils/index.d.cts +5 -5
  65. package/dist/utils/index.d.cts.map +1 -1
  66. package/dist/utils/index.d.mts +5 -5
  67. package/dist/utils/index.d.mts.map +1 -1
  68. package/dist/utils/index.mjs +3 -3
  69. package/dist/utils/index.mjs.map +1 -1
  70. package/dist/utils/normalizeAssetId.cjs +17 -3
  71. package/dist/utils/normalizeAssetId.cjs.map +1 -1
  72. package/dist/utils/normalizeAssetId.d.cts +10 -1
  73. package/dist/utils/normalizeAssetId.d.cts.map +1 -1
  74. package/dist/utils/normalizeAssetId.d.mts +10 -1
  75. package/dist/utils/normalizeAssetId.d.mts.map +1 -1
  76. package/dist/utils/normalizeAssetId.mjs +19 -1
  77. package/dist/utils/normalizeAssetId.mjs.map +1 -1
  78. package/package.json +7 -4
@@ -9,7 +9,7 @@ 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_getAssetType, _TokenDataSource_messenger, _TokenDataSource_fetchTimeoutMs, _TokenDataSource_getSupportedNetworks, _TokenDataSource_filterAssetsByNetwork, _TokenDataSource_filterBlockaidSpamTokens;
12
+ var _TokenDataSource_instances, _TokenDataSource_apiClient, _TokenDataSource_getNativeAssetIds, _TokenDataSource_getAssetType, _TokenDataSource_messenger, _TokenDataSource_fetchTimeoutMs, _TokenDataSource_getSupportedNetworks, _TokenDataSource_getSuggestedOccurrenceFloors, _TokenDataSource_filterAssetsByNetwork, _TokenDataSource_filterBlockaidSpamTokens;
13
13
  import { ApiPlatformClient } from "@metamask/core-backend";
14
14
  import { TokenScanResultType } from "@metamask/phishing-controller";
15
15
  import { KnownCaipNamespace, parseCaipAssetType } from "@metamask/utils";
@@ -28,10 +28,11 @@ const TOKENS_API_BATCH_SIZE = 50;
28
28
  /** Max tokens per PhishingController:bulkScanTokens request (see PhishingController). */
29
29
  const BULK_SCAN_BATCH_SIZE = 100;
30
30
  /**
31
- * Minimum number of aggregator occurrences required for an EVM ERC-20 token to
32
- * pass the spam filter. Non-EVM tokens are filtered via Blockaid bulk scan instead.
31
+ * Fallback minimum aggregator occurrences for EVM ERC-20 spam filtering when
32
+ * Token API `/v1/suggestedOccurrenceFloors` has no entry for the chain (or
33
+ * the floors request fails). Non-EVM tokens are filtered via Blockaid instead.
33
34
  */
34
- const MIN_TOKEN_OCCURRENCES = 3;
35
+ const DEFAULT_OCCURRENCE_FLOOR = 3;
35
36
  /** CAIP-19 `assetNamespace` segments used across filtering logic. */
36
37
  export var CaipAssetNamespace;
37
38
  (function (CaipAssetNamespace) {
@@ -78,6 +79,23 @@ function transformV3AssetResponseToMetadata(assetId, assetData, getAssetType) {
78
79
  };
79
80
  return metadata;
80
81
  }
82
+ /**
83
+ * Resolve the occurrence floor for an EVM asset from suggested floors keyed by
84
+ * decimal chain ID (e.g. `{ "1": 3, "143": 1 }`).
85
+ *
86
+ * @param assetId - CAIP-19 asset ID.
87
+ * @param floors - Map of decimal chain ID → suggested floor.
88
+ * @returns Floor to apply, or {@link DEFAULT_OCCURRENCE_FLOOR} when missing.
89
+ */
90
+ function getOccurrenceFloorForAsset(assetId, floors) {
91
+ try {
92
+ const { chain } = parseCaipAssetType(assetId);
93
+ return floors[chain.reference] ?? DEFAULT_OCCURRENCE_FLOOR;
94
+ }
95
+ catch {
96
+ return DEFAULT_OCCURRENCE_FLOOR;
97
+ }
98
+ }
81
99
  // ============================================================================
82
100
  // TOKEN DATA SOURCE
83
101
  // ============================================================================
@@ -86,7 +104,10 @@ function transformV3AssetResponseToMetadata(assetId, assetData, getAssetType) {
86
104
  *
87
105
  * This middleware-based data source:
88
106
  * - Checks detected assets for missing metadata/images
107
+ * - Also checks `assetsBalance` entries missing metadata/images
89
108
  * - Fetches metadata from Tokens API v3 for assets needing enrichment
109
+ * - Filters EVM ERC-20 spam using per-chain floors from Token API
110
+ * `/v1/suggestedOccurrenceFloors` (default floor 3)
90
111
  * - Merges fetched metadata into the response
91
112
  *
92
113
  * Pass the same {@link AssetsControllerMessenger} as other data sources for Blockaid
@@ -119,7 +140,7 @@ export class TokenDataSource {
119
140
  *
120
141
  * This middleware:
121
142
  * 1. Extracts the response from context
122
- * 2. Fetches metadata for detected assets (assets without metadata)
143
+ * 2. Fetches metadata for detected assets and balances missing metadata
123
144
  * 3. Enriches the response with fetched metadata
124
145
  * 4. Calls next() at the end to continue the middleware chain
125
146
  *
@@ -166,11 +187,32 @@ export class TokenDataSource {
166
187
  }
167
188
  }
168
189
  }
190
+ // Also fetch metadata for balances that are missing it
191
+ if (response.assetsBalance) {
192
+ for (const accountBalances of Object.values(response.assetsBalance)) {
193
+ for (const assetId of Object.keys(accountBalances)) {
194
+ if (response.assetsInfo?.[assetId]?.image) {
195
+ continue;
196
+ }
197
+ if (stateMetadata[assetId]?.image) {
198
+ continue;
199
+ }
200
+ if (isStakingContractAssetId(assetId)) {
201
+ continue;
202
+ }
203
+ assetIdsNeedingMetadata.add(assetId);
204
+ }
205
+ }
206
+ }
169
207
  if (assetIdsNeedingMetadata.size === 0) {
170
208
  return next(ctx);
171
209
  }
172
- // Filter asset IDs to only include supported networks
173
- const supportedNetworks = await __classPrivateFieldGet(this, _TokenDataSource_instances, "m", _TokenDataSource_getSupportedNetworks).call(this);
210
+ // Filter asset IDs to only include supported networks; load per-chain
211
+ // occurrence floors in parallel (Token API suggested floors).
212
+ const [supportedNetworks, suggestedOccurrenceFloors] = await Promise.all([
213
+ __classPrivateFieldGet(this, _TokenDataSource_instances, "m", _TokenDataSource_getSupportedNetworks).call(this),
214
+ __classPrivateFieldGet(this, _TokenDataSource_instances, "m", _TokenDataSource_getSuggestedOccurrenceFloors).call(this),
215
+ ]);
174
216
  const supportedAssetIds = __classPrivateFieldGet(this, _TokenDataSource_instances, "m", _TokenDataSource_filterAssetsByNetwork).call(this, [...assetIdsNeedingMetadata], supportedNetworks);
175
217
  if (supportedAssetIds.length === 0) {
176
218
  return next(ctx);
@@ -213,14 +255,16 @@ export class TokenDataSource {
213
255
  nonEvmTokenIds.push(assetId);
214
256
  }
215
257
  }
216
- // EVM: require minimum occurrence count to suppress low-signal tokens.
217
- // Tokens with no occurrence data (undefined) are treated the same as
218
- // zero occurrences and filtered out.
258
+ // EVM: require per-chain suggested occurrence floor (from Token API
259
+ // `/v1/suggestedOccurrenceFloors`, default {@link DEFAULT_OCCURRENCE_FLOOR})
260
+ // to suppress low-signal tokens. Tokens with no occurrence data
261
+ // (undefined) are treated the same as zero occurrences and filtered out.
219
262
  // Custom assets (user-imported) bypass the occurrence filter — users
220
263
  // can import whatever they want and we must keep their metadata even
221
- // if the API has fewer than `MIN_TOKEN_OCCURRENCES` aggregator hits.
264
+ // if the API has fewer aggregator hits than the floor.
222
265
  const allowedEvmIds = new Set(evmErc20Ids.filter((id) => customAssetIds.has(id.toLowerCase()) ||
223
- (occurrencesByAssetId.get(id) ?? 0) >= MIN_TOKEN_OCCURRENCES ||
266
+ (occurrencesByAssetId.get(id) ?? 0) >=
267
+ getOccurrenceFloorForAsset(id, suggestedOccurrenceFloors) ||
224
268
  id.includes(`/erc20:${MUSD_ADDRESS_LOWERCASE}`)));
225
269
  // Non-EVM: Blockaid bulk scan.
226
270
  // Custom assets (user-imported) bypass Blockaid filtering.
@@ -296,6 +340,23 @@ async function _TokenDataSource_getSupportedNetworks() {
296
340
  log('Failed to fetch supported networks', { error });
297
341
  return new Set();
298
342
  }
343
+ }, _TokenDataSource_getSuggestedOccurrenceFloors =
344
+ /**
345
+ * Fetches per-chain suggested occurrence floors from Token API
346
+ * (`GET /v1/suggestedOccurrenceFloors`). Caching is handled by
347
+ * ApiPlatformClient. Fails open to an empty map so callers fall back to
348
+ * {@link DEFAULT_OCCURRENCE_FLOOR}.
349
+ *
350
+ * @returns Map of decimal chain ID → suggested occurrence floor.
351
+ */
352
+ async function _TokenDataSource_getSuggestedOccurrenceFloors() {
353
+ try {
354
+ return await fetchWithTimeout(() => __classPrivateFieldGet(this, _TokenDataSource_apiClient, "f").token.fetchV1SuggestedOccurrenceFloors(), __classPrivateFieldGet(this, _TokenDataSource_fetchTimeoutMs, "f"));
355
+ }
356
+ catch (error) {
357
+ log('Failed to fetch suggested occurrence floors', { error });
358
+ return {};
359
+ }
299
360
  }, _TokenDataSource_filterAssetsByNetwork = function _TokenDataSource_filterAssetsByNetwork(assetIds, supportedNetworks) {
300
361
  return assetIds.filter((assetId) => {
301
362
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"TokenDataSource.mjs","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 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';\nimport { fetchWithTimeout } from '../utils';\nimport {\n isStakingContractAssetId,\n reduceInBatchesSerially,\n} from './evm-rpc-services';\n\n// ============================================================================\n// CONSTANTS\n// ============================================================================\n\nconst CONTROLLER_NAME = 'TokenDataSource';\nconst DEFAULT_FETCH_TIMEOUT_MS = 15_000;\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. */\nexport enum CaipAssetNamespace {\n Slip44 = 'slip44',\n Erc20 = 'erc20',\n Token = 'token',\n}\n\nconst MUSD_ADDRESS_LOWERCASE = '0xaca92e438df0b2401ff60da7e4337b687a2435da';\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 /** Returns the asset type ('native' | 'erc20' | 'spl') for a given CAIP-19 asset ID */\n getAssetType: (assetId: Caip19AssetId) => 'native' | 'erc20' | 'spl';\n /**\n * Timeout in ms for a single Tokens API call (default: 15000). When it\n * fires, the batch rejects so metadata enrichment proceeds without it.\n */\n fetchTimeoutMs?: number;\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 * @param getAssetType - Returns the asset type for a given CAIP-19 asset ID.\n * @returns FungibleAssetMetadata for state storage.\n */\nfunction transformV3AssetResponseToMetadata(\n assetId: Caip19AssetId,\n assetData: V3AssetResponse,\n getAssetType: (id: Caip19AssetId) => 'native' | 'erc20' | 'spl',\n): AssetMetadata {\n const metadata: FungibleAssetMetadata = {\n type: getAssetType(assetId),\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 /** Returns the asset type for a given CAIP-19 asset ID */\n readonly #getAssetType: (\n assetId: Caip19AssetId,\n ) => 'native' | 'erc20' | 'spl';\n\n /** Shared controller messenger — used for `PhishingController:bulkScanTokens`. */\n readonly #messenger: AssetsControllerMessenger;\n\n readonly #fetchTimeoutMs: number;\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 this.#getAssetType = options.getAssetType;\n this.#fetchTimeoutMs = options.fetchTimeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;\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 = await fetchWithTimeout(\n () => this.#apiClient.tokens.fetchTokenV2SupportedNetworks(),\n this.#fetchTimeoutMs,\n );\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, customAssets } = ctx.getAssetsState();\n const assetIdsNeedingMetadata = new Set<string>();\n\n // Custom assets are user-imported — exempt from spam filtering.\n // State stores asset IDs in their normalized (checksummed) form, but the\n // V3 Tokens API can return them lower-cased. Lowercase both sides so the\n // bypass is robust to address-case differences across data sources.\n const customAssetIds = new Set<string>(\n Object.values(customAssets ?? {})\n .flat()\n .map((id) => id.toLowerCase()),\n );\n\n // Always include native asset IDs from NetworkEnablementController\n const nativeAssetIdsList = this.#getNativeAssetIds();\n const nativeAssetIds = new Set(\n nativeAssetIdsList.map((id) => id.toLowerCase()),\n );\n for (const nativeAssetId of nativeAssetIdsList) {\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 fetchWithTimeout(\n () => this.#apiClient.tokens.fetchV3Assets(batch, fetchOptions),\n this.#fetchTimeoutMs,\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 assets are 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 assetId = assetData.assetId as Caip19AssetId;\n const { assetNamespace, chain } = parseCaipAssetType(assetId);\n if (nativeAssetIds.has(assetId.toLowerCase())) {\n // Native assets are always kept — no filtering.\n } else if (\n assetNamespace === CaipAssetNamespace.Erc20 &&\n chain.namespace === KnownCaipNamespace.Eip155\n ) {\n evmErc20Ids.push(assetId);\n } else if (assetNamespace === CaipAssetNamespace.Token) {\n nonEvmTokenIds.push(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 // Custom assets (user-imported) bypass the occurrence filter — users\n // can import whatever they want and we must keep their metadata even\n // if the API has fewer than `MIN_TOKEN_OCCURRENCES` aggregator hits.\n const allowedEvmIds = new Set(\n evmErc20Ids.filter(\n (id) =>\n customAssetIds.has(id.toLowerCase()) ||\n (occurrencesByAssetId.get(id) ?? 0) >= MIN_TOKEN_OCCURRENCES ||\n id.includes(`/erc20:${MUSD_ADDRESS_LOWERCASE}`),\n ),\n );\n\n // Non-EVM: Blockaid bulk scan.\n // Custom assets (user-imported) bypass Blockaid filtering.\n const nonEvmToScan = nonEvmTokenIds.filter(\n (id) => !customAssetIds.has(id.toLowerCase()),\n );\n const allowedNonEvmIds = new Set([\n ...nonEvmTokenIds.filter((id) =>\n customAssetIds.has(id.toLowerCase()),\n ),\n ...(await this.#filterBlockaidSpamTokens(nonEvmToScan)),\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 caipAssetId,\n assetData,\n this.#getAssetType,\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.mjs","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 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';\nimport { fetchWithTimeout } from '../utils';\nimport {\n isStakingContractAssetId,\n reduceInBatchesSerially,\n} from './evm-rpc-services';\n\n// ============================================================================\n// CONSTANTS\n// ============================================================================\n\nconst CONTROLLER_NAME = 'TokenDataSource';\nconst DEFAULT_FETCH_TIMEOUT_MS = 15_000;\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 * Fallback minimum aggregator occurrences for EVM ERC-20 spam filtering when\n * Token API `/v1/suggestedOccurrenceFloors` has no entry for the chain (or\n * the floors request fails). Non-EVM tokens are filtered via Blockaid instead.\n */\nconst DEFAULT_OCCURRENCE_FLOOR = 3;\n\n/** CAIP-19 `assetNamespace` segments used across filtering logic. */\nexport enum CaipAssetNamespace {\n Slip44 = 'slip44',\n Erc20 = 'erc20',\n Token = 'token',\n}\n\nconst MUSD_ADDRESS_LOWERCASE = '0xaca92e438df0b2401ff60da7e4337b687a2435da';\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 /** Returns the asset type ('native' | 'erc20' | 'spl') for a given CAIP-19 asset ID */\n getAssetType: (assetId: Caip19AssetId) => 'native' | 'erc20' | 'spl';\n /**\n * Timeout in ms for a single Tokens API call (default: 15000). When it\n * fires, the batch rejects so metadata enrichment proceeds without it.\n */\n fetchTimeoutMs?: number;\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 * @param getAssetType - Returns the asset type for a given CAIP-19 asset ID.\n * @returns FungibleAssetMetadata for state storage.\n */\nfunction transformV3AssetResponseToMetadata(\n assetId: Caip19AssetId,\n assetData: V3AssetResponse,\n getAssetType: (id: Caip19AssetId) => 'native' | 'erc20' | 'spl',\n): AssetMetadata {\n const metadata: FungibleAssetMetadata = {\n type: getAssetType(assetId),\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 * Resolve the occurrence floor for an EVM asset from suggested floors keyed by\n * decimal chain ID (e.g. `{ \"1\": 3, \"143\": 1 }`).\n *\n * @param assetId - CAIP-19 asset ID.\n * @param floors - Map of decimal chain ID → suggested floor.\n * @returns Floor to apply, or {@link DEFAULT_OCCURRENCE_FLOOR} when missing.\n */\nfunction getOccurrenceFloorForAsset(\n assetId: string,\n floors: Record<string, number>,\n): number {\n try {\n const { chain } = parseCaipAssetType(assetId as CaipAssetType);\n return floors[chain.reference] ?? DEFAULT_OCCURRENCE_FLOOR;\n } catch {\n return DEFAULT_OCCURRENCE_FLOOR;\n }\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 * - Also checks `assetsBalance` entries missing metadata/images\n * - Fetches metadata from Tokens API v3 for assets needing enrichment\n * - Filters EVM ERC-20 spam using per-chain floors from Token API\n * `/v1/suggestedOccurrenceFloors` (default floor 3)\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 /** Returns the asset type for a given CAIP-19 asset ID */\n readonly #getAssetType: (\n assetId: Caip19AssetId,\n ) => 'native' | 'erc20' | 'spl';\n\n /** Shared controller messenger — used for `PhishingController:bulkScanTokens`. */\n readonly #messenger: AssetsControllerMessenger;\n\n readonly #fetchTimeoutMs: number;\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 this.#getAssetType = options.getAssetType;\n this.#fetchTimeoutMs = options.fetchTimeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;\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 = await fetchWithTimeout(\n () => this.#apiClient.tokens.fetchTokenV2SupportedNetworks(),\n this.#fetchTimeoutMs,\n );\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 * Fetches per-chain suggested occurrence floors from Token API\n * (`GET /v1/suggestedOccurrenceFloors`). Caching is handled by\n * ApiPlatformClient. Fails open to an empty map so callers fall back to\n * {@link DEFAULT_OCCURRENCE_FLOOR}.\n *\n * @returns Map of decimal chain ID → suggested occurrence floor.\n */\n async #getSuggestedOccurrenceFloors(): Promise<Record<string, number>> {\n try {\n return await fetchWithTimeout(\n () => this.#apiClient.token.fetchV1SuggestedOccurrenceFloors(),\n this.#fetchTimeoutMs,\n );\n } catch (error) {\n log('Failed to fetch suggested occurrence floors', { error });\n return {};\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 and balances missing 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, customAssets } = ctx.getAssetsState();\n const assetIdsNeedingMetadata = new Set<string>();\n\n // Custom assets are user-imported — exempt from spam filtering.\n // State stores asset IDs in their normalized (checksummed) form, but the\n // V3 Tokens API can return them lower-cased. Lowercase both sides so the\n // bypass is robust to address-case differences across data sources.\n const customAssetIds = new Set<string>(\n Object.values(customAssets ?? {})\n .flat()\n .map((id) => id.toLowerCase()),\n );\n\n // Always include native asset IDs from NetworkEnablementController\n const nativeAssetIdsList = this.#getNativeAssetIds();\n const nativeAssetIds = new Set(\n nativeAssetIdsList.map((id) => id.toLowerCase()),\n );\n for (const nativeAssetId of nativeAssetIdsList) {\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 // Also fetch metadata for balances that are missing it\n if (response.assetsBalance) {\n for (const accountBalances of Object.values(response.assetsBalance)) {\n for (const assetId of Object.keys(\n accountBalances,\n ) as Caip19AssetId[]) {\n if (response.assetsInfo?.[assetId]?.image) {\n continue;\n }\n if (stateMetadata[assetId]?.image) {\n continue;\n }\n if (isStakingContractAssetId(assetId)) {\n continue;\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; load per-chain\n // occurrence floors in parallel (Token API suggested floors).\n const [supportedNetworks, suggestedOccurrenceFloors] = await Promise.all([\n this.#getSupportedNetworks(),\n this.#getSuggestedOccurrenceFloors(),\n ]);\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 fetchWithTimeout(\n () => this.#apiClient.tokens.fetchV3Assets(batch, fetchOptions),\n this.#fetchTimeoutMs,\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 assets are 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 assetId = assetData.assetId as Caip19AssetId;\n const { assetNamespace, chain } = parseCaipAssetType(assetId);\n if (nativeAssetIds.has(assetId.toLowerCase())) {\n // Native assets are always kept — no filtering.\n } else if (\n assetNamespace === CaipAssetNamespace.Erc20 &&\n chain.namespace === KnownCaipNamespace.Eip155\n ) {\n evmErc20Ids.push(assetId);\n } else if (assetNamespace === CaipAssetNamespace.Token) {\n nonEvmTokenIds.push(assetId);\n }\n }\n\n // EVM: require per-chain suggested occurrence floor (from Token API\n // `/v1/suggestedOccurrenceFloors`, default {@link DEFAULT_OCCURRENCE_FLOOR})\n // to suppress low-signal tokens. Tokens with no occurrence data\n // (undefined) are treated the same as zero occurrences and filtered out.\n // Custom assets (user-imported) bypass the occurrence filter — users\n // can import whatever they want and we must keep their metadata even\n // if the API has fewer aggregator hits than the floor.\n const allowedEvmIds = new Set(\n evmErc20Ids.filter(\n (id) =>\n customAssetIds.has(id.toLowerCase()) ||\n (occurrencesByAssetId.get(id) ?? 0) >=\n getOccurrenceFloorForAsset(id, suggestedOccurrenceFloors) ||\n id.includes(`/erc20:${MUSD_ADDRESS_LOWERCASE}`),\n ),\n );\n\n // Non-EVM: Blockaid bulk scan.\n // Custom assets (user-imported) bypass Blockaid filtering.\n const nonEvmToScan = nonEvmTokenIds.filter(\n (id) => !customAssetIds.has(id.toLowerCase()),\n );\n const allowedNonEvmIds = new Set([\n ...nonEvmTokenIds.filter((id) =>\n customAssetIds.has(id.toLowerCase()),\n ),\n ...(await this.#filterBlockaidSpamTokens(nonEvmToScan)),\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 caipAssetId,\n assetData,\n this.#getAssetType,\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"]}
@@ -10,7 +10,7 @@ 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 _TokensApiClient_instances, _TokensApiClient_fetch, _TokensApiClient_queryClient, _TokensApiClient_supportedChainIds, _TokensApiClient_supportedChainIdsCachedAt, _TokensApiClient_supportedChainIdsRefreshPromise, _TokensApiClient_isSupportedChain, _TokensApiClient_refreshSupportedChainIds, _TokensApiClient_fetchTokenListUncached;
13
+ var _TokensApiClient_instances, _TokensApiClient_fetch, _TokensApiClient_queryClient, _TokensApiClient_supportedChainIds, _TokensApiClient_supportedChainIdsCachedAt, _TokensApiClient_supportedChainIdsRefreshPromise, _TokensApiClient_suggestedOccurrenceFloors, _TokensApiClient_suggestedOccurrenceFloorsCachedAt, _TokensApiClient_suggestedOccurrenceFloorsRefreshPromise, _TokensApiClient_isSupportedChain, _TokensApiClient_refreshSupportedChainIds, _TokensApiClient_getOccurrenceFloor, _TokensApiClient_refreshSuggestedOccurrenceFloors, _TokensApiClient_fetchTokenListUncached;
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.TokensApiClient = void 0;
16
16
  const controller_utils_1 = require("@metamask/controller-utils");
@@ -27,6 +27,17 @@ const TOKEN_END_POINT_API = 'https://token.api.cx.metamask.io';
27
27
  * that are not in the supported-networks list.
28
28
  */
29
29
  const SUPPORTED_NETWORKS_URL = `${TOKEN_END_POINT_API}/v2/supportedNetworks`;
30
+ /**
31
+ * Per-chain suggested occurrence floors used as the `occurrenceFloor` query
32
+ * param when fetching token lists (and aligned with TokenDataSource spam
33
+ * filtering). Keys are decimal chain IDs.
34
+ */
35
+ const SUGGESTED_OCCURRENCE_FLOORS_URL = `${TOKEN_END_POINT_API}/v1/suggestedOccurrenceFloors`;
36
+ /**
37
+ * Fallback `occurrenceFloor` when `/v1/suggestedOccurrenceFloors` has no entry
38
+ * for the chain, or the floors request fails.
39
+ */
40
+ const DEFAULT_OCCURRENCE_FLOOR = 3;
30
41
  /**
31
42
  * How long to keep the supported-networks response in the instance-level
32
43
  * cache before refreshing it. One hour is sufficient — the list rarely
@@ -35,27 +46,10 @@ const SUPPORTED_NETWORKS_URL = `${TOKEN_END_POINT_API}/v2/supportedNetworks`;
35
46
  */
36
47
  const SUPPORTED_NETWORKS_CACHE_TTL_MS = 60 * 60000;
37
48
  /**
38
- * Tempo Mainnet not yet present in `@metamask/controller-utils`'s `ChainId`
39
- * map at the time of writing, so it's spelled out as a literal here exactly as
40
- * `TokenListController` does (see `token-service.ts:getTokensURL`).
41
- */
42
- const TEMPO_MAINNET_CHAIN_ID = '0x1079';
43
- /**
44
- * Per-chain occurrence floor, mirroring `TokenListController.getTokensURL`:
45
- * Linea mainnet, MegaETH mainnet, and Tempo mainnet have thinner aggregator
46
- * coverage so we lower the floor; everything else uses the default 3.
47
- *
48
- * @param hexChainId - Hex chain ID.
49
- * @returns The occurrence floor to send to the Tokens API.
49
+ * How long to keep suggested occurrence floors cached. Same TTL as supported
50
+ * networks floors change infrequently.
50
51
  */
51
- function getOccurrenceFloor(hexChainId) {
52
- if (hexChainId === controller_utils_1.ChainId['linea-mainnet'] ||
53
- hexChainId === controller_utils_1.ChainId['megaeth-mainnet'] ||
54
- hexChainId === TEMPO_MAINNET_CHAIN_ID) {
55
- return 1;
56
- }
57
- return 3;
58
- }
52
+ const SUGGESTED_OCCURRENCE_FLOORS_CACHE_TTL_MS = 60 * 60000;
59
53
  /**
60
54
  * TanStack-Query cache config for the cached `fetchTokenList` path.
61
55
  *
@@ -72,9 +66,10 @@ const TOKEN_LIST_GC_TIME_MS = 60 * 60000;
72
66
  *
73
67
  * Fetches the per-chain ERC-20 token list from the same endpoint that
74
68
  * `TokenListController` uses (`token.api.cx.metamask.io/tokens/{chainId}`),
75
- * with the same query parameters and the same per-chain occurrence floor /
76
- * Linea aggregator filter. This keeps RPC token detection in lockstep with
77
- * the wallet's primary token list.
69
+ * with the same query parameters. The `occurrenceFloor` query param comes from
70
+ * Token API `/v1/suggestedOccurrenceFloors` (fallback {@link DEFAULT_OCCURRENCE_FLOOR}),
71
+ * matching `TokenDataSource` spam filtering. Linea's post-response aggregator
72
+ * filter is still applied client-side.
78
73
  *
79
74
  * Before fetching a chain's token list, the client checks
80
75
  * `/v2/supportedNetworks` and returns `[]` immediately for chains that are
@@ -100,6 +95,12 @@ class TokensApiClient {
100
95
  * calls arrive before the first one resolves.
101
96
  */
102
97
  _TokensApiClient_supportedChainIdsRefreshPromise.set(this, void 0);
98
+ /** Decimal chain ID → suggested occurrence floor from Token API. */
99
+ _TokensApiClient_suggestedOccurrenceFloors.set(this, void 0);
100
+ /** Timestamp of the last successful floors fetch. */
101
+ _TokensApiClient_suggestedOccurrenceFloorsCachedAt.set(this, 0);
102
+ /** In-flight floors request shared across concurrent callers. */
103
+ _TokensApiClient_suggestedOccurrenceFloorsRefreshPromise.set(this, void 0);
103
104
  __classPrivateFieldSet(this, _TokensApiClient_fetch, config?.fetch ?? globalThis.fetch.bind(globalThis), "f");
104
105
  __classPrivateFieldSet(this, _TokensApiClient_queryClient, config?.queryClient, "f");
105
106
  }
@@ -140,7 +141,7 @@ class TokensApiClient {
140
141
  }
141
142
  }
142
143
  exports.TokensApiClient = TokensApiClient;
143
- _TokensApiClient_fetch = new WeakMap(), _TokensApiClient_queryClient = new WeakMap(), _TokensApiClient_supportedChainIds = new WeakMap(), _TokensApiClient_supportedChainIdsCachedAt = new WeakMap(), _TokensApiClient_supportedChainIdsRefreshPromise = new WeakMap(), _TokensApiClient_instances = new WeakSet(), _TokensApiClient_isSupportedChain =
144
+ _TokensApiClient_fetch = new WeakMap(), _TokensApiClient_queryClient = new WeakMap(), _TokensApiClient_supportedChainIds = new WeakMap(), _TokensApiClient_supportedChainIdsCachedAt = new WeakMap(), _TokensApiClient_supportedChainIdsRefreshPromise = new WeakMap(), _TokensApiClient_suggestedOccurrenceFloors = new WeakMap(), _TokensApiClient_suggestedOccurrenceFloorsCachedAt = new WeakMap(), _TokensApiClient_suggestedOccurrenceFloorsRefreshPromise = new WeakMap(), _TokensApiClient_instances = new WeakSet(), _TokensApiClient_isSupportedChain =
144
145
  /**
145
146
  * Check whether the given chain is present in the supported-networks list.
146
147
  *
@@ -189,10 +190,68 @@ async function _TokensApiClient_refreshSupportedChainIds(now) {
189
190
  }
190
191
  })(), "f");
191
192
  return __classPrivateFieldGet(this, _TokensApiClient_supportedChainIdsRefreshPromise, "f");
193
+ }, _TokensApiClient_getOccurrenceFloor =
194
+ /**
195
+ * Resolve the `occurrenceFloor` query param for a chain from Token API
196
+ * `/v1/suggestedOccurrenceFloors`. Falls back to
197
+ * {@link DEFAULT_OCCURRENCE_FLOOR} when the chain is missing or the request
198
+ * fails.
199
+ *
200
+ * @param hexChainId - Hex chain ID.
201
+ * @returns Occurrence floor to send to `/tokens/{chainId}`.
202
+ */
203
+ async function _TokensApiClient_getOccurrenceFloor(hexChainId) {
204
+ const now = Date.now();
205
+ if (__classPrivateFieldGet(this, _TokensApiClient_suggestedOccurrenceFloors, "f") === undefined ||
206
+ now - __classPrivateFieldGet(this, _TokensApiClient_suggestedOccurrenceFloorsCachedAt, "f") >=
207
+ SUGGESTED_OCCURRENCE_FLOORS_CACHE_TTL_MS) {
208
+ await __classPrivateFieldGet(this, _TokensApiClient_instances, "m", _TokensApiClient_refreshSuggestedOccurrenceFloors).call(this, now);
209
+ }
210
+ const decimalChainId = String((0, controller_utils_1.convertHexToDecimal)(hexChainId));
211
+ return (__classPrivateFieldGet(this, _TokensApiClient_suggestedOccurrenceFloors, "f")?.[decimalChainId] ??
212
+ DEFAULT_OCCURRENCE_FLOOR);
213
+ }, _TokensApiClient_refreshSuggestedOccurrenceFloors =
214
+ /**
215
+ * Fetch `/v1/suggestedOccurrenceFloors` and update the instance cache.
216
+ * Concurrent callers share the in-flight Promise. Failures leave the cache
217
+ * empty so {@link #getOccurrenceFloor} falls back to the default.
218
+ *
219
+ * @param now - Current timestamp used to stamp the cache entry.
220
+ * @returns A promise that resolves once the cache has been refreshed.
221
+ */
222
+ async function _TokensApiClient_refreshSuggestedOccurrenceFloors(now) {
223
+ if (__classPrivateFieldGet(this, _TokensApiClient_suggestedOccurrenceFloorsRefreshPromise, "f") !== undefined) {
224
+ return __classPrivateFieldGet(this, _TokensApiClient_suggestedOccurrenceFloorsRefreshPromise, "f");
225
+ }
226
+ __classPrivateFieldSet(this, _TokensApiClient_suggestedOccurrenceFloorsRefreshPromise, (async () => {
227
+ try {
228
+ const response = await __classPrivateFieldGet(this, _TokensApiClient_fetch, "f").call(this, SUGGESTED_OCCURRENCE_FLOORS_URL);
229
+ if (response.ok) {
230
+ const data = (await response.json());
231
+ __classPrivateFieldSet(this, _TokensApiClient_suggestedOccurrenceFloors, data && typeof data === 'object' && !Array.isArray(data)
232
+ ? data
233
+ : {}, "f");
234
+ __classPrivateFieldSet(this, _TokensApiClient_suggestedOccurrenceFloorsCachedAt, now, "f");
235
+ }
236
+ else {
237
+ __classPrivateFieldSet(this, _TokensApiClient_suggestedOccurrenceFloors, {}, "f");
238
+ __classPrivateFieldSet(this, _TokensApiClient_suggestedOccurrenceFloorsCachedAt, now, "f");
239
+ }
240
+ }
241
+ catch {
242
+ __classPrivateFieldSet(this, _TokensApiClient_suggestedOccurrenceFloors, {}, "f");
243
+ __classPrivateFieldSet(this, _TokensApiClient_suggestedOccurrenceFloorsCachedAt, now, "f");
244
+ }
245
+ finally {
246
+ __classPrivateFieldSet(this, _TokensApiClient_suggestedOccurrenceFloorsRefreshPromise, undefined, "f");
247
+ }
248
+ })(), "f");
249
+ return __classPrivateFieldGet(this, _TokensApiClient_suggestedOccurrenceFloorsRefreshPromise, "f");
192
250
  }, _TokensApiClient_fetchTokenListUncached = async function _TokensApiClient_fetchTokenListUncached(hexChainId) {
193
251
  const decimalChainId = (0, controller_utils_1.convertHexToDecimal)(hexChainId);
194
- const occurrenceFloor = getOccurrenceFloor(hexChainId);
195
- // Mirrors `TokenListController.getTokensURL` exactly (token-service.ts).
252
+ const occurrenceFloor = await __classPrivateFieldGet(this, _TokensApiClient_instances, "m", _TokensApiClient_getOccurrenceFloor).call(this, hexChainId);
253
+ // Same query shape as `TokenListController.getTokensURL` (token-service.ts),
254
+ // but `occurrenceFloor` comes from `/v1/suggestedOccurrenceFloors`.
196
255
  // No `first=...` cap — the API returns the full per-chain list bounded
197
256
  // server-side by `occurrenceFloor`.
198
257
  const url = `${TOKEN_END_POINT_API}/tokens/${decimalChainId}` +
@@ -1 +1 @@
1
- {"version":3,"file":"TokensApiClient.cjs","sourceRoot":"","sources":["../../../../src/data-sources/evm-rpc-services/clients/TokensApiClient.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,iEAGoC;AAIpC;;;;;GAKG;AACH,MAAM,mBAAmB,GAAG,kCAAkC,CAAC;AAE/D;;;;GAIG;AACH,MAAM,sBAAsB,GAAG,GAAG,mBAAmB,uBAAuB,CAAC;AAE7E;;;;;GAKG;AACH,MAAM,+BAA+B,GAAG,EAAE,GAAG,KAAM,CAAC;AAEpD;;;;GAIG;AACH,MAAM,sBAAsB,GAAG,QAAiB,CAAC;AAEjD;;;;;;;GAOG;AACH,SAAS,kBAAkB,CAAC,UAAmB;IAC7C,IACE,UAAU,KAAK,0BAAiB,CAAC,eAAe,CAAC;QACjD,UAAU,KAAK,0BAAiB,CAAC,iBAAiB,CAAC;QACnD,UAAU,KAAK,sBAAsB,EACrC,CAAC;QACD,OAAO,CAAC,CAAC;IACX,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,wBAAwB,GAAG,CAAC,GAAG,KAAM,CAAC;AAC5C,MAAM,qBAAqB,GAAG,EAAE,GAAG,KAAM,CAAC;AAoD1C;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAa,eAAe;IAkB1B,YAAY,MAA8B;;QAjBjC,yCAAgC;QAEhC,+CAA+C;QAExD,uEAAuE;QACvE,qDAA4C;QAE5C,sEAAsE;QACtE,qDAA6B,CAAC,EAAC;QAE/B;;;;WAIG;QACH,mEAA4D;QAG1D,uBAAA,IAAI,0BAAU,MAAM,EAAE,KAAK,IAAI,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,MAAA,CAAC;QACjE,uBAAA,IAAI,gCAAgB,MAAM,EAAE,WAAW,MAAA,CAAC;IAC1C,CAAC;IAED;;;;;;;;;;OAUG;IACH,KAAK,CAAC,cAAc,CAAC,UAAmB;QACtC,wEAAwE;QACxE,wEAAwE;QACxE,IAAI,CAAC,CAAC,MAAM,uBAAA,IAAI,qEAAkB,MAAtB,IAAI,EAAmB,UAAU,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YACnE,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,MAAM,WAAW,GAAG,uBAAA,IAAI,oCAAa,CAAC;QACtC,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;YAC9B,OAAO,uBAAA,IAAI,2EAAwB,MAA5B,IAAI,EAAyB,UAAU,CAAC,CAAC;QAClD,CAAC;QAED,OAAO,WAAW,CAAC,UAAU,CAAC;YAC5B,oEAAoE;YACpE,sEAAsE;YACtE,QAAQ,EAAE;gBACR,mBAAmB;gBACnB,eAAe;gBACf,YAAY;gBACZ,EAAE,OAAO,EAAE,UAAU,EAAE;aACxB;YACD,OAAO,EAAE,GAAG,EAAE,CAAC,uBAAA,IAAI,2EAAwB,MAA5B,IAAI,EAAyB,UAAU,CAAC;YACvD,SAAS,EAAE,wBAAwB;YACnC,MAAM,EAAE,qBAAqB;SAC9B,CAAC,CAAC;IACL,CAAC;CA+GF;AA1KD,0CA0KC;;AA7GC;;;;;;;;GAQG;AACH,KAAK,4CAAmB,UAAmB;IACzC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,IACE,uBAAA,IAAI,0CAAmB,KAAK,SAAS;QACrC,GAAG,GAAG,uBAAA,IAAI,kDAA2B,IAAI,+BAA+B,EACxE,CAAC;QACD,MAAM,uBAAA,IAAI,6EAA0B,MAA9B,IAAI,EAA2B,GAAG,CAAC,CAAC;IAC5C,CAAC;IAED,MAAM,WAAW,GAAG,UAAU,IAAA,sCAAmB,EAAC,UAAU,CAAC,EAAE,CAAC;IAChE,OAAO,uBAAA,IAAI,0CAAmB,EAAE,GAAG,CAAC,WAAW,CAAC,IAAI,KAAK,CAAC;AAC5D,CAAC;AAED;;;;;;;;GAQG;AACH,KAAK,oDAA2B,GAAW;IACzC,IAAI,uBAAA,IAAI,wDAAiC,KAAK,SAAS,EAAE,CAAC;QACxD,OAAO,uBAAA,IAAI,wDAAiC,CAAC;IAC/C,CAAC;IAED,uBAAA,IAAI,oDAAoC,CAAC,KAAK,IAAmB,EAAE;QACjE,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,uBAAA,IAAI,8BAAO,MAAX,IAAI,EAAQ,sBAAsB,CAAC,CAAC;YAC3D,IAAI,QAAQ,CAAC,EAAE,EAAE,CAAC;gBAChB,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA8B,CAAC;gBAClE,uBAAA,IAAI,sCAAsB,IAAI,GAAG,CAAC;oBAChC,GAAG,CAAC,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC;oBAC3B,GAAG,CAAC,IAAI,CAAC,cAAc,IAAI,EAAE,CAAC;iBAC/B,CAAC,MAAA,CAAC;gBACH,uBAAA,IAAI,8CAA8B,GAAG,MAAA,CAAC;YACxC,CAAC;QACH,CAAC;gBAAS,CAAC;YACT,uBAAA,IAAI,oDAAoC,SAAS,MAAA,CAAC;QACpD,CAAC;IACH,CAAC,CAAC,EAAE,MAAA,CAAC;IAEL,OAAO,uBAAA,IAAI,wDAAiC,CAAC;AAC/C,CAAC,4CAED,KAAK,kDACH,UAAmB;IAEnB,MAAM,cAAc,GAAG,IAAA,sCAAmB,EAAC,UAAU,CAAC,CAAC;IACvD,MAAM,eAAe,GAAG,kBAAkB,CAAC,UAAU,CAAC,CAAC;IAEvD,yEAAyE;IACzE,uEAAuE;IACvE,oCAAoC;IACpC,MAAM,GAAG,GACP,GAAG,mBAAmB,WAAW,cAAc,EAAE;QACjD,oBAAoB,eAAe,EAAE;QACrC,4BAA4B;QAC5B,yBAAyB;QACzB,yBAAyB;QACzB,2BAA2B;QAC3B,uBAAuB;QACvB,sBAAsB,CAAC;IAEzB,IAAI,QAAkB,CAAC;IACvB,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,uBAAA,IAAI,8BAAO,MAAX,IAAI,EAAQ,GAAG,CAAC,CAAC;IACpC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CACX,uCAAuC,UAAU,GAAG,EACpD,KAAK,CACN,CAAC;QACF,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,OAAO,CAAC,KAAK,CACX,6BAA6B,QAAQ,CAAC,MAAM,cAAc,UAAU,EAAE,CACvE,CAAC;QACF,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,GAAG,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAY,CAAC;IAC/C,MAAM,KAAK,GAAuB,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;QAClD,CAAC,CAAE,GAA0B;QAC7B,CAAC,CAAC,EAAE,CAAC;IAEP,MAAM,QAAQ,GAAG,yBAAyB,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;IAE9D,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAC7B,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,EAAE;QACzB,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,EAAE;QACrB,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,EAAE;QAC7B,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,WAAW,EAAE,IAAI,CAAC,WAAW;KAC9B,CAAC,CAAC,CAAC;AACN,CAAC;AAGH;;;;;;;;;;GAUG;AACH,SAAS,yBAAyB,CAChC,UAAmB,EACnB,KAAyB;IAEzB,IAAI,UAAU,KAAK,0BAAiB,CAAC,eAAe,CAAC,EAAE,CAAC;QACtD,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE;YAC3B,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC;YAC3C,OAAO,WAAW,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,WAAW,CAAC,MAAM,IAAI,CAAC,CAAC;QACtE,CAAC,CAAC,CAAC;IACL,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["import {\n ChainId as ControllerChainId,\n convertHexToDecimal,\n} from '@metamask/controller-utils';\n\nimport type { ChainId, TokenListEntry } from '../types';\n\n/**\n * Same host + path that `TokenListController` (assets-controllers) uses\n * (`token-service.ts` → `getTokensURL`). Sharing this endpoint keeps the\n * RPC token detector aligned with the rest of the wallet's token listing\n * (occurrence floors, aggregator filters, icon URLs, etc.).\n */\nconst TOKEN_END_POINT_API = 'https://token.api.cx.metamask.io';\n\n/**\n * Endpoint that returns which CAIP chain IDs the Tokens API supports.\n * Used to skip detection (and avoid unnecessary network requests) for chains\n * that are not in the supported-networks list.\n */\nconst SUPPORTED_NETWORKS_URL = `${TOKEN_END_POINT_API}/v2/supportedNetworks`;\n\n/**\n * How long to keep the supported-networks response in the instance-level\n * cache before refreshing it. One hour is sufficient — the list rarely\n * changes and a stale cache just means we may do an unnecessary token-list\n * fetch for a newly-unsupported chain (harmless).\n */\nconst SUPPORTED_NETWORKS_CACHE_TTL_MS = 60 * 60_000;\n\n/**\n * Tempo Mainnet — not yet present in `@metamask/controller-utils`'s `ChainId`\n * map at the time of writing, so it's spelled out as a literal here exactly as\n * `TokenListController` does (see `token-service.ts:getTokensURL`).\n */\nconst TEMPO_MAINNET_CHAIN_ID = '0x1079' as const;\n\n/**\n * Per-chain occurrence floor, mirroring `TokenListController.getTokensURL`:\n * Linea mainnet, MegaETH mainnet, and Tempo mainnet have thinner aggregator\n * coverage so we lower the floor; everything else uses the default 3.\n *\n * @param hexChainId - Hex chain ID.\n * @returns The occurrence floor to send to the Tokens API.\n */\nfunction getOccurrenceFloor(hexChainId: ChainId): number {\n if (\n hexChainId === ControllerChainId['linea-mainnet'] ||\n hexChainId === ControllerChainId['megaeth-mainnet'] ||\n hexChainId === TEMPO_MAINNET_CHAIN_ID\n ) {\n return 1;\n }\n return 3;\n}\n\n/**\n * TanStack-Query cache config for the cached `fetchTokenList` path.\n *\n * The Tokens API per-chain list barely changes between polls, so we keep\n * results fresh for a few minutes (`staleTime`) and retain them in cache for\n * an hour (`gcTime`) so re-detections across accounts/chains hit the cache.\n * These tunings only apply when a `queryClient` is provided to the client;\n * the uncached fallback path (used in standalone tests) is unaffected.\n */\nconst TOKEN_LIST_STALE_TIME_MS = 5 * 60_000;\nconst TOKEN_LIST_GC_TIME_MS = 60 * 60_000;\n\n/**\n * Shape of the `/v2/supportedNetworks` response.\n */\ntype SupportedNetworksResponse = {\n fullSupport?: string[];\n partialSupport?: string[];\n};\n\n/**\n * Shape of a single item returned by the Tokens API `/tokens/{chainId}`\n * endpoint. Mirrors `TokenListToken` in\n * `packages/assets-controllers/src/TokenListController.ts` and the response\n * parsed by `fetchTokenListByChainId` in `token-service.ts`.\n */\ntype ApiTokenListItem = {\n address: string;\n symbol?: string;\n name?: string;\n decimals?: number;\n occurrences?: number;\n aggregators?: string[];\n iconUrl?: string;\n};\n\n/**\n * Minimal structural type for the TanStack Query client method we use.\n * Avoids a direct dependency on `@tanstack/query-core` while still being\n * fully compatible with the shared `QueryClient` exposed by\n * `ApiPlatformClient.queryClient` (`@metamask/core-backend`).\n */\nexport type TokenListQueryClient = {\n fetchQuery<TData>(options: {\n queryKey: readonly unknown[];\n queryFn: () => Promise<TData>;\n staleTime?: number;\n gcTime?: number;\n }): Promise<TData>;\n};\n\nexport type TokensApiClientConfig = {\n /** Fetch function (defaults to globalThis.fetch). */\n fetch?: typeof globalThis.fetch;\n /**\n * Optional TanStack-Query client used to cache token-list responses across\n * detector polls / accounts / instances. When omitted, every call hits the\n * network directly (preserves prior behaviour for tests and standalone use).\n */\n queryClient?: TokenListQueryClient;\n};\n\n/**\n * Client for the MetaMask Tokens API.\n *\n * Fetches the per-chain ERC-20 token list from the same endpoint that\n * `TokenListController` uses (`token.api.cx.metamask.io/tokens/{chainId}`),\n * with the same query parameters and the same per-chain occurrence floor /\n * Linea aggregator filter. This keeps RPC token detection in lockstep with\n * the wallet's primary token list.\n *\n * Before fetching a chain's token list, the client checks\n * `/v2/supportedNetworks` and returns `[]` immediately for chains that are\n * not in the supported list, avoiding unnecessary API calls. The supported-\n * networks response is cached in-memory for one hour.\n *\n * When constructed with a `queryClient`, token-list results are cached and\n * deduped via TanStack Query (5 min staleTime, 1 h gcTime), so concurrent\n * detection cycles for the same chain coalesce into a single network request.\n */\nexport class TokensApiClient {\n readonly #fetch: typeof globalThis.fetch;\n\n readonly #queryClient: TokenListQueryClient | undefined;\n\n /** CAIP chain IDs returned by `/v2/supportedNetworks` (both tiers). */\n #supportedChainIds: Set<string> | undefined;\n\n /** Timestamp of the last successful `/v2/supportedNetworks` fetch. */\n #supportedChainIdsCachedAt = 0;\n\n /**\n * In-flight `/v2/supportedNetworks` request shared across concurrent callers\n * so only one network request is made even when multiple `fetchTokenList`\n * calls arrive before the first one resolves.\n */\n #supportedChainIdsRefreshPromise: Promise<void> | undefined;\n\n constructor(config?: TokensApiClientConfig) {\n this.#fetch = config?.fetch ?? globalThis.fetch.bind(globalThis);\n this.#queryClient = config?.queryClient;\n }\n\n /**\n * Fetch the list of ERC-20 tokens for a chain from the Tokens API.\n *\n * Returns `[]` immediately (without hitting the token-list endpoint) when\n * the chain is not in the `/v2/supportedNetworks` response, or when the\n * supported-networks check fails for any reason.\n *\n * @param hexChainId - Chain ID in hex format (e.g. `'0x1'` for Ethereum mainnet).\n * @returns Array of token list entries with address and metadata, or an empty\n * array if the chain is unsupported or the request fails.\n */\n async fetchTokenList(hexChainId: ChainId): Promise<TokenListEntry[]> {\n // Treat any error from the supported-networks check as \"not supported\":\n // the token-list endpoint is only contacted for known-supported chains.\n if (!(await this.#isSupportedChain(hexChainId).catch(() => false))) {\n return [];\n }\n\n const queryClient = this.#queryClient;\n if (queryClient === undefined) {\n return this.#fetchTokenListUncached(hexChainId);\n }\n\n return queryClient.fetchQuery({\n // Namespacing keeps this key from colliding with other clients that\n // share the same QueryClient (e.g. core-backend's ApiPlatformClient).\n queryKey: [\n 'assets-controller',\n 'rpc-detection',\n 'token-list',\n { chainId: hexChainId },\n ],\n queryFn: () => this.#fetchTokenListUncached(hexChainId),\n staleTime: TOKEN_LIST_STALE_TIME_MS,\n gcTime: TOKEN_LIST_GC_TIME_MS,\n });\n }\n\n /**\n * Check whether the given chain is present in the supported-networks list.\n *\n * Uses an instance-level cache (1 h TTL). Throws if the network request\n * fails — callers should handle this (e.g. via `.catch(() => false)`).\n *\n * @param hexChainId - Hex chain ID to check.\n * @returns `true` when the chain is in the supported-networks list.\n */\n async #isSupportedChain(hexChainId: ChainId): Promise<boolean> {\n const now = Date.now();\n if (\n this.#supportedChainIds === undefined ||\n now - this.#supportedChainIdsCachedAt >= SUPPORTED_NETWORKS_CACHE_TTL_MS\n ) {\n await this.#refreshSupportedChainIds(now);\n }\n\n const caipChainId = `eip155:${convertHexToDecimal(hexChainId)}`;\n return this.#supportedChainIds?.has(caipChainId) ?? false;\n }\n\n /**\n * Fetch `/v2/supportedNetworks` and update the instance cache.\n * Concurrent callers share the same in-flight Promise so only one network\n * request is made. If the response is not 2xx the cache is left unchanged\n * (the chain will be treated as unsupported this cycle).\n *\n * @param now - Current timestamp used to stamp the cache entry.\n * @returns A promise that resolves once the cache has been refreshed.\n */\n async #refreshSupportedChainIds(now: number): Promise<void> {\n if (this.#supportedChainIdsRefreshPromise !== undefined) {\n return this.#supportedChainIdsRefreshPromise;\n }\n\n this.#supportedChainIdsRefreshPromise = (async (): Promise<void> => {\n try {\n const response = await this.#fetch(SUPPORTED_NETWORKS_URL);\n if (response.ok) {\n const data = (await response.json()) as SupportedNetworksResponse;\n this.#supportedChainIds = new Set([\n ...(data.fullSupport ?? []),\n ...(data.partialSupport ?? []),\n ]);\n this.#supportedChainIdsCachedAt = now;\n }\n } finally {\n this.#supportedChainIdsRefreshPromise = undefined;\n }\n })();\n\n return this.#supportedChainIdsRefreshPromise;\n }\n\n async #fetchTokenListUncached(\n hexChainId: ChainId,\n ): Promise<TokenListEntry[]> {\n const decimalChainId = convertHexToDecimal(hexChainId);\n const occurrenceFloor = getOccurrenceFloor(hexChainId);\n\n // Mirrors `TokenListController.getTokensURL` exactly (token-service.ts).\n // No `first=...` cap — the API returns the full per-chain list bounded\n // server-side by `occurrenceFloor`.\n const url =\n `${TOKEN_END_POINT_API}/tokens/${decimalChainId}` +\n `?occurrenceFloor=${occurrenceFloor}` +\n `&includeNativeAssets=false` +\n `&includeTokenFees=false` +\n `&includeAssetType=false` +\n `&includeERC20Permit=false` +\n `&includeStorage=false` +\n `&includeRwaData=true`;\n\n let response: Response;\n try {\n response = await this.#fetch(url);\n } catch (error) {\n console.error(\n `Tokens API request failed for chain ${hexChainId}:`,\n error,\n );\n return [];\n }\n\n if (!response.ok) {\n console.error(\n `Tokens API responded with ${response.status} for chain ${hexChainId}`,\n );\n return [];\n }\n\n const raw = (await response.json()) as unknown;\n const items: ApiTokenListItem[] = Array.isArray(raw)\n ? (raw as ApiTokenListItem[])\n : [];\n\n const filtered = applyChainSpecificFilters(hexChainId, items);\n\n return filtered.map((item) => ({\n address: item.address,\n symbol: item.symbol ?? '',\n name: item.name ?? '',\n decimals: item.decimals ?? 18,\n iconUrl: item.iconUrl,\n aggregators: item.aggregators,\n occurrences: item.occurrences,\n }));\n }\n}\n\n/**\n * Apply chain-specific filters to a raw token list response, mirroring\n * `fetchTokenListByChainId` in `assets-controllers/src/token-service.ts`.\n *\n * For Linea mainnet, the API returns extras with low aggregator coverage, so\n * we keep only entries flagged by Linea's own team or seen by ≥3 aggregators.\n *\n * @param hexChainId - Hex chain ID.\n * @param items - Raw items from the API response.\n * @returns Items after chain-specific filtering.\n */\nfunction applyChainSpecificFilters(\n hexChainId: ChainId,\n items: ApiTokenListItem[],\n): ApiTokenListItem[] {\n if (hexChainId === ControllerChainId['linea-mainnet']) {\n return items.filter((item) => {\n const aggregators = item.aggregators ?? [];\n return aggregators.includes('lineaTeam') || aggregators.length >= 3;\n });\n }\n return items;\n}\n"]}
1
+ {"version":3,"file":"TokensApiClient.cjs","sourceRoot":"","sources":["../../../../src/data-sources/evm-rpc-services/clients/TokensApiClient.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,iEAGoC;AAIpC;;;;;GAKG;AACH,MAAM,mBAAmB,GAAG,kCAAkC,CAAC;AAE/D;;;;GAIG;AACH,MAAM,sBAAsB,GAAG,GAAG,mBAAmB,uBAAuB,CAAC;AAE7E;;;;GAIG;AACH,MAAM,+BAA+B,GAAG,GAAG,mBAAmB,+BAA+B,CAAC;AAE9F;;;GAGG;AACH,MAAM,wBAAwB,GAAG,CAAC,CAAC;AAEnC;;;;;GAKG;AACH,MAAM,+BAA+B,GAAG,EAAE,GAAG,KAAM,CAAC;AAEpD;;;GAGG;AACH,MAAM,wCAAwC,GAAG,EAAE,GAAG,KAAM,CAAC;AAE7D;;;;;;;;GAQG;AACH,MAAM,wBAAwB,GAAG,CAAC,GAAG,KAAM,CAAC;AAC5C,MAAM,qBAAqB,GAAG,EAAE,GAAG,KAAM,CAAC;AAoD1C;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAa,eAAe;IA2B1B,YAAY,MAA8B;;QA1BjC,yCAAgC;QAEhC,+CAA+C;QAExD,uEAAuE;QACvE,qDAA4C;QAE5C,sEAAsE;QACtE,qDAA6B,CAAC,EAAC;QAE/B;;;;WAIG;QACH,mEAA4D;QAE5D,oEAAoE;QACpE,6DAA+D;QAE/D,qDAAqD;QACrD,6DAAqC,CAAC,EAAC;QAEvC,iEAAiE;QACjE,2EAAoE;QAGlE,uBAAA,IAAI,0BAAU,MAAM,EAAE,KAAK,IAAI,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,MAAA,CAAC;QACjE,uBAAA,IAAI,gCAAgB,MAAM,EAAE,WAAW,MAAA,CAAC;IAC1C,CAAC;IAED;;;;;;;;;;OAUG;IACH,KAAK,CAAC,cAAc,CAAC,UAAmB;QACtC,wEAAwE;QACxE,wEAAwE;QACxE,IAAI,CAAC,CAAC,MAAM,uBAAA,IAAI,qEAAkB,MAAtB,IAAI,EAAmB,UAAU,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YACnE,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,MAAM,WAAW,GAAG,uBAAA,IAAI,oCAAa,CAAC;QACtC,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;YAC9B,OAAO,uBAAA,IAAI,2EAAwB,MAA5B,IAAI,EAAyB,UAAU,CAAC,CAAC;QAClD,CAAC;QAED,OAAO,WAAW,CAAC,UAAU,CAAC;YAC5B,oEAAoE;YACpE,sEAAsE;YACtE,QAAQ,EAAE;gBACR,mBAAmB;gBACnB,eAAe;gBACf,YAAY;gBACZ,EAAE,OAAO,EAAE,UAAU,EAAE;aACxB;YACD,OAAO,EAAE,GAAG,EAAE,CAAC,uBAAA,IAAI,2EAAwB,MAA5B,IAAI,EAAyB,UAAU,CAAC;YACvD,SAAS,EAAE,wBAAwB;YACnC,MAAM,EAAE,qBAAqB;SAC9B,CAAC,CAAC;IACL,CAAC;CAiLF;AArPD,0CAqPC;;AA/KC;;;;;;;;GAQG;AACH,KAAK,4CAAmB,UAAmB;IACzC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,IACE,uBAAA,IAAI,0CAAmB,KAAK,SAAS;QACrC,GAAG,GAAG,uBAAA,IAAI,kDAA2B,IAAI,+BAA+B,EACxE,CAAC;QACD,MAAM,uBAAA,IAAI,6EAA0B,MAA9B,IAAI,EAA2B,GAAG,CAAC,CAAC;IAC5C,CAAC;IAED,MAAM,WAAW,GAAG,UAAU,IAAA,sCAAmB,EAAC,UAAU,CAAC,EAAE,CAAC;IAChE,OAAO,uBAAA,IAAI,0CAAmB,EAAE,GAAG,CAAC,WAAW,CAAC,IAAI,KAAK,CAAC;AAC5D,CAAC;AAED;;;;;;;;GAQG;AACH,KAAK,oDAA2B,GAAW;IACzC,IAAI,uBAAA,IAAI,wDAAiC,KAAK,SAAS,EAAE,CAAC;QACxD,OAAO,uBAAA,IAAI,wDAAiC,CAAC;IAC/C,CAAC;IAED,uBAAA,IAAI,oDAAoC,CAAC,KAAK,IAAmB,EAAE;QACjE,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,uBAAA,IAAI,8BAAO,MAAX,IAAI,EAAQ,sBAAsB,CAAC,CAAC;YAC3D,IAAI,QAAQ,CAAC,EAAE,EAAE,CAAC;gBAChB,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA8B,CAAC;gBAClE,uBAAA,IAAI,sCAAsB,IAAI,GAAG,CAAC;oBAChC,GAAG,CAAC,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC;oBAC3B,GAAG,CAAC,IAAI,CAAC,cAAc,IAAI,EAAE,CAAC;iBAC/B,CAAC,MAAA,CAAC;gBACH,uBAAA,IAAI,8CAA8B,GAAG,MAAA,CAAC;YACxC,CAAC;QACH,CAAC;gBAAS,CAAC;YACT,uBAAA,IAAI,oDAAoC,SAAS,MAAA,CAAC;QACpD,CAAC;IACH,CAAC,CAAC,EAAE,MAAA,CAAC;IAEL,OAAO,uBAAA,IAAI,wDAAiC,CAAC;AAC/C,CAAC;AAED;;;;;;;;GAQG;AACH,KAAK,8CAAqB,UAAmB;IAC3C,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,IACE,uBAAA,IAAI,kDAA2B,KAAK,SAAS;QAC7C,GAAG,GAAG,uBAAA,IAAI,0DAAmC;YAC3C,wCAAwC,EAC1C,CAAC;QACD,MAAM,uBAAA,IAAI,qFAAkC,MAAtC,IAAI,EAAmC,GAAG,CAAC,CAAC;IACpD,CAAC;IAED,MAAM,cAAc,GAAG,MAAM,CAAC,IAAA,sCAAmB,EAAC,UAAU,CAAC,CAAC,CAAC;IAC/D,OAAO,CACL,uBAAA,IAAI,kDAA2B,EAAE,CAAC,cAAc,CAAC;QACjD,wBAAwB,CACzB,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,KAAK,4DAAmC,GAAW;IACjD,IAAI,uBAAA,IAAI,gEAAyC,KAAK,SAAS,EAAE,CAAC;QAChE,OAAO,uBAAA,IAAI,gEAAyC,CAAC;IACvD,CAAC;IAED,uBAAA,IAAI,4DACF,CAAC,KAAK,IAAmB,EAAE;QACzB,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,uBAAA,IAAI,8BAAO,MAAX,IAAI,EAAQ,+BAA+B,CAAC,CAAC;YACpE,IAAI,QAAQ,CAAC,EAAE,EAAE,CAAC;gBAChB,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA2B,CAAC;gBAC/D,uBAAA,IAAI,8CACF,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;oBACtD,CAAC,CAAC,IAAI;oBACN,CAAC,CAAC,EAAE,MAAA,CAAC;gBACT,uBAAA,IAAI,sDAAsC,GAAG,MAAA,CAAC;YAChD,CAAC;iBAAM,CAAC;gBACN,uBAAA,IAAI,8CAA8B,EAAE,MAAA,CAAC;gBACrC,uBAAA,IAAI,sDAAsC,GAAG,MAAA,CAAC;YAChD,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,uBAAA,IAAI,8CAA8B,EAAE,MAAA,CAAC;YACrC,uBAAA,IAAI,sDAAsC,GAAG,MAAA,CAAC;QAChD,CAAC;gBAAS,CAAC;YACT,uBAAA,IAAI,4DAA4C,SAAS,MAAA,CAAC;QAC5D,CAAC;IACH,CAAC,CAAC,EAAE,MAAA,CAAC;IAEP,OAAO,uBAAA,IAAI,gEAAyC,CAAC;AACvD,CAAC,4CAED,KAAK,kDACH,UAAmB;IAEnB,MAAM,cAAc,GAAG,IAAA,sCAAmB,EAAC,UAAU,CAAC,CAAC;IACvD,MAAM,eAAe,GAAG,MAAM,uBAAA,IAAI,uEAAoB,MAAxB,IAAI,EAAqB,UAAU,CAAC,CAAC;IAEnE,6EAA6E;IAC7E,oEAAoE;IACpE,uEAAuE;IACvE,oCAAoC;IACpC,MAAM,GAAG,GACP,GAAG,mBAAmB,WAAW,cAAc,EAAE;QACjD,oBAAoB,eAAe,EAAE;QACrC,4BAA4B;QAC5B,yBAAyB;QACzB,yBAAyB;QACzB,2BAA2B;QAC3B,uBAAuB;QACvB,sBAAsB,CAAC;IAEzB,IAAI,QAAkB,CAAC;IACvB,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,uBAAA,IAAI,8BAAO,MAAX,IAAI,EAAQ,GAAG,CAAC,CAAC;IACpC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CACX,uCAAuC,UAAU,GAAG,EACpD,KAAK,CACN,CAAC;QACF,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,OAAO,CAAC,KAAK,CACX,6BAA6B,QAAQ,CAAC,MAAM,cAAc,UAAU,EAAE,CACvE,CAAC;QACF,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,GAAG,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAY,CAAC;IAC/C,MAAM,KAAK,GAAuB,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;QAClD,CAAC,CAAE,GAA0B;QAC7B,CAAC,CAAC,EAAE,CAAC;IAEP,MAAM,QAAQ,GAAG,yBAAyB,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;IAE9D,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAC7B,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,EAAE;QACzB,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,EAAE;QACrB,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,EAAE;QAC7B,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,WAAW,EAAE,IAAI,CAAC,WAAW;KAC9B,CAAC,CAAC,CAAC;AACN,CAAC;AAGH;;;;;;;;;;GAUG;AACH,SAAS,yBAAyB,CAChC,UAAmB,EACnB,KAAyB;IAEzB,IAAI,UAAU,KAAK,0BAAiB,CAAC,eAAe,CAAC,EAAE,CAAC;QACtD,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE;YAC3B,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC;YAC3C,OAAO,WAAW,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,WAAW,CAAC,MAAM,IAAI,CAAC,CAAC;QACtE,CAAC,CAAC,CAAC;IACL,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["import {\n ChainId as ControllerChainId,\n convertHexToDecimal,\n} from '@metamask/controller-utils';\n\nimport type { ChainId, TokenListEntry } from '../types';\n\n/**\n * Same host + path that `TokenListController` (assets-controllers) uses\n * (`token-service.ts` → `getTokensURL`). Sharing this endpoint keeps the\n * RPC token detector aligned with the rest of the wallet's token listing\n * (occurrence floors, aggregator filters, icon URLs, etc.).\n */\nconst TOKEN_END_POINT_API = 'https://token.api.cx.metamask.io';\n\n/**\n * Endpoint that returns which CAIP chain IDs the Tokens API supports.\n * Used to skip detection (and avoid unnecessary network requests) for chains\n * that are not in the supported-networks list.\n */\nconst SUPPORTED_NETWORKS_URL = `${TOKEN_END_POINT_API}/v2/supportedNetworks`;\n\n/**\n * Per-chain suggested occurrence floors used as the `occurrenceFloor` query\n * param when fetching token lists (and aligned with TokenDataSource spam\n * filtering). Keys are decimal chain IDs.\n */\nconst SUGGESTED_OCCURRENCE_FLOORS_URL = `${TOKEN_END_POINT_API}/v1/suggestedOccurrenceFloors`;\n\n/**\n * Fallback `occurrenceFloor` when `/v1/suggestedOccurrenceFloors` has no entry\n * for the chain, or the floors request fails.\n */\nconst DEFAULT_OCCURRENCE_FLOOR = 3;\n\n/**\n * How long to keep the supported-networks response in the instance-level\n * cache before refreshing it. One hour is sufficient — the list rarely\n * changes and a stale cache just means we may do an unnecessary token-list\n * fetch for a newly-unsupported chain (harmless).\n */\nconst SUPPORTED_NETWORKS_CACHE_TTL_MS = 60 * 60_000;\n\n/**\n * How long to keep suggested occurrence floors cached. Same TTL as supported\n * networks — floors change infrequently.\n */\nconst SUGGESTED_OCCURRENCE_FLOORS_CACHE_TTL_MS = 60 * 60_000;\n\n/**\n * TanStack-Query cache config for the cached `fetchTokenList` path.\n *\n * The Tokens API per-chain list barely changes between polls, so we keep\n * results fresh for a few minutes (`staleTime`) and retain them in cache for\n * an hour (`gcTime`) so re-detections across accounts/chains hit the cache.\n * These tunings only apply when a `queryClient` is provided to the client;\n * the uncached fallback path (used in standalone tests) is unaffected.\n */\nconst TOKEN_LIST_STALE_TIME_MS = 5 * 60_000;\nconst TOKEN_LIST_GC_TIME_MS = 60 * 60_000;\n\n/**\n * Shape of the `/v2/supportedNetworks` response.\n */\ntype SupportedNetworksResponse = {\n fullSupport?: string[];\n partialSupport?: string[];\n};\n\n/**\n * Shape of a single item returned by the Tokens API `/tokens/{chainId}`\n * endpoint. Mirrors `TokenListToken` in\n * `packages/assets-controllers/src/TokenListController.ts` and the response\n * parsed by `fetchTokenListByChainId` in `token-service.ts`.\n */\ntype ApiTokenListItem = {\n address: string;\n symbol?: string;\n name?: string;\n decimals?: number;\n occurrences?: number;\n aggregators?: string[];\n iconUrl?: string;\n};\n\n/**\n * Minimal structural type for the TanStack Query client method we use.\n * Avoids a direct dependency on `@tanstack/query-core` while still being\n * fully compatible with the shared `QueryClient` exposed by\n * `ApiPlatformClient.queryClient` (`@metamask/core-backend`).\n */\nexport type TokenListQueryClient = {\n fetchQuery<TData>(options: {\n queryKey: readonly unknown[];\n queryFn: () => Promise<TData>;\n staleTime?: number;\n gcTime?: number;\n }): Promise<TData>;\n};\n\nexport type TokensApiClientConfig = {\n /** Fetch function (defaults to globalThis.fetch). */\n fetch?: typeof globalThis.fetch;\n /**\n * Optional TanStack-Query client used to cache token-list responses across\n * detector polls / accounts / instances. When omitted, every call hits the\n * network directly (preserves prior behaviour for tests and standalone use).\n */\n queryClient?: TokenListQueryClient;\n};\n\n/**\n * Client for the MetaMask Tokens API.\n *\n * Fetches the per-chain ERC-20 token list from the same endpoint that\n * `TokenListController` uses (`token.api.cx.metamask.io/tokens/{chainId}`),\n * with the same query parameters. The `occurrenceFloor` query param comes from\n * Token API `/v1/suggestedOccurrenceFloors` (fallback {@link DEFAULT_OCCURRENCE_FLOOR}),\n * matching `TokenDataSource` spam filtering. Linea's post-response aggregator\n * filter is still applied client-side.\n *\n * Before fetching a chain's token list, the client checks\n * `/v2/supportedNetworks` and returns `[]` immediately for chains that are\n * not in the supported list, avoiding unnecessary API calls. The supported-\n * networks response is cached in-memory for one hour.\n *\n * When constructed with a `queryClient`, token-list results are cached and\n * deduped via TanStack Query (5 min staleTime, 1 h gcTime), so concurrent\n * detection cycles for the same chain coalesce into a single network request.\n */\nexport class TokensApiClient {\n readonly #fetch: typeof globalThis.fetch;\n\n readonly #queryClient: TokenListQueryClient | undefined;\n\n /** CAIP chain IDs returned by `/v2/supportedNetworks` (both tiers). */\n #supportedChainIds: Set<string> | undefined;\n\n /** Timestamp of the last successful `/v2/supportedNetworks` fetch. */\n #supportedChainIdsCachedAt = 0;\n\n /**\n * In-flight `/v2/supportedNetworks` request shared across concurrent callers\n * so only one network request is made even when multiple `fetchTokenList`\n * calls arrive before the first one resolves.\n */\n #supportedChainIdsRefreshPromise: Promise<void> | undefined;\n\n /** Decimal chain ID → suggested occurrence floor from Token API. */\n #suggestedOccurrenceFloors: Record<string, number> | undefined;\n\n /** Timestamp of the last successful floors fetch. */\n #suggestedOccurrenceFloorsCachedAt = 0;\n\n /** In-flight floors request shared across concurrent callers. */\n #suggestedOccurrenceFloorsRefreshPromise: Promise<void> | undefined;\n\n constructor(config?: TokensApiClientConfig) {\n this.#fetch = config?.fetch ?? globalThis.fetch.bind(globalThis);\n this.#queryClient = config?.queryClient;\n }\n\n /**\n * Fetch the list of ERC-20 tokens for a chain from the Tokens API.\n *\n * Returns `[]` immediately (without hitting the token-list endpoint) when\n * the chain is not in the `/v2/supportedNetworks` response, or when the\n * supported-networks check fails for any reason.\n *\n * @param hexChainId - Chain ID in hex format (e.g. `'0x1'` for Ethereum mainnet).\n * @returns Array of token list entries with address and metadata, or an empty\n * array if the chain is unsupported or the request fails.\n */\n async fetchTokenList(hexChainId: ChainId): Promise<TokenListEntry[]> {\n // Treat any error from the supported-networks check as \"not supported\":\n // the token-list endpoint is only contacted for known-supported chains.\n if (!(await this.#isSupportedChain(hexChainId).catch(() => false))) {\n return [];\n }\n\n const queryClient = this.#queryClient;\n if (queryClient === undefined) {\n return this.#fetchTokenListUncached(hexChainId);\n }\n\n return queryClient.fetchQuery({\n // Namespacing keeps this key from colliding with other clients that\n // share the same QueryClient (e.g. core-backend's ApiPlatformClient).\n queryKey: [\n 'assets-controller',\n 'rpc-detection',\n 'token-list',\n { chainId: hexChainId },\n ],\n queryFn: () => this.#fetchTokenListUncached(hexChainId),\n staleTime: TOKEN_LIST_STALE_TIME_MS,\n gcTime: TOKEN_LIST_GC_TIME_MS,\n });\n }\n\n /**\n * Check whether the given chain is present in the supported-networks list.\n *\n * Uses an instance-level cache (1 h TTL). Throws if the network request\n * fails — callers should handle this (e.g. via `.catch(() => false)`).\n *\n * @param hexChainId - Hex chain ID to check.\n * @returns `true` when the chain is in the supported-networks list.\n */\n async #isSupportedChain(hexChainId: ChainId): Promise<boolean> {\n const now = Date.now();\n if (\n this.#supportedChainIds === undefined ||\n now - this.#supportedChainIdsCachedAt >= SUPPORTED_NETWORKS_CACHE_TTL_MS\n ) {\n await this.#refreshSupportedChainIds(now);\n }\n\n const caipChainId = `eip155:${convertHexToDecimal(hexChainId)}`;\n return this.#supportedChainIds?.has(caipChainId) ?? false;\n }\n\n /**\n * Fetch `/v2/supportedNetworks` and update the instance cache.\n * Concurrent callers share the same in-flight Promise so only one network\n * request is made. If the response is not 2xx the cache is left unchanged\n * (the chain will be treated as unsupported this cycle).\n *\n * @param now - Current timestamp used to stamp the cache entry.\n * @returns A promise that resolves once the cache has been refreshed.\n */\n async #refreshSupportedChainIds(now: number): Promise<void> {\n if (this.#supportedChainIdsRefreshPromise !== undefined) {\n return this.#supportedChainIdsRefreshPromise;\n }\n\n this.#supportedChainIdsRefreshPromise = (async (): Promise<void> => {\n try {\n const response = await this.#fetch(SUPPORTED_NETWORKS_URL);\n if (response.ok) {\n const data = (await response.json()) as SupportedNetworksResponse;\n this.#supportedChainIds = new Set([\n ...(data.fullSupport ?? []),\n ...(data.partialSupport ?? []),\n ]);\n this.#supportedChainIdsCachedAt = now;\n }\n } finally {\n this.#supportedChainIdsRefreshPromise = undefined;\n }\n })();\n\n return this.#supportedChainIdsRefreshPromise;\n }\n\n /**\n * Resolve the `occurrenceFloor` query param for a chain from Token API\n * `/v1/suggestedOccurrenceFloors`. Falls back to\n * {@link DEFAULT_OCCURRENCE_FLOOR} when the chain is missing or the request\n * fails.\n *\n * @param hexChainId - Hex chain ID.\n * @returns Occurrence floor to send to `/tokens/{chainId}`.\n */\n async #getOccurrenceFloor(hexChainId: ChainId): Promise<number> {\n const now = Date.now();\n if (\n this.#suggestedOccurrenceFloors === undefined ||\n now - this.#suggestedOccurrenceFloorsCachedAt >=\n SUGGESTED_OCCURRENCE_FLOORS_CACHE_TTL_MS\n ) {\n await this.#refreshSuggestedOccurrenceFloors(now);\n }\n\n const decimalChainId = String(convertHexToDecimal(hexChainId));\n return (\n this.#suggestedOccurrenceFloors?.[decimalChainId] ??\n DEFAULT_OCCURRENCE_FLOOR\n );\n }\n\n /**\n * Fetch `/v1/suggestedOccurrenceFloors` and update the instance cache.\n * Concurrent callers share the in-flight Promise. Failures leave the cache\n * empty so {@link #getOccurrenceFloor} falls back to the default.\n *\n * @param now - Current timestamp used to stamp the cache entry.\n * @returns A promise that resolves once the cache has been refreshed.\n */\n async #refreshSuggestedOccurrenceFloors(now: number): Promise<void> {\n if (this.#suggestedOccurrenceFloorsRefreshPromise !== undefined) {\n return this.#suggestedOccurrenceFloorsRefreshPromise;\n }\n\n this.#suggestedOccurrenceFloorsRefreshPromise =\n (async (): Promise<void> => {\n try {\n const response = await this.#fetch(SUGGESTED_OCCURRENCE_FLOORS_URL);\n if (response.ok) {\n const data = (await response.json()) as Record<string, number>;\n this.#suggestedOccurrenceFloors =\n data && typeof data === 'object' && !Array.isArray(data)\n ? data\n : {};\n this.#suggestedOccurrenceFloorsCachedAt = now;\n } else {\n this.#suggestedOccurrenceFloors = {};\n this.#suggestedOccurrenceFloorsCachedAt = now;\n }\n } catch {\n this.#suggestedOccurrenceFloors = {};\n this.#suggestedOccurrenceFloorsCachedAt = now;\n } finally {\n this.#suggestedOccurrenceFloorsRefreshPromise = undefined;\n }\n })();\n\n return this.#suggestedOccurrenceFloorsRefreshPromise;\n }\n\n async #fetchTokenListUncached(\n hexChainId: ChainId,\n ): Promise<TokenListEntry[]> {\n const decimalChainId = convertHexToDecimal(hexChainId);\n const occurrenceFloor = await this.#getOccurrenceFloor(hexChainId);\n\n // Same query shape as `TokenListController.getTokensURL` (token-service.ts),\n // but `occurrenceFloor` comes from `/v1/suggestedOccurrenceFloors`.\n // No `first=...` cap — the API returns the full per-chain list bounded\n // server-side by `occurrenceFloor`.\n const url =\n `${TOKEN_END_POINT_API}/tokens/${decimalChainId}` +\n `?occurrenceFloor=${occurrenceFloor}` +\n `&includeNativeAssets=false` +\n `&includeTokenFees=false` +\n `&includeAssetType=false` +\n `&includeERC20Permit=false` +\n `&includeStorage=false` +\n `&includeRwaData=true`;\n\n let response: Response;\n try {\n response = await this.#fetch(url);\n } catch (error) {\n console.error(\n `Tokens API request failed for chain ${hexChainId}:`,\n error,\n );\n return [];\n }\n\n if (!response.ok) {\n console.error(\n `Tokens API responded with ${response.status} for chain ${hexChainId}`,\n );\n return [];\n }\n\n const raw = (await response.json()) as unknown;\n const items: ApiTokenListItem[] = Array.isArray(raw)\n ? (raw as ApiTokenListItem[])\n : [];\n\n const filtered = applyChainSpecificFilters(hexChainId, items);\n\n return filtered.map((item) => ({\n address: item.address,\n symbol: item.symbol ?? '',\n name: item.name ?? '',\n decimals: item.decimals ?? 18,\n iconUrl: item.iconUrl,\n aggregators: item.aggregators,\n occurrences: item.occurrences,\n }));\n }\n}\n\n/**\n * Apply chain-specific filters to a raw token list response, mirroring\n * `fetchTokenListByChainId` in `assets-controllers/src/token-service.ts`.\n *\n * For Linea mainnet, the API returns extras with low aggregator coverage, so\n * we keep only entries flagged by Linea's own team or seen by ≥3 aggregators.\n *\n * @param hexChainId - Hex chain ID.\n * @param items - Raw items from the API response.\n * @returns Items after chain-specific filtering.\n */\nfunction applyChainSpecificFilters(\n hexChainId: ChainId,\n items: ApiTokenListItem[],\n): ApiTokenListItem[] {\n if (hexChainId === ControllerChainId['linea-mainnet']) {\n return items.filter((item) => {\n const aggregators = item.aggregators ?? [];\n return aggregators.includes('lineaTeam') || aggregators.length >= 3;\n });\n }\n return items;\n}\n"]}
@@ -28,9 +28,10 @@ export type TokensApiClientConfig = {
28
28
  *
29
29
  * Fetches the per-chain ERC-20 token list from the same endpoint that
30
30
  * `TokenListController` uses (`token.api.cx.metamask.io/tokens/{chainId}`),
31
- * with the same query parameters and the same per-chain occurrence floor /
32
- * Linea aggregator filter. This keeps RPC token detection in lockstep with
33
- * the wallet's primary token list.
31
+ * with the same query parameters. The `occurrenceFloor` query param comes from
32
+ * Token API `/v1/suggestedOccurrenceFloors` (fallback {@link DEFAULT_OCCURRENCE_FLOOR}),
33
+ * matching `TokenDataSource` spam filtering. Linea's post-response aggregator
34
+ * filter is still applied client-side.
34
35
  *
35
36
  * Before fetching a chain's token list, the client checks
36
37
  * `/v2/supportedNetworks` and returns `[]` immediately for chains that are
@@ -1 +1 @@
1
- {"version":3,"file":"TokensApiClient.d.cts","sourceRoot":"","sources":["../../../../src/data-sources/evm-rpc-services/clients/TokensApiClient.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,2BAAiB;AAuFxD;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC,UAAU,CAAC,KAAK,EAAE,OAAO,EAAE;QACzB,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC;QAC7B,OAAO,EAAE,MAAM,OAAO,CAAC,KAAK,CAAC,CAAC;QAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,qDAAqD;IACrD,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC;;;;OAIG;IACH,WAAW,CAAC,EAAE,oBAAoB,CAAC;CACpC,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,eAAe;;gBAkBd,MAAM,CAAC,EAAE,qBAAqB;IAK1C;;;;;;;;;;OAUG;IACG,cAAc,CAAC,UAAU,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;CAwIrE"}
1
+ {"version":3,"file":"TokensApiClient.d.cts","sourceRoot":"","sources":["../../../../src/data-sources/evm-rpc-services/clients/TokensApiClient.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,2BAAiB;AAgFxD;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC,UAAU,CAAC,KAAK,EAAE,OAAO,EAAE;QACzB,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC;QAC7B,OAAO,EAAE,MAAM,OAAO,CAAC,KAAK,CAAC,CAAC;QAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,qDAAqD;IACrD,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC;;;;OAIG;IACH,WAAW,CAAC,EAAE,oBAAoB,CAAC;CACpC,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,eAAe;;gBA2Bd,MAAM,CAAC,EAAE,qBAAqB;IAK1C;;;;;;;;;;OAUG;IACG,cAAc,CAAC,UAAU,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;CA0MrE"}
@@ -28,9 +28,10 @@ export type TokensApiClientConfig = {
28
28
  *
29
29
  * Fetches the per-chain ERC-20 token list from the same endpoint that
30
30
  * `TokenListController` uses (`token.api.cx.metamask.io/tokens/{chainId}`),
31
- * with the same query parameters and the same per-chain occurrence floor /
32
- * Linea aggregator filter. This keeps RPC token detection in lockstep with
33
- * the wallet's primary token list.
31
+ * with the same query parameters. The `occurrenceFloor` query param comes from
32
+ * Token API `/v1/suggestedOccurrenceFloors` (fallback {@link DEFAULT_OCCURRENCE_FLOOR}),
33
+ * matching `TokenDataSource` spam filtering. Linea's post-response aggregator
34
+ * filter is still applied client-side.
34
35
  *
35
36
  * Before fetching a chain's token list, the client checks
36
37
  * `/v2/supportedNetworks` and returns `[]` immediately for chains that are
@@ -1 +1 @@
1
- {"version":3,"file":"TokensApiClient.d.mts","sourceRoot":"","sources":["../../../../src/data-sources/evm-rpc-services/clients/TokensApiClient.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,2BAAiB;AAuFxD;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC,UAAU,CAAC,KAAK,EAAE,OAAO,EAAE;QACzB,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC;QAC7B,OAAO,EAAE,MAAM,OAAO,CAAC,KAAK,CAAC,CAAC;QAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,qDAAqD;IACrD,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC;;;;OAIG;IACH,WAAW,CAAC,EAAE,oBAAoB,CAAC;CACpC,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,eAAe;;gBAkBd,MAAM,CAAC,EAAE,qBAAqB;IAK1C;;;;;;;;;;OAUG;IACG,cAAc,CAAC,UAAU,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;CAwIrE"}
1
+ {"version":3,"file":"TokensApiClient.d.mts","sourceRoot":"","sources":["../../../../src/data-sources/evm-rpc-services/clients/TokensApiClient.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,2BAAiB;AAgFxD;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC,UAAU,CAAC,KAAK,EAAE,OAAO,EAAE;QACzB,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC;QAC7B,OAAO,EAAE,MAAM,OAAO,CAAC,KAAK,CAAC,CAAC;QAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,qDAAqD;IACrD,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC;;;;OAIG;IACH,WAAW,CAAC,EAAE,oBAAoB,CAAC;CACpC,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,eAAe;;gBA2Bd,MAAM,CAAC,EAAE,qBAAqB;IAK1C;;;;;;;;;;OAUG;IACG,cAAc,CAAC,UAAU,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;CA0MrE"}