Skip to content

Instantly share code, notes, and snippets.

@F0rzend
Created April 12, 2023 00:46
Show Gist options
  • Save F0rzend/5aa6c19b9e6da2af3a7f4c1ec88593fe to your computer and use it in GitHub Desktop.
Save F0rzend/5aa6c19b9e6da2af3a7f4c1ec88593fe to your computer and use it in GitHub Desktop.
go-ethereum errors handling utilities
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