synapsecns/sanguine

View on GitHub
ethergo/client/client.go

Summary

Maintainability
A
0 mins
Test Coverage
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)
}