ethergo/chain/client/client.go
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
}