code.vegaprotocol.io/vega@v0.79.0/core/client/eth/client_wrapper.go (about)

     1  // Copyright (C) 2023 Gobalsky Labs Limited
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    15  
    16  package eth
    17  
    18  import (
    19  	"context"
    20  	"math/big"
    21  	"regexp"
    22  	"time"
    23  
    24  	"code.vegaprotocol.io/vega/core/metrics"
    25  
    26  	"github.com/ethereum/go-ethereum"
    27  	ethcommon "github.com/ethereum/go-ethereum/common"
    28  	ethtypes "github.com/ethereum/go-ethereum/core/types"
    29  	"github.com/hashicorp/golang-lru/v2/expirable"
    30  )
    31  
    32  // ETHClient ...
    33  //
    34  //go:generate go run github.com/golang/mock/mockgen -destination mocks/eth_client_mock.go -package mocks code.vegaprotocol.io/vega/core/client/eth ETHClient
    35  type ETHClient interface { //revive:disable:exported
    36  	// bind.ContractBackend
    37  	// ethereum.ChainReader
    38  
    39  	// client
    40  	ChainID(context.Context) (*big.Int, error)
    41  	NetworkID(context.Context) (*big.Int, error)
    42  
    43  	// ethereum.ChainReader
    44  	BlockByHash(ctx context.Context, hash ethcommon.Hash) (*ethtypes.Block, error)
    45  	HeaderByNumber(ctx context.Context, number *big.Int) (*ethtypes.Header, error)
    46  	BlockByNumber(ctx context.Context, number *big.Int) (*ethtypes.Block, error)
    47  	HeaderByHash(ctx context.Context, hash ethcommon.Hash) (*ethtypes.Header, error)
    48  	SubscribeNewHead(ctx context.Context, ch chan<- *ethtypes.Header) (ethereum.Subscription, error)
    49  	TransactionCount(ctx context.Context, blockHash ethcommon.Hash) (uint, error)
    50  	TransactionInBlock(ctx context.Context, blockHash ethcommon.Hash, index uint) (*ethtypes.Transaction, error)
    51  
    52  	// bind.ContractCaller
    53  	CodeAt(ctx context.Context, contract ethcommon.Address, blockNumber *big.Int) ([]byte, error)
    54  	CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error)
    55  
    56  	// bind.ContractTransactor
    57  	EstimateGas(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error)
    58  	PendingCodeAt(ctx context.Context, account ethcommon.Address) ([]byte, error)
    59  	PendingNonceAt(ctx context.Context, account ethcommon.Address) (uint64, error)
    60  	SendTransaction(ctx context.Context, tx *ethtypes.Transaction) error
    61  	SuggestGasPrice(ctx context.Context) (*big.Int, error)
    62  	SuggestGasTipCap(ctx context.Context) (*big.Int, error)
    63  
    64  	// bind.ContractFilterer
    65  	FilterLogs(ctx context.Context, query ethereum.FilterQuery) ([]ethtypes.Log, error)
    66  	SubscribeFilterLogs(ctx context.Context, query ethereum.FilterQuery, ch chan<- ethtypes.Log) (ethereum.Subscription, error)
    67  }
    68  
    69  type ethClientWrapper struct {
    70  	clt ETHClient
    71  
    72  	headerByNumberCache *expirable.LRU[string, *ethtypes.Header]
    73  }
    74  
    75  func newEthClientWrapper(clt ETHClient) *ethClientWrapper {
    76  	return &ethClientWrapper{
    77  		clt: clt,
    78  		// arbitrary size of 100 blocks, kept for at most 10 minutes,
    79  		// let see later how to make this less hardcoded
    80  		headerByNumberCache: expirable.NewLRU[string, *ethtypes.Header](100, nil, 10*time.Minute),
    81  	}
    82  }
    83  
    84  func (c *ethClientWrapper) ChainID(ctx context.Context) (*big.Int, error) {
    85  	metrics.EthereumRPCCallCounterInc("chain_id")
    86  	id, err := c.clt.ChainID(ctx)
    87  	if err != nil {
    88  		return nil, ErrorWithStrippedSecrets{err: err}
    89  	}
    90  	return id, nil
    91  }
    92  
    93  func (c *ethClientWrapper) NetworkID(ctx context.Context) (*big.Int, error) {
    94  	metrics.EthereumRPCCallCounterInc("network_id")
    95  	id, err := c.clt.NetworkID(ctx)
    96  	if err != nil {
    97  		return nil, ErrorWithStrippedSecrets{err: err}
    98  	}
    99  	return id, nil
   100  }
   101  
   102  func (c *ethClientWrapper) BlockByHash(ctx context.Context, hash ethcommon.Hash) (*ethtypes.Block, error) {
   103  	metrics.EthereumRPCCallCounterInc("block_by_hash")
   104  	byHash, err := c.clt.BlockByHash(ctx, hash)
   105  	if err != nil {
   106  		return nil, ErrorWithStrippedSecrets{err: err}
   107  	}
   108  	return byHash, nil
   109  }
   110  
   111  func (c *ethClientWrapper) HeaderByNumber(ctx context.Context, number *big.Int) (*ethtypes.Header, error) {
   112  	if number != nil {
   113  		// first check the cache
   114  		if header, ok := c.headerByNumberCache.Get(number.String()); ok {
   115  			return ethtypes.CopyHeader(header), nil
   116  		}
   117  	}
   118  
   119  	// cache miss, so let's inc the counter, and call the rpc.
   120  	metrics.EthereumRPCCallCounterInc("header_by_number")
   121  	header, err := c.clt.HeaderByNumber(ctx, number)
   122  	if err != nil {
   123  		return nil, ErrorWithStrippedSecrets{err: err}
   124  	}
   125  
   126  	c.headerByNumberCache.Add(header.Number.String(), ethtypes.CopyHeader(header))
   127  
   128  	return header, nil
   129  }
   130  
   131  func (c *ethClientWrapper) BlockByNumber(ctx context.Context, number *big.Int) (*ethtypes.Block, error) {
   132  	metrics.EthereumRPCCallCounterInc("block_by_number")
   133  	byNumber, err := c.clt.BlockByNumber(ctx, number)
   134  	if err != nil {
   135  		return nil, ErrorWithStrippedSecrets{err: err}
   136  	}
   137  	return byNumber, nil
   138  }
   139  
   140  func (c *ethClientWrapper) HeaderByHash(ctx context.Context, hash ethcommon.Hash) (*ethtypes.Header, error) {
   141  	metrics.EthereumRPCCallCounterInc("header_by_hash")
   142  	byHash, err := c.clt.HeaderByHash(ctx, hash)
   143  	if err != nil {
   144  		return nil, ErrorWithStrippedSecrets{err: err}
   145  	}
   146  	return byHash, nil
   147  }
   148  
   149  func (c *ethClientWrapper) SubscribeNewHead(ctx context.Context, ch chan<- *ethtypes.Header) (ethereum.Subscription, error) {
   150  	head, err := c.clt.SubscribeNewHead(ctx, ch)
   151  	if err != nil {
   152  		return nil, ErrorWithStrippedSecrets{err: err}
   153  	}
   154  	return head, nil
   155  }
   156  
   157  func (c *ethClientWrapper) TransactionCount(ctx context.Context, blockHash ethcommon.Hash) (uint, error) {
   158  	metrics.EthereumRPCCallCounterInc("transaction_count")
   159  	count, err := c.clt.TransactionCount(ctx, blockHash)
   160  	if err != nil {
   161  		return 0, ErrorWithStrippedSecrets{err: err}
   162  	}
   163  	return count, nil
   164  }
   165  
   166  func (c *ethClientWrapper) TransactionInBlock(ctx context.Context, blockHash ethcommon.Hash, index uint) (*ethtypes.Transaction, error) {
   167  	metrics.EthereumRPCCallCounterInc("transaction_in_block")
   168  	block, err := c.clt.TransactionInBlock(ctx, blockHash, index)
   169  	if err != nil {
   170  		return nil, ErrorWithStrippedSecrets{err: err}
   171  	}
   172  	return block, nil
   173  }
   174  
   175  func (c *ethClientWrapper) CodeAt(ctx context.Context, contract ethcommon.Address, blockNumber *big.Int) ([]byte, error) {
   176  	metrics.EthereumRPCCallCounterInc("code_at")
   177  	at, err := c.clt.CodeAt(ctx, contract, blockNumber)
   178  	if err != nil {
   179  		return nil, ErrorWithStrippedSecrets{err: err}
   180  	}
   181  	return at, nil
   182  }
   183  
   184  func (c *ethClientWrapper) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) {
   185  	metrics.EthereumRPCCallCounterInc("call_contract")
   186  	contract, err := c.clt.CallContract(ctx, call, blockNumber)
   187  	if err != nil {
   188  		return nil, ErrorWithStrippedSecrets{err: err}
   189  	}
   190  	return contract, nil
   191  }
   192  
   193  func (c *ethClientWrapper) EstimateGas(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error) {
   194  	metrics.EthereumRPCCallCounterInc("estimate_gas")
   195  	estimateGas, err := c.clt.EstimateGas(ctx, call)
   196  	if err != nil {
   197  		return 0, ErrorWithStrippedSecrets{err: err}
   198  	}
   199  	return estimateGas, nil
   200  }
   201  
   202  func (c *ethClientWrapper) PendingCodeAt(ctx context.Context, account ethcommon.Address) ([]byte, error) {
   203  	metrics.EthereumRPCCallCounterInc("pending_code_at")
   204  	at, err := c.clt.PendingCodeAt(ctx, account)
   205  	if err != nil {
   206  		return nil, ErrorWithStrippedSecrets{err: err}
   207  	}
   208  	return at, nil
   209  }
   210  
   211  func (c *ethClientWrapper) PendingNonceAt(ctx context.Context, account ethcommon.Address) (uint64, error) {
   212  	metrics.EthereumRPCCallCounterInc("pending_nonce_at")
   213  	at, err := c.clt.PendingNonceAt(ctx, account)
   214  	if err != nil {
   215  		return 0, ErrorWithStrippedSecrets{err: err}
   216  	}
   217  	return at, nil
   218  }
   219  
   220  func (c *ethClientWrapper) SendTransaction(ctx context.Context, tx *ethtypes.Transaction) error {
   221  	metrics.EthereumRPCCallCounterInc("send_transaction")
   222  	err := c.clt.SendTransaction(ctx, tx)
   223  	return ErrorWithStrippedSecrets{err: err}
   224  }
   225  
   226  func (c *ethClientWrapper) SuggestGasPrice(ctx context.Context) (*big.Int, error) {
   227  	metrics.EthereumRPCCallCounterInc("suggest_gas_price")
   228  	price, err := c.clt.SuggestGasPrice(ctx)
   229  	if err != nil {
   230  		return nil, ErrorWithStrippedSecrets{err: err}
   231  	}
   232  	return price, nil
   233  }
   234  
   235  func (c *ethClientWrapper) SuggestGasTipCap(ctx context.Context) (*big.Int, error) {
   236  	metrics.EthereumRPCCallCounterInc("suggest_gas_tip_cap")
   237  	tipCap, err := c.clt.SuggestGasTipCap(ctx)
   238  	if err != nil {
   239  		return nil, ErrorWithStrippedSecrets{err: err}
   240  	}
   241  	return tipCap, nil
   242  }
   243  
   244  func (c *ethClientWrapper) FilterLogs(ctx context.Context, query ethereum.FilterQuery) ([]ethtypes.Log, error) {
   245  	metrics.EthereumRPCCallCounterInc("filter_logs")
   246  	logs, err := c.clt.FilterLogs(ctx, query)
   247  	if err != nil {
   248  		return nil, ErrorWithStrippedSecrets{err: err}
   249  	}
   250  	return logs, nil
   251  }
   252  
   253  func (c *ethClientWrapper) SubscribeFilterLogs(ctx context.Context, query ethereum.FilterQuery, ch chan<- ethtypes.Log) (ethereum.Subscription, error) {
   254  	metrics.EthereumRPCCallCounterInc("subscribe_filter_logs")
   255  	logs, err := c.clt.SubscribeFilterLogs(ctx, query, ch)
   256  	if err != nil {
   257  		return nil, ErrorWithStrippedSecrets{err: err}
   258  	}
   259  	return logs, nil
   260  }
   261  
   262  // ErrorWithStrippedSecrets is an extremely naïve implementation of an error that
   263  // will strip the API token from a URL.
   264  type ErrorWithStrippedSecrets struct {
   265  	err error
   266  }
   267  
   268  func (e ErrorWithStrippedSecrets) Error() string {
   269  	return regexp.
   270  		MustCompile(`(?i)(apitoken|token|apikey|key)=(.+)"`).
   271  		ReplaceAllString(e.err.Error(), "$1=xxx\"")
   272  }