@metamask/assets-controller 8.3.3 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -1
- package/dist/AssetsController.cjs +31 -7
- package/dist/AssetsController.cjs.map +1 -1
- package/dist/AssetsController.d.cts +2 -2
- package/dist/AssetsController.d.cts.map +1 -1
- package/dist/AssetsController.d.mts +2 -2
- package/dist/AssetsController.d.mts.map +1 -1
- package/dist/AssetsController.mjs +31 -7
- package/dist/AssetsController.mjs.map +1 -1
- package/dist/data-sources/BackendWebsocketDataSource.cjs +5 -7
- package/dist/data-sources/BackendWebsocketDataSource.cjs.map +1 -1
- package/dist/data-sources/BackendWebsocketDataSource.d.cts +2 -2
- package/dist/data-sources/BackendWebsocketDataSource.d.cts.map +1 -1
- package/dist/data-sources/BackendWebsocketDataSource.d.mts +2 -2
- package/dist/data-sources/BackendWebsocketDataSource.d.mts.map +1 -1
- package/dist/data-sources/BackendWebsocketDataSource.mjs +5 -7
- package/dist/data-sources/BackendWebsocketDataSource.mjs.map +1 -1
- package/dist/data-sources/RpcDataSource.cjs +31 -51
- package/dist/data-sources/RpcDataSource.cjs.map +1 -1
- package/dist/data-sources/RpcDataSource.d.cts +4 -2
- package/dist/data-sources/RpcDataSource.d.cts.map +1 -1
- package/dist/data-sources/RpcDataSource.d.mts +4 -2
- package/dist/data-sources/RpcDataSource.d.mts.map +1 -1
- package/dist/data-sources/RpcDataSource.mjs +31 -51
- package/dist/data-sources/RpcDataSource.mjs.map +1 -1
- package/dist/data-sources/StakedBalanceDataSource.cjs +1 -27
- package/dist/data-sources/StakedBalanceDataSource.cjs.map +1 -1
- package/dist/data-sources/StakedBalanceDataSource.d.cts.map +1 -1
- package/dist/data-sources/StakedBalanceDataSource.d.mts.map +1 -1
- package/dist/data-sources/StakedBalanceDataSource.mjs +1 -27
- package/dist/data-sources/StakedBalanceDataSource.mjs.map +1 -1
- package/dist/data-sources/TokenDataSource.cjs +9 -18
- package/dist/data-sources/TokenDataSource.cjs.map +1 -1
- package/dist/data-sources/TokenDataSource.d.cts +3 -1
- package/dist/data-sources/TokenDataSource.d.cts.map +1 -1
- package/dist/data-sources/TokenDataSource.d.mts +3 -1
- package/dist/data-sources/TokenDataSource.d.mts.map +1 -1
- package/dist/data-sources/TokenDataSource.mjs +9 -18
- package/dist/data-sources/TokenDataSource.mjs.map +1 -1
- package/dist/data-sources/evm-rpc-services/clients/MulticallClient.cjs +3 -5
- package/dist/data-sources/evm-rpc-services/clients/MulticallClient.cjs.map +1 -1
- package/dist/data-sources/evm-rpc-services/clients/MulticallClient.d.cts.map +1 -1
- package/dist/data-sources/evm-rpc-services/clients/MulticallClient.d.mts.map +1 -1
- package/dist/data-sources/evm-rpc-services/clients/MulticallClient.mjs +4 -6
- package/dist/data-sources/evm-rpc-services/clients/MulticallClient.mjs.map +1 -1
- package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.cjs +103 -9
- package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.cjs.map +1 -1
- package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.d.cts +14 -5
- package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.d.cts.map +1 -1
- package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.d.mts +14 -5
- package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.d.mts.map +1 -1
- package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.mjs +103 -9
- package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.mjs.map +1 -1
- package/dist/defaults.cjs +20 -1
- package/dist/defaults.cjs.map +1 -1
- package/dist/defaults.d.cts.map +1 -1
- package/dist/defaults.d.mts.map +1 -1
- package/dist/defaults.mjs +20 -1
- package/dist/defaults.mjs.map +1 -1
- package/package.json +3 -3
|
@@ -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_fetchTokenListUncached;
|
|
13
|
+
var _TokensApiClient_instances, _TokensApiClient_fetch, _TokensApiClient_queryClient, _TokensApiClient_supportedChainIds, _TokensApiClient_supportedChainIdsCachedAt, _TokensApiClient_supportedChainIdsRefreshPromise, _TokensApiClient_isSupportedChain, _TokensApiClient_refreshSupportedChainIds, _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");
|
|
@@ -21,6 +21,19 @@ const controller_utils_1 = require("@metamask/controller-utils");
|
|
|
21
21
|
* (occurrence floors, aggregator filters, icon URLs, etc.).
|
|
22
22
|
*/
|
|
23
23
|
const TOKEN_END_POINT_API = 'https://token.api.cx.metamask.io';
|
|
24
|
+
/**
|
|
25
|
+
* Endpoint that returns which CAIP chain IDs the Tokens API supports.
|
|
26
|
+
* Used to skip detection (and avoid unnecessary network requests) for chains
|
|
27
|
+
* that are not in the supported-networks list.
|
|
28
|
+
*/
|
|
29
|
+
const SUPPORTED_NETWORKS_URL = `${TOKEN_END_POINT_API}/v2/supportedNetworks`;
|
|
30
|
+
/**
|
|
31
|
+
* How long to keep the supported-networks response in the instance-level
|
|
32
|
+
* cache before refreshing it. One hour is sufficient — the list rarely
|
|
33
|
+
* changes and a stale cache just means we may do an unnecessary token-list
|
|
34
|
+
* fetch for a newly-unsupported chain (harmless).
|
|
35
|
+
*/
|
|
36
|
+
const SUPPORTED_NETWORKS_CACHE_TTL_MS = 60 * 60000;
|
|
24
37
|
/**
|
|
25
38
|
* Tempo Mainnet — not yet present in `@metamask/controller-utils`'s `ChainId`
|
|
26
39
|
* map at the time of writing, so it's spelled out as a literal here exactly as
|
|
@@ -63,26 +76,50 @@ const TOKEN_LIST_GC_TIME_MS = 60 * 60000;
|
|
|
63
76
|
* Linea aggregator filter. This keeps RPC token detection in lockstep with
|
|
64
77
|
* the wallet's primary token list.
|
|
65
78
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
79
|
+
* Before fetching a chain's token list, the client checks
|
|
80
|
+
* `/v2/supportedNetworks` and returns `[]` immediately for chains that are
|
|
81
|
+
* not in the supported list, avoiding unnecessary API calls. The supported-
|
|
82
|
+
* networks response is cached in-memory for one hour.
|
|
83
|
+
*
|
|
84
|
+
* When constructed with a `queryClient`, token-list results are cached and
|
|
85
|
+
* deduped via TanStack Query (5 min staleTime, 1 h gcTime), so concurrent
|
|
86
|
+
* detection cycles for the same chain coalesce into a single network request.
|
|
69
87
|
*/
|
|
70
88
|
class TokensApiClient {
|
|
71
89
|
constructor(config) {
|
|
72
90
|
_TokensApiClient_instances.add(this);
|
|
73
91
|
_TokensApiClient_fetch.set(this, void 0);
|
|
74
92
|
_TokensApiClient_queryClient.set(this, void 0);
|
|
93
|
+
/** CAIP chain IDs returned by `/v2/supportedNetworks` (both tiers). */
|
|
94
|
+
_TokensApiClient_supportedChainIds.set(this, void 0);
|
|
95
|
+
/** Timestamp of the last successful `/v2/supportedNetworks` fetch. */
|
|
96
|
+
_TokensApiClient_supportedChainIdsCachedAt.set(this, 0);
|
|
97
|
+
/**
|
|
98
|
+
* In-flight `/v2/supportedNetworks` request shared across concurrent callers
|
|
99
|
+
* so only one network request is made even when multiple `fetchTokenList`
|
|
100
|
+
* calls arrive before the first one resolves.
|
|
101
|
+
*/
|
|
102
|
+
_TokensApiClient_supportedChainIdsRefreshPromise.set(this, void 0);
|
|
75
103
|
__classPrivateFieldSet(this, _TokensApiClient_fetch, config?.fetch ?? globalThis.fetch.bind(globalThis), "f");
|
|
76
104
|
__classPrivateFieldSet(this, _TokensApiClient_queryClient, config?.queryClient, "f");
|
|
77
105
|
}
|
|
78
106
|
/**
|
|
79
107
|
* Fetch the list of ERC-20 tokens for a chain from the Tokens API.
|
|
80
108
|
*
|
|
109
|
+
* Returns `[]` immediately (without hitting the token-list endpoint) when
|
|
110
|
+
* the chain is not in the `/v2/supportedNetworks` response, or when the
|
|
111
|
+
* supported-networks check fails for any reason.
|
|
112
|
+
*
|
|
81
113
|
* @param hexChainId - Chain ID in hex format (e.g. `'0x1'` for Ethereum mainnet).
|
|
82
|
-
* @returns Array of token list entries with address and metadata
|
|
83
|
-
*
|
|
114
|
+
* @returns Array of token list entries with address and metadata, or an empty
|
|
115
|
+
* array if the chain is unsupported or the request fails.
|
|
84
116
|
*/
|
|
85
117
|
async fetchTokenList(hexChainId) {
|
|
118
|
+
// Treat any error from the supported-networks check as "not supported":
|
|
119
|
+
// the token-list endpoint is only contacted for known-supported chains.
|
|
120
|
+
if (!(await __classPrivateFieldGet(this, _TokensApiClient_instances, "m", _TokensApiClient_isSupportedChain).call(this, hexChainId).catch(() => false))) {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
86
123
|
const queryClient = __classPrivateFieldGet(this, _TokensApiClient_queryClient, "f");
|
|
87
124
|
if (queryClient === undefined) {
|
|
88
125
|
return __classPrivateFieldGet(this, _TokensApiClient_instances, "m", _TokensApiClient_fetchTokenListUncached).call(this, hexChainId);
|
|
@@ -103,7 +140,56 @@ class TokensApiClient {
|
|
|
103
140
|
}
|
|
104
141
|
}
|
|
105
142
|
exports.TokensApiClient = TokensApiClient;
|
|
106
|
-
_TokensApiClient_fetch = new WeakMap(), _TokensApiClient_queryClient = new WeakMap(),
|
|
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
|
+
/**
|
|
145
|
+
* Check whether the given chain is present in the supported-networks list.
|
|
146
|
+
*
|
|
147
|
+
* Uses an instance-level cache (1 h TTL). Throws if the network request
|
|
148
|
+
* fails — callers should handle this (e.g. via `.catch(() => false)`).
|
|
149
|
+
*
|
|
150
|
+
* @param hexChainId - Hex chain ID to check.
|
|
151
|
+
* @returns `true` when the chain is in the supported-networks list.
|
|
152
|
+
*/
|
|
153
|
+
async function _TokensApiClient_isSupportedChain(hexChainId) {
|
|
154
|
+
const now = Date.now();
|
|
155
|
+
if (__classPrivateFieldGet(this, _TokensApiClient_supportedChainIds, "f") === undefined ||
|
|
156
|
+
now - __classPrivateFieldGet(this, _TokensApiClient_supportedChainIdsCachedAt, "f") >= SUPPORTED_NETWORKS_CACHE_TTL_MS) {
|
|
157
|
+
await __classPrivateFieldGet(this, _TokensApiClient_instances, "m", _TokensApiClient_refreshSupportedChainIds).call(this, now);
|
|
158
|
+
}
|
|
159
|
+
const caipChainId = `eip155:${(0, controller_utils_1.convertHexToDecimal)(hexChainId)}`;
|
|
160
|
+
return __classPrivateFieldGet(this, _TokensApiClient_supportedChainIds, "f")?.has(caipChainId) ?? false;
|
|
161
|
+
}, _TokensApiClient_refreshSupportedChainIds =
|
|
162
|
+
/**
|
|
163
|
+
* Fetch `/v2/supportedNetworks` and update the instance cache.
|
|
164
|
+
* Concurrent callers share the same in-flight Promise so only one network
|
|
165
|
+
* request is made. If the response is not 2xx the cache is left unchanged
|
|
166
|
+
* (the chain will be treated as unsupported this cycle).
|
|
167
|
+
*
|
|
168
|
+
* @param now - Current timestamp used to stamp the cache entry.
|
|
169
|
+
* @returns A promise that resolves once the cache has been refreshed.
|
|
170
|
+
*/
|
|
171
|
+
async function _TokensApiClient_refreshSupportedChainIds(now) {
|
|
172
|
+
if (__classPrivateFieldGet(this, _TokensApiClient_supportedChainIdsRefreshPromise, "f") !== undefined) {
|
|
173
|
+
return __classPrivateFieldGet(this, _TokensApiClient_supportedChainIdsRefreshPromise, "f");
|
|
174
|
+
}
|
|
175
|
+
__classPrivateFieldSet(this, _TokensApiClient_supportedChainIdsRefreshPromise, (async () => {
|
|
176
|
+
try {
|
|
177
|
+
const response = await __classPrivateFieldGet(this, _TokensApiClient_fetch, "f").call(this, SUPPORTED_NETWORKS_URL);
|
|
178
|
+
if (response.ok) {
|
|
179
|
+
const data = (await response.json());
|
|
180
|
+
__classPrivateFieldSet(this, _TokensApiClient_supportedChainIds, new Set([
|
|
181
|
+
...(data.fullSupport ?? []),
|
|
182
|
+
...(data.partialSupport ?? []),
|
|
183
|
+
]), "f");
|
|
184
|
+
__classPrivateFieldSet(this, _TokensApiClient_supportedChainIdsCachedAt, now, "f");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
__classPrivateFieldSet(this, _TokensApiClient_supportedChainIdsRefreshPromise, undefined, "f");
|
|
189
|
+
}
|
|
190
|
+
})(), "f");
|
|
191
|
+
return __classPrivateFieldGet(this, _TokensApiClient_supportedChainIdsRefreshPromise, "f");
|
|
192
|
+
}, _TokensApiClient_fetchTokenListUncached = async function _TokensApiClient_fetchTokenListUncached(hexChainId) {
|
|
107
193
|
const decimalChainId = (0, controller_utils_1.convertHexToDecimal)(hexChainId);
|
|
108
194
|
const occurrenceFloor = getOccurrenceFloor(hexChainId);
|
|
109
195
|
// Mirrors `TokenListController.getTokensURL` exactly (token-service.ts).
|
|
@@ -117,9 +203,17 @@ _TokensApiClient_fetch = new WeakMap(), _TokensApiClient_queryClient = new WeakM
|
|
|
117
203
|
`&includeERC20Permit=false` +
|
|
118
204
|
`&includeStorage=false` +
|
|
119
205
|
`&includeRwaData=true`;
|
|
120
|
-
|
|
206
|
+
let response;
|
|
207
|
+
try {
|
|
208
|
+
response = await __classPrivateFieldGet(this, _TokensApiClient_fetch, "f").call(this, url);
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
console.error(`Tokens API request failed for chain ${hexChainId}:`, error);
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
121
214
|
if (!response.ok) {
|
|
122
|
-
|
|
215
|
+
console.error(`Tokens API responded with ${response.status} for chain ${hexChainId}`);
|
|
216
|
+
return [];
|
|
123
217
|
}
|
|
124
218
|
const raw = (await response.json());
|
|
125
219
|
const items = Array.isArray(raw)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TokensApiClient.cjs","sourceRoot":"","sources":["../../../../src/data-sources/evm-rpc-services/clients/TokensApiClient.ts"],"names":[],"mappings":"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","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 * 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 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 * When constructed with a `queryClient`, results are cached and deduped via\n * TanStack Query (5 min staleTime, 1 h gcTime), so concurrent detection cycles\n * 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 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 * @param hexChainId - Chain ID in hex format (e.g. `'0x1'` for Ethereum mainnet).\n * @returns Array of token list entries with address and metadata.\n * @throws If the API responds with a non-2xx status.\n */\n async fetchTokenList(hexChainId: ChainId): Promise<TokenListEntry[]> {\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 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 const response = await this.#fetch(url);\n if (!response.ok) {\n throw new Error(\n `Tokens API responded with ${response.status} for chain ${hexChainId}`,\n );\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":"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","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"]}
|
|
@@ -32,9 +32,14 @@ export type TokensApiClientConfig = {
|
|
|
32
32
|
* Linea aggregator filter. This keeps RPC token detection in lockstep with
|
|
33
33
|
* the wallet's primary token list.
|
|
34
34
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
35
|
+
* Before fetching a chain's token list, the client checks
|
|
36
|
+
* `/v2/supportedNetworks` and returns `[]` immediately for chains that are
|
|
37
|
+
* not in the supported list, avoiding unnecessary API calls. The supported-
|
|
38
|
+
* networks response is cached in-memory for one hour.
|
|
39
|
+
*
|
|
40
|
+
* When constructed with a `queryClient`, token-list results are cached and
|
|
41
|
+
* deduped via TanStack Query (5 min staleTime, 1 h gcTime), so concurrent
|
|
42
|
+
* detection cycles for the same chain coalesce into a single network request.
|
|
38
43
|
*/
|
|
39
44
|
export declare class TokensApiClient {
|
|
40
45
|
#private;
|
|
@@ -42,9 +47,13 @@ export declare class TokensApiClient {
|
|
|
42
47
|
/**
|
|
43
48
|
* Fetch the list of ERC-20 tokens for a chain from the Tokens API.
|
|
44
49
|
*
|
|
50
|
+
* Returns `[]` immediately (without hitting the token-list endpoint) when
|
|
51
|
+
* the chain is not in the `/v2/supportedNetworks` response, or when the
|
|
52
|
+
* supported-networks check fails for any reason.
|
|
53
|
+
*
|
|
45
54
|
* @param hexChainId - Chain ID in hex format (e.g. `'0x1'` for Ethereum mainnet).
|
|
46
|
-
* @returns Array of token list entries with address and metadata
|
|
47
|
-
*
|
|
55
|
+
* @returns Array of token list entries with address and metadata, or an empty
|
|
56
|
+
* array if the chain is unsupported or the request fails.
|
|
48
57
|
*/
|
|
49
58
|
fetchTokenList(hexChainId: ChainId): Promise<TokenListEntry[]>;
|
|
50
59
|
}
|
|
@@ -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;
|
|
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"}
|
|
@@ -32,9 +32,14 @@ export type TokensApiClientConfig = {
|
|
|
32
32
|
* Linea aggregator filter. This keeps RPC token detection in lockstep with
|
|
33
33
|
* the wallet's primary token list.
|
|
34
34
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
35
|
+
* Before fetching a chain's token list, the client checks
|
|
36
|
+
* `/v2/supportedNetworks` and returns `[]` immediately for chains that are
|
|
37
|
+
* not in the supported list, avoiding unnecessary API calls. The supported-
|
|
38
|
+
* networks response is cached in-memory for one hour.
|
|
39
|
+
*
|
|
40
|
+
* When constructed with a `queryClient`, token-list results are cached and
|
|
41
|
+
* deduped via TanStack Query (5 min staleTime, 1 h gcTime), so concurrent
|
|
42
|
+
* detection cycles for the same chain coalesce into a single network request.
|
|
38
43
|
*/
|
|
39
44
|
export declare class TokensApiClient {
|
|
40
45
|
#private;
|
|
@@ -42,9 +47,13 @@ export declare class TokensApiClient {
|
|
|
42
47
|
/**
|
|
43
48
|
* Fetch the list of ERC-20 tokens for a chain from the Tokens API.
|
|
44
49
|
*
|
|
50
|
+
* Returns `[]` immediately (without hitting the token-list endpoint) when
|
|
51
|
+
* the chain is not in the `/v2/supportedNetworks` response, or when the
|
|
52
|
+
* supported-networks check fails for any reason.
|
|
53
|
+
*
|
|
45
54
|
* @param hexChainId - Chain ID in hex format (e.g. `'0x1'` for Ethereum mainnet).
|
|
46
|
-
* @returns Array of token list entries with address and metadata
|
|
47
|
-
*
|
|
55
|
+
* @returns Array of token list entries with address and metadata, or an empty
|
|
56
|
+
* array if the chain is unsupported or the request fails.
|
|
48
57
|
*/
|
|
49
58
|
fetchTokenList(hexChainId: ChainId): Promise<TokenListEntry[]>;
|
|
50
59
|
}
|
|
@@ -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;
|
|
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"}
|
|
@@ -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 _TokensApiClient_instances, _TokensApiClient_fetch, _TokensApiClient_queryClient, _TokensApiClient_fetchTokenListUncached;
|
|
12
|
+
var _TokensApiClient_instances, _TokensApiClient_fetch, _TokensApiClient_queryClient, _TokensApiClient_supportedChainIds, _TokensApiClient_supportedChainIdsCachedAt, _TokensApiClient_supportedChainIdsRefreshPromise, _TokensApiClient_isSupportedChain, _TokensApiClient_refreshSupportedChainIds, _TokensApiClient_fetchTokenListUncached;
|
|
13
13
|
import { ChainId as ControllerChainId, convertHexToDecimal } from "@metamask/controller-utils";
|
|
14
14
|
/**
|
|
15
15
|
* Same host + path that `TokenListController` (assets-controllers) uses
|
|
@@ -18,6 +18,19 @@ import { ChainId as ControllerChainId, convertHexToDecimal } from "@metamask/con
|
|
|
18
18
|
* (occurrence floors, aggregator filters, icon URLs, etc.).
|
|
19
19
|
*/
|
|
20
20
|
const TOKEN_END_POINT_API = 'https://token.api.cx.metamask.io';
|
|
21
|
+
/**
|
|
22
|
+
* Endpoint that returns which CAIP chain IDs the Tokens API supports.
|
|
23
|
+
* Used to skip detection (and avoid unnecessary network requests) for chains
|
|
24
|
+
* that are not in the supported-networks list.
|
|
25
|
+
*/
|
|
26
|
+
const SUPPORTED_NETWORKS_URL = `${TOKEN_END_POINT_API}/v2/supportedNetworks`;
|
|
27
|
+
/**
|
|
28
|
+
* How long to keep the supported-networks response in the instance-level
|
|
29
|
+
* cache before refreshing it. One hour is sufficient — the list rarely
|
|
30
|
+
* changes and a stale cache just means we may do an unnecessary token-list
|
|
31
|
+
* fetch for a newly-unsupported chain (harmless).
|
|
32
|
+
*/
|
|
33
|
+
const SUPPORTED_NETWORKS_CACHE_TTL_MS = 60 * 60000;
|
|
21
34
|
/**
|
|
22
35
|
* Tempo Mainnet — not yet present in `@metamask/controller-utils`'s `ChainId`
|
|
23
36
|
* map at the time of writing, so it's spelled out as a literal here exactly as
|
|
@@ -60,26 +73,50 @@ const TOKEN_LIST_GC_TIME_MS = 60 * 60000;
|
|
|
60
73
|
* Linea aggregator filter. This keeps RPC token detection in lockstep with
|
|
61
74
|
* the wallet's primary token list.
|
|
62
75
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
76
|
+
* Before fetching a chain's token list, the client checks
|
|
77
|
+
* `/v2/supportedNetworks` and returns `[]` immediately for chains that are
|
|
78
|
+
* not in the supported list, avoiding unnecessary API calls. The supported-
|
|
79
|
+
* networks response is cached in-memory for one hour.
|
|
80
|
+
*
|
|
81
|
+
* When constructed with a `queryClient`, token-list results are cached and
|
|
82
|
+
* deduped via TanStack Query (5 min staleTime, 1 h gcTime), so concurrent
|
|
83
|
+
* detection cycles for the same chain coalesce into a single network request.
|
|
66
84
|
*/
|
|
67
85
|
export class TokensApiClient {
|
|
68
86
|
constructor(config) {
|
|
69
87
|
_TokensApiClient_instances.add(this);
|
|
70
88
|
_TokensApiClient_fetch.set(this, void 0);
|
|
71
89
|
_TokensApiClient_queryClient.set(this, void 0);
|
|
90
|
+
/** CAIP chain IDs returned by `/v2/supportedNetworks` (both tiers). */
|
|
91
|
+
_TokensApiClient_supportedChainIds.set(this, void 0);
|
|
92
|
+
/** Timestamp of the last successful `/v2/supportedNetworks` fetch. */
|
|
93
|
+
_TokensApiClient_supportedChainIdsCachedAt.set(this, 0);
|
|
94
|
+
/**
|
|
95
|
+
* In-flight `/v2/supportedNetworks` request shared across concurrent callers
|
|
96
|
+
* so only one network request is made even when multiple `fetchTokenList`
|
|
97
|
+
* calls arrive before the first one resolves.
|
|
98
|
+
*/
|
|
99
|
+
_TokensApiClient_supportedChainIdsRefreshPromise.set(this, void 0);
|
|
72
100
|
__classPrivateFieldSet(this, _TokensApiClient_fetch, config?.fetch ?? globalThis.fetch.bind(globalThis), "f");
|
|
73
101
|
__classPrivateFieldSet(this, _TokensApiClient_queryClient, config?.queryClient, "f");
|
|
74
102
|
}
|
|
75
103
|
/**
|
|
76
104
|
* Fetch the list of ERC-20 tokens for a chain from the Tokens API.
|
|
77
105
|
*
|
|
106
|
+
* Returns `[]` immediately (without hitting the token-list endpoint) when
|
|
107
|
+
* the chain is not in the `/v2/supportedNetworks` response, or when the
|
|
108
|
+
* supported-networks check fails for any reason.
|
|
109
|
+
*
|
|
78
110
|
* @param hexChainId - Chain ID in hex format (e.g. `'0x1'` for Ethereum mainnet).
|
|
79
|
-
* @returns Array of token list entries with address and metadata
|
|
80
|
-
*
|
|
111
|
+
* @returns Array of token list entries with address and metadata, or an empty
|
|
112
|
+
* array if the chain is unsupported or the request fails.
|
|
81
113
|
*/
|
|
82
114
|
async fetchTokenList(hexChainId) {
|
|
115
|
+
// Treat any error from the supported-networks check as "not supported":
|
|
116
|
+
// the token-list endpoint is only contacted for known-supported chains.
|
|
117
|
+
if (!(await __classPrivateFieldGet(this, _TokensApiClient_instances, "m", _TokensApiClient_isSupportedChain).call(this, hexChainId).catch(() => false))) {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
83
120
|
const queryClient = __classPrivateFieldGet(this, _TokensApiClient_queryClient, "f");
|
|
84
121
|
if (queryClient === undefined) {
|
|
85
122
|
return __classPrivateFieldGet(this, _TokensApiClient_instances, "m", _TokensApiClient_fetchTokenListUncached).call(this, hexChainId);
|
|
@@ -99,7 +136,56 @@ export class TokensApiClient {
|
|
|
99
136
|
});
|
|
100
137
|
}
|
|
101
138
|
}
|
|
102
|
-
_TokensApiClient_fetch = new WeakMap(), _TokensApiClient_queryClient = new WeakMap(),
|
|
139
|
+
_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 =
|
|
140
|
+
/**
|
|
141
|
+
* Check whether the given chain is present in the supported-networks list.
|
|
142
|
+
*
|
|
143
|
+
* Uses an instance-level cache (1 h TTL). Throws if the network request
|
|
144
|
+
* fails — callers should handle this (e.g. via `.catch(() => false)`).
|
|
145
|
+
*
|
|
146
|
+
* @param hexChainId - Hex chain ID to check.
|
|
147
|
+
* @returns `true` when the chain is in the supported-networks list.
|
|
148
|
+
*/
|
|
149
|
+
async function _TokensApiClient_isSupportedChain(hexChainId) {
|
|
150
|
+
const now = Date.now();
|
|
151
|
+
if (__classPrivateFieldGet(this, _TokensApiClient_supportedChainIds, "f") === undefined ||
|
|
152
|
+
now - __classPrivateFieldGet(this, _TokensApiClient_supportedChainIdsCachedAt, "f") >= SUPPORTED_NETWORKS_CACHE_TTL_MS) {
|
|
153
|
+
await __classPrivateFieldGet(this, _TokensApiClient_instances, "m", _TokensApiClient_refreshSupportedChainIds).call(this, now);
|
|
154
|
+
}
|
|
155
|
+
const caipChainId = `eip155:${convertHexToDecimal(hexChainId)}`;
|
|
156
|
+
return __classPrivateFieldGet(this, _TokensApiClient_supportedChainIds, "f")?.has(caipChainId) ?? false;
|
|
157
|
+
}, _TokensApiClient_refreshSupportedChainIds =
|
|
158
|
+
/**
|
|
159
|
+
* Fetch `/v2/supportedNetworks` and update the instance cache.
|
|
160
|
+
* Concurrent callers share the same in-flight Promise so only one network
|
|
161
|
+
* request is made. If the response is not 2xx the cache is left unchanged
|
|
162
|
+
* (the chain will be treated as unsupported this cycle).
|
|
163
|
+
*
|
|
164
|
+
* @param now - Current timestamp used to stamp the cache entry.
|
|
165
|
+
* @returns A promise that resolves once the cache has been refreshed.
|
|
166
|
+
*/
|
|
167
|
+
async function _TokensApiClient_refreshSupportedChainIds(now) {
|
|
168
|
+
if (__classPrivateFieldGet(this, _TokensApiClient_supportedChainIdsRefreshPromise, "f") !== undefined) {
|
|
169
|
+
return __classPrivateFieldGet(this, _TokensApiClient_supportedChainIdsRefreshPromise, "f");
|
|
170
|
+
}
|
|
171
|
+
__classPrivateFieldSet(this, _TokensApiClient_supportedChainIdsRefreshPromise, (async () => {
|
|
172
|
+
try {
|
|
173
|
+
const response = await __classPrivateFieldGet(this, _TokensApiClient_fetch, "f").call(this, SUPPORTED_NETWORKS_URL);
|
|
174
|
+
if (response.ok) {
|
|
175
|
+
const data = (await response.json());
|
|
176
|
+
__classPrivateFieldSet(this, _TokensApiClient_supportedChainIds, new Set([
|
|
177
|
+
...(data.fullSupport ?? []),
|
|
178
|
+
...(data.partialSupport ?? []),
|
|
179
|
+
]), "f");
|
|
180
|
+
__classPrivateFieldSet(this, _TokensApiClient_supportedChainIdsCachedAt, now, "f");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
__classPrivateFieldSet(this, _TokensApiClient_supportedChainIdsRefreshPromise, undefined, "f");
|
|
185
|
+
}
|
|
186
|
+
})(), "f");
|
|
187
|
+
return __classPrivateFieldGet(this, _TokensApiClient_supportedChainIdsRefreshPromise, "f");
|
|
188
|
+
}, _TokensApiClient_fetchTokenListUncached = async function _TokensApiClient_fetchTokenListUncached(hexChainId) {
|
|
103
189
|
const decimalChainId = convertHexToDecimal(hexChainId);
|
|
104
190
|
const occurrenceFloor = getOccurrenceFloor(hexChainId);
|
|
105
191
|
// Mirrors `TokenListController.getTokensURL` exactly (token-service.ts).
|
|
@@ -113,9 +199,17 @@ _TokensApiClient_fetch = new WeakMap(), _TokensApiClient_queryClient = new WeakM
|
|
|
113
199
|
`&includeERC20Permit=false` +
|
|
114
200
|
`&includeStorage=false` +
|
|
115
201
|
`&includeRwaData=true`;
|
|
116
|
-
|
|
202
|
+
let response;
|
|
203
|
+
try {
|
|
204
|
+
response = await __classPrivateFieldGet(this, _TokensApiClient_fetch, "f").call(this, url);
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
console.error(`Tokens API request failed for chain ${hexChainId}:`, error);
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
117
210
|
if (!response.ok) {
|
|
118
|
-
|
|
211
|
+
console.error(`Tokens API responded with ${response.status} for chain ${hexChainId}`);
|
|
212
|
+
return [];
|
|
119
213
|
}
|
|
120
214
|
const raw = (await response.json());
|
|
121
215
|
const items = Array.isArray(raw)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TokensApiClient.mjs","sourceRoot":"","sources":["../../../../src/data-sources/evm-rpc-services/clients/TokensApiClient.ts"],"names":[],"mappings":"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","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 * 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 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 * When constructed with a `queryClient`, results are cached and deduped via\n * TanStack Query (5 min staleTime, 1 h gcTime), so concurrent detection cycles\n * 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 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 * @param hexChainId - Chain ID in hex format (e.g. `'0x1'` for Ethereum mainnet).\n * @returns Array of token list entries with address and metadata.\n * @throws If the API responds with a non-2xx status.\n */\n async fetchTokenList(hexChainId: ChainId): Promise<TokenListEntry[]> {\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 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 const response = await this.#fetch(url);\n if (!response.ok) {\n throw new Error(\n `Tokens API responded with ${response.status} for chain ${hexChainId}`,\n );\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.mjs","sourceRoot":"","sources":["../../../../src/data-sources/evm-rpc-services/clients/TokensApiClient.ts"],"names":[],"mappings":"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","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"]}
|
package/dist/defaults.cjs
CHANGED
|
@@ -25,6 +25,20 @@ const MUSD_METADATA = {
|
|
|
25
25
|
name: 'MetaMask USD',
|
|
26
26
|
decimals: 6,
|
|
27
27
|
};
|
|
28
|
+
/**
|
|
29
|
+
* Harcoded metadata for USDC on Arc (5042/0x13b2).
|
|
30
|
+
* USDC exists as both native (18 decimals) and ERC20 (6 decimals).
|
|
31
|
+
* We choose to force-hide the native version to avoid double listing using
|
|
32
|
+
* `CHAIN_IDS_WITH_NO_NATIVE_TOKEN`.
|
|
33
|
+
* In the meantime, the code below force-shows USDC the ERC20 token.
|
|
34
|
+
*/
|
|
35
|
+
const USDC_ON_ARC_ASSET_ID = 'eip155:5042/erc20:0x3600000000000000000000000000000000000000';
|
|
36
|
+
const USDC_ON_ARC_METADATA = {
|
|
37
|
+
type: 'erc20',
|
|
38
|
+
symbol: 'USDC',
|
|
39
|
+
name: 'USDC',
|
|
40
|
+
decimals: 6,
|
|
41
|
+
};
|
|
28
42
|
/**
|
|
29
43
|
* Build the CAIP-19 asset ID for the mUSD ERC-20 token on a given EVM
|
|
30
44
|
* chain.
|
|
@@ -49,6 +63,7 @@ exports.DEFAULT_TRACKED_ASSETS_BY_CHAIN = new Map([
|
|
|
49
63
|
['eip155:1', [musdAssetId('eip155:1')]],
|
|
50
64
|
['eip155:59144', [musdAssetId('eip155:59144')]],
|
|
51
65
|
['eip155:143', [musdAssetId('eip155:143')]],
|
|
66
|
+
['eip155:5042', [USDC_ON_ARC_ASSET_ID]],
|
|
52
67
|
]);
|
|
53
68
|
/**
|
|
54
69
|
* Chains that have at least one default tracked asset. Useful for
|
|
@@ -64,6 +79,7 @@ exports.DEFAULT_ASSET_METADATA = new Map([
|
|
|
64
79
|
[musdAssetId('eip155:1'), MUSD_METADATA],
|
|
65
80
|
[musdAssetId('eip155:59144'), MUSD_METADATA],
|
|
66
81
|
[musdAssetId('eip155:143'), MUSD_METADATA],
|
|
82
|
+
[USDC_ON_ARC_ASSET_ID, USDC_ON_ARC_METADATA],
|
|
67
83
|
]);
|
|
68
84
|
/**
|
|
69
85
|
* Return the default tracked assets for a CAIP-2 chain id. Empty when
|
|
@@ -100,7 +116,10 @@ function buildDefaultAssetsInfo() {
|
|
|
100
116
|
const info = {};
|
|
101
117
|
for (const assetIds of exports.DEFAULT_TRACKED_ASSETS_BY_CHAIN.values()) {
|
|
102
118
|
for (const assetId of assetIds) {
|
|
103
|
-
|
|
119
|
+
const metadata = exports.DEFAULT_ASSET_METADATA.get(assetId);
|
|
120
|
+
if (metadata) {
|
|
121
|
+
info[assetId] = metadata;
|
|
122
|
+
}
|
|
104
123
|
}
|
|
105
124
|
}
|
|
106
125
|
return info;
|