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