Created
April 12, 2023 00:46
-
-
Save F0rzend/5aa6c19b9e6da2af3a7f4c1ec88593fe to your computer and use it in GitHub Desktop.
go-ethereum errors handling utilities
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package utils | |
import ( | |
"bytes" | |
"context" | |
"crypto/ecdsa" | |
"github.com/ethereum/go-ethereum/accounts/abi" | |
"github.com/ethereum/go-ethereum/common/hexutil" | |
"github.com/ethereum/go-ethereum/rpc" | |
"math/big" | |
"reflect" | |
"github.com/ethereum/go-ethereum/accounts/abi/bind" | |
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends" | |
"github.com/ethereum/go-ethereum/common" | |
"github.com/ethereum/go-ethereum/core" | |
"github.com/ethereum/go-ethereum/core/types" | |
"github.com/ethereum/go-ethereum/crypto" | |
"github.com/pkg/errors" | |
) | |
const ( | |
// TestChainID is a number of a backend chainID | |
// A simulated backend always uses chainID 1337 | |
TestChainID = 1337 | |
// TestAccountBalance Balance of the testing account. | |
// This balance is set to the transactional account in the genesis block | |
// It must be large enough to be enough for all manipulations in a test | |
TestAccountBalance = ^uint64(0) | |
// TestGasLimit is a Gas limit for the testing | |
// GasLimit specifies the maximum size of a transaction | |
// It must be large enough to be enough for all manipulations in a test | |
TestGasLimit = 0 | |
) | |
type Account struct { | |
key *ecdsa.PrivateKey | |
address common.Address | |
} | |
func NewAccount() (*Account, error) { | |
key, err := crypto.GenerateKey() | |
if err != nil { | |
return nil, errors.Wrap(err, "Unexpected error occurred while getting account keys pair") | |
} | |
addr := crypto.PubkeyToAddress(key.PublicKey) | |
return &Account{ | |
key: key, | |
address: addr, | |
}, nil | |
} | |
type DeployFn[ContractInterface any] func( | |
auth *bind.TransactOpts, | |
backend bind.ContractBackend, | |
) ( | |
common.Address, | |
*types.Transaction, | |
*ContractInterface, | |
error, | |
) | |
type TestBlockchain[ContractInterface any] struct { | |
Contract *ContractInterface | |
ABI *abi.ABI | |
Metadata *bind.MetaData | |
Address common.Address | |
Backend *backends.SimulatedBackend | |
Transactor *bind.TransactOpts | |
Caller *bind.CallOpts | |
} | |
func NewTestBlockchain[ContractInterface any](deployFn DeployFn[ContractInterface], metadata *bind.MetaData) ( | |
*TestBlockchain[ContractInterface], | |
error, | |
) { | |
blockchain := new(TestBlockchain[ContractInterface]) | |
contractABI, err := metadata.GetAbi() | |
if err != nil { | |
return nil, errors.Wrap(err, "unexpected error occurred while getting contract abi") | |
} | |
blockchain.ABI = contractABI | |
blockchain.Metadata = metadata | |
chainID := big.NewInt(TestChainID) | |
account, err := NewAccount() | |
if err != nil { | |
return nil, err | |
} | |
blockchain.Backend = backends.NewSimulatedBackend( | |
core.GenesisAlloc{ | |
account.address: core.GenesisAccount{ | |
Balance: big.NewInt(0).SetUint64(TestAccountBalance), | |
}, | |
}, | |
TestGasLimit, | |
) | |
blockchain.Transactor, err = bind.NewKeyedTransactorWithChainID(account.key, chainID) | |
if err != nil { | |
return nil, err | |
} | |
blockchain.Caller = &bind.CallOpts{ | |
Pending: false, | |
From: account.address, | |
BlockNumber: nil, | |
Context: context.Background(), | |
} | |
blockchain.Address, _, blockchain.Contract, err = deployFn(blockchain.Transactor, blockchain.Backend) | |
if err != nil { | |
return nil, err | |
} | |
blockchain.Backend.Commit() | |
return blockchain, nil | |
} | |
func DecodeError(err error) ([]byte, error) { | |
var dataError rpc.DataError | |
if !errors.As(err, &dataError) { | |
return nil, errors.New("error is not a data error") | |
} | |
data, ok := dataError.ErrorData().(string) | |
if !ok { | |
return nil, errors.New("error data is not a string") | |
} | |
decoded, err := hexutil.Decode(data) | |
if err != nil { | |
return nil, errors.Wrap(err, "unexpected error occurred while decoding error reason") | |
} | |
return decoded, nil | |
} | |
func GetRevertReason(target error) (string, error) { | |
decoded, err := DecodeError(target) | |
if err != nil { | |
return "", err | |
} | |
reason, err := abi.UnpackRevert(decoded) | |
if err != nil { | |
return "", errors.Wrap(err, "unexpected error occurred while unpacking revert reason") | |
} | |
return reason, nil | |
} | |
func GetPanicCode(target error) (*big.Int, error) { | |
decoded, err := DecodeError(target) | |
if err != nil { | |
return nil, err | |
} | |
code, err := UnpackPanic(decoded) | |
if err != nil { | |
return nil, errors.Wrap(err, "unexpected error occurred while unpacking panic code") | |
} | |
return code, nil | |
} | |
func UnpackPanic(data []byte) (*big.Int, error) { | |
if !isPanic(data) { | |
return nil, errors.New("data is not a panic") | |
} | |
uintType, err := abi.NewType("uint256", "", nil) | |
if err != nil { | |
return nil, err | |
} | |
unpacked, err := (abi.Arguments{{Type: uintType}}).Unpack(data[4:]) | |
if err != nil { | |
return nil, err | |
} | |
return unpacked[0].(*big.Int), nil | |
} | |
func isPanic(data []byte) bool { | |
const panicSignature = "Panic(uint256)" | |
selector := crypto.Keccak256([]byte(panicSignature))[:4] | |
if len(data) < 4 { | |
return false | |
} | |
return bytes.Equal(data[:4], selector) | |
} | |
func isCorrectDestination(dst any) bool { | |
kind := reflect.ValueOf(dst).Kind() | |
return kind == reflect.Interface || kind == reflect.Ptr | |
} | |
func GetErrorValues(target error, dst any, abiError abi.Error) error { | |
if !isCorrectDestination(dst) { | |
return errors.New("destination must be a pointer or an interface") | |
} | |
if !ContractErrorIs(target, abiError) { | |
return errors.New("revert reason is not the expected one") | |
} | |
decoded, err := DecodeError(target) | |
if err != nil { | |
return err | |
} | |
unpacked, err := abiError.Unpack(decoded) | |
if err != nil { | |
return errors.Wrap(err, "unexpected error occurred while unpacking revert values") | |
} | |
values, ok := unpacked.([]any) | |
if !ok { | |
return errors.New("unexpected error occurred while unpacking revert values") | |
} | |
err = abiError.Inputs.Copy(dst, values) | |
if err != nil { | |
return errors.Wrap(err, "unexpected error occurred while copying revert values") | |
} | |
return nil | |
} | |
func IsError(target error) bool { | |
const errorSignature = "Error(string)" | |
selector := crypto.Keccak256([]byte(errorSignature))[:4] | |
decoded, err := DecodeError(target) | |
if err != nil { | |
return false | |
} | |
if len(decoded) < 4 { | |
return false | |
} | |
return bytes.Equal(decoded[:4], selector) | |
} | |
func IsPanic(target error) bool { | |
decoded, err := DecodeError(target) | |
if err != nil { | |
return false | |
} | |
return isPanic(decoded) | |
} | |
func ContractErrorIs(target error, expected abi.Error) bool { | |
if target == nil { | |
return false | |
} | |
decoded, err := DecodeError(target) | |
if err != nil { | |
return false | |
} | |
selector := expected.ID.Bytes()[:4] | |
return bytes.Equal(decoded[:4], selector) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment