@metamask/assets-controllers 74.0.0 → 74.1.1

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 (30) hide show
  1. package/CHANGELOG.md +50 -1
  2. package/dist/AccountTrackerController.cjs +222 -84
  3. package/dist/AccountTrackerController.cjs.map +1 -1
  4. package/dist/AccountTrackerController.d.cts +7 -1
  5. package/dist/AccountTrackerController.d.cts.map +1 -1
  6. package/dist/AccountTrackerController.d.mts +7 -1
  7. package/dist/AccountTrackerController.d.mts.map +1 -1
  8. package/dist/AccountTrackerController.mjs +224 -85
  9. package/dist/AccountTrackerController.mjs.map +1 -1
  10. package/dist/TokenBalancesController.cjs +17 -14
  11. package/dist/TokenBalancesController.cjs.map +1 -1
  12. package/dist/TokenBalancesController.d.cts.map +1 -1
  13. package/dist/TokenBalancesController.d.mts.map +1 -1
  14. package/dist/TokenBalancesController.mjs +18 -15
  15. package/dist/TokenBalancesController.mjs.map +1 -1
  16. package/dist/rpc-service/rpc-balance-fetcher.cjs +31 -9
  17. package/dist/rpc-service/rpc-balance-fetcher.cjs.map +1 -1
  18. package/dist/rpc-service/rpc-balance-fetcher.d.cts.map +1 -1
  19. package/dist/rpc-service/rpc-balance-fetcher.d.mts.map +1 -1
  20. package/dist/rpc-service/rpc-balance-fetcher.mjs +32 -10
  21. package/dist/rpc-service/rpc-balance-fetcher.mjs.map +1 -1
  22. package/dist/selectors/token-selectors.cjs +24 -9
  23. package/dist/selectors/token-selectors.cjs.map +1 -1
  24. package/dist/selectors/token-selectors.d.cts +96 -20
  25. package/dist/selectors/token-selectors.d.cts.map +1 -1
  26. package/dist/selectors/token-selectors.d.mts +96 -20
  27. package/dist/selectors/token-selectors.d.mts.map +1 -1
  28. package/dist/selectors/token-selectors.mjs +24 -9
  29. package/dist/selectors/token-selectors.mjs.map +1 -1
  30. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -7,6 +7,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [74.1.1]
11
+
12
+ ### Changed
13
+
14
+ - Improve balance fetching performance and resilience by parallelizing multi-chain operations and moving timeout handling to fetchers ([#6390](https://github.com/MetaMask/core/pull/6390))
15
+
16
+ - Replace sequential `for` loops with `Promise.allSettled` in `RpcBalanceFetcher` and `AccountTrackerController` for parallel chain processing
17
+ - Move timeout handling from controller-level `Promise.race` to fetcher-level `safelyExecuteWithTimeout` for better error isolation
18
+ - Add `safelyExecuteWithTimeout` to both `RpcBalanceFetcher` and `AccountsApiBalanceFetcher` to prevent individual chain timeouts from blocking other chains
19
+ - Remove redundant timeout wrappers from `TokenBalancesController` and `AccountTrackerController`
20
+ - Improve test coverage for timeout and error handling scenarios in all balance fetchers
21
+
22
+ ## [74.1.0]
23
+
24
+ ### Added
25
+
26
+ - Enable `AccountTrackerController` to fetch native balances using AccountsAPI when `allowExternalServices` is enabled ([#6369](https://github.com/MetaMask/core/pull/6369))
27
+
28
+ - Implement native balance fetching via AccountsAPI when `useAccountsAPI` and `allowExternalServices` are both true
29
+ - Add fallback to RPC balance fetching when external services are disabled
30
+ - Add comprehensive test coverage for both AccountsAPI and RPC balance fetching scenarios
31
+
32
+ ### Changed
33
+
34
+ - Bump `@metamask/base-controller` from `^8.1.0` to `^8.2.0` ([#6355](https://github.com/MetaMask/core/pull/6355))
35
+
36
+ - Add new `accountId` field to the `Asset` type ([#6358](https://github.com/MetaMask/core/pull/6358))
37
+
38
+ ### Fixed
39
+
40
+ - Uses `InternalAccount['type']` for the `Asset['type']` property ([#6358](https://github.com/MetaMask/core/pull/6358))
41
+
42
+ - Ensure that the evm addresses used to fetch balances from AccountTrackerController state is lowercase, in order to account for discrepancies between clients ([#6358](https://github.com/MetaMask/core/pull/6358))
43
+
44
+ - Prevents mutation of memoized fields used inside selectors ([#6358](https://github.com/MetaMask/core/pull/6358))
45
+
46
+ - Fix duplicate token balance entries caused by case-sensitive address comparison in `TokenBalancesController.updateBalances` ([#6354](https://github.com/MetaMask/core/pull/6354))
47
+
48
+ - Normalize token addresses to proper EIP-55 checksum format before using as object keys to prevent the same token from appearing multiple times with different cases
49
+ - Add comprehensive unit tests for token address normalization scenarios
50
+
51
+ - Fix TokenBalancesController timeout handling by replacing `safelyExecuteWithTimeout` with proper `Promise.race` implementation ([#6365](https://github.com/MetaMask/core/pull/6365))
52
+
53
+ - Replace `safelyExecuteWithTimeout` which was silently swallowing timeout errors with direct `Promise.race` that properly throws
54
+ - Reduce RPC timeout from 3 minutes to 15 seconds for better responsiveness and batch size
55
+ - Enable proper fallback between API and RPC balance fetchers when timeouts occur
56
+
10
57
  ## [74.0.0]
11
58
 
12
59
  ### Added
@@ -1879,7 +1926,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1879
1926
 
1880
1927
  - Use Ethers for AssetsContractController ([#845](https://github.com/MetaMask/core/pull/845))
1881
1928
 
1882
- [Unreleased]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@74.0.0...HEAD
1929
+ [Unreleased]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@74.1.1...HEAD
1930
+ [74.1.1]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@74.1.0...@metamask/assets-controllers@74.1.1
1931
+ [74.1.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@74.0.0...@metamask/assets-controllers@74.1.0
1883
1932
  [74.0.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@73.3.0...@metamask/assets-controllers@74.0.0
1884
1933
  [73.3.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@73.2.0...@metamask/assets-controllers@73.3.0
1885
1934
  [73.2.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@73.1.0...@metamask/assets-controllers@73.2.0
@@ -13,7 +13,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
13
13
  var __importDefault = (this && this.__importDefault) || function (mod) {
14
14
  return (mod && mod.__esModule) ? mod : { "default": mod };
15
15
  };
16
- var _AccountTrackerController_instances, _AccountTrackerController_refreshMutex, _AccountTrackerController_includeStakedAssets, _AccountTrackerController_getStakedBalanceForChain, _AccountTrackerController_getCorrectNetworkClient, _AccountTrackerController_getNetworkClientIds, _AccountTrackerController_getBalanceFromChain, _AccountTrackerController_registerMessageHandlers;
16
+ var _AccountTrackerRpcBalanceFetcher_instances, _AccountTrackerRpcBalanceFetcher_getProvider, _AccountTrackerRpcBalanceFetcher_getNetworkClient, _AccountTrackerRpcBalanceFetcher_includeStakedAssets, _AccountTrackerRpcBalanceFetcher_getStakedBalanceForChain, _AccountTrackerRpcBalanceFetcher_getBalanceFromChain, _AccountTrackerController_instances, _AccountTrackerController_refreshMutex, _AccountTrackerController_includeStakedAssets, _AccountTrackerController_getStakedBalanceForChain, _AccountTrackerController_balanceFetchers, _AccountTrackerController_getProvider, _AccountTrackerController_getNetworkClient, _AccountTrackerController_getCorrectNetworkClient, _AccountTrackerController_getNetworkClientIds, _AccountTrackerController_registerMessageHandlers;
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.AccountTrackerController = void 0;
19
19
  const contracts_1 = require("@ethersproject/contracts");
@@ -23,14 +23,149 @@ const eth_query_1 = __importDefault(require("@metamask/eth-query"));
23
23
  const polling_controller_1 = require("@metamask/polling-controller");
24
24
  const utils_1 = require("@metamask/utils");
25
25
  const async_mutex_1 = require("async-mutex");
26
+ const bn_js_1 = __importDefault(require("bn.js"));
26
27
  const lodash_1 = require("lodash");
27
28
  const single_call_balance_checker_abi_1 = __importDefault(require("single-call-balance-checker-abi"));
28
29
  const AssetsContractController_1 = require("./AssetsContractController.cjs");
29
30
  const assetsUtil_1 = require("./assetsUtil.cjs");
31
+ const api_balance_fetcher_1 = require("./multi-chain-accounts-service/api-balance-fetcher.cjs");
30
32
  /**
31
33
  * The name of the {@link AccountTrackerController}.
32
34
  */
33
35
  const controllerName = 'AccountTrackerController';
36
+ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
37
+ /**
38
+ * RPC-based balance fetcher for AccountTrackerController.
39
+ * Fetches only native balances and staked balances (no token balances).
40
+ */
41
+ class AccountTrackerRpcBalanceFetcher {
42
+ constructor(getProvider, getNetworkClient, includeStakedAssets, getStakedBalanceForChain) {
43
+ _AccountTrackerRpcBalanceFetcher_instances.add(this);
44
+ _AccountTrackerRpcBalanceFetcher_getProvider.set(this, void 0);
45
+ _AccountTrackerRpcBalanceFetcher_getNetworkClient.set(this, void 0);
46
+ _AccountTrackerRpcBalanceFetcher_includeStakedAssets.set(this, void 0);
47
+ _AccountTrackerRpcBalanceFetcher_getStakedBalanceForChain.set(this, void 0);
48
+ __classPrivateFieldSet(this, _AccountTrackerRpcBalanceFetcher_getProvider, getProvider, "f");
49
+ __classPrivateFieldSet(this, _AccountTrackerRpcBalanceFetcher_getNetworkClient, getNetworkClient, "f");
50
+ __classPrivateFieldSet(this, _AccountTrackerRpcBalanceFetcher_includeStakedAssets, includeStakedAssets, "f");
51
+ __classPrivateFieldSet(this, _AccountTrackerRpcBalanceFetcher_getStakedBalanceForChain, getStakedBalanceForChain, "f");
52
+ }
53
+ supports() {
54
+ return true; // fallback – supports every chain
55
+ }
56
+ async fetch({ chainIds, queryAllAccounts, selectedAccount, allAccounts, }) {
57
+ // Process all chains in parallel for better performance
58
+ const chainProcessingPromises = chainIds.map(async (chainId) => {
59
+ const accountsToUpdate = queryAllAccounts
60
+ ? Object.values(allAccounts).map((account) => (0, controller_utils_1.toChecksumHexAddress)(account.address))
61
+ : [selectedAccount];
62
+ const { provider, blockTracker } = __classPrivateFieldGet(this, _AccountTrackerRpcBalanceFetcher_getNetworkClient, "f").call(this, chainId);
63
+ const ethQuery = new eth_query_1.default(provider);
64
+ const chainResults = [];
65
+ // Force fresh block data before multicall
66
+ await (0, controller_utils_1.safelyExecuteWithTimeout)(() => blockTracker?.checkForLatestBlock?.());
67
+ // Fetch native balances
68
+ if ((0, utils_1.hasProperty)(AssetsContractController_1.SINGLE_CALL_BALANCES_ADDRESS_BY_CHAINID, chainId)) {
69
+ const contractAddress = AssetsContractController_1.SINGLE_CALL_BALANCES_ADDRESS_BY_CHAINID[chainId];
70
+ const contract = new contracts_1.Contract(contractAddress, single_call_balance_checker_abi_1.default, __classPrivateFieldGet(this, _AccountTrackerRpcBalanceFetcher_getProvider, "f").call(this, chainId));
71
+ const nativeBalances = await (0, controller_utils_1.safelyExecuteWithTimeout)(() => contract.balances(accountsToUpdate, [ZERO_ADDRESS]), false, 3000);
72
+ if (nativeBalances) {
73
+ accountsToUpdate.forEach((address, index) => {
74
+ chainResults.push({
75
+ success: true,
76
+ value: new bn_js_1.default(nativeBalances[index].toString()),
77
+ account: address,
78
+ token: ZERO_ADDRESS,
79
+ chainId,
80
+ });
81
+ });
82
+ }
83
+ }
84
+ else {
85
+ // Process accounts in batches using reduceInBatchesSerially
86
+ await (0, assetsUtil_1.reduceInBatchesSerially)({
87
+ values: accountsToUpdate,
88
+ batchSize: assetsUtil_1.TOKEN_PRICES_BATCH_SIZE,
89
+ initialResult: undefined,
90
+ eachBatch: async (workingResult, batch) => {
91
+ const balancePromises = batch.map(async (address) => {
92
+ const balanceResult = await __classPrivateFieldGet(this, _AccountTrackerRpcBalanceFetcher_instances, "m", _AccountTrackerRpcBalanceFetcher_getBalanceFromChain).call(this, address, ethQuery).catch(() => null);
93
+ if (balanceResult) {
94
+ chainResults.push({
95
+ success: true,
96
+ value: new bn_js_1.default(balanceResult.replace('0x', ''), 16),
97
+ account: address,
98
+ token: ZERO_ADDRESS,
99
+ chainId,
100
+ });
101
+ }
102
+ else {
103
+ chainResults.push({
104
+ success: false,
105
+ account: address,
106
+ token: ZERO_ADDRESS,
107
+ chainId,
108
+ });
109
+ }
110
+ });
111
+ await Promise.allSettled(balancePromises);
112
+ return workingResult;
113
+ },
114
+ });
115
+ }
116
+ // Fetch staked balances if enabled
117
+ if (__classPrivateFieldGet(this, _AccountTrackerRpcBalanceFetcher_includeStakedAssets, "f")) {
118
+ const stakedBalancesPromise = __classPrivateFieldGet(this, _AccountTrackerRpcBalanceFetcher_getStakedBalanceForChain, "f").call(this, accountsToUpdate, chainId);
119
+ const stakedBalanceResult = await (0, controller_utils_1.safelyExecuteWithTimeout)(async () => (await stakedBalancesPromise));
120
+ if (stakedBalanceResult) {
121
+ // Find the staking contract address for this chain
122
+ const stakingContractAddress = AssetsContractController_1.STAKING_CONTRACT_ADDRESS_BY_CHAINID[chainId];
123
+ if (stakingContractAddress) {
124
+ Object.entries(stakedBalanceResult).forEach(([address, balance]) => {
125
+ chainResults.push({
126
+ success: true,
127
+ value: balance
128
+ ? new bn_js_1.default(balance.replace('0x', ''), 16)
129
+ : new bn_js_1.default('0'),
130
+ account: address,
131
+ token: (0, controller_utils_1.toChecksumHexAddress)(stakingContractAddress),
132
+ chainId,
133
+ });
134
+ });
135
+ }
136
+ }
137
+ }
138
+ return chainResults;
139
+ });
140
+ // Wait for all chains to complete (or fail) and collect results
141
+ const chainResultsArray = await Promise.allSettled(chainProcessingPromises);
142
+ const results = [];
143
+ chainResultsArray.forEach((chainResult) => {
144
+ if (chainResult.status === 'fulfilled') {
145
+ results.push(...chainResult.value);
146
+ }
147
+ else {
148
+ // Log error but continue with other chains
149
+ console.warn('Chain processing failed:', chainResult.reason);
150
+ }
151
+ });
152
+ return results;
153
+ }
154
+ }
155
+ _AccountTrackerRpcBalanceFetcher_getProvider = new WeakMap(), _AccountTrackerRpcBalanceFetcher_getNetworkClient = new WeakMap(), _AccountTrackerRpcBalanceFetcher_includeStakedAssets = new WeakMap(), _AccountTrackerRpcBalanceFetcher_getStakedBalanceForChain = new WeakMap(), _AccountTrackerRpcBalanceFetcher_instances = new WeakSet(), _AccountTrackerRpcBalanceFetcher_getBalanceFromChain =
156
+ /**
157
+ * Fetches the balance of a given address from the blockchain.
158
+ *
159
+ * @param address - The account address to fetch the balance for.
160
+ * @param ethQuery - The EthQuery instance to query getBalance with.
161
+ * @returns A promise that resolves to the balance in a hex string format.
162
+ */
163
+ async function _AccountTrackerRpcBalanceFetcher_getBalanceFromChain(address, ethQuery) {
164
+ return await (0, controller_utils_1.safelyExecuteWithTimeout)(async () => {
165
+ (0, utils_1.assert)(ethQuery, 'Provider not set.');
166
+ return await (0, controller_utils_1.query)(ethQuery, 'getBalance', [address]);
167
+ });
168
+ };
34
169
  const accountTrackerMetadata = {
35
170
  accountsByChainId: {
36
171
  persist: true,
@@ -50,8 +185,10 @@ class AccountTrackerController extends (0, polling_controller_1.StaticIntervalPo
50
185
  * @param options.messenger - The controller messaging system.
51
186
  * @param options.getStakedBalanceForChain - The function to get the staked native asset balance for a chain.
52
187
  * @param options.includeStakedAssets - Whether to include staked assets in the account balances.
188
+ * @param options.useAccountsAPI - Enable Accounts‑API strategy (if supported chain).
189
+ * @param options.allowExternalServices - Disable external HTTP calls (privacy / offline mode).
53
190
  */
54
- constructor({ interval = 10000, state, messenger, getStakedBalanceForChain, includeStakedAssets = false, }) {
191
+ constructor({ interval = 10000, state, messenger, getStakedBalanceForChain, includeStakedAssets = false, useAccountsAPI = false, allowExternalServices = () => true, }) {
55
192
  const { selectedNetworkClientId } = messenger.call('NetworkController:getState');
56
193
  const { configuration: { chainId }, } = messenger.call('NetworkController:getNetworkClientById', selectedNetworkClientId);
57
194
  super({
@@ -69,8 +206,29 @@ class AccountTrackerController extends (0, polling_controller_1.StaticIntervalPo
69
206
  _AccountTrackerController_refreshMutex.set(this, new async_mutex_1.Mutex());
70
207
  _AccountTrackerController_includeStakedAssets.set(this, void 0);
71
208
  _AccountTrackerController_getStakedBalanceForChain.set(this, void 0);
209
+ _AccountTrackerController_balanceFetchers.set(this, void 0);
210
+ _AccountTrackerController_getProvider.set(this, (chainId) => {
211
+ const { networkConfigurationsByChainId } = this.messagingSystem.call('NetworkController:getState');
212
+ const cfg = networkConfigurationsByChainId[chainId];
213
+ const { networkClientId } = cfg.rpcEndpoints[cfg.defaultRpcEndpointIndex];
214
+ const client = this.messagingSystem.call('NetworkController:getNetworkClientById', networkClientId);
215
+ return new providers_1.Web3Provider(client.provider);
216
+ });
217
+ _AccountTrackerController_getNetworkClient.set(this, (chainId) => {
218
+ const { networkConfigurationsByChainId } = this.messagingSystem.call('NetworkController:getState');
219
+ const cfg = networkConfigurationsByChainId[chainId];
220
+ const { networkClientId } = cfg.rpcEndpoints[cfg.defaultRpcEndpointIndex];
221
+ return this.messagingSystem.call('NetworkController:getNetworkClientById', networkClientId);
222
+ });
72
223
  __classPrivateFieldSet(this, _AccountTrackerController_getStakedBalanceForChain, getStakedBalanceForChain, "f");
73
224
  __classPrivateFieldSet(this, _AccountTrackerController_includeStakedAssets, includeStakedAssets, "f");
225
+ // Initialize balance fetchers - Strategy order: API first, then RPC fallback
226
+ __classPrivateFieldSet(this, _AccountTrackerController_balanceFetchers, [
227
+ ...(useAccountsAPI && allowExternalServices()
228
+ ? [new api_balance_fetcher_1.AccountsApiBalanceFetcher('extension', __classPrivateFieldGet(this, _AccountTrackerController_getProvider, "f"))]
229
+ : []),
230
+ new AccountTrackerRpcBalanceFetcher(__classPrivateFieldGet(this, _AccountTrackerController_getProvider, "f"), __classPrivateFieldGet(this, _AccountTrackerController_getNetworkClient, "f"), includeStakedAssets, getStakedBalanceForChain),
231
+ ], "f");
74
232
  this.setIntervalLength(interval);
75
233
  this.messagingSystem.subscribe('AccountsController:selectedEvmAccountChange', (newAddress, prevAddress) => {
76
234
  if (newAddress !== prevAddress) {
@@ -140,6 +298,8 @@ class AccountTrackerController extends (0, polling_controller_1.StaticIntervalPo
140
298
  */
141
299
  async refresh(networkClientIds) {
142
300
  const selectedAccount = this.messagingSystem.call('AccountsController:getSelectedAccount');
301
+ const allAccounts = this.messagingSystem.call('AccountsController:listAccounts');
302
+ const { isMultiAccountBalancesEnabled } = this.messagingSystem.call('PreferencesController:getState');
143
303
  const releaseLock = await __classPrivateFieldGet(this, _AccountTrackerController_refreshMutex, "f").acquire();
144
304
  try {
145
305
  const chainIds = networkClientIds.map((networkClientId) => {
@@ -147,83 +307,74 @@ class AccountTrackerController extends (0, polling_controller_1.StaticIntervalPo
147
307
  return chainId;
148
308
  });
149
309
  this.syncAccounts(chainIds);
150
- // Create an array of promises for each networkClientId
151
- const updatePromises = networkClientIds.map(async (networkClientId) => {
152
- const { chainId, ethQuery, provider, blockTracker } = __classPrivateFieldGet(this, _AccountTrackerController_instances, "m", _AccountTrackerController_getCorrectNetworkClient).call(this, networkClientId);
153
- const { accountsByChainId } = this.state;
154
- const { isMultiAccountBalancesEnabled } = this.messagingSystem.call('PreferencesController:getState');
155
- const accountsToUpdate = isMultiAccountBalancesEnabled
156
- ? Object.keys(accountsByChainId[chainId])
157
- : [(0, controller_utils_1.toChecksumHexAddress)(selectedAccount.address)];
158
- const accountsForChain = { ...accountsByChainId[chainId] };
159
- // Force fresh block data before multicall
160
- // TODO: This is a temporary fix to ensure that the block number is up to date.
161
- // We should remove this once we have a better solution for this on the block tracker controller.
162
- await (0, controller_utils_1.safelyExecuteWithTimeout)(() => blockTracker?.checkForLatestBlock?.());
163
- const stakedBalancesPromise = __classPrivateFieldGet(this, _AccountTrackerController_includeStakedAssets, "f")
164
- ? __classPrivateFieldGet(this, _AccountTrackerController_getStakedBalanceForChain, "f").call(this, accountsToUpdate, networkClientId)
165
- : Promise.resolve({});
166
- if ((0, utils_1.hasProperty)(AssetsContractController_1.SINGLE_CALL_BALANCES_ADDRESS_BY_CHAINID, chainId)) {
167
- const contractAddress = AssetsContractController_1.SINGLE_CALL_BALANCES_ADDRESS_BY_CHAINID[chainId];
168
- const contract = new contracts_1.Contract(contractAddress, single_call_balance_checker_abi_1.default, new providers_1.Web3Provider(provider));
169
- const nativeBalances = await (0, controller_utils_1.safelyExecuteWithTimeout)(() => contract.balances(accountsToUpdate, [
170
- '0x0000000000000000000000000000000000000000',
171
- ]), false, 3000);
172
- if (nativeBalances) {
173
- accountsToUpdate.forEach((address, index) => {
174
- accountsForChain[address] = {
175
- balance: nativeBalances[index].toHexString(),
176
- };
177
- });
178
- }
310
+ // Use balance fetchers with fallback strategy
311
+ const aggregated = [];
312
+ let remainingChains = [...chainIds];
313
+ // Try each fetcher in order, removing successfully processed chains
314
+ for (const fetcher of __classPrivateFieldGet(this, _AccountTrackerController_balanceFetchers, "f")) {
315
+ const supportedChains = remainingChains.filter((c) => fetcher.supports(c));
316
+ if (!supportedChains.length) {
317
+ continue;
179
318
  }
180
- else {
181
- // Process accounts in batches using reduceInBatchesSerially
182
- await (0, assetsUtil_1.reduceInBatchesSerially)({
183
- values: accountsToUpdate,
184
- batchSize: assetsUtil_1.TOKEN_PRICES_BATCH_SIZE,
185
- initialResult: undefined,
186
- eachBatch: async (workingResult, batch) => {
187
- const balancePromises = batch.map(async (address) => {
188
- const balanceResult = await __classPrivateFieldGet(this, _AccountTrackerController_instances, "m", _AccountTrackerController_getBalanceFromChain).call(this, address, ethQuery).catch(() => null);
189
- // Update account balances
190
- if (balanceResult) {
191
- accountsForChain[address] = {
192
- balance: balanceResult,
193
- };
194
- }
195
- });
196
- await Promise.allSettled(balancePromises);
197
- return workingResult;
198
- },
319
+ try {
320
+ const balances = await fetcher.fetch({
321
+ chainIds: supportedChains,
322
+ queryAllAccounts: isMultiAccountBalancesEnabled,
323
+ selectedAccount: (0, controller_utils_1.toChecksumHexAddress)(selectedAccount.address),
324
+ allAccounts,
199
325
  });
326
+ if (balances && balances.length > 0) {
327
+ aggregated.push(...balances);
328
+ // Remove chains that were successfully processed
329
+ const processedChains = new Set(balances.map((b) => b.chainId));
330
+ remainingChains = remainingChains.filter((chain) => !processedChains.has(chain));
331
+ }
200
332
  }
201
- const stakedBalanceResult = await (0, controller_utils_1.safelyExecuteWithTimeout)(async () => (await stakedBalancesPromise));
202
- Object.entries(stakedBalanceResult ?? {}).forEach(([address, balance]) => {
203
- accountsForChain[address] = {
204
- ...accountsForChain[address],
205
- stakedBalance: balance,
206
- };
207
- });
208
- // After all batches are processed, return the updated data
209
- return { chainId, accountsForChain };
210
- });
211
- // Wait for all networkClientId updates to settle in parallel
212
- const allResults = await Promise.allSettled(updatePromises);
333
+ catch (error) {
334
+ console.warn(`Balance fetcher failed for chains ${supportedChains.join(', ')}: ${String(error)}`);
335
+ // Continue to next fetcher (fallback)
336
+ }
337
+ // If all chains have been processed, break early
338
+ if (remainingChains.length === 0) {
339
+ break;
340
+ }
341
+ }
213
342
  // Build a _copy_ of the current state and track whether anything changed
214
343
  const nextAccountsByChainId = (0, lodash_1.cloneDeep)(this.state.accountsByChainId);
215
344
  let hasChanges = false;
216
- allResults.forEach((result) => {
217
- if (result.status === 'fulfilled') {
218
- const { chainId, accountsForChain } = result.value;
219
- // Only mark as changed if the incoming data differs
220
- if (!(0, lodash_1.isEqual)(nextAccountsByChainId[chainId], accountsForChain)) {
221
- nextAccountsByChainId[chainId] = accountsForChain;
222
- hasChanges = true;
345
+ // Process the aggregated balance results
346
+ const stakedBalancesByChainAndAddress = {};
347
+ aggregated.forEach(({ success, value, account, token, chainId }) => {
348
+ if (success && value !== undefined) {
349
+ const hexValue = `0x${value.toString(16)}`;
350
+ if (token === ZERO_ADDRESS) {
351
+ // Native balance
352
+ if (nextAccountsByChainId[chainId][account].balance !== hexValue) {
353
+ nextAccountsByChainId[chainId][account].balance = hexValue;
354
+ hasChanges = true;
355
+ }
356
+ }
357
+ else {
358
+ // Staked balance (from staking contract address)
359
+ if (!stakedBalancesByChainAndAddress[chainId]) {
360
+ stakedBalancesByChainAndAddress[chainId] = {};
361
+ }
362
+ stakedBalancesByChainAndAddress[chainId][account] = hexValue;
223
363
  }
224
364
  }
225
365
  });
226
- // 👇🏻 call `update` only when something is new / different
366
+ // Apply staked balances
367
+ Object.entries(stakedBalancesByChainAndAddress).forEach(([chainId, balancesByAddress]) => {
368
+ Object.entries(balancesByAddress).forEach(([address, stakedBalance]) => {
369
+ if (nextAccountsByChainId[chainId][address].stakedBalance !==
370
+ stakedBalance) {
371
+ nextAccountsByChainId[chainId][address].stakedBalance =
372
+ stakedBalance;
373
+ hasChanges = true;
374
+ }
375
+ });
376
+ });
377
+ // Only update state if something changed
227
378
  if (hasChanges) {
228
379
  this.update((state) => {
229
380
  state.accountsByChainId = nextAccountsByChainId;
@@ -318,7 +469,7 @@ class AccountTrackerController extends (0, polling_controller_1.StaticIntervalPo
318
469
  }
319
470
  }
320
471
  exports.AccountTrackerController = AccountTrackerController;
321
- _AccountTrackerController_refreshMutex = new WeakMap(), _AccountTrackerController_includeStakedAssets = new WeakMap(), _AccountTrackerController_getStakedBalanceForChain = new WeakMap(), _AccountTrackerController_instances = new WeakSet(), _AccountTrackerController_getCorrectNetworkClient = function _AccountTrackerController_getCorrectNetworkClient(networkClientId) {
472
+ _AccountTrackerController_refreshMutex = new WeakMap(), _AccountTrackerController_includeStakedAssets = new WeakMap(), _AccountTrackerController_getStakedBalanceForChain = new WeakMap(), _AccountTrackerController_balanceFetchers = new WeakMap(), _AccountTrackerController_getProvider = new WeakMap(), _AccountTrackerController_getNetworkClient = new WeakMap(), _AccountTrackerController_instances = new WeakSet(), _AccountTrackerController_getCorrectNetworkClient = function _AccountTrackerController_getCorrectNetworkClient(networkClientId) {
322
473
  const selectedNetworkClientId = networkClientId ??
323
474
  this.messagingSystem.call('NetworkController:getState')
324
475
  .selectedNetworkClientId;
@@ -332,19 +483,6 @@ _AccountTrackerController_refreshMutex = new WeakMap(), _AccountTrackerControlle
332
483
  }, _AccountTrackerController_getNetworkClientIds = function _AccountTrackerController_getNetworkClientIds() {
333
484
  const { networkConfigurationsByChainId } = this.messagingSystem.call('NetworkController:getState');
334
485
  return Object.values(networkConfigurationsByChainId).flatMap((networkConfiguration) => networkConfiguration.rpcEndpoints.map((rpcEndpoint) => rpcEndpoint.networkClientId));
335
- }, _AccountTrackerController_getBalanceFromChain =
336
- /**
337
- * Fetches the balance of a given address from the blockchain.
338
- *
339
- * @param address - The account address to fetch the balance for.
340
- * @param ethQuery - The EthQuery instance to query getBalnce with.
341
- * @returns A promise that resolves to the balance in a hex string format.
342
- */
343
- async function _AccountTrackerController_getBalanceFromChain(address, ethQuery) {
344
- return await (0, controller_utils_1.safelyExecuteWithTimeout)(async () => {
345
- (0, utils_1.assert)(ethQuery, 'Provider not set.');
346
- return await (0, controller_utils_1.query)(ethQuery, 'getBalance', [address]);
347
- });
348
486
  }, _AccountTrackerController_registerMessageHandlers = function _AccountTrackerController_registerMessageHandlers() {
349
487
  this.messagingSystem.registerActionHandler(`${controllerName}:updateNativeBalances`, this.updateNativeBalances.bind(this));
350
488
  this.messagingSystem.registerActionHandler(`${controllerName}:updateStakedBalances`, this.updateStakedBalances.bind(this));