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

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