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

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