synapsecns/sanguine

View on GitHub
ethergo/chain/client/client.go

Summary

Maintainability
A
0 mins
Test Coverage
package client

import (
    "context"
    "fmt"
    "github.com/synapsecns/sanguine/ethergo/client"
    "math/big"
    "net/url"
    "time"

    "github.com/ethereum/go-ethereum"
    "github.com/ethereum/go-ethereum/accounts/abi/bind"
    "github.com/ethereum/go-ethereum/ethclient"
    "github.com/ethereum/go-ethereum/params"
    "github.com/ethereum/go-ethereum/rpc"
    "github.com/lmittmann/w3"
    "github.com/lmittmann/w3/w3types"
    "github.com/pkg/errors"
)

// EVMClient is the chain client. It defines everything necessary to create a Chain.
//
// Deprecated: use ethergo/client
//
//go:generate go run github.com/vektra/mockery/v2 --name EVMClient --output ./mocks --case=underscore
type EVMClient 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)
    // BatchContext uses w3 as a helper method for batch calls
    // Deprecated: use BatchCallContext and stop using chain/client
    BatchContext(ctx context.Context, calls ...w3types.Caller) error
    // Web3Version gets the web3 version
    Web3Version(ctx context.Context) (version string, err error)
}

// clientImpl is a client implementation for an ethclient.
// it adds the chainconfig to the chain.
type clientImpl struct {
    *ethclient.Client
    // rpcClient is the underlying rpc client
    rpcClient *rpc.Client
    // w3Client is sued for batch calls
    w3Client *w3.Client
    // chainID contains the chain id
    chainID *big.Int
    // config is the chain config
    config *params.ChainConfig
    // wsURL is stored for reconnection attempts
    wsURL string
    // ctx stores the context of the original client
    //nolint: containedctx
    ctx context.Context
}

func (c *clientImpl) Web3Version(ctx context.Context) (version string, err error) {
    if err := c.rpcClient.CallContext(ctx, &version, client.Web3VersionMethod.String()); err != nil {
        // nolint: wrapcheck
        return "", err
    }
    return version, nil
}

func (c *clientImpl) BatchContext(ctx context.Context, calls ...w3types.Caller) error {
    //nolint:wrapcheck
    return c.w3Client.CallCtx(ctx, calls...)
}

// connectionResetTimeout is how long the client should wait before rehupping.
var connectionResetTimeout = time.Minute * 5

const (
    // secureWebsocketScheme defines the scheme used to connect to a secure websocket.
    secureWebsocketScheme = "wss"
    // insecureWebsocketScheme defines the scheme used to connect to an insecure websocket.
    insecureWebsocketScheme = "ws"
)

// StartConnectionResetTicker should be fired off as a goroutine upon a baseChain's
// initialization. This is only used for websocket connections
// At every connectionResetTimeout (as of writing, every 5 minutes), the client will
// attempt to reconnect to its RPC client as a way of "rehup"ing the connection, which
// is a workaround for an issue documented in https://github.com/ethereum/go-ethereum/issues/22266
// with websocket connections. This can be removed when that issue is fixed
// If the attempted reconnect fails, it is tried again on the next tick.
func (c *clientImpl) StartConnectionResetTicker(ctx context.Context) {
    parsedURL, err := url.Parse(c.wsURL)
    if err != nil {
        // this should never happen because we make sure the url can be parsed when validating the config. Nonetheless, we want to avoid a panic here
        logger.Warn(err)
        return
    }

    // we don't need to rehup if this isn't a websocket
    if parsedURL.Scheme != secureWebsocketScheme && parsedURL.Scheme != insecureWebsocketScheme {
        return
    }

    select {
    case <-ctx.Done():
        return
    case <-time.After(connectionResetTimeout):
        if err := c.AttemptReconnect(); err != nil {
            logger.Warn(errors.Wrapf(err, "reconnect attempt for Chain client for Chain ID %s failed", c.config.ChainID))
        }
    }
}

// AttemptReconnect attempts to create a new client. Because this replaces
// a pointer, this should be subscription safe.
func (c *clientImpl) AttemptReconnect() error {
    tmpRPCClient, err := rpc.DialContext(c.ctx, c.wsURL)
    if err != nil {
        return fmt.Errorf("could not create new client, continuing to use original: %w", err)
    }

    c.rpcClient = tmpRPCClient
    c.Client = ethclient.NewClient(c.rpcClient)
    c.w3Client = w3.NewClient(c.rpcClient)

    return nil
}

// CallContext exposes the CallContext methods in the underlying ethereum rpc client.
//
//nolint:wrapcheck
func (c *clientImpl) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) (err error) {
    //nolint:asasalint
    return c.rpcClient.CallContext(ctx, result, method, args)
}

// BatchCallContext calls BatchCallContext on the underlying ethereum rpc client.
//
//nolint:wrapcheck
func (c *clientImpl) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error {
    return c.rpcClient.BatchCallContext(ctx, b)
}

// NewClient creates a client from a url.
func NewClient(ctx context.Context, url string) (EVMClient, error) {
    rpcClient, err := rpc.DialContext(ctx, url)
    if err != nil {
        return nil, fmt.Errorf("could not connect to rpc server %s. Received error: %w", url, err)
    }

    ethClient := ethclient.NewClient(rpcClient)
    w3Client := w3.NewClient(rpcClient)

    chainID, err := ethClient.ChainID(ctx)
    if err != nil {
        return nil, fmt.Errorf("could not get chain id: %w", err)
    }

    client := &clientImpl{
        chainID:   chainID,
        rpcClient: rpcClient,
        Client:    ethClient,
        wsURL:     url,
        ctx:       ctx,
        w3Client:  w3Client,
    }

    go client.StartConnectionResetTicker(ctx)

    return client, nil
}

// NewClientFromChainID creates a new client from a chain id.
func NewClientFromChainID(ctx context.Context, url string, chainID *big.Int) (EVMClient, error) {
    rpcClient, err := rpc.DialContext(ctx, url)
    if err != nil {
        return nil, fmt.Errorf("could not connect to rpc server %s. Received error: %w", url, err)
    }

    ethClient := ethclient.NewClient(rpcClient)
    w3Client := w3.NewClient(rpcClient)

    client := &clientImpl{
        chainID:   chainID,
        rpcClient: rpcClient,
        Client:    ethClient,
        wsURL:     url,
        ctx:       ctx,
        w3Client:  w3Client,
    }

    go client.StartConnectionResetTicker(ctx)

    return client, nil
}