@metamask/assets-controllers 94.0.0 → 94.1.0

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