@metamask-previews/assets-controller 18.0.1-preview-44b7ded21 → 18.0.1-preview-cbae429b5

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 (25) hide show
  1. package/CHANGELOG.md +0 -6
  2. package/dist/AssetsController.d.ts.map +1 -1
  3. package/dist/AssetsController.js +5 -1
  4. package/dist/AssetsController.js.map +1 -1
  5. package/dist/data-sources/RpcDataSource.d.ts +12 -3
  6. package/dist/data-sources/RpcDataSource.d.ts.map +1 -1
  7. package/dist/data-sources/RpcDataSource.js +9 -2
  8. package/dist/data-sources/RpcDataSource.js.map +1 -1
  9. package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.d.ts +61 -0
  10. package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.d.ts.map +1 -0
  11. package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.js +297 -0
  12. package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.js.map +1 -0
  13. package/dist/data-sources/evm-rpc-services/clients/index.d.ts +1 -0
  14. package/dist/data-sources/evm-rpc-services/clients/index.d.ts.map +1 -1
  15. package/dist/data-sources/evm-rpc-services/clients/index.js +1 -0
  16. package/dist/data-sources/evm-rpc-services/clients/index.js.map +1 -1
  17. package/dist/data-sources/evm-rpc-services/index.d.ts +1 -1
  18. package/dist/data-sources/evm-rpc-services/index.d.ts.map +1 -1
  19. package/dist/data-sources/evm-rpc-services/index.js +1 -1
  20. package/dist/data-sources/evm-rpc-services/index.js.map +1 -1
  21. package/dist/data-sources/evm-rpc-services/services/TokenDetector.d.ts +3 -15
  22. package/dist/data-sources/evm-rpc-services/services/TokenDetector.d.ts.map +1 -1
  23. package/dist/data-sources/evm-rpc-services/services/TokenDetector.js +20 -153
  24. package/dist/data-sources/evm-rpc-services/services/TokenDetector.js.map +1 -1
  25. package/package.json +7 -7
@@ -1,22 +1,6 @@
1
- import { ChainId as ControllerChainId, convertHexToDecimal, } from '@metamask/controller-utils';
2
1
  import { StaticIntervalPollingControllerOnly } from '@metamask/polling-controller';
3
2
  import { projectLogger, createModuleLogger } from '../../../logger.js';
4
3
  import { reduceInBatchesSerially } from '../utils/index.js';
5
- /**
6
- * Fallback `occurrenceFloor` when `/v1/suggestedOccurrenceFloors` has no entry
7
- * for the chain, or the floors request fails.
8
- */
9
- const DEFAULT_OCCURRENCE_FLOOR = 3;
10
- /** How long to keep `/v2/supportedNetworks` cached. */
11
- const SUPPORTED_NETWORKS_CACHE_TTL_MS = 60 * 60_000;
12
- /** How long to keep `/v1/suggestedOccurrenceFloors` cached. */
13
- const SUGGESTED_OCCURRENCE_FLOORS_CACHE_TTL_MS = 60 * 60_000;
14
- /**
15
- * Token-list cache window used when the caller shares a QueryClient.
16
- * Without one, the list is refetched on every call.
17
- */
18
- const TOKEN_LIST_STALE_TIME_MS = 5 * 60_000;
19
- const TOKEN_LIST_GC_TIME_MS = 60 * 60_000;
20
4
  const log = createModuleLogger(projectLogger, 'TokenDetector');
21
5
  const DEFAULT_DETECTION_INTERVAL = 180_000; // 3 minutes
22
6
  /**
@@ -26,19 +10,14 @@ const DEFAULT_DETECTION_INTERVAL = 180_000; // 3 minutes
26
10
  */
27
11
  export class TokenDetector extends StaticIntervalPollingControllerOnly() {
28
12
  #multicallClient;
29
- #tokenApi;
13
+ #tokensApiClient;
30
14
  #config;
31
15
  #tokenListCache = new Map();
32
- /**
33
- * Last successful `/v2/supportedNetworks` CAIP chain IDs.
34
- * A failed refresh leaves this in place so detection can continue.
35
- */
36
- #supportedChainIds;
37
16
  #onDetectionUpdate;
38
- constructor(multicallClient, tokenApi, config) {
17
+ constructor(multicallClient, tokensApiClient, config) {
39
18
  super();
40
19
  this.#multicallClient = multicallClient;
41
- this.#tokenApi = tokenApi;
20
+ this.#tokensApiClient = tokensApiClient;
42
21
  this.#config = {
43
22
  tokenDetectionEnabled: config?.tokenDetectionEnabled ?? (() => true),
44
23
  useExternalService: config?.useExternalService ?? (() => true),
@@ -80,31 +59,9 @@ export class TokenDetector extends StaticIntervalPollingControllerOnly() {
80
59
  * @returns Array of token contract addresses.
81
60
  */
82
61
  async getTokensToCheck(chainId) {
83
- const tokenList = await this.fetchAndCacheTokenList(chainId);
62
+ const tokenList = await this.#fetchAndCacheTokenList(chainId);
84
63
  return tokenList.map((entry) => entry.address);
85
64
  }
86
- /**
87
- * Fetch the mapped token list for a chain and keep it for metadata lookups.
88
- *
89
- * @param chainId - Chain ID in hex format.
90
- * @returns Token list entries, including fields the address list drops.
91
- */
92
- async fetchAndCacheTokenList(chainId) {
93
- try {
94
- const list = await this.#fetchTokenList(chainId);
95
- this.#tokenListCache.set(chainId, list);
96
- return list;
97
- }
98
- catch (error) {
99
- const cached = this.#tokenListCache.get(chainId);
100
- log('Failed to fetch token list; using stale cache', {
101
- chainId,
102
- cachedCount: cached?.length ?? 0,
103
- error,
104
- });
105
- return cached ?? [];
106
- }
107
- }
108
65
  async detectTokens(chainId, accountId, accountAddress, options) {
109
66
  const tokenDetectionEnabled = options?.tokenDetectionEnabled ?? this.#config.tokenDetectionEnabled();
110
67
  const useExternalService = options?.useExternalService ?? this.#config.useExternalService();
@@ -161,6 +118,22 @@ export class TokenDetector extends StaticIntervalPollingControllerOnly() {
161
118
  timestamp,
162
119
  };
163
120
  }
121
+ async #fetchAndCacheTokenList(chainId) {
122
+ try {
123
+ const list = await this.#tokensApiClient.fetchTokenList(chainId);
124
+ this.#tokenListCache.set(chainId, list);
125
+ return list;
126
+ }
127
+ catch (error) {
128
+ const cached = this.#tokenListCache.get(chainId);
129
+ log('Failed to fetch token list; using stale cache', {
130
+ chainId,
131
+ cachedCount: cached?.length ?? 0,
132
+ error,
133
+ });
134
+ return cached ?? [];
135
+ }
136
+ }
164
137
  #processBalanceResponses(responses, accumulator, chainId, accountId, timestamp) {
165
138
  const { detectedAssets, detectedBalances, zeroBalanceAddresses, failedAddresses, } = accumulator;
166
139
  for (const response of responses) {
@@ -215,112 +188,6 @@ export class TokenDetector extends StaticIntervalPollingControllerOnly() {
215
188
  return rawBalance;
216
189
  }
217
190
  }
218
- /**
219
- * Load the ERC-20 list for a chain from `@metamask/core-backend`
220
- * `TokenApiClient`:
221
- * - `GET /v2/supportedNetworks`
222
- * - `GET /v1/suggestedOccurrenceFloors`
223
- * - `GET /tokens/{chainId}`
224
- *
225
- * Returns `[]` when the chain is unsupported, supported networks have never
226
- * loaded, or the token-list request fails. A failed refresh of
227
- * `/v2/supportedNetworks` reuses the last successful list. Linea mainnet
228
- * keeps entries flagged by `lineaTeam` or seen by at least 3 aggregators.
229
- *
230
- * @param hexChainId - Chain ID in hex format (for example `'0x1'`).
231
- * @returns Token list entries, or an empty array.
232
- */
233
- async #fetchTokenList(hexChainId) {
234
- if (!(await this.#isSupportedChain(hexChainId))) {
235
- return [];
236
- }
237
- return this.#fetchTokenListEntries(hexChainId);
238
- }
239
- async #isSupportedChain(hexChainId) {
240
- const caipChainId = `eip155:${convertHexToDecimal(hexChainId)}`;
241
- try {
242
- const data = await this.#tokenApi.fetchV2SupportedNetworks({
243
- staleTime: SUPPORTED_NETWORKS_CACHE_TTL_MS,
244
- gcTime: SUPPORTED_NETWORKS_CACHE_TTL_MS,
245
- retry: false,
246
- });
247
- this.#supportedChainIds = new Set([
248
- ...(data.fullSupport ?? []),
249
- ...(data.partialSupport ?? []),
250
- ]);
251
- }
252
- catch {
253
- // Leave `#supportedChainIds` unchanged. The first failure has no list yet.
254
- }
255
- return this.#supportedChainIds?.has(caipChainId) ?? false;
256
- }
257
- async #getOccurrenceFloor(hexChainId) {
258
- try {
259
- const data = await this.#tokenApi.fetchV1SuggestedOccurrenceFloors({
260
- staleTime: SUGGESTED_OCCURRENCE_FLOORS_CACHE_TTL_MS,
261
- gcTime: SUGGESTED_OCCURRENCE_FLOORS_CACHE_TTL_MS,
262
- retry: false,
263
- });
264
- if (data && typeof data === 'object' && !Array.isArray(data)) {
265
- const decimalChainId = String(convertHexToDecimal(hexChainId));
266
- return data[decimalChainId] ?? DEFAULT_OCCURRENCE_FLOOR;
267
- }
268
- }
269
- catch {
270
- // Fall through to the default floor.
271
- }
272
- return DEFAULT_OCCURRENCE_FLOOR;
273
- }
274
- async #fetchTokenListEntries(hexChainId) {
275
- const decimalChainId = convertHexToDecimal(hexChainId);
276
- const occurrenceFloor = await this.#getOccurrenceFloor(hexChainId);
277
- try {
278
- const raw = await this.#tokenApi.fetchTokenList(decimalChainId, {
279
- occurrenceFloor,
280
- includeNativeAssets: false,
281
- includeTokenFees: false,
282
- includeAssetType: false,
283
- includeERC20Permit: false,
284
- includeStorage: false,
285
- includeRwaData: true,
286
- }, {
287
- staleTime: TOKEN_LIST_STALE_TIME_MS,
288
- gcTime: TOKEN_LIST_GC_TIME_MS,
289
- retry: false,
290
- });
291
- const items = Array.isArray(raw) ? raw : [];
292
- return this.#applyChainSpecificFilters(hexChainId, items).map((item) => ({
293
- address: item.address,
294
- symbol: item.symbol ?? '',
295
- name: item.name ?? '',
296
- decimals: item.decimals ?? 18,
297
- iconUrl: item.iconUrl,
298
- aggregators: item.aggregators,
299
- occurrences: item.occurrences,
300
- }));
301
- }
302
- catch (error) {
303
- console.error(`Tokens API request failed for chain ${hexChainId}:`, error);
304
- return [];
305
- }
306
- }
307
- /**
308
- * Apply chain-specific filters, mirroring `fetchTokenListByChainId` in
309
- * `assets-controllers/src/token-service.ts`.
310
- *
311
- * @param hexChainId - Hex chain ID.
312
- * @param items - Raw items from the API response.
313
- * @returns Items after chain-specific filtering.
314
- */
315
- #applyChainSpecificFilters(hexChainId, items) {
316
- if (hexChainId === ControllerChainId['linea-mainnet']) {
317
- return items.filter((item) => {
318
- const aggregators = item.aggregators ?? [];
319
- return aggregators.includes('lineaTeam') || aggregators.length >= 3;
320
- });
321
- }
322
- return items;
323
- }
324
191
  #getTokenMetadata(chainId, tokenAddress) {
325
192
  const list = this.#tokenListCache.get(chainId) ?? [];
326
193
  const lowerAddress = tokenAddress.toLowerCase();
@@ -1 +1 @@
1
- {"version":3,"file":"TokenDetector.js","sourceRoot":"","sources":["../../../../src/data-sources/evm-rpc-services/services/TokenDetector.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ChainId as ControllerChainId,\n convertHexToDecimal,\n} from '@metamask/controller-utils';\nimport type { TokenApiClient } from '@metamask/core-backend';\nimport { StaticIntervalPollingControllerOnly } from '@metamask/polling-controller';\nimport type { CaipAssetType } from '@metamask/utils';\n\nimport { projectLogger, createModuleLogger } from '../../../logger.js';\nimport type { MulticallClient } from '../clients/index.js';\nimport type {\n AccountId,\n Address,\n Asset,\n AssetBalance,\n BalanceOfRequest,\n BalanceOfResponse,\n ChainId,\n TokenDetectionOptions,\n TokenDetectionResult,\n TokenListEntry,\n} from '../types/index.js';\nimport { reduceInBatchesSerially } from '../utils/index.js';\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/** How long to keep `/v2/supportedNetworks` cached. */\nconst SUPPORTED_NETWORKS_CACHE_TTL_MS = 60 * 60_000;\n\n/** How long to keep `/v1/suggestedOccurrenceFloors` cached. */\nconst SUGGESTED_OCCURRENCE_FLOORS_CACHE_TTL_MS = 60 * 60_000;\n\n/**\n * Token-list cache window used when the caller shares a QueryClient.\n * Without one, the list is refetched on every call.\n */\nconst TOKEN_LIST_STALE_TIME_MS = 5 * 60_000;\nconst TOKEN_LIST_GC_TIME_MS = 60 * 60_000;\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 * Token-list calls TokenDetector makes. `TokenApiClient` satisfies this, and\n * tests can pass an object with only these methods.\n */\ntype TokenListApi = Pick<\n TokenApiClient,\n | 'fetchV2SupportedNetworks'\n | 'fetchV1SuggestedOccurrenceFloors'\n | 'fetchTokenList'\n>;\n\nconst log = createModuleLogger(projectLogger, 'TokenDetector');\n\nconst DEFAULT_DETECTION_INTERVAL = 180_000; // 3 minutes\n\nexport type TokenDetectorConfig = {\n /** Function returning whether token detection is enabled (avoids stale value) */\n tokenDetectionEnabled?: () => boolean;\n /** Function returning whether external services are allowed (avoids stale value; default: () => true) */\n useExternalService?: () => boolean;\n defaultBatchSize?: number;\n defaultTimeoutMs?: number;\n /** Polling interval in ms (default: 3 minutes) */\n pollingInterval?: number;\n};\n\n/**\n * Polling input for TokenDetector - identifies what to poll for.\n */\nexport type DetectionPollingInput = {\n /** Chain ID (hex format) */\n chainId: ChainId;\n /** Account ID */\n accountId: AccountId;\n /** Account address */\n accountAddress: Address;\n};\n\n/**\n * Callback type for token detection updates.\n */\nexport type OnDetectionUpdateCallback = (result: TokenDetectionResult) => void;\n\n/**\n * TokenDetector - Detects tokens with non-zero balances via multicall.\n * Fetches the token list from the Tokens API and uses multicall to check balances.\n * Extends StaticIntervalPollingControllerOnly for built-in polling support.\n */\nexport class TokenDetector extends StaticIntervalPollingControllerOnly<DetectionPollingInput>() {\n readonly #multicallClient: MulticallClient;\n\n readonly #tokenApi: TokenListApi;\n\n readonly #config: Required<Omit<TokenDetectorConfig, 'pollingInterval'>>;\n\n readonly #tokenListCache: Map<ChainId, TokenListEntry[]> = new Map();\n\n /**\n * Last successful `/v2/supportedNetworks` CAIP chain IDs.\n * A failed refresh leaves this in place so detection can continue.\n */\n #supportedChainIds: Set<string> | undefined;\n\n #onDetectionUpdate: OnDetectionUpdateCallback | undefined;\n\n constructor(\n multicallClient: MulticallClient,\n tokenApi: TokenListApi,\n config?: TokenDetectorConfig,\n ) {\n super();\n this.#multicallClient = multicallClient;\n this.#tokenApi = tokenApi;\n this.#config = {\n tokenDetectionEnabled:\n config?.tokenDetectionEnabled ?? ((): boolean => true),\n useExternalService: config?.useExternalService ?? ((): boolean => true),\n defaultBatchSize: config?.defaultBatchSize ?? 300,\n defaultTimeoutMs: config?.defaultTimeoutMs ?? 30000,\n };\n\n this.setIntervalLength(\n config?.pollingInterval ?? DEFAULT_DETECTION_INTERVAL,\n );\n }\n\n /**\n * Set the callback to receive detection updates during polling.\n *\n * @param callback - Function to call with detection results.\n */\n setOnDetectionUpdate(callback: OnDetectionUpdateCallback): void {\n this.#onDetectionUpdate = callback;\n }\n\n /**\n * Execute a poll cycle (required by base class).\n * Detects tokens and calls the update callback.\n *\n * @param input - The polling input.\n */\n async _executePoll(input: DetectionPollingInput): Promise<void> {\n try {\n const result = await this.detectTokens(\n input.chainId,\n input.accountId,\n input.accountAddress,\n );\n\n if (this.#onDetectionUpdate && result.detectedAssets.length > 0) {\n this.#onDetectionUpdate(result);\n }\n } catch (error) {\n log('Token detection poll failed', { chainId: input.chainId, error });\n }\n }\n\n /**\n * Fetch the list of token addresses to check for the given chain.\n * Calls the Tokens API and caches the result for metadata lookups.\n *\n * @param chainId - Chain ID in hex format.\n * @returns Array of token contract addresses.\n */\n async getTokensToCheck(chainId: ChainId): Promise<Address[]> {\n const tokenList = await this.fetchAndCacheTokenList(chainId);\n return tokenList.map((entry) => entry.address as Address);\n }\n\n /**\n * Fetch the mapped token list for a chain and keep it for metadata lookups.\n *\n * @param chainId - Chain ID in hex format.\n * @returns Token list entries, including fields the address list drops.\n */\n async fetchAndCacheTokenList(chainId: ChainId): Promise<TokenListEntry[]> {\n try {\n const list = await this.#fetchTokenList(chainId);\n this.#tokenListCache.set(chainId, list);\n return list;\n } catch (error) {\n const cached = this.#tokenListCache.get(chainId);\n log('Failed to fetch token list; using stale cache', {\n chainId,\n cachedCount: cached?.length ?? 0,\n error,\n });\n return cached ?? [];\n }\n }\n\n async detectTokens(\n chainId: ChainId,\n accountId: AccountId,\n accountAddress: Address,\n options?: TokenDetectionOptions,\n ): Promise<TokenDetectionResult> {\n const tokenDetectionEnabled =\n options?.tokenDetectionEnabled ?? this.#config.tokenDetectionEnabled();\n const useExternalService =\n options?.useExternalService ?? this.#config.useExternalService();\n if (!tokenDetectionEnabled || !useExternalService) {\n return {\n chainId,\n accountId,\n accountAddress,\n detectedAssets: [],\n detectedBalances: [],\n zeroBalanceAddresses: [],\n failedAddresses: [],\n timestamp: Date.now(),\n };\n }\n const batchSize = options?.batchSize ?? this.#config.defaultBatchSize;\n const timestamp = Date.now();\n\n const tokensToCheck = await this.getTokensToCheck(chainId);\n\n if (tokensToCheck.length === 0) {\n return {\n chainId,\n accountId,\n accountAddress,\n detectedAssets: [],\n detectedBalances: [],\n zeroBalanceAddresses: [],\n failedAddresses: [],\n timestamp,\n };\n }\n\n const balanceRequests: BalanceOfRequest[] = tokensToCheck.map(\n (tokenAddress) => ({\n tokenAddress,\n accountAddress,\n }),\n );\n\n type DetectionAccumulator = {\n detectedAssets: Asset[];\n detectedBalances: AssetBalance[];\n zeroBalanceAddresses: Address[];\n failedAddresses: Address[];\n };\n\n const result = await reduceInBatchesSerially<\n BalanceOfRequest,\n DetectionAccumulator\n >({\n values: balanceRequests,\n batchSize,\n initialResult: {\n detectedAssets: [],\n detectedBalances: [],\n zeroBalanceAddresses: [],\n failedAddresses: [],\n },\n eachBatch: async (workingResult, batch) => {\n const responses = await this.#multicallClient.batchBalanceOf(\n chainId,\n batch,\n );\n\n return this.#processBalanceResponses(\n responses,\n workingResult as DetectionAccumulator,\n chainId,\n accountId,\n timestamp,\n );\n },\n });\n\n return {\n chainId,\n accountId,\n accountAddress,\n ...result,\n timestamp,\n };\n }\n\n #processBalanceResponses(\n responses: BalanceOfResponse[],\n accumulator: {\n detectedAssets: Asset[];\n detectedBalances: AssetBalance[];\n zeroBalanceAddresses: Address[];\n failedAddresses: Address[];\n },\n chainId: ChainId,\n accountId: AccountId,\n timestamp: number,\n ): {\n detectedAssets: Asset[];\n detectedBalances: AssetBalance[];\n zeroBalanceAddresses: Address[];\n failedAddresses: Address[];\n } {\n const {\n detectedAssets,\n detectedBalances,\n zeroBalanceAddresses,\n failedAddresses,\n } = accumulator;\n\n for (const response of responses) {\n if (!response.success) {\n failedAddresses.push(response.tokenAddress);\n continue;\n }\n\n const balance = response.balance ?? '0';\n\n if (balance === '0' || balance === '') {\n zeroBalanceAddresses.push(response.tokenAddress);\n continue;\n }\n\n const tokenMetadata = this.#getTokenMetadata(\n chainId,\n response.tokenAddress,\n );\n\n const asset = this.#createAsset(\n chainId,\n response.tokenAddress,\n tokenMetadata,\n );\n detectedAssets.push(asset);\n\n if (tokenMetadata?.decimals === undefined) {\n continue;\n }\n\n const { decimals } = tokenMetadata;\n const formattedBalance = this.#formatBalance(balance, decimals);\n detectedBalances.push({\n assetId: asset.assetId,\n accountId,\n chainId,\n balance,\n formattedBalance,\n decimals,\n timestamp,\n });\n }\n\n return {\n detectedAssets,\n detectedBalances,\n zeroBalanceAddresses,\n failedAddresses,\n };\n }\n\n #formatBalance(rawBalance: string, decimals: number): string {\n try {\n const balanceBigInt = BigInt(rawBalance);\n const divisor = BigInt(10 ** decimals);\n\n const integerPart = balanceBigInt / divisor;\n const remainder = balanceBigInt % divisor;\n const fractionalStr = remainder.toString().padStart(decimals, '0');\n const trimmedFractional = fractionalStr.replace(/0+$/u, '');\n\n if (trimmedFractional === '') {\n return integerPart.toString();\n }\n\n return `${integerPart}.${trimmedFractional}`;\n } catch {\n return rawBalance;\n }\n }\n\n /**\n * Load the ERC-20 list for a chain from `@metamask/core-backend`\n * `TokenApiClient`:\n * - `GET /v2/supportedNetworks`\n * - `GET /v1/suggestedOccurrenceFloors`\n * - `GET /tokens/{chainId}`\n *\n * Returns `[]` when the chain is unsupported, supported networks have never\n * loaded, or the token-list request fails. A failed refresh of\n * `/v2/supportedNetworks` reuses the last successful list. Linea mainnet\n * keeps entries flagged by `lineaTeam` or seen by at least 3 aggregators.\n *\n * @param hexChainId - Chain ID in hex format (for example `'0x1'`).\n * @returns Token list entries, or an empty array.\n */\n async #fetchTokenList(hexChainId: ChainId): Promise<TokenListEntry[]> {\n if (!(await this.#isSupportedChain(hexChainId))) {\n return [];\n }\n\n return this.#fetchTokenListEntries(hexChainId);\n }\n\n async #isSupportedChain(hexChainId: ChainId): Promise<boolean> {\n const caipChainId = `eip155:${convertHexToDecimal(hexChainId)}`;\n try {\n const data = await this.#tokenApi.fetchV2SupportedNetworks({\n staleTime: SUPPORTED_NETWORKS_CACHE_TTL_MS,\n gcTime: SUPPORTED_NETWORKS_CACHE_TTL_MS,\n retry: false,\n });\n this.#supportedChainIds = new Set([\n ...(data.fullSupport ?? []),\n ...(data.partialSupport ?? []),\n ]);\n } catch {\n // Leave `#supportedChainIds` unchanged. The first failure has no list yet.\n }\n return this.#supportedChainIds?.has(caipChainId) ?? false;\n }\n\n async #getOccurrenceFloor(hexChainId: ChainId): Promise<number> {\n try {\n const data = await this.#tokenApi.fetchV1SuggestedOccurrenceFloors({\n staleTime: SUGGESTED_OCCURRENCE_FLOORS_CACHE_TTL_MS,\n gcTime: SUGGESTED_OCCURRENCE_FLOORS_CACHE_TTL_MS,\n retry: false,\n });\n if (data && typeof data === 'object' && !Array.isArray(data)) {\n const decimalChainId = String(convertHexToDecimal(hexChainId));\n return data[decimalChainId] ?? DEFAULT_OCCURRENCE_FLOOR;\n }\n } catch {\n // Fall through to the default floor.\n }\n return DEFAULT_OCCURRENCE_FLOOR;\n }\n\n async #fetchTokenListEntries(hexChainId: ChainId): Promise<TokenListEntry[]> {\n const decimalChainId = convertHexToDecimal(hexChainId);\n const occurrenceFloor = await this.#getOccurrenceFloor(hexChainId);\n\n try {\n const raw = await this.#tokenApi.fetchTokenList(\n decimalChainId,\n {\n occurrenceFloor,\n includeNativeAssets: false,\n includeTokenFees: false,\n includeAssetType: false,\n includeERC20Permit: false,\n includeStorage: false,\n includeRwaData: true,\n },\n {\n staleTime: TOKEN_LIST_STALE_TIME_MS,\n gcTime: TOKEN_LIST_GC_TIME_MS,\n retry: false,\n },\n );\n const items: ApiTokenListItem[] = Array.isArray(raw) ? raw : [];\n return this.#applyChainSpecificFilters(hexChainId, items).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 } catch (error) {\n console.error(\n `Tokens API request failed for chain ${hexChainId}:`,\n error,\n );\n return [];\n }\n }\n\n /**\n * Apply chain-specific filters, mirroring `fetchTokenListByChainId` in\n * `assets-controllers/src/token-service.ts`.\n *\n * @param hexChainId - Hex chain ID.\n * @param items - Raw items from the API response.\n * @returns Items after chain-specific filtering.\n */\n #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\n #getTokenMetadata(\n chainId: ChainId,\n tokenAddress: Address,\n ): TokenListEntry | undefined {\n const list = this.#tokenListCache.get(chainId) ?? [];\n const lowerAddress = tokenAddress.toLowerCase();\n\n const exact = list.find((entry) => entry.address === tokenAddress);\n if (exact) {\n return exact;\n }\n\n return list.find((entry) => entry.address.toLowerCase() === lowerAddress);\n }\n\n #createAsset(\n chainId: ChainId,\n tokenAddress: Address,\n metadata: TokenListEntry | undefined,\n ): Asset {\n const chainIdDecimal = parseInt(chainId, 16);\n\n const assetId =\n `eip155:${chainIdDecimal}/erc20:${tokenAddress.toLowerCase()}` as CaipAssetType;\n\n return {\n assetId,\n chainId,\n address: tokenAddress,\n type: 'erc20',\n symbol: metadata?.symbol,\n name: metadata?.name,\n decimals: metadata?.decimals,\n image: metadata?.iconUrl,\n isNative: false,\n aggregators: metadata?.aggregators,\n };\n }\n}\n"]}
1
+ {"version":3,"file":"TokenDetector.js","sourceRoot":"","sources":["../../../../src/data-sources/evm-rpc-services/services/TokenDetector.ts"],"names":[],"mappings":"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","sourcesContent":["import { StaticIntervalPollingControllerOnly } from '@metamask/polling-controller';\nimport type { CaipAssetType } from '@metamask/utils';\n\nimport { projectLogger, createModuleLogger } from '../../../logger.js';\nimport type { MulticallClient } from '../clients/index.js';\nimport type { TokensApiClient } from '../clients/TokensApiClient.js';\nimport type {\n AccountId,\n Address,\n Asset,\n AssetBalance,\n BalanceOfRequest,\n BalanceOfResponse,\n ChainId,\n TokenDetectionOptions,\n TokenDetectionResult,\n TokenListEntry,\n} from '../types/index.js';\nimport { reduceInBatchesSerially } from '../utils/index.js';\n\nconst log = createModuleLogger(projectLogger, 'TokenDetector');\n\nconst DEFAULT_DETECTION_INTERVAL = 180_000; // 3 minutes\n\nexport type TokenDetectorConfig = {\n /** Function returning whether token detection is enabled (avoids stale value) */\n tokenDetectionEnabled?: () => boolean;\n /** Function returning whether external services are allowed (avoids stale value; default: () => true) */\n useExternalService?: () => boolean;\n defaultBatchSize?: number;\n defaultTimeoutMs?: number;\n /** Polling interval in ms (default: 3 minutes) */\n pollingInterval?: number;\n};\n\n/**\n * Polling input for TokenDetector - identifies what to poll for.\n */\nexport type DetectionPollingInput = {\n /** Chain ID (hex format) */\n chainId: ChainId;\n /** Account ID */\n accountId: AccountId;\n /** Account address */\n accountAddress: Address;\n};\n\n/**\n * Callback type for token detection updates.\n */\nexport type OnDetectionUpdateCallback = (result: TokenDetectionResult) => void;\n\n/**\n * TokenDetector - Detects tokens with non-zero balances via multicall.\n * Fetches the token list from the Tokens API and uses multicall to check balances.\n * Extends StaticIntervalPollingControllerOnly for built-in polling support.\n */\nexport class TokenDetector extends StaticIntervalPollingControllerOnly<DetectionPollingInput>() {\n readonly #multicallClient: MulticallClient;\n\n readonly #tokensApiClient: TokensApiClient;\n\n readonly #config: Required<Omit<TokenDetectorConfig, 'pollingInterval'>>;\n\n readonly #tokenListCache: Map<ChainId, TokenListEntry[]> = new Map();\n\n #onDetectionUpdate: OnDetectionUpdateCallback | undefined;\n\n constructor(\n multicallClient: MulticallClient,\n tokensApiClient: TokensApiClient,\n config?: TokenDetectorConfig,\n ) {\n super();\n this.#multicallClient = multicallClient;\n this.#tokensApiClient = tokensApiClient;\n this.#config = {\n tokenDetectionEnabled:\n config?.tokenDetectionEnabled ?? ((): boolean => true),\n useExternalService: config?.useExternalService ?? ((): boolean => true),\n defaultBatchSize: config?.defaultBatchSize ?? 300,\n defaultTimeoutMs: config?.defaultTimeoutMs ?? 30000,\n };\n\n this.setIntervalLength(\n config?.pollingInterval ?? DEFAULT_DETECTION_INTERVAL,\n );\n }\n\n /**\n * Set the callback to receive detection updates during polling.\n *\n * @param callback - Function to call with detection results.\n */\n setOnDetectionUpdate(callback: OnDetectionUpdateCallback): void {\n this.#onDetectionUpdate = callback;\n }\n\n /**\n * Execute a poll cycle (required by base class).\n * Detects tokens and calls the update callback.\n *\n * @param input - The polling input.\n */\n async _executePoll(input: DetectionPollingInput): Promise<void> {\n try {\n const result = await this.detectTokens(\n input.chainId,\n input.accountId,\n input.accountAddress,\n );\n\n if (this.#onDetectionUpdate && result.detectedAssets.length > 0) {\n this.#onDetectionUpdate(result);\n }\n } catch (error) {\n log('Token detection poll failed', { chainId: input.chainId, error });\n }\n }\n\n /**\n * Fetch the list of token addresses to check for the given chain.\n * Calls the Tokens API and caches the result for metadata lookups.\n *\n * @param chainId - Chain ID in hex format.\n * @returns Array of token contract addresses.\n */\n async getTokensToCheck(chainId: ChainId): Promise<Address[]> {\n const tokenList = await this.#fetchAndCacheTokenList(chainId);\n return tokenList.map((entry) => entry.address as Address);\n }\n\n async detectTokens(\n chainId: ChainId,\n accountId: AccountId,\n accountAddress: Address,\n options?: TokenDetectionOptions,\n ): Promise<TokenDetectionResult> {\n const tokenDetectionEnabled =\n options?.tokenDetectionEnabled ?? this.#config.tokenDetectionEnabled();\n const useExternalService =\n options?.useExternalService ?? this.#config.useExternalService();\n if (!tokenDetectionEnabled || !useExternalService) {\n return {\n chainId,\n accountId,\n accountAddress,\n detectedAssets: [],\n detectedBalances: [],\n zeroBalanceAddresses: [],\n failedAddresses: [],\n timestamp: Date.now(),\n };\n }\n const batchSize = options?.batchSize ?? this.#config.defaultBatchSize;\n const timestamp = Date.now();\n\n const tokensToCheck = await this.getTokensToCheck(chainId);\n\n if (tokensToCheck.length === 0) {\n return {\n chainId,\n accountId,\n accountAddress,\n detectedAssets: [],\n detectedBalances: [],\n zeroBalanceAddresses: [],\n failedAddresses: [],\n timestamp,\n };\n }\n\n const balanceRequests: BalanceOfRequest[] = tokensToCheck.map(\n (tokenAddress) => ({\n tokenAddress,\n accountAddress,\n }),\n );\n\n type DetectionAccumulator = {\n detectedAssets: Asset[];\n detectedBalances: AssetBalance[];\n zeroBalanceAddresses: Address[];\n failedAddresses: Address[];\n };\n\n const result = await reduceInBatchesSerially<\n BalanceOfRequest,\n DetectionAccumulator\n >({\n values: balanceRequests,\n batchSize,\n initialResult: {\n detectedAssets: [],\n detectedBalances: [],\n zeroBalanceAddresses: [],\n failedAddresses: [],\n },\n eachBatch: async (workingResult, batch) => {\n const responses = await this.#multicallClient.batchBalanceOf(\n chainId,\n batch,\n );\n\n return this.#processBalanceResponses(\n responses,\n workingResult as DetectionAccumulator,\n chainId,\n accountId,\n timestamp,\n );\n },\n });\n\n return {\n chainId,\n accountId,\n accountAddress,\n ...result,\n timestamp,\n };\n }\n\n async #fetchAndCacheTokenList(chainId: ChainId): Promise<TokenListEntry[]> {\n try {\n const list = await this.#tokensApiClient.fetchTokenList(chainId);\n this.#tokenListCache.set(chainId, list);\n return list;\n } catch (error) {\n const cached = this.#tokenListCache.get(chainId);\n log('Failed to fetch token list; using stale cache', {\n chainId,\n cachedCount: cached?.length ?? 0,\n error,\n });\n return cached ?? [];\n }\n }\n\n #processBalanceResponses(\n responses: BalanceOfResponse[],\n accumulator: {\n detectedAssets: Asset[];\n detectedBalances: AssetBalance[];\n zeroBalanceAddresses: Address[];\n failedAddresses: Address[];\n },\n chainId: ChainId,\n accountId: AccountId,\n timestamp: number,\n ): {\n detectedAssets: Asset[];\n detectedBalances: AssetBalance[];\n zeroBalanceAddresses: Address[];\n failedAddresses: Address[];\n } {\n const {\n detectedAssets,\n detectedBalances,\n zeroBalanceAddresses,\n failedAddresses,\n } = accumulator;\n\n for (const response of responses) {\n if (!response.success) {\n failedAddresses.push(response.tokenAddress);\n continue;\n }\n\n const balance = response.balance ?? '0';\n\n if (balance === '0' || balance === '') {\n zeroBalanceAddresses.push(response.tokenAddress);\n continue;\n }\n\n const tokenMetadata = this.#getTokenMetadata(\n chainId,\n response.tokenAddress,\n );\n\n const asset = this.#createAsset(\n chainId,\n response.tokenAddress,\n tokenMetadata,\n );\n detectedAssets.push(asset);\n\n if (tokenMetadata?.decimals === undefined) {\n continue;\n }\n\n const { decimals } = tokenMetadata;\n const formattedBalance = this.#formatBalance(balance, decimals);\n detectedBalances.push({\n assetId: asset.assetId,\n accountId,\n chainId,\n balance,\n formattedBalance,\n decimals,\n timestamp,\n });\n }\n\n return {\n detectedAssets,\n detectedBalances,\n zeroBalanceAddresses,\n failedAddresses,\n };\n }\n\n #formatBalance(rawBalance: string, decimals: number): string {\n try {\n const balanceBigInt = BigInt(rawBalance);\n const divisor = BigInt(10 ** decimals);\n\n const integerPart = balanceBigInt / divisor;\n const remainder = balanceBigInt % divisor;\n const fractionalStr = remainder.toString().padStart(decimals, '0');\n const trimmedFractional = fractionalStr.replace(/0+$/u, '');\n\n if (trimmedFractional === '') {\n return integerPart.toString();\n }\n\n return `${integerPart}.${trimmedFractional}`;\n } catch {\n return rawBalance;\n }\n }\n\n #getTokenMetadata(\n chainId: ChainId,\n tokenAddress: Address,\n ): TokenListEntry | undefined {\n const list = this.#tokenListCache.get(chainId) ?? [];\n const lowerAddress = tokenAddress.toLowerCase();\n\n const exact = list.find((entry) => entry.address === tokenAddress);\n if (exact) {\n return exact;\n }\n\n return list.find((entry) => entry.address.toLowerCase() === lowerAddress);\n }\n\n #createAsset(\n chainId: ChainId,\n tokenAddress: Address,\n metadata: TokenListEntry | undefined,\n ): Asset {\n const chainIdDecimal = parseInt(chainId, 16);\n\n const assetId =\n `eip155:${chainIdDecimal}/erc20:${tokenAddress.toLowerCase()}` as CaipAssetType;\n\n return {\n assetId,\n chainId,\n address: tokenAddress,\n type: 'erc20',\n symbol: metadata?.symbol,\n name: metadata?.name,\n decimals: metadata?.decimals,\n image: metadata?.iconUrl,\n isNative: false,\n aggregators: metadata?.aggregators,\n };\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metamask-previews/assets-controller",
3
- "version": "18.0.1-preview-44b7ded21",
3
+ "version": "18.0.1-preview-cbae429b5",
4
4
  "description": "Tracks assets balances/prices and handles token detection across all digital assets",
5
5
  "keywords": [
6
6
  "Ethereum",
@@ -63,9 +63,9 @@
63
63
  "@metamask/config-registry-controller": "^5.0.0",
64
64
  "@metamask/controller-utils": "^13.0.0",
65
65
  "@metamask/core-backend": "^12.0.1",
66
- "@metamask/keyring-api": "^24.1.0",
66
+ "@metamask/keyring-api": "^24.0.0",
67
67
  "@metamask/keyring-controller": "^28.1.0",
68
- "@metamask/keyring-internal-api": "^12.1.0",
68
+ "@metamask/keyring-internal-api": "^12.0.0",
69
69
  "@metamask/keyring-snap-client": "^10.0.0",
70
70
  "@metamask/messenger": "^3.0.0",
71
71
  "@metamask/multichain-transactions-controller": "^8.0.0",
@@ -76,9 +76,9 @@
76
76
  "@metamask/polling-controller": "^17.0.0",
77
77
  "@metamask/preferences-controller": "^24.0.0",
78
78
  "@metamask/remote-feature-flag-controller": "^7.0.0",
79
- "@metamask/snaps-controllers": "^19.0.1",
79
+ "@metamask/snaps-controllers": "^19.0.0",
80
80
  "@metamask/snaps-utils": "^12.1.2",
81
- "@metamask/transaction-controller": "^72.2.0",
81
+ "@metamask/transaction-controller": "^72.1.0",
82
82
  "@metamask/utils": "^12.0.0",
83
83
  "async-mutex": "^0.5.0",
84
84
  "bignumber.js": "^9.1.2",
@@ -95,8 +95,8 @@
95
95
  "nock": "^13.5.6",
96
96
  "rimraf": "^6.1.3",
97
97
  "ts-jest": "^29.4.14",
98
- "typedoc": "^0.28.20",
99
- "typedoc-plugin-missing-exports": "^4.1.4",
98
+ "typedoc": "^0.25.13",
99
+ "typedoc-plugin-missing-exports": "^2.0.0",
100
100
  "typescript": "npm:@typescript/typescript6@^6.0.2"
101
101
  },
102
102
  "engines": {