status-go/rpc/chain/client.go
Stefan a6d553c937 fix(wallet) ClientWithFallback handling state errors required by BH
The "not found" and "no contract" expected error is caught and wrapped
by ClientWithFallback. The fetching of balance history of next blocks
is aborted which is not desired.

Fix by not treating the error as a connection error in
ClientWithFallback.
2023-05-02 12:07:06 +02:00

841 lines
21 KiB
Go

package chain
import (
"context"
"fmt"
"math/big"
"strings"
"sync"
"time"
"github.com/afex/hystrix-go/hystrix"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/rpc"
"github.com/status-im/status-go/services/rpcstats"
)
type FeeHistory struct {
BaseFeePerGas []string `json:"baseFeePerGas"`
}
type ClientWithFallback struct {
ChainID uint64
main *ethclient.Client
fallback *ethclient.Client
mainRPC *rpc.Client
fallbackRPC *rpc.Client
WalletNotifier func(chainId uint64, message string)
IsConnected bool
IsConnectedLock sync.RWMutex
LastCheckedAt int64
}
// Don't mark connection as failed if we get one of these errors
var propagateErrors = []error{
vm.ErrOutOfGas,
vm.ErrCodeStoreOutOfGas,
vm.ErrDepth,
vm.ErrInsufficientBalance,
vm.ErrContractAddressCollision,
vm.ErrExecutionReverted,
vm.ErrMaxCodeSizeExceeded,
vm.ErrInvalidJump,
vm.ErrWriteProtection,
vm.ErrReturnDataOutOfBounds,
vm.ErrGasUintOverflow,
vm.ErrInvalidCode,
vm.ErrNonceUintOverflow,
// Used by balance history to check state
ethereum.NotFound,
bind.ErrNoCode,
}
type CommandResult struct {
res1 any
res2 any
vmError error
}
func NewSimpleClient(main *rpc.Client, chainID uint64) *ClientWithFallback {
hystrix.ConfigureCommand(fmt.Sprintf("ethClient_%d", chainID), hystrix.CommandConfig{
Timeout: 10000,
MaxConcurrentRequests: 100,
SleepWindow: 300000,
ErrorPercentThreshold: 25,
})
return &ClientWithFallback{
ChainID: chainID,
main: ethclient.NewClient(main),
fallback: nil,
mainRPC: main,
fallbackRPC: nil,
IsConnected: true,
LastCheckedAt: time.Now().Unix(),
}
}
func NewClient(main, fallback *rpc.Client, chainID uint64) *ClientWithFallback {
hystrix.ConfigureCommand(fmt.Sprintf("ethClient_%d", chainID), hystrix.CommandConfig{
Timeout: 20000,
MaxConcurrentRequests: 100,
SleepWindow: 300000,
ErrorPercentThreshold: 25,
})
var fallbackEthClient *ethclient.Client
if fallback != nil {
fallbackEthClient = ethclient.NewClient(fallback)
}
return &ClientWithFallback{
ChainID: chainID,
main: ethclient.NewClient(main),
fallback: fallbackEthClient,
mainRPC: main,
fallbackRPC: fallback,
IsConnected: true,
LastCheckedAt: time.Now().Unix(),
}
}
func (c *ClientWithFallback) Close() {
c.main.Close()
if c.fallback != nil {
c.fallback.Close()
}
}
func isVMError(err error) bool {
if strings.HasPrefix(err.Error(), "execution reverted") {
return true
}
for _, vmError := range propagateErrors {
if err == vmError {
return true
}
}
return false
}
func (c *ClientWithFallback) SetIsConnected(value bool) {
c.IsConnectedLock.Lock()
defer c.IsConnectedLock.Unlock()
c.LastCheckedAt = time.Now().Unix()
if !value {
if c.IsConnected {
if c.WalletNotifier != nil {
c.WalletNotifier(c.ChainID, "down")
}
c.IsConnected = false
}
} else {
if !c.IsConnected {
c.IsConnected = true
if c.WalletNotifier != nil {
c.WalletNotifier(c.ChainID, "up")
}
}
}
}
func (c *ClientWithFallback) makeCallNoReturn(main func() error, fallback func() error) error {
resultChan := make(chan CommandResult, 1)
c.LastCheckedAt = time.Now().Unix()
errChan := hystrix.Go(fmt.Sprintf("ethClient_%d", c.ChainID), func() error {
err := main()
if err != nil {
if isVMError(err) {
resultChan <- CommandResult{vmError: err}
return nil
}
return err
}
c.SetIsConnected(true)
resultChan <- CommandResult{}
return nil
}, func(err error) error {
if c.fallback == nil {
c.SetIsConnected(false)
return err
}
err = fallback()
if err != nil {
if isVMError(err) {
resultChan <- CommandResult{vmError: err}
return nil
}
c.SetIsConnected(false)
return err
}
resultChan <- CommandResult{}
return nil
})
select {
case result := <-resultChan:
if result.vmError != nil {
return result.vmError
}
return nil
case err := <-errChan:
return err
}
}
func (c *ClientWithFallback) makeCallSingleReturn(main func() (any, error), fallback func() (any, error), toggleIsConnected bool) (any, error) {
resultChan := make(chan CommandResult, 1)
errChan := hystrix.Go(fmt.Sprintf("ethClient_%d", c.ChainID), func() error {
res, err := main()
if err != nil {
if isVMError(err) {
resultChan <- CommandResult{vmError: err}
return nil
}
return err
}
if toggleIsConnected {
c.SetIsConnected(true)
}
resultChan <- CommandResult{res1: res}
return nil
}, func(err error) error {
if c.fallback == nil {
if toggleIsConnected {
c.SetIsConnected(false)
}
return err
}
res, err := fallback()
if err != nil {
if isVMError(err) {
resultChan <- CommandResult{vmError: err}
return nil
}
if toggleIsConnected {
c.SetIsConnected(false)
}
return err
}
if toggleIsConnected {
c.SetIsConnected(true)
}
resultChan <- CommandResult{res1: res}
return nil
})
select {
case result := <-resultChan:
if result.vmError != nil {
return nil, result.vmError
}
return result.res1, nil
case err := <-errChan:
return nil, err
}
}
func (c *ClientWithFallback) makeCallDoubleReturn(main func() (any, any, error), fallback func() (any, any, error)) (any, any, error) {
resultChan := make(chan CommandResult, 1)
c.LastCheckedAt = time.Now().Unix()
errChan := hystrix.Go(fmt.Sprintf("ethClient_%d", c.ChainID), func() error {
a, b, err := main()
if err != nil {
if isVMError(err) {
resultChan <- CommandResult{vmError: err}
return nil
}
return err
}
c.SetIsConnected(true)
resultChan <- CommandResult{res1: a, res2: b}
return nil
}, func(err error) error {
if c.fallback == nil {
c.SetIsConnected(false)
return err
}
a, b, err := fallback()
if err != nil {
if isVMError(err) {
resultChan <- CommandResult{vmError: err}
return nil
}
c.SetIsConnected(false)
return err
}
c.SetIsConnected(true)
resultChan <- CommandResult{res1: a, res2: b}
return nil
})
select {
case result := <-resultChan:
if result.vmError != nil {
return nil, nil, result.vmError
}
return result.res1, result.res2, nil
case err := <-errChan:
return nil, nil, err
}
}
func (c *ClientWithFallback) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) {
rpcstats.CountCall("eth_BlockByHash")
block, err := c.makeCallSingleReturn(
func() (any, error) { return c.main.BlockByHash(ctx, hash) },
func() (any, error) { return c.fallback.BlockByHash(ctx, hash) },
true,
)
if err != nil {
return nil, err
}
return block.(*types.Block), nil
}
func (c *ClientWithFallback) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) {
rpcstats.CountCall("eth_BlockByNumber")
block, err := c.makeCallSingleReturn(
func() (any, error) { return c.main.BlockByNumber(ctx, number) },
func() (any, error) { return c.fallback.BlockByNumber(ctx, number) },
true,
)
if err != nil {
return nil, err
}
return block.(*types.Block), nil
}
func (c *ClientWithFallback) BlockNumber(ctx context.Context) (uint64, error) {
rpcstats.CountCall("eth_BlockNumber")
number, err := c.makeCallSingleReturn(
func() (any, error) { return c.main.BlockNumber(ctx) },
func() (any, error) { return c.fallback.BlockNumber(ctx) },
true,
)
if err != nil {
return 0, err
}
return number.(uint64), nil
}
func (c *ClientWithFallback) PeerCount(ctx context.Context) (uint64, error) {
rpcstats.CountCall("eth_PeerCount")
peerCount, err := c.makeCallSingleReturn(
func() (any, error) { return c.main.PeerCount(ctx) },
func() (any, error) { return c.fallback.PeerCount(ctx) },
true,
)
if err != nil {
return 0, err
}
return peerCount.(uint64), nil
}
func (c *ClientWithFallback) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) {
rpcstats.CountCall("eth_HeaderByHash")
header, err := c.makeCallSingleReturn(
func() (any, error) { return c.main.HeaderByHash(ctx, hash) },
func() (any, error) { return c.fallback.HeaderByHash(ctx, hash) },
false,
)
if err != nil {
return nil, err
}
return header.(*types.Header), nil
}
func (c *ClientWithFallback) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) {
rpcstats.CountCall("eth_HeaderByNumber")
header, err := c.makeCallSingleReturn(
func() (any, error) { return c.main.HeaderByNumber(ctx, number) },
func() (any, error) { return c.fallback.HeaderByNumber(ctx, number) },
false,
)
if err != nil {
return nil, err
}
return header.(*types.Header), nil
}
func (c *ClientWithFallback) TransactionByHash(ctx context.Context, hash common.Hash) (*types.Transaction, bool, error) {
rpcstats.CountCall("eth_TransactionByHash")
tx, isPending, err := c.makeCallDoubleReturn(
func() (any, any, error) { return c.main.TransactionByHash(ctx, hash) },
func() (any, any, error) { return c.fallback.TransactionByHash(ctx, hash) },
)
if err != nil {
return nil, false, err
}
return tx.(*types.Transaction), isPending.(bool), nil
}
func (c *ClientWithFallback) TransactionSender(ctx context.Context, tx *types.Transaction, block common.Hash, index uint) (common.Address, error) {
rpcstats.CountCall("eth_TransactionSender")
address, err := c.makeCallSingleReturn(
func() (any, error) { return c.main.TransactionSender(ctx, tx, block, index) },
func() (any, error) { return c.fallback.TransactionSender(ctx, tx, block, index) },
true,
)
return address.(common.Address), err
}
func (c *ClientWithFallback) TransactionCount(ctx context.Context, blockHash common.Hash) (uint, error) {
rpcstats.CountCall("eth_TransactionCount")
count, err := c.makeCallSingleReturn(
func() (any, error) { return c.main.TransactionCount(ctx, blockHash) },
func() (any, error) { return c.fallback.TransactionCount(ctx, blockHash) },
true,
)
if err != nil {
return 0, err
}
return count.(uint), nil
}
func (c *ClientWithFallback) TransactionInBlock(ctx context.Context, blockHash common.Hash, index uint) (*types.Transaction, error) {
rpcstats.CountCall("eth_TransactionInBlock")
transactions, err := c.makeCallSingleReturn(
func() (any, error) { return c.main.TransactionInBlock(ctx, blockHash, index) },
func() (any, error) { return c.fallback.TransactionInBlock(ctx, blockHash, index) },
true,
)
if err != nil {
return nil, err
}
return transactions.(*types.Transaction), nil
}
func (c *ClientWithFallback) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) {
rpcstats.CountCall("eth_TransactionReceipt")
receipt, err := c.makeCallSingleReturn(
func() (any, error) { return c.main.TransactionReceipt(ctx, txHash) },
func() (any, error) { return c.fallback.TransactionReceipt(ctx, txHash) },
true,
)
if err != nil {
return nil, err
}
return receipt.(*types.Receipt), nil
}
func (c *ClientWithFallback) SyncProgress(ctx context.Context) (*ethereum.SyncProgress, error) {
rpcstats.CountCall("eth_SyncProgress")
progress, err := c.makeCallSingleReturn(
func() (any, error) { return c.main.SyncProgress(ctx) },
func() (any, error) { return c.fallback.SyncProgress(ctx) },
true,
)
if err != nil {
return nil, err
}
return progress.(*ethereum.SyncProgress), nil
}
func (c *ClientWithFallback) SubscribeNewHead(ctx context.Context, ch chan<- *types.Header) (ethereum.Subscription, error) {
rpcstats.CountCall("eth_SubscribeNewHead")
sub, err := c.makeCallSingleReturn(
func() (any, error) { return c.main.SubscribeNewHead(ctx, ch) },
func() (any, error) { return c.fallback.SubscribeNewHead(ctx, ch) },
true,
)
if err != nil {
return nil, err
}
return sub.(ethereum.Subscription), nil
}
func (c *ClientWithFallback) NetworkID(ctx context.Context) (*big.Int, error) {
rpcstats.CountCall("eth_NetworkID")
networkID, err := c.makeCallSingleReturn(
func() (any, error) { return c.main.NetworkID(ctx) },
func() (any, error) { return c.fallback.NetworkID(ctx) },
true,
)
if err != nil {
return nil, err
}
return networkID.(*big.Int), nil
}
func (c *ClientWithFallback) BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) {
rpcstats.CountCall("eth_BalanceAt")
balance, err := c.makeCallSingleReturn(
func() (any, error) { return c.main.BalanceAt(ctx, account, blockNumber) },
func() (any, error) { return c.fallback.BalanceAt(ctx, account, blockNumber) },
true,
)
if err != nil {
return nil, err
}
return balance.(*big.Int), nil
}
func (c *ClientWithFallback) StorageAt(ctx context.Context, account common.Address, key common.Hash, blockNumber *big.Int) ([]byte, error) {
rpcstats.CountCall("eth_StorageAt")
storage, err := c.makeCallSingleReturn(
func() (any, error) { return c.main.StorageAt(ctx, account, key, blockNumber) },
func() (any, error) { return c.fallback.StorageAt(ctx, account, key, blockNumber) },
true,
)
if err != nil {
return nil, err
}
return storage.([]byte), nil
}
func (c *ClientWithFallback) CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) {
rpcstats.CountCall("eth_CodeAt")
code, err := c.makeCallSingleReturn(
func() (any, error) { return c.main.CodeAt(ctx, account, blockNumber) },
func() (any, error) { return c.fallback.CodeAt(ctx, account, blockNumber) },
true,
)
if err != nil {
return nil, err
}
return code.([]byte), nil
}
func (c *ClientWithFallback) NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) {
rpcstats.CountCall("eth_NonceAt")
nonce, err := c.makeCallSingleReturn(
func() (any, error) { return c.main.NonceAt(ctx, account, blockNumber) },
func() (any, error) { return c.fallback.NonceAt(ctx, account, blockNumber) },
true,
)
if err != nil {
return 0, err
}
return nonce.(uint64), nil
}
func (c *ClientWithFallback) FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) {
rpcstats.CountCall("eth_FilterLogs")
logs, err := c.makeCallSingleReturn(
func() (any, error) { return c.main.FilterLogs(ctx, q) },
func() (any, error) { return c.fallback.FilterLogs(ctx, q) },
true,
)
if err != nil {
return nil, err
}
return logs.([]types.Log), nil
}
func (c *ClientWithFallback) SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) {
rpcstats.CountCall("eth_SubscribeFilterLogs")
sub, err := c.makeCallSingleReturn(
func() (any, error) { return c.main.SubscribeFilterLogs(ctx, q, ch) },
func() (any, error) { return c.fallback.SubscribeFilterLogs(ctx, q, ch) },
true,
)
if err != nil {
return nil, err
}
return sub.(ethereum.Subscription), nil
}
func (c *ClientWithFallback) PendingBalanceAt(ctx context.Context, account common.Address) (*big.Int, error) {
rpcstats.CountCall("eth_PendingBalanceAt")
balance, err := c.makeCallSingleReturn(
func() (any, error) { return c.main.PendingBalanceAt(ctx, account) },
func() (any, error) { return c.fallback.PendingBalanceAt(ctx, account) },
true,
)
if err != nil {
return nil, err
}
return balance.(*big.Int), nil
}
func (c *ClientWithFallback) PendingStorageAt(ctx context.Context, account common.Address, key common.Hash) ([]byte, error) {
rpcstats.CountCall("eth_PendingStorageAt")
storage, err := c.makeCallSingleReturn(
func() (any, error) { return c.main.PendingStorageAt(ctx, account, key) },
func() (any, error) { return c.fallback.PendingStorageAt(ctx, account, key) },
true,
)
if err != nil {
return nil, err
}
return storage.([]byte), nil
}
func (c *ClientWithFallback) PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error) {
rpcstats.CountCall("eth_PendingCodeAt")
code, err := c.makeCallSingleReturn(
func() (any, error) { return c.main.PendingCodeAt(ctx, account) },
func() (any, error) { return c.fallback.PendingCodeAt(ctx, account) },
true,
)
if err != nil {
return nil, err
}
return code.([]byte), nil
}
func (c *ClientWithFallback) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) {
rpcstats.CountCall("eth_PendingNonceAt")
nonce, err := c.makeCallSingleReturn(
func() (any, error) { return c.main.PendingNonceAt(ctx, account) },
func() (any, error) { return c.fallback.PendingNonceAt(ctx, account) },
true,
)
if err != nil {
return 0, err
}
return nonce.(uint64), nil
}
func (c *ClientWithFallback) PendingTransactionCount(ctx context.Context) (uint, error) {
rpcstats.CountCall("eth_PendingTransactionCount")
count, err := c.makeCallSingleReturn(
func() (any, error) { return c.main.PendingTransactionCount(ctx) },
func() (any, error) { return c.fallback.PendingTransactionCount(ctx) },
true,
)
if err != nil {
return 0, err
}
return count.(uint), nil
}
func (c *ClientWithFallback) CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) {
rpcstats.CountCall("eth_CallContract")
data, err := c.makeCallSingleReturn(
func() (any, error) { return c.main.CallContract(ctx, msg, blockNumber) },
func() (any, error) { return c.fallback.CallContract(ctx, msg, blockNumber) },
true,
)
if err != nil {
return nil, err
}
return data.([]byte), nil
}
func (c *ClientWithFallback) CallContractAtHash(ctx context.Context, msg ethereum.CallMsg, blockHash common.Hash) ([]byte, error) {
rpcstats.CountCall("eth_CallContractAtHash")
data, err := c.makeCallSingleReturn(
func() (any, error) { return c.main.CallContractAtHash(ctx, msg, blockHash) },
func() (any, error) { return c.fallback.CallContractAtHash(ctx, msg, blockHash) },
true,
)
if err != nil {
return nil, err
}
return data.([]byte), nil
}
func (c *ClientWithFallback) PendingCallContract(ctx context.Context, msg ethereum.CallMsg) ([]byte, error) {
rpcstats.CountCall("eth_PendingCallContract")
data, err := c.makeCallSingleReturn(
func() (any, error) { return c.main.PendingCallContract(ctx, msg) },
func() (any, error) { return c.fallback.PendingCallContract(ctx, msg) },
true,
)
if err != nil {
return nil, err
}
return data.([]byte), nil
}
func (c *ClientWithFallback) SuggestGasPrice(ctx context.Context) (*big.Int, error) {
rpcstats.CountCall("eth_SuggestGasPrice")
gasPrice, err := c.makeCallSingleReturn(
func() (any, error) { return c.main.SuggestGasPrice(ctx) },
func() (any, error) { return c.fallback.SuggestGasPrice(ctx) },
true,
)
if err != nil {
return nil, err
}
return gasPrice.(*big.Int), nil
}
func (c *ClientWithFallback) SuggestGasTipCap(ctx context.Context) (*big.Int, error) {
rpcstats.CountCall("eth_SuggestGasTipCap")
tip, err := c.makeCallSingleReturn(
func() (any, error) { return c.main.SuggestGasTipCap(ctx) },
func() (any, error) { return c.fallback.SuggestGasTipCap(ctx) },
true,
)
if err != nil {
return nil, err
}
return tip.(*big.Int), nil
}
func (c *ClientWithFallback) FeeHistory(ctx context.Context, blockCount uint64, lastBlock *big.Int, rewardPercentiles []float64) (*ethereum.FeeHistory, error) {
rpcstats.CountCall("eth_FeeHistory")
feeHistory, err := c.makeCallSingleReturn(
func() (any, error) { return c.main.FeeHistory(ctx, blockCount, lastBlock, rewardPercentiles) },
func() (any, error) { return c.fallback.FeeHistory(ctx, blockCount, lastBlock, rewardPercentiles) },
true,
)
if err != nil {
return nil, err
}
return feeHistory.(*ethereum.FeeHistory), nil
}
func (c *ClientWithFallback) EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error) {
rpcstats.CountCall("eth_EstimateGas")
estimate, err := c.makeCallSingleReturn(
func() (any, error) { return c.main.EstimateGas(ctx, msg) },
func() (any, error) { return c.fallback.EstimateGas(ctx, msg) },
true,
)
if err != nil {
return 0, err
}
return estimate.(uint64), nil
}
func (c *ClientWithFallback) SendTransaction(ctx context.Context, tx *types.Transaction) error {
rpcstats.CountCall("eth_SendTransaction")
return c.makeCallNoReturn(
func() error { return c.main.SendTransaction(ctx, tx) },
func() error { return c.fallback.SendTransaction(ctx, tx) },
)
}
func (c *ClientWithFallback) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error {
rpcstats.CountCall("eth_CallContext")
return c.makeCallNoReturn(
func() error { return c.mainRPC.CallContext(ctx, result, method, args...) },
func() error { return c.fallbackRPC.CallContext(ctx, result, method, args...) },
)
}
func (c *ClientWithFallback) ToBigInt() *big.Int {
return big.NewInt(int64(c.ChainID))
}
func (c *ClientWithFallback) GetBaseFeeFromBlock(blockNumber *big.Int) (string, error) {
rpcstats.CountCall("eth_GetBaseFeeFromBlock")
var feeHistory FeeHistory
err := c.mainRPC.Call(&feeHistory, "eth_feeHistory", "0x1", (*hexutil.Big)(blockNumber), nil)
if err != nil {
if err.Error() == "the method eth_feeHistory does not exist/is not available" {
return "", nil
}
return "", err
}
var baseGasFee string = ""
if len(feeHistory.BaseFeePerGas) > 0 {
baseGasFee = feeHistory.BaseFeePerGas[0]
}
return baseGasFee, err
}