@metamask-previews/assets-controllers 79.0.1-preview-e8868d87 → 79.0.1-preview-5f3688c1

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 (33) hide show
  1. package/CHANGELOG.md +21 -2
  2. package/README.md +1 -1
  3. package/dist/TokenBalancesController.cjs +229 -8
  4. package/dist/TokenBalancesController.cjs.map +1 -1
  5. package/dist/TokenBalancesController.d.cts +27 -3
  6. package/dist/TokenBalancesController.d.cts.map +1 -1
  7. package/dist/TokenBalancesController.d.mts +27 -3
  8. package/dist/TokenBalancesController.d.mts.map +1 -1
  9. package/dist/TokenBalancesController.mjs +227 -8
  10. package/dist/TokenBalancesController.mjs.map +1 -1
  11. package/dist/TokenDetectionController.cjs +73 -44
  12. package/dist/TokenDetectionController.cjs.map +1 -1
  13. package/dist/TokenDetectionController.d.cts +37 -9
  14. package/dist/TokenDetectionController.d.cts.map +1 -1
  15. package/dist/TokenDetectionController.d.mts +37 -9
  16. package/dist/TokenDetectionController.d.mts.map +1 -1
  17. package/dist/TokenDetectionController.mjs +73 -44
  18. package/dist/TokenDetectionController.mjs.map +1 -1
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.cts +1 -1
  21. package/dist/index.d.cts.map +1 -1
  22. package/dist/index.d.mts +1 -1
  23. package/dist/index.d.mts.map +1 -1
  24. package/dist/index.mjs.map +1 -1
  25. package/dist/multi-chain-accounts-service/api-balance-fetcher.cjs +2 -6
  26. package/dist/multi-chain-accounts-service/api-balance-fetcher.cjs.map +1 -1
  27. package/dist/multi-chain-accounts-service/api-balance-fetcher.d.cts +1 -1
  28. package/dist/multi-chain-accounts-service/api-balance-fetcher.d.cts.map +1 -1
  29. package/dist/multi-chain-accounts-service/api-balance-fetcher.d.mts +1 -1
  30. package/dist/multi-chain-accounts-service/api-balance-fetcher.d.mts.map +1 -1
  31. package/dist/multi-chain-accounts-service/api-balance-fetcher.mjs +2 -6
  32. package/dist/multi-chain-accounts-service/api-balance-fetcher.mjs.map +1 -1
  33. package/package.json +3 -1
package/CHANGELOG.md CHANGED
@@ -7,9 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ### Fixed
10
+ ### Added
11
11
 
12
- - Fix address format compatibility between `TokenBalancesController` and `AccountTrackerController` in `AccountsApiBalanceFetcher` ([#6812](https://github.com/MetaMask/core/pull/6812))
12
+ - Add real-time balance updates via WebSocket integration with `AccountActivityService` to `TokenBalancesController` ([#6784](https://github.com/MetaMask/core/pull/6784))
13
+ - Add `@metamask/core-backend` as a dependency and peer dependency ([#6784](https://github.com/MetaMask/core/pull/6784))
14
+ - Controller now subscribes to `AccountActivityService:balanceUpdated` events for instant balance updates
15
+ - Add support for real-time balance updates for both ERC20 tokens and native tokens
16
+ - Add `TokenDetectionController:addDetectedTokensViaWs` action handler for adding tokens detected via WebSocket
17
+ - Controller now subscribes to `AccountActivityService:statusChanged` events to dynamically adjust polling intervals
18
+ - When WebSocket service is "up", polling interval increases to backup interval (5 minutes)
19
+ - When WebSocket service is "down", polling interval restores to default interval (30 seconds)
20
+ - Status changes are debounced (5 seconds) and jittered to prevent thundering herd
21
+ - Add fallback to polling when balance updates contain errors or unsupported asset types
22
+
23
+ ### Changed
24
+
25
+ - **BREAKING:** `TokenBalancesController` messenger must now allow `AccountActivityService:balanceUpdated` and `AccountActivityService:statusChanged` events ([#6784](https://github.com/MetaMask/core/pull/6784))
26
+ - **BREAKING:** `TokenBalancesController` messenger must now allow `TokenDetectionController:addDetectedTokensViaWs` action ([#6784](https://github.com/MetaMask/core/pull/6784))
27
+ - **BREAKING:** Change `TokenBalancesController` default polling interval to 30 seconds (was 180 seconds) ([#6784](https://github.com/MetaMask/core/pull/6784))
28
+ - With real-time WebSocket updates, the default interval only applies when WebSocket is disconnected
29
+ - When WebSocket is connected, polling automatically adjusts to 5 minutes as a backup
30
+ - Update `TokenBalancesController` README documentation to mention real-time balance updates via WebSocket and intelligent polling management ([#6784](https://github.com/MetaMask/core/pull/6784))
31
+ - `TokenDetectionController` code cleanup: remove unused private properties and ESLint disable comments ([#6784](https://github.com/MetaMask/core/pull/6784))
13
32
 
14
33
  ## [79.0.1]
15
34
 
package/README.md CHANGED
@@ -21,7 +21,7 @@ This package features the following controllers:
21
21
  - [**CurrencyRateController**](src/CurrencyRateController.ts) keeps a periodically updated value of the exchange rate from the currently selected "native" currency to another (handling testnet tokens specially).
22
22
  - [**DeFiPositionsController**](src/DeFiPositionsController/DeFiPositionsController.ts.ts) keeps a periodically updated value of the DeFi positions for the owner EVM addresses.
23
23
  - [**RatesController**](src/RatesController/RatesController.ts) keeps a periodically updated value for the exchange rates for different cryptocurrencies. The difference between the `RatesController` and `CurrencyRateController` is that the second one is coupled to the `NetworksController` and is EVM specific, whilst the first one can handle different blockchain currencies like BTC and SOL.
24
- - [**TokenBalancesController**](src/TokenBalancesController.ts) keeps a periodically updated set of balances for the current set of ERC-20 tokens.
24
+ - [**TokenBalancesController**](src/TokenBalancesController.ts) keeps a periodically updated set of balances for the current set of ERC-20 tokens. It supports real-time balance updates via WebSocket and intelligent polling management. See [Real-Time Balance Updates Flow](./REAL_TIME_BALANCE_UPDATES.md) for details.
25
25
  - [**TokenDetectionController**](src/TokenDetectionController.ts) keeps a periodically updated list of ERC-20 tokens assigned to the currently selected address.
26
26
  - [**TokenListController**](src/TokenListController.ts) uses the MetaSwap API to keep a periodically updated list of known ERC-20 tokens along with their metadata.
27
27
  - [**TokenRatesController**](src/TokenRatesController.ts) keeps a periodically updated list of exchange rates for known ERC-20 tokens relative to the currently selected native currency.
@@ -10,9 +10,9 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
10
10
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
11
11
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
12
12
  };
13
- var _TokenBalancesController_instances, _TokenBalancesController_platform, _TokenBalancesController_queryAllAccounts, _TokenBalancesController_accountsApiChainIds, _TokenBalancesController_balanceFetchers, _TokenBalancesController_allTokens, _TokenBalancesController_detectedTokens, _TokenBalancesController_defaultInterval, _TokenBalancesController_chainPollingConfig, _TokenBalancesController_intervalPollingTimers, _TokenBalancesController_isControllerPollingActive, _TokenBalancesController_requestedChainIds, _TokenBalancesController_chainIdsWithTokens, _TokenBalancesController_getProvider, _TokenBalancesController_getNetworkClient, _TokenBalancesController_createAccountsApiFetcher, _TokenBalancesController_startIntervalGroupPolling, _TokenBalancesController_startPollingForInterval, _TokenBalancesController_setPollingTimer, _TokenBalancesController_onTokensChanged, _TokenBalancesController_onNetworkChanged, _TokenBalancesController_onAccountRemoved;
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_chainIdsWithTokens, _TokenBalancesController_getProvider, _TokenBalancesController_getNetworkClient, _TokenBalancesController_createAccountsApiFetcher, _TokenBalancesController_startIntervalGroupPolling, _TokenBalancesController_startPollingForInterval, _TokenBalancesController_setPollingTimer, _TokenBalancesController_isTokenTracked, _TokenBalancesController_onTokensChanged, _TokenBalancesController_onNetworkChanged, _TokenBalancesController_onAccountRemoved, _TokenBalancesController_prepareBalanceUpdates, _TokenBalancesController_onAccountActivityBalanceUpdate, _TokenBalancesController_onAccountActivityStatusChanged, _TokenBalancesController_processAccumulatedStatusChanges;
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.TokenBalancesController = void 0;
15
+ exports.TokenBalancesController = exports.parseAssetType = exports.caipChainIdToHex = void 0;
16
16
  const providers_1 = require("@ethersproject/providers");
17
17
  const controller_utils_1 = require("@metamask/controller-utils");
18
18
  const polling_controller_1 = require("@metamask/polling-controller");
@@ -23,7 +23,8 @@ const AssetsContractController_1 = require("./AssetsContractController.cjs");
23
23
  const api_balance_fetcher_1 = require("./multi-chain-accounts-service/api-balance-fetcher.cjs");
24
24
  const rpc_balance_fetcher_1 = require("./rpc-service/rpc-balance-fetcher.cjs");
25
25
  const CONTROLLER = 'TokenBalancesController';
26
- const DEFAULT_INTERVAL_MS = 180000; // 3 minutes
26
+ const DEFAULT_INTERVAL_MS = 30000; // 30 seconds
27
+ const DEFAULT_WEBSOCKET_ACTIVE_POLLING_INTERVAL_MS = 300000; // 5 minutes
27
28
  const metadata = {
28
29
  tokenBalances: {
29
30
  includeInStateLogs: false,
@@ -38,11 +39,52 @@ const metadata = {
38
39
  const draft = (base, fn) => (0, immer_1.produce)(base, fn);
39
40
  const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
40
41
  const checksum = (addr) => (0, controller_utils_1.toChecksumHexAddress)(addr);
42
+ /**
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")
45
+ *
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
49
+ */
50
+ const caipChainIdToHex = (chainId) => {
51
+ if ((0, utils_1.isStrictHexString)(chainId)) {
52
+ return chainId;
53
+ }
54
+ if ((0, utils_1.isCaipChainId)(chainId)) {
55
+ return (0, controller_utils_1.toHex)((0, utils_1.parseCaipChainId)(chainId).reference);
56
+ }
57
+ throw new Error('caipChainIdToHex - Failed to provide CAIP-2 or Hex chainId');
58
+ };
59
+ exports.caipChainIdToHex = caipChainIdToHex;
60
+ /**
61
+ * Extract token address from asset type
62
+ * Returns tuple of [tokenAddress, isNativeToken] or null if invalid
63
+ *
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
66
+ */
67
+ const parseAssetType = (assetType) => {
68
+ if (!(0, utils_1.isCaipAssetType)(assetType)) {
69
+ return null;
70
+ }
71
+ const parsed = (0, utils_1.parseCaipAssetType)(assetType);
72
+ // ERC20 token (e.g., "eip155:1/erc20:0x...")
73
+ if (parsed.assetNamespace === 'erc20') {
74
+ return [parsed.assetReference, false];
75
+ }
76
+ // Native token (e.g., "eip155:1/slip44:60")
77
+ if (parsed.assetNamespace === 'slip44') {
78
+ return [ZERO_ADDRESS, true];
79
+ }
80
+ return null;
81
+ };
82
+ exports.parseAssetType = parseAssetType;
41
83
  // endregion
42
84
  // ────────────────────────────────────────────────────────────────────────────
43
85
  // region: Main controller
44
86
  class TokenBalancesController extends (0, polling_controller_1.StaticIntervalPollingController)() {
45
- constructor({ messenger, interval = DEFAULT_INTERVAL_MS, chainPollingIntervals = {}, state = {}, queryMultipleAccounts = true, accountsApiChainIds = () => [], allowExternalServices = () => true, platform, }) {
87
+ constructor({ messenger, interval = DEFAULT_INTERVAL_MS, websocketActivePollingInterval = DEFAULT_WEBSOCKET_ACTIVE_POLLING_INTERVAL_MS, chainPollingIntervals = {}, state = {}, queryMultipleAccounts = true, accountsApiChainIds = () => [], allowExternalServices = () => true, platform, }) {
46
88
  super({
47
89
  name: CONTROLLER,
48
90
  messenger,
@@ -56,8 +98,11 @@ class TokenBalancesController extends (0, polling_controller_1.StaticIntervalPol
56
98
  _TokenBalancesController_balanceFetchers.set(this, void 0);
57
99
  _TokenBalancesController_allTokens.set(this, {});
58
100
  _TokenBalancesController_detectedTokens.set(this, {});
101
+ _TokenBalancesController_allIgnoredTokens.set(this, {});
59
102
  /** Default polling interval for chains without specific configuration */
60
103
  _TokenBalancesController_defaultInterval.set(this, void 0);
104
+ /** Polling interval when WebSocket is active and providing real-time updates */
105
+ _TokenBalancesController_websocketActivePollingInterval.set(this, void 0);
61
106
  /** Per-chain polling configuration */
62
107
  _TokenBalancesController_chainPollingConfig.set(this, void 0);
63
108
  /** Active polling timers grouped by interval */
@@ -66,6 +111,11 @@ class TokenBalancesController extends (0, polling_controller_1.StaticIntervalPol
66
111
  _TokenBalancesController_isControllerPollingActive.set(this, false);
67
112
  /** Store original chainIds from startPolling to preserve intent */
68
113
  _TokenBalancesController_requestedChainIds.set(this, []);
114
+ /** Debouncing for rapid status changes to prevent excessive HTTP calls */
115
+ _TokenBalancesController_statusChangeDebouncer.set(this, {
116
+ timer: null,
117
+ pendingChanges: new Map(),
118
+ });
69
119
  _TokenBalancesController_getProvider.set(this, (chainId) => {
70
120
  const { networkConfigurationsByChainId } = this.messagingSystem.call('NetworkController:getState');
71
121
  const cfg = networkConfigurationsByChainId[chainId];
@@ -167,6 +217,7 @@ class TokenBalancesController extends (0, polling_controller_1.StaticIntervalPol
167
217
  });
168
218
  __classPrivateFieldSet(this, _TokenBalancesController_allTokens, state.allTokens, "f");
169
219
  __classPrivateFieldSet(this, _TokenBalancesController_detectedTokens, state.allDetectedTokens, "f");
220
+ __classPrivateFieldSet(this, _TokenBalancesController_allIgnoredTokens, state.allIgnoredTokens, "f");
170
221
  // Only update balances for chains that still have tokens (and only if we haven't already updated state)
171
222
  if (changed.length && !hasChanges) {
172
223
  this.updateBalances({ chainIds: changed }).catch((error) => {
@@ -210,10 +261,85 @@ class TokenBalancesController extends (0, polling_controller_1.StaticIntervalPol
210
261
  delete s.tokenBalances[addr];
211
262
  });
212
263
  });
264
+ // ────────────────────────────────────────────────────────────────────────────
265
+ // AccountActivityService event handlers
266
+ /**
267
+ * Handle real-time balance updates from AccountActivityService
268
+ * Processes balance updates and updates the token balance state
269
+ * If any balance update has an error, triggers fallback polling for the chain
270
+ *
271
+ * @param options0 - Balance update parameters
272
+ * @param options0.address - Account address
273
+ * @param options0.chain - CAIP chain identifier
274
+ * @param options0.updates - Array of balance updates for the account
275
+ */
276
+ _TokenBalancesController_onAccountActivityBalanceUpdate.set(this, async ({ address, chain, updates, }) => {
277
+ const chainId = (0, exports.caipChainIdToHex)(chain);
278
+ const account = checksum(address);
279
+ try {
280
+ // Process all balance updates at once
281
+ const { tokenBalances, newTokens, nativeBalanceUpdates } = __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_prepareBalanceUpdates).call(this, updates, account, chainId);
282
+ // Update state once with all token balances
283
+ if (tokenBalances.length > 0) {
284
+ this.update((state) => {
285
+ var _a, _b;
286
+ // Initialize account and chain structure
287
+ (_a = state.tokenBalances)[account] ?? (_a[account] = {});
288
+ (_b = state.tokenBalances[account])[chainId] ?? (_b[chainId] = {});
289
+ // Apply all token balance updates
290
+ for (const { tokenAddress, balance } of tokenBalances) {
291
+ state.tokenBalances[account][chainId][tokenAddress] = balance;
292
+ }
293
+ });
294
+ }
295
+ // Update native balances in AccountTrackerController
296
+ if (nativeBalanceUpdates.length > 0) {
297
+ this.messagingSystem.call('AccountTrackerController:updateNativeBalances', nativeBalanceUpdates);
298
+ }
299
+ // Import any new tokens that were discovered (balance already updated from websocket)
300
+ if (newTokens.length > 0) {
301
+ await this.messagingSystem.call('TokenDetectionController:addDetectedTokensViaWs', {
302
+ tokensSlice: newTokens,
303
+ chainId: chainId,
304
+ });
305
+ }
306
+ }
307
+ catch (error) {
308
+ console.warn(`Error updating balances from AccountActivityService for chain ${chain}, account ${address}:`, error);
309
+ console.warn('Balance update data:', JSON.stringify(updates, null, 2));
310
+ // On error, trigger fallback polling
311
+ await this.updateBalances({ chainIds: [chainId] }).catch(() => {
312
+ // Silently handle polling errors
313
+ });
314
+ }
315
+ });
316
+ /**
317
+ * Handle status changes from AccountActivityService
318
+ * Uses aggressive debouncing to prevent excessive HTTP calls from rapid up/down changes
319
+ *
320
+ * @param options0 - Status change event data
321
+ * @param options0.chainIds - Array of chain identifiers
322
+ * @param options0.status - Connection status ('up' for connected, 'down' for disconnected)
323
+ */
324
+ _TokenBalancesController_onAccountActivityStatusChanged.set(this, ({ chainIds, status, }) => {
325
+ // Update pending changes (latest status wins for each chain)
326
+ for (const chainId of chainIds) {
327
+ __classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").pendingChanges.set(chainId, status);
328
+ }
329
+ // Clear existing timer to extend debounce window
330
+ if (__classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").timer) {
331
+ clearTimeout(__classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").timer);
332
+ }
333
+ // Set new timer - only process changes after activity settles
334
+ __classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").timer = setTimeout(() => {
335
+ __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_processAccumulatedStatusChanges).call(this);
336
+ }, 5000); // 5-second debounce window
337
+ });
213
338
  __classPrivateFieldSet(this, _TokenBalancesController_platform, platform ?? 'extension', "f");
214
339
  __classPrivateFieldSet(this, _TokenBalancesController_queryAllAccounts, queryMultipleAccounts, "f");
215
340
  __classPrivateFieldSet(this, _TokenBalancesController_accountsApiChainIds, accountsApiChainIds, "f");
216
341
  __classPrivateFieldSet(this, _TokenBalancesController_defaultInterval, interval, "f");
342
+ __classPrivateFieldSet(this, _TokenBalancesController_websocketActivePollingInterval, websocketActivePollingInterval, "f");
217
343
  __classPrivateFieldSet(this, _TokenBalancesController_chainPollingConfig, { ...chainPollingIntervals }, "f");
218
344
  // Strategy order: API first, then RPC fallback
219
345
  __classPrivateFieldSet(this, _TokenBalancesController_balanceFetchers, [
@@ -227,9 +353,10 @@ class TokenBalancesController extends (0, polling_controller_1.StaticIntervalPol
227
353
  ], "f");
228
354
  this.setIntervalLength(interval);
229
355
  // initial token state & subscriptions
230
- const { allTokens, allDetectedTokens } = this.messagingSystem.call('TokensController:getState');
356
+ const { allTokens, allDetectedTokens, allIgnoredTokens } = this.messagingSystem.call('TokensController:getState');
231
357
  __classPrivateFieldSet(this, _TokenBalancesController_allTokens, allTokens, "f");
232
358
  __classPrivateFieldSet(this, _TokenBalancesController_detectedTokens, allDetectedTokens, "f");
359
+ __classPrivateFieldSet(this, _TokenBalancesController_allIgnoredTokens, allIgnoredTokens, "f");
233
360
  this.messagingSystem.subscribe('TokensController:stateChange', (tokensState) => {
234
361
  __classPrivateFieldGet(this, _TokenBalancesController_onTokensChanged, "f").call(this, tokensState).catch((error) => {
235
362
  console.warn('Error handling token state change:', error);
@@ -240,6 +367,10 @@ class TokenBalancesController extends (0, polling_controller_1.StaticIntervalPol
240
367
  // Register action handlers for polling interval control
241
368
  this.messagingSystem.registerActionHandler(`TokenBalancesController:updateChainPollingConfigs`, this.updateChainPollingConfigs.bind(this));
242
369
  this.messagingSystem.registerActionHandler(`TokenBalancesController:getChainPollingConfig`, this.getChainPollingConfig.bind(this));
370
+ // Subscribe to AccountActivityService balance updates for real-time updates
371
+ this.messagingSystem.subscribe('AccountActivityService:balanceUpdated', __classPrivateFieldGet(this, _TokenBalancesController_onAccountActivityBalanceUpdate, "f").bind(this));
372
+ // Subscribe to AccountActivityService status changes for dynamic polling management
373
+ this.messagingSystem.subscribe('AccountActivityService:statusChanged', __classPrivateFieldGet(this, _TokenBalancesController_onAccountActivityStatusChanged, "f").bind(this));
243
374
  }
244
375
  /**
245
376
  * Override to support per-chain polling intervals by grouping chains by interval
@@ -394,14 +525,15 @@ class TokenBalancesController extends (0, polling_controller_1.StaticIntervalPol
394
525
  }
395
526
  // Update with actual fetched balances only if the value has changed
396
527
  aggregated.forEach(({ success, value, account, token, chainId }) => {
397
- var _a, _b, _c;
528
+ var _a, _b;
398
529
  if (success && value !== undefined) {
399
530
  const newBalance = (0, controller_utils_1.toHex)(value);
400
531
  const tokenAddress = checksum(token);
401
532
  const currentBalance = d.tokenBalances[account]?.[chainId]?.[tokenAddress];
402
533
  // Only update if the balance has actually changed
403
534
  if (currentBalance !== newBalance) {
404
- ((_c = ((_a = d.tokenBalances)[_b = account] ?? (_a[_b] = {})))[chainId] ?? (_c[chainId] = {}))[tokenAddress] = newBalance;
535
+ ((_b = ((_a = d.tokenBalances)[account] ?? (_a[account] = {})))[chainId] ?? (_b[chainId] = {}))[tokenAddress] =
536
+ newBalance;
405
537
  }
406
538
  }
407
539
  });
@@ -466,6 +598,11 @@ class TokenBalancesController extends (0, polling_controller_1.StaticIntervalPol
466
598
  __classPrivateFieldSet(this, _TokenBalancesController_isControllerPollingActive, false, "f");
467
599
  __classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").forEach((timer) => clearInterval(timer));
468
600
  __classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").clear();
601
+ // Clean up debouncing timer
602
+ if (__classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").timer) {
603
+ clearTimeout(__classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").timer);
604
+ __classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").timer = null;
605
+ }
469
606
  // Unregister action handlers
470
607
  this.messagingSystem.unregisterActionHandler(`TokenBalancesController:updateChainPollingConfigs`);
471
608
  this.messagingSystem.unregisterActionHandler(`TokenBalancesController:getChainPollingConfig`);
@@ -473,7 +610,7 @@ class TokenBalancesController extends (0, polling_controller_1.StaticIntervalPol
473
610
  }
474
611
  }
475
612
  exports.TokenBalancesController = TokenBalancesController;
476
- _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_defaultInterval = new WeakMap(), _TokenBalancesController_chainPollingConfig = new WeakMap(), _TokenBalancesController_intervalPollingTimers = new WeakMap(), _TokenBalancesController_isControllerPollingActive = new WeakMap(), _TokenBalancesController_requestedChainIds = 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_instances = new WeakSet(), _TokenBalancesController_chainIdsWithTokens = function _TokenBalancesController_chainIdsWithTokens() {
613
+ _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_onAccountActivityBalanceUpdate = new WeakMap(), _TokenBalancesController_onAccountActivityStatusChanged = new WeakMap(), _TokenBalancesController_instances = new WeakSet(), _TokenBalancesController_chainIdsWithTokens = function _TokenBalancesController_chainIdsWithTokens() {
477
614
  return [
478
615
  ...new Set([
479
616
  ...Object.keys(__classPrivateFieldGet(this, _TokenBalancesController_allTokens, "f")),
@@ -528,6 +665,90 @@ _TokenBalancesController_platform = new WeakMap(), _TokenBalancesController_quer
528
665
  });
529
666
  }, interval);
530
667
  __classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").set(interval, timer);
668
+ }, _TokenBalancesController_isTokenTracked = function _TokenBalancesController_isTokenTracked(tokenAddress, account, chainId) {
669
+ const normalizedAddress = tokenAddress.toLowerCase();
670
+ // Check if token exists in allTokens
671
+ if (__classPrivateFieldGet(this, _TokenBalancesController_allTokens, "f")?.[chainId]?.[account.toLowerCase()]?.some((token) => token.address.toLowerCase() === normalizedAddress)) {
672
+ return true;
673
+ }
674
+ // Check if token exists in allIgnoredTokens
675
+ if (__classPrivateFieldGet(this, _TokenBalancesController_allIgnoredTokens, "f")?.[chainId]?.[account.toLowerCase()]?.some((addr) => addr.toLowerCase() === normalizedAddress)) {
676
+ return true;
677
+ }
678
+ return false;
679
+ }, _TokenBalancesController_prepareBalanceUpdates = function _TokenBalancesController_prepareBalanceUpdates(updates, account, chainId) {
680
+ const tokenBalances = [];
681
+ const newTokens = [];
682
+ const nativeBalanceUpdates = [];
683
+ for (const update of updates) {
684
+ const { asset, postBalance } = update;
685
+ // Throw if balance update has an error
686
+ if (postBalance.error) {
687
+ throw new Error('Balance update has error');
688
+ }
689
+ // Parse token address from asset type
690
+ const parsed = (0, exports.parseAssetType)(asset.type);
691
+ if (!parsed) {
692
+ throw new Error('Failed to parse asset type');
693
+ }
694
+ const [tokenAddress, isNativeToken] = parsed;
695
+ // Validate token address
696
+ if (!(0, utils_1.isStrictHexString)(tokenAddress) ||
697
+ !(0, controller_utils_1.isValidHexAddress)(tokenAddress)) {
698
+ throw new Error('Invalid token address');
699
+ }
700
+ const checksumTokenAddress = checksum(tokenAddress);
701
+ const isTracked = __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_isTokenTracked).call(this, checksumTokenAddress, account, chainId);
702
+ // postBalance.amount is in hex format (raw units)
703
+ const balanceHex = postBalance.amount;
704
+ // Add token balance (tracked tokens, ignored tokens, and native tokens all get balance updates)
705
+ tokenBalances.push({
706
+ tokenAddress: checksumTokenAddress,
707
+ balance: balanceHex,
708
+ });
709
+ // Add native balance update if this is a native token
710
+ if (isNativeToken) {
711
+ nativeBalanceUpdates.push({
712
+ address: account,
713
+ chainId,
714
+ balance: balanceHex,
715
+ });
716
+ }
717
+ // Handle untracked ERC20 tokens - queue for import
718
+ if (!isNativeToken && !isTracked) {
719
+ newTokens.push(checksumTokenAddress);
720
+ }
721
+ }
722
+ return { tokenBalances, newTokens, nativeBalanceUpdates };
723
+ }, _TokenBalancesController_processAccumulatedStatusChanges = function _TokenBalancesController_processAccumulatedStatusChanges() {
724
+ const changes = Array.from(__classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").pendingChanges.entries());
725
+ __classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").pendingChanges.clear();
726
+ __classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").timer = null;
727
+ if (changes.length === 0) {
728
+ return;
729
+ }
730
+ // Calculate final polling configurations
731
+ const chainConfigs = {};
732
+ for (const [chainId, status] of changes) {
733
+ // Convert CAIP format (eip155:1) to hex format (0x1)
734
+ // chainId is always in CAIP format from AccountActivityService
735
+ const hexChainId = (0, exports.caipChainIdToHex)(chainId);
736
+ if (status === 'down') {
737
+ // Chain is down - use default polling since no real-time updates available
738
+ chainConfigs[hexChainId] = { interval: __classPrivateFieldGet(this, _TokenBalancesController_defaultInterval, "f") };
739
+ }
740
+ else {
741
+ // Chain is up - use longer intervals since WebSocket provides real-time updates
742
+ chainConfigs[hexChainId] = {
743
+ interval: __classPrivateFieldGet(this, _TokenBalancesController_websocketActivePollingInterval, "f"),
744
+ };
745
+ }
746
+ }
747
+ // Add jitter to prevent synchronized requests across instances
748
+ const jitterDelay = Math.random() * __classPrivateFieldGet(this, _TokenBalancesController_defaultInterval, "f"); // 0 to default interval
749
+ setTimeout(() => {
750
+ this.updateChainPollingConfigs(chainConfigs, { immediateUpdate: true });
751
+ }, jitterDelay);
531
752
  };
532
753
  exports.default = TokenBalancesController;
533
754
  //# sourceMappingURL=TokenBalancesController.cjs.map