Commit 53573e0e authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

op-service: Make Multicaller more generic (#9767)

* multicaller: Support generic calls, not just eth_call

* multicaller: Support generic calls, not just eth_call

* multicaller: Implement balance_call

* multicaller: Split out a generic rpc stub for testing RPC requests other than eth_call

Test balance call
parent 1dacba5a
......@@ -138,7 +138,7 @@ func (c *FaultDisputeGameContract) GetCredit(ctx context.Context, recipient comm
}
func (c *FaultDisputeGameContract) GetCredits(ctx context.Context, block batching.Block, recipients ...common.Address) ([]*big.Int, error) {
calls := make([]*batching.ContractCall, 0, len(recipients))
calls := make([]batching.Call, 0, len(recipients))
for _, recipient := range recipients {
calls = append(calls, c.contract.Call(methodCredit, recipient))
}
......
......@@ -86,7 +86,7 @@ func (f *DisputeGameFactoryContract) GetGamesAtOrAfter(ctx context.Context, bloc
if rangeEnd > batchSize {
rangeStart = rangeEnd - batchSize
}
calls := make([]*batching.ContractCall, 0, rangeEnd-rangeStart)
calls := make([]batching.Call, 0, rangeEnd-rangeStart)
for i := rangeEnd - 1; ; i-- {
calls = append(calls, f.contract.Call(methodGameAtIndex, new(big.Int).SetUint64(i)))
// Break once we've added the last call to avoid underflow when rangeStart == 0
......@@ -117,7 +117,7 @@ func (f *DisputeGameFactoryContract) GetAllGames(ctx context.Context, blockHash
return nil, err
}
calls := make([]*batching.ContractCall, count)
calls := make([]batching.Call, count)
for i := uint64(0); i < count; i++ {
calls[i] = f.contract.Call(methodGameAtIndex, new(big.Int).SetUint64(i))
}
......
......@@ -213,7 +213,7 @@ func (c *PreimageOracleContract) GetActivePreimages(ctx context.Context, blockHa
}
func (c *PreimageOracleContract) GetProposalMetadata(ctx context.Context, block batching.Block, idents ...keccakTypes.LargePreimageIdent) ([]keccakTypes.LargePreimageMetaData, error) {
var calls []*batching.ContractCall
var calls []batching.Call
for _, ident := range idents {
calls = append(calls, c.contract.Call(methodProposalMetadata, ident.Claimant, ident.UUID))
}
......
......@@ -98,7 +98,7 @@ func TestPreimageOracleContract_ChallengePeriod(t *testing.T) {
require.Equal(t, uint64(123), challengePeriod)
// Should cache responses
stubRpc.ClearResponses(methodChallengePeriod)
stubRpc.ClearResponses()
challengePeriod, err = oracle.ChallengePeriod(context.Background())
require.NoError(t, err)
require.Equal(t, uint64(123), challengePeriod)
......@@ -126,7 +126,7 @@ func TestPreimageOracleContract_MinBondSizeLPP(t *testing.T) {
require.Equal(t, big.NewInt(123), minBond)
// Should cache responses
stubRpc.ClearResponses(methodMinBondSizeLPP)
stubRpc.ClearResponses()
minBond, err = oracle.GetMinBondLPP(context.Background())
require.NoError(t, err)
require.Equal(t, big.NewInt(123), minBond)
......
......@@ -16,7 +16,7 @@ func ReadArray(ctx context.Context, caller *MultiCaller, block Block, countCall
return nil, fmt.Errorf("failed to load array length: %w", err)
}
count := result.GetBigInt(0).Uint64()
calls := make([]*ContractCall, count)
calls := make([]Call, count)
for i := uint64(0); i < count; i++ {
calls[i] = getCall(new(big.Int).SetUint64(i))
}
......
package batching
import (
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/rpc"
)
type BalanceCall struct {
addr common.Address
}
var _ Call = (*BalanceCall)(nil)
func NewBalanceCall(addr common.Address) *BalanceCall {
return &BalanceCall{addr}
}
func (b *BalanceCall) ToBatchElemCreator() (BatchElementCreator, error) {
return func(block Block) (any, rpc.BatchElem) {
out := new(hexutil.Big)
return out, rpc.BatchElem{
Method: "eth_getBalance",
Args: []interface{}{b.addr, block.value},
Result: &out,
}
}, nil
}
func (b *BalanceCall) HandleResult(result interface{}) (*CallResult, error) {
val, ok := result.(*hexutil.Big)
if !ok {
return nil, fmt.Errorf("response %v was not a *big.Int", result)
}
return &CallResult{out: []interface{}{(*big.Int)(val)}}, nil
}
package batching
import (
"fmt"
"math/big"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/rpc"
)
type ContractCall struct {
Abi *abi.ABI
Addr common.Address
Method string
Args []interface{}
From common.Address
}
func NewContractCall(abi *abi.ABI, addr common.Address, method string, args ...interface{}) *ContractCall {
return &ContractCall{
Abi: abi,
Addr: addr,
Method: method,
Args: args,
}
}
func (c *ContractCall) Pack() ([]byte, error) {
return c.Abi.Pack(c.Method, c.Args...)
}
func (c *ContractCall) ToCallArgs() (interface{}, error) {
data, err := c.Pack()
if err != nil {
return nil, fmt.Errorf("failed to pack arguments: %w", err)
}
arg := map[string]interface{}{
"from": c.From,
"to": &c.Addr,
"input": hexutil.Bytes(data),
}
return arg, nil
}
func (c *ContractCall) Unpack(hex hexutil.Bytes) (*CallResult, error) {
out, err := c.Abi.Unpack(c.Method, hex)
if err != nil {
return nil, fmt.Errorf("failed to unpack data: %w", err)
}
return &CallResult{out: out}, nil
}
type BatchElementCreator func(block Block) (any, rpc.BatchElem)
func (c *ContractCall) ToTxCandidate() (txmgr.TxCandidate, error) {
data, err := c.Pack()
if err != nil {
return txmgr.TxCandidate{}, fmt.Errorf("failed to pack arguments: %w", err)
}
return txmgr.TxCandidate{
TxData: data,
To: &c.Addr,
}, nil
type Call interface {
ToBatchElemCreator() (BatchElementCreator, error)
HandleResult(interface{}) (*CallResult, error)
}
type CallResult struct {
......
......@@ -4,103 +4,10 @@ import (
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/stretchr/testify/require"
)
func TestContractCall_ToCallArgs(t *testing.T) {
addr := common.Address{0xbd}
testAbi, err := bindings.ERC20MetaData.GetAbi()
require.NoError(t, err)
call := NewContractCall(testAbi, addr, "approve", common.Address{0xcc}, big.NewInt(1234444))
call.From = common.Address{0xab}
args, err := call.ToCallArgs()
require.NoError(t, err)
argMap, ok := args.(map[string]interface{})
require.True(t, ok)
require.Equal(t, argMap["from"], call.From)
require.Equal(t, argMap["to"], &addr)
expectedData, err := call.Pack()
require.NoError(t, err)
require.Equal(t, argMap["input"], hexutil.Bytes(expectedData))
require.NotContains(t, argMap, "value")
require.NotContains(t, argMap, "gas")
require.NotContains(t, argMap, "gasPrice")
}
func TestContractCall_ToTxCandidate(t *testing.T) {
addr := common.Address{0xbd}
testAbi, err := bindings.ERC20MetaData.GetAbi()
require.NoError(t, err)
call := NewContractCall(testAbi, addr, "approve", common.Address{0xcc}, big.NewInt(1234444))
candidate, err := call.ToTxCandidate()
require.NoError(t, err)
require.Equal(t, candidate.To, &addr)
expectedData, err := call.Pack()
require.NoError(t, err)
require.Equal(t, candidate.TxData, expectedData)
require.Nil(t, candidate.Value)
require.Zero(t, candidate.GasLimit)
}
func TestContractCall_Pack(t *testing.T) {
addr := common.Address{0xbd}
testAbi, err := bindings.ERC20MetaData.GetAbi()
require.NoError(t, err)
sender := common.Address{0xcc}
amount := big.NewInt(1234444)
call := NewContractCall(testAbi, addr, "approve", sender, amount)
actual, err := call.Pack()
require.NoError(t, err)
expected, err := testAbi.Pack("approve", sender, amount)
require.NoError(t, err)
require.Equal(t, actual, expected)
}
func TestContractCall_PackInvalid(t *testing.T) {
addr := common.Address{0xbd}
testAbi, err := bindings.ERC20MetaData.GetAbi()
require.NoError(t, err)
// Second arg should be a *big.Int so packing should fail
call := NewContractCall(testAbi, addr, "approve", common.Address{0xcc}, uint32(123))
_, err = call.Pack()
require.Error(t, err)
}
func TestContractCall_Unpack(t *testing.T) {
addr := common.Address{0xbd}
testAbi, err := bindings.ERC20MetaData.GetAbi()
require.NoError(t, err)
call := NewContractCall(testAbi, addr, "balanceOf", common.Address{0xcc})
outputs := testAbi.Methods["balanceOf"].Outputs
expected := big.NewInt(1234)
packed, err := outputs.Pack(expected)
require.NoError(t, err)
unpacked, err := call.Unpack(packed)
require.NoError(t, err)
require.Equal(t, unpacked.GetBigInt(0), expected)
}
func TestContractCall_UnpackInvalid(t *testing.T) {
addr := common.Address{0xbd}
testAbi, err := bindings.ERC20MetaData.GetAbi()
require.NoError(t, err)
call := NewContractCall(testAbi, addr, "balanceOf", common.Address{0xcc})
// Input data is the wrong format and won't unpack successfully
inputPacked, err := call.Pack()
require.NoError(t, err)
_, err = call.Unpack(inputPacked)
require.Error(t, err)
}
func TestCallResult_GetValues(t *testing.T) {
tests := []struct {
name string
......
package batching
import (
"fmt"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/rpc"
)
type ContractCall struct {
Abi *abi.ABI
Addr common.Address
Method string
Args []interface{}
From common.Address
}
func NewContractCall(abi *abi.ABI, addr common.Address, method string, args ...interface{}) *ContractCall {
return &ContractCall{
Abi: abi,
Addr: addr,
Method: method,
Args: args,
}
}
func (c *ContractCall) Pack() ([]byte, error) {
return c.Abi.Pack(c.Method, c.Args...)
}
func (c *ContractCall) CallMethod() string {
return "eth_call"
}
func (c *ContractCall) ToBatchElemCreator() (BatchElementCreator, error) {
args, err := c.ToCallArgs()
if err != nil {
return nil, err
}
f := func(block Block) (any, rpc.BatchElem) {
out := new(hexutil.Bytes)
return out, rpc.BatchElem{
Method: "eth_call",
Args: []interface{}{args, block.value},
Result: &out,
}
}
return f, nil
}
func (c *ContractCall) ToCallArgs() (interface{}, error) {
data, err := c.Pack()
if err != nil {
return nil, fmt.Errorf("failed to pack arguments: %w", err)
}
arg := map[string]interface{}{
"from": c.From,
"to": &c.Addr,
"input": hexutil.Bytes(data),
}
return arg, nil
}
func (c *ContractCall) CreateResult() interface{} {
return new(hexutil.Bytes)
}
func (c *ContractCall) HandleResult(result interface{}) (*CallResult, error) {
out, err := c.Unpack(*result.(*hexutil.Bytes))
return out, err
}
func (c *ContractCall) Unpack(hex hexutil.Bytes) (*CallResult, error) {
out, err := c.Abi.Unpack(c.Method, hex)
if err != nil {
return nil, fmt.Errorf("failed to unpack data: %w", err)
}
return &CallResult{out: out}, nil
}
func (c *ContractCall) ToTxCandidate() (txmgr.TxCandidate, error) {
data, err := c.Pack()
if err != nil {
return txmgr.TxCandidate{}, fmt.Errorf("failed to pack arguments: %w", err)
}
return txmgr.TxCandidate{
TxData: data,
To: &c.Addr,
}, nil
}
package batching
import (
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/stretchr/testify/require"
)
func TestContractCall_ToCallArgs(t *testing.T) {
addr := common.Address{0xbd}
testAbi, err := bindings.ERC20MetaData.GetAbi()
require.NoError(t, err)
call := NewContractCall(testAbi, addr, "approve", common.Address{0xcc}, big.NewInt(1234444))
call.From = common.Address{0xab}
args, err := call.ToCallArgs()
require.NoError(t, err)
argMap, ok := args.(map[string]interface{})
require.True(t, ok)
require.Equal(t, argMap["from"], call.From)
require.Equal(t, argMap["to"], &addr)
expectedData, err := call.Pack()
require.NoError(t, err)
require.Equal(t, argMap["input"], hexutil.Bytes(expectedData))
require.NotContains(t, argMap, "value")
require.NotContains(t, argMap, "gas")
require.NotContains(t, argMap, "gasPrice")
}
func TestContractCall_ToTxCandidate(t *testing.T) {
addr := common.Address{0xbd}
testAbi, err := bindings.ERC20MetaData.GetAbi()
require.NoError(t, err)
call := NewContractCall(testAbi, addr, "approve", common.Address{0xcc}, big.NewInt(1234444))
candidate, err := call.ToTxCandidate()
require.NoError(t, err)
require.Equal(t, candidate.To, &addr)
expectedData, err := call.Pack()
require.NoError(t, err)
require.Equal(t, candidate.TxData, expectedData)
require.Nil(t, candidate.Value)
require.Zero(t, candidate.GasLimit)
}
func TestContractCall_Pack(t *testing.T) {
addr := common.Address{0xbd}
testAbi, err := bindings.ERC20MetaData.GetAbi()
require.NoError(t, err)
sender := common.Address{0xcc}
amount := big.NewInt(1234444)
call := NewContractCall(testAbi, addr, "approve", sender, amount)
actual, err := call.Pack()
require.NoError(t, err)
expected, err := testAbi.Pack("approve", sender, amount)
require.NoError(t, err)
require.Equal(t, actual, expected)
}
func TestContractCall_PackInvalid(t *testing.T) {
addr := common.Address{0xbd}
testAbi, err := bindings.ERC20MetaData.GetAbi()
require.NoError(t, err)
// Second arg should be a *big.Int so packing should fail
call := NewContractCall(testAbi, addr, "approve", common.Address{0xcc}, uint32(123))
_, err = call.Pack()
require.Error(t, err)
}
func TestContractCall_Unpack(t *testing.T) {
addr := common.Address{0xbd}
testAbi, err := bindings.ERC20MetaData.GetAbi()
require.NoError(t, err)
call := NewContractCall(testAbi, addr, "balanceOf", common.Address{0xcc})
outputs := testAbi.Methods["balanceOf"].Outputs
expected := big.NewInt(1234)
packed, err := outputs.Pack(expected)
require.NoError(t, err)
unpacked, err := call.Unpack(packed)
require.NoError(t, err)
require.Equal(t, unpacked.GetBigInt(0), expected)
}
func TestContractCall_UnpackInvalid(t *testing.T) {
addr := common.Address{0xbd}
testAbi, err := bindings.ERC20MetaData.GetAbi()
require.NoError(t, err)
call := NewContractCall(testAbi, addr, "balanceOf", common.Address{0xcc})
// Input data is the wrong format and won't unpack successfully
inputPacked, err := call.Pack()
require.NoError(t, err)
_, err = call.Unpack(inputPacked)
require.Error(t, err)
}
......@@ -6,7 +6,6 @@ import (
"io"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/rpc"
)
......@@ -33,7 +32,7 @@ func (m *MultiCaller) BatchSize() int {
return m.batchSize
}
func (m *MultiCaller) SingleCall(ctx context.Context, block Block, call *ContractCall) (*CallResult, error) {
func (m *MultiCaller) SingleCall(ctx context.Context, block Block, call Call) (*CallResult, error) {
results, err := m.Call(ctx, block, call)
if err != nil {
return nil, err
......@@ -41,24 +40,19 @@ func (m *MultiCaller) SingleCall(ctx context.Context, block Block, call *Contrac
return results[0], nil
}
func (m *MultiCaller) Call(ctx context.Context, block Block, calls ...*ContractCall) ([]*CallResult, error) {
keys := make([]interface{}, len(calls))
func (m *MultiCaller) Call(ctx context.Context, block Block, calls ...Call) ([]*CallResult, error) {
keys := make([]BatchElementCreator, len(calls))
for i := 0; i < len(calls); i++ {
args, err := calls[i].ToCallArgs()
creator, err := calls[i].ToBatchElemCreator()
if err != nil {
return nil, err
}
keys[i] = args
keys[i] = creator
}
fetcher := NewIterativeBatchCall[interface{}, *hexutil.Bytes](
fetcher := NewIterativeBatchCall[BatchElementCreator, any](
keys,
func(args interface{}) (*hexutil.Bytes, rpc.BatchElem) {
out := new(hexutil.Bytes)
return out, rpc.BatchElem{
Method: "eth_call",
Args: []interface{}{args, block.value},
Result: &out,
}
func(key BatchElementCreator) (any, rpc.BatchElem) {
return key(block)
},
m.rpc.BatchCallContext,
m.rpc.CallContext,
......@@ -78,7 +72,7 @@ func (m *MultiCaller) Call(ctx context.Context, block Block, calls ...*ContractC
callResults := make([]*CallResult, len(results))
for i, result := range results {
call := calls[i]
out, err := call.Unpack(*result)
out, err := call.HandleResult(result)
if err != nil {
return nil, fmt.Errorf("failed to unpack result: %w", err)
}
......
package test
import (
"context"
"encoding/json"
"errors"
"fmt"
......@@ -12,13 +11,13 @@ import (
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/rpc"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/exp/slices"
)
type expectedCall struct {
abiMethod abi.Method
to common.Address
block batching.Block
args []interface{}
......@@ -26,24 +25,75 @@ type expectedCall struct {
outputs []interface{}
}
func (e *expectedCall) String() string {
return fmt.Sprintf("{to: %v, block: %v, args: %v, outputs: %v}", e.to, e.block, e.args, e.outputs)
func (c *expectedCall) Matches(rpcMethod string, args ...interface{}) error {
if rpcMethod != "eth_call" {
return fmt.Errorf("expected rpcMethod eth_call but was %v", rpcMethod)
}
if len(args) != 2 {
return fmt.Errorf("expected arg count 2 but was %v", len(args))
}
callOpts, ok := args[0].(map[string]any)
if !ok {
return fmt.Errorf("arg 0 is not a map[string]any")
}
actualBlockRef := args[1]
to, ok := callOpts["to"].(*common.Address)
if !ok {
return errors.New("to is not an address")
}
if to == nil {
return errors.New("to is nil")
}
if *to != c.to {
return fmt.Errorf("expected to %v but was %v", c.to, *to)
}
data, ok := callOpts["input"].(hexutil.Bytes)
if !ok {
return errors.New("input is not hexutil.Bytes")
}
if len(data) < 4 {
return fmt.Errorf("expected input to have at least 4 bytes but was %v", len(data))
}
if !slices.Equal(c.abiMethod.ID, data[:4]) {
return fmt.Errorf("expected abi method ID %x but was %x", c.abiMethod.ID, data[:4])
}
if !slices.Equal(c.packedArgs, data[4:]) {
return fmt.Errorf("expected args %x but was %x", c.packedArgs, data[4:])
}
if !assert.ObjectsAreEqualValues(c.block.ArgValue(), actualBlockRef) {
return fmt.Errorf("expected block ref %v but was %v", c.block.ArgValue(), actualBlockRef)
}
return nil
}
func (c *expectedCall) Execute(t *testing.T, out interface{}) {
output, err := c.abiMethod.Outputs.Pack(c.outputs...)
require.NoErrorf(t, err, "Invalid outputs for method %v: %v", c.abiMethod.Name, c.outputs)
// I admit I do not understand Go reflection.
// So leverage json.Unmarshal to set the out value correctly.
j, err := json.Marshal(hexutil.Bytes(output))
require.NoError(t, err)
require.NoError(t, json.Unmarshal(j, out))
}
func (c *expectedCall) String() string {
return fmt.Sprintf("{to: %v, block: %v, args: %v, outputs: %v}", c.to, c.block, c.args, c.outputs)
}
type AbiBasedRpc struct {
t *testing.T
RpcStub
abis map[common.Address]*abi.ABI
expectedCalls map[string][]*expectedCall
}
func NewAbiBasedRpc(t *testing.T, to common.Address, contractAbi *abi.ABI) *AbiBasedRpc {
abis := make(map[common.Address]*abi.ABI)
abis[to] = contractAbi
return &AbiBasedRpc{
t: t,
abis: abis,
expectedCalls: make(map[string][]*expectedCall),
RpcStub: RpcStub{
t: t,
},
abis: abis,
}
}
......@@ -68,7 +118,8 @@ func (l *AbiBasedRpc) SetResponse(to common.Address, method string, block batchi
require.Truef(l.t, ok, "No method: %v", method)
packedArgs, err := abiMethod.Inputs.Pack(expected...)
require.NoErrorf(l.t, err, "Invalid expected arguments for method %v: %v", method, expected)
l.expectedCalls[method] = append(l.expectedCalls[method], &expectedCall{
l.AddExpectedCall(&expectedCall{
abiMethod: abiMethod,
to: to,
block: block,
args: expected,
......@@ -77,71 +128,11 @@ func (l *AbiBasedRpc) SetResponse(to common.Address, method string, block batchi
})
}
func (l *AbiBasedRpc) ClearResponses(method string) {
delete(l.expectedCalls, method)
}
func (l *AbiBasedRpc) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error {
var errs []error
for _, elem := range b {
elem.Error = l.CallContext(ctx, elem.Result, elem.Method, elem.Args...)
errs = append(errs, elem.Error)
}
return errors.Join(errs...)
}
func (l *AbiBasedRpc) VerifyTxCandidate(candidate txmgr.TxCandidate) {
require.NotNil(l.t, candidate.To)
l.findExpectedCall(*candidate.To, candidate.TxData, batching.BlockLatest.ArgValue())
}
func (l *AbiBasedRpc) CallContext(_ context.Context, out interface{}, method string, args ...interface{}) error {
require.Equal(l.t, "eth_call", method)
require.Len(l.t, args, 2)
actualBlockRef := args[1]
callOpts, ok := args[0].(map[string]any)
require.True(l.t, ok)
to, ok := callOpts["to"].(*common.Address)
require.True(l.t, ok)
require.NotNil(l.t, to)
data, ok := callOpts["input"].(hexutil.Bytes)
require.True(l.t, ok)
call, abiMethod := l.findExpectedCall(*to, data, actualBlockRef)
output, err := abiMethod.Outputs.Pack(call.outputs...)
require.NoErrorf(l.t, err, "Invalid outputs for method %v: %v", abiMethod.Name, call.outputs)
// I admit I do not understand Go reflection.
// So leverage json.Unmarshal to set the out value correctly.
j, err := json.Marshal(hexutil.Bytes(output))
require.NoError(l.t, err)
require.NoError(l.t, json.Unmarshal(j, out))
return nil
}
func (l *AbiBasedRpc) findExpectedCall(to common.Address, data []byte, actualBlockRef interface{}) (*expectedCall, *abi.Method) {
abiMethod, err := l.abi(to).MethodById(data[0:4])
require.NoError(l.t, err)
argData := data[4:]
args, err := abiMethod.Inputs.Unpack(argData)
require.NoError(l.t, err)
require.Len(l.t, args, len(abiMethod.Inputs))
expectedCalls, ok := l.expectedCalls[abiMethod.Name]
require.Truef(l.t, ok, "Unexpected call to %v", abiMethod.Name)
var call *expectedCall
for _, candidate := range expectedCalls {
if to == candidate.to &&
slices.Equal(candidate.packedArgs, argData) &&
assert.ObjectsAreEqualValues(candidate.block.ArgValue(), actualBlockRef) {
call = candidate
break
}
}
require.NotNilf(l.t, call, "No expected calls to %v at block %v with to: %v, arguments: %v\nExpected calls: %v",
to, abiMethod.Name, actualBlockRef, args, expectedCalls)
return call, abiMethod
l.findExpectedCall("eth_call", map[string]any{
"to": candidate.To,
"input": hexutil.Bytes(candidate.TxData),
"value": candidate.Value,
}, batching.BlockLatest.ArgValue())
}
package test
import (
"context"
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-service/sources/batching"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
// Note: These tests are in the test subpackage to avoid dependency cycles since they need to use the stubs
func TestGetBalance(t *testing.T) {
addr := common.Address{0xab, 0xcd}
expectedBalance := big.NewInt(248924)
stub := NewRpcStub(t)
stub.AddExpectedCall(NewGetBalanceCall(addr, batching.BlockLatest, expectedBalance))
caller := batching.NewMultiCaller(stub, batching.DefaultBatchSize)
result, err := caller.SingleCall(context.Background(), batching.BlockLatest, batching.NewBalanceCall(addr))
require.NoError(t, err)
require.Equal(t, expectedBalance, result.GetBigInt(0))
}
package test
import (
"context"
"encoding/json"
"errors"
"fmt"
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-service/sources/batching"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/rpc"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type ExpectedRpcCall interface {
fmt.Stringer
Matches(rpcMethod string, args ...interface{}) error
Execute(t *testing.T, out interface{})
}
type RpcStub struct {
t *testing.T
expectedCalls []ExpectedRpcCall
}
func NewRpcStub(t *testing.T) *RpcStub {
return &RpcStub{t: t}
}
func (r *RpcStub) ClearResponses() {
r.expectedCalls = nil
}
func (r *RpcStub) AddExpectedCall(call ExpectedRpcCall) {
r.expectedCalls = append(r.expectedCalls, call)
}
func (r *RpcStub) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error {
var errs []error
for _, elem := range b {
elem.Error = r.CallContext(ctx, elem.Result, elem.Method, elem.Args...)
errs = append(errs, elem.Error)
}
return errors.Join(errs...)
}
func (r *RpcStub) CallContext(_ context.Context, out interface{}, method string, args ...interface{}) error {
call := r.findExpectedCall(method, args...)
call.Execute(r.t, out)
return nil
}
func (r *RpcStub) findExpectedCall(rpcMethod string, args ...interface{}) ExpectedRpcCall {
var matchResults string
for _, call := range r.expectedCalls {
if err := call.Matches(rpcMethod, args...); err == nil {
return call
} else {
matchResults += fmt.Sprintf("%v: %v", call, err)
}
}
require.Failf(r.t, "No matching expected calls.", matchResults)
return nil
}
type GenericExpectedCall struct {
method string
args []interface{}
result interface{}
}
func NewGetBalanceCall(addr common.Address, block batching.Block, balance *big.Int) ExpectedRpcCall {
return &GenericExpectedCall{
method: "eth_getBalance",
args: []interface{}{addr, block.ArgValue()},
result: (*hexutil.Big)(balance),
}
}
func (c *GenericExpectedCall) Matches(rpcMethod string, args ...interface{}) error {
if rpcMethod != c.method {
return fmt.Errorf("expected method %v but was %v", c.method, rpcMethod)
}
if !assert.ObjectsAreEqualValues(c.args, args) {
return fmt.Errorf("expected args %v but was %v", c.args, args)
}
return nil
}
func (c *GenericExpectedCall) Execute(t *testing.T, out interface{}) {
// I admit I do not understand Go reflection.
// So leverage json.Unmarshal to set the out value correctly.
j, err := json.Marshal(c.result)
require.NoError(t, err)
require.NoError(t, json.Unmarshal(j, out))
}
func (c *GenericExpectedCall) String() string {
return fmt.Sprintf("%v(%v)->%v", c.method, c.args, c.result)
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment