From 48f678052c8968487cf35a1614006ebdffae1502 Mon Sep 17 00:00:00 2001 From: Anthony Laibe <491074+alaibe@users.noreply.github.com> Date: Fri, 9 Sep 2022 08:58:36 +0200 Subject: [PATCH] feat: batch balance (#2833) --- contracts/contracts.go | 18 ++ contracts/ethscan/address.go | 24 +++ contracts/ethscan/doc.go | 3 + contracts/ethscan/ethscan.go | 342 ++++++++++++++++++++++++++++++++++ contracts/ethscan/ethscan.sol | 64 +++++++ go.mod | 1 - services/wallet/token.go | 126 ++++++++++--- 7 files changed, 553 insertions(+), 25 deletions(-) create mode 100644 contracts/ethscan/address.go create mode 100644 contracts/ethscan/doc.go create mode 100644 contracts/ethscan/ethscan.go create mode 100644 contracts/ethscan/ethscan.sol diff --git a/contracts/contracts.go b/contracts/contracts.go index 0d156364f..2bcad97a9 100644 --- a/contracts/contracts.go +++ b/contracts/contracts.go @@ -4,6 +4,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" "github.com/status-im/status-go/contracts/directory" + "github.com/status-im/status-go/contracts/ethscan" "github.com/status-im/status-go/contracts/registrar" "github.com/status-im/status-go/contracts/resolver" "github.com/status-im/status-go/contracts/snt" @@ -146,3 +147,20 @@ func (c *ContractMaker) NewDirectoryWithBackend(chainID uint64, backend *ethclie backend, ) } + +func (c *ContractMaker) NewEthScan(chainID uint64) (*ethscan.BalanceScanner, error) { + contractAddr, err := ethscan.ContractAddress(chainID) + if err != nil { + return nil, err + } + + backend, err := c.RPCClient.EthClient(chainID) + if err != nil { + return nil, err + } + + return ethscan.NewBalanceScanner( + contractAddr, + backend, + ) +} diff --git a/contracts/ethscan/address.go b/contracts/ethscan/address.go new file mode 100644 index 000000000..8d6b2e4cc --- /dev/null +++ b/contracts/ethscan/address.go @@ -0,0 +1,24 @@ +package ethscan + +import ( + "errors" + + "github.com/ethereum/go-ethereum/common" +) + +var errorNotAvailableOnChainID = errors.New("not available for chainID") + +var contractAddressByChainID = map[uint64]common.Address{ + 1: common.HexToAddress("0x08A8fDBddc160A7d5b957256b903dCAb1aE512C5"), // mainnet + 3: common.HexToAddress("0x08A8fDBddc160A7d5b957256b903dCAb1aE512C5"), // ropsten + 4: common.HexToAddress("0x08A8fDBddc160A7d5b957256b903dCAb1aE512C5"), // rinkeby + 5: common.HexToAddress("0x08A8fDBddc160A7d5b957256b903dCAb1aE512C5"), // goerli +} + +func ContractAddress(chainID uint64) (common.Address, error) { + addr, exists := contractAddressByChainID[chainID] + if !exists { + return *new(common.Address), errorNotAvailableOnChainID + } + return addr, nil +} diff --git a/contracts/ethscan/doc.go b/contracts/ethscan/doc.go new file mode 100644 index 000000000..bf403f316 --- /dev/null +++ b/contracts/ethscan/doc.go @@ -0,0 +1,3 @@ +package ethscan + +//go:generate abigen -sol ethscan.sol -pkg ethscan -out ethscan.go diff --git a/contracts/ethscan/ethscan.go b/contracts/ethscan/ethscan.go new file mode 100644 index 000000000..64439e50f --- /dev/null +++ b/contracts/ethscan/ethscan.go @@ -0,0 +1,342 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package ethscan + +import ( + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription +) + +// BalanceScannerResult is an auto generated low-level Go binding around an user-defined struct. +type BalanceScannerResult struct { + Success bool + Data []byte +} + +// BalanceScannerABI is the input ABI used to generate the binding from. +const BalanceScannerABI = "[{\"inputs\":[{\"internalType\":\"address[]\",\"name\":\"contracts\",\"type\":\"address[]\"},{\"internalType\":\"bytes[]\",\"name\":\"data\",\"type\":\"bytes[]\"},{\"internalType\":\"uint256\",\"name\":\"gas\",\"type\":\"uint256\"}],\"name\":\"call\",\"outputs\":[{\"components\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"}],\"internalType\":\"structBalanceScanner.Result[]\",\"name\":\"results\",\"type\":\"tuple[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address[]\",\"name\":\"contracts\",\"type\":\"address[]\"},{\"internalType\":\"bytes[]\",\"name\":\"data\",\"type\":\"bytes[]\"}],\"name\":\"call\",\"outputs\":[{\"components\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"}],\"internalType\":\"structBalanceScanner.Result[]\",\"name\":\"results\",\"type\":\"tuple[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address[]\",\"name\":\"addresses\",\"type\":\"address[]\"}],\"name\":\"etherBalances\",\"outputs\":[{\"components\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"}],\"internalType\":\"structBalanceScanner.Result[]\",\"name\":\"results\",\"type\":\"tuple[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address[]\",\"name\":\"addresses\",\"type\":\"address[]\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"}],\"name\":\"tokenBalances\",\"outputs\":[{\"components\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"}],\"internalType\":\"structBalanceScanner.Result[]\",\"name\":\"results\",\"type\":\"tuple[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"internalType\":\"address[]\",\"name\":\"contracts\",\"type\":\"address[]\"}],\"name\":\"tokensBalance\",\"outputs\":[{\"components\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"}],\"internalType\":\"structBalanceScanner.Result[]\",\"name\":\"results\",\"type\":\"tuple[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]" + +// BalanceScannerFuncSigs maps the 4-byte function signature to its string representation. +var BalanceScannerFuncSigs = map[string]string{ + "458b3a7c": "call(address[],bytes[])", + "36738374": "call(address[],bytes[],uint256)", + "dbdbb51b": "etherBalances(address[])", + "aad33091": "tokenBalances(address[],address)", + "e5da1b68": "tokensBalance(address,address[])", +} + +// BalanceScanner is an auto generated Go binding around an Ethereum contract. +type BalanceScanner struct { + BalanceScannerCaller // Read-only binding to the contract + BalanceScannerTransactor // Write-only binding to the contract + BalanceScannerFilterer // Log filterer for contract events +} + +// BalanceScannerCaller is an auto generated read-only Go binding around an Ethereum contract. +type BalanceScannerCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// BalanceScannerTransactor is an auto generated write-only Go binding around an Ethereum contract. +type BalanceScannerTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// BalanceScannerFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type BalanceScannerFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// BalanceScannerSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type BalanceScannerSession struct { + Contract *BalanceScanner // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// BalanceScannerCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type BalanceScannerCallerSession struct { + Contract *BalanceScannerCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// BalanceScannerTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type BalanceScannerTransactorSession struct { + Contract *BalanceScannerTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// BalanceScannerRaw is an auto generated low-level Go binding around an Ethereum contract. +type BalanceScannerRaw struct { + Contract *BalanceScanner // Generic contract binding to access the raw methods on +} + +// BalanceScannerCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type BalanceScannerCallerRaw struct { + Contract *BalanceScannerCaller // Generic read-only contract binding to access the raw methods on +} + +// BalanceScannerTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type BalanceScannerTransactorRaw struct { + Contract *BalanceScannerTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewBalanceScanner creates a new instance of BalanceScanner, bound to a specific deployed contract. +func NewBalanceScanner(address common.Address, backend bind.ContractBackend) (*BalanceScanner, error) { + contract, err := bindBalanceScanner(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &BalanceScanner{BalanceScannerCaller: BalanceScannerCaller{contract: contract}, BalanceScannerTransactor: BalanceScannerTransactor{contract: contract}, BalanceScannerFilterer: BalanceScannerFilterer{contract: contract}}, nil +} + +// NewBalanceScannerCaller creates a new read-only instance of BalanceScanner, bound to a specific deployed contract. +func NewBalanceScannerCaller(address common.Address, caller bind.ContractCaller) (*BalanceScannerCaller, error) { + contract, err := bindBalanceScanner(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &BalanceScannerCaller{contract: contract}, nil +} + +// NewBalanceScannerTransactor creates a new write-only instance of BalanceScanner, bound to a specific deployed contract. +func NewBalanceScannerTransactor(address common.Address, transactor bind.ContractTransactor) (*BalanceScannerTransactor, error) { + contract, err := bindBalanceScanner(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &BalanceScannerTransactor{contract: contract}, nil +} + +// NewBalanceScannerFilterer creates a new log filterer instance of BalanceScanner, bound to a specific deployed contract. +func NewBalanceScannerFilterer(address common.Address, filterer bind.ContractFilterer) (*BalanceScannerFilterer, error) { + contract, err := bindBalanceScanner(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &BalanceScannerFilterer{contract: contract}, nil +} + +// bindBalanceScanner binds a generic wrapper to an already deployed contract. +func bindBalanceScanner(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := abi.JSON(strings.NewReader(BalanceScannerABI)) + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_BalanceScanner *BalanceScannerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _BalanceScanner.Contract.BalanceScannerCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_BalanceScanner *BalanceScannerRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _BalanceScanner.Contract.BalanceScannerTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_BalanceScanner *BalanceScannerRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _BalanceScanner.Contract.BalanceScannerTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_BalanceScanner *BalanceScannerCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _BalanceScanner.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_BalanceScanner *BalanceScannerTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _BalanceScanner.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_BalanceScanner *BalanceScannerTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _BalanceScanner.Contract.contract.Transact(opts, method, params...) +} + +// Call is a free data retrieval call binding the contract method 0x36738374. +// +// Solidity: function call(address[] contracts, bytes[] data, uint256 gas) view returns((bool,bytes)[] results) +func (_BalanceScanner *BalanceScannerCaller) Call(opts *bind.CallOpts, contracts []common.Address, data [][]byte, gas *big.Int) ([]BalanceScannerResult, error) { + var out []interface{} + err := _BalanceScanner.contract.Call(opts, &out, "call", contracts, data, gas) + + if err != nil { + return *new([]BalanceScannerResult), err + } + + out0 := *abi.ConvertType(out[0], new([]BalanceScannerResult)).(*[]BalanceScannerResult) + + return out0, err + +} + +// Call is a free data retrieval call binding the contract method 0x36738374. +// +// Solidity: function call(address[] contracts, bytes[] data, uint256 gas) view returns((bool,bytes)[] results) +func (_BalanceScanner *BalanceScannerSession) Call(contracts []common.Address, data [][]byte, gas *big.Int) ([]BalanceScannerResult, error) { + return _BalanceScanner.Contract.Call(&_BalanceScanner.CallOpts, contracts, data, gas) +} + +// Call is a free data retrieval call binding the contract method 0x36738374. +// +// Solidity: function call(address[] contracts, bytes[] data, uint256 gas) view returns((bool,bytes)[] results) +func (_BalanceScanner *BalanceScannerCallerSession) Call(contracts []common.Address, data [][]byte, gas *big.Int) ([]BalanceScannerResult, error) { + return _BalanceScanner.Contract.Call(&_BalanceScanner.CallOpts, contracts, data, gas) +} + +// Call0 is a free data retrieval call binding the contract method 0x458b3a7c. +// +// Solidity: function call(address[] contracts, bytes[] data) view returns((bool,bytes)[] results) +func (_BalanceScanner *BalanceScannerCaller) Call0(opts *bind.CallOpts, contracts []common.Address, data [][]byte) ([]BalanceScannerResult, error) { + var out []interface{} + err := _BalanceScanner.contract.Call(opts, &out, "call0", contracts, data) + + if err != nil { + return *new([]BalanceScannerResult), err + } + + out0 := *abi.ConvertType(out[0], new([]BalanceScannerResult)).(*[]BalanceScannerResult) + + return out0, err + +} + +// Call0 is a free data retrieval call binding the contract method 0x458b3a7c. +// +// Solidity: function call(address[] contracts, bytes[] data) view returns((bool,bytes)[] results) +func (_BalanceScanner *BalanceScannerSession) Call0(contracts []common.Address, data [][]byte) ([]BalanceScannerResult, error) { + return _BalanceScanner.Contract.Call0(&_BalanceScanner.CallOpts, contracts, data) +} + +// Call0 is a free data retrieval call binding the contract method 0x458b3a7c. +// +// Solidity: function call(address[] contracts, bytes[] data) view returns((bool,bytes)[] results) +func (_BalanceScanner *BalanceScannerCallerSession) Call0(contracts []common.Address, data [][]byte) ([]BalanceScannerResult, error) { + return _BalanceScanner.Contract.Call0(&_BalanceScanner.CallOpts, contracts, data) +} + +// EtherBalances is a free data retrieval call binding the contract method 0xdbdbb51b. +// +// Solidity: function etherBalances(address[] addresses) view returns((bool,bytes)[] results) +func (_BalanceScanner *BalanceScannerCaller) EtherBalances(opts *bind.CallOpts, addresses []common.Address) ([]BalanceScannerResult, error) { + var out []interface{} + err := _BalanceScanner.contract.Call(opts, &out, "etherBalances", addresses) + + if err != nil { + return *new([]BalanceScannerResult), err + } + + out0 := *abi.ConvertType(out[0], new([]BalanceScannerResult)).(*[]BalanceScannerResult) + + return out0, err + +} + +// EtherBalances is a free data retrieval call binding the contract method 0xdbdbb51b. +// +// Solidity: function etherBalances(address[] addresses) view returns((bool,bytes)[] results) +func (_BalanceScanner *BalanceScannerSession) EtherBalances(addresses []common.Address) ([]BalanceScannerResult, error) { + return _BalanceScanner.Contract.EtherBalances(&_BalanceScanner.CallOpts, addresses) +} + +// EtherBalances is a free data retrieval call binding the contract method 0xdbdbb51b. +// +// Solidity: function etherBalances(address[] addresses) view returns((bool,bytes)[] results) +func (_BalanceScanner *BalanceScannerCallerSession) EtherBalances(addresses []common.Address) ([]BalanceScannerResult, error) { + return _BalanceScanner.Contract.EtherBalances(&_BalanceScanner.CallOpts, addresses) +} + +// TokenBalances is a free data retrieval call binding the contract method 0xaad33091. +// +// Solidity: function tokenBalances(address[] addresses, address token) view returns((bool,bytes)[] results) +func (_BalanceScanner *BalanceScannerCaller) TokenBalances(opts *bind.CallOpts, addresses []common.Address, token common.Address) ([]BalanceScannerResult, error) { + var out []interface{} + err := _BalanceScanner.contract.Call(opts, &out, "tokenBalances", addresses, token) + + if err != nil { + return *new([]BalanceScannerResult), err + } + + out0 := *abi.ConvertType(out[0], new([]BalanceScannerResult)).(*[]BalanceScannerResult) + + return out0, err + +} + +// TokenBalances is a free data retrieval call binding the contract method 0xaad33091. +// +// Solidity: function tokenBalances(address[] addresses, address token) view returns((bool,bytes)[] results) +func (_BalanceScanner *BalanceScannerSession) TokenBalances(addresses []common.Address, token common.Address) ([]BalanceScannerResult, error) { + return _BalanceScanner.Contract.TokenBalances(&_BalanceScanner.CallOpts, addresses, token) +} + +// TokenBalances is a free data retrieval call binding the contract method 0xaad33091. +// +// Solidity: function tokenBalances(address[] addresses, address token) view returns((bool,bytes)[] results) +func (_BalanceScanner *BalanceScannerCallerSession) TokenBalances(addresses []common.Address, token common.Address) ([]BalanceScannerResult, error) { + return _BalanceScanner.Contract.TokenBalances(&_BalanceScanner.CallOpts, addresses, token) +} + +// TokensBalance is a free data retrieval call binding the contract method 0xe5da1b68. +// +// Solidity: function tokensBalance(address owner, address[] contracts) view returns((bool,bytes)[] results) +func (_BalanceScanner *BalanceScannerCaller) TokensBalance(opts *bind.CallOpts, owner common.Address, contracts []common.Address) ([]BalanceScannerResult, error) { + var out []interface{} + err := _BalanceScanner.contract.Call(opts, &out, "tokensBalance", owner, contracts) + + if err != nil { + return *new([]BalanceScannerResult), err + } + + out0 := *abi.ConvertType(out[0], new([]BalanceScannerResult)).(*[]BalanceScannerResult) + + return out0, err + +} + +// TokensBalance is a free data retrieval call binding the contract method 0xe5da1b68. +// +// Solidity: function tokensBalance(address owner, address[] contracts) view returns((bool,bytes)[] results) +func (_BalanceScanner *BalanceScannerSession) TokensBalance(owner common.Address, contracts []common.Address) ([]BalanceScannerResult, error) { + return _BalanceScanner.Contract.TokensBalance(&_BalanceScanner.CallOpts, owner, contracts) +} + +// TokensBalance is a free data retrieval call binding the contract method 0xe5da1b68. +// +// Solidity: function tokensBalance(address owner, address[] contracts) view returns((bool,bytes)[] results) +func (_BalanceScanner *BalanceScannerCallerSession) TokensBalance(owner common.Address, contracts []common.Address) ([]BalanceScannerResult, error) { + return _BalanceScanner.Contract.TokensBalance(&_BalanceScanner.CallOpts, owner, contracts) +} diff --git a/contracts/ethscan/ethscan.sol b/contracts/ethscan/ethscan.sol new file mode 100644 index 000000000..656e83917 --- /dev/null +++ b/contracts/ethscan/ethscan.sol @@ -0,0 +1,64 @@ +/** + *Submitted for verification at Etherscan.io on 2021-04-07 +*/ + +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.3; + +/** + * @title An Ether or token balance scanner + * @author Maarten Zuidhoorn + * @author Luit Hollander + */ +abstract contract BalanceScanner { + struct Result { + bool success; + bytes data; + } + + /** + * @notice Get the Ether balance for all addresses specified + * @param addresses The addresses to get the Ether balance for + * @return results The Ether balance for all addresses in the same order as specified + */ + function etherBalances(address[] calldata addresses) external virtual view returns (Result[] memory results); + + /** + * @notice Get the ERC-20 token balance of `token` for all addresses specified + * @dev This does not check if the `token` address specified is actually an ERC-20 token + * @param addresses The addresses to get the token balance for + * @param token The address of the ERC-20 token contract + * @return results The token balance for all addresses in the same order as specified + */ + function tokenBalances(address[] calldata addresses, address token) external virtual view returns (Result[] memory results); + + /** + * @notice Get the ERC-20 token balance from multiple contracts for a single owner + * @param owner The address of the token owner + * @param contracts The addresses of the ERC-20 token contracts + * @return results The token balances in the same order as the addresses specified + */ + function tokensBalance(address owner, address[] calldata contracts) external virtual view returns (Result[] memory results); + + /** + * @notice Call multiple contracts with the provided arbitrary data + * @param contracts The contracts to call + * @param data The data to call the contracts with + * @return results The raw result of the contract calls + */ + function call(address[] calldata contracts, bytes[] calldata data) external virtual view returns (Result[] memory results); + + /** + * @notice Call multiple contracts with the provided arbitrary data + * @param contracts The contracts to call + * @param data The data to call the contracts with + * @param gas The amount of gas to call the contracts with + * @return results The raw result of the contract calls + */ + function call( + address[] calldata contracts, + bytes[] calldata data, + uint256 gas + ) public view virtual returns (Result[] memory results); +} \ No newline at end of file diff --git a/go.mod b/go.mod index faef012e7..8aad7b430 100644 --- a/go.mod +++ b/go.mod @@ -259,7 +259,6 @@ require ( golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect golang.org/x/tools v0.1.11 // indirect golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect - gopkg.in/go-playground/assert.v1 v1.2.1 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/urfave/cli.v1 v1.20.0 // indirect diff --git a/services/wallet/token.go b/services/wallet/token.go index e0bf1d248..7a68d3389 100644 --- a/services/wallet/token.go +++ b/services/wallet/token.go @@ -12,6 +12,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/log" + "github.com/status-im/status-go/contracts" "github.com/status-im/status-go/contracts/ierc20" "github.com/status-im/status-go/rpc" "github.com/status-im/status-go/rpc/network" @@ -20,6 +21,7 @@ import ( ) var requestTimeout = 20 * time.Second +var nativeChainAddress = common.HexToAddress("0x") type Token struct { Address common.Address `json:"address"` @@ -279,7 +281,7 @@ func (tm *TokenManager) getChainBalance(ctx context.Context, client *chain.Clien } func (tm *TokenManager) getBalance(ctx context.Context, client *chain.Client, account common.Address, token common.Address) (*big.Int, error) { - if token == common.HexToAddress("0x") { + if token == nativeChainAddress { return tm.getChainBalance(ctx, client, account) } @@ -292,42 +294,118 @@ func (tm *TokenManager) getBalances(parent context.Context, clients []*chain.Cli mu sync.Mutex response = map[common.Address]map[common.Address]*hexutil.Big{} ) + + updateBalance := func(account common.Address, token common.Address, balance *big.Int) { + mu.Lock() + if _, ok := response[account]; !ok { + response[account] = map[common.Address]*hexutil.Big{} + } + + if _, ok := response[account][token]; !ok { + zeroHex := hexutil.Big(*big.NewInt(0)) + response[account][token] = &zeroHex + } + sum := big.NewInt(0).Add(response[account][token].ToInt(), balance) + sumHex := hexutil.Big(*sum) + response[account][token] = &sumHex + mu.Unlock() + } + + contractMaker := contracts.ContractMaker{RPCClient: tm.RPCClient} for clientIdx := range clients { - for tokenIdx := range tokens { - for accountIdx := range accounts { - // Below, we set account, token and client from idx on purpose to avoid override - account := accounts[accountIdx] - token := tokens[tokenIdx] - client := clients[clientIdx] + client := clients[clientIdx] + ethScanContract, err := contractMaker.NewEthScan(client.ChainID) + + if err == nil { + fetchChainBalance := false + var tokenChunks [][]common.Address + chunkSize := 100 + for i := 0; i < len(tokens); i += chunkSize { + end := i + chunkSize + if end > len(tokens) { + end = len(tokens) + } + + tokenChunks = append(tokenChunks, tokens[i:end]) + } + + for _, token := range tokens { + if token == nativeChainAddress { + fetchChainBalance = true + } + } + if fetchChainBalance { group.Add(func(parent context.Context) error { ctx, cancel := context.WithTimeout(parent, requestTimeout) defer cancel() - balance, err := tm.getBalance(ctx, client, account, token) - - // We don't want to return an error here and prevent - // the rest from completing + res, err := ethScanContract.EtherBalances(&bind.CallOpts{ + Context: ctx, + }, accounts) if err != nil { - log.Error("can't fetch erc20 token balance", "account", account, "token", token, "error", err) - + log.Error("can't fetch chain balance", err) return nil } - mu.Lock() - if _, ok := response[account]; !ok { - response[account] = map[common.Address]*hexutil.Big{} + for idx, account := range accounts { + balance := new(big.Int) + balance.SetBytes(res[idx].Data) + updateBalance(account, common.HexToAddress("0x"), balance) } - if _, ok := response[account][token]; !ok { - zeroHex := hexutil.Big(*big.NewInt(0)) - response[account][token] = &zeroHex - } - sum := big.NewInt(0).Add(response[account][token].ToInt(), balance) - sumHex := hexutil.Big(*sum) - response[account][token] = &sumHex - mu.Unlock() return nil }) } + + for accountIdx := range accounts { + account := accounts[accountIdx] + for idx := range tokenChunks { + chunk := tokenChunks[idx] + group.Add(func(parent context.Context) error { + ctx, cancel := context.WithTimeout(parent, requestTimeout) + defer cancel() + res, err := ethScanContract.TokensBalance(&bind.CallOpts{ + Context: ctx, + }, account, chunk) + if err != nil { + log.Error("can't fetch erc20 token balance", "account", account, "error", err) + return nil + } + + for idx, token := range chunk { + if !res[idx].Success { + continue + } + balance := new(big.Int) + balance.SetBytes(res[idx].Data) + updateBalance(account, token, balance) + } + return nil + }) + } + } + } else { + for tokenIdx := range tokens { + for accountIdx := range accounts { + // Below, we set account, token and client from idx on purpose to avoid override + account := accounts[accountIdx] + token := tokens[tokenIdx] + client := clients[clientIdx] + group.Add(func(parent context.Context) error { + ctx, cancel := context.WithTimeout(parent, requestTimeout) + defer cancel() + balance, err := tm.getBalance(ctx, client, account, token) + + if err != nil { + log.Error("can't fetch erc20 token balance", "account", account, "token", token, "error", err) + + return nil + } + updateBalance(account, token, balance) + return nil + }) + } + } } + } select { case <-group.WaitAsync():