Skip to content

Commit b749edc

Browse files
committed
feat: wip
1 parent e09b3a1 commit b749edc

File tree

3 files changed

+216
-0
lines changed

3 files changed

+216
-0
lines changed

etherman/default_eth_client.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package etherman
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"math/big"
7+
8+
aggkitcommon "github.com/agglayer/aggkit/common"
9+
commontypes "github.com/agglayer/aggkit/common/types"
10+
"github.com/agglayer/aggkit/log"
11+
aggkittypes "github.com/agglayer/aggkit/types"
12+
"github.com/ethereum/go-ethereum/ethclient"
13+
"github.com/ethereum/go-ethereum/rpc"
14+
)
15+
16+
var _ aggkittypes.EthClienter = (*DefaultEthClient)(nil)
17+
18+
type DefaultEthClient struct {
19+
aggkittypes.EthereumClienter
20+
aggkittypes.RPCClienter
21+
22+
// If true, the block Hash is getted from JSON RPC
23+
// if false, the block Hash is getted from go-ethereum RLP hashing of header
24+
HashFromJSON bool
25+
}
26+
27+
// DialWithRetry attempts to connect to an Ethereum client with retries and exponential backoff.
28+
// It returns an EthClienter on success or an error if all attempts fail.
29+
func DialWithRetry(ctx context.Context, url string, retryHandler commontypes.RetryHandler) (aggkittypes.EthClienter, error) {
30+
return aggkitcommon.Execute(retryHandler, ctx, log.Infof, fmt.Sprintf("dial %s rpc", url),
31+
func() (aggkittypes.EthClienter, error) {
32+
client, err := ethclient.Dial(url)
33+
if err != nil {
34+
return nil, err
35+
}
36+
return NewDefaultEthClient(client, client.Client()), nil
37+
})
38+
}
39+
40+
func NewDefaultEthClient(client aggkittypes.EthereumClienter, rpcClient aggkittypes.RPCClienter) *DefaultEthClient {
41+
if rpcClient == nil {
42+
rpcClient = &NoopRPCClient{}
43+
}
44+
return &DefaultEthClient{
45+
EthereumClienter: client,
46+
RPCClienter: rpcClient,
47+
}
48+
}
49+
50+
func (c *DefaultEthClient) CustomBlockNumber(ctx context.Context, number aggkittypes.BlockName) (uint64, error) {
51+
ethHeader, err := c.EthereumClienter.HeaderByNumber(ctx, number.ToBigInt())
52+
if err != nil {
53+
return 0, err
54+
}
55+
return ethHeader.Number.Uint64(), nil
56+
}
57+
58+
func (c *DefaultEthClient) CustomHeaderByNumber(ctx context.Context, number *aggkittypes.BlockNumberFinality) (*aggkittypes.BlockHeader, error) {
59+
if number == nil {
60+
number = &aggkittypes.LatestBlock
61+
}
62+
// The number can have an offset, so maybe we need to resolve the blockName, apply offset to require the header
63+
numberBigInt, err := c.resolveBlockNumber(ctx, number)
64+
if err != nil {
65+
return nil, err
66+
}
67+
68+
if c.HashFromJSON {
69+
rpcGetBlockByNumber, err := c.rpcGetBlockByNumber(ctx, numberBigInt)
70+
if err != nil {
71+
return nil, err
72+
}
73+
return rpcGetBlockByNumber, nil
74+
}
75+
76+
ethHeader, err := c.EthereumClienter.HeaderByNumber(ctx, numberBigInt)
77+
if err != nil {
78+
return nil, err
79+
}
80+
res := aggkittypes.NewBlockHeaderFromEthHeader(ethHeader)
81+
res.RequestedBlock = number
82+
return res, nil
83+
84+
}
85+
86+
func (c *DefaultEthClient) resolveBlockNumber(ctx context.Context, number *aggkittypes.BlockNumberFinality) (*big.Int, error) {
87+
// If is a number or don't have offset with 1 query it's enough
88+
if number.IsConstant() || !number.HasOffset() {
89+
return number.ToBigInt(), nil
90+
}
91+
// Resolve the base block number
92+
hdr, err := c.rpcGetBlockByNumber(ctx, number.ToBigInt())
93+
if err != nil {
94+
return nil, err
95+
}
96+
num := number.CalculateBlockNumber(hdr.Number)
97+
return big.NewInt(int64(num)), nil
98+
}
99+
100+
func (c *DefaultEthClient) rpcGetBlockByNumber(ctx context.Context, number *big.Int) (*aggkittypes.BlockHeader, error) {
101+
blockArg := rpc.BlockNumber(number.Int64()).String()
102+
fmt.Printf("rpcGetBlockByNumber: requesting block %s via JSON RPC\n", blockArg)
103+
var rawEthHeader *blockRawEth
104+
err := c.CallContext(ctx, &rawEthHeader, "eth_getBlockByNumber", blockArg, false)
105+
if err != nil {
106+
return nil, fmt.Errorf("rpcGetBlockByNumber: %w", err)
107+
}
108+
return rawEthHeader.ToBlockHeader()
109+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package etherman
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"math/big"
7+
"os"
8+
"testing"
9+
10+
ethermanconfig "github.com/agglayer/aggkit/etherman/config"
11+
aggkittypes "github.com/agglayer/aggkit/types"
12+
"github.com/agglayer/aggkit/types/mocks"
13+
"github.com/ethereum/go-ethereum/rpc"
14+
"github.com/stretchr/testify/mock"
15+
"github.com/stretchr/testify/require"
16+
)
17+
18+
func TestDefaultEthClientExploratory(t *testing.T) {
19+
l2url := os.Getenv("L2URL")
20+
ctx := t.Context()
21+
cfg := ethermanconfig.L2RPCClientConfig{
22+
RPCClientConfig: ethermanconfig.RPCClientConfig{
23+
URL: l2url,
24+
},
25+
Mode: ethermanconfig.RPCModeBasic,
26+
}
27+
28+
client, err := NewRPCClient(ctx, cfg)
29+
require.NoError(t, err)
30+
clientEth, ok := client.(*DefaultEthClient)
31+
require.True(t, ok)
32+
clientEth.HashFromJSON = true
33+
number := big.NewInt(123)
34+
fmt.Printf("block: %s\n", rpc.BlockNumber(number.Int64()).String())
35+
fmt.Printf("block: %s\n", rpc.BlockNumber(rpc.SafeBlockNumber).String())
36+
fmt.Printf("block: %s\n", rpc.BlockNumber(rpc.LatestBlockNumber).String())
37+
bn, err := aggkittypes.NewBlockNumberFinality("FinalizedBlock/10")
38+
require.NoError(t, err)
39+
header, err := clientEth.CustomHeaderByNumber(ctx, &bn)
40+
require.NoError(t, err)
41+
fmt.Printf("header: %+v\n", header)
42+
43+
//client.CustomHeaderByNumber(ctx, number)
44+
}
45+
46+
func TestDefaultEthClient_CustomHeaderByNumber(t *testing.T) {
47+
mockEthClient := mocks.NewEthereumClienter(t)
48+
mockRPCClient := mocks.NewRPCClienter(t)
49+
50+
client := NewDefaultEthClient(mockEthClient, mockRPCClient)
51+
bn, err := aggkittypes.NewBlockNumberFinality("FinalizedBlock/5")
52+
require.NoError(t, err)
53+
54+
// Setup mock for rpcGetBlockByNumber
55+
mockRPCClient.
56+
EXPECT().
57+
CallContext(
58+
mock.Anything,
59+
mock.AnythingOfType("*etherman.blockRawEth"),
60+
"eth_getBlockByNumber",
61+
rpc.BlockNumber(100).String(),
62+
).
63+
Return(nil).
64+
Run(func(ctx context.Context, result interface{}, method string, args ...interface{}) {
65+
rawEth := args[0].(**blockRawEth)
66+
*rawEth = &blockRawEth{
67+
Number: "0x64", // 100 in hex
68+
Hash: "0xabc123",
69+
}
70+
})
71+
// Call CustomHeaderByNumber
72+
header, err := client.CustomHeaderByNumber(context.Background(), &bn)
73+
require.NoError(t, err)
74+
require.NotNil(t, header)
75+
require.Equal(t, uint64(100), header.Number)
76+
require.Equal(t, "0xabc123", header.Hash.Hex())
77+
require.Equal(t, &bn, header.RequestedBlock)
78+
}

etherman/rpcnoopclient.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package etherman
2+
3+
import (
4+
"context"
5+
"errors"
6+
7+
aggkittypes "github.com/agglayer/aggkit/types"
8+
"github.com/ethereum/go-ethereum/rpc"
9+
)
10+
11+
var (
12+
_ aggkittypes.RPCClienter = (*NoopRPCClient)(nil)
13+
ErrNotImplemented = errors.New("Not implemented")
14+
)
15+
16+
// NoopRPCClient is no operation implementation for the RPCClienter interface
17+
type NoopRPCClient struct{}
18+
19+
func (c *NoopRPCClient) Call(result any, method string, args ...any) error {
20+
return ErrNotImplemented
21+
}
22+
23+
func (c *NoopRPCClient) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error {
24+
return ErrNotImplemented
25+
}
26+
27+
func (c *NoopRPCClient) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error {
28+
return ErrNotImplemented
29+
}

0 commit comments

Comments
 (0)