eth_call batch it tests cleanup

This commit is contained in:
pragmaxim
2026-01-14 09:15:35 +01:00
parent 660685ea98
commit e94af5cf50
8 changed files with 131 additions and 79 deletions

View File

@@ -6,13 +6,14 @@ import (
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/trezor/blockbook/bchain/coins"
"github.com/trezor/blockbook/bchain"
"github.com/trezor/blockbook/bchain/coins/eth"
)
func TestAvalancheErc20ContractBalancesIntegration(t *testing.T) {
coins.RunERC20BatchBalanceTest(t, coins.ERC20BatchCase{
bchain.RunERC20BatchBalanceTest(t, bchain.ERC20BatchCase{
Name: "avalanche",
RPCURL: coins.RPCURLFromConfig(t, "avalanche"),
RPCURL: bchain.RPCURLFromConfig(t, "avalanche"),
// Token-rich address on Avalanche C-Chain (balanceOf works for any address).
Addr: common.HexToAddress("0x60aE616a2155Ee3d9A68541Ba4544862310933d4"),
Contracts: []common.Address{
@@ -25,5 +26,6 @@ func TestAvalancheErc20ContractBalancesIntegration(t *testing.T) {
},
BatchSize: 200,
SkipUnavailable: true,
NewClient: eth.NewERC20BatchIntegrationClient,
})
}

View File

@@ -6,13 +6,14 @@ import (
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/trezor/blockbook/bchain/coins"
"github.com/trezor/blockbook/bchain"
"github.com/trezor/blockbook/bchain/coins/eth"
)
func TestBaseErc20ContractBalancesIntegration(t *testing.T) {
coins.RunERC20BatchBalanceTest(t, coins.ERC20BatchCase{
bchain.RunERC20BatchBalanceTest(t, bchain.ERC20BatchCase{
Name: "base",
RPCURL: coins.RPCURLFromConfig(t, "base"),
RPCURL: bchain.RPCURLFromConfig(t, "base"),
Addr: common.HexToAddress("0x242E2d70d3AdC00a9eF23CeD6E88811fCefCA788"),
Contracts: []common.Address{
common.HexToAddress("0x4200000000000000000000000000000000000006"), // WETH
@@ -22,5 +23,6 @@ func TestBaseErc20ContractBalancesIntegration(t *testing.T) {
},
BatchSize: 200,
SkipUnavailable: true,
NewClient: eth.NewERC20BatchIntegrationClient,
})
}

View File

@@ -6,13 +6,14 @@ import (
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/trezor/blockbook/bchain/coins"
"github.com/trezor/blockbook/bchain"
"github.com/trezor/blockbook/bchain/coins/eth"
)
func TestBNBSmartChainErc20ContractBalancesIntegration(t *testing.T) {
coins.RunERC20BatchBalanceTest(t, coins.ERC20BatchCase{
bchain.RunERC20BatchBalanceTest(t, bchain.ERC20BatchCase{
Name: "bsc",
RPCURL: coins.RPCURLFromConfig(t, "bsc"),
RPCURL: bchain.RPCURLFromConfig(t, "bsc"),
Addr: common.HexToAddress("0x21d45650db732cE5dF77685d6021d7D5d1da807f"),
Contracts: []common.Address{
common.HexToAddress("0xBB4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c"), // WBNB
@@ -23,5 +24,6 @@ func TestBNBSmartChainErc20ContractBalancesIntegration(t *testing.T) {
},
BatchSize: 200,
SkipUnavailable: true,
NewClient: eth.NewERC20BatchIntegrationClient,
})
}

View File

@@ -6,13 +6,14 @@ import (
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/trezor/blockbook/bchain/coins"
"github.com/trezor/blockbook/bchain"
"github.com/trezor/blockbook/bchain/coins/eth"
)
func TestEthereumTypeGetErc20ContractBalancesIntegration(t *testing.T) {
coins.RunERC20BatchBalanceTest(t, coins.ERC20BatchCase{
bchain.RunERC20BatchBalanceTest(t, bchain.ERC20BatchCase{
Name: "ethereum",
RPCURL: coins.RPCURLFromConfig(t, "ethereum"),
RPCURL: bchain.RPCURLFromConfig(t, "ethereum"),
// Token-rich EOA (CEX hot wallet) used as a stable address reference.
Addr: common.HexToAddress("0x28C6c06298d514Db089934071355E5743bf21d60"),
Contracts: []common.Address{
@@ -23,5 +24,6 @@ func TestEthereumTypeGetErc20ContractBalancesIntegration(t *testing.T) {
},
BatchSize: 200,
SkipUnavailable: false,
NewClient: eth.NewERC20BatchIntegrationClient,
})
}

View File

@@ -0,0 +1,24 @@
//go:build integration
package eth
import (
"time"
"github.com/trezor/blockbook/bchain"
)
// NewERC20BatchIntegrationClient builds an ERC20-capable RPC client for integration tests.
// EVM chains share ERC20 balanceOf semantics (eth_call) and coin wrappers embed EthereumRPC.
func NewERC20BatchIntegrationClient(rpcURL string, batchSize int) (bchain.ERC20BatchClient, func(), error) {
rc, _, err := OpenRPC(rpcURL)
if err != nil {
return nil, nil, err
}
client := &EthereumRPC{
RPC: rc,
Timeout: 15 * time.Second,
ChainConfig: &Configuration{Erc20BatchSize: batchSize},
}
return client, func() { rc.Close() }, nil
}

View File

@@ -6,13 +6,14 @@ import (
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/trezor/blockbook/bchain/coins"
"github.com/trezor/blockbook/bchain"
"github.com/trezor/blockbook/bchain/coins/eth"
)
func TestOptimismErc20ContractBalancesIntegration(t *testing.T) {
coins.RunERC20BatchBalanceTest(t, coins.ERC20BatchCase{
bchain.RunERC20BatchBalanceTest(t, bchain.ERC20BatchCase{
Name: "optimism",
RPCURL: coins.RPCURLFromConfig(t, "optimism"),
RPCURL: bchain.RPCURLFromConfig(t, "optimism"),
Addr: common.HexToAddress("0xDF90C9B995a3b10A5b8570a47101e6c6a29eb945"),
Contracts: []common.Address{
common.HexToAddress("0x4200000000000000000000000000000000000006"), // WETH
@@ -23,5 +24,6 @@ func TestOptimismErc20ContractBalancesIntegration(t *testing.T) {
},
BatchSize: 200,
SkipUnavailable: true,
NewClient: eth.NewERC20BatchIntegrationClient,
})
}

View File

@@ -1,24 +1,17 @@
//go:build integration
package coins
package bchain
import (
"bytes"
"context"
"errors"
"fmt"
"math/big"
"net"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/trezor/blockbook/bchain"
"github.com/trezor/blockbook/bchain/coins/eth"
buildcfg "github.com/trezor/blockbook/build/tools"
)
const defaultBatchSize = 200
@@ -30,24 +23,25 @@ type ERC20BatchCase struct {
Contracts []common.Address
BatchSize int
SkipUnavailable bool
NewClient ERC20BatchClientFactory
}
// RunERC20BatchBalanceTest validates batch balanceOf results against single calls.
func RunERC20BatchBalanceTest(t *testing.T, tc ERC20BatchCase) {
t.Helper()
if tc.BatchSize <= 0 {
tc.BatchSize = defaultBatchSize
}
rc, _, err := eth.OpenRPC(tc.RPCURL)
if tc.NewClient == nil {
t.Fatalf("NewClient is required for ERC20 batch integration test")
}
rpcClient, closeFn, err := tc.NewClient(tc.RPCURL, tc.BatchSize)
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 closeFn != nil {
t.Cleanup(closeFn)
}
if err := verifyBatchBalances(rpcClient, tc.Addr, tc.Contracts); err != nil {
handleRPCError(t, tc, err)
@@ -60,50 +54,6 @@ func RunERC20BatchBalanceTest(t *testing.T, tc ERC20BatchCase) {
}
}
func RPCURLFromConfig(t *testing.T, coinAlias string) string {
t.Helper()
configsDir, err := repoConfigsDir()
if err != nil {
t.Fatalf("integration config path error: %v", err)
}
cfg, err := buildcfg.LoadConfig(configsDir, coinAlias)
if err != nil {
t.Fatalf("load config for %s: %v", coinAlias, err)
}
templ := cfg.ParseTemplate()
var out bytes.Buffer
if err := templ.ExecuteTemplate(&out, "IPC.RPCURLTemplate", cfg); err != nil {
t.Fatalf("render rpc_url_template for %s: %v", coinAlias, err)
}
rpcURL := strings.TrimSpace(out.String())
if rpcURL == "" {
t.Fatalf("empty rpc url from config for %s", coinAlias)
}
return rpcURL
}
func repoConfigsDir() (string, error) {
_, file, _, ok := runtime.Caller(0)
if !ok {
return "", errors.New("unable to resolve caller path")
}
dir := filepath.Dir(file)
for i := 0; i < 6; i++ {
configsDir := filepath.Join(dir, "configs")
if _, err := os.Stat(filepath.Join(configsDir, "coins")); err == nil {
return configsDir, nil
} else if !os.IsNotExist(err) {
return "", err
}
parent := filepath.Dir(dir)
if parent == dir {
break
}
dir = parent
}
return "", errors.New("configs/coins not found from caller path")
}
func handleRPCError(t *testing.T, tc ERC20BatchCase, err error) {
t.Helper()
if tc.SkipUnavailable && isRPCUnavailable(err) {
@@ -127,15 +77,22 @@ func expandContracts(contracts []common.Address, minLen int) []common.Address {
return out
}
func verifyBatchBalances(rpcClient *eth.EthereumRPC, addr common.Address, contracts []common.Address) error {
type ERC20BatchClient interface {
EthereumTypeGetErc20ContractBalances(addrDesc AddressDescriptor, contractDescs []AddressDescriptor) ([]*big.Int, error)
EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc AddressDescriptor) (*big.Int, error)
}
type ERC20BatchClientFactory func(rpcURL string, batchSize int) (ERC20BatchClient, func(), error)
func verifyBatchBalances(rpcClient ERC20BatchClient, addr common.Address, contracts []common.Address) error {
if len(contracts) == 0 {
return errors.New("no contracts to query")
}
contractDescs := make([]bchain.AddressDescriptor, len(contracts))
contractDescs := make([]AddressDescriptor, len(contracts))
for i, c := range contracts {
contractDescs[i] = bchain.AddressDescriptor(c.Bytes())
contractDescs[i] = AddressDescriptor(c.Bytes())
}
addrDesc := bchain.AddressDescriptor(addr.Bytes())
addrDesc := AddressDescriptor(addr.Bytes())
balances, err := rpcClient.EthereumTypeGetErc20ContractBalances(addrDesc, contractDescs)
if err != nil {
return fmt.Errorf("batch balances error: %w", err)

View File

@@ -0,0 +1,61 @@
//go:build integration
package bchain
import (
"bytes"
"errors"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
buildcfg "github.com/trezor/blockbook/build/tools"
)
// RPCURLFromConfig renders ipc.rpc_url_template from the coin config for integration tests.
func RPCURLFromConfig(t *testing.T, coinAlias string) string {
t.Helper()
configsDir, err := repoConfigsDir()
if err != nil {
t.Fatalf("integration config path error: %v", err)
}
cfg, err := buildcfg.LoadConfig(configsDir, coinAlias)
if err != nil {
t.Fatalf("load config for %s: %v", coinAlias, err)
}
templ := cfg.ParseTemplate()
var out bytes.Buffer
if err := templ.ExecuteTemplate(&out, "IPC.RPCURLTemplate", cfg); err != nil {
t.Fatalf("render rpc_url_template for %s: %v", coinAlias, err)
}
rpcURL := strings.TrimSpace(out.String())
if rpcURL == "" {
t.Fatalf("empty rpc url from config for %s", coinAlias)
}
return rpcURL
}
func repoConfigsDir() (string, error) {
_, file, _, ok := runtime.Caller(0)
if !ok {
return "", errors.New("unable to resolve caller path")
}
dir := filepath.Dir(file)
// search the config directory in the parent folders so it is agnostic to the caller location
for i := 0; i < 3; i++ {
configsDir := filepath.Join(dir, "configs")
if _, err := os.Stat(filepath.Join(configsDir, "coins")); err == nil {
return configsDir, nil
} else if !os.IsNotExist(err) {
return "", err
}
parent := filepath.Dir(dir)
if parent == dir {
break
}
dir = parent
}
return "", errors.New("configs/coins not found from caller path")
}