@metamask-previews/assets-controllers 93.1.0-preview-c92c27f1 → 93.1.0-preview-d717276a

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