@metamask/assets-controllers 97.0.0 → 99.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.
@@ -4,12 +4,17 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
4
4
  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");
5
5
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
6
6
  };
7
- var _TokenListController_instances, _TokenListController_onNetworkControllerStateChange, _TokenListController_startDeprecatedPolling;
7
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
8
+ if (kind === "m") throw new TypeError("Private method is not writable");
9
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
10
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
11
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
12
+ };
13
+ var _TokenListController_instances, _a, _TokenListController_persistDebounceTimer, _TokenListController_persistInFlightPromise, _TokenListController_changedChainsToPersist, _TokenListController_chainsLoadedFromStorage, _TokenListController_previousTokensChainsCache, _TokenListController_persistDebounceMs, _TokenListController_storageKeyPrefix, _TokenListController_getChainStorageKey, _TokenListController_intervalId, _TokenListController_intervalDelay, _TokenListController_cacheRefreshThreshold, _TokenListController_chainId, _TokenListController_abortController, _TokenListController_onCacheChanged, _TokenListController_debouncePersist, _TokenListController_persistChangedChains, _TokenListController_synchronizeCacheWithStorage, _TokenListController_saveChainCacheToStorage, _TokenListController_onNetworkControllerStateChange, _TokenListController_stopPolling, _TokenListController_startDeprecatedPolling;
8
14
  Object.defineProperty(exports, "__esModule", { value: true });
9
15
  exports.TokenListController = exports.getDefaultTokenListState = void 0;
10
16
  const controller_utils_1 = require("@metamask/controller-utils");
11
17
  const polling_controller_1 = require("@metamask/polling-controller");
12
- const async_mutex_1 = require("async-mutex");
13
18
  const assetsUtil_1 = require("./assetsUtil.cjs");
14
19
  const token_service_1 = require("./token-service.cjs");
15
20
  // 4 Hour Interval Cache Refresh Threshold
@@ -19,21 +24,14 @@ const name = 'TokenListController';
19
24
  const metadata = {
20
25
  tokensChainsCache: {
21
26
  includeInStateLogs: false,
22
- persist: true,
27
+ persist: false, // Persisted separately via StorageService
23
28
  includeInDebugSnapshot: true,
24
29
  usedInUi: true,
25
30
  },
26
- preventPollingOnNetworkRestart: {
27
- includeInStateLogs: false,
28
- persist: true,
29
- includeInDebugSnapshot: true,
30
- usedInUi: false,
31
- },
32
31
  };
33
32
  const getDefaultTokenListState = () => {
34
33
  return {
35
34
  tokensChainsCache: {},
36
- preventPollingOnNetworkRestart: false,
37
35
  };
38
36
  };
39
37
  exports.getDefaultTokenListState = getDefaultTokenListState;
@@ -51,9 +49,8 @@ class TokenListController extends (0, polling_controller_1.StaticIntervalPolling
51
49
  * @param options.cacheRefreshThreshold - The token cache expiry time, in milliseconds.
52
50
  * @param options.messenger - A restricted messenger.
53
51
  * @param options.state - Initial state to set on this controller.
54
- * @param options.preventPollingOnNetworkRestart - Determines whether to prevent poilling on network restart in extension.
55
52
  */
56
- constructor({ chainId, preventPollingOnNetworkRestart = false, onNetworkStateChange, interval = DEFAULT_INTERVAL, cacheRefreshThreshold = DEFAULT_THRESHOLD, messenger, state, }) {
53
+ constructor({ chainId, onNetworkStateChange, interval = DEFAULT_INTERVAL, cacheRefreshThreshold = DEFAULT_THRESHOLD, messenger, state, }) {
57
54
  super({
58
55
  name,
59
56
  metadata,
@@ -61,13 +58,43 @@ class TokenListController extends (0, polling_controller_1.StaticIntervalPolling
61
58
  state: { ...(0, exports.getDefaultTokenListState)(), ...state },
62
59
  });
63
60
  _TokenListController_instances.add(this);
64
- this.mutex = new async_mutex_1.Mutex();
65
- this.intervalDelay = interval;
61
+ /**
62
+ * Debounce timer for persisting state changes to storage.
63
+ */
64
+ _TokenListController_persistDebounceTimer.set(this, void 0);
65
+ /**
66
+ * Promise that resolves when the current persist operation completes.
67
+ * Used to prevent race conditions between persist operations.
68
+ */
69
+ _TokenListController_persistInFlightPromise.set(this, void 0);
70
+ /**
71
+ * Tracks which chains have pending changes to persist.
72
+ * Only changed chains are persisted to reduce write amplification.
73
+ */
74
+ _TokenListController_changedChainsToPersist.set(this, new Set());
75
+ /**
76
+ * Tracks chains that were just loaded from storage and should skip
77
+ * the next persistence cycle. This prevents redundant writes where
78
+ * data loaded from storage would be immediately written back.
79
+ * Chains are removed from this set after being skipped once.
80
+ */
81
+ _TokenListController_chainsLoadedFromStorage.set(this, new Set());
82
+ /**
83
+ * Previous tokensChainsCache for detecting which chains changed.
84
+ */
85
+ _TokenListController_previousTokensChainsCache.set(this, {});
86
+ _TokenListController_intervalId.set(this, void 0);
87
+ _TokenListController_intervalDelay.set(this, void 0);
88
+ _TokenListController_cacheRefreshThreshold.set(this, void 0);
89
+ _TokenListController_chainId.set(this, void 0);
90
+ _TokenListController_abortController.set(this, void 0);
91
+ __classPrivateFieldSet(this, _TokenListController_intervalDelay, interval, "f");
66
92
  this.setIntervalLength(interval);
67
- this.cacheRefreshThreshold = cacheRefreshThreshold;
68
- this.chainId = chainId;
69
- this.updatePreventPollingOnNetworkRestart(preventPollingOnNetworkRestart);
70
- this.abortController = new AbortController();
93
+ __classPrivateFieldSet(this, _TokenListController_cacheRefreshThreshold, cacheRefreshThreshold, "f");
94
+ __classPrivateFieldSet(this, _TokenListController_chainId, chainId, "f");
95
+ __classPrivateFieldSet(this, _TokenListController_abortController, new AbortController(), "f");
96
+ // Subscribe to state changes to automatically persist tokensChainsCache
97
+ this.messenger.subscribe('TokenListController:stateChange', (newCache) => __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_onCacheChanged).call(this, newCache), (controllerState) => controllerState.tokensChainsCache);
71
98
  if (onNetworkStateChange) {
72
99
  // TODO: Either fix this lint violation or explain why it's necessary to ignore.
73
100
  // eslint-disable-next-line @typescript-eslint/no-misused-promises
@@ -84,6 +111,15 @@ class TokenListController extends (0, polling_controller_1.StaticIntervalPolling
84
111
  });
85
112
  }
86
113
  }
114
+ /**
115
+ * Initialize the controller by loading cache from storage and running migration.
116
+ * This method should be called by clients after construction.
117
+ *
118
+ * @returns A promise that resolves when initialization is complete.
119
+ */
120
+ async initialize() {
121
+ await __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_synchronizeCacheWithStorage).call(this);
122
+ }
87
123
  // Eventually we want to remove start/restart/stop controls in favor of new _executePoll API
88
124
  // Maintaining these functions for now until we can safely deprecate them for backwards compatibility
89
125
  /**
@@ -93,7 +129,7 @@ class TokenListController extends (0, polling_controller_1.StaticIntervalPolling
93
129
  * Consider using the new polling approach instead
94
130
  */
95
131
  async start() {
96
- if (!(0, assetsUtil_1.isTokenListSupportedForNetwork)(this.chainId)) {
132
+ if (!(0, assetsUtil_1.isTokenListSupportedForNetwork)(__classPrivateFieldGet(this, _TokenListController_chainId, "f"))) {
97
133
  return;
98
134
  }
99
135
  await __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_startDeprecatedPolling).call(this);
@@ -105,7 +141,7 @@ class TokenListController extends (0, polling_controller_1.StaticIntervalPolling
105
141
  * Consider using the new polling approach instead
106
142
  */
107
143
  async restart() {
108
- this.stopPolling();
144
+ __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_stopPolling).call(this);
109
145
  await __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_startDeprecatedPolling).call(this);
110
146
  }
111
147
  /**
@@ -115,7 +151,7 @@ class TokenListController extends (0, polling_controller_1.StaticIntervalPolling
115
151
  * Consider using the new polling approach instead
116
152
  */
117
153
  stop() {
118
- this.stopPolling();
154
+ __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_stopPolling).call(this);
119
155
  }
120
156
  /**
121
157
  * This stops any active polling.
@@ -125,18 +161,14 @@ class TokenListController extends (0, polling_controller_1.StaticIntervalPolling
125
161
  */
126
162
  destroy() {
127
163
  super.destroy();
128
- this.stopPolling();
129
- }
130
- /**
131
- * This stops any active polling intervals.
132
- *
133
- * @deprecated This method is deprecated and will be removed in the future.
134
- * Consider using the new polling approach instead
135
- */
136
- stopPolling() {
137
- if (this.intervalId) {
138
- clearInterval(this.intervalId);
164
+ __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_stopPolling).call(this);
165
+ // Cancel any pending debounced persistence operations
166
+ if (__classPrivateFieldGet(this, _TokenListController_persistDebounceTimer, "f")) {
167
+ clearTimeout(__classPrivateFieldGet(this, _TokenListController_persistDebounceTimer, "f"));
168
+ __classPrivateFieldSet(this, _TokenListController_persistDebounceTimer, undefined, "f");
139
169
  }
170
+ __classPrivateFieldGet(this, _TokenListController_changedChainsToPersist, "f").clear();
171
+ __classPrivateFieldGet(this, _TokenListController_chainsLoadedFromStorage, "f").clear();
140
172
  }
141
173
  /**
142
174
  * This starts a new polling loop for any given chain. Under the hood it is deduping polls
@@ -149,88 +181,232 @@ class TokenListController extends (0, polling_controller_1.StaticIntervalPolling
149
181
  return this.fetchTokenList(chainId);
150
182
  }
151
183
  /**
152
- * Fetching token list from the Token Service API. This will fetch tokens across chains. It will update tokensChainsCache (scoped across chains), and also the tokenList (scoped for the selected chain)
184
+ * Fetching token list from the Token Service API. This will fetch tokens across chains.
185
+ * State changes are automatically persisted via the stateChange subscription.
153
186
  *
154
187
  * @param chainId - The chainId of the current chain triggering the fetch.
155
188
  */
156
189
  async fetchTokenList(chainId) {
157
- const releaseLock = await this.mutex.acquire();
158
- try {
159
- if (this.isCacheValid(chainId)) {
160
- return;
161
- }
162
- // Fetch fresh token list from the API
163
- const tokensFromAPI = await (0, controller_utils_1.safelyExecute)(() => (0, token_service_1.fetchTokenListByChainId)(chainId, this.abortController.signal));
164
- // Have response - process and update list
165
- if (tokensFromAPI) {
166
- // Format tokens from API (HTTP) and update tokenList
167
- const tokenList = {};
168
- for (const token of tokensFromAPI) {
169
- tokenList[token.address] = {
170
- ...token,
171
- aggregators: (0, assetsUtil_1.formatAggregatorNames)(token.aggregators),
172
- iconUrl: (0, assetsUtil_1.formatIconUrlWithProxy)({
173
- chainId,
174
- tokenAddress: token.address,
175
- }),
176
- };
177
- }
178
- this.update((state) => {
179
- var _a;
180
- const newDataCache = { data: {}, timestamp: Date.now() };
181
- (_a = state.tokensChainsCache)[chainId] ?? (_a[chainId] = newDataCache);
182
- state.tokensChainsCache[chainId].data = tokenList;
183
- state.tokensChainsCache[chainId].timestamp = Date.now();
184
- });
185
- return;
190
+ if (this.isCacheValid(chainId)) {
191
+ return;
192
+ }
193
+ // Fetch fresh token list from the API
194
+ const tokensFromAPI = await (0, controller_utils_1.safelyExecute)(() => (0, token_service_1.fetchTokenListByChainId)(chainId, __classPrivateFieldGet(this, _TokenListController_abortController, "f").signal));
195
+ // Have response - process and update list
196
+ if (tokensFromAPI) {
197
+ // Format tokens from API (HTTP) and update tokenList
198
+ const tokenList = {};
199
+ for (const token of tokensFromAPI) {
200
+ tokenList[token.address] = {
201
+ ...token,
202
+ aggregators: (0, assetsUtil_1.formatAggregatorNames)(token.aggregators),
203
+ iconUrl: (0, assetsUtil_1.formatIconUrlWithProxy)({
204
+ chainId,
205
+ tokenAddress: token.address,
206
+ }),
207
+ };
186
208
  }
187
- // No response - fallback to previous state, or initialise empty
188
- if (!tokensFromAPI) {
209
+ // Update state - persistence happens automatically via subscription
210
+ const newDataCache = {
211
+ data: tokenList,
212
+ timestamp: Date.now(),
213
+ };
214
+ this.update((state) => {
215
+ state.tokensChainsCache[chainId] = newDataCache;
216
+ });
217
+ return;
218
+ }
219
+ // No response - fallback to previous state, or initialise empty.
220
+ // Only initialize with a new timestamp if there's no existing cache.
221
+ // If there's existing cache, keep it as-is without updating the timestamp
222
+ // to avoid making stale data appear "fresh" and preventing retry attempts.
223
+ if (!tokensFromAPI) {
224
+ const existingCache = this.state.tokensChainsCache[chainId];
225
+ if (!existingCache) {
226
+ // No existing cache - initialize empty (persistence happens automatically)
227
+ const newDataCache = { data: {}, timestamp: Date.now() };
189
228
  this.update((state) => {
190
- var _a;
191
- const newDataCache = { data: {}, timestamp: Date.now() };
192
- (_a = state.tokensChainsCache)[chainId] ?? (_a[chainId] = newDataCache);
193
- state.tokensChainsCache[chainId].timestamp = Date.now();
229
+ state.tokensChainsCache[chainId] = newDataCache;
194
230
  });
195
231
  }
196
- }
197
- finally {
198
- releaseLock();
232
+ // If there's existing cache, keep it as-is (don't update timestamp or persist)
199
233
  }
200
234
  }
201
235
  isCacheValid(chainId) {
202
236
  const { tokensChainsCache } = this.state;
203
237
  const timestamp = tokensChainsCache[chainId]?.timestamp;
204
238
  const now = Date.now();
205
- return (timestamp !== undefined && now - timestamp < this.cacheRefreshThreshold);
239
+ return (timestamp !== undefined && now - timestamp < __classPrivateFieldGet(this, _TokenListController_cacheRefreshThreshold, "f"));
206
240
  }
207
- /**
208
- * Clearing tokenList and tokensChainsCache explicitly.
209
- */
210
- clearingTokenListData() {
211
- this.update(() => {
212
- return {
213
- ...this.state,
214
- tokensChainsCache: {},
215
- };
216
- });
241
+ }
242
+ exports.TokenListController = TokenListController;
243
+ _a = TokenListController, _TokenListController_persistDebounceTimer = new WeakMap(), _TokenListController_persistInFlightPromise = new WeakMap(), _TokenListController_changedChainsToPersist = new WeakMap(), _TokenListController_chainsLoadedFromStorage = new WeakMap(), _TokenListController_previousTokensChainsCache = new WeakMap(), _TokenListController_intervalId = new WeakMap(), _TokenListController_intervalDelay = new WeakMap(), _TokenListController_cacheRefreshThreshold = new WeakMap(), _TokenListController_chainId = new WeakMap(), _TokenListController_abortController = new WeakMap(), _TokenListController_instances = new WeakSet(), _TokenListController_getChainStorageKey = function _TokenListController_getChainStorageKey(chainId) {
244
+ return `${__classPrivateFieldGet(_a, _a, "f", _TokenListController_storageKeyPrefix)}:${chainId}`;
245
+ }, _TokenListController_onCacheChanged = function _TokenListController_onCacheChanged(newCache) {
246
+ // Detect which chains changed by comparing with previous cache
247
+ for (const chainId of Object.keys(newCache)) {
248
+ const newData = newCache[chainId];
249
+ const prevData = __classPrivateFieldGet(this, _TokenListController_previousTokensChainsCache, "f")[chainId];
250
+ // Chain is new or timestamp changed (indicating data update)
251
+ if (!prevData || prevData.timestamp !== newData.timestamp) {
252
+ // Skip persistence for chains that were just loaded from storage
253
+ // (they don't need to be written back immediately)
254
+ if (__classPrivateFieldGet(this, _TokenListController_chainsLoadedFromStorage, "f").has(chainId)) {
255
+ __classPrivateFieldGet(this, _TokenListController_chainsLoadedFromStorage, "f").delete(chainId); // Clean up - future updates should persist
256
+ }
257
+ else {
258
+ __classPrivateFieldGet(this, _TokenListController_changedChainsToPersist, "f").add(chainId);
259
+ }
260
+ }
217
261
  }
218
- /**
219
- * Updates preventPollingOnNetworkRestart from extension.
220
- *
221
- * @param shouldPreventPolling - Determine whether to prevent polling on network change
222
- */
223
- updatePreventPollingOnNetworkRestart(shouldPreventPolling) {
224
- this.update(() => {
225
- return {
226
- ...this.state,
227
- preventPollingOnNetworkRestart: shouldPreventPolling,
228
- };
262
+ // Update previous cache reference
263
+ __classPrivateFieldSet(this, _TokenListController_previousTokensChainsCache, { ...newCache }, "f");
264
+ // Schedule persistence if there are changes
265
+ if (__classPrivateFieldGet(this, _TokenListController_changedChainsToPersist, "f").size > 0) {
266
+ __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_debouncePersist).call(this);
267
+ }
268
+ }, _TokenListController_debouncePersist = function _TokenListController_debouncePersist() {
269
+ if (__classPrivateFieldGet(this, _TokenListController_persistDebounceTimer, "f")) {
270
+ clearTimeout(__classPrivateFieldGet(this, _TokenListController_persistDebounceTimer, "f"));
271
+ }
272
+ __classPrivateFieldSet(this, _TokenListController_persistDebounceTimer, setTimeout(() => {
273
+ // Note: #persistChangedChains handles errors internally via #saveChainCacheToStorage,
274
+ // so this promise will not reject.
275
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
276
+ __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_persistChangedChains).call(this);
277
+ }, __classPrivateFieldGet(_a, _a, "f", _TokenListController_persistDebounceMs)), "f");
278
+ }, _TokenListController_persistChangedChains =
279
+ /**
280
+ * Persist only the chains that have changed to storage.
281
+ * Reduces write amplification by skipping unchanged chains.
282
+ *
283
+ * If a persist operation is already in-flight, this method returns early
284
+ * and reschedules the debounce to ensure accumulated changes are retried
285
+ * after the current operation completes.
286
+ *
287
+ * @returns A promise that resolves when changed chains are persisted.
288
+ */
289
+ async function _TokenListController_persistChangedChains() {
290
+ if (__classPrivateFieldGet(this, _TokenListController_persistInFlightPromise, "f")) {
291
+ // Reschedule debounce to retry accumulated changes after in-flight persist completes
292
+ if (__classPrivateFieldGet(this, _TokenListController_changedChainsToPersist, "f").size > 0) {
293
+ __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_debouncePersist).call(this);
294
+ }
295
+ return;
296
+ }
297
+ const chainsToPersist = [...__classPrivateFieldGet(this, _TokenListController_changedChainsToPersist, "f")];
298
+ __classPrivateFieldGet(this, _TokenListController_changedChainsToPersist, "f").clear();
299
+ if (chainsToPersist.length === 0) {
300
+ return;
301
+ }
302
+ __classPrivateFieldSet(this, _TokenListController_persistInFlightPromise, Promise.all(chainsToPersist.map((chainId) => __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_saveChainCacheToStorage).call(this, chainId))).then(() => undefined), "f");
303
+ try {
304
+ await __classPrivateFieldGet(this, _TokenListController_persistInFlightPromise, "f");
305
+ }
306
+ finally {
307
+ __classPrivateFieldSet(this, _TokenListController_persistInFlightPromise, undefined, "f");
308
+ }
309
+ }, _TokenListController_synchronizeCacheWithStorage =
310
+ /**
311
+ * Synchronize tokensChainsCache between state and storage bidirectionally.
312
+ *
313
+ * This method:
314
+ * 1. Loads cached chains from storage (per-chain files) in parallel
315
+ * 2. Merges loaded data into state (preferring existing state to avoid overwriting fresh data)
316
+ * 3. Persists any chains that exist in state but not in storage
317
+ *
318
+ * Called during initialization to ensure state and storage are consistent.
319
+ *
320
+ * @returns A promise that resolves when synchronization is complete.
321
+ */
322
+ async function _TokenListController_synchronizeCacheWithStorage() {
323
+ try {
324
+ const allKeys = await this.messenger.call('StorageService:getAllKeys', name);
325
+ // Filter keys that belong to tokensChainsCache (per-chain files)
326
+ const cacheKeys = allKeys.filter((key) => key.startsWith(`${__classPrivateFieldGet(_a, _a, "f", _TokenListController_storageKeyPrefix)}:`));
327
+ // Load all chains in parallel
328
+ const chainCaches = await Promise.all(cacheKeys.map(async (key) => {
329
+ // Extract chainId from key: 'tokensChainsCache:0x1' → '0x1'
330
+ const chainId = key.split(':')[1];
331
+ const { result, error } = await this.messenger.call('StorageService:getItem', name, key);
332
+ if (error) {
333
+ console.error(`TokenListController: Error loading cache for ${chainId}:`, error);
334
+ return null;
335
+ }
336
+ return result ? { chainId, data: result } : null;
337
+ }));
338
+ // Build complete cache from loaded chains
339
+ const loadedCache = {};
340
+ chainCaches.forEach((chainCache) => {
341
+ if (chainCache) {
342
+ loadedCache[chainCache.chainId] = chainCache.data;
343
+ }
229
344
  });
345
+ // Merge loaded cache with existing state, preferring existing data
346
+ // (which may be fresher if fetched during initialization)
347
+ if (Object.keys(loadedCache).length > 0) {
348
+ // Track which chains we're actually loading from storage
349
+ // These will be skipped in the next #onCacheChanged to avoid redundant writes
350
+ for (const chainId of Object.keys(loadedCache)) {
351
+ if (!this.state.tokensChainsCache[chainId]) {
352
+ __classPrivateFieldGet(this, _TokenListController_chainsLoadedFromStorage, "f").add(chainId);
353
+ }
354
+ }
355
+ this.update((state) => {
356
+ // Only load chains that don't already exist in state
357
+ // This prevents overwriting fresh API data with stale cached data
358
+ for (const [chainId, cacheData] of Object.entries(loadedCache)) {
359
+ if (!state.tokensChainsCache[chainId]) {
360
+ state.tokensChainsCache[chainId] = cacheData;
361
+ }
362
+ }
363
+ });
364
+ // Note: The update() call above only triggers #onCacheChanged if chains
365
+ // were actually added to state. If initial state already contains chains
366
+ // from storage, the update() is a no-op and #onCacheChanged is never called.
367
+ }
368
+ // Persist chains that exist in state but were not loaded from storage.
369
+ // This handles the case where initial state contains chains that don't exist
370
+ // in storage yet (e.g., fresh data from API). Without this, those chains
371
+ // would be lost on the next app restart.
372
+ const loadedChainIds = new Set(Object.keys(loadedCache));
373
+ const chainsInState = new Set(Object.keys(this.state.tokensChainsCache));
374
+ for (const chainId of chainsInState) {
375
+ if (!loadedChainIds.has(chainId)) {
376
+ __classPrivateFieldGet(this, _TokenListController_changedChainsToPersist, "f").add(chainId);
377
+ }
378
+ }
379
+ // Persist any chains that need to be saved
380
+ if (__classPrivateFieldGet(this, _TokenListController_changedChainsToPersist, "f").size > 0) {
381
+ __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_debouncePersist).call(this);
382
+ }
383
+ __classPrivateFieldSet(this, _TokenListController_previousTokensChainsCache, { ...this.state.tokensChainsCache }, "f");
230
384
  }
231
- }
232
- exports.TokenListController = TokenListController;
233
- _TokenListController_instances = new WeakSet(), _TokenListController_onNetworkControllerStateChange =
385
+ catch (error) {
386
+ console.error('TokenListController: Failed to load cache from storage:', error);
387
+ }
388
+ }, _TokenListController_saveChainCacheToStorage =
389
+ /**
390
+ * Save a specific chain's cache to StorageService.
391
+ * This persists only the updated chain's data, reducing write amplification.
392
+ *
393
+ * @param chainId - The chain ID to save.
394
+ * @returns A promise that resolves when saving is complete.
395
+ */
396
+ async function _TokenListController_saveChainCacheToStorage(chainId) {
397
+ try {
398
+ const chainData = this.state.tokensChainsCache[chainId];
399
+ if (!chainData) {
400
+ console.warn(`TokenListController: No cache data for chain ${chainId}`);
401
+ return;
402
+ }
403
+ const storageKey = __classPrivateFieldGet(_a, _a, "m", _TokenListController_getChainStorageKey).call(_a, chainId);
404
+ await this.messenger.call('StorageService:setItem', name, storageKey, chainData);
405
+ }
406
+ catch (error) {
407
+ console.error(`TokenListController: Failed to save cache for ${chainId}:`, error);
408
+ }
409
+ }, _TokenListController_onNetworkControllerStateChange =
234
410
  /**
235
411
  * Updates state and restarts polling on changes to the network controller
236
412
  * state.
@@ -240,13 +416,14 @@ _TokenListController_instances = new WeakSet(), _TokenListController_onNetworkCo
240
416
  async function _TokenListController_onNetworkControllerStateChange(networkControllerState) {
241
417
  const selectedNetworkClient = this.messenger.call('NetworkController:getNetworkClientById', networkControllerState.selectedNetworkClientId);
242
418
  const { chainId } = selectedNetworkClient.configuration;
243
- if (this.chainId !== chainId) {
244
- this.abortController.abort();
245
- this.abortController = new AbortController();
246
- this.chainId = chainId;
247
- if (this.state.preventPollingOnNetworkRestart) {
248
- this.clearingTokenListData();
249
- }
419
+ if (__classPrivateFieldGet(this, _TokenListController_chainId, "f") !== chainId) {
420
+ __classPrivateFieldGet(this, _TokenListController_abortController, "f").abort();
421
+ __classPrivateFieldSet(this, _TokenListController_abortController, new AbortController(), "f");
422
+ __classPrivateFieldSet(this, _TokenListController_chainId, chainId, "f");
423
+ }
424
+ }, _TokenListController_stopPolling = function _TokenListController_stopPolling() {
425
+ if (__classPrivateFieldGet(this, _TokenListController_intervalId, "f")) {
426
+ clearInterval(__classPrivateFieldGet(this, _TokenListController_intervalId, "f"));
250
427
  }
251
428
  }, _TokenListController_startDeprecatedPolling =
252
429
  /**
@@ -257,12 +434,18 @@ async function _TokenListController_onNetworkControllerStateChange(networkContro
257
434
  */
258
435
  async function _TokenListController_startDeprecatedPolling() {
259
436
  // renaming this to avoid collision with base class
260
- await (0, controller_utils_1.safelyExecute)(() => this.fetchTokenList(this.chainId));
437
+ await (0, controller_utils_1.safelyExecute)(() => this.fetchTokenList(__classPrivateFieldGet(this, _TokenListController_chainId, "f")));
261
438
  // TODO: Either fix this lint violation or explain why it's necessary to ignore.
262
439
  // eslint-disable-next-line @typescript-eslint/no-misused-promises
263
- this.intervalId = setInterval(async () => {
264
- await (0, controller_utils_1.safelyExecute)(() => this.fetchTokenList(this.chainId));
265
- }, this.intervalDelay);
440
+ __classPrivateFieldSet(this, _TokenListController_intervalId, setInterval(async () => {
441
+ await (0, controller_utils_1.safelyExecute)(() => this.fetchTokenList(__classPrivateFieldGet(this, _TokenListController_chainId, "f")));
442
+ }, __classPrivateFieldGet(this, _TokenListController_intervalDelay, "f")), "f");
266
443
  };
444
+ /**
445
+ * Debounce delay for persisting state changes (in milliseconds).
446
+ */
447
+ _TokenListController_persistDebounceMs = { value: 500 };
448
+ // Storage key prefix for per-chain files
449
+ _TokenListController_storageKeyPrefix = { value: 'tokensChainsCache' };
267
450
  exports.default = TokenListController;
268
451
  //# sourceMappingURL=TokenListController.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"TokenListController.cjs","sourceRoot":"","sources":["../src/TokenListController.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n ControllerGetStateAction,\n ControllerStateChangeEvent,\n StateMetadata,\n} from '@metamask/base-controller';\nimport { safelyExecute } from '@metamask/controller-utils';\nimport type { Messenger } from '@metamask/messenger';\nimport type {\n NetworkControllerStateChangeEvent,\n NetworkState,\n NetworkControllerGetNetworkClientByIdAction,\n} from '@metamask/network-controller';\nimport { StaticIntervalPollingController } from '@metamask/polling-controller';\nimport type { Hex } from '@metamask/utils';\nimport { Mutex } from 'async-mutex';\n\nimport {\n isTokenListSupportedForNetwork,\n formatAggregatorNames,\n formatIconUrlWithProxy,\n} from './assetsUtil';\nimport { TokenRwaData, fetchTokenListByChainId } from './token-service';\n\n// 4 Hour Interval Cache Refresh Threshold\nconst DEFAULT_INTERVAL = 4 * 60 * 60 * 1000;\nconst DEFAULT_THRESHOLD = 4 * 60 * 60 * 1000;\n\nconst name = 'TokenListController';\n\nexport type TokenListToken = {\n name: string;\n symbol: string;\n decimals: number;\n address: string;\n occurrences: number;\n aggregators: string[];\n iconUrl: string;\n rwaData?: TokenRwaData;\n};\n\nexport type TokenListMap = Record<string, TokenListToken>;\n\ntype DataCache = {\n timestamp: number;\n data: TokenListMap;\n};\nexport type TokensChainsCache = {\n [chainId: Hex]: DataCache;\n};\n\nexport type TokenListState = {\n tokensChainsCache: TokensChainsCache;\n preventPollingOnNetworkRestart: boolean;\n};\n\nexport type TokenListStateChange = ControllerStateChangeEvent<\n typeof name,\n TokenListState\n>;\n\nexport type TokenListControllerEvents = TokenListStateChange;\n\nexport type GetTokenListState = ControllerGetStateAction<\n typeof name,\n TokenListState\n>;\n\nexport type TokenListControllerActions = GetTokenListState;\n\ntype AllowedActions = NetworkControllerGetNetworkClientByIdAction;\n\ntype AllowedEvents = NetworkControllerStateChangeEvent;\n\nexport type TokenListControllerMessenger = Messenger<\n typeof name,\n TokenListControllerActions | AllowedActions,\n TokenListControllerEvents | AllowedEvents\n>;\n\nconst metadata: StateMetadata<TokenListState> = {\n tokensChainsCache: {\n includeInStateLogs: false,\n persist: true,\n includeInDebugSnapshot: true,\n usedInUi: true,\n },\n preventPollingOnNetworkRestart: {\n includeInStateLogs: false,\n persist: true,\n includeInDebugSnapshot: true,\n usedInUi: false,\n },\n};\n\nexport const getDefaultTokenListState = (): TokenListState => {\n return {\n tokensChainsCache: {},\n preventPollingOnNetworkRestart: false,\n };\n};\n\n/** The input to start polling for the {@link TokenListController} */\ntype TokenListPollingInput = {\n chainId: Hex;\n};\n\n/**\n * Controller that passively polls on a set interval for the list of tokens from metaswaps api\n */\nexport class TokenListController extends StaticIntervalPollingController<TokenListPollingInput>()<\n typeof name,\n TokenListState,\n TokenListControllerMessenger\n> {\n private readonly mutex = new Mutex();\n\n private intervalId?: ReturnType<typeof setTimeout>;\n\n private readonly intervalDelay: number;\n\n private readonly cacheRefreshThreshold: number;\n\n private chainId: Hex;\n\n private abortController: AbortController;\n\n /**\n * Creates a TokenListController instance.\n *\n * @param options - The controller options.\n * @param options.chainId - The chain ID of the current network.\n * @param options.onNetworkStateChange - A function for registering an event handler for network state changes.\n * @param options.interval - The polling interval, in milliseconds.\n * @param options.cacheRefreshThreshold - The token cache expiry time, in milliseconds.\n * @param options.messenger - A restricted messenger.\n * @param options.state - Initial state to set on this controller.\n * @param options.preventPollingOnNetworkRestart - Determines whether to prevent poilling on network restart in extension.\n */\n constructor({\n chainId,\n preventPollingOnNetworkRestart = false,\n onNetworkStateChange,\n interval = DEFAULT_INTERVAL,\n cacheRefreshThreshold = DEFAULT_THRESHOLD,\n messenger,\n state,\n }: {\n chainId: Hex;\n preventPollingOnNetworkRestart?: boolean;\n onNetworkStateChange?: (\n listener: (networkState: NetworkState) => void,\n ) => void;\n interval?: number;\n cacheRefreshThreshold?: number;\n messenger: TokenListControllerMessenger;\n state?: Partial<TokenListState>;\n }) {\n super({\n name,\n metadata,\n messenger,\n state: { ...getDefaultTokenListState(), ...state },\n });\n this.intervalDelay = interval;\n this.setIntervalLength(interval);\n this.cacheRefreshThreshold = cacheRefreshThreshold;\n this.chainId = chainId;\n this.updatePreventPollingOnNetworkRestart(preventPollingOnNetworkRestart);\n this.abortController = new AbortController();\n if (onNetworkStateChange) {\n // TODO: Either fix this lint violation or explain why it's necessary to ignore.\n // eslint-disable-next-line @typescript-eslint/no-misused-promises\n onNetworkStateChange(async (networkControllerState) => {\n await this.#onNetworkControllerStateChange(networkControllerState);\n });\n } else {\n this.messenger.subscribe(\n 'NetworkController:stateChange',\n // TODO: Either fix this lint violation or explain why it's necessary to ignore.\n // eslint-disable-next-line @typescript-eslint/no-misused-promises\n async (networkControllerState) => {\n await this.#onNetworkControllerStateChange(networkControllerState);\n },\n );\n }\n }\n\n /**\n * Updates state and restarts polling on changes to the network controller\n * state.\n *\n * @param networkControllerState - The updated network controller state.\n */\n async #onNetworkControllerStateChange(\n networkControllerState: NetworkState,\n ): Promise<void> {\n const selectedNetworkClient = this.messenger.call(\n 'NetworkController:getNetworkClientById',\n networkControllerState.selectedNetworkClientId,\n );\n const { chainId } = selectedNetworkClient.configuration;\n\n if (this.chainId !== chainId) {\n this.abortController.abort();\n this.abortController = new AbortController();\n this.chainId = chainId;\n if (this.state.preventPollingOnNetworkRestart) {\n this.clearingTokenListData();\n }\n }\n }\n\n // Eventually we want to remove start/restart/stop controls in favor of new _executePoll API\n // Maintaining these functions for now until we can safely deprecate them for backwards compatibility\n /**\n * Start polling for the token list.\n *\n * @deprecated This method is deprecated and will be removed in the future.\n * Consider using the new polling approach instead\n */\n async start(): Promise<void> {\n if (!isTokenListSupportedForNetwork(this.chainId)) {\n return;\n }\n await this.#startDeprecatedPolling();\n }\n\n /**\n * Restart polling for the token list.\n *\n * @deprecated This method is deprecated and will be removed in the future.\n * Consider using the new polling approach instead\n */\n async restart(): Promise<void> {\n this.stopPolling();\n await this.#startDeprecatedPolling();\n }\n\n /**\n * Stop polling for the token list.\n *\n * @deprecated This method is deprecated and will be removed in the future.\n * Consider using the new polling approach instead\n */\n stop(): void {\n this.stopPolling();\n }\n\n /**\n * This stops any active polling.\n *\n * @deprecated This method is deprecated and will be removed in the future.\n * Consider using the new polling approach instead\n */\n override destroy(): void {\n super.destroy();\n this.stopPolling();\n }\n\n /**\n * This stops any active polling intervals.\n *\n * @deprecated This method is deprecated and will be removed in the future.\n * Consider using the new polling approach instead\n */\n private stopPolling() {\n if (this.intervalId) {\n clearInterval(this.intervalId);\n }\n }\n\n /**\n * Starts a new polling interval for a given chainId (this should be deprecated in favor of _executePoll)\n *\n * @deprecated This method is deprecated and will be removed in the future.\n * Consider using the new polling approach instead\n */\n async #startDeprecatedPolling(): Promise<void> {\n // renaming this to avoid collision with base class\n await safelyExecute(() => this.fetchTokenList(this.chainId));\n // TODO: Either fix this lint violation or explain why it's necessary to ignore.\n // eslint-disable-next-line @typescript-eslint/no-misused-promises\n this.intervalId = setInterval(async () => {\n await safelyExecute(() => this.fetchTokenList(this.chainId));\n }, this.intervalDelay);\n }\n\n /**\n * This starts a new polling loop for any given chain. Under the hood it is deduping polls\n *\n * @param input - The input for the poll.\n * @param input.chainId - The chainId of the chain to trigger the fetch.\n * @returns A promise that resolves when this operation completes.\n */\n async _executePoll({ chainId }: TokenListPollingInput): Promise<void> {\n return this.fetchTokenList(chainId);\n }\n\n /**\n * Fetching token list from the Token Service API. This will fetch tokens across chains. It will update tokensChainsCache (scoped across chains), and also the tokenList (scoped for the selected chain)\n *\n * @param chainId - The chainId of the current chain triggering the fetch.\n */\n async fetchTokenList(chainId: Hex): Promise<void> {\n const releaseLock = await this.mutex.acquire();\n try {\n if (this.isCacheValid(chainId)) {\n return;\n }\n\n // Fetch fresh token list from the API\n const tokensFromAPI = await safelyExecute(\n () =>\n fetchTokenListByChainId(\n chainId,\n this.abortController.signal,\n ) as Promise<TokenListToken[]>,\n );\n\n // Have response - process and update list\n if (tokensFromAPI) {\n // Format tokens from API (HTTP) and update tokenList\n const tokenList: TokenListMap = {};\n for (const token of tokensFromAPI) {\n tokenList[token.address] = {\n ...token,\n aggregators: formatAggregatorNames(token.aggregators),\n iconUrl: formatIconUrlWithProxy({\n chainId,\n tokenAddress: token.address,\n }),\n };\n }\n\n this.update((state) => {\n const newDataCache: DataCache = { data: {}, timestamp: Date.now() };\n state.tokensChainsCache[chainId] ??= newDataCache;\n state.tokensChainsCache[chainId].data = tokenList;\n state.tokensChainsCache[chainId].timestamp = Date.now();\n });\n return;\n }\n\n // No response - fallback to previous state, or initialise empty\n if (!tokensFromAPI) {\n this.update((state) => {\n const newDataCache: DataCache = { data: {}, timestamp: Date.now() };\n state.tokensChainsCache[chainId] ??= newDataCache;\n state.tokensChainsCache[chainId].timestamp = Date.now();\n });\n }\n } finally {\n releaseLock();\n }\n }\n\n isCacheValid(chainId: Hex): boolean {\n const { tokensChainsCache }: TokenListState = this.state;\n const timestamp: number | undefined = tokensChainsCache[chainId]?.timestamp;\n const now = Date.now();\n return (\n timestamp !== undefined && now - timestamp < this.cacheRefreshThreshold\n );\n }\n\n /**\n * Clearing tokenList and tokensChainsCache explicitly.\n */\n clearingTokenListData(): void {\n this.update(() => {\n return {\n ...this.state,\n tokensChainsCache: {},\n };\n });\n }\n\n /**\n * Updates preventPollingOnNetworkRestart from extension.\n *\n * @param shouldPreventPolling - Determine whether to prevent polling on network change\n */\n updatePreventPollingOnNetworkRestart(shouldPreventPolling: boolean): void {\n this.update(() => {\n return {\n ...this.state,\n preventPollingOnNetworkRestart: shouldPreventPolling,\n };\n });\n }\n}\n\nexport default TokenListController;\n"]}
1
+ {"version":3,"file":"TokenListController.cjs","sourceRoot":"","sources":["../src/TokenListController.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAKA,iEAA2D;AAO3D,qEAA+E;AAQ/E,iDAIsB;AACtB,uDAAwE;AAExE,0CAA0C;AAC1C,MAAM,gBAAgB,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAC5C,MAAM,iBAAiB,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAE7C,MAAM,IAAI,GAAG,qBAAqB,CAAC;AAuDnC,MAAM,QAAQ,GAAkC;IAC9C,iBAAiB,EAAE;QACjB,kBAAkB,EAAE,KAAK;QACzB,OAAO,EAAE,KAAK,EAAE,0CAA0C;QAC1D,sBAAsB,EAAE,IAAI;QAC5B,QAAQ,EAAE,IAAI;KACf;CACF,CAAC;AAEK,MAAM,wBAAwB,GAAG,GAAmB,EAAE;IAC3D,OAAO;QACL,iBAAiB,EAAE,EAAE;KACtB,CAAC;AACJ,CAAC,CAAC;AAJW,QAAA,wBAAwB,4BAInC;AAOF;;GAEG;AACH,MAAa,mBAAoB,SAAQ,IAAA,oDAA+B,GAIvE;IA2DC;;;;;;;;;;OAUG;IACH,YAAY,EACV,OAAO,EACP,oBAAoB,EACpB,QAAQ,GAAG,gBAAgB,EAC3B,qBAAqB,GAAG,iBAAiB,EACzC,SAAS,EACT,KAAK,GAUN;QACC,KAAK,CAAC;YACJ,IAAI;YACJ,QAAQ;YACR,SAAS;YACT,KAAK,EAAE,EAAE,GAAG,IAAA,gCAAwB,GAAE,EAAE,GAAG,KAAK,EAAE;SACnD,CAAC,CAAC;;QA3FL;;WAEG;QACH,4DAAsD;QAEtD;;;WAGG;QACH,8DAAwC;QAExC;;;WAGG;QACM,sDAAoC,IAAI,GAAG,EAAE,EAAC;QAEvD;;;;;WAKG;QACM,uDAAqC,IAAI,GAAG,EAAE,EAAC;QAExD;;WAEG;QACH,yDAAgD,EAAE,EAAC;QAoBnD,kDAA4C;QAEnC,qDAAuB;QAEvB,6DAA+B;QAExC,+CAAc;QAEd,uDAAkC;QAqChC,uBAAA,IAAI,sCAAkB,QAAQ,MAAA,CAAC;QAC/B,IAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC;QACjC,uBAAA,IAAI,8CAA0B,qBAAqB,MAAA,CAAC;QACpD,uBAAA,IAAI,gCAAY,OAAO,MAAA,CAAC;QACxB,uBAAA,IAAI,wCAAoB,IAAI,eAAe,EAAE,MAAA,CAAC;QAE9C,wEAAwE;QACxE,IAAI,CAAC,SAAS,CAAC,SAAS,CACtB,iCAAiC,EACjC,CAAC,QAA2B,EAAE,EAAE,CAAC,uBAAA,IAAI,2EAAgB,MAApB,IAAI,EAAiB,QAAQ,CAAC,EAC/D,CAAC,eAAe,EAAE,EAAE,CAAC,eAAe,CAAC,iBAAiB,CACvD,CAAC;QAEF,IAAI,oBAAoB,EAAE,CAAC;YACzB,gFAAgF;YAChF,kEAAkE;YAClE,oBAAoB,CAAC,KAAK,EAAE,sBAAsB,EAAE,EAAE;gBACpD,MAAM,uBAAA,IAAI,2FAAgC,MAApC,IAAI,EAAiC,sBAAsB,CAAC,CAAC;YACrE,CAAC,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,SAAS,CAAC,SAAS,CACtB,+BAA+B;YAC/B,gFAAgF;YAChF,kEAAkE;YAClE,KAAK,EAAE,sBAAsB,EAAE,EAAE;gBAC/B,MAAM,uBAAA,IAAI,2FAAgC,MAApC,IAAI,EAAiC,sBAAsB,CAAC,CAAC;YACrE,CAAC,CACF,CAAC;QACJ,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,UAAU;QACd,MAAM,uBAAA,IAAI,wFAA6B,MAAjC,IAAI,CAA+B,CAAC;IAC5C,CAAC;IA4PD,4FAA4F;IAC5F,qGAAqG;IACrG;;;;;OAKG;IACH,KAAK,CAAC,KAAK;QACT,IAAI,CAAC,IAAA,2CAA8B,EAAC,uBAAA,IAAI,oCAAS,CAAC,EAAE,CAAC;YACnD,OAAO;QACT,CAAC;QACD,MAAM,uBAAA,IAAI,mFAAwB,MAA5B,IAAI,CAA0B,CAAC;IACvC,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,OAAO;QACX,uBAAA,IAAI,wEAAa,MAAjB,IAAI,CAAe,CAAC;QACpB,MAAM,uBAAA,IAAI,mFAAwB,MAA5B,IAAI,CAA0B,CAAC;IACvC,CAAC;IAED;;;;;OAKG;IACH,IAAI;QACF,uBAAA,IAAI,wEAAa,MAAjB,IAAI,CAAe,CAAC;IACtB,CAAC;IAED;;;;;OAKG;IACM,OAAO;QACd,KAAK,CAAC,OAAO,EAAE,CAAC;QAChB,uBAAA,IAAI,wEAAa,MAAjB,IAAI,CAAe,CAAC;QAEpB,sDAAsD;QACtD,IAAI,uBAAA,IAAI,iDAAsB,EAAE,CAAC;YAC/B,YAAY,CAAC,uBAAA,IAAI,iDAAsB,CAAC,CAAC;YACzC,uBAAA,IAAI,6CAAyB,SAAS,MAAA,CAAC;QACzC,CAAC;QACD,uBAAA,IAAI,mDAAwB,CAAC,KAAK,EAAE,CAAC;QACrC,uBAAA,IAAI,oDAAyB,CAAC,KAAK,EAAE,CAAC;IACxC,CAAC;IA8BD;;;;;;OAMG;IACH,KAAK,CAAC,YAAY,CAAC,EAAE,OAAO,EAAyB;QACnD,OAAO,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;IACtC,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,cAAc,CAAC,OAAY;QAC/B,IAAI,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE,CAAC;YAC/B,OAAO;QACT,CAAC;QAED,sCAAsC;QACtC,MAAM,aAAa,GAAG,MAAM,IAAA,gCAAa,EACvC,GAAG,EAAE,CACH,IAAA,uCAAuB,EACrB,OAAO,EACP,uBAAA,IAAI,4CAAiB,CAAC,MAAM,CACA,CACjC,CAAC;QAEF,0CAA0C;QAC1C,IAAI,aAAa,EAAE,CAAC;YAClB,qDAAqD;YACrD,MAAM,SAAS,GAAiB,EAAE,CAAC;YACnC,KAAK,MAAM,KAAK,IAAI,aAAa,EAAE,CAAC;gBAClC,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG;oBACzB,GAAG,KAAK;oBACR,WAAW,EAAE,IAAA,kCAAqB,EAAC,KAAK,CAAC,WAAW,CAAC;oBACrD,OAAO,EAAE,IAAA,mCAAsB,EAAC;wBAC9B,OAAO;wBACP,YAAY,EAAE,KAAK,CAAC,OAAO;qBAC5B,CAAC;iBACH,CAAC;YACJ,CAAC;YAED,oEAAoE;YACpE,MAAM,YAAY,GAAc;gBAC9B,IAAI,EAAE,SAAS;gBACf,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;aACtB,CAAC;YACF,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;gBACpB,KAAK,CAAC,iBAAiB,CAAC,OAAO,CAAC,GAAG,YAAY,CAAC;YAClD,CAAC,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QAED,iEAAiE;QACjE,qEAAqE;QACrE,0EAA0E;QAC1E,2EAA2E;QAC3E,IAAI,CAAC,aAAa,EAAE,CAAC;YACnB,MAAM,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC;YAC5D,IAAI,CAAC,aAAa,EAAE,CAAC;gBACnB,2EAA2E;gBAC3E,MAAM,YAAY,GAAc,EAAE,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;gBACpE,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;oBACpB,KAAK,CAAC,iBAAiB,CAAC,OAAO,CAAC,GAAG,YAAY,CAAC;gBAClD,CAAC,CAAC,CAAC;YACL,CAAC;YACD,+EAA+E;QACjF,CAAC;IACH,CAAC;IAED,YAAY,CAAC,OAAY;QACvB,MAAM,EAAE,iBAAiB,EAAE,GAAmB,IAAI,CAAC,KAAK,CAAC;QACzD,MAAM,SAAS,GAAuB,iBAAiB,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC;QAC5E,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,OAAO,CACL,SAAS,KAAK,SAAS,IAAI,GAAG,GAAG,SAAS,GAAG,uBAAA,IAAI,kDAAuB,CACzE,CAAC;IACJ,CAAC;;AAziBH,kDA0iBC;6tBAzf4B,OAAY;IACrC,OAAO,GAAG,uBAAA,EAAmB,iDAAkB,IAAI,OAAO,EAAE,CAAC;AAC/D,CAAC,qFA8Fe,QAA2B;IACzC,+DAA+D;IAC/D,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAU,EAAE,CAAC;QACrD,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;QAClC,MAAM,QAAQ,GAAG,uBAAA,IAAI,sDAA2B,CAAC,OAAO,CAAC,CAAC;QAE1D,6DAA6D;QAC7D,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,SAAS,KAAK,OAAO,CAAC,SAAS,EAAE,CAAC;YAC1D,iEAAiE;YACjE,mDAAmD;YACnD,IAAI,uBAAA,IAAI,oDAAyB,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC/C,uBAAA,IAAI,oDAAyB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,2CAA2C;YAC5F,CAAC;iBAAM,CAAC;gBACN,uBAAA,IAAI,mDAAwB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YAC5C,CAAC;QACH,CAAC;IACH,CAAC;IAED,kCAAkC;IAClC,uBAAA,IAAI,kDAA8B,EAAE,GAAG,QAAQ,EAAE,MAAA,CAAC;IAElD,4CAA4C;IAC5C,IAAI,uBAAA,IAAI,mDAAwB,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;QAC1C,uBAAA,IAAI,4EAAiB,MAArB,IAAI,CAAmB,CAAC;IAC1B,CAAC;AACH,CAAC;IAMC,IAAI,uBAAA,IAAI,iDAAsB,EAAE,CAAC;QAC/B,YAAY,CAAC,uBAAA,IAAI,iDAAsB,CAAC,CAAC;IAC3C,CAAC;IAED,uBAAA,IAAI,6CAAyB,UAAU,CAAC,GAAG,EAAE;QAC3C,sFAAsF;QACtF,mCAAmC;QACnC,mEAAmE;QACnE,uBAAA,IAAI,iFAAsB,MAA1B,IAAI,CAAwB,CAAC;IAC/B,CAAC,EAAE,uBAAA,EAAmB,kDAAmB,CAAC,MAAA,CAAC;AAC7C,CAAC;AAED;;;;;;;;;GASG;AACH,KAAK;IACH,IAAI,uBAAA,IAAI,mDAAwB,EAAE,CAAC;QACjC,qFAAqF;QACrF,IAAI,uBAAA,IAAI,mDAAwB,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;YAC1C,uBAAA,IAAI,4EAAiB,MAArB,IAAI,CAAmB,CAAC;QAC1B,CAAC;QACD,OAAO;IACT,CAAC;IAED,MAAM,eAAe,GAAG,CAAC,GAAG,uBAAA,IAAI,mDAAwB,CAAC,CAAC;IAC1D,uBAAA,IAAI,mDAAwB,CAAC,KAAK,EAAE,CAAC;IAErC,IAAI,eAAe,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACjC,OAAO;IACT,CAAC;IAED,uBAAA,IAAI,+CAA2B,OAAO,CAAC,GAAG,CACxC,eAAe,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,uBAAA,IAAI,oFAAyB,MAA7B,IAAI,EAA0B,OAAO,CAAC,CAAC,CACzE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,MAAA,CAAC;IAExB,IAAI,CAAC;QACH,MAAM,uBAAA,IAAI,mDAAwB,CAAC;IACrC,CAAC;YAAS,CAAC;QACT,uBAAA,IAAI,+CAA2B,SAAS,MAAA,CAAC;IAC3C,CAAC;AACH,CAAC;AAED;;;;;;;;;;;GAWG;AACH,KAAK;IACH,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CACvC,2BAA2B,EAC3B,IAAI,CACL,CAAC;QAEF,iEAAiE;QACjE,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CACvC,GAAG,CAAC,UAAU,CAAC,GAAG,uBAAA,EAAmB,iDAAkB,GAAG,CAAC,CAC5D,CAAC;QAEF,8BAA8B;QAC9B,MAAM,WAAW,GAAG,MAAM,OAAO,CAAC,GAAG,CACnC,SAAS,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;YAC1B,4DAA4D;YAC5D,MAAM,OAAO,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAQ,CAAC;YAEzC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CACjD,wBAAwB,EACxB,IAAI,EACJ,GAAG,CACJ,CAAC;YAEF,IAAI,KAAK,EAAE,CAAC;gBACV,OAAO,CAAC,KAAK,CACX,gDAAgD,OAAO,GAAG,EAC1D,KAAK,CACN,CAAC;gBACF,OAAO,IAAI,CAAC;YACd,CAAC;YAED,OAAO,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,MAAmB,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QAChE,CAAC,CAAC,CACH,CAAC;QAEF,0CAA0C;QAC1C,MAAM,WAAW,GAAsB,EAAE,CAAC;QAC1C,WAAW,CAAC,OAAO,CAAC,CAAC,UAAU,EAAE,EAAE;YACjC,IAAI,UAAU,EAAE,CAAC;gBACf,WAAW,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,UAAU,CAAC,IAAI,CAAC;YACpD,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,mEAAmE;QACnE,0DAA0D;QAC1D,IAAI,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxC,yDAAyD;YACzD,8EAA8E;YAC9E,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,WAAW,CAAU,EAAE,CAAC;gBACxD,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,iBAAiB,CAAC,OAAO,CAAC,EAAE,CAAC;oBAC3C,uBAAA,IAAI,oDAAyB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;gBAC7C,CAAC;YACH,CAAC;YAED,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;gBACpB,qDAAqD;gBACrD,kEAAkE;gBAClE,KAAK,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;oBAC/D,IAAI,CAAC,KAAK,CAAC,iBAAiB,CAAC,OAAc,CAAC,EAAE,CAAC;wBAC7C,KAAK,CAAC,iBAAiB,CAAC,OAAc,CAAC,GAAG,SAAS,CAAC;oBACtD,CAAC;gBACH,CAAC;YACH,CAAC,CAAC,CAAC;YAEH,wEAAwE;YACxE,yEAAyE;YACzE,6EAA6E;QAC/E,CAAC;QAED,uEAAuE;QACvE,6EAA6E;QAC7E,yEAAyE;QACzE,yCAAyC;QACzC,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAU,CAAC,CAAC;QAClE,MAAM,aAAa,GAAG,IAAI,GAAG,CAC3B,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,iBAAiB,CAAU,CACnD,CAAC;QACF,KAAK,MAAM,OAAO,IAAI,aAAa,EAAE,CAAC;YACpC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;gBACjC,uBAAA,IAAI,mDAAwB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YAC5C,CAAC;QACH,CAAC;QAED,2CAA2C;QAC3C,IAAI,uBAAA,IAAI,mDAAwB,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;YAC1C,uBAAA,IAAI,4EAAiB,MAArB,IAAI,CAAmB,CAAC;QAC1B,CAAC;QAED,uBAAA,IAAI,kDAA8B,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,iBAAiB,EAAE,MAAA,CAAC;IACxE,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CACX,yDAAyD,EACzD,KAAK,CACN,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,KAAK,uDAA0B,OAAY;IACzC,IAAI,CAAC;QACH,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC;QAExD,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,OAAO,CAAC,IAAI,CAAC,gDAAgD,OAAO,EAAE,CAAC,CAAC;YACxE,OAAO;QACT,CAAC;QAED,MAAM,UAAU,GAAG,uBAAA,EAAmB,mDAAoB,MAAvC,EAAmB,EAAqB,OAAO,CAAC,CAAC;QAEpE,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CACvB,wBAAwB,EACxB,IAAI,EACJ,UAAU,EACV,SAAS,CACV,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CACX,iDAAiD,OAAO,GAAG,EAC3D,KAAK,CACN,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,KAAK,8DACH,sBAAoC;IAEpC,MAAM,qBAAqB,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAC/C,wCAAwC,EACxC,sBAAsB,CAAC,uBAAuB,CAC/C,CAAC;IACF,MAAM,EAAE,OAAO,EAAE,GAAG,qBAAqB,CAAC,aAAa,CAAC;IAExD,IAAI,uBAAA,IAAI,oCAAS,KAAK,OAAO,EAAE,CAAC;QAC9B,uBAAA,IAAI,4CAAiB,CAAC,KAAK,EAAE,CAAC;QAC9B,uBAAA,IAAI,wCAAoB,IAAI,eAAe,EAAE,MAAA,CAAC;QAC9C,uBAAA,IAAI,gCAAY,OAAO,MAAA,CAAC;IAC1B,CAAC;AACH,CAAC;IAgEC,IAAI,uBAAA,IAAI,uCAAY,EAAE,CAAC;QACrB,aAAa,CAAC,uBAAA,IAAI,uCAAY,CAAC,CAAC;IAClC,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,KAAK;IACH,mDAAmD;IACnD,MAAM,IAAA,gCAAa,EAAC,GAAG,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,uBAAA,IAAI,oCAAS,CAAC,CAAC,CAAC;IAC9D,gFAAgF;IAChF,kEAAkE;IAClE,uBAAA,IAAI,mCAAe,WAAW,CAAC,KAAK,IAAI,EAAE;QACxC,MAAM,IAAA,gCAAa,EAAC,GAAG,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,uBAAA,IAAI,oCAAS,CAAC,CAAC,CAAC;IAChE,CAAC,EAAE,uBAAA,IAAI,0CAAe,CAAC,MAAA,CAAC;AAC1B,CAAC;AAnbD;;GAEG;AACa,kDAAqB,GAAG,EAAN,CAAO;AAEzC,yCAAyC;AACzB,iDAAoB,mBAAmB,EAAtB,CAAuB;AAmgB1D,kBAAe,mBAAmB,CAAC","sourcesContent":["import type {\n ControllerGetStateAction,\n ControllerStateChangeEvent,\n StateMetadata,\n} from '@metamask/base-controller';\nimport { safelyExecute } from '@metamask/controller-utils';\nimport type { Messenger } from '@metamask/messenger';\nimport type {\n NetworkControllerStateChangeEvent,\n NetworkState,\n NetworkControllerGetNetworkClientByIdAction,\n} from '@metamask/network-controller';\nimport { StaticIntervalPollingController } from '@metamask/polling-controller';\nimport type {\n StorageServiceSetItemAction,\n StorageServiceGetItemAction,\n StorageServiceGetAllKeysAction,\n} from '@metamask/storage-service';\nimport type { Hex } from '@metamask/utils';\n\nimport {\n isTokenListSupportedForNetwork,\n formatAggregatorNames,\n formatIconUrlWithProxy,\n} from './assetsUtil';\nimport { TokenRwaData, fetchTokenListByChainId } from './token-service';\n\n// 4 Hour Interval Cache Refresh Threshold\nconst DEFAULT_INTERVAL = 4 * 60 * 60 * 1000;\nconst DEFAULT_THRESHOLD = 4 * 60 * 60 * 1000;\n\nconst name = 'TokenListController';\n\nexport type TokenListToken = {\n name: string;\n symbol: string;\n decimals: number;\n address: string;\n occurrences: number;\n aggregators: string[];\n iconUrl: string;\n rwaData?: TokenRwaData;\n};\n\nexport type TokenListMap = Record<string, TokenListToken>;\n\nexport type DataCache = {\n timestamp: number;\n data: TokenListMap;\n};\nexport type TokensChainsCache = {\n [chainId: Hex]: DataCache;\n};\n\nexport type TokenListState = {\n tokensChainsCache: TokensChainsCache;\n};\n\nexport type TokenListStateChange = ControllerStateChangeEvent<\n typeof name,\n TokenListState\n>;\n\nexport type TokenListControllerEvents = TokenListStateChange;\n\nexport type GetTokenListState = ControllerGetStateAction<\n typeof name,\n TokenListState\n>;\n\nexport type TokenListControllerActions = GetTokenListState;\n\ntype AllowedActions =\n | NetworkControllerGetNetworkClientByIdAction\n | StorageServiceSetItemAction\n | StorageServiceGetItemAction\n | StorageServiceGetAllKeysAction;\n\ntype AllowedEvents = NetworkControllerStateChangeEvent;\n\nexport type TokenListControllerMessenger = Messenger<\n typeof name,\n TokenListControllerActions | AllowedActions,\n TokenListControllerEvents | AllowedEvents\n>;\n\nconst metadata: StateMetadata<TokenListState> = {\n tokensChainsCache: {\n includeInStateLogs: false,\n persist: false, // Persisted separately via StorageService\n includeInDebugSnapshot: true,\n usedInUi: true,\n },\n};\n\nexport const getDefaultTokenListState = (): TokenListState => {\n return {\n tokensChainsCache: {},\n };\n};\n\n/** The input to start polling for the {@link TokenListController} */\ntype TokenListPollingInput = {\n chainId: Hex;\n};\n\n/**\n * Controller that passively polls on a set interval for the list of tokens from metaswaps api\n */\nexport class TokenListController extends StaticIntervalPollingController<TokenListPollingInput>()<\n typeof name,\n TokenListState,\n TokenListControllerMessenger\n> {\n /**\n * Debounce timer for persisting state changes to storage.\n */\n #persistDebounceTimer?: ReturnType<typeof setTimeout>;\n\n /**\n * Promise that resolves when the current persist operation completes.\n * Used to prevent race conditions between persist operations.\n */\n #persistInFlightPromise?: Promise<void>;\n\n /**\n * Tracks which chains have pending changes to persist.\n * Only changed chains are persisted to reduce write amplification.\n */\n readonly #changedChainsToPersist: Set<Hex> = new Set();\n\n /**\n * Tracks chains that were just loaded from storage and should skip\n * the next persistence cycle. This prevents redundant writes where\n * data loaded from storage would be immediately written back.\n * Chains are removed from this set after being skipped once.\n */\n readonly #chainsLoadedFromStorage: Set<Hex> = new Set();\n\n /**\n * Previous tokensChainsCache for detecting which chains changed.\n */\n #previousTokensChainsCache: TokensChainsCache = {};\n\n /**\n * Debounce delay for persisting state changes (in milliseconds).\n */\n static readonly #persistDebounceMs = 500;\n\n // Storage key prefix for per-chain files\n static readonly #storageKeyPrefix = 'tokensChainsCache';\n\n /**\n * Get storage key for a specific chain.\n *\n * @param chainId - The chain ID.\n * @returns Storage key for the chain.\n */\n static #getChainStorageKey(chainId: Hex): string {\n return `${TokenListController.#storageKeyPrefix}:${chainId}`;\n }\n\n #intervalId?: ReturnType<typeof setTimeout>;\n\n readonly #intervalDelay: number;\n\n readonly #cacheRefreshThreshold: number;\n\n #chainId: Hex;\n\n #abortController: AbortController;\n\n /**\n * Creates a TokenListController instance.\n *\n * @param options - The controller options.\n * @param options.chainId - The chain ID of the current network.\n * @param options.onNetworkStateChange - A function for registering an event handler for network state changes.\n * @param options.interval - The polling interval, in milliseconds.\n * @param options.cacheRefreshThreshold - The token cache expiry time, in milliseconds.\n * @param options.messenger - A restricted messenger.\n * @param options.state - Initial state to set on this controller.\n */\n constructor({\n chainId,\n onNetworkStateChange,\n interval = DEFAULT_INTERVAL,\n cacheRefreshThreshold = DEFAULT_THRESHOLD,\n messenger,\n state,\n }: {\n chainId: Hex;\n onNetworkStateChange?: (\n listener: (networkState: NetworkState) => void,\n ) => void;\n interval?: number;\n cacheRefreshThreshold?: number;\n messenger: TokenListControllerMessenger;\n state?: Partial<TokenListState>;\n }) {\n super({\n name,\n metadata,\n messenger,\n state: { ...getDefaultTokenListState(), ...state },\n });\n\n this.#intervalDelay = interval;\n this.setIntervalLength(interval);\n this.#cacheRefreshThreshold = cacheRefreshThreshold;\n this.#chainId = chainId;\n this.#abortController = new AbortController();\n\n // Subscribe to state changes to automatically persist tokensChainsCache\n this.messenger.subscribe(\n 'TokenListController:stateChange',\n (newCache: TokensChainsCache) => this.#onCacheChanged(newCache),\n (controllerState) => controllerState.tokensChainsCache,\n );\n\n if (onNetworkStateChange) {\n // TODO: Either fix this lint violation or explain why it's necessary to ignore.\n // eslint-disable-next-line @typescript-eslint/no-misused-promises\n onNetworkStateChange(async (networkControllerState) => {\n await this.#onNetworkControllerStateChange(networkControllerState);\n });\n } else {\n this.messenger.subscribe(\n 'NetworkController:stateChange',\n // TODO: Either fix this lint violation or explain why it's necessary to ignore.\n // eslint-disable-next-line @typescript-eslint/no-misused-promises\n async (networkControllerState) => {\n await this.#onNetworkControllerStateChange(networkControllerState);\n },\n );\n }\n }\n\n /**\n * Initialize the controller by loading cache from storage and running migration.\n * This method should be called by clients after construction.\n *\n * @returns A promise that resolves when initialization is complete.\n */\n async initialize(): Promise<void> {\n await this.#synchronizeCacheWithStorage();\n }\n\n /**\n * Handle tokensChainsCache changes by detecting which chains changed\n * and scheduling debounced persistence.\n *\n * @param newCache - The new tokensChainsCache state.\n */\n #onCacheChanged(newCache: TokensChainsCache): void {\n // Detect which chains changed by comparing with previous cache\n for (const chainId of Object.keys(newCache) as Hex[]) {\n const newData = newCache[chainId];\n const prevData = this.#previousTokensChainsCache[chainId];\n\n // Chain is new or timestamp changed (indicating data update)\n if (!prevData || prevData.timestamp !== newData.timestamp) {\n // Skip persistence for chains that were just loaded from storage\n // (they don't need to be written back immediately)\n if (this.#chainsLoadedFromStorage.has(chainId)) {\n this.#chainsLoadedFromStorage.delete(chainId); // Clean up - future updates should persist\n } else {\n this.#changedChainsToPersist.add(chainId);\n }\n }\n }\n\n // Update previous cache reference\n this.#previousTokensChainsCache = { ...newCache };\n\n // Schedule persistence if there are changes\n if (this.#changedChainsToPersist.size > 0) {\n this.#debouncePersist();\n }\n }\n\n /**\n * Debounce persistence of changed chains to storage.\n */\n #debouncePersist(): void {\n if (this.#persistDebounceTimer) {\n clearTimeout(this.#persistDebounceTimer);\n }\n\n this.#persistDebounceTimer = setTimeout(() => {\n // Note: #persistChangedChains handles errors internally via #saveChainCacheToStorage,\n // so this promise will not reject.\n // eslint-disable-next-line @typescript-eslint/no-floating-promises\n this.#persistChangedChains();\n }, TokenListController.#persistDebounceMs);\n }\n\n /**\n * Persist only the chains that have changed to storage.\n * Reduces write amplification by skipping unchanged chains.\n *\n * If a persist operation is already in-flight, this method returns early\n * and reschedules the debounce to ensure accumulated changes are retried\n * after the current operation completes.\n *\n * @returns A promise that resolves when changed chains are persisted.\n */\n async #persistChangedChains(): Promise<void> {\n if (this.#persistInFlightPromise) {\n // Reschedule debounce to retry accumulated changes after in-flight persist completes\n if (this.#changedChainsToPersist.size > 0) {\n this.#debouncePersist();\n }\n return;\n }\n\n const chainsToPersist = [...this.#changedChainsToPersist];\n this.#changedChainsToPersist.clear();\n\n if (chainsToPersist.length === 0) {\n return;\n }\n\n this.#persistInFlightPromise = Promise.all(\n chainsToPersist.map((chainId) => this.#saveChainCacheToStorage(chainId)),\n ).then(() => undefined);\n\n try {\n await this.#persistInFlightPromise;\n } finally {\n this.#persistInFlightPromise = undefined;\n }\n }\n\n /**\n * Synchronize tokensChainsCache between state and storage bidirectionally.\n *\n * This method:\n * 1. Loads cached chains from storage (per-chain files) in parallel\n * 2. Merges loaded data into state (preferring existing state to avoid overwriting fresh data)\n * 3. Persists any chains that exist in state but not in storage\n *\n * Called during initialization to ensure state and storage are consistent.\n *\n * @returns A promise that resolves when synchronization is complete.\n */\n async #synchronizeCacheWithStorage(): Promise<void> {\n try {\n const allKeys = await this.messenger.call(\n 'StorageService:getAllKeys',\n name,\n );\n\n // Filter keys that belong to tokensChainsCache (per-chain files)\n const cacheKeys = allKeys.filter((key) =>\n key.startsWith(`${TokenListController.#storageKeyPrefix}:`),\n );\n\n // Load all chains in parallel\n const chainCaches = await Promise.all(\n cacheKeys.map(async (key) => {\n // Extract chainId from key: 'tokensChainsCache:0x1' → '0x1'\n const chainId = key.split(':')[1] as Hex;\n\n const { result, error } = await this.messenger.call(\n 'StorageService:getItem',\n name,\n key,\n );\n\n if (error) {\n console.error(\n `TokenListController: Error loading cache for ${chainId}:`,\n error,\n );\n return null;\n }\n\n return result ? { chainId, data: result as DataCache } : null;\n }),\n );\n\n // Build complete cache from loaded chains\n const loadedCache: TokensChainsCache = {};\n chainCaches.forEach((chainCache) => {\n if (chainCache) {\n loadedCache[chainCache.chainId] = chainCache.data;\n }\n });\n\n // Merge loaded cache with existing state, preferring existing data\n // (which may be fresher if fetched during initialization)\n if (Object.keys(loadedCache).length > 0) {\n // Track which chains we're actually loading from storage\n // These will be skipped in the next #onCacheChanged to avoid redundant writes\n for (const chainId of Object.keys(loadedCache) as Hex[]) {\n if (!this.state.tokensChainsCache[chainId]) {\n this.#chainsLoadedFromStorage.add(chainId);\n }\n }\n\n this.update((state) => {\n // Only load chains that don't already exist in state\n // This prevents overwriting fresh API data with stale cached data\n for (const [chainId, cacheData] of Object.entries(loadedCache)) {\n if (!state.tokensChainsCache[chainId as Hex]) {\n state.tokensChainsCache[chainId as Hex] = cacheData;\n }\n }\n });\n\n // Note: The update() call above only triggers #onCacheChanged if chains\n // were actually added to state. If initial state already contains chains\n // from storage, the update() is a no-op and #onCacheChanged is never called.\n }\n\n // Persist chains that exist in state but were not loaded from storage.\n // This handles the case where initial state contains chains that don't exist\n // in storage yet (e.g., fresh data from API). Without this, those chains\n // would be lost on the next app restart.\n const loadedChainIds = new Set(Object.keys(loadedCache) as Hex[]);\n const chainsInState = new Set(\n Object.keys(this.state.tokensChainsCache) as Hex[],\n );\n for (const chainId of chainsInState) {\n if (!loadedChainIds.has(chainId)) {\n this.#changedChainsToPersist.add(chainId);\n }\n }\n\n // Persist any chains that need to be saved\n if (this.#changedChainsToPersist.size > 0) {\n this.#debouncePersist();\n }\n\n this.#previousTokensChainsCache = { ...this.state.tokensChainsCache };\n } catch (error) {\n console.error(\n 'TokenListController: Failed to load cache from storage:',\n error,\n );\n }\n }\n\n /**\n * Save a specific chain's cache to StorageService.\n * This persists only the updated chain's data, reducing write amplification.\n *\n * @param chainId - The chain ID to save.\n * @returns A promise that resolves when saving is complete.\n */\n async #saveChainCacheToStorage(chainId: Hex): Promise<void> {\n try {\n const chainData = this.state.tokensChainsCache[chainId];\n\n if (!chainData) {\n console.warn(`TokenListController: No cache data for chain ${chainId}`);\n return;\n }\n\n const storageKey = TokenListController.#getChainStorageKey(chainId);\n\n await this.messenger.call(\n 'StorageService:setItem',\n name,\n storageKey,\n chainData,\n );\n } catch (error) {\n console.error(\n `TokenListController: Failed to save cache for ${chainId}:`,\n error,\n );\n }\n }\n\n /**\n * Updates state and restarts polling on changes to the network controller\n * state.\n *\n * @param networkControllerState - The updated network controller state.\n */\n async #onNetworkControllerStateChange(\n networkControllerState: NetworkState,\n ): Promise<void> {\n const selectedNetworkClient = this.messenger.call(\n 'NetworkController:getNetworkClientById',\n networkControllerState.selectedNetworkClientId,\n );\n const { chainId } = selectedNetworkClient.configuration;\n\n if (this.#chainId !== chainId) {\n this.#abortController.abort();\n this.#abortController = new AbortController();\n this.#chainId = chainId;\n }\n }\n\n // Eventually we want to remove start/restart/stop controls in favor of new _executePoll API\n // Maintaining these functions for now until we can safely deprecate them for backwards compatibility\n /**\n * Start polling for the token list.\n *\n * @deprecated This method is deprecated and will be removed in the future.\n * Consider using the new polling approach instead\n */\n async start(): Promise<void> {\n if (!isTokenListSupportedForNetwork(this.#chainId)) {\n return;\n }\n await this.#startDeprecatedPolling();\n }\n\n /**\n * Restart polling for the token list.\n *\n * @deprecated This method is deprecated and will be removed in the future.\n * Consider using the new polling approach instead\n */\n async restart(): Promise<void> {\n this.#stopPolling();\n await this.#startDeprecatedPolling();\n }\n\n /**\n * Stop polling for the token list.\n *\n * @deprecated This method is deprecated and will be removed in the future.\n * Consider using the new polling approach instead\n */\n stop(): void {\n this.#stopPolling();\n }\n\n /**\n * This stops any active polling.\n *\n * @deprecated This method is deprecated and will be removed in the future.\n * Consider using the new polling approach instead\n */\n override destroy(): void {\n super.destroy();\n this.#stopPolling();\n\n // Cancel any pending debounced persistence operations\n if (this.#persistDebounceTimer) {\n clearTimeout(this.#persistDebounceTimer);\n this.#persistDebounceTimer = undefined;\n }\n this.#changedChainsToPersist.clear();\n this.#chainsLoadedFromStorage.clear();\n }\n\n /**\n * This stops any active polling intervals.\n *\n * @deprecated This method is deprecated and will be removed in the future.\n * Consider using the new polling approach instead\n */\n #stopPolling(): void {\n if (this.#intervalId) {\n clearInterval(this.#intervalId);\n }\n }\n\n /**\n * Starts a new polling interval for a given chainId (this should be deprecated in favor of _executePoll)\n *\n * @deprecated This method is deprecated and will be removed in the future.\n * Consider using the new polling approach instead\n */\n async #startDeprecatedPolling(): Promise<void> {\n // renaming this to avoid collision with base class\n await safelyExecute(() => this.fetchTokenList(this.#chainId));\n // TODO: Either fix this lint violation or explain why it's necessary to ignore.\n // eslint-disable-next-line @typescript-eslint/no-misused-promises\n this.#intervalId = setInterval(async () => {\n await safelyExecute(() => this.fetchTokenList(this.#chainId));\n }, this.#intervalDelay);\n }\n\n /**\n * This starts a new polling loop for any given chain. Under the hood it is deduping polls\n *\n * @param input - The input for the poll.\n * @param input.chainId - The chainId of the chain to trigger the fetch.\n * @returns A promise that resolves when this operation completes.\n */\n async _executePoll({ chainId }: TokenListPollingInput): Promise<void> {\n return this.fetchTokenList(chainId);\n }\n\n /**\n * Fetching token list from the Token Service API. This will fetch tokens across chains.\n * State changes are automatically persisted via the stateChange subscription.\n *\n * @param chainId - The chainId of the current chain triggering the fetch.\n */\n async fetchTokenList(chainId: Hex): Promise<void> {\n if (this.isCacheValid(chainId)) {\n return;\n }\n\n // Fetch fresh token list from the API\n const tokensFromAPI = await safelyExecute(\n () =>\n fetchTokenListByChainId(\n chainId,\n this.#abortController.signal,\n ) as Promise<TokenListToken[]>,\n );\n\n // Have response - process and update list\n if (tokensFromAPI) {\n // Format tokens from API (HTTP) and update tokenList\n const tokenList: TokenListMap = {};\n for (const token of tokensFromAPI) {\n tokenList[token.address] = {\n ...token,\n aggregators: formatAggregatorNames(token.aggregators),\n iconUrl: formatIconUrlWithProxy({\n chainId,\n tokenAddress: token.address,\n }),\n };\n }\n\n // Update state - persistence happens automatically via subscription\n const newDataCache: DataCache = {\n data: tokenList,\n timestamp: Date.now(),\n };\n this.update((state) => {\n state.tokensChainsCache[chainId] = newDataCache;\n });\n return;\n }\n\n // No response - fallback to previous state, or initialise empty.\n // Only initialize with a new timestamp if there's no existing cache.\n // If there's existing cache, keep it as-is without updating the timestamp\n // to avoid making stale data appear \"fresh\" and preventing retry attempts.\n if (!tokensFromAPI) {\n const existingCache = this.state.tokensChainsCache[chainId];\n if (!existingCache) {\n // No existing cache - initialize empty (persistence happens automatically)\n const newDataCache: DataCache = { data: {}, timestamp: Date.now() };\n this.update((state) => {\n state.tokensChainsCache[chainId] = newDataCache;\n });\n }\n // If there's existing cache, keep it as-is (don't update timestamp or persist)\n }\n }\n\n isCacheValid(chainId: Hex): boolean {\n const { tokensChainsCache }: TokenListState = this.state;\n const timestamp: number | undefined = tokensChainsCache[chainId]?.timestamp;\n const now = Date.now();\n return (\n timestamp !== undefined && now - timestamp < this.#cacheRefreshThreshold\n );\n }\n}\n\nexport default TokenListController;\n"]}