diff --git a/ethclient/ethclient.go b/ethclient/ethclient.go
index 0972644d80..f10626c01f 100644
--- a/ethclient/ethclient.go
+++ b/ethclient/ethclient.go
@@ -630,6 +630,23 @@ 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 {
+ if eds, ok := ed.ErrorData().(string); 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..4ad8a552d2 100644
--- a/ethclient/ethclient_test.go
+++ b/ethclient/ethclient_test.go
@@ -14,18 +14,20 @@
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see .
-package ethclient
+package ethclient_test
import (
"bytes"
"context"
"errors"
+ "fmt"
"math/big"
"reflect"
"testing"
"time"
"github.com/ethereum/go-ethereum"
+ "github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus/ethash"
"github.com/ethereum/go-ethereum/core"
@@ -33,6 +35,7 @@ import (
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/eth/ethconfig"
+ "github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc"
@@ -40,154 +43,33 @@ import (
// Verify that Client implements the ethereum interfaces.
var (
- _ = ethereum.ChainReader(&Client{})
- _ = ethereum.TransactionReader(&Client{})
- _ = ethereum.ChainStateReader(&Client{})
- _ = ethereum.ChainSyncReader(&Client{})
- _ = ethereum.ContractCaller(&Client{})
- _ = ethereum.GasEstimator(&Client{})
- _ = ethereum.GasPricer(&Client{})
- _ = ethereum.LogFilterer(&Client{})
- _ = ethereum.PendingStateReader(&Client{})
- // _ = ethereum.PendingStateEventer(&Client{})
- _ = ethereum.PendingContractCaller(&Client{})
+ _ = ethereum.ChainReader(ðclient.Client{})
+ _ = ethereum.TransactionReader(ðclient.Client{})
+ _ = ethereum.ChainStateReader(ðclient.Client{})
+ _ = ethereum.ChainSyncReader(ðclient.Client{})
+ _ = ethereum.ContractCaller(ðclient.Client{})
+ _ = ethereum.GasEstimator(ðclient.Client{})
+ _ = ethereum.GasPricer(ðclient.Client{})
+ _ = ethereum.LogFilterer(ðclient.Client{})
+ _ = ethereum.PendingStateReader(ðclient.Client{})
+ // _ = ethereum.PendingStateEventer(ðclient.Client{})
+ _ = ethereum.PendingContractCaller(ðclient.Client{})
)
-func TestToFilterArg(t *testing.T) {
- blockHashErr := errors.New("cannot specify both BlockHash and FromBlock/ToBlock")
- addresses := []common.Address{
- common.HexToAddress("0xD36722ADeC3EdCB29c8e7b5a47f352D701393462"),
- }
- blockHash := common.HexToHash(
- "0xeb94bb7d78b73657a9d7a99792413f50c0a45c51fc62bdcb08a53f18e9a2b4eb",
- )
-
- for _, testCase := range []struct {
- name string
- input ethereum.FilterQuery
- output interface{}
- err error
- }{
- {
- "without BlockHash",
- ethereum.FilterQuery{
- Addresses: addresses,
- FromBlock: big.NewInt(1),
- ToBlock: big.NewInt(2),
- Topics: [][]common.Hash{},
- },
- map[string]interface{}{
- "address": addresses,
- "fromBlock": "0x1",
- "toBlock": "0x2",
- "topics": [][]common.Hash{},
- },
- nil,
- },
- {
- "with nil fromBlock and nil toBlock",
- ethereum.FilterQuery{
- Addresses: addresses,
- Topics: [][]common.Hash{},
- },
- map[string]interface{}{
- "address": addresses,
- "fromBlock": "0x0",
- "toBlock": "latest",
- "topics": [][]common.Hash{},
- },
- nil,
- },
- {
- "with negative fromBlock and negative toBlock",
- ethereum.FilterQuery{
- Addresses: addresses,
- FromBlock: big.NewInt(-1),
- ToBlock: big.NewInt(-1),
- Topics: [][]common.Hash{},
- },
- map[string]interface{}{
- "address": addresses,
- "fromBlock": "pending",
- "toBlock": "pending",
- "topics": [][]common.Hash{},
- },
- nil,
- },
- {
- "with blockhash",
- ethereum.FilterQuery{
- Addresses: addresses,
- BlockHash: &blockHash,
- Topics: [][]common.Hash{},
- },
- map[string]interface{}{
- "address": addresses,
- "blockHash": blockHash,
- "topics": [][]common.Hash{},
- },
- nil,
- },
- {
- "with blockhash and from block",
- ethereum.FilterQuery{
- Addresses: addresses,
- BlockHash: &blockHash,
- FromBlock: big.NewInt(1),
- Topics: [][]common.Hash{},
- },
- nil,
- blockHashErr,
- },
- {
- "with blockhash and to block",
- ethereum.FilterQuery{
- Addresses: addresses,
- BlockHash: &blockHash,
- ToBlock: big.NewInt(1),
- Topics: [][]common.Hash{},
- },
- nil,
- blockHashErr,
- },
- {
- "with blockhash and both from / to block",
- ethereum.FilterQuery{
- Addresses: addresses,
- BlockHash: &blockHash,
- FromBlock: big.NewInt(1),
- ToBlock: big.NewInt(2),
- Topics: [][]common.Hash{},
- },
- nil,
- blockHashErr,
- },
- } {
- t.Run(testCase.name, func(t *testing.T) {
- output, err := toFilterArg(testCase.input)
- if (testCase.err == nil) != (err == nil) {
- t.Fatalf("expected error %v but got %v", testCase.err, err)
- }
- if testCase.err != nil {
- if testCase.err.Error() != err.Error() {
- t.Fatalf("expected error %v but got %v", testCase.err, err)
- }
- } else if !reflect.DeepEqual(testCase.output, output) {
- t.Fatalf("expected filter arg %v but got %v", testCase.output, output)
- }
- })
- }
-}
-
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 +91,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 +123,7 @@ func newTestBackend(t *testing.T) (*node.Node, []*types.Block) {
break
}
}
- return n, blocks
+ return n, blocks, nil
}
func generateTestChain() []*types.Block {
@@ -256,7 +141,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()
@@ -324,7 +212,7 @@ func testHeader(t *testing.T, chain []*types.Block, client *rpc.Client) {
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
- ec := NewClient(client)
+ ec := ethclient.NewClient(client)
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
@@ -373,7 +261,7 @@ func testBalanceAt(t *testing.T, client *rpc.Client) {
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
- ec := NewClient(client)
+ ec := ethclient.NewClient(client)
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
@@ -389,7 +277,7 @@ func testBalanceAt(t *testing.T, client *rpc.Client) {
}
func testTransactionInBlock(t *testing.T, client *rpc.Client) {
- ec := NewClient(client)
+ ec := ethclient.NewClient(client)
// Get current block by number.
block, err := ec.BlockByNumber(context.Background(), nil)
@@ -421,7 +309,7 @@ func testTransactionInBlock(t *testing.T, client *rpc.Client) {
}
func testChainID(t *testing.T, client *rpc.Client) {
- ec := NewClient(client)
+ ec := ethclient.NewClient(client)
id, err := ec.ChainID(context.Background())
if err != nil {
t.Fatalf("unexpected error: %v", err)
@@ -432,7 +320,7 @@ func testChainID(t *testing.T, client *rpc.Client) {
}
func testGetBlock(t *testing.T, client *rpc.Client) {
- ec := NewClient(client)
+ ec := ethclient.NewClient(client)
// Get current block number
blockNumber, err := ec.BlockNumber(context.Background())
@@ -477,7 +365,7 @@ func testGetBlock(t *testing.T, client *rpc.Client) {
}
func testStatusFunctions(t *testing.T, client *rpc.Client) {
- ec := NewClient(client)
+ ec := ethclient.NewClient(client)
// Sync progress
progress, err := ec.SyncProgress(context.Background())
@@ -540,7 +428,7 @@ func testStatusFunctions(t *testing.T, client *rpc.Client) {
}
func testCallContractAtHash(t *testing.T, client *rpc.Client) {
- ec := NewClient(client)
+ ec := ethclient.NewClient(client)
// EstimateGas
msg := ethereum.CallMsg{
@@ -567,7 +455,7 @@ func testCallContractAtHash(t *testing.T, client *rpc.Client) {
}
func testCallContract(t *testing.T, client *rpc.Client) {
- ec := NewClient(client)
+ ec := ethclient.NewClient(client)
// EstimateGas
msg := ethereum.CallMsg{
@@ -594,7 +482,7 @@ func testCallContract(t *testing.T, client *rpc.Client) {
}
func testAtFunctions(t *testing.T, client *rpc.Client) {
- ec := NewClient(client)
+ ec := ethclient.NewClient(client)
block, err := ec.HeaderByNumber(context.Background(), big.NewInt(1))
if err != nil {
@@ -697,7 +585,7 @@ func testAtFunctions(t *testing.T, client *rpc.Client) {
}
func testTransactionSender(t *testing.T, client *rpc.Client) {
- ec := NewClient(client)
+ ec := ethclient.NewClient(client)
ctx := context.Background()
// Retrieve testTx1 via RPC.
@@ -737,7 +625,7 @@ func testTransactionSender(t *testing.T, client *rpc.Client) {
}
}
-func sendTransaction(ec *Client) error {
+func sendTransaction(ec *ethclient.Client) error {
chainID, err := ec.ChainID(context.Background())
if err != nil {
return err
@@ -760,3 +648,40 @@ func sendTransaction(ec *Client) error {
}
return ec.SendTransaction(context.Background(), tx)
}
+
+// 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, _ := ethclient.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 := ethclient.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)
+ if err != nil {
+ panic("parsing ABI error failed: " + err.Error())
+ }
+ fmt.Println("message:", message)
+
+ // Output:
+ // revert: 08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000a75736572206572726f72
+ // message: user error
+}
diff --git a/ethclient/example_test.go b/ethclient/example_test.go
new file mode 100644
index 0000000000..5d0038f0c7
--- /dev/null
+++ b/ethclient/example_test.go
@@ -0,0 +1,35 @@
+// 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_test
+
+import (
+ "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
+}
diff --git a/ethclient/types_test.go b/ethclient/types_test.go
new file mode 100644
index 0000000000..02f9f21758
--- /dev/null
+++ b/ethclient/types_test.go
@@ -0,0 +1,153 @@
+// Copyright 2016 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 (
+ "errors"
+ "math/big"
+ "reflect"
+ "testing"
+
+ "github.com/ethereum/go-ethereum"
+ "github.com/ethereum/go-ethereum/common"
+)
+
+func TestToFilterArg(t *testing.T) {
+ blockHashErr := errors.New("cannot specify both BlockHash and FromBlock/ToBlock")
+ addresses := []common.Address{
+ common.HexToAddress("0xD36722ADeC3EdCB29c8e7b5a47f352D701393462"),
+ }
+ blockHash := common.HexToHash(
+ "0xeb94bb7d78b73657a9d7a99792413f50c0a45c51fc62bdcb08a53f18e9a2b4eb",
+ )
+
+ for _, testCase := range []struct {
+ name string
+ input ethereum.FilterQuery
+ output interface{}
+ err error
+ }{
+ {
+ "without BlockHash",
+ ethereum.FilterQuery{
+ Addresses: addresses,
+ FromBlock: big.NewInt(1),
+ ToBlock: big.NewInt(2),
+ Topics: [][]common.Hash{},
+ },
+ map[string]interface{}{
+ "address": addresses,
+ "fromBlock": "0x1",
+ "toBlock": "0x2",
+ "topics": [][]common.Hash{},
+ },
+ nil,
+ },
+ {
+ "with nil fromBlock and nil toBlock",
+ ethereum.FilterQuery{
+ Addresses: addresses,
+ Topics: [][]common.Hash{},
+ },
+ map[string]interface{}{
+ "address": addresses,
+ "fromBlock": "0x0",
+ "toBlock": "latest",
+ "topics": [][]common.Hash{},
+ },
+ nil,
+ },
+ {
+ "with negative fromBlock and negative toBlock",
+ ethereum.FilterQuery{
+ Addresses: addresses,
+ FromBlock: big.NewInt(-1),
+ ToBlock: big.NewInt(-1),
+ Topics: [][]common.Hash{},
+ },
+ map[string]interface{}{
+ "address": addresses,
+ "fromBlock": "pending",
+ "toBlock": "pending",
+ "topics": [][]common.Hash{},
+ },
+ nil,
+ },
+ {
+ "with blockhash",
+ ethereum.FilterQuery{
+ Addresses: addresses,
+ BlockHash: &blockHash,
+ Topics: [][]common.Hash{},
+ },
+ map[string]interface{}{
+ "address": addresses,
+ "blockHash": blockHash,
+ "topics": [][]common.Hash{},
+ },
+ nil,
+ },
+ {
+ "with blockhash and from block",
+ ethereum.FilterQuery{
+ Addresses: addresses,
+ BlockHash: &blockHash,
+ FromBlock: big.NewInt(1),
+ Topics: [][]common.Hash{},
+ },
+ nil,
+ blockHashErr,
+ },
+ {
+ "with blockhash and to block",
+ ethereum.FilterQuery{
+ Addresses: addresses,
+ BlockHash: &blockHash,
+ ToBlock: big.NewInt(1),
+ Topics: [][]common.Hash{},
+ },
+ nil,
+ blockHashErr,
+ },
+ {
+ "with blockhash and both from / to block",
+ ethereum.FilterQuery{
+ Addresses: addresses,
+ BlockHash: &blockHash,
+ FromBlock: big.NewInt(1),
+ ToBlock: big.NewInt(2),
+ Topics: [][]common.Hash{},
+ },
+ nil,
+ blockHashErr,
+ },
+ } {
+ t.Run(testCase.name, func(t *testing.T) {
+ output, err := toFilterArg(testCase.input)
+ if (testCase.err == nil) != (err == nil) {
+ t.Fatalf("expected error %v but got %v", testCase.err, err)
+ }
+ if testCase.err != nil {
+ if testCase.err.Error() != err.Error() {
+ t.Fatalf("expected error %v but got %v", testCase.err, err)
+ }
+ } else if !reflect.DeepEqual(testCase.output, output) {
+ t.Fatalf("expected filter arg %v but got %v", testCase.output, output)
+ }
+ })
+ }
+}