@metamask/assets-controllers 73.3.0 → 74.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +51 -1
  2. package/dist/AccountTrackerController.cjs +214 -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 +216 -85
  9. package/dist/AccountTrackerController.mjs.map +1 -1
  10. package/dist/TokenBalancesController.cjs +14 -9
  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 +15 -10
  15. package/dist/TokenBalancesController.mjs.map +1 -1
  16. package/dist/index.cjs +3 -1
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +2 -0
  19. package/dist/index.d.cts.map +1 -1
  20. package/dist/index.d.mts +2 -0
  21. package/dist/index.d.mts.map +1 -1
  22. package/dist/index.mjs +1 -0
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/multicall.cjs +1 -1
  25. package/dist/multicall.cjs.map +1 -1
  26. package/dist/multicall.mjs +1 -1
  27. package/dist/multicall.mjs.map +1 -1
  28. package/dist/selectors/stringify-balance.cjs +43 -0
  29. package/dist/selectors/stringify-balance.cjs.map +1 -0
  30. package/dist/selectors/stringify-balance.d.cts +8 -0
  31. package/dist/selectors/stringify-balance.d.cts.map +1 -0
  32. package/dist/selectors/stringify-balance.d.mts +8 -0
  33. package/dist/selectors/stringify-balance.d.mts.map +1 -0
  34. package/dist/selectors/stringify-balance.mjs +39 -0
  35. package/dist/selectors/stringify-balance.mjs.map +1 -0
  36. package/dist/selectors/token-selectors.cjs +290 -0
  37. package/dist/selectors/token-selectors.cjs.map +1 -0
  38. package/dist/selectors/token-selectors.d.cts +810 -0
  39. package/dist/selectors/token-selectors.d.cts.map +1 -0
  40. package/dist/selectors/token-selectors.d.mts +810 -0
  41. package/dist/selectors/token-selectors.d.mts.map +1 -0
  42. package/dist/selectors/token-selectors.mjs +287 -0
  43. package/dist/selectors/token-selectors.mjs.map +1 -0
  44. package/package.json +12 -12
package/CHANGELOG.md CHANGED
@@ -7,6 +7,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [74.1.0]
11
+
12
+ ### Added
13
+
14
+ - Enable `AccountTrackerController` to fetch native balances using AccountsAPI when `allowExternalServices` is enabled ([#6369](https://github.com/MetaMask/core/pull/6369))
15
+
16
+ - Implement native balance fetching via AccountsAPI when `useAccountsAPI` and `allowExternalServices` are both true
17
+ - Add fallback to RPC balance fetching when external services are disabled
18
+ - Add comprehensive test coverage for both AccountsAPI and RPC balance fetching scenarios
19
+
20
+ ### Changed
21
+
22
+ - Bump `@metamask/base-controller` from `^8.1.0` to `^8.2.0` ([#6355](https://github.com/MetaMask/core/pull/6355))
23
+
24
+ - Add new `accountId` field to the `Asset` type ([#6358](https://github.com/MetaMask/core/pull/6358))
25
+
26
+ ### Fixed
27
+
28
+ - Uses `InternalAccount['type']` for the `Asset['type']` property ([#6358](https://github.com/MetaMask/core/pull/6358))
29
+
30
+ - 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))
31
+
32
+ - Prevents mutation of memoized fields used inside selectors ([#6358](https://github.com/MetaMask/core/pull/6358))
33
+
34
+ - Fix duplicate token balance entries caused by case-sensitive address comparison in `TokenBalancesController.updateBalances` ([#6354](https://github.com/MetaMask/core/pull/6354))
35
+
36
+ - 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
37
+ - Add comprehensive unit tests for token address normalization scenarios
38
+
39
+ - Fix TokenBalancesController timeout handling by replacing `safelyExecuteWithTimeout` with proper `Promise.race` implementation ([#6365](https://github.com/MetaMask/core/pull/6365))
40
+
41
+ - Replace `safelyExecuteWithTimeout` which was silently swallowing timeout errors with direct `Promise.race` that properly throws
42
+ - Reduce RPC timeout from 3 minutes to 15 seconds for better responsiveness and batch size
43
+ - Enable proper fallback between API and RPC balance fetchers when timeouts occur
44
+
45
+ ## [74.0.0]
46
+
47
+ ### Added
48
+
49
+ - Added a token selector that returns list of tokens and balances for evm and multichain assets based on the selected account group ([#6226](https://github.com/MetaMask/core/pull/6226))
50
+
51
+ ### Changed
52
+
53
+ - **BREAKING:** Bump peer dependency `@metamask/accounts-controller` from `^32.0.0` to `^33.0.0` ([#6345](https://github.com/MetaMask/core/pull/6345))
54
+ - **BREAKING:** Bump peer dependency `@metamask/keyring-controller` from `^22.0.0` to `^23.0.0` ([#6345](https://github.com/MetaMask/core/pull/6345))
55
+ - **BREAKING:** Bump peer dependency `@metamask/preferences-controller` from `^18.0.0` to `^19.0.0` ([#6345](https://github.com/MetaMask/core/pull/6345))
56
+ - **BREAKING:** Bump peer dependency `@metamask/transaction-controller` from `^59.0.0` to `^60.0.0` ([#6345](https://github.com/MetaMask/core/pull/6345))
57
+
10
58
  ## [73.3.0]
11
59
 
12
60
  ### Changed
@@ -1866,7 +1914,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1866
1914
 
1867
1915
  - Use Ethers for AssetsContractController ([#845](https://github.com/MetaMask/core/pull/845))
1868
1916
 
1869
- [Unreleased]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@73.3.0...HEAD
1917
+ [Unreleased]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@74.1.0...HEAD
1918
+ [74.1.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@74.0.0...@metamask/assets-controllers@74.1.0
1919
+ [74.0.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@73.3.0...@metamask/assets-controllers@74.0.0
1870
1920
  [73.3.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@73.2.0...@metamask/assets-controllers@73.3.0
1871
1921
  [73.2.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@73.1.0...@metamask/assets-controllers@73.2.0
1872
1922
  [73.1.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@73.0.2...@metamask/assets-controllers@73.1.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,136 @@ 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 DEFAULT_TIMEOUT_MS = 15000;
37
+ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
38
+ /**
39
+ * RPC-based balance fetcher for AccountTrackerController.
40
+ * Fetches only native balances and staked balances (no token balances).
41
+ */
42
+ class AccountTrackerRpcBalanceFetcher {
43
+ constructor(getProvider, getNetworkClient, includeStakedAssets, getStakedBalanceForChain) {
44
+ _AccountTrackerRpcBalanceFetcher_instances.add(this);
45
+ _AccountTrackerRpcBalanceFetcher_getProvider.set(this, void 0);
46
+ _AccountTrackerRpcBalanceFetcher_getNetworkClient.set(this, void 0);
47
+ _AccountTrackerRpcBalanceFetcher_includeStakedAssets.set(this, void 0);
48
+ _AccountTrackerRpcBalanceFetcher_getStakedBalanceForChain.set(this, void 0);
49
+ __classPrivateFieldSet(this, _AccountTrackerRpcBalanceFetcher_getProvider, getProvider, "f");
50
+ __classPrivateFieldSet(this, _AccountTrackerRpcBalanceFetcher_getNetworkClient, getNetworkClient, "f");
51
+ __classPrivateFieldSet(this, _AccountTrackerRpcBalanceFetcher_includeStakedAssets, includeStakedAssets, "f");
52
+ __classPrivateFieldSet(this, _AccountTrackerRpcBalanceFetcher_getStakedBalanceForChain, getStakedBalanceForChain, "f");
53
+ }
54
+ supports() {
55
+ return true; // fallback – supports every chain
56
+ }
57
+ async fetch({ chainIds, queryAllAccounts, selectedAccount, allAccounts, }) {
58
+ const results = [];
59
+ for (const chainId of chainIds) {
60
+ const accountsToUpdate = queryAllAccounts
61
+ ? Object.values(allAccounts).map((account) => (0, controller_utils_1.toChecksumHexAddress)(account.address))
62
+ : [selectedAccount];
63
+ const { provider, blockTracker } = __classPrivateFieldGet(this, _AccountTrackerRpcBalanceFetcher_getNetworkClient, "f").call(this, chainId);
64
+ const ethQuery = new eth_query_1.default(provider);
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
+ results.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
+ results.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
+ results.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
+ results.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
+ }
139
+ return results;
140
+ }
141
+ }
142
+ _AccountTrackerRpcBalanceFetcher_getProvider = new WeakMap(), _AccountTrackerRpcBalanceFetcher_getNetworkClient = new WeakMap(), _AccountTrackerRpcBalanceFetcher_includeStakedAssets = new WeakMap(), _AccountTrackerRpcBalanceFetcher_getStakedBalanceForChain = new WeakMap(), _AccountTrackerRpcBalanceFetcher_instances = new WeakSet(), _AccountTrackerRpcBalanceFetcher_getBalanceFromChain =
143
+ /**
144
+ * Fetches the balance of a given address from the blockchain.
145
+ *
146
+ * @param address - The account address to fetch the balance for.
147
+ * @param ethQuery - The EthQuery instance to query getBalance with.
148
+ * @returns A promise that resolves to the balance in a hex string format.
149
+ */
150
+ async function _AccountTrackerRpcBalanceFetcher_getBalanceFromChain(address, ethQuery) {
151
+ return await (0, controller_utils_1.safelyExecuteWithTimeout)(async () => {
152
+ (0, utils_1.assert)(ethQuery, 'Provider not set.');
153
+ return await (0, controller_utils_1.query)(ethQuery, 'getBalance', [address]);
154
+ });
155
+ };
34
156
  const accountTrackerMetadata = {
35
157
  accountsByChainId: {
36
158
  persist: true,
@@ -50,8 +172,10 @@ class AccountTrackerController extends (0, polling_controller_1.StaticIntervalPo
50
172
  * @param options.messenger - The controller messaging system.
51
173
  * @param options.getStakedBalanceForChain - The function to get the staked native asset balance for a chain.
52
174
  * @param options.includeStakedAssets - Whether to include staked assets in the account balances.
175
+ * @param options.useAccountsAPI - Enable Accounts‑API strategy (if supported chain).
176
+ * @param options.allowExternalServices - Disable external HTTP calls (privacy / offline mode).
53
177
  */
54
- constructor({ interval = 10000, state, messenger, getStakedBalanceForChain, includeStakedAssets = false, }) {
178
+ constructor({ interval = 10000, state, messenger, getStakedBalanceForChain, includeStakedAssets = false, useAccountsAPI = false, allowExternalServices = () => true, }) {
55
179
  const { selectedNetworkClientId } = messenger.call('NetworkController:getState');
56
180
  const { configuration: { chainId }, } = messenger.call('NetworkController:getNetworkClientById', selectedNetworkClientId);
57
181
  super({
@@ -69,8 +193,29 @@ class AccountTrackerController extends (0, polling_controller_1.StaticIntervalPo
69
193
  _AccountTrackerController_refreshMutex.set(this, new async_mutex_1.Mutex());
70
194
  _AccountTrackerController_includeStakedAssets.set(this, void 0);
71
195
  _AccountTrackerController_getStakedBalanceForChain.set(this, void 0);
196
+ _AccountTrackerController_balanceFetchers.set(this, void 0);
197
+ _AccountTrackerController_getProvider.set(this, (chainId) => {
198
+ const { networkConfigurationsByChainId } = this.messagingSystem.call('NetworkController:getState');
199
+ const cfg = networkConfigurationsByChainId[chainId];
200
+ const { networkClientId } = cfg.rpcEndpoints[cfg.defaultRpcEndpointIndex];
201
+ const client = this.messagingSystem.call('NetworkController:getNetworkClientById', networkClientId);
202
+ return new providers_1.Web3Provider(client.provider);
203
+ });
204
+ _AccountTrackerController_getNetworkClient.set(this, (chainId) => {
205
+ const { networkConfigurationsByChainId } = this.messagingSystem.call('NetworkController:getState');
206
+ const cfg = networkConfigurationsByChainId[chainId];
207
+ const { networkClientId } = cfg.rpcEndpoints[cfg.defaultRpcEndpointIndex];
208
+ return this.messagingSystem.call('NetworkController:getNetworkClientById', networkClientId);
209
+ });
72
210
  __classPrivateFieldSet(this, _AccountTrackerController_getStakedBalanceForChain, getStakedBalanceForChain, "f");
73
211
  __classPrivateFieldSet(this, _AccountTrackerController_includeStakedAssets, includeStakedAssets, "f");
212
+ // Initialize balance fetchers - Strategy order: API first, then RPC fallback
213
+ __classPrivateFieldSet(this, _AccountTrackerController_balanceFetchers, [
214
+ ...(useAccountsAPI && allowExternalServices()
215
+ ? [new api_balance_fetcher_1.AccountsApiBalanceFetcher('extension', __classPrivateFieldGet(this, _AccountTrackerController_getProvider, "f"))]
216
+ : []),
217
+ new AccountTrackerRpcBalanceFetcher(__classPrivateFieldGet(this, _AccountTrackerController_getProvider, "f"), __classPrivateFieldGet(this, _AccountTrackerController_getNetworkClient, "f"), includeStakedAssets, getStakedBalanceForChain),
218
+ ], "f");
74
219
  this.setIntervalLength(interval);
75
220
  this.messagingSystem.subscribe('AccountsController:selectedEvmAccountChange', (newAddress, prevAddress) => {
76
221
  if (newAddress !== prevAddress) {
@@ -140,6 +285,8 @@ class AccountTrackerController extends (0, polling_controller_1.StaticIntervalPo
140
285
  */
141
286
  async refresh(networkClientIds) {
142
287
  const selectedAccount = this.messagingSystem.call('AccountsController:getSelectedAccount');
288
+ const allAccounts = this.messagingSystem.call('AccountsController:listAccounts');
289
+ const { isMultiAccountBalancesEnabled } = this.messagingSystem.call('PreferencesController:getState');
143
290
  const releaseLock = await __classPrivateFieldGet(this, _AccountTrackerController_refreshMutex, "f").acquire();
144
291
  try {
145
292
  const chainIds = networkClientIds.map((networkClientId) => {
@@ -147,83 +294,79 @@ class AccountTrackerController extends (0, polling_controller_1.StaticIntervalPo
147
294
  return chainId;
148
295
  });
149
296
  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
- });
297
+ // Use balance fetchers with fallback strategy
298
+ const aggregated = [];
299
+ let remainingChains = [...chainIds];
300
+ // Try each fetcher in order, removing successfully processed chains
301
+ for (const fetcher of __classPrivateFieldGet(this, _AccountTrackerController_balanceFetchers, "f")) {
302
+ const supportedChains = remainingChains.filter((c) => fetcher.supports(c));
303
+ if (!supportedChains.length) {
304
+ continue;
305
+ }
306
+ try {
307
+ const balances = await Promise.race([
308
+ fetcher.fetch({
309
+ chainIds: supportedChains,
310
+ queryAllAccounts: isMultiAccountBalancesEnabled,
311
+ selectedAccount: (0, controller_utils_1.toChecksumHexAddress)(selectedAccount.address),
312
+ allAccounts,
313
+ }),
314
+ new Promise((_resolve, reject) => setTimeout(() => {
315
+ reject(new Error(`Timeout after ${DEFAULT_TIMEOUT_MS}ms`));
316
+ }, DEFAULT_TIMEOUT_MS)),
317
+ ]);
318
+ if (balances && balances.length > 0) {
319
+ aggregated.push(...balances);
320
+ // Remove chains that were successfully processed
321
+ const processedChains = new Set(balances.map((b) => b.chainId));
322
+ remainingChains = remainingChains.filter((chain) => !processedChains.has(chain));
178
323
  }
179
324
  }
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
- },
199
- });
325
+ catch (error) {
326
+ console.warn(`Balance fetcher failed for chains ${supportedChains.join(', ')}: ${String(error)}`);
327
+ // Continue to next fetcher (fallback)
200
328
  }
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);
329
+ // If all chains have been processed, break early
330
+ if (remainingChains.length === 0) {
331
+ break;
332
+ }
333
+ }
213
334
  // Build a _copy_ of the current state and track whether anything changed
214
335
  const nextAccountsByChainId = (0, lodash_1.cloneDeep)(this.state.accountsByChainId);
215
336
  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;
337
+ // Process the aggregated balance results
338
+ const stakedBalancesByChainAndAddress = {};
339
+ aggregated.forEach(({ success, value, account, token, chainId }) => {
340
+ if (success && value !== undefined) {
341
+ const hexValue = `0x${value.toString(16)}`;
342
+ if (token === ZERO_ADDRESS) {
343
+ // Native balance
344
+ if (nextAccountsByChainId[chainId][account].balance !== hexValue) {
345
+ nextAccountsByChainId[chainId][account].balance = hexValue;
346
+ hasChanges = true;
347
+ }
348
+ }
349
+ else {
350
+ // Staked balance (from staking contract address)
351
+ if (!stakedBalancesByChainAndAddress[chainId]) {
352
+ stakedBalancesByChainAndAddress[chainId] = {};
353
+ }
354
+ stakedBalancesByChainAndAddress[chainId][account] = hexValue;
223
355
  }
224
356
  }
225
357
  });
226
- // 👇🏻 call `update` only when something is new / different
358
+ // Apply staked balances
359
+ Object.entries(stakedBalancesByChainAndAddress).forEach(([chainId, balancesByAddress]) => {
360
+ Object.entries(balancesByAddress).forEach(([address, stakedBalance]) => {
361
+ if (nextAccountsByChainId[chainId][address].stakedBalance !==
362
+ stakedBalance) {
363
+ nextAccountsByChainId[chainId][address].stakedBalance =
364
+ stakedBalance;
365
+ hasChanges = true;
366
+ }
367
+ });
368
+ });
369
+ // Only update state if something changed
227
370
  if (hasChanges) {
228
371
  this.update((state) => {
229
372
  state.accountsByChainId = nextAccountsByChainId;
@@ -318,7 +461,7 @@ class AccountTrackerController extends (0, polling_controller_1.StaticIntervalPo
318
461
  }
319
462
  }
320
463
  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) {
464
+ _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
465
  const selectedNetworkClientId = networkClientId ??
323
466
  this.messagingSystem.call('NetworkController:getState')
324
467
  .selectedNetworkClientId;
@@ -332,19 +475,6 @@ _AccountTrackerController_refreshMutex = new WeakMap(), _AccountTrackerControlle
332
475
  }, _AccountTrackerController_getNetworkClientIds = function _AccountTrackerController_getNetworkClientIds() {
333
476
  const { networkConfigurationsByChainId } = this.messagingSystem.call('NetworkController:getState');
334
477
  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
478
  }, _AccountTrackerController_registerMessageHandlers = function _AccountTrackerController_registerMessageHandlers() {
349
479
  this.messagingSystem.registerActionHandler(`${controllerName}:updateNativeBalances`, this.updateNativeBalances.bind(this));
350
480
  this.messagingSystem.registerActionHandler(`${controllerName}:updateStakedBalances`, this.updateStakedBalances.bind(this));