Skip to content

Commit

Permalink
feat: include abi custom error params in the returned error (#4376)
Browse files Browse the repository at this point in the history
  • Loading branch information
mrekucci committed Nov 13, 2023
1 parent 230973c commit c31866f
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 10 deletions.
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,6 @@ github.com/ethereum/go-ethereum v1.13.4 h1:25HJnaWVg3q1O7Z62LaaI6S9wVq8QCw3K88g8
github.com/ethereum/go-ethereum v1.13.4/go.mod h1:I0U5VewuuTzvBtVzKo7b3hJzDhXOUtn9mJW7SsIPB0Q=
github.com/ethersphere/go-price-oracle-abi v0.1.0 h1:yg/hK8nETNvk+GEBASlbakMFv/CVp7HXiycrHw1pRV8=
github.com/ethersphere/go-price-oracle-abi v0.1.0/go.mod h1:sI/Qj4/zJ23/b1enzwMMv0/hLTpPNVNacEwCWjo6yBk=
github.com/ethersphere/go-storage-incentives-abi v0.6.0-rc5 h1:JaiGMFoycByFkaCv6KFg79tfRlBJ5zY01dLWPnf48v0=
github.com/ethersphere/go-storage-incentives-abi v0.6.0-rc5/go.mod h1:SXvJVtM4sEsaSKD0jc1ClpDLw8ErPoROZDme4Wrc/Nc=
github.com/ethersphere/go-storage-incentives-abi v0.6.0-rc7 h1:SPg0WY5rCWt7Y72hAj0o1juLtNi9lqx7b/6fNaNa4H0=
github.com/ethersphere/go-storage-incentives-abi v0.6.0-rc7/go.mod h1:SXvJVtM4sEsaSKD0jc1ClpDLw8ErPoROZDme4Wrc/Nc=
github.com/ethersphere/go-sw3-abi v0.4.0 h1:T3ANY+ktWrPAwe2U0tZi+DILpkHzto5ym/XwV/Bbz8g=
Expand Down
12 changes: 11 additions & 1 deletion pkg/transaction/backendmock/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (

type backendMock struct {
codeAt func(ctx context.Context, contract common.Address, blockNumber *big.Int) ([]byte, error)
callContract func(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error)
sendTransaction func(ctx context.Context, tx *types.Transaction) error
suggestGasPrice func(ctx context.Context) (*big.Int, error)
suggestGasTipCap func(ctx context.Context) (*big.Int, error)
Expand All @@ -38,7 +39,10 @@ func (m *backendMock) CodeAt(ctx context.Context, contract common.Address, block
return nil, errors.New("not implemented")
}

func (*backendMock) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) {
func (m *backendMock) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) {
if m.callContract != nil {
return m.callContract(ctx, call, blockNumber)
}
return nil, errors.New("not implemented")
}

Expand Down Expand Up @@ -161,6 +165,12 @@ type optionFunc func(*backendMock)

func (f optionFunc) apply(r *backendMock) { f(r) }

func WithCallContractFunc(f func(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error)) Option {
return optionFunc(func(s *backendMock) {
s.callContract = f
})
}

func WithCodeAtFunc(f func(ctx context.Context, contract common.Address, blockNumber *big.Int) ([]byte, error)) Option {
return optionFunc(func(s *backendMock) {
s.codeAt = f
Expand Down
48 changes: 42 additions & 6 deletions pkg/transaction/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/rpc"
"github.com/ethersphere/bee/pkg/crypto"
"github.com/ethersphere/bee/pkg/log"
"github.com/ethersphere/bee/pkg/sctx"
Expand Down Expand Up @@ -596,20 +597,55 @@ func (t *transactionService) UnwrapABIError(ctx context.Context, req *TxRequest,
return nil
}

res, cErr := t.Call(ctx, req)
if cErr != nil {
_, cErr := t.Call(ctx, req)
if cErr == nil {
return err
}
err = fmt.Errorf("%w: %s", err, cErr)

if reason, uErr := abi.UnpackRevert(res); uErr == nil {
var derr rpc.DataError
if !errors.As(cErr, &derr) {
return err
}

res, ok := derr.ErrorData().(string)
if !ok {
return err
}
buf := common.FromHex(res)

if reason, uErr := abi.UnpackRevert(buf); uErr == nil {
return fmt.Errorf("%w: %s", err, reason)
}

for _, abiError := range abiErrors {
if bytes.Equal(res[:4], abiError.ID[:4]) {
//abiError.Unpack(res[4:])
return fmt.Errorf("%w: %s", err, abiError)
if !bytes.Equal(buf[:4], abiError.ID[:4]) {
continue
}

data, uErr := abiError.Unpack(buf)
if uErr != nil {
continue
}

values, ok := data.([]interface{})
if !ok {
values = make([]interface{}, len(abiError.Inputs))
for i := range values {
values[i] = "?"
}
}

params := make([]string, len(abiError.Inputs))
for i, input := range abiError.Inputs {
if input.Name == "" {
input.Name = fmt.Sprintf("arg%d", i)
}
params[i] = fmt.Sprintf("%s=%v", input.Name, values[i])

}

return fmt.Errorf("%w: %s(%s)", err, abiError.Name, strings.Join(params, ","))
}

return err
Expand Down
92 changes: 91 additions & 1 deletion pkg/transaction/transaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ import (
"errors"
"fmt"
"math/big"
"strings"
"testing"
"time"

"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/rpc"
"github.com/ethersphere/bee/pkg/crypto"
signermock "github.com/ethersphere/bee/pkg/crypto/mock"
"github.com/ethersphere/bee/pkg/log"
Expand All @@ -23,6 +26,7 @@ import (
"github.com/ethersphere/bee/pkg/transaction"
"github.com/ethersphere/bee/pkg/transaction/backendmock"
"github.com/ethersphere/bee/pkg/transaction/monitormock"
"github.com/ethersphere/bee/pkg/util/abiutil"
"github.com/ethersphere/bee/pkg/util/testutil"
)

Expand Down Expand Up @@ -239,7 +243,7 @@ func TestTransactionSend(t *testing.T) {
t.Fatal(err)
}

transactionService, err := transaction.NewService(logger,
transactionService, err := transaction.NewService(logger, sender,
backendmock.New(
backendmock.WithSendTransactionFunc(func(ctx context.Context, tx *types.Transaction) error {
if tx != signedTx {
Expand Down Expand Up @@ -901,3 +905,89 @@ func TestTransactionCancel(t *testing.T) {
}
})
}

// rpcAPIError is a copy of engine.EngineAPIError from go-ethereum pkg.
type rpcAPIError struct {
code int
msg string
err string
}

func (e *rpcAPIError) ErrorCode() int { return e.code }
func (e *rpcAPIError) Error() string { return e.msg }
func (e *rpcAPIError) ErrorData() interface{} { return e.err }

var _ rpc.DataError = (*rpcAPIError)(nil)

func TestTransactionService_UnwrapABIError(t *testing.T) {
t.Parallel()

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

var (
sender = common.HexToAddress("0xddff")
recipient = common.HexToAddress("0xbbbddd")
chainID = big.NewInt(5)
nonce = uint64(10)
gasTip = big.NewInt(100)
gasFee = big.NewInt(1100)
txData = common.Hex2Bytes("0xabcdee")
value = big.NewInt(1)

// This is the ABI of the following contract: https://goerli.etherscan.io/address/0xd29d9e385f19d888557cd609006bb1934cb5d1e2#code
contractABI = abiutil.MustParseABI(`[{"inputs":[{"internalType":"uint256","name":"available","type":"uint256"},{"internalType":"uint256","name":"required","type":"uint256"}],"name":"InsufficientBalance","type":"error"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[],"stateMutability":"nonpayable","type":"function"}]`)
rpcAPIErr = &rpcAPIError{
code: 3,
msg: "execution reverted",
err: "0xcf4791810000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006f", // This is the ABI encoded error form the following failed transaction: https://goerli.etherscan.io/tx/0x74a2577db1c325c41e38977aa1eb32ab03dfa17cc1fa0649e84f3d8c0f0882ee
}
)

gasTipCap := new(big.Int).Div(new(big.Int).Mul(big.NewInt(int64(10)+100), gasTip), big.NewInt(100))
gasFeeCap := new(big.Int).Add(gasFee, gasTipCap)

signedTx := types.NewTx(&types.DynamicFeeTx{
ChainID: chainID,
Nonce: nonce,
To: &recipient,
Value: value,
Gas: 21000,
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
Data: txData,
})
request := &transaction.TxRequest{
To: &recipient,
Data: txData,
Value: value,
}

transactionService, err := transaction.NewService(log.Noop, sender,
backendmock.New(
backendmock.WithCallContractFunc(func(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) {
return nil, rpcAPIErr
}),
),
signerMockForTransaction(t, signedTx, recipient, chainID),
storemock.NewStateStore(),
chainID,
monitormock.New(),
)
if err != nil {
t.Fatal(err)
}
testutil.CleanupCloser(t, transactionService)

originErr := errors.New("origin error")
wrappedErr := transactionService.UnwrapABIError(ctx, request, originErr, contractABI.Errors)
if !errors.Is(wrappedErr, originErr) {
t.Fatal("origin error not wrapped")
}
if !strings.Contains(wrappedErr.Error(), rpcAPIErr.Error()) {
t.Fatal("wrapped error without rpc api main error")
}
if !strings.Contains(wrappedErr.Error(), "InsufficientBalance(available=0,required=111)") {
t.Fatal("wrapped error without rpc api error data")
}
}

0 comments on commit c31866f

Please sign in to comment.