mirror of
https://github.com/trezor/blockbook.git
synced 2026-02-20 00:51:39 +01:00
eth_call batch it tests cleanup
This commit is contained in:
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
24
bchain/coins/eth/erc20_batch_integration_client.go
Normal file
24
bchain/coins/eth/erc20_batch_integration_client.go
Normal 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
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 := ð.EthereumRPC{
|
||||
RPC: rc,
|
||||
Timeout: 15 * time.Second,
|
||||
ChainConfig: ð.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)
|
||||
61
bchain/integration_helpers.go
Normal file
61
bchain/integration_helpers.go
Normal 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")
|
||||
}
|
||||
Reference in New Issue
Block a user