diff --git a/ethclient/ethclient.go b/ethclient/ethclient.go index 0972644d80..04f66f2742 100644 --- a/ethclient/ethclient.go +++ b/ethclient/ethclient.go @@ -630,6 +630,24 @@ func (ec *Client) SendTransaction(ctx context.Context, tx *types.Transaction) er return ec.c.CallContext(ctx, nil, "eth_sendRawTransaction", hexutil.Encode(data)) } +// RevertErrorData returns the 'revert reason' data of a contract call. +// +// This can be used with CallContract and EstimateGas, and only when the server is Geth. +func RevertErrorData(err error) ([]byte, bool) { + var ec rpc.Error + var ed rpc.DataError + if errors.As(err, &ec) && errors.As(err, &ed) && ec.ErrorCode() == 3 { + eds, ok := ed.ErrorData().(string) + if ok { + revertData, err := hexutil.Decode(eds) + if err == nil { + return revertData, true + } + } + } + return nil, false +} + func toBlockNumArg(number *big.Int) string { if number == nil { return "latest" diff --git a/ethclient/ethclient_test.go b/ethclient/ethclient_test.go index 1b7e26fb74..76960ee3fb 100644 --- a/ethclient/ethclient_test.go +++ b/ethclient/ethclient_test.go @@ -20,6 +20,7 @@ import ( "bytes" "context" "errors" + "fmt" "math/big" "reflect" "testing" @@ -180,14 +181,19 @@ func TestToFilterArg(t *testing.T) { } var ( - testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") - testAddr = crypto.PubkeyToAddress(testKey.PublicKey) - testBalance = big.NewInt(2e15) + testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + testAddr = crypto.PubkeyToAddress(testKey.PublicKey) + testBalance = big.NewInt(2e15) + revertContractAddr = common.HexToAddress("290f1b36649a61e369c6276f6d29463335b4400c") + revertCode = common.FromHex("7f08c379a0000000000000000000000000000000000000000000000000000000006000526020600452600a6024527f75736572206572726f7200000000000000000000000000000000000000000000604452604e6000fd") ) var genesis = &core.Genesis{ - Config: params.AllEthashProtocolChanges, - Alloc: types.GenesisAlloc{testAddr: {Balance: testBalance}}, + Config: params.AllEthashProtocolChanges, + Alloc: types.GenesisAlloc{ + testAddr: {Balance: testBalance}, + revertContractAddr: {Code: revertCode}, + }, ExtraData: []byte("test genesis"), Timestamp: 9000, BaseFee: big.NewInt(params.InitialBaseFee), @@ -209,27 +215,30 @@ var testTx2 = types.MustSignNewTx(testKey, types.LatestSigner(genesis.Config), & To: &common.Address{2}, }) -func newTestBackend(t *testing.T) (*node.Node, []*types.Block) { +func newTestBackend(config *node.Config) (*node.Node, []*types.Block, error) { // Generate test chain. blocks := generateTestChain() // Create node - n, err := node.New(&node.Config{}) + if config == nil { + config = new(node.Config) + } + n, err := node.New(config) if err != nil { - t.Fatalf("can't create new node: %v", err) + return nil, nil, fmt.Errorf("can't create new node: %v", err) } // Create Ethereum Service - config := ðconfig.Config{Genesis: genesis, RPCGasCap: 1000000} - ethservice, err := eth.New(n, config) + ecfg := ðconfig.Config{Genesis: genesis, RPCGasCap: 1000000} + ethservice, err := eth.New(n, ecfg) if err != nil { - t.Fatalf("can't create new ethereum service: %v", err) + return nil, nil, fmt.Errorf("can't create new ethereum service: %v", err) } // Import the test chain. if err := n.Start(); err != nil { - t.Fatalf("can't start test node: %v", err) + return nil, nil, fmt.Errorf("can't start test node: %v", err) } if _, err := ethservice.BlockChain().InsertChain(blocks[1:]); err != nil { - t.Fatalf("can't import test blocks: %v", err) + return nil, nil, fmt.Errorf("can't import test blocks: %v", err) } // Ensure the tx indexing is fully generated for ; ; time.Sleep(time.Millisecond * 100) { @@ -238,7 +247,7 @@ func newTestBackend(t *testing.T) (*node.Node, []*types.Block) { break } } - return n, blocks + return n, blocks, nil } func generateTestChain() []*types.Block { @@ -256,7 +265,10 @@ func generateTestChain() []*types.Block { } func TestEthClient(t *testing.T) { - backend, chain := newTestBackend(t) + backend, chain, err := newTestBackend(nil) + if err != nil { + t.Fatal(err) + } client := backend.Attach() defer backend.Close() defer client.Close() diff --git a/ethclient/example_test.go b/ethclient/example_test.go new file mode 100644 index 0000000000..b0e8477b24 --- /dev/null +++ b/ethclient/example_test.go @@ -0,0 +1,75 @@ +// Copyright 2024 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package ethclient + +import ( + "context" + "fmt" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/node" +) + +var exampleNode *node.Node + +// launch example server +func init() { + config := &node.Config{ + HTTPHost: "127.0.0.1", + } + n, _, err := newTestBackend(config) + if err != nil { + panic("can't launch node: " + err.Error()) + } + exampleNode = n +} + +// Here we show how to get the error message of reverted contract call. +func ExampleRevertErrorData() { + // First create an ethclient.Client instance. + ctx := context.Background() + ec, _ := DialContext(ctx, exampleNode.HTTPEndpoint()) + + // Call the contract. + // Note we expect the call to return an error. + contract := common.HexToAddress("290f1b36649a61e369c6276f6d29463335b4400c") + call := ethereum.CallMsg{To: &contract, Gas: 30000} + result, err := ec.CallContract(ctx, call, nil) + if len(result) > 0 { + panic("got result") + } + if err == nil { + panic("call did not return error") + } + + // Extract the low-level revert data from the error. + revertData, ok := RevertErrorData(err) + if !ok { + panic("unpacking revert failed") + } + fmt.Printf("revert: %x\n", revertData) + + // Parse the revert data to obtain the error message. + message, err := abi.UnpackRevert(revertData) + fmt.Println("message:", message) + + // Output: + // revert: 08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000a75736572206572726f72 + // message: user error +}