eth_call batch integration tests for avax,op,base,bsc

This commit is contained in:
pragmaxim
2026-01-13 12:20:13 +01:00
parent 3868aa8b65
commit c07c869a8a
6 changed files with 267 additions and 66 deletions

View File

@@ -0,0 +1,31 @@
//go:build integration
package avalanche
import (
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/trezor/blockbook/bchain/coins"
)
const defaultAvaxRpcURL = "http://localhost:8098/ext/bc/C/rpc"
func TestAvalancheErc20ContractBalancesIntegration(t *testing.T) {
coins.RunERC20BatchBalanceTest(t, coins.ERC20BatchCase{
Name: "avalanche",
RPCURL: defaultAvaxRpcURL,
// Token-rich address on Avalanche C-Chain (balanceOf works for any address).
Addr: common.HexToAddress("0x60aE616a2155Ee3d9A68541Ba4544862310933d4"),
Contracts: []common.Address{
common.HexToAddress("0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7"), // WAVAX
common.HexToAddress("0xA7D7079b0FEAD91F3e65f86E8915Cb59c1a4C664"), // USDC.e
common.HexToAddress("0xc7198437980c041c805A1EDcbA50c1Ce5db95118"), // USDT.e
common.HexToAddress("0xd586e7f844cea2f87f50152665bcbc2c279d8d70"), // DAI.e
common.HexToAddress("0x49D5c2BdFfac6Ce2BFdB6640F4F80f226bc10bAB"), // WETH.e
common.HexToAddress("0x60781C2586D68229fde47564546784ab3fACA982"), // PNG
},
BatchSize: 200,
SkipUnavailable: true,
})
}

View File

@@ -0,0 +1,28 @@
//go:build integration
package base_test
import (
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/trezor/blockbook/bchain/coins"
)
const defaultBaseRpcURL = "ws://localhost:8309"
func TestBaseErc20ContractBalancesIntegration(t *testing.T) {
coins.RunERC20BatchBalanceTest(t, coins.ERC20BatchCase{
Name: "base",
RPCURL: defaultBaseRpcURL,
Addr: common.HexToAddress("0x242E2d70d3AdC00a9eF23CeD6E88811fCefCA788"),
Contracts: []common.Address{
common.HexToAddress("0x4200000000000000000000000000000000000006"), // WETH
common.HexToAddress("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"), // USDC
common.HexToAddress("0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb"), // DAI
common.HexToAddress("0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22"), // cbETH
},
BatchSize: 200,
SkipUnavailable: true,
})
}

View File

@@ -0,0 +1,29 @@
//go:build integration
package bsc_test
import (
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/trezor/blockbook/bchain/coins"
)
const defaultBscRpcURL = "ws://localhost:8064"
func TestBNBSmartChainErc20ContractBalancesIntegration(t *testing.T) {
coins.RunERC20BatchBalanceTest(t, coins.ERC20BatchCase{
Name: "bsc",
RPCURL: defaultBscRpcURL,
Addr: common.HexToAddress("0x21d45650db732cE5dF77685d6021d7D5d1da807f"),
Contracts: []common.Address{
common.HexToAddress("0xBB4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c"), // WBNB
common.HexToAddress("0x55d398326f99059fF775485246999027B3197955"), // USDT
common.HexToAddress("0xe9e7CEA3Dedca5984780Bafc599bd69ADd087d56"), // BUSD
common.HexToAddress("0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d"), // USDC
common.HexToAddress("0x1AF3F329e8BE154074D8769D1FFa4eE058B1DBc3"), // DAI
},
BatchSize: 200,
SkipUnavailable: true,
})
}

View File

@@ -0,0 +1,133 @@
//go:build integration
package coins
import (
"context"
"errors"
"fmt"
"net"
"strings"
"testing"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/trezor/blockbook/bchain"
"github.com/trezor/blockbook/bchain/coins/eth"
)
const defaultBatchSize = 200
type ERC20BatchCase struct {
Name string
RPCURL string
Addr common.Address
Contracts []common.Address
BatchSize int
SkipUnavailable bool
}
func RunERC20BatchBalanceTest(t *testing.T, tc ERC20BatchCase) {
t.Helper()
if tc.BatchSize <= 0 {
tc.BatchSize = defaultBatchSize
}
rc, _, err := eth.OpenRPC(tc.RPCURL)
if err != nil {
handleRPCError(t, tc, fmt.Errorf("rpc dial error: %w", err))
return
}
t.Cleanup(func() { rc.Close() })
rpcClient := &eth.EthereumRPC{
RPC: rc,
Timeout: 15 * time.Second,
ChainConfig: &eth.Configuration{Erc20BatchSize: tc.BatchSize},
}
if err := verifyBatchBalances(rpcClient, tc.Addr, tc.Contracts); err != nil {
handleRPCError(t, tc, err)
return
}
chunkedContracts := expandContracts(tc.Contracts, tc.BatchSize+1)
if err := verifyBatchBalances(rpcClient, tc.Addr, chunkedContracts); err != nil {
handleRPCError(t, tc, err)
return
}
}
func handleRPCError(t *testing.T, tc ERC20BatchCase, err error) {
t.Helper()
if tc.SkipUnavailable && isRPCUnavailable(err) {
t.Skipf("WARN: %s RPC not available: %v", tc.Name, err)
return
}
t.Fatalf("%v", err)
}
func expandContracts(contracts []common.Address, minLen int) []common.Address {
if len(contracts) >= minLen {
return contracts
}
out := make([]common.Address, 0, minLen)
for len(out) < minLen {
out = append(out, contracts...)
}
if len(out) > minLen {
out = out[:minLen]
}
return out
}
func verifyBatchBalances(rpcClient *eth.EthereumRPC, addr common.Address, contracts []common.Address) error {
if len(contracts) == 0 {
return errors.New("no contracts to query")
}
contractDescs := make([]bchain.AddressDescriptor, len(contracts))
for i, c := range contracts {
contractDescs[i] = bchain.AddressDescriptor(c.Bytes())
}
addrDesc := bchain.AddressDescriptor(addr.Bytes())
balances, err := rpcClient.EthereumTypeGetErc20ContractBalances(addrDesc, contractDescs)
if err != nil {
return fmt.Errorf("batch balances error: %w", err)
}
if len(balances) != len(contractDescs) {
return fmt.Errorf("expected %d balances, got %d", len(contractDescs), len(balances))
}
for i, contractDesc := range contractDescs {
single, err := rpcClient.EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc)
if err != nil {
return fmt.Errorf("single balance error for %s: %w", contracts[i].Hex(), err)
}
if balances[i] == nil {
return fmt.Errorf("batch balance missing for %s", contracts[i].Hex())
}
if balances[i].Cmp(single) != 0 {
return fmt.Errorf("balance mismatch for %s: batch=%s single=%s", contracts[i].Hex(), balances[i].String(), single.String())
}
}
return nil
}
func isRPCUnavailable(err error) bool {
if err == nil {
return false
}
if errors.Is(err, context.DeadlineExceeded) {
return true
}
var netErr net.Error
if errors.As(err, &netErr) {
return true
}
msg := strings.ToLower(err.Error())
switch {
case strings.Contains(msg, "context deadline exceeded"),
strings.Contains(msg, "connection refused"),
strings.Contains(msg, "no such host"),
strings.Contains(msg, "i/o timeout"),
strings.Contains(msg, "timeout"):
return true
}
return false
}

View File

@@ -1,78 +1,29 @@
//go:build integration
package eth
package eth_test
import (
"testing"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/trezor/blockbook/bchain"
"github.com/trezor/blockbook/bchain/coins"
)
const defaultEthRpcURL = "http://naked:8545"
func verifyBatchBalances(t *testing.T, rpcClient *EthereumRPC, addr common.Address, contracts []common.Address) {
t.Helper()
contractDescs := make([]bchain.AddressDescriptor, len(contracts))
for i, c := range contracts {
contractDescs[i] = bchain.AddressDescriptor(c.Bytes())
}
addrDesc := bchain.AddressDescriptor(addr.Bytes())
balances, err := rpcClient.EthereumTypeGetErc20ContractBalances(addrDesc, contractDescs)
if err != nil {
t.Fatalf("batch balances error: %v", err)
}
if len(balances) != len(contractDescs) {
t.Fatalf("expected %d balances, got %d", len(contractDescs), len(balances))
}
for i, contractDesc := range contractDescs {
single, err := rpcClient.EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc)
if err != nil {
t.Fatalf("single balance error for %s: %v", contracts[i].Hex(), err)
}
if balances[i] == nil {
t.Fatalf("batch balance missing for %s", contracts[i].Hex())
}
if balances[i].Cmp(single) != 0 {
t.Fatalf("balance mismatch for %s: batch=%s single=%s", contracts[i].Hex(), balances[i].String(), single.String())
}
}
}
const defaultEthRpcURL = "http://localhost:8545"
func TestEthereumTypeGetErc20ContractBalancesIntegration(t *testing.T) {
rpcURL := defaultEthRpcURL
rc, _, err := OpenRPC(rpcURL)
if err != nil {
t.Skipf("skipping: cannot connect to RPC at %s: %v", rpcURL, err)
return
}
defer rc.Close()
// Use stable mainnet ERC20 contracts and a well-known EOA.
addr := common.HexToAddress("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
baseContracts := []common.Address{
common.HexToAddress("0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), // USDC
common.HexToAddress("0xdAC17F958D2ee523a2206206994597C13D831ec7"), // USDT
common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"), // WETH
}
rpcClient := &EthereumRPC{
RPC: rc,
Timeout: 15 * time.Second,
}
verifyBatchBalances(t, rpcClient, addr, baseContracts)
chunkedContracts := []common.Address{
common.HexToAddress("0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), // USDC
common.HexToAddress("0xdAC17F958D2ee523a2206206994597C13D831ec7"), // USDT
common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"), // WETH
common.HexToAddress("0x6B175474E89094C44Da98b954EedeAC495271d0F"), // DAI
}
rpcClientChunked := &EthereumRPC{
RPC: rc,
Timeout: 15 * time.Second,
ChainConfig: &Configuration{Erc20BatchSize: 2},
}
verifyBatchBalances(t, rpcClientChunked, addr, chunkedContracts)
coins.RunERC20BatchBalanceTest(t, coins.ERC20BatchCase{
Name: "ethereum",
RPCURL: defaultEthRpcURL,
// Token-rich EOA (CEX hot wallet) used as a stable address reference.
Addr: common.HexToAddress("0x28C6c06298d514Db089934071355E5743bf21d60"),
Contracts: []common.Address{
common.HexToAddress("0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), // USDC
common.HexToAddress("0xdAC17F958D2ee523a2206206994597C13D831ec7"), // USDT
common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"), // WETH
common.HexToAddress("0x6B175474E89094C44Da98b954EedeAC495271d0F"), // DAI
},
BatchSize: 200,
SkipUnavailable: false,
})
}

View File

@@ -0,0 +1,29 @@
//go:build integration
package optimism_test
import (
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/trezor/blockbook/bchain/coins"
)
const defaultOptimismRpcURL = "ws://localhost:8200"
func TestOptimismErc20ContractBalancesIntegration(t *testing.T) {
coins.RunERC20BatchBalanceTest(t, coins.ERC20BatchCase{
Name: "optimism",
RPCURL: defaultOptimismRpcURL,
Addr: common.HexToAddress("0xDF90C9B995a3b10A5b8570a47101e6c6a29eb945"),
Contracts: []common.Address{
common.HexToAddress("0x4200000000000000000000000000000000000006"), // WETH
common.HexToAddress("0x7F5c764cBc14f9669B88837ca1490cCa17c31607"), // USDC
common.HexToAddress("0x94b008aa00579c1307b0ef2c499ad98a8ce58e58"), // USDT
common.HexToAddress("0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"), // DAI
common.HexToAddress("0x4200000000000000000000000000000000000042"), // OP
},
BatchSize: 200,
SkipUnavailable: true,
})
}