Skip to content

Commit

Permalink
feat: create interface for eth EL client
Browse files Browse the repository at this point in the history
and+ move current JSON-RPC implementation in sub package
  • Loading branch information
MattKetmo committed Jan 16, 2023
1 parent 5138f08 commit b90e7f6
Show file tree
Hide file tree
Showing 14 changed files with 18,738 additions and 270 deletions.
2 changes: 1 addition & 1 deletion cmd/all_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package cmd
import (
"github.com/kilnfi/go-utils/app"
consclienthttp "github.com/kilnfi/go-utils/ethereum/consensus/client/http"
execclient "github.com/kilnfi/go-utils/ethereum/execution/client"
execclient "github.com/kilnfi/go-utils/ethereum/execution/client/jsonrpc"
"github.com/kilnfi/go-utils/hashicorp"
gethkeystore "github.com/kilnfi/go-utils/keystore/geth"
"github.com/kilnfi/go-utils/sql"
Expand Down
2 changes: 1 addition & 1 deletion cmd/execution_layer.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"context"

"github.com/kilnfi/go-utils/cmd/utils"
execclient "github.com/kilnfi/go-utils/ethereum/execution/client"
execclient "github.com/kilnfi/go-utils/ethereum/execution/client/jsonrpc"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
Expand Down
261 changes: 21 additions & 240 deletions ethereum/execution/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,248 +2,29 @@ package client

import (
"context"
"fmt"
"math/big"

geth "github.com/ethereum/go-ethereum"
gethcommon "github.com/ethereum/go-ethereum/common"
gethhexutil "github.com/ethereum/go-ethereum/common/hexutil"
gethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/sirupsen/logrus"

"github.com/kilnfi/go-utils/common/interfaces"
"github.com/kilnfi/go-utils/ethereum/execution/types"
"github.com/kilnfi/go-utils/net/jsonrpc"
jsonrpchttp "github.com/kilnfi/go-utils/net/jsonrpc/http"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
)

// Client provides methods to interface with a JSON-RPC Ethereum 1.0 node
type Client struct {
client jsonrpc.Client
}

// New creates a new client
func NewFromClient(cli jsonrpc.Client) *Client {
return &Client{
client: cli,
}
}

// NewFromAddress creates a new client connecting to an Ethereum node at addr
func New(cfg *jsonrpchttp.Config) (*Client, error) {
jsonrpcc, err := jsonrpchttp.NewClient(cfg)
if err != nil {
return nil, err
}

return NewFromClient(jsonrpc.WithIncrementalID()(jsonrpc.WithVersion("2.0")(jsonrpcc))), nil
}

func (c *Client) Logger() logrus.FieldLogger {
if loggable, ok := c.client.(interfaces.Loggable); ok {
return loggable.Logger()
}
return nil
}

func (c *Client) SetLogger(logger logrus.FieldLogger) {
if loggable, ok := c.client.(interfaces.Loggable); ok {
loggable.SetLogger(logger)
}
}

func (c *Client) call(ctx context.Context, res interface{}, method string, params ...interface{}) error {
return c.client.Call(
ctx,
&jsonrpc.Request{
Method: method,
Params: params,
},
res,
)
}

// ChainID returns chain id
func (c *Client) ChainID(ctx context.Context) (*big.Int, error) {
res := new(gethhexutil.Big)
err := c.call(ctx, res, "eth_chainId")
if err != nil {
return nil, err
}

return (*big.Int)(res), nil
}

// BlockNumber returns current chain head number
func (c *Client) BlockNumber(ctx context.Context) (uint64, error) {
res := new(gethhexutil.Uint64)
err := c.call(ctx, res, "eth_blockNumber")
if err != nil {
return 0, err
}

return uint64(*res), nil
}

// HeaderByNumber returns header a given block number
func (c *Client) HeaderByNumber(ctx context.Context, blockNumber *big.Int) (*gethtypes.Header, error) {
res := new(gethtypes.Header)
err := c.call(ctx, res, "eth_getBlockByNumber", types.ToBlockNumArg(blockNumber), false)
if err == nil && res == nil {
err = geth.NotFound
}

return res, err
}

// CallContract executes contract call
// The block number can be nil, in which case call is executed at the latest block.
//nolint:gocritic
func (c *Client) CallContract(ctx context.Context, msg geth.CallMsg, blockNumber *big.Int) ([]byte, error) {
res := new(gethhexutil.Bytes)
err := c.call(ctx, res, "eth_call", toCallArg(&msg), types.ToBlockNumArg(blockNumber))
if err != nil {
return nil, err
}

return []byte(*res), nil
}

// CodeAt returns the contract code of the given account.
// The block number can be nil, in which case the code is taken from the latest block.
func (c *Client) CodeAt(ctx context.Context, account gethcommon.Address, blockNumber *big.Int) ([]byte, error) {
res := new(gethhexutil.Bytes)
err := c.call(ctx, res, "eth_getCode", account, types.ToBlockNumArg(blockNumber))
if err != nil {
return nil, err
}

return []byte(*res), nil
}

// PendingCodeAt returns the contract code of the given account on pending state
func (c *Client) PendingCodeAt(ctx context.Context, account gethcommon.Address) ([]byte, error) {
return c.CodeAt(ctx, account, big.NewInt(-1))
}

// NonceAt returns the next nonce for the given account.
// The block number can be nil, in which case the code is taken from the latest block.
func (c *Client) NonceAt(ctx context.Context, account gethcommon.Address, blockNumber *big.Int) (uint64, error) {
res := new(gethhexutil.Uint64)
err := c.call(ctx, res, "eth_getTransactionCount", account, types.ToBlockNumArg(blockNumber))
if err != nil {
return 0, err
}

return uint64(*res), nil
}

// PendingNonceAt returns the next nonce for the given account considering pending transaction.
func (c *Client) PendingNonceAt(ctx context.Context, account gethcommon.Address) (uint64, error) {
return c.NonceAt(ctx, account, big.NewInt(-1))
}

// SuggestGasPrice returns gas price for a transaction to be included in a miner block in a timely
// manner considering current network activity
func (c *Client) SuggestGasPrice(ctx context.Context) (*big.Int, error) {
res := new(gethhexutil.Big)
err := c.call(ctx, res, "eth_gasPrice")
if err != nil {
return nil, err
}

return (*big.Int)(res), nil
}

// SuggestGasPrice returns a gas tip cap after EIP-1559 for a transaction to be included in a miner block in a timely
// manner considering current network activity
func (c *Client) SuggestGasTipCap(ctx context.Context) (*big.Int, error) {
res := new(gethhexutil.Big)
err := c.call(ctx, res, "eth_maxPriorityFeePerGas")
if err != nil {
return nil, err
}

return (*big.Int)(res), nil
}

// EstimateGas tries to estimate the gas needed to execute a specific transaction based on
// the current pending state of the chain.
//nolint:gocritic
func (c *Client) EstimateGas(ctx context.Context, msg geth.CallMsg) (uint64, error) {
res := new(gethhexutil.Uint64)
err := c.call(ctx, res, "eth_estimateGas", toCallArg(&msg))
if err != nil {
return 0, err
}
return uint64(*res), nil
}

// SendTransaction injects a signed transaction into the pending pool for execution.
func (c *Client) SendTransaction(ctx context.Context, tx *gethtypes.Transaction) error {
data, err := tx.MarshalBinary()
if err != nil {
return err
}

return c.call(ctx, nil, "eth_sendRawTransaction", gethhexutil.Encode(data))
}

// FilterLogs executes a filter query.
func (c *Client) FilterLogs(ctx context.Context, q geth.FilterQuery) ([]gethtypes.Log, error) {
var res []gethtypes.Log
arg, err := toFilterArg(q)
if err != nil {
return nil, err
}

err = c.call(ctx, res, "eth_getLogs", arg)

return res, err
}

// SubscribeFilterLogs subscribes to the results of a streaming filter query.
func (c *Client) SubscribeFilterLogs(ctx context.Context, _ geth.FilterQuery, _ chan<- gethtypes.Log) (geth.Subscription, error) {
return nil, fmt.Errorf("not implemented")
}

func toCallArg(msg *geth.CallMsg) interface{} {
arg := map[string]interface{}{
"from": msg.From,
"to": msg.To,
}
if len(msg.Data) > 0 {
arg["data"] = gethhexutil.Bytes(msg.Data)
}
if msg.Value != nil {
arg["value"] = (*gethhexutil.Big)(msg.Value)
}
if msg.Gas != 0 {
arg["gas"] = gethhexutil.Uint64(msg.Gas)
}
if msg.GasPrice != nil {
arg["gasPrice"] = (*gethhexutil.Big)(msg.GasPrice)
}
return arg
}

func toFilterArg(q geth.FilterQuery) (interface{}, error) {
arg := map[string]interface{}{
"address": q.Addresses,
"topics": q.Topics,
}
if q.BlockHash != nil {
arg["blockHash"] = *q.BlockHash
if q.FromBlock != nil || q.ToBlock != nil {
return nil, fmt.Errorf("cannot specify both BlockHash and FromBlock/ToBlock")
}
} else {
if q.FromBlock == nil {
arg["fromBlock"] = "0x0"
} else {
arg["fromBlock"] = types.ToBlockNumArg(q.FromBlock)
}
arg["toBlock"] = types.ToBlockNumArg(q.ToBlock)
}
return arg, nil
//go:generate mockgen -source client.go -destination mock/client.go -package mock client
type Client interface {
bind.ContractBackend

ethereum.ChainReader
ethereum.ChainStateReader
ethereum.ChainSyncReader
ethereum.ContractCaller
ethereum.GasEstimator
ethereum.GasPricer
ethereum.LogFilterer
ethereum.PendingContractCaller
ethereum.PendingStateReader
ethereum.TransactionReader
ethereum.TransactionSender

BlockNumber(ctx context.Context) (uint64, error)
ChainID(ctx context.Context) (*big.Int, error)
NetworkID(ctx context.Context) (*big.Int, error)
}
21 changes: 0 additions & 21 deletions ethereum/execution/client/config.go

This file was deleted.

81 changes: 81 additions & 0 deletions ethereum/execution/client/geth/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package geth

import (
"context"
"encoding/json"
"fmt"
"math/big"
"sync"

"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/rpc"
"github.com/kilnfi/go-utils/ethereum/execution/client"
)

// Ensure Client interface is fully implemented
var _ client.Client = (*Client)(nil)

// Wrapper for the go-ethereum client
type Client struct {
*ethclient.Client

address string
rpcclient *rpc.Client

chainID *big.Int
mu sync.Mutex
}

func NewClient(address string) *Client {
return &Client{
address: address,
}
}

func (c *Client) Init(ctx context.Context) error {
rpcClient, err := rpc.Dial(c.address)
if err != nil {
return fmt.Errorf("failed to connect execution layer: %w", err)
}
c.rpcclient = rpcClient
c.Client = ethclient.NewClient(rpcClient)
return nil
}

func (c *Client) ChainID(ctx context.Context) (*big.Int, error) {
c.mu.Lock()
defer c.mu.Unlock()
if c.chainID != nil {
return c.chainID, nil
}
id, err := c.Client.ChainID(ctx)
if err != nil {
return nil, err
}
c.chainID = id
return id, nil
}

// While the last release of go-ethereum can't make a call to eth_getBlockByNumber
// with finalized arg, we hook into the method to make this custom call.
// Once go-ethereum will release this:
// https://github.com/ethereum/go-ethereum/blob/c1aa1db69e74c71f251fc83cf7c120b4d0222728/ethclient/gethclient/gethclient.go#L189
// then we could simply remove this condition
// finalizedBlock, err := s.core.ElClient().BlockByNumber(ctx, big.NewInt(int64(rpc.finalizedBlockNumber)))
func (c *Client) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) {
finalized := big.NewInt(int64(rpc.FinalizedBlockNumber))
if number.Cmp(finalized) == 0 {
var raw json.RawMessage
if err := c.rpcclient.CallContext(ctx, &raw, "eth_getBlockByNumber", "finalized", true); err != nil {
return nil, err
}
var head *types.Header
if err := json.Unmarshal(raw, &head); err != nil {
return nil, err
}
return types.NewBlockWithHeader(head), nil // this block object is incomplete but enough for current usage.
}

return c.Client.BlockByNumber(ctx, number)
}
Loading

0 comments on commit b90e7f6

Please sign in to comment.