ethergo/client/client.go
package client
import (
"context"
"fmt"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/rpc"
"github.com/lmittmann/w3"
"github.com/lmittmann/w3/w3types"
"github.com/synapsecns/sanguine/core/metrics"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"math/big"
"reflect"
)
// EVM is the set of functions that the scribe needs from a client.
//
//go:generate go run github.com/vektra/mockery/v2 --name=EVM --output=mocks --case=underscore
type EVM interface {
// ContractBackend defines the methods needed to work with contracts on a read-write basis.
// this is used for deploying an interacting with contracts
bind.ContractBackend
// ChainReader ethereum.ChainReader for getting transactions
ethereum.ChainReader
// TransactionReader is used for reading txes by hash
ethereum.TransactionReader
// ChainStateReader gets the chain state reader
ethereum.ChainStateReader
// PendingStateReader handles pending state calls
ethereum.PendingStateReader
// ChainSyncReader tracks state head
ethereum.ChainSyncReader
// PendingContractCaller tracks pending contract calls
ethereum.PendingContractCaller
// FeeHistory gets the fee history for a given block
FeeHistory(ctx context.Context, blockCount uint64, lastBlock *big.Int, rewardPercentiles []float64) (*ethereum.FeeHistory, error)
// NetworkID returns the network ID (also known as the chain ID) for this chain.
NetworkID(ctx context.Context) (*big.Int, error)
// ChainID gets the chain id from the rpc server
ChainID(ctx context.Context) (*big.Int, error)
// CallContext is used for manual overrides
CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error
// BatchCallContext is used for manual overrides
BatchCallContext(ctx context.Context, b []rpc.BatchElem) error
// BlockNumber gets the latest block number
BlockNumber(ctx context.Context) (uint64, error)
// BatchWithContext batches multiple w3type calls
BatchWithContext(ctx context.Context, calls ...w3types.Caller) error
// Web3Version gets the web3 version
Web3Version(ctx context.Context) (version string, err error)
}
type clientImpl struct {
tracing metrics.Handler
captureClient *captureClient
endpoint string
captureRequestRes bool
rpcClient *rpc.Client
// TODO: consider using sync.Pool for capture clients to improve performance
}
// DialBackend returns a scribe backend.
func DialBackend(ctx context.Context, url string, handler metrics.Handler, opts ...Options) (_ EVM, err error) {
client := &clientImpl{
endpoint: url,
tracing: handler,
}
for _, opt := range opts {
opt(client)
}
// TODO: port to master wether or not pr gets merged
client.captureClient, err = newCaptureClient(ctx, url, handler, client.captureRequestRes)
if err != nil {
return nil, fmt.Errorf("could not create capture client: %w", err)
}
client.rpcClient = client.captureClient.rpcClient
return client, nil
}
const (
batchAttribute = "batch"
methodsAttribute = "methods"
endpointAttribute = "endpoint"
)
func (c *clientImpl) getEthClient() *ethclient.Client {
return c.captureClient.ethClient
}
func (c *clientImpl) getW3Client() *w3.Client {
return c.captureClient.w3Client
}
// BatchWithContext batches multiple w3 calls.
func (c *clientImpl) BatchWithContext(ctx context.Context, calls ...w3types.Caller) (err error) {
ctx, span := c.tracing.Tracer().Start(ctx, batchAttribute)
span.SetAttributes(parseCalls(calls))
span.SetAttributes(attribute.String(endpointAttribute, c.endpoint))
defer func() {
metrics.EndSpanWithErr(span, err)
}()
//nolint: wrapcheck
return c.getW3Client().CallCtx(ctx, calls...)
}
// BatchCallContext calls BatchCallContext on the underlying client. Note: this will bypass the rate-limiter.
//
//nolint:wrapcheck
func (c *clientImpl) BatchCallContext(ctx context.Context, b []rpc.BatchElem) (err error) {
requestCtx, span := c.startSpan(ctx, NetVersionMethod)
span.SetAttributes(parseBatch(b))
span.SetAttributes(attribute.String(endpointAttribute, c.endpoint))
defer func() {
metrics.EndSpanWithErr(span, err)
}()
return c.captureClient.rpcClient.BatchCallContext(requestCtx, b)
}
func (c *clientImpl) startSpan(parentCtx context.Context, method RPCMethod, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
ctx, span := c.tracing.Tracer().Start(parentCtx, method.String(), opts...)
span.SetAttributes(attribute.String("endpoint", c.endpoint))
return ctx, span
}
// CallContract calls contract on the underlying client
//
//nolint:wrapcheck
func (c *clientImpl) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) (contractResponse []byte, err error) {
requestCtx, span := c.startSpan(ctx, CallMethod, trace.WithAttributes(attribute.String(metrics.ContractAddress, nillableToString(call.To)), attribute.String("data", common.Bytes2Hex(call.Data)), attribute.Bool("pending", false)))
defer func() {
metrics.EndSpanWithErr(span, err)
}()
return c.getEthClient().CallContract(requestCtx, call, blockNumber)
}
// toStrings converts a slice of any type that satisfies the Stringer interface into a slice of strings.
func toStrings[T fmt.Stringer](items []T) (res []string) {
for _, item := range items {
res = append(res, item.String())
}
return res
}
func nillableToString(nillable fmt.Stringer) string {
if nillable == nil {
return ""
}
// Use reflection to check if the interface's underlying value is nil.
val := reflect.ValueOf(nillable)
if val.Kind() == reflect.Ptr && val.IsNil() {
return ""
}
return nillable.String()
}
// PendingCallContract calls contract on the underlying client
//
//nolint:wrapcheck
func (c *clientImpl) PendingCallContract(ctx context.Context, call ethereum.CallMsg) (contractResponse []byte, err error) {
requestCtx, span := c.startSpan(ctx, CallMethod, trace.WithAttributes(attribute.String(metrics.ContractAddress, nillableToString(call.To)), attribute.String("data", common.Bytes2Hex(call.Data)), attribute.Bool("pending", true)))
defer func() {
metrics.EndSpanWithErr(span, err)
}()
return c.getEthClient().PendingCallContract(requestCtx, call)
}
// PendingCodeAt calls PendingCodeAt on the underlying client
//
//nolint:wrapcheck
func (c *clientImpl) PendingCodeAt(ctx context.Context, account common.Address) (codeResponse []byte, err error) {
requestCtx, span := c.startSpan(ctx, GetCodeMethod)
defer func() {
metrics.EndSpanWithErr(span, err)
}()
return c.getEthClient().PendingCodeAt(requestCtx, account)
}
// PendingBalanceAt calls PendingBalanceAt on the underlying client
//
//nolint:wrapcheck
func (c *clientImpl) PendingBalanceAt(ctx context.Context, account common.Address) (pendingBalance *big.Int, err error) {
requestCtx, span := c.startSpan(ctx, GetBalanceMethod)
defer func() {
metrics.EndSpanWithErr(span, err)
}()
return c.getEthClient().PendingBalanceAt(requestCtx, account)
}
// PendingStorageAt calls PendingStorageAt on the underlying client
//
//nolint:wrapcheck
func (c *clientImpl) PendingStorageAt(ctx context.Context, account common.Address, key common.Hash) (pendingStorage []byte, err error) {
requestCtx, span := c.startSpan(ctx, StorageAtMethod)
defer func() {
metrics.EndSpanWithErr(span, err)
}()
return c.getEthClient().PendingStorageAt(requestCtx, account, key)
}
// PendingNonceAt calls PendingNonceAt on the underlying client
//
//nolint:wrapcheck
func (c *clientImpl) PendingNonceAt(ctx context.Context, account common.Address) (pendingNonce uint64, err error) {
requestCtx, span := c.startSpan(ctx, TransactionCountMethod)
defer func() {
metrics.EndSpanWithErr(span, err)
}()
return c.getEthClient().PendingNonceAt(requestCtx, account)
}
// PendingTransactionCount calls PendingTransactionCount on the underlying client
//
//nolint:wrapcheck
func (c *clientImpl) PendingTransactionCount(ctx context.Context) (count uint, err error) {
requestCtx, span := c.startSpan(ctx, PendingTransactionCountMethod)
defer func() {
metrics.EndSpanWithErr(span, err)
}()
return c.getEthClient().PendingTransactionCount(requestCtx)
}
// NetworkID calls NetworkID on the underlying client
//
//nolint:wrapcheck
func (c *clientImpl) NetworkID(ctx context.Context) (id *big.Int, err error) {
requestCtx, span := c.startSpan(ctx, NetVersionMethod)
defer func() {
metrics.EndSpanWithErr(span, err)
}()
return c.getEthClient().NetworkID(requestCtx)
}
// Web3Version calls Web3Version on the underlying client
//
//nolint:wrapcheck
func (c *clientImpl) Web3Version(ctx context.Context) (version string, err error) {
requestCtx, span := c.startSpan(ctx, Web3VersionMethod)
defer func() {
metrics.EndSpanWithErr(span, err)
}()
var ver string
if err := c.rpcClient.CallContext(requestCtx, &ver, Web3VersionMethod.String()); err != nil {
return "", err
}
return ver, nil
}
// SyncProgress calls SyncProgress on the underlying client
//
//nolint:wrapcheck
func (c *clientImpl) SyncProgress(ctx context.Context) (syncProgress *ethereum.SyncProgress, err error) {
requestCtx, span := c.startSpan(ctx, SyncProgressMethod)
defer func() {
metrics.EndSpanWithErr(span, err)
}()
return c.getEthClient().SyncProgress(requestCtx)
}
// SuggestGasPrice calls SuggestGasPrice on the underlying client
//
//nolint:wrapcheck
func (c *clientImpl) SuggestGasPrice(ctx context.Context) (gasPrice *big.Int, err error) {
requestCtx, span := c.startSpan(ctx, GasPriceMethod)
defer func() {
metrics.EndSpanWithErr(span, err)
}()
return c.getEthClient().SuggestGasPrice(requestCtx)
}
// EstimateGas calls EstimateGas on the underlying client
//
//nolint:wrapcheck
func (c *clientImpl) EstimateGas(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error) {
requestCtx, span := c.startSpan(ctx, EstimateGasMethod)
defer func() {
metrics.EndSpanWithErr(span, err)
}()
return c.getEthClient().EstimateGas(requestCtx, call)
}
// SendTransaction calls SendTransaction on the underlying client
//
//nolint:wrapcheck
func (c *clientImpl) SendTransaction(ctx context.Context, tx *types.Transaction) (err error) {
requestCtx, span := c.startSpan(ctx, SendRawTransactionMethod)
defer func() {
metrics.EndSpanWithErr(span, err)
}()
return c.getEthClient().SendTransaction(requestCtx, tx)
}
func queryToTraceParams(query ethereum.FilterQuery) (res []attribute.KeyValue) {
res = append(res, attribute.String(metrics.BlockHash, nillableToString(query.BlockHash)))
res = append(res, attribute.String(metrics.FromBlock, nillableToString(query.FromBlock)))
res = append(res, attribute.String(metrics.ToBlock, nillableToString(query.ToBlock)))
res = append(res, attribute.StringSlice("addresses", toStrings(query.Addresses)))
// TODO: add topics
return res
}
// FilterLogs calls FilterLogs on the underlying client
//
//nolint:wrapcheck
func (c *clientImpl) FilterLogs(ctx context.Context, query ethereum.FilterQuery) (logs []types.Log, err error) {
requestCtx, span := c.startSpan(ctx, GetLogsMethod, trace.WithAttributes(queryToTraceParams(query)...))
defer func() {
metrics.EndSpanWithErr(span, err)
}()
return c.getEthClient().FilterLogs(requestCtx, query)
}
// SubscribeFilterLogs calls SubscribeFilterLogs on the underlying client
//
//nolint:wrapcheck
func (c *clientImpl) SubscribeFilterLogs(ctx context.Context, query ethereum.FilterQuery, ch chan<- types.Log) (sub ethereum.Subscription, err error) {
requestCtx, span := c.startSpan(ctx, SubscribeMethod)
defer func() {
metrics.EndSpanWithErr(span, err)
}()
return c.getEthClient().SubscribeFilterLogs(requestCtx, query, ch)
}
// BlockByHash calls BlockByHash on the underlying client
//
//nolint:wrapcheck
func (c *clientImpl) BlockByHash(ctx context.Context, hash common.Hash) (block *types.Block, err error) {
requestCtx, span := c.startSpan(ctx, BlockByHashMethod)
defer func() {
metrics.EndSpanWithErr(span, err)
}()
return c.getEthClient().BlockByHash(requestCtx, hash)
}
// BlockByNumber calls BlockByNumber on the underlying client
//
//nolint:wrapcheck
func (c *clientImpl) BlockByNumber(ctx context.Context, number *big.Int) (block *types.Block, err error) {
requestCtx, span := c.startSpan(ctx, BlockByNumberMethod)
defer func() {
metrics.EndSpanWithErr(span, err)
}()
return c.getEthClient().BlockByNumber(requestCtx, number)
}
// HeaderByHash calls HeaderByHash on the underlying client
//
//nolint:wrapcheck
func (c *clientImpl) HeaderByHash(ctx context.Context, hash common.Hash) (header *types.Header, err error) {
requestCtx, span := c.startSpan(ctx, BlockByHashMethod)
defer func() {
metrics.EndSpanWithErr(span, err)
}()
return c.getEthClient().HeaderByHash(requestCtx, hash)
}
// HeaderByNumber calls HeaderByNumber on the underlying client
//
//nolint:wrapcheck
func (c *clientImpl) HeaderByNumber(ctx context.Context, number *big.Int) (header *types.Header, err error) {
requestCtx, span := c.startSpan(ctx, BlockByNumberMethod)
defer func() {
metrics.EndSpanWithErr(span, err)
}()
return c.getEthClient().HeaderByNumber(requestCtx, number)
}
// TransactionCount calls TransactionCount on the underlying client
//
//nolint:wrapcheck
func (c *clientImpl) TransactionCount(ctx context.Context, blockHash common.Hash) (txCount uint, err error) {
requestCtx, span := c.startSpan(ctx, TransactionCountByHashMethod)
defer func() {
metrics.EndSpanWithErr(span, err)
}()
return c.getEthClient().TransactionCount(requestCtx, blockHash)
}
// TransactionInBlock calls TransactionInBlock on the underlying client
//
//nolint:wrapcheck
func (c *clientImpl) TransactionInBlock(ctx context.Context, blockHash common.Hash, index uint) (tx *types.Transaction, err error) {
requestCtx, span := c.startSpan(ctx, TransactionByBlockHashAndIndexMethod)
defer func() {
metrics.EndSpanWithErr(span, err)
}()
return c.getEthClient().TransactionInBlock(requestCtx, blockHash, index)
}
// SubscribeNewHead calls SubscribeNewHead on the underlying client
//
//nolint:wrapcheck
func (c *clientImpl) SubscribeNewHead(ctx context.Context, ch chan<- *types.Header) (sub ethereum.Subscription, err error) {
requestCtx, span := c.startSpan(ctx, SubscribeMethod)
defer func() {
metrics.EndSpanWithErr(span, err)
}()
return c.getEthClient().SubscribeNewHead(requestCtx, ch)
}
// TransactionByHash calls TransactionByHash on the underlying client
//
//nolint:wrapcheck
func (c *clientImpl) TransactionByHash(ctx context.Context, txHash common.Hash) (tx *types.Transaction, isPending bool, err error) {
requestCtx, span := c.startSpan(ctx, TransactionByHashMethod)
defer func() {
metrics.EndSpanWithErr(span, err)
}()
return c.getEthClient().TransactionByHash(requestCtx, txHash)
}
// TransactionReceipt calls TransactionReceipt on the underlying client
//
//nolint:wrapcheck
func (c *clientImpl) TransactionReceipt(ctx context.Context, txHash common.Hash) (receipt *types.Receipt, err error) {
requestCtx, span := c.startSpan(ctx, TransactionReceiptByHashMethod)
defer func() {
metrics.EndSpanWithErr(span, err)
}()
return c.getEthClient().TransactionReceipt(requestCtx, txHash)
}
// BalanceAt calls BalanceAt on the underlying client
//
//nolint:wrapcheck
func (c *clientImpl) BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (balance *big.Int, err error) {
requestCtx, span := c.startSpan(ctx, GetBalanceMethod)
defer func() {
metrics.EndSpanWithErr(span, err)
}()
return c.getEthClient().BalanceAt(requestCtx, account, blockNumber)
}
// StorageAt calls StorageAt on the underlying client
//
//nolint:wrapcheck
func (c *clientImpl) StorageAt(ctx context.Context, account common.Address, key common.Hash, blockNumber *big.Int) (storage []byte, err error) {
requestCtx, span := c.startSpan(ctx, StorageAtMethod)
defer func() {
metrics.EndSpanWithErr(span, err)
}()
return c.getEthClient().StorageAt(requestCtx, account, key, blockNumber)
}
// BlockNumber gets the latest block number
//
//nolint:wrapcheck
func (c *clientImpl) BlockNumber(ctx context.Context) (_ uint64, err error) {
requestCtx, span := c.startSpan(ctx, BlockNumberMethod)
defer func() {
metrics.EndSpanWithErr(span, err)
}()
return c.getEthClient().BlockNumber(requestCtx)
}
// CodeAt calls CodeAt on the underlying client
//
//nolint:wrapcheck
func (c *clientImpl) CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) (codeAt []byte, err error) {
requestCtx, span := c.startSpan(ctx, GetCodeMethod)
defer func() {
metrics.EndSpanWithErr(span, err)
}()
return c.getEthClient().CodeAt(requestCtx, account, blockNumber)
}
// SuggestGasTipCap gets the suggested gas tip for a chain.
//
//nolint:wrapcheck
func (c *clientImpl) SuggestGasTipCap(ctx context.Context) (tip *big.Int, err error) {
requestCtx, span := c.startSpan(ctx, MaxPriorityMethod)
defer func() {
metrics.EndSpanWithErr(span, err)
}()
return c.getEthClient().SuggestGasTipCap(requestCtx)
}
// CallContext calls CallContext on the underlying client. Note: this will bypass the rate-limiter.
//
//nolint:wrapcheck
func (c *clientImpl) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) (err error) {
requestCtx, span := c.startSpan(ctx, RPCMethod(method))
defer func() {
metrics.EndSpanWithErr(span, err)
}()
return c.captureClient.rpcClient.CallContext(requestCtx, result, method, args...)
}
// NonceAt calls NonceAt on the underlying client
//
//nolint:wrapcheck
func (c *clientImpl) NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (nonce uint64, err error) {
requestCtx, span := c.startSpan(ctx, TransactionCountMethod)
defer func() {
metrics.EndSpanWithErr(span, err)
}()
return c.getEthClient().NonceAt(requestCtx, account, blockNumber)
}
// ChainID calls ChainID on the underlying client.
//
//nolint:wrapcheck
func (c *clientImpl) ChainID(ctx context.Context) (chainID *big.Int, err error) {
requestCtx, span := c.startSpan(ctx, ChainIDMethod)
defer func() {
metrics.EndSpanWithErr(span, err)
}()
return c.getEthClient().ChainID(requestCtx)
}
// FeeHistory calls FeeHistory on the underlying client
//
//nolint:wrapcheck
func (c *clientImpl) FeeHistory(ctx context.Context, blockCount uint64, lastBlock *big.Int, rewardPercentiles []float64) (_ *ethereum.FeeHistory, err error) {
requestCtx, span := c.startSpan(ctx, FeeHistoryMethod)
defer func() {
metrics.EndSpanWithErr(span, err)
}()
return c.getEthClient().FeeHistory(requestCtx, blockCount, lastBlock, rewardPercentiles)
}
// parseCalls parses out calls from w3types.Caller.
func parseCalls(calls []w3types.Caller) attribute.KeyValue {
res := make([]string, len(calls))
for i, call := range calls {
req, err := call.CreateRequest()
if err != nil {
res[i] = fmt.Sprintf("unknown: %v", err)
continue
}
res[i] = req.Method
}
return attribute.StringSlice(methodsAttribute, res)
}
func parseBatch(batchElem []rpc.BatchElem) attribute.KeyValue {
res := make([]string, len(batchElem))
for i, elem := range batchElem {
res[i] = elem.Method
}
return attribute.StringSlice(methodsAttribute, res)
}