@metamask-previews/assets-controllers 73.1.0-preview-c3b78c5 → 73.1.0-preview-343f3d69

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.
package/CHANGELOG.md CHANGED
@@ -9,8 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ### Added
11
11
 
12
- - Implement network filtering for aggregated balance selectors across mobile and extension ([#6285](https://github.com/MetaMask/core/pull/6285))
13
- - Add aggregated portfolio change calculator in core with period support (1d/7d/30d) and unit tests; exposes `calculateAggregatedChangeForAllWallets` for mobile/extension selectors ([#6285](https://github.com/MetaMask/core/pull/6285))
12
+ - Implement balance change calculator and network filtering ([#6285](https://github.com/MetaMask/core/pull/6285))
13
+ - Add core balance change calculators with period support (1d/7d/30d), network filtering, and group-level computation
14
+ - Add new utility functions for efficient balance fetching using Multicall3 ([#6212](https://github.com/MetaMask/core/pull/6212))
15
+ - Added `aggregate3` function for direct access to Multicall3's aggregate3 method with individual failure handling
16
+ - Added `getTokenBalancesForMultipleAddresses` function to efficiently batch ERC20 and native token balance queries for multiple addresses
17
+ - Supports up to 300 calls per batch with automatic fallback to individual calls on unsupported chains
18
+ - Returns organized balance data as nested maps for easy consumption by client applications
14
19
 
15
20
  ### Changed
16
21
 
@@ -1,7 +1,12 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.multicallOrFallback = void 0;
6
+ exports.getTokenBalancesForMultipleAddresses = exports.aggregate3 = exports.multicallOrFallback = void 0;
4
7
  const contracts_1 = require("@ethersproject/contracts");
8
+ const bn_js_1 = __importDefault(require("bn.js"));
9
+ const AssetsContractController_1 = require("./AssetsContractController.cjs");
5
10
  const assetsUtil_1 = require("./assetsUtil.cjs");
6
11
  // https://github.com/mds1/multicall/blob/main/deployments.json
7
12
  const MULTICALL_CONTRACT_BY_CHAINID = {
@@ -285,6 +290,70 @@ const multicallAbi = [
285
290
  ],
286
291
  },
287
292
  ];
293
+ // Multicall3 ABI for aggregate3 function
294
+ const multicall3Abi = [
295
+ {
296
+ name: 'aggregate3',
297
+ type: 'function',
298
+ stateMutability: 'payable',
299
+ inputs: [
300
+ {
301
+ name: 'calls',
302
+ type: 'tuple[]',
303
+ components: [
304
+ { name: 'target', type: 'address' },
305
+ { name: 'allowFailure', type: 'bool' },
306
+ { name: 'callData', type: 'bytes' },
307
+ ],
308
+ },
309
+ ],
310
+ outputs: [
311
+ {
312
+ name: 'returnData',
313
+ type: 'tuple[]',
314
+ components: [
315
+ { name: 'success', type: 'bool' },
316
+ { name: 'returnData', type: 'bytes' },
317
+ ],
318
+ },
319
+ ],
320
+ },
321
+ ];
322
+ // Constants for encoded strings and addresses
323
+ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
324
+ const BALANCE_OF_FUNCTION = 'balanceOf(address)';
325
+ const GET_ETH_BALANCE_FUNCTION = 'getEthBalance';
326
+ const GET_SHARES_FUNCTION = 'getShares';
327
+ // ERC20 balanceOf ABI
328
+ const ERC20_BALANCE_OF_ABI = [
329
+ {
330
+ name: 'balanceOf',
331
+ type: 'function',
332
+ inputs: [{ name: 'account', type: 'address' }],
333
+ outputs: [{ name: '', type: 'uint256' }],
334
+ stateMutability: 'view',
335
+ },
336
+ ];
337
+ // Multicall3 getEthBalance ABI
338
+ const MULTICALL3_GET_ETH_BALANCE_ABI = [
339
+ {
340
+ name: 'getEthBalance',
341
+ type: 'function',
342
+ inputs: [{ name: 'addr', type: 'address' }],
343
+ outputs: [{ name: 'balance', type: 'uint256' }],
344
+ stateMutability: 'view',
345
+ },
346
+ ];
347
+ // Staking contract getShares ABI
348
+ const STAKING_GET_SHARES_ABI = [
349
+ {
350
+ inputs: [{ internalType: 'address', name: 'account', type: 'address' }],
351
+ name: 'getShares',
352
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
353
+ stateMutability: 'view',
354
+ type: 'function',
355
+ },
356
+ ];
288
357
  const multicall = async (calls, multicallAddress, provider, maxCallsPerMulticall) => {
289
358
  const multicallContract = new contracts_1.Contract(multicallAddress, multicallAbi, provider);
290
359
  return await (0, assetsUtil_1.reduceInBatchesSerially)({
@@ -360,4 +429,329 @@ const multicallOrFallback = async (calls, chainId, provider, maxCallsPerMultical
360
429
  return await fallback(calls, maxCallsParallel);
361
430
  };
362
431
  exports.multicallOrFallback = multicallOrFallback;
432
+ /**
433
+ * Execute multiple contract calls using Multicall3's aggregate3 function.
434
+ * This allows for more efficient batch calls with individual failure handling.
435
+ *
436
+ * @param calls - Array of calls to execute via aggregate3
437
+ * @param chainId - The hexadecimal chain id
438
+ * @param provider - An ethers rpc provider
439
+ * @returns Promise resolving to array of results from aggregate3
440
+ */
441
+ const aggregate3 = async (calls, chainId, provider) => {
442
+ if (calls.length === 0) {
443
+ return [];
444
+ }
445
+ const multicall3Address = MULTICALL_CONTRACT_BY_CHAINID[chainId];
446
+ const multicall3Contract = new contracts_1.Contract(multicall3Address, multicall3Abi, provider);
447
+ return await multicall3Contract.callStatic.aggregate3(calls);
448
+ };
449
+ exports.aggregate3 = aggregate3;
450
+ /**
451
+ * Processes and decodes balance results from aggregate3 calls
452
+ *
453
+ * @param results - Array of results from aggregate3 calls
454
+ * @param callMapping - Array mapping call indices to token and user addresses
455
+ * @param chainId - The hexadecimal chain id
456
+ * @param provider - An ethers rpc provider
457
+ * @param includeStaked - Whether to include staked balances
458
+ * @returns Map of token address to map of user address to balance
459
+ */
460
+ const processBalanceResults = (results, callMapping, chainId, provider, includeStaked) => {
461
+ const balanceMap = {};
462
+ const stakedBalanceMap = {};
463
+ // Create contract instances for decoding
464
+ const erc20Contract = new contracts_1.Contract(ZERO_ADDRESS, ERC20_BALANCE_OF_ABI, provider);
465
+ const multicall3Address = MULTICALL_CONTRACT_BY_CHAINID[chainId];
466
+ const multicall3Contract = new contracts_1.Contract(multicall3Address, MULTICALL3_GET_ETH_BALANCE_ABI, provider);
467
+ const stakingContractAddress = AssetsContractController_1.STAKING_CONTRACT_ADDRESS_BY_CHAINID[chainId];
468
+ const stakingContract = stakingContractAddress
469
+ ? new contracts_1.Contract(stakingContractAddress, STAKING_GET_SHARES_ABI, provider)
470
+ : null;
471
+ results.forEach((result, index) => {
472
+ if (result.success) {
473
+ const { tokenAddress, userAddress, callType } = callMapping[index];
474
+ let balance;
475
+ if (callType === 'native') {
476
+ // For native token, decode the getEthBalance result
477
+ balance = multicall3Contract.interface.decodeFunctionResult(GET_ETH_BALANCE_FUNCTION, result.returnData)[0];
478
+ if (!balanceMap[tokenAddress]) {
479
+ balanceMap[tokenAddress] = {};
480
+ }
481
+ balanceMap[tokenAddress][userAddress] = balance;
482
+ }
483
+ else if (callType === 'staking') {
484
+ // For staking contract, decode the getShares result
485
+ if (stakingContract) {
486
+ balance = stakingContract.interface.decodeFunctionResult(GET_SHARES_FUNCTION, result.returnData)[0];
487
+ stakedBalanceMap[userAddress] = balance;
488
+ }
489
+ }
490
+ else {
491
+ // For ERC20 tokens, decode the balanceOf result
492
+ balance = erc20Contract.interface.decodeFunctionResult(BALANCE_OF_FUNCTION, result.returnData)[0];
493
+ if (!balanceMap[tokenAddress]) {
494
+ balanceMap[tokenAddress] = {};
495
+ }
496
+ balanceMap[tokenAddress][userAddress] = balance;
497
+ }
498
+ }
499
+ });
500
+ const result = { tokenBalances: balanceMap };
501
+ if (includeStaked && Object.keys(stakedBalanceMap).length > 0) {
502
+ result.stakedBalances = stakedBalanceMap;
503
+ }
504
+ return result;
505
+ };
506
+ /**
507
+ * Fallback function to get native token balances using individual eth_getBalance calls
508
+ * when Multicall3 is not supported on the chain.
509
+ *
510
+ * @param userAddresses - Array of user addresses to check balances for
511
+ * @param provider - An ethers rpc provider
512
+ * @param maxCallsParallel - Maximum number of parallel calls (default: 20)
513
+ * @returns Promise resolving to map of user address to balance
514
+ */
515
+ const getNativeBalancesFallback = async (userAddresses, provider, maxCallsParallel = 20) => {
516
+ const balanceMap = {};
517
+ await (0, assetsUtil_1.reduceInBatchesSerially)({
518
+ values: userAddresses,
519
+ batchSize: maxCallsParallel,
520
+ initialResult: undefined,
521
+ eachBatch: async (_, batch) => {
522
+ const results = await Promise.allSettled(batch.map(async (userAddress) => {
523
+ const balance = await provider.getBalance(userAddress);
524
+ return {
525
+ success: true,
526
+ balance: new bn_js_1.default(balance.toString()),
527
+ userAddress,
528
+ };
529
+ }));
530
+ results.forEach((result) => {
531
+ if (result.status === 'fulfilled' &&
532
+ result.value.success &&
533
+ result.value.balance !== null) {
534
+ balanceMap[result.value.userAddress] = result.value.balance;
535
+ }
536
+ });
537
+ },
538
+ });
539
+ return balanceMap;
540
+ };
541
+ /**
542
+ * Fallback function to get token balances using individual calls
543
+ * when Multicall3 is not supported or when aggregate3 calls fail.
544
+ *
545
+ * @param tokenAddresses - Array of ERC20 token contract addresses
546
+ * @param userAddresses - Array of user addresses to check balances for
547
+ * @param provider - An ethers rpc provider
548
+ * @param includeNative - Whether to include native token balances (default: true)
549
+ * @param maxCallsParallel - Maximum number of parallel calls (default: 20)
550
+ * @returns Promise resolving to map of token address to map of user address to balance
551
+ */
552
+ const getTokenBalancesFallback = async (tokenAddresses, userAddresses, provider, includeNative, maxCallsParallel) => {
553
+ const balanceMap = {};
554
+ // Handle ERC20 token balances using the existing fallback function
555
+ if (tokenAddresses.length > 0) {
556
+ const erc20Calls = [];
557
+ const callMapping = [];
558
+ tokenAddresses.forEach((tokenAddress) => {
559
+ userAddresses.forEach((userAddress) => {
560
+ const contract = new contracts_1.Contract(tokenAddress, ERC20_BALANCE_OF_ABI, provider);
561
+ erc20Calls.push({
562
+ contract,
563
+ functionSignature: BALANCE_OF_FUNCTION,
564
+ arguments: [userAddress],
565
+ });
566
+ callMapping.push({ tokenAddress, userAddress });
567
+ });
568
+ });
569
+ const erc20Results = await fallback(erc20Calls, maxCallsParallel);
570
+ erc20Results.forEach((result, index) => {
571
+ if (result.success) {
572
+ const { tokenAddress, userAddress } = callMapping[index];
573
+ if (!balanceMap[tokenAddress]) {
574
+ balanceMap[tokenAddress] = {};
575
+ }
576
+ balanceMap[tokenAddress][userAddress] = result.value;
577
+ }
578
+ });
579
+ }
580
+ // Handle native token balances using the native fallback function
581
+ if (includeNative) {
582
+ const nativeBalances = await getNativeBalancesFallback(userAddresses, provider, maxCallsParallel);
583
+ if (Object.keys(nativeBalances).length > 0) {
584
+ balanceMap[ZERO_ADDRESS] = nativeBalances;
585
+ }
586
+ }
587
+ return balanceMap;
588
+ };
589
+ /**
590
+ * Fallback function to get staked balances using individual calls
591
+ * when Multicall3 is not supported or when aggregate3 calls fail.
592
+ *
593
+ * @param userAddresses - Array of user addresses to check staked balances for
594
+ * @param chainId - The hexadecimal chain id
595
+ * @param provider - An ethers rpc provider
596
+ * @param maxCallsParallel - Maximum number of parallel calls (default: 20)
597
+ * @returns Promise resolving to map of user address to staked balance
598
+ */
599
+ const getStakedBalancesFallback = async (userAddresses, chainId, provider, maxCallsParallel) => {
600
+ const stakedBalanceMap = {};
601
+ const stakingContractAddress = AssetsContractController_1.STAKING_CONTRACT_ADDRESS_BY_CHAINID[chainId];
602
+ if (!stakingContractAddress) {
603
+ // No staking support for this chain
604
+ return stakedBalanceMap;
605
+ }
606
+ const stakingCalls = [];
607
+ const callMapping = [];
608
+ userAddresses.forEach((userAddress) => {
609
+ const contract = new contracts_1.Contract(stakingContractAddress, STAKING_GET_SHARES_ABI, provider);
610
+ stakingCalls.push({
611
+ contract,
612
+ functionSignature: GET_SHARES_FUNCTION,
613
+ arguments: [userAddress],
614
+ });
615
+ callMapping.push({ userAddress });
616
+ });
617
+ const stakingResults = await fallback(stakingCalls, maxCallsParallel);
618
+ stakingResults.forEach((result, index) => {
619
+ if (result.success) {
620
+ const { userAddress } = callMapping[index];
621
+ stakedBalanceMap[userAddress] = result.value;
622
+ }
623
+ });
624
+ return stakedBalanceMap;
625
+ };
626
+ /**
627
+ * Get token balances (both ERC20 and native) for multiple addresses using aggregate3.
628
+ * This is more efficient than individual balanceOf calls for multiple addresses and tokens.
629
+ * Native token balances are mapped to the zero address (0x0000000000000000000000000000000000000000).
630
+ *
631
+ * @param accountTokenGroups - Array of objects containing account addresses and their associated token addresses
632
+ * @param chainId - The hexadecimal chain id
633
+ * @param provider - An ethers rpc provider
634
+ * @param includeNative - Whether to include native token balances (default: true)
635
+ * @param includeStaked - Whether to include staked balances from supported staking contracts (default: false)
636
+ * @returns Promise resolving to object containing tokenBalances map and optional stakedBalances map
637
+ */
638
+ const getTokenBalancesForMultipleAddresses = async (accountTokenGroups, chainId, provider, includeNative, includeStaked) => {
639
+ // Return early if no groups provided
640
+ if (accountTokenGroups.length === 0 && !includeNative && !includeStaked) {
641
+ return { tokenBalances: {} };
642
+ }
643
+ // Extract unique token addresses and user addresses from groups
644
+ const uniqueTokenAddresses = Array.from(new Set(accountTokenGroups.flatMap((group) => group.tokenAddresses))).filter((tokenAddress) => tokenAddress !== ZERO_ADDRESS); // Exclude native token from ERC20 calls
645
+ const uniqueUserAddresses = Array.from(new Set(accountTokenGroups.map((group) => group.accountAddress)));
646
+ // Check if Multicall3 is supported on this chain
647
+ if (!MULTICALL_CONTRACT_BY_CHAINID[chainId]) {
648
+ // Fallback to individual balance calls when Multicall3 is not supported
649
+ const tokenBalances = await getTokenBalancesFallback(uniqueTokenAddresses, uniqueUserAddresses, provider, includeNative, 20);
650
+ const result = { tokenBalances };
651
+ // Handle staked balances fallback if requested
652
+ if (includeStaked) {
653
+ const stakedBalances = await getStakedBalancesFallback(uniqueUserAddresses, chainId, provider, 20);
654
+ if (Object.keys(stakedBalances).length > 0) {
655
+ result.stakedBalances = stakedBalances;
656
+ }
657
+ }
658
+ return result;
659
+ }
660
+ try {
661
+ // Create calls directly from pairs
662
+ const allCalls = [];
663
+ const allCallMapping = [];
664
+ // Create a temporary ERC20 contract for encoding
665
+ const tempERC20Contract = new contracts_1.Contract(ZERO_ADDRESS, ERC20_BALANCE_OF_ABI, provider);
666
+ // Create ERC20 balance calls for all account-token combinations
667
+ accountTokenGroups.forEach((group) => {
668
+ group.tokenAddresses
669
+ .filter((tokenAddress) => tokenAddress !== ZERO_ADDRESS)
670
+ .forEach((tokenAddress) => {
671
+ allCalls.push({
672
+ target: tokenAddress,
673
+ allowFailure: true,
674
+ callData: tempERC20Contract.interface.encodeFunctionData(BALANCE_OF_FUNCTION, [group.accountAddress]),
675
+ });
676
+ allCallMapping.push({
677
+ tokenAddress,
678
+ userAddress: group.accountAddress,
679
+ callType: 'erc20',
680
+ });
681
+ });
682
+ });
683
+ // Add native token balance calls if requested
684
+ if (includeNative) {
685
+ const multicall3Address = MULTICALL_CONTRACT_BY_CHAINID[chainId];
686
+ const multicall3TempContract = new contracts_1.Contract(multicall3Address, MULTICALL3_GET_ETH_BALANCE_ABI, provider);
687
+ uniqueUserAddresses.forEach((userAddress) => {
688
+ allCalls.push({
689
+ target: multicall3Address,
690
+ allowFailure: true,
691
+ callData: multicall3TempContract.interface.encodeFunctionData(GET_ETH_BALANCE_FUNCTION, [userAddress]),
692
+ });
693
+ allCallMapping.push({
694
+ tokenAddress: ZERO_ADDRESS,
695
+ userAddress,
696
+ callType: 'native',
697
+ });
698
+ });
699
+ }
700
+ // Add staking balance calls if requested
701
+ if (includeStaked) {
702
+ const stakingContractAddress = AssetsContractController_1.STAKING_CONTRACT_ADDRESS_BY_CHAINID[chainId];
703
+ if (stakingContractAddress) {
704
+ const stakingContract = new contracts_1.Contract(stakingContractAddress, STAKING_GET_SHARES_ABI, provider);
705
+ uniqueUserAddresses.forEach((userAddress) => {
706
+ allCalls.push({
707
+ target: stakingContractAddress,
708
+ allowFailure: true,
709
+ callData: stakingContract.interface.encodeFunctionData(GET_SHARES_FUNCTION, [userAddress]),
710
+ });
711
+ allCallMapping.push({
712
+ tokenAddress: stakingContractAddress,
713
+ userAddress,
714
+ callType: 'staking',
715
+ });
716
+ });
717
+ }
718
+ }
719
+ // Execute all calls in batches
720
+ const maxCallsPerBatch = 300; // Limit calls per batch to avoid gas/size limits
721
+ const allResults = [];
722
+ await (0, assetsUtil_1.reduceInBatchesSerially)({
723
+ values: allCalls,
724
+ batchSize: maxCallsPerBatch,
725
+ initialResult: undefined,
726
+ eachBatch: async (_, batch) => {
727
+ const batchResults = await (0, exports.aggregate3)(batch, chainId, provider);
728
+ allResults.push(...batchResults);
729
+ },
730
+ });
731
+ // Process and return results
732
+ return processBalanceResults(allResults, allCallMapping, chainId, provider, includeStaked);
733
+ }
734
+ catch (error) {
735
+ // Fallback only on revert
736
+ // https://docs.ethers.org/v5/troubleshooting/errors/#help-CALL_EXCEPTION
737
+ if (!error ||
738
+ typeof error !== 'object' ||
739
+ !('code' in error) ||
740
+ error.code !== 'CALL_EXCEPTION') {
741
+ throw error;
742
+ }
743
+ // Fallback to individual balance calls when aggregate3 fails
744
+ const tokenBalances = await getTokenBalancesFallback(uniqueTokenAddresses, uniqueUserAddresses, provider, includeNative, 20);
745
+ const result = { tokenBalances };
746
+ // Handle staked balances fallback if requested
747
+ if (includeStaked) {
748
+ const stakedBalances = await getStakedBalancesFallback(uniqueUserAddresses, chainId, provider, 20);
749
+ if (Object.keys(stakedBalances).length > 0) {
750
+ result.stakedBalances = stakedBalances;
751
+ }
752
+ }
753
+ return result;
754
+ }
755
+ };
756
+ exports.getTokenBalancesForMultipleAddresses = getTokenBalancesForMultipleAddresses;
363
757
  //# sourceMappingURL=multicall.cjs.map