Skip to content

Commit

Permalink
feat: add generic evm client package (#11)
Browse files Browse the repository at this point in the history
* feat: add generic evm client package

* ci: update go version
  • Loading branch information
piavgh authored Apr 10, 2024
1 parent 5253059 commit 57ae993
Show file tree
Hide file tree
Showing 9 changed files with 920 additions and 11 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ jobs:
- name: Install Go
uses: actions/setup-go@v3
with:
go-version: "1.19.x"
go-version: "1.21.x"
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.52.2
version: latest
skip-pkg-cache: true
skip-build-cache: true
args: --timeout=2m
Expand All @@ -38,7 +38,7 @@ jobs:
- name: Install Go
uses: actions/setup-go@v3
with:
go-version: "1.19.x"
go-version: "1.21.x"
- name: Run tests
run: go test -race -coverprofile cover.out -vet=off ./...

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: "1.19.x"
go-version: "1.21.x"

- name: Setup Git
run: |
Expand Down
82 changes: 82 additions & 0 deletions evmclient/avalanche/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package avalanche

import (
"context"
"errors"
"math/big"
"net/http"
"time"

avaxtypes "github.com/ava-labs/coreth/core/types"
avaxclient "github.com/ava-labs/coreth/ethclient"
"github.com/ava-labs/coreth/interfaces"
"github.com/ava-labs/coreth/rpc"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"

"github.com/KyberNetwork/blockchain-toolkit/evmclient"
)

var ErrNoConfiguredPRC = errors.New("no configured rpc")

type client struct {
avaxClient avaxclient.Client
}

func NewClientWithTimeout(rpcs []string, timeout time.Duration) (*client, error) {
hc := &http.Client{Timeout: timeout}
for _, url := range rpcs {
rc, err := rpc.DialOptions(context.Background(), url, rpc.WithHTTPClient(hc))
if err != nil {
continue
}

ec := avaxclient.NewClient(rc)

return &client{avaxClient: ec}, nil
}

return nil, ErrNoConfiguredPRC
}

func (c *client) BlockNumber(ctx context.Context) (uint64, error) {
return c.avaxClient.BlockNumber(ctx)
}

func (c *client) HeaderByNumber(ctx context.Context, number *big.Int) (evmclient.Header, error) {
header, err := c.avaxClient.HeaderByNumber(ctx, number)
return convertAvaxHeader(header), err
}

func (c *client) HeaderByHash(ctx context.Context, hash common.Hash) (evmclient.Header, error) {
header, err := c.avaxClient.HeaderByHash(ctx, hash)
return convertAvaxHeader(header), err
}

func (c *client) FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) {
ecq := interfaces.FilterQuery(q)

logs, err := c.avaxClient.FilterLogs(ctx, ecq)

if err != nil {
return nil, err
}

evmLogs := []types.Log{}
for _, log := range logs {
evmLogs = append(evmLogs, types.Log(log))
}

return evmLogs, nil
}

func convertAvaxHeader(header *avaxtypes.Header) evmclient.Header {
// Only convert fields that we use.
return evmclient.Header{
Hash: header.Hash(),
ParentHash: header.ParentHash,
Number: header.Number,
Time: header.Time,
}
}
56 changes: 56 additions & 0 deletions evmclient/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package evmclient

import (
"context"
"errors"
"math/big"

"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
)

var ErrNoConfiguredPRC = errors.New("no configured rpc")

type client struct {
client *ethclient.Client
}

func NewClient(c *ethclient.Client) *client {
return &client{client: c}
}

type Header struct {
Hash common.Hash `json:"hash"`
ParentHash common.Hash `json:"parentHash"`
Number *big.Int `json:"number"`
Time uint64 `json:"timestamp"`
}

func (c *client) BlockNumber(ctx context.Context) (uint64, error) {
return c.client.BlockNumber(ctx)
}

func (c *client) HeaderByNumber(ctx context.Context, number *big.Int) (Header, error) {
header, err := c.client.HeaderByNumber(ctx, number)
return convertHeader(header), err
}

func (c *client) HeaderByHash(ctx context.Context, hash common.Hash) (Header, error) {
header, err := c.client.HeaderByHash(ctx, hash)
return convertHeader(header), err
}

func (c *client) FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) {
return c.client.FilterLogs(ctx, q)
}

func convertHeader(header *types.Header) Header {
return Header{
Hash: header.Hash(),
ParentHash: header.ParentHash,
Number: header.Number,
Time: header.Time,
}
}
218 changes: 218 additions & 0 deletions evmclient/fantom/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
package fantom

import (
"context"
"encoding/json"
"errors"
"math/big"
"net/http"
"time"

"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/rpc"

"github.com/KyberNetwork/blockchain-toolkit/evmclient"
)

var ErrNoConfiguredPRC = errors.New("no configured rpc")

type client struct {
ethClient *ethclient.Client
rpcClient *rpc.Client
}

func NewClientWithTimeout(rpcs []string, timeout time.Duration) (*client, error) {
hc := &http.Client{Timeout: timeout}
for _, url := range rpcs {
rc, err := rpc.DialOptions(context.Background(), url, rpc.WithHTTPClient(hc))
if err != nil {
continue
}

ec := ethclient.NewClient(rc)

return &client{ethClient: ec, rpcClient: rc}, nil
}
return nil, ErrNoConfiguredPRC
}

type Header struct {
Hash common.Hash `json:"hash"`
types.Header
}

func (h *Header) UnmarshalJSON(data []byte) error {
type Header struct {
Hash *common.Hash `json:"hash"`
ParentHash *common.Hash `json:"parentHash"`
UncleHash *common.Hash `json:"sha3Uncles"`
Coinbase *common.Address `json:"miner"`
Root *common.Hash `json:"stateRoot"`
TxHash *common.Hash `json:"transactionsRoot"`
ReceiptHash *common.Hash `json:"receiptsRoot"`
Bloom *types.Bloom `json:"logsBloom"`
Difficulty *hexutil.Big `json:"difficulty"`
Number *hexutil.Big `json:"number"`
GasLimit *hexutil.Uint64 `json:"gasLimit"`
GasUsed *hexutil.Uint64 `json:"gasUsed"`
Time *hexutil.Uint64 `json:"timestamp"`
Extra *hexutil.Bytes `json:"extraData"`
MixDigest *common.Hash `json:"mixHash"`
Nonce *types.BlockNonce `json:"nonce"`
BaseFee *hexutil.Big `json:"baseFeePerGas"`
WithdrawalsHash *common.Hash `json:"withdrawalsRoot"`
}

var dec Header
if err := json.Unmarshal(data, &dec); err != nil {
return err
}

if dec.Hash == nil {
return errors.New("missing required field 'hash' for Header")
}
h.Hash = *dec.Hash

if dec.ParentHash == nil {
return errors.New("missing required field 'parentHash' for Header")
}
h.ParentHash = *dec.ParentHash

if dec.UncleHash == nil {
return errors.New("missing required field 'sha3Uncles' for Header")
}
h.UncleHash = *dec.UncleHash

if dec.Coinbase != nil {
h.Coinbase = *dec.Coinbase
}
if dec.Root == nil {
return errors.New("missing required field 'stateRoot' for Header")
}
h.Root = *dec.Root

if dec.TxHash == nil {
return errors.New("missing required field 'transactionsRoot' for Header")
}
h.TxHash = *dec.TxHash

if dec.ReceiptHash == nil {
return errors.New("missing required field 'receiptsRoot' for Header")
}
h.ReceiptHash = *dec.ReceiptHash

if dec.Bloom == nil {
return errors.New("missing required field 'logsBloom' for Header")
}
h.Bloom = *dec.Bloom

if dec.Difficulty == nil {
return errors.New("missing required field 'difficulty' for Header")
}
h.Difficulty = (*big.Int)(dec.Difficulty)

if dec.Number == nil {
return errors.New("missing required field 'number' for Header")
}
h.Number = (*big.Int)(dec.Number)

if dec.GasLimit == nil {
return errors.New("missing required field 'gasLimit' for Header")
}
h.GasLimit = uint64(*dec.GasLimit)

if dec.GasUsed == nil {
return errors.New("missing required field 'gasUsed' for Header")
}
h.GasUsed = uint64(*dec.GasUsed)

if dec.Time == nil {
return errors.New("missing required field 'timestamp' for Header")
}
h.Time = uint64(*dec.Time)

if dec.Extra == nil {
return errors.New("missing required field 'extraData' for Header")
}
h.Extra = *dec.Extra

if dec.MixDigest != nil {
h.MixDigest = *dec.MixDigest
}
if dec.Nonce != nil {
h.Nonce = *dec.Nonce
}
if dec.BaseFee != nil {
h.BaseFee = (*big.Int)(dec.BaseFee)
}
if dec.WithdrawalsHash != nil {
h.WithdrawalsHash = dec.WithdrawalsHash
}

return nil
}

func (c *client) BlockNumber(ctx context.Context) (uint64, error) {
return c.ethClient.BlockNumber(ctx)
}

func (c *client) HeaderByNumber(ctx context.Context, number *big.Int) (evmclient.Header, error) {
var head *Header
err := c.rpcClient.CallContext(ctx, &head, "eth_getBlockByNumber", toBlockNumArg(number), false)
if err == nil && head == nil {
err = ethereum.NotFound
}

return evmclient.Header{
Hash: head.Hash,
ParentHash: head.ParentHash,
Number: head.Number,
Time: head.Time,
}, err
}

func (c *client) HeaderByHash(ctx context.Context, hash common.Hash) (evmclient.Header, error) {
var head *Header
err := c.rpcClient.CallContext(ctx, &head, "eth_getBlockByHash", hash, false)
if err == nil && head == nil {
err = ethereum.NotFound
}

return evmclient.Header{
Hash: head.Hash,
ParentHash: head.ParentHash,
Number: head.Number,
Time: head.Time,
}, err
}

func (c *client) FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) {
return c.ethClient.FilterLogs(ctx, q)
}

func toBlockNumArg(number *big.Int) string {
if number == nil {
return "latest"
}

pending := big.NewInt(-1)
if number.Cmp(pending) == 0 {
return "pending"
}

finalized := big.NewInt(int64(rpc.FinalizedBlockNumber))
if number.Cmp(finalized) == 0 {
return "finalized"
}

safe := big.NewInt(int64(rpc.SafeBlockNumber))
if number.Cmp(safe) == 0 {
return "safe"
}

return hexutil.EncodeBig(number)
}
Loading

0 comments on commit 57ae993

Please sign in to comment.