@metamask-previews/assets-controllers 93.1.0-preview-1dd40c75 → 93.1.0-preview-267e79c3

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.
@@ -9,7 +9,7 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (
9
9
  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");
10
10
  return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
11
11
  };
12
- var _TokenBalancesController_instances, _TokenBalancesController_platform, _TokenBalancesController_queryAllAccounts, _TokenBalancesController_accountsApiChainIds, _TokenBalancesController_balanceFetchers, _TokenBalancesController_allTokens, _TokenBalancesController_detectedTokens, _TokenBalancesController_allIgnoredTokens, _TokenBalancesController_defaultInterval, _TokenBalancesController_websocketActivePollingInterval, _TokenBalancesController_chainPollingConfig, _TokenBalancesController_intervalPollingTimers, _TokenBalancesController_isControllerPollingActive, _TokenBalancesController_requestedChainIds, _TokenBalancesController_statusChangeDebouncer, _TokenBalancesController_normalizeAccountAddresses, _TokenBalancesController_chainIdsWithTokens, _TokenBalancesController_getProvider, _TokenBalancesController_getNetworkClient, _TokenBalancesController_createAccountsApiFetcher, _TokenBalancesController_startIntervalGroupPolling, _TokenBalancesController_startPollingForInterval, _TokenBalancesController_setPollingTimer, _TokenBalancesController_isTokenTracked, _TokenBalancesController_onTokensChanged, _TokenBalancesController_onNetworkChanged, _TokenBalancesController_onAccountRemoved, _TokenBalancesController_onAccountChanged, _TokenBalancesController_prepareBalanceUpdates, _TokenBalancesController_onAccountActivityBalanceUpdate, _TokenBalancesController_onAccountActivityStatusChanged, _TokenBalancesController_processAccumulatedStatusChanges;
12
+ var _TokenBalancesController_instances, _TokenBalancesController_platform, _TokenBalancesController_queryAllAccounts, _TokenBalancesController_accountsApiChainIds, _TokenBalancesController_balanceFetchers, _TokenBalancesController_allTokens, _TokenBalancesController_detectedTokens, _TokenBalancesController_allIgnoredTokens, _TokenBalancesController_tokensChainsCache, _TokenBalancesController_defaultInterval, _TokenBalancesController_websocketActivePollingInterval, _TokenBalancesController_chainPollingConfig, _TokenBalancesController_intervalPollingTimers, _TokenBalancesController_isControllerPollingActive, _TokenBalancesController_isUnlocked, _TokenBalancesController_requestedChainIds, _TokenBalancesController_statusChangeDebouncer, _TokenBalancesController_subscribeToControllers, _TokenBalancesController_registerActions, _TokenBalancesController_normalizeAccountAddresses, _TokenBalancesController_chainIdsWithTokens, _TokenBalancesController_getProvider, _TokenBalancesController_getNetworkClient, _TokenBalancesController_createAccountsApiFetcher, _TokenBalancesController_startIntervalGroupPolling, _TokenBalancesController_startPollingForInterval, _TokenBalancesController_setPollingTimer, _TokenBalancesController_stopAllPolling, _TokenBalancesController_getTargetChains, _TokenBalancesController_getAccountsAndJwt, _TokenBalancesController_fetchAllBalances, _TokenBalancesController_filterByTokenAddresses, _TokenBalancesController_getAccountsToProcess, _TokenBalancesController_applyTokenBalancesToState, _TokenBalancesController_buildNativeBalanceUpdates, _TokenBalancesController_buildStakedBalanceUpdates, _TokenBalancesController_importUntrackedTokens, _TokenBalancesController_isTokenTracked, _TokenBalancesController_onTokensChanged, _TokenBalancesController_onNetworkChanged, _TokenBalancesController_onAccountRemoved, _TokenBalancesController_onAccountChanged, _TokenBalancesController_prepareBalanceUpdates, _TokenBalancesController_onAccountActivityBalanceUpdate, _TokenBalancesController_onAccountActivityStatusChanged, _TokenBalancesController_processAccumulatedStatusChanges;
13
13
  import { Web3Provider } from "@ethersproject/providers";
14
14
  import { BNToHex, isValidHexAddress, safelyExecuteWithTimeout, toChecksumHexAddress, toHex } from "@metamask/controller-utils";
15
15
  import { StaticIntervalPollingController } from "@metamask/polling-controller";
@@ -31,19 +31,14 @@ const metadata = {
31
31
  usedInUi: true,
32
32
  },
33
33
  };
34
- // endregion
35
- // ────────────────────────────────────────────────────────────────────────────
36
- // region: Helper utilities
37
34
  const draft = (base, fn) => produce(base, fn);
38
35
  const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
39
36
  const checksum = (addr) => toChecksumHexAddress(addr);
40
37
  /**
41
- * Convert CAIP chain ID or hex chain ID to hex chain ID
42
- * Handles both CAIP-2 format (e.g., "eip155:1") and hex format (e.g., "0x1")
38
+ * Convert CAIP chain ID or hex chain ID to hex chain ID.
43
39
  *
44
- * @param chainId - CAIP chain ID (e.g., "eip155:1") or hex chain ID (e.g., "0x1")
45
- * @returns Hex chain ID (e.g., "0x1")
46
- * @throws {Error} If chainId is neither a valid CAIP-2 chain ID nor a hex string
40
+ * @param chainId - CAIP chain ID or hex chain ID.
41
+ * @returns Hex chain ID.
47
42
  */
48
43
  export const caipChainIdToHex = (chainId) => {
49
44
  if (isStrictHexString(chainId)) {
@@ -55,30 +50,26 @@ export const caipChainIdToHex = (chainId) => {
55
50
  throw new Error('caipChainIdToHex - Failed to provide CAIP-2 or Hex chainId');
56
51
  };
57
52
  /**
58
- * Extract token address from asset type
59
- * Returns tuple of [tokenAddress, isNativeToken] or null if invalid
53
+ * Extract token address from asset type.
60
54
  *
61
- * @param assetType - Asset type string (e.g., 'eip155:1/erc20:0x...' or 'eip155:1/slip44:60')
62
- * @returns Tuple of [tokenAddress, isNativeToken] or null if invalid
55
+ * @param assetType - Asset type string.
56
+ * @returns Tuple of [tokenAddress, isNativeToken] or null if invalid.
63
57
  */
64
58
  export const parseAssetType = (assetType) => {
65
59
  if (!isCaipAssetType(assetType)) {
66
60
  return null;
67
61
  }
68
62
  const parsed = parseCaipAssetType(assetType);
69
- // ERC20 token (e.g., "eip155:1/erc20:0x...")
70
63
  if (parsed.assetNamespace === 'erc20') {
71
64
  return [parsed.assetReference, false];
72
65
  }
73
- // Native token (e.g., "eip155:1/slip44:60")
74
66
  if (parsed.assetNamespace === 'slip44') {
75
67
  return [ZERO_ADDRESS, true];
76
68
  }
77
69
  return null;
78
70
  };
79
- // endregion
80
71
  // ────────────────────────────────────────────────────────────────────────────
81
- // region: Main controller
72
+ // Main controller
82
73
  export class TokenBalancesController extends StaticIntervalPollingController() {
83
74
  constructor({ messenger, interval = DEFAULT_INTERVAL_MS, websocketActivePollingInterval = DEFAULT_WEBSOCKET_ACTIVE_POLLING_INTERVAL_MS, chainPollingIntervals = {}, state = {}, queryMultipleAccounts = true, accountsApiChainIds = () => [], allowExternalServices = () => true, platform, }) {
84
75
  super({
@@ -95,6 +86,8 @@ export class TokenBalancesController extends StaticIntervalPollingController() {
95
86
  _TokenBalancesController_allTokens.set(this, {});
96
87
  _TokenBalancesController_detectedTokens.set(this, {});
97
88
  _TokenBalancesController_allIgnoredTokens.set(this, {});
89
+ /** Token metadata cache from TokenListController */
90
+ _TokenBalancesController_tokensChainsCache.set(this, {});
98
91
  /** Default polling interval for chains without specific configuration */
99
92
  _TokenBalancesController_defaultInterval.set(this, void 0);
100
93
  /** Polling interval when WebSocket is active and providing real-time updates */
@@ -105,6 +98,8 @@ export class TokenBalancesController extends StaticIntervalPollingController() {
105
98
  _TokenBalancesController_intervalPollingTimers.set(this, new Map());
106
99
  /** Track if controller-level polling is active */
107
100
  _TokenBalancesController_isControllerPollingActive.set(this, false);
101
+ /** Track if the keyring is unlocked */
102
+ _TokenBalancesController_isUnlocked.set(this, false);
108
103
  /** Store original chainIds from startPolling to preserve intent */
109
104
  _TokenBalancesController_requestedChainIds.set(this, []);
110
105
  /** Debouncing for rapid status changes to prevent excessive HTTP calls */
@@ -114,52 +109,34 @@ export class TokenBalancesController extends StaticIntervalPollingController() {
114
109
  });
115
110
  _TokenBalancesController_getProvider.set(this, (chainId) => {
116
111
  const { networkConfigurationsByChainId } = this.messenger.call('NetworkController:getState');
117
- const cfg = networkConfigurationsByChainId[chainId];
118
- const { networkClientId } = cfg.rpcEndpoints[cfg.defaultRpcEndpointIndex];
112
+ const networkConfig = networkConfigurationsByChainId[chainId];
113
+ const { networkClientId } = networkConfig.rpcEndpoints[networkConfig.defaultRpcEndpointIndex];
119
114
  const client = this.messenger.call('NetworkController:getNetworkClientById', networkClientId);
120
115
  return new Web3Provider(client.provider);
121
116
  });
122
117
  _TokenBalancesController_getNetworkClient.set(this, (chainId) => {
123
118
  const { networkConfigurationsByChainId } = this.messenger.call('NetworkController:getState');
124
- const cfg = networkConfigurationsByChainId[chainId];
125
- const { networkClientId } = cfg.rpcEndpoints[cfg.defaultRpcEndpointIndex];
119
+ const networkConfig = networkConfigurationsByChainId[chainId];
120
+ const { networkClientId } = networkConfig.rpcEndpoints[networkConfig.defaultRpcEndpointIndex];
126
121
  return this.messenger.call('NetworkController:getNetworkClientById', networkClientId);
127
122
  });
128
- /**
129
- * Creates an AccountsApiBalanceFetcher that only supports chains in the accountsApiChainIds array
130
- *
131
- * @returns A BalanceFetcher that wraps AccountsApiBalanceFetcher with chainId filtering
132
- */
133
123
  _TokenBalancesController_createAccountsApiFetcher.set(this, () => {
134
124
  const originalFetcher = new AccountsApiBalanceFetcher(__classPrivateFieldGet(this, _TokenBalancesController_platform, "f"), __classPrivateFieldGet(this, _TokenBalancesController_getProvider, "f"));
135
125
  return {
136
- supports: (chainId) => {
137
- // Only support chains that are both:
138
- // 1. In our specified accountsApiChainIds array
139
- // 2. Actually supported by the AccountsApi
140
- return (__classPrivateFieldGet(this, _TokenBalancesController_accountsApiChainIds, "f").call(this).includes(chainId) &&
141
- originalFetcher.supports(chainId));
142
- },
126
+ supports: (chainId) => __classPrivateFieldGet(this, _TokenBalancesController_accountsApiChainIds, "f").call(this).includes(chainId) &&
127
+ originalFetcher.supports(chainId),
143
128
  fetch: originalFetcher.fetch.bind(originalFetcher),
144
129
  };
145
130
  });
131
+ // ────────────────────────────────────────────────────────────────────────
132
+ // TokensController / Network / Accounts events
146
133
  _TokenBalancesController_onTokensChanged.set(this, async (state) => {
147
134
  const changed = [];
148
135
  let hasChanges = false;
149
- // Get chains that have existing balances
150
- const chainsWithBalances = new Set();
151
- for (const address of Object.keys(this.state.tokenBalances)) {
152
- const addressKey = address;
153
- for (const chainId of Object.keys(this.state.tokenBalances[addressKey] || {})) {
154
- chainsWithBalances.add(chainId);
155
- }
156
- }
157
- // Only process chains that are explicitly mentioned in the incoming state change
158
136
  const incomingChainIds = new Set([
159
137
  ...Object.keys(state.allTokens),
160
138
  ...Object.keys(state.allDetectedTokens),
161
139
  ]);
162
- // Only proceed if there are actual changes to chains that have balances or are being added
163
140
  const relevantChainIds = Array.from(incomingChainIds).filter((chainId) => {
164
141
  const id = chainId;
165
142
  const hasTokensNow = (state.allTokens[id] && Object.keys(state.allTokens[id]).length > 0) ||
@@ -168,20 +145,16 @@ export class TokenBalancesController extends StaticIntervalPollingController() {
168
145
  const hadTokensBefore = (__classPrivateFieldGet(this, _TokenBalancesController_allTokens, "f")[id] && Object.keys(__classPrivateFieldGet(this, _TokenBalancesController_allTokens, "f")[id]).length > 0) ||
169
146
  (__classPrivateFieldGet(this, _TokenBalancesController_detectedTokens, "f")[id] &&
170
147
  Object.keys(__classPrivateFieldGet(this, _TokenBalancesController_detectedTokens, "f")[id]).length > 0);
171
- // Check if there's an actual change in token state
172
148
  const hasTokenChange = !isEqual(state.allTokens[id], __classPrivateFieldGet(this, _TokenBalancesController_allTokens, "f")[id]) ||
173
149
  !isEqual(state.allDetectedTokens[id], __classPrivateFieldGet(this, _TokenBalancesController_detectedTokens, "f")[id]);
174
- // Process chains that have actual changes OR are new chains getting tokens
175
150
  return hasTokenChange || (!hadTokensBefore && hasTokensNow);
176
151
  });
177
- if (relevantChainIds.length === 0) {
178
- // No relevant changes, just update internal state
152
+ if (!relevantChainIds.length) {
179
153
  __classPrivateFieldSet(this, _TokenBalancesController_allTokens, state.allTokens, "f");
180
154
  __classPrivateFieldSet(this, _TokenBalancesController_detectedTokens, state.allDetectedTokens, "f");
181
155
  return;
182
156
  }
183
- // Handle both cleanup and updates in a single state update
184
- this.update((s) => {
157
+ this.update((currentState) => {
185
158
  for (const chainId of relevantChainIds) {
186
159
  const id = chainId;
187
160
  const hasTokensNow = (state.allTokens[id] &&
@@ -192,20 +165,20 @@ export class TokenBalancesController extends StaticIntervalPollingController() {
192
165
  Object.keys(__classPrivateFieldGet(this, _TokenBalancesController_allTokens, "f")[id]).length > 0) ||
193
166
  (__classPrivateFieldGet(this, _TokenBalancesController_detectedTokens, "f")[id] &&
194
167
  Object.keys(__classPrivateFieldGet(this, _TokenBalancesController_detectedTokens, "f")[id]).length > 0);
195
- if (!isEqual(state.allTokens[id], __classPrivateFieldGet(this, _TokenBalancesController_allTokens, "f")[id]) ||
196
- !isEqual(state.allDetectedTokens[id], __classPrivateFieldGet(this, _TokenBalancesController_detectedTokens, "f")[id])) {
197
- if (hasTokensNow) {
198
- // Chain still has tokens - mark for async balance update
199
- changed.push(id);
200
- }
201
- else if (hadTokensBefore) {
202
- // Chain had tokens before but doesn't now - clean up balances immediately
203
- for (const address of Object.keys(s.tokenBalances)) {
204
- const addressKey = address;
205
- if (s.tokenBalances[addressKey]?.[id]) {
206
- s.tokenBalances[addressKey][id] = {};
207
- hasChanges = true;
208
- }
168
+ const tokensChanged = !isEqual(state.allTokens[id], __classPrivateFieldGet(this, _TokenBalancesController_allTokens, "f")[id]) ||
169
+ !isEqual(state.allDetectedTokens[id], __classPrivateFieldGet(this, _TokenBalancesController_detectedTokens, "f")[id]);
170
+ if (!tokensChanged) {
171
+ continue;
172
+ }
173
+ if (hasTokensNow) {
174
+ changed.push(id);
175
+ }
176
+ else if (hadTokensBefore) {
177
+ for (const address of Object.keys(currentState.tokenBalances)) {
178
+ const addressKey = address;
179
+ if (currentState.tokenBalances[addressKey]?.[id]) {
180
+ currentState.tokenBalances[addressKey][id] = {};
181
+ hasChanges = true;
209
182
  }
210
183
  }
211
184
  }
@@ -214,7 +187,6 @@ export class TokenBalancesController extends StaticIntervalPollingController() {
214
187
  __classPrivateFieldSet(this, _TokenBalancesController_allTokens, state.allTokens, "f");
215
188
  __classPrivateFieldSet(this, _TokenBalancesController_detectedTokens, state.allDetectedTokens, "f");
216
189
  __classPrivateFieldSet(this, _TokenBalancesController_allIgnoredTokens, state.allIgnoredTokens, "f");
217
- // Only update balances for chains that still have tokens (and only if we haven't already updated state)
218
190
  if (changed.length && !hasChanges) {
219
191
  this.updateBalances({ chainIds: changed }).catch((error) => {
220
192
  console.warn('Error updating balances after token change:', error);
@@ -222,9 +194,7 @@ export class TokenBalancesController extends StaticIntervalPollingController() {
222
194
  }
223
195
  });
224
196
  _TokenBalancesController_onNetworkChanged.set(this, (state) => {
225
- // Check if any networks were removed by comparing with previous state
226
197
  const currentNetworks = new Set(Object.keys(state.networkConfigurationsByChainId));
227
- // Get all networks that currently have balances
228
198
  const networksWithBalances = new Set();
229
199
  for (const address of Object.keys(this.state.tokenBalances)) {
230
200
  const addressKey = address;
@@ -232,83 +202,59 @@ export class TokenBalancesController extends StaticIntervalPollingController() {
232
202
  networksWithBalances.add(network);
233
203
  }
234
204
  }
235
- // Find networks that were removed
236
205
  const removedNetworks = Array.from(networksWithBalances).filter((network) => !currentNetworks.has(network));
237
- if (removedNetworks.length > 0) {
238
- this.update((s) => {
239
- // Remove balances for all accounts on the deleted networks
240
- for (const address of Object.keys(s.tokenBalances)) {
241
- const addressKey = address;
242
- for (const removedNetwork of removedNetworks) {
243
- const networkKey = removedNetwork;
244
- if (s.tokenBalances[addressKey]?.[networkKey]) {
245
- delete s.tokenBalances[addressKey][networkKey];
246
- }
206
+ if (!removedNetworks.length) {
207
+ return;
208
+ }
209
+ this.update((currentState) => {
210
+ for (const address of Object.keys(currentState.tokenBalances)) {
211
+ const addressKey = address;
212
+ for (const removedNetwork of removedNetworks) {
213
+ const networkKey = removedNetwork;
214
+ if (currentState.tokenBalances[addressKey]?.[networkKey]) {
215
+ delete currentState.tokenBalances[addressKey][networkKey];
247
216
  }
248
217
  }
249
- });
250
- }
218
+ }
219
+ });
251
220
  });
252
221
  _TokenBalancesController_onAccountRemoved.set(this, (addr) => {
253
222
  if (!isStrictHexString(addr) || !isValidHexAddress(addr)) {
254
223
  return;
255
224
  }
256
- this.update((s) => {
257
- delete s.tokenBalances[addr];
225
+ this.update((currentState) => {
226
+ delete currentState.tokenBalances[addr];
258
227
  });
259
228
  });
260
- /**
261
- * Handle account selection changes
262
- * Triggers immediate balance fetch to ensure we have the latest balances
263
- * since WebSocket only provides updates for changes going forward
264
- */
265
229
  _TokenBalancesController_onAccountChanged.set(this, () => {
266
- // Fetch balances for all chains with tokens when account changes
267
230
  const chainIds = __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_chainIdsWithTokens).call(this);
268
- if (chainIds.length > 0) {
269
- this.updateBalances({ chainIds }).catch(() => {
270
- // Silently handle polling errors
271
- });
231
+ if (!chainIds.length) {
232
+ return;
272
233
  }
234
+ this.updateBalances({ chainIds }).catch(() => {
235
+ // Silently handle polling errors
236
+ });
273
237
  });
274
- // ────────────────────────────────────────────────────────────────────────────
275
- // AccountActivityService event handlers
276
- /**
277
- * Handle real-time balance updates from AccountActivityService
278
- * Processes balance updates and updates the token balance state
279
- * If any balance update has an error, triggers fallback polling for the chain
280
- *
281
- * @param options0 - Balance update parameters
282
- * @param options0.address - Account address
283
- * @param options0.chain - CAIP chain identifier
284
- * @param options0.updates - Array of balance updates for the account
285
- */
286
238
  _TokenBalancesController_onAccountActivityBalanceUpdate.set(this, async ({ address, chain, updates, }) => {
287
239
  const chainId = caipChainIdToHex(chain);
288
240
  const checksummedAccount = checksum(address);
289
241
  try {
290
- // Process all balance updates at once
291
242
  const { tokenBalances, newTokens, nativeBalanceUpdates } = __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_prepareBalanceUpdates).call(this, updates, checksummedAccount, chainId);
292
- // Update state once with all token balances
293
243
  if (tokenBalances.length > 0) {
294
244
  this.update((state) => {
295
245
  var _a, _b;
296
- // Temporary until ADR to normalize all keys - tokenBalances state requires: account in lowercase, token in checksum
297
246
  const lowercaseAccount = checksummedAccount.toLowerCase();
298
247
  (_a = state.tokenBalances)[lowercaseAccount] ?? (_a[lowercaseAccount] = {});
299
248
  (_b = state.tokenBalances[lowercaseAccount])[chainId] ?? (_b[chainId] = {});
300
- // Apply all token balance updates
301
249
  for (const { tokenAddress, balance } of tokenBalances) {
302
250
  state.tokenBalances[lowercaseAccount][chainId][tokenAddress] =
303
251
  balance;
304
252
  }
305
253
  });
306
254
  }
307
- // Update native balances in AccountTrackerController
308
255
  if (nativeBalanceUpdates.length > 0) {
309
256
  this.messenger.call('AccountTrackerController:updateNativeBalances', nativeBalanceUpdates);
310
257
  }
311
- // Import any new tokens that were discovered (balance already updated from websocket)
312
258
  if (newTokens.length > 0) {
313
259
  await this.messenger.call('TokenDetectionController:addDetectedTokensViaWs', {
314
260
  tokensSlice: newTokens,
@@ -319,35 +265,22 @@ export class TokenBalancesController extends StaticIntervalPollingController() {
319
265
  catch (error) {
320
266
  console.warn(`Error updating balances from AccountActivityService for chain ${chain}, account ${address}:`, error);
321
267
  console.warn('Balance update data:', JSON.stringify(updates, null, 2));
322
- // On error, trigger fallback polling
323
268
  await this.updateBalances({ chainIds: [chainId] }).catch(() => {
324
269
  // Silently handle polling errors
325
270
  });
326
271
  }
327
272
  });
328
- /**
329
- * Handle status changes from AccountActivityService
330
- * Uses aggressive debouncing to prevent excessive HTTP calls from rapid up/down changes
331
- *
332
- * @param options0 - Status change event data
333
- * @param options0.chainIds - Array of chain identifiers
334
- * @param options0.status - Connection status ('up' for connected, 'down' for disconnected)
335
- */
336
273
  _TokenBalancesController_onAccountActivityStatusChanged.set(this, ({ chainIds, status, }) => {
337
- // Update pending changes (latest status wins for each chain)
338
274
  for (const chainId of chainIds) {
339
275
  __classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").pendingChanges.set(chainId, status);
340
276
  }
341
- // Clear existing timer to extend debounce window
342
277
  if (__classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").timer) {
343
278
  clearTimeout(__classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").timer);
344
279
  }
345
- // Set new timer - only process changes after activity settles
346
280
  __classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").timer = setTimeout(() => {
347
281
  __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_processAccumulatedStatusChanges).call(this);
348
- }, 5000); // 5-second debounce window
282
+ }, 5000);
349
283
  });
350
- // Normalize all account addresses to lowercase in existing state
351
284
  __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_normalizeAccountAddresses).call(this);
352
285
  __classPrivateFieldSet(this, _TokenBalancesController_platform, platform ?? 'extension', "f");
353
286
  __classPrivateFieldSet(this, _TokenBalancesController_queryAllAccounts, queryMultipleAccounts, "f");
@@ -355,7 +288,6 @@ export class TokenBalancesController extends StaticIntervalPollingController() {
355
288
  __classPrivateFieldSet(this, _TokenBalancesController_defaultInterval, interval, "f");
356
289
  __classPrivateFieldSet(this, _TokenBalancesController_websocketActivePollingInterval, websocketActivePollingInterval, "f");
357
290
  __classPrivateFieldSet(this, _TokenBalancesController_chainPollingConfig, { ...chainPollingIntervals }, "f");
358
- // Strategy order: API first, then RPC fallback
359
291
  __classPrivateFieldSet(this, _TokenBalancesController_balanceFetchers, [
360
292
  ...(accountsApiChainIds().length > 0 && allowExternalServices()
361
293
  ? [__classPrivateFieldGet(this, _TokenBalancesController_createAccountsApiFetcher, "f").call(this)]
@@ -366,302 +298,185 @@ export class TokenBalancesController extends StaticIntervalPollingController() {
366
298
  })),
367
299
  ], "f");
368
300
  this.setIntervalLength(interval);
369
- // initial token state & subscriptions
370
301
  const { allTokens, allDetectedTokens, allIgnoredTokens } = this.messenger.call('TokensController:getState');
371
302
  __classPrivateFieldSet(this, _TokenBalancesController_allTokens, allTokens, "f");
372
303
  __classPrivateFieldSet(this, _TokenBalancesController_detectedTokens, allDetectedTokens, "f");
373
304
  __classPrivateFieldSet(this, _TokenBalancesController_allIgnoredTokens, allIgnoredTokens, "f");
374
- this.messenger.subscribe('TokensController:stateChange', (tokensState) => {
375
- __classPrivateFieldGet(this, _TokenBalancesController_onTokensChanged, "f").call(this, tokensState).catch((error) => {
376
- console.warn('Error handling token state change:', error);
377
- });
378
- });
379
- this.messenger.subscribe('NetworkController:stateChange', __classPrivateFieldGet(this, _TokenBalancesController_onNetworkChanged, "f"));
380
- this.messenger.subscribe('KeyringController:accountRemoved', __classPrivateFieldGet(this, _TokenBalancesController_onAccountRemoved, "f"));
381
- this.messenger.subscribe('AccountsController:selectedEvmAccountChange', __classPrivateFieldGet(this, _TokenBalancesController_onAccountChanged, "f"));
382
- // Register action handlers for polling interval control
383
- this.messenger.registerActionHandler(`TokenBalancesController:updateChainPollingConfigs`, this.updateChainPollingConfigs.bind(this));
384
- this.messenger.registerActionHandler(`TokenBalancesController:getChainPollingConfig`, this.getChainPollingConfig.bind(this));
385
- // Subscribe to AccountActivityService balance updates for real-time updates
386
- this.messenger.subscribe('AccountActivityService:balanceUpdated', __classPrivateFieldGet(this, _TokenBalancesController_onAccountActivityBalanceUpdate, "f").bind(this));
387
- // Subscribe to AccountActivityService status changes for dynamic polling management
388
- this.messenger.subscribe('AccountActivityService:statusChanged', __classPrivateFieldGet(this, _TokenBalancesController_onAccountActivityStatusChanged, "f").bind(this));
305
+ const { tokensChainsCache } = this.messenger.call('TokenListController:getState');
306
+ __classPrivateFieldSet(this, _TokenBalancesController_tokensChainsCache, tokensChainsCache, "f");
307
+ const { isUnlocked } = this.messenger.call('KeyringController:getState');
308
+ __classPrivateFieldSet(this, _TokenBalancesController_isUnlocked, isUnlocked, "f");
309
+ __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_subscribeToControllers).call(this);
310
+ __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_registerActions).call(this);
389
311
  }
312
+ // ────────────────────────────────────────────────────────────────────────
313
+ // Address + network helpers
390
314
  /**
391
- * Override to support per-chain polling intervals by grouping chains by interval
315
+ * Whether the controller is active (keyring is unlocked).
316
+ * When locked, balance updates should be skipped.
392
317
  *
393
- * @param options0 - The polling options
394
- * @param options0.chainIds - Chain IDs to start polling for
318
+ * @returns Whether the keyring is unlocked.
395
319
  */
320
+ get isActive() {
321
+ return __classPrivateFieldGet(this, _TokenBalancesController_isUnlocked, "f");
322
+ }
323
+ // ────────────────────────────────────────────────────────────────────────
324
+ // Polling overrides
396
325
  _startPolling({ chainIds }) {
397
- // Store the original chainIds to preserve intent across config updates
398
326
  __classPrivateFieldSet(this, _TokenBalancesController_requestedChainIds, [...chainIds], "f");
399
327
  __classPrivateFieldSet(this, _TokenBalancesController_isControllerPollingActive, true, "f");
400
328
  __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_startIntervalGroupPolling).call(this, chainIds, true);
401
329
  }
402
- /**
403
- * Override to handle our custom polling approach
404
- *
405
- * @param tokenSetId - The token set ID to stop polling for
406
- */
407
330
  _stopPollingByPollingTokenSetId(tokenSetId) {
408
- let parsedTokenSetId;
409
331
  let chainsToStop = [];
410
332
  try {
411
- parsedTokenSetId = JSON.parse(tokenSetId);
412
- chainsToStop = parsedTokenSetId.chainIds || [];
333
+ const parsedTokenSetId = JSON.parse(tokenSetId);
334
+ chainsToStop = parsedTokenSetId.chainIds ?? [];
413
335
  }
414
336
  catch (error) {
415
337
  console.warn('Failed to parse tokenSetId, stopping all polling:', error);
416
- // Fallback: stop all polling if we can't parse the tokenSetId
417
- __classPrivateFieldSet(this, _TokenBalancesController_isControllerPollingActive, false, "f");
418
- __classPrivateFieldSet(this, _TokenBalancesController_requestedChainIds, [], "f");
419
- __classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").forEach((timer) => clearInterval(timer));
420
- __classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").clear();
338
+ __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_stopAllPolling).call(this);
421
339
  return;
422
340
  }
423
- // Compare with current chains - only stop if it matches our current session
424
341
  const currentChainsSet = new Set(__classPrivateFieldGet(this, _TokenBalancesController_requestedChainIds, "f"));
425
342
  const stopChainsSet = new Set(chainsToStop);
426
- // Check if this stop request is for our current session
427
343
  const isCurrentSession = currentChainsSet.size === stopChainsSet.size &&
428
344
  [...currentChainsSet].every((chain) => stopChainsSet.has(chain));
429
345
  if (isCurrentSession) {
430
- __classPrivateFieldSet(this, _TokenBalancesController_isControllerPollingActive, false, "f");
431
- __classPrivateFieldSet(this, _TokenBalancesController_requestedChainIds, [], "f");
432
- __classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").forEach((timer) => clearInterval(timer));
433
- __classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").clear();
346
+ __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_stopAllPolling).call(this);
434
347
  }
435
348
  }
436
- /**
437
- * Get polling configuration for a chain (includes default fallback)
438
- *
439
- * @param chainId - The chain ID to get config for
440
- * @returns The polling configuration for the chain
441
- */
442
349
  getChainPollingConfig(chainId) {
443
350
  return (__classPrivateFieldGet(this, _TokenBalancesController_chainPollingConfig, "f")[chainId] ?? {
444
351
  interval: __classPrivateFieldGet(this, _TokenBalancesController_defaultInterval, "f"),
445
352
  });
446
353
  }
447
354
  async _executePoll({ chainIds, queryAllAccounts = false, }) {
448
- // This won't be called with our custom implementation, but keep for compatibility
449
355
  await this.updateBalances({ chainIds, queryAllAccounts });
450
356
  }
451
- /**
452
- * Update multiple chain polling configurations at once
453
- *
454
- * @param configs - Object mapping chain IDs to polling configurations
455
- * @param options - Optional configuration for the update behavior
456
- * @param options.immediateUpdate - Whether to immediately fetch balances after updating configs (default: true)
457
- */
458
357
  updateChainPollingConfigs(configs, options = { immediateUpdate: true }) {
459
358
  Object.assign(__classPrivateFieldGet(this, _TokenBalancesController_chainPollingConfig, "f"), configs);
460
- // If polling is currently active, restart with new interval groupings
461
359
  if (__classPrivateFieldGet(this, _TokenBalancesController_isControllerPollingActive, "f")) {
462
- // Restart polling with immediate fetch by default, unless explicitly disabled
463
360
  __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_startIntervalGroupPolling).call(this, __classPrivateFieldGet(this, _TokenBalancesController_requestedChainIds, "f"), options.immediateUpdate);
464
361
  }
465
362
  }
466
- async updateBalances({ chainIds, queryAllAccounts = false, } = {}) {
467
- const targetChains = chainIds ?? __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_chainIdsWithTokens).call(this);
468
- if (!targetChains.length) {
363
+ // ────────────────────────────────────────────────────────────────────────
364
+ // Balances update (main flow, refactored)
365
+ async updateBalances({ chainIds, tokenAddresses, queryAllAccounts = false, } = {}) {
366
+ if (!this.isActive) {
469
367
  return;
470
368
  }
471
- const { address: selected } = this.messenger.call('AccountsController:getSelectedAccount');
472
- const allAccounts = this.messenger.call('AccountsController:listAccounts');
473
- const jwtToken = await safelyExecuteWithTimeout(() => {
474
- return this.messenger.call('AuthenticationController:getBearerToken');
475
- }, false, 5000);
476
- const aggregated = [];
477
- let remainingChains = [...targetChains];
478
- // Try each fetcher in order, removing successfully processed chains
479
- for (const fetcher of __classPrivateFieldGet(this, _TokenBalancesController_balanceFetchers, "f")) {
480
- const supportedChains = remainingChains.filter((c) => fetcher.supports(c));
481
- if (!supportedChains.length) {
482
- continue;
483
- }
484
- try {
485
- const result = await fetcher.fetch({
486
- chainIds: supportedChains,
487
- queryAllAccounts: queryAllAccounts ?? __classPrivateFieldGet(this, _TokenBalancesController_queryAllAccounts, "f"),
488
- selectedAccount: selected,
489
- allAccounts,
490
- jwtToken,
491
- });
492
- if (result.balances && result.balances.length > 0) {
493
- aggregated.push(...result.balances);
494
- // Remove chains that were successfully processed
495
- const processedChains = new Set(result.balances.map((b) => b.chainId));
496
- remainingChains = remainingChains.filter((chain) => !processedChains.has(chain));
497
- }
498
- // Add unprocessed chains back to remainingChains for next fetcher
499
- if (result.unprocessedChainIds &&
500
- result.unprocessedChainIds.length > 0) {
501
- const currentRemainingChains = remainingChains;
502
- const chainsToAdd = result.unprocessedChainIds.filter((chainId) => supportedChains.includes(chainId) &&
503
- !currentRemainingChains.includes(chainId));
504
- remainingChains.push(...chainsToAdd);
505
- }
506
- }
507
- catch (error) {
508
- console.warn(`Balance fetcher failed for chains ${supportedChains.join(', ')}: ${String(error)}`);
509
- // Continue to next fetcher (fallback)
510
- }
511
- // If all chains have been processed, break early
512
- if (remainingChains.length === 0) {
513
- break;
514
- }
369
+ const targetChains = __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_getTargetChains).call(this, chainIds);
370
+ if (!targetChains.length) {
371
+ return;
515
372
  }
516
- // Determine which accounts to process based on queryAllAccounts parameter
517
- const accountsToProcess = (queryAllAccounts ?? __classPrivateFieldGet(this, _TokenBalancesController_queryAllAccounts, "f"))
518
- ? allAccounts.map((a) => a.address)
519
- : [selected];
373
+ const { selectedAccount, allAccounts, jwtToken } = await __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_getAccountsAndJwt).call(this);
374
+ const aggregatedBalances = await __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_fetchAllBalances).call(this, {
375
+ targetChains,
376
+ selectedAccount,
377
+ allAccounts,
378
+ jwtToken,
379
+ queryAllAccounts: queryAllAccounts ?? __classPrivateFieldGet(this, _TokenBalancesController_queryAllAccounts, "f"),
380
+ });
381
+ const filteredAggregated = __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_filterByTokenAddresses).call(this, aggregatedBalances, tokenAddresses);
382
+ const accountsToProcess = __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_getAccountsToProcess).call(this, queryAllAccounts, allAccounts, selectedAccount);
520
383
  const prev = this.state;
521
- const next = draft(prev, (d) => {
522
- var _a, _b;
523
- // Initialize account and chain structures if they don't exist, but preserve existing balances
524
- for (const chainId of targetChains) {
525
- for (const account of accountsToProcess) {
526
- // Ensure the nested structure exists without overwriting existing balances
527
- (_a = d.tokenBalances)[account] ?? (_a[account] = {});
528
- (_b = d.tokenBalances[account])[chainId] ?? (_b[chainId] = {});
529
- // Initialize tokens from allTokens only if they don't exist yet
530
- const chainTokens = __classPrivateFieldGet(this, _TokenBalancesController_allTokens, "f")[chainId];
531
- if (chainTokens?.[account]) {
532
- Object.values(chainTokens[account]).forEach((token) => {
533
- const tokenAddress = checksum(token.address);
534
- // Only initialize if the token balance doesn't exist yet
535
- if (!(tokenAddress in d.tokenBalances[account][chainId])) {
536
- d.tokenBalances[account][chainId][tokenAddress] = '0x0';
537
- }
538
- });
539
- }
540
- // Initialize tokens from allDetectedTokens only if they don't exist yet
541
- const detectedChainTokens = __classPrivateFieldGet(this, _TokenBalancesController_detectedTokens, "f")[chainId];
542
- if (detectedChainTokens?.[account]) {
543
- Object.values(detectedChainTokens[account]).forEach((token) => {
544
- const tokenAddress = checksum(token.address);
545
- // Only initialize if the token balance doesn't exist yet
546
- if (!(tokenAddress in d.tokenBalances[account][chainId])) {
547
- d.tokenBalances[account][chainId][tokenAddress] = '0x0';
548
- }
549
- });
550
- }
551
- }
552
- }
553
- // Update with actual fetched balances only if the value has changed
554
- aggregated.forEach(({ success, value, account, token, chainId }) => {
555
- var _a, _b;
556
- if (success && value !== undefined) {
557
- // Ensure all accounts we add/update are in lower-case
558
- const lowerCaseAccount = account.toLowerCase();
559
- const newBalance = toHex(value);
560
- const tokenAddress = checksum(token);
561
- const currentBalance = d.tokenBalances[lowerCaseAccount]?.[chainId]?.[tokenAddress];
562
- // Only update if the balance has actually changed
563
- if (currentBalance !== newBalance) {
564
- ((_b = ((_a = d.tokenBalances)[lowerCaseAccount] ?? (_a[lowerCaseAccount] = {})))[chainId] ?? (_b[chainId] = {}))[tokenAddress] = newBalance;
565
- }
566
- }
567
- });
384
+ const next = __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_applyTokenBalancesToState).call(this, {
385
+ prev,
386
+ targetChains,
387
+ accountsToProcess,
388
+ balances: filteredAggregated,
568
389
  });
569
390
  if (!isEqual(prev, next)) {
570
391
  this.update(() => next);
571
- const nativeBalances = aggregated.filter((r) => r.success && r.token === ZERO_ADDRESS);
572
- // Get current AccountTracker state to compare existing balances
573
392
  const accountTrackerState = this.messenger.call('AccountTrackerController:getState');
574
- // Update native token balances only if they have changed
575
- if (nativeBalances.length > 0) {
576
- const balanceUpdates = nativeBalances
577
- .map((balance) => ({
578
- address: balance.account,
579
- chainId: balance.chainId,
580
- balance: balance.value ? BNToHex(balance.value) : '0x0',
581
- }))
582
- .filter((update) => {
583
- const currentBalance = accountTrackerState.accountsByChainId[update.chainId]?.[checksum(update.address)]?.balance;
584
- // Only include if the balance has actually changed
585
- return currentBalance !== update.balance;
586
- });
587
- if (balanceUpdates.length > 0) {
588
- this.messenger.call('AccountTrackerController:updateNativeBalances', balanceUpdates);
589
- }
393
+ const nativeUpdates = __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_buildNativeBalanceUpdates).call(this, filteredAggregated, accountTrackerState);
394
+ if (nativeUpdates.length > 0) {
395
+ this.messenger.call('AccountTrackerController:updateNativeBalances', nativeUpdates);
590
396
  }
591
- // Filter and update staked balances in a single batch operation for better performance
592
- const stakedBalances = aggregated.filter((r) => {
593
- if (!r.success || r.token === ZERO_ADDRESS) {
594
- return false;
595
- }
596
- // Check if the chainId and token address match any staking contract
597
- const stakingContractAddress = STAKING_CONTRACT_ADDRESS_BY_CHAINID[r.chainId];
598
- return (stakingContractAddress &&
599
- stakingContractAddress.toLowerCase() === r.token.toLowerCase());
600
- });
601
- if (stakedBalances.length > 0) {
602
- const stakedBalanceUpdates = stakedBalances
603
- .map((balance) => ({
604
- address: balance.account,
605
- chainId: balance.chainId,
606
- stakedBalance: balance.value ? toHex(balance.value) : '0x0',
607
- }))
608
- .filter((update) => {
609
- const currentStakedBalance = accountTrackerState.accountsByChainId[update.chainId]?.[checksum(update.address)]?.stakedBalance;
610
- // Only include if the staked balance has actually changed
611
- return currentStakedBalance !== update.stakedBalance;
612
- });
613
- if (stakedBalanceUpdates.length > 0) {
614
- this.messenger.call('AccountTrackerController:updateStakedBalances', stakedBalanceUpdates);
615
- }
397
+ const stakedUpdates = __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_buildStakedBalanceUpdates).call(this, filteredAggregated, accountTrackerState);
398
+ if (stakedUpdates.length > 0) {
399
+ this.messenger.call('AccountTrackerController:updateStakedBalances', stakedUpdates);
616
400
  }
617
401
  }
402
+ await __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_importUntrackedTokens).call(this, filteredAggregated);
618
403
  }
619
404
  resetState() {
620
405
  this.update(() => ({ tokenBalances: {} }));
621
406
  }
622
- /**
623
- * Clean up all timers and resources when controller is destroyed
624
- */
407
+ // ────────────────────────────────────────────────────────────────────────
408
+ // Destroy
625
409
  destroy() {
626
410
  __classPrivateFieldSet(this, _TokenBalancesController_isControllerPollingActive, false, "f");
627
411
  __classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").forEach((timer) => clearInterval(timer));
628
412
  __classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").clear();
629
- // Clean up debouncing timer
630
413
  if (__classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").timer) {
631
414
  clearTimeout(__classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").timer);
632
415
  __classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").timer = null;
633
416
  }
634
- // Unregister action handlers
635
417
  this.messenger.unregisterActionHandler(`TokenBalancesController:updateChainPollingConfigs`);
636
418
  this.messenger.unregisterActionHandler(`TokenBalancesController:getChainPollingConfig`);
637
419
  super.destroy();
638
420
  }
639
421
  }
640
- _TokenBalancesController_platform = new WeakMap(), _TokenBalancesController_queryAllAccounts = new WeakMap(), _TokenBalancesController_accountsApiChainIds = new WeakMap(), _TokenBalancesController_balanceFetchers = new WeakMap(), _TokenBalancesController_allTokens = new WeakMap(), _TokenBalancesController_detectedTokens = new WeakMap(), _TokenBalancesController_allIgnoredTokens = new WeakMap(), _TokenBalancesController_defaultInterval = new WeakMap(), _TokenBalancesController_websocketActivePollingInterval = new WeakMap(), _TokenBalancesController_chainPollingConfig = new WeakMap(), _TokenBalancesController_intervalPollingTimers = new WeakMap(), _TokenBalancesController_isControllerPollingActive = new WeakMap(), _TokenBalancesController_requestedChainIds = new WeakMap(), _TokenBalancesController_statusChangeDebouncer = new WeakMap(), _TokenBalancesController_getProvider = new WeakMap(), _TokenBalancesController_getNetworkClient = new WeakMap(), _TokenBalancesController_createAccountsApiFetcher = new WeakMap(), _TokenBalancesController_onTokensChanged = new WeakMap(), _TokenBalancesController_onNetworkChanged = new WeakMap(), _TokenBalancesController_onAccountRemoved = new WeakMap(), _TokenBalancesController_onAccountChanged = new WeakMap(), _TokenBalancesController_onAccountActivityBalanceUpdate = new WeakMap(), _TokenBalancesController_onAccountActivityStatusChanged = new WeakMap(), _TokenBalancesController_instances = new WeakSet(), _TokenBalancesController_normalizeAccountAddresses = function _TokenBalancesController_normalizeAccountAddresses() {
422
+ _TokenBalancesController_platform = new WeakMap(), _TokenBalancesController_queryAllAccounts = new WeakMap(), _TokenBalancesController_accountsApiChainIds = new WeakMap(), _TokenBalancesController_balanceFetchers = new WeakMap(), _TokenBalancesController_allTokens = new WeakMap(), _TokenBalancesController_detectedTokens = new WeakMap(), _TokenBalancesController_allIgnoredTokens = new WeakMap(), _TokenBalancesController_tokensChainsCache = new WeakMap(), _TokenBalancesController_defaultInterval = new WeakMap(), _TokenBalancesController_websocketActivePollingInterval = new WeakMap(), _TokenBalancesController_chainPollingConfig = new WeakMap(), _TokenBalancesController_intervalPollingTimers = new WeakMap(), _TokenBalancesController_isControllerPollingActive = new WeakMap(), _TokenBalancesController_isUnlocked = new WeakMap(), _TokenBalancesController_requestedChainIds = new WeakMap(), _TokenBalancesController_statusChangeDebouncer = new WeakMap(), _TokenBalancesController_getProvider = new WeakMap(), _TokenBalancesController_getNetworkClient = new WeakMap(), _TokenBalancesController_createAccountsApiFetcher = new WeakMap(), _TokenBalancesController_onTokensChanged = new WeakMap(), _TokenBalancesController_onNetworkChanged = new WeakMap(), _TokenBalancesController_onAccountRemoved = new WeakMap(), _TokenBalancesController_onAccountChanged = new WeakMap(), _TokenBalancesController_onAccountActivityBalanceUpdate = new WeakMap(), _TokenBalancesController_onAccountActivityStatusChanged = new WeakMap(), _TokenBalancesController_instances = new WeakSet(), _TokenBalancesController_subscribeToControllers = function _TokenBalancesController_subscribeToControllers() {
423
+ this.messenger.subscribe('TokensController:stateChange', (tokensState) => {
424
+ __classPrivateFieldGet(this, _TokenBalancesController_onTokensChanged, "f").call(this, tokensState).catch((error) => {
425
+ console.warn('Error handling token state change:', error);
426
+ });
427
+ });
428
+ this.messenger.subscribe('NetworkController:stateChange', __classPrivateFieldGet(this, _TokenBalancesController_onNetworkChanged, "f"));
429
+ this.messenger.subscribe('TokenListController:stateChange', ({ tokensChainsCache }) => {
430
+ __classPrivateFieldSet(this, _TokenBalancesController_tokensChainsCache, tokensChainsCache, "f");
431
+ });
432
+ this.messenger.subscribe('KeyringController:unlock', () => {
433
+ __classPrivateFieldSet(this, _TokenBalancesController_isUnlocked, true, "f");
434
+ });
435
+ this.messenger.subscribe('KeyringController:lock', () => {
436
+ __classPrivateFieldSet(this, _TokenBalancesController_isUnlocked, false, "f");
437
+ });
438
+ this.messenger.subscribe('KeyringController:accountRemoved', __classPrivateFieldGet(this, _TokenBalancesController_onAccountRemoved, "f"));
439
+ this.messenger.subscribe('AccountsController:selectedEvmAccountChange', __classPrivateFieldGet(this, _TokenBalancesController_onAccountChanged, "f"));
440
+ this.messenger.subscribe('AccountActivityService:balanceUpdated', (event) => {
441
+ __classPrivateFieldGet(this, _TokenBalancesController_onAccountActivityBalanceUpdate, "f").call(this, event).catch((error) => {
442
+ console.warn('Error handling balance update:', error);
443
+ });
444
+ });
445
+ this.messenger.subscribe('AccountActivityService:statusChanged', __classPrivateFieldGet(this, _TokenBalancesController_onAccountActivityStatusChanged, "f").bind(this));
446
+ this.messenger.subscribe('TransactionController:transactionConfirmed', (transactionMeta) => {
447
+ this.updateBalances({
448
+ chainIds: [transactionMeta.chainId],
449
+ }).catch(() => {
450
+ // Silently handle balance update errors
451
+ });
452
+ });
453
+ this.messenger.subscribe('TransactionController:incomingTransactionsReceived', (incomingTransactions) => {
454
+ this.updateBalances({
455
+ chainIds: incomingTransactions.map((tx) => tx.chainId),
456
+ }).catch(() => {
457
+ // Silently handle balance update errors
458
+ });
459
+ });
460
+ }, _TokenBalancesController_registerActions = function _TokenBalancesController_registerActions() {
461
+ this.messenger.registerActionHandler(`TokenBalancesController:updateChainPollingConfigs`, this.updateChainPollingConfigs.bind(this));
462
+ this.messenger.registerActionHandler(`TokenBalancesController:getChainPollingConfig`, this.getChainPollingConfig.bind(this));
463
+ }, _TokenBalancesController_normalizeAccountAddresses = function _TokenBalancesController_normalizeAccountAddresses() {
464
+ var _a;
641
465
  const currentState = this.state.tokenBalances;
642
466
  const normalizedBalances = {};
643
- // Iterate through all accounts and normalize to lowercase
644
467
  for (const address of Object.keys(currentState)) {
645
468
  const lowercaseAddress = address.toLowerCase();
646
469
  const accountBalances = currentState[address];
647
470
  if (!accountBalances) {
648
471
  continue;
649
472
  }
650
- // If this lowercase address doesn't exist yet, create it
651
- if (!normalizedBalances[lowercaseAddress]) {
652
- normalizedBalances[lowercaseAddress] = {};
653
- }
654
- // Merge chain data
473
+ normalizedBalances[lowercaseAddress] ?? (normalizedBalances[lowercaseAddress] = {});
655
474
  for (const chainId of Object.keys(accountBalances)) {
656
475
  const chainIdKey = chainId;
657
- if (!normalizedBalances[lowercaseAddress][chainIdKey]) {
658
- normalizedBalances[lowercaseAddress][chainIdKey] = {};
659
- }
660
- // Merge token balances (later values override earlier ones if duplicates exist)
476
+ (_a = normalizedBalances[lowercaseAddress])[chainIdKey] ?? (_a[chainIdKey] = {});
661
477
  Object.assign(normalizedBalances[lowercaseAddress][chainIdKey], accountBalances[chainIdKey]);
662
478
  }
663
479
  }
664
- // Only update if there were changes
665
480
  if (Object.keys(currentState).length !==
666
481
  Object.keys(normalizedBalances).length ||
667
482
  Object.keys(currentState).some((addr) => addr !== addr.toLowerCase())) {
@@ -675,18 +490,15 @@ _TokenBalancesController_platform = new WeakMap(), _TokenBalancesController_quer
675
490
  ]),
676
491
  ];
677
492
  }, _TokenBalancesController_startIntervalGroupPolling = function _TokenBalancesController_startIntervalGroupPolling(chainIds, immediate = true) {
678
- // Stop any existing interval timers
679
493
  __classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").forEach((timer) => clearInterval(timer));
680
494
  __classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").clear();
681
- // Group chains by their polling intervals
682
495
  const intervalGroups = new Map();
683
496
  for (const chainId of chainIds) {
684
497
  const config = this.getChainPollingConfig(chainId);
685
- const existing = intervalGroups.get(config.interval) || [];
686
- existing.push(chainId);
687
- intervalGroups.set(config.interval, existing);
498
+ const group = intervalGroups.get(config.interval) ?? [];
499
+ group.push(chainId);
500
+ intervalGroups.set(config.interval, group);
688
501
  }
689
- // Start separate polling loop for each interval group
690
502
  for (const [interval, chainIdsGroup] of intervalGroups) {
691
503
  __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_startPollingForInterval).call(this, interval, chainIdsGroup, immediate);
692
504
  }
@@ -702,33 +514,237 @@ _TokenBalancesController_platform = new WeakMap(), _TokenBalancesController_quer
702
514
  console.warn(`Polling failed for chains ${chainIds.join(', ')} with interval ${interval}:`, error);
703
515
  }
704
516
  };
705
- // Poll immediately first if requested
706
517
  if (immediate) {
707
518
  pollFunction().catch((error) => {
708
519
  console.warn(`Immediate polling failed for chains ${chainIds.join(', ')}:`, error);
709
520
  });
710
521
  }
711
- // Then start regular interval polling
712
522
  __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_setPollingTimer).call(this, interval, chainIds, pollFunction);
713
523
  }, _TokenBalancesController_setPollingTimer = function _TokenBalancesController_setPollingTimer(interval, chainIds, pollFunction) {
714
- // Clear any existing timer for this interval first
715
- const existingTimer = __classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").get(interval);
716
- if (existingTimer) {
717
- clearInterval(existingTimer);
718
- }
719
524
  const timer = setInterval(() => {
720
525
  pollFunction().catch((error) => {
721
526
  console.warn(`Interval polling failed for chains ${chainIds.join(', ')}:`, error);
722
527
  });
723
528
  }, interval);
724
529
  __classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").set(interval, timer);
530
+ }, _TokenBalancesController_stopAllPolling = function _TokenBalancesController_stopAllPolling() {
531
+ __classPrivateFieldSet(this, _TokenBalancesController_isControllerPollingActive, false, "f");
532
+ __classPrivateFieldSet(this, _TokenBalancesController_requestedChainIds, [], "f");
533
+ __classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").forEach((timer) => clearInterval(timer));
534
+ __classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").clear();
535
+ }, _TokenBalancesController_getTargetChains = function _TokenBalancesController_getTargetChains(chainIds) {
536
+ return chainIds?.length ? chainIds : __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_chainIdsWithTokens).call(this);
537
+ }, _TokenBalancesController_getAccountsAndJwt = async function _TokenBalancesController_getAccountsAndJwt() {
538
+ const { address: selected } = this.messenger.call('AccountsController:getSelectedAccount');
539
+ const allAccounts = this.messenger.call('AccountsController:listAccounts');
540
+ const jwtToken = await safelyExecuteWithTimeout(() => {
541
+ return this.messenger.call('AuthenticationController:getBearerToken');
542
+ }, false, 5000);
543
+ return {
544
+ selectedAccount: selected,
545
+ allAccounts,
546
+ jwtToken,
547
+ };
548
+ }, _TokenBalancesController_fetchAllBalances = async function _TokenBalancesController_fetchAllBalances({ targetChains, selectedAccount, allAccounts, jwtToken, queryAllAccounts, }) {
549
+ const aggregated = [];
550
+ let remainingChains = [...targetChains];
551
+ for (const fetcher of __classPrivateFieldGet(this, _TokenBalancesController_balanceFetchers, "f")) {
552
+ const supportedChains = remainingChains.filter((chain) => fetcher.supports(chain));
553
+ if (!supportedChains.length) {
554
+ continue;
555
+ }
556
+ try {
557
+ const result = await fetcher.fetch({
558
+ chainIds: supportedChains,
559
+ queryAllAccounts,
560
+ selectedAccount,
561
+ allAccounts,
562
+ jwtToken,
563
+ });
564
+ if (result.balances?.length) {
565
+ aggregated.push(...result.balances);
566
+ const processed = new Set(result.balances.map((b) => b.chainId));
567
+ remainingChains = remainingChains.filter((chain) => !processed.has(chain));
568
+ }
569
+ if (result.unprocessedChainIds?.length) {
570
+ const currentRemaining = [...remainingChains];
571
+ const chainsToAdd = result.unprocessedChainIds.filter((chainId) => supportedChains.includes(chainId) &&
572
+ !currentRemaining.includes(chainId));
573
+ remainingChains.push(...chainsToAdd);
574
+ this.messenger
575
+ .call('TokenDetectionController:detectTokens', {
576
+ chainIds: result.unprocessedChainIds,
577
+ forceRpc: true,
578
+ })
579
+ .catch(() => {
580
+ // Silently handle token detection errors
581
+ });
582
+ }
583
+ }
584
+ catch (error) {
585
+ console.warn(`Balance fetcher failed for chains ${supportedChains.join(', ')}: ${String(error)}`);
586
+ this.messenger
587
+ .call('TokenDetectionController:detectTokens', {
588
+ chainIds: supportedChains,
589
+ forceRpc: true,
590
+ })
591
+ .catch(() => {
592
+ // Silently handle token detection errors
593
+ });
594
+ }
595
+ if (!remainingChains.length) {
596
+ break;
597
+ }
598
+ }
599
+ return aggregated;
600
+ }, _TokenBalancesController_filterByTokenAddresses = function _TokenBalancesController_filterByTokenAddresses(balances, tokenAddresses) {
601
+ if (!tokenAddresses?.length) {
602
+ return balances;
603
+ }
604
+ const lowered = tokenAddresses.map((a) => a.toLowerCase());
605
+ return balances.filter((balance) => lowered.includes(balance.token.toLowerCase()));
606
+ }, _TokenBalancesController_getAccountsToProcess = function _TokenBalancesController_getAccountsToProcess(queryAllAccountsParam, allAccounts, selectedAccount) {
607
+ const effectiveQueryAll = queryAllAccountsParam ?? __classPrivateFieldGet(this, _TokenBalancesController_queryAllAccounts, "f") ?? false;
608
+ if (!effectiveQueryAll) {
609
+ return [selectedAccount];
610
+ }
611
+ return allAccounts.map((account) => account.address);
612
+ }, _TokenBalancesController_applyTokenBalancesToState = function _TokenBalancesController_applyTokenBalancesToState({ prev, targetChains, accountsToProcess, balances, }) {
613
+ return draft(prev, (draftState) => {
614
+ var _a, _b;
615
+ for (const chainId of targetChains) {
616
+ for (const account of accountsToProcess) {
617
+ (_a = draftState.tokenBalances)[account] ?? (_a[account] = {});
618
+ (_b = draftState.tokenBalances[account])[chainId] ?? (_b[chainId] = {});
619
+ const chainTokens = __classPrivateFieldGet(this, _TokenBalancesController_allTokens, "f")[chainId];
620
+ if (chainTokens?.[account]) {
621
+ Object.values(chainTokens[account]).forEach((token) => {
622
+ var _a;
623
+ const tokenAddress = checksum(token.address);
624
+ (_a = draftState.tokenBalances[account][chainId])[tokenAddress] ?? (_a[tokenAddress] = '0x0');
625
+ });
626
+ }
627
+ const detectedChainTokens = __classPrivateFieldGet(this, _TokenBalancesController_detectedTokens, "f")[chainId];
628
+ if (detectedChainTokens?.[account]) {
629
+ Object.values(detectedChainTokens[account]).forEach((token) => {
630
+ var _a;
631
+ const tokenAddress = checksum(token.address);
632
+ (_a = draftState.tokenBalances[account][chainId])[tokenAddress] ?? (_a[tokenAddress] = '0x0');
633
+ });
634
+ }
635
+ }
636
+ }
637
+ balances.forEach(({ success, value, account, token, chainId }) => {
638
+ var _a, _b;
639
+ if (!success || value === undefined) {
640
+ return;
641
+ }
642
+ const lowerCaseAccount = account.toLowerCase();
643
+ const newBalance = toHex(value);
644
+ const tokenAddress = checksum(token);
645
+ const currentBalance = draftState.tokenBalances[lowerCaseAccount]?.[chainId]?.[tokenAddress];
646
+ if (currentBalance !== newBalance) {
647
+ ((_b = ((_a = draftState.tokenBalances)[lowerCaseAccount] ?? (_a[lowerCaseAccount] = {})))[chainId] ?? (_b[chainId] = {}))[tokenAddress] = newBalance;
648
+ }
649
+ });
650
+ });
651
+ }, _TokenBalancesController_buildNativeBalanceUpdates = function _TokenBalancesController_buildNativeBalanceUpdates(balances, accountTrackerState) {
652
+ const nativeBalances = balances.filter((balance) => balance.success && balance.token === ZERO_ADDRESS);
653
+ if (!nativeBalances.length) {
654
+ return [];
655
+ }
656
+ return nativeBalances
657
+ .map((balance) => ({
658
+ address: balance.account,
659
+ chainId: balance.chainId,
660
+ balance: balance.value ? BNToHex(balance.value) : '0x0',
661
+ }))
662
+ .filter((update) => {
663
+ const currentBalance = accountTrackerState.accountsByChainId[update.chainId]?.[checksum(update.address)]?.balance;
664
+ return currentBalance !== update.balance;
665
+ });
666
+ }, _TokenBalancesController_buildStakedBalanceUpdates = function _TokenBalancesController_buildStakedBalanceUpdates(balances, accountTrackerState) {
667
+ const stakedBalances = balances.filter((balance) => {
668
+ if (!balance.success || balance.token === ZERO_ADDRESS) {
669
+ return false;
670
+ }
671
+ const stakingContractAddress = STAKING_CONTRACT_ADDRESS_BY_CHAINID[balance.chainId];
672
+ return (stakingContractAddress &&
673
+ stakingContractAddress.toLowerCase() === balance.token.toLowerCase());
674
+ });
675
+ if (!stakedBalances.length) {
676
+ return [];
677
+ }
678
+ return stakedBalances
679
+ .map((balance) => ({
680
+ address: balance.account,
681
+ chainId: balance.chainId,
682
+ stakedBalance: balance.value ? toHex(balance.value) : '0x0',
683
+ }))
684
+ .filter((update) => {
685
+ const currentStakedBalance = accountTrackerState.accountsByChainId[update.chainId]?.[checksum(update.address)]?.stakedBalance;
686
+ return currentStakedBalance !== update.stakedBalance;
687
+ });
688
+ }, _TokenBalancesController_importUntrackedTokens =
689
+ /**
690
+ * Import untracked tokens that have non-zero balances.
691
+ * This mirrors the v2 behavior where only tokens with actual balances are added.
692
+ * Directly calls TokensController:addTokens for the polling flow.
693
+ *
694
+ * @param balances - Array of processed balance results from fetchers
695
+ */
696
+ async function _TokenBalancesController_importUntrackedTokens(balances) {
697
+ const untrackedTokensByChain = new Map();
698
+ for (const balance of balances) {
699
+ // Skip failed fetches, native tokens, and zero balances (like v2 did)
700
+ if (!balance.success ||
701
+ balance.token === ZERO_ADDRESS ||
702
+ !balance.value ||
703
+ balance.value.isZero()) {
704
+ continue;
705
+ }
706
+ const tokenAddress = checksum(balance.token);
707
+ const account = balance.account.toLowerCase();
708
+ if (!__classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_isTokenTracked).call(this, tokenAddress, account, balance.chainId)) {
709
+ const existing = untrackedTokensByChain.get(balance.chainId) ?? [];
710
+ if (!existing.includes(tokenAddress)) {
711
+ existing.push(tokenAddress);
712
+ untrackedTokensByChain.set(balance.chainId, existing);
713
+ }
714
+ }
715
+ }
716
+ // Add detected tokens directly via TokensController:addTokens (polling flow)
717
+ for (const [chainId, tokenAddresses] of untrackedTokensByChain) {
718
+ const tokensWithMetadata = [];
719
+ for (const tokenAddress of tokenAddresses) {
720
+ const lowercaseAddress = tokenAddress.toLowerCase();
721
+ const tokenData = __classPrivateFieldGet(this, _TokenBalancesController_tokensChainsCache, "f")[chainId]?.data?.[lowercaseAddress];
722
+ if (!tokenData) {
723
+ console.warn(`Token metadata not found in cache for ${tokenAddress} on chain ${chainId}`);
724
+ continue;
725
+ }
726
+ const { decimals, symbol, aggregators, iconUrl, name } = tokenData;
727
+ tokensWithMetadata.push({
728
+ address: tokenAddress,
729
+ decimals,
730
+ symbol,
731
+ aggregators,
732
+ image: iconUrl,
733
+ isERC721: false,
734
+ name,
735
+ });
736
+ }
737
+ if (tokensWithMetadata.length) {
738
+ const networkClientId = this.messenger.call('NetworkController:findNetworkClientIdByChainId', chainId);
739
+ await this.messenger.call('TokensController:addTokens', tokensWithMetadata, networkClientId);
740
+ }
741
+ }
725
742
  }, _TokenBalancesController_isTokenTracked = function _TokenBalancesController_isTokenTracked(tokenAddress, account, chainId) {
726
- // Check if token exists in allTokens
727
- if (__classPrivateFieldGet(this, _TokenBalancesController_allTokens, "f")?.[chainId]?.[account.toLowerCase()]?.some((token) => token.address === tokenAddress)) {
743
+ const normalizedAccount = account.toLowerCase();
744
+ if (__classPrivateFieldGet(this, _TokenBalancesController_allTokens, "f")?.[chainId]?.[normalizedAccount]?.some((token) => token.address === tokenAddress)) {
728
745
  return true;
729
746
  }
730
- // Check if token exists in allIgnoredTokens
731
- if (__classPrivateFieldGet(this, _TokenBalancesController_allIgnoredTokens, "f")?.[chainId]?.[account.toLowerCase()]?.some((token) => token === tokenAddress)) {
747
+ if (__classPrivateFieldGet(this, _TokenBalancesController_allIgnoredTokens, "f")?.[chainId]?.[normalizedAccount]?.some((token) => token === tokenAddress)) {
732
748
  return true;
733
749
  }
734
750
  return false;
@@ -738,31 +754,25 @@ _TokenBalancesController_platform = new WeakMap(), _TokenBalancesController_quer
738
754
  const nativeBalanceUpdates = [];
739
755
  for (const update of updates) {
740
756
  const { asset, postBalance } = update;
741
- // Throw if balance update has an error
742
757
  if (postBalance.error) {
743
758
  throw new Error('Balance update has error');
744
759
  }
745
- // Parse token address from asset type
746
760
  const parsed = parseAssetType(asset.type);
747
761
  if (!parsed) {
748
762
  throw new Error('Failed to parse asset type');
749
763
  }
750
764
  const [tokenAddress, isNativeToken] = parsed;
751
- // Validate token address
752
765
  if (!isStrictHexString(tokenAddress) ||
753
766
  !isValidHexAddress(tokenAddress)) {
754
767
  throw new Error('Invalid token address');
755
768
  }
756
769
  const checksumTokenAddress = checksum(tokenAddress);
757
770
  const isTracked = __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_isTokenTracked).call(this, checksumTokenAddress, account, chainId);
758
- // postBalance.amount is in hex format (raw units)
759
771
  const balanceHex = postBalance.amount;
760
- // Add token balance (tracked tokens, ignored tokens, and native tokens all get balance updates)
761
772
  tokenBalances.push({
762
773
  tokenAddress: checksumTokenAddress,
763
774
  balance: balanceHex,
764
775
  });
765
- // Add native balance update if this is a native token
766
776
  if (isNativeToken) {
767
777
  nativeBalanceUpdates.push({
768
778
  address: account,
@@ -770,7 +780,6 @@ _TokenBalancesController_platform = new WeakMap(), _TokenBalancesController_quer
770
780
  balance: balanceHex,
771
781
  });
772
782
  }
773
- // Handle untracked ERC20 tokens - queue for import
774
783
  if (!isNativeToken && !isTracked) {
775
784
  newTokens.push(checksumTokenAddress);
776
785
  }
@@ -780,28 +789,18 @@ _TokenBalancesController_platform = new WeakMap(), _TokenBalancesController_quer
780
789
  const changes = Array.from(__classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").pendingChanges.entries());
781
790
  __classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").pendingChanges.clear();
782
791
  __classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").timer = null;
783
- if (changes.length === 0) {
792
+ if (!changes.length) {
784
793
  return;
785
794
  }
786
- // Calculate final polling configurations
787
795
  const chainConfigs = {};
788
796
  for (const [chainId, status] of changes) {
789
- // Convert CAIP format (eip155:1) to hex format (0x1)
790
- // chainId is always in CAIP format from AccountActivityService
791
797
  const hexChainId = caipChainIdToHex(chainId);
792
- if (status === 'down') {
793
- // Chain is down - use default polling since no real-time updates available
794
- chainConfigs[hexChainId] = { interval: __classPrivateFieldGet(this, _TokenBalancesController_defaultInterval, "f") };
795
- }
796
- else {
797
- // Chain is up - use longer intervals since WebSocket provides real-time updates
798
- chainConfigs[hexChainId] = {
799
- interval: __classPrivateFieldGet(this, _TokenBalancesController_websocketActivePollingInterval, "f"),
800
- };
801
- }
798
+ chainConfigs[hexChainId] =
799
+ status === 'down'
800
+ ? { interval: __classPrivateFieldGet(this, _TokenBalancesController_defaultInterval, "f") }
801
+ : { interval: __classPrivateFieldGet(this, _TokenBalancesController_websocketActivePollingInterval, "f") };
802
802
  }
803
- // Add jitter to prevent synchronized requests across instances
804
- const jitterDelay = Math.random() * __classPrivateFieldGet(this, _TokenBalancesController_defaultInterval, "f"); // 0 to default interval
803
+ const jitterDelay = Math.random() * __classPrivateFieldGet(this, _TokenBalancesController_defaultInterval, "f");
805
804
  setTimeout(() => {
806
805
  this.updateChainPollingConfigs(chainConfigs, { immediateUpdate: true });
807
806
  }, jitterDelay);