diff --git a/ethclient/ethclient.go b/ethclient/ethclient.go
index 80240dcb9e..9f68323134 100644
--- a/ethclient/ethclient.go
+++ b/ethclient/ethclient.go
@@ -284,17 +284,6 @@ func (ec *Client) TransactionReceipt(ctx context.Context, txHash common.Hash) (*
return r, err
}
-func toBlockNumArg(number *big.Int) string {
- if number == nil {
- return "latest"
- }
- pending := big.NewInt(-1)
- if number.Cmp(pending) == 0 {
- return "pending"
- }
- return hexutil.EncodeBig(number)
-}
-
type rpcProgress struct {
StartingBlock hexutil.Uint64
CurrentBlock hexutil.Uint64
@@ -462,8 +451,6 @@ func (ec *Client) PendingTransactionCount(ctx context.Context) (uint, error) {
return uint(num), err
}
-// TODO: SubscribePendingTransactions (needs server side)
-
// Contract Calling
// CallContract executes a message call transaction, which is directly executed in the VM
@@ -537,6 +524,17 @@ func (ec *Client) SendTransaction(ctx context.Context, tx *types.Transaction) er
return ec.c.CallContext(ctx, nil, "eth_sendRawTransaction", hexutil.Encode(data))
}
+func toBlockNumArg(number *big.Int) string {
+ if number == nil {
+ return "latest"
+ }
+ pending := big.NewInt(-1)
+ if number.Cmp(pending) == 0 {
+ return "pending"
+ }
+ return hexutil.EncodeBig(number)
+}
+
func toCallArg(msg ethereum.CallMsg) interface{} {
arg := map[string]interface{}{
"from": msg.From,
diff --git a/ethclient/gethclient/gethclient.go b/ethclient/gethclient/gethclient.go
new file mode 100644
index 0000000000..538e23727d
--- /dev/null
+++ b/ethclient/gethclient/gethclient.go
@@ -0,0 +1,235 @@
+// Copyright 2021 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 gethclient provides an RPC client for geth-specific APIs.
+package gethclient
+
+import (
+ "context"
+ "math/big"
+ "runtime"
+ "runtime/debug"
+
+ "github.com/ethereum/go-ethereum"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/p2p"
+ "github.com/ethereum/go-ethereum/rpc"
+)
+
+// Client is a wrapper around rpc.Client that implements geth-specific functionality.
+//
+// If you want to use the standardized Ethereum RPC functionality, use ethclient.Client instead.
+type Client struct {
+ c *rpc.Client
+}
+
+// New creates a client that uses the given RPC client.
+func New(c *rpc.Client) *Client {
+ return &Client{c}
+}
+
+// CreateAccessList tries to create an access list for a specific transaction based on the
+// current pending state of the blockchain.
+func (ec *Client) CreateAccessList(ctx context.Context, msg ethereum.CallMsg) (*types.AccessList, uint64, string, error) {
+ type accessListResult struct {
+ Accesslist *types.AccessList `json:"accessList"`
+ Error string `json:"error,omitempty"`
+ GasUsed hexutil.Uint64 `json:"gasUsed"`
+ }
+ var result accessListResult
+ if err := ec.c.CallContext(ctx, &result, "eth_createAccessList", toCallArg(msg)); err != nil {
+ return nil, 0, "", err
+ }
+ return result.Accesslist, uint64(result.GasUsed), result.Error, nil
+}
+
+// AccountResult is the result of a GetProof operation.
+type AccountResult struct {
+ Address common.Address `json:"address"`
+ AccountProof []string `json:"accountProof"`
+ Balance *big.Int `json:"balance"`
+ CodeHash common.Hash `json:"codeHash"`
+ Nonce uint64 `json:"nonce"`
+ StorageHash common.Hash `json:"storageHash"`
+ StorageProof []StorageResult `json:"storageProof"`
+}
+
+// StorageResult provides a proof for a key-value pair.
+type StorageResult struct {
+ Key string `json:"key"`
+ Value *big.Int `json:"value"`
+ Proof []string `json:"proof"`
+}
+
+// GetProof returns the account and storage values of the specified account including the Merkle-proof.
+// The block number can be nil, in which case the value is taken from the latest known block.
+func (ec *Client) GetProof(ctx context.Context, account common.Address, keys []string, blockNumber *big.Int) (*AccountResult, error) {
+
+ type storageResult struct {
+ Key string `json:"key"`
+ Value *hexutil.Big `json:"value"`
+ Proof []string `json:"proof"`
+ }
+
+ type accountResult struct {
+ Address common.Address `json:"address"`
+ AccountProof []string `json:"accountProof"`
+ Balance *hexutil.Big `json:"balance"`
+ CodeHash common.Hash `json:"codeHash"`
+ Nonce hexutil.Uint64 `json:"nonce"`
+ StorageHash common.Hash `json:"storageHash"`
+ StorageProof []storageResult `json:"storageProof"`
+ }
+
+ var res accountResult
+ err := ec.c.CallContext(ctx, &res, "eth_getProof", account, keys, toBlockNumArg(blockNumber))
+ // Turn hexutils back to normal datatypes
+ storageResults := make([]StorageResult, 0, len(res.StorageProof))
+ for _, st := range res.StorageProof {
+ storageResults = append(storageResults, StorageResult{
+ Key: st.Key,
+ Value: st.Value.ToInt(),
+ Proof: st.Proof,
+ })
+ }
+ result := AccountResult{
+ Address: res.Address,
+ AccountProof: res.AccountProof,
+ Balance: res.Balance.ToInt(),
+ Nonce: uint64(res.Nonce),
+ CodeHash: res.CodeHash,
+ StorageHash: res.StorageHash,
+ }
+ return &result, err
+}
+
+// OverrideAccount specifies the state of an account to be overridden.
+type OverrideAccount struct {
+ Nonce uint64 `json:"nonce"`
+ Code []byte `json:"code"`
+ Balance *big.Int `json:"balance"`
+ State map[common.Hash]common.Hash `json:"state"`
+ StateDiff map[common.Hash]common.Hash `json:"stateDiff"`
+}
+
+// CallContract executes a message call transaction, which is directly executed in the VM
+// of the node, but never mined into the blockchain.
+//
+// blockNumber selects the block height at which the call runs. It can be nil, in which
+// case the code is taken from the latest known block. Note that state from very old
+// blocks might not be available.
+//
+// overrides specifies a map of contract states that should be overwritten before executing
+// the message call.
+// Please use ethclient.CallContract instead if you don't need the override functionality.
+func (ec *Client) CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int, overrides *map[common.Address]OverrideAccount) ([]byte, error) {
+ var hex hexutil.Bytes
+ err := ec.c.CallContext(
+ ctx, &hex, "eth_call", toCallArg(msg),
+ toBlockNumArg(blockNumber), toOverrideMap(overrides),
+ )
+ return hex, err
+}
+
+// GCStats retrieves the current garbage collection stats from a geth node.
+func (ec *Client) GCStats(ctx context.Context) (*debug.GCStats, error) {
+ var result debug.GCStats
+ err := ec.c.CallContext(ctx, &result, "debug_gcStats")
+ return &result, err
+}
+
+// MemStats retrieves the current memory stats from a geth node.
+func (ec *Client) MemStats(ctx context.Context) (*runtime.MemStats, error) {
+ var result runtime.MemStats
+ err := ec.c.CallContext(ctx, &result, "debug_memStats")
+ return &result, err
+}
+
+// SetHead sets the current head of the local chain by block number.
+// Note, this is a destructive action and may severely damage your chain.
+// Use with extreme caution.
+func (ec *Client) SetHead(ctx context.Context, number *big.Int) error {
+ return ec.c.CallContext(ctx, nil, "debug_setHead", toBlockNumArg(number))
+}
+
+// GetNodeInfo retrieves the node info of a geth node.
+func (ec *Client) GetNodeInfo(ctx context.Context) (*p2p.NodeInfo, error) {
+ var result p2p.NodeInfo
+ err := ec.c.CallContext(ctx, &result, "admin_nodeInfo")
+ return &result, err
+}
+
+// SubscribePendingTransactions subscribes to new pending transactions.
+func (ec *Client) SubscribePendingTransactions(ctx context.Context, ch chan<- common.Hash) (*rpc.ClientSubscription, error) {
+ return ec.c.EthSubscribe(ctx, ch, "newPendingTransactions")
+}
+
+func toBlockNumArg(number *big.Int) string {
+ if number == nil {
+ return "latest"
+ }
+ pending := big.NewInt(-1)
+ if number.Cmp(pending) == 0 {
+ return "pending"
+ }
+ return hexutil.EncodeBig(number)
+}
+
+func toCallArg(msg ethereum.CallMsg) interface{} {
+ arg := map[string]interface{}{
+ "from": msg.From,
+ "to": msg.To,
+ }
+ if len(msg.Data) > 0 {
+ arg["data"] = hexutil.Bytes(msg.Data)
+ }
+ if msg.Value != nil {
+ arg["value"] = (*hexutil.Big)(msg.Value)
+ }
+ if msg.Gas != 0 {
+ arg["gas"] = hexutil.Uint64(msg.Gas)
+ }
+ if msg.GasPrice != nil {
+ arg["gasPrice"] = (*hexutil.Big)(msg.GasPrice)
+ }
+ return arg
+}
+
+func toOverrideMap(overrides *map[common.Address]OverrideAccount) interface{} {
+ if overrides == nil {
+ return nil
+ }
+ type overrideAccount struct {
+ Nonce hexutil.Uint64 `json:"nonce"`
+ Code hexutil.Bytes `json:"code"`
+ Balance *hexutil.Big `json:"balance"`
+ State map[common.Hash]common.Hash `json:"state"`
+ StateDiff map[common.Hash]common.Hash `json:"stateDiff"`
+ }
+ result := make(map[common.Address]overrideAccount)
+ for addr, override := range *overrides {
+ result[addr] = overrideAccount{
+ Nonce: hexutil.Uint64(override.Nonce),
+ Code: override.Code,
+ Balance: (*hexutil.Big)(override.Balance),
+ State: override.State,
+ StateDiff: override.StateDiff,
+ }
+ }
+ return &result
+}
diff --git a/ethclient/gethclient/gethclient_test.go b/ethclient/gethclient/gethclient_test.go
new file mode 100644
index 0000000000..26970277c3
--- /dev/null
+++ b/ethclient/gethclient/gethclient_test.go
@@ -0,0 +1,305 @@
+// Copyright 2021 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 gethclient
+
+import (
+ "bytes"
+ "context"
+ "math/big"
+ "testing"
+
+ "github.com/ethereum/go-ethereum"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/consensus/ethash"
+ "github.com/ethereum/go-ethereum/core"
+ "github.com/ethereum/go-ethereum/core/rawdb"
+ "github.com/ethereum/go-ethereum/core/types"
+ "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"
+)
+
+var (
+ testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
+ testAddr = crypto.PubkeyToAddress(testKey.PublicKey)
+ testBalance = big.NewInt(2e10)
+)
+
+func newTestBackend(t *testing.T) (*node.Node, []*types.Block) {
+ // Generate test chain.
+ genesis, blocks := generateTestChain()
+ // Create node
+ n, err := node.New(&node.Config{})
+ if err != nil {
+ t.Fatalf("can't create new node: %v", err)
+ }
+ // Create Ethereum Service
+ config := ðconfig.Config{Genesis: genesis}
+ config.Ethash.PowMode = ethash.ModeFake
+ ethservice, err := eth.New(n, config)
+ if err != nil {
+ t.Fatalf("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)
+ }
+ if _, err := ethservice.BlockChain().InsertChain(blocks[1:]); err != nil {
+ t.Fatalf("can't import test blocks: %v", err)
+ }
+ return n, blocks
+}
+
+func generateTestChain() (*core.Genesis, []*types.Block) {
+ db := rawdb.NewMemoryDatabase()
+ config := params.AllEthashProtocolChanges
+ genesis := &core.Genesis{
+ Config: config,
+ Alloc: core.GenesisAlloc{testAddr: {Balance: testBalance}},
+ ExtraData: []byte("test genesis"),
+ Timestamp: 9000,
+ }
+ generate := func(i int, g *core.BlockGen) {
+ g.OffsetTime(5)
+ g.SetExtra([]byte("test"))
+ }
+ gblock := genesis.ToBlock(db)
+ engine := ethash.NewFaker()
+ blocks, _ := core.GenerateChain(config, gblock, engine, db, 1, generate)
+ blocks = append([]*types.Block{gblock}, blocks...)
+ return genesis, blocks
+}
+
+func TestEthClient(t *testing.T) {
+ backend, _ := newTestBackend(t)
+ client, err := backend.Attach()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer backend.Close()
+ defer client.Close()
+
+ tests := map[string]struct {
+ test func(t *testing.T)
+ }{
+ "TestAccessList": {
+ func(t *testing.T) { testAccessList(t, client) },
+ },
+ "TestGetProof": {
+ func(t *testing.T) { testGetProof(t, client) },
+ },
+ "TestGCStats": {
+ func(t *testing.T) { testGCStats(t, client) },
+ },
+ "TestMemStats": {
+ func(t *testing.T) { testMemStats(t, client) },
+ },
+ "TestGetNodeInfo": {
+ func(t *testing.T) { testGetNodeInfo(t, client) },
+ },
+ "TestSetHead": {
+ func(t *testing.T) { testSetHead(t, client) },
+ },
+ "TestSubscribePendingTxs": {
+ func(t *testing.T) { testSubscribePendingTransactions(t, client) },
+ },
+ "TestCallContract": {
+ func(t *testing.T) { testCallContract(t, client) },
+ },
+ }
+ t.Parallel()
+ for name, tt := range tests {
+ t.Run(name, tt.test)
+ }
+}
+
+func testAccessList(t *testing.T, client *rpc.Client) {
+ ec := New(client)
+ // Test transfer
+ msg := ethereum.CallMsg{
+ From: testAddr,
+ To: &common.Address{},
+ Gas: 21000,
+ GasPrice: big.NewInt(1),
+ Value: big.NewInt(1),
+ }
+ al, gas, vmErr, err := ec.CreateAccessList(context.Background(), msg)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if vmErr != "" {
+ t.Fatalf("unexpected vm error: %v", vmErr)
+ }
+ if gas != 21000 {
+ t.Fatalf("unexpected gas used: %v", gas)
+ }
+ if len(*al) != 0 {
+ t.Fatalf("unexpected length of accesslist: %v", len(*al))
+ }
+ // Test reverting transaction
+ msg = ethereum.CallMsg{
+ From: testAddr,
+ To: nil,
+ Gas: 100000,
+ GasPrice: big.NewInt(1),
+ Value: big.NewInt(1),
+ Data: common.FromHex("0x608060806080608155fd"),
+ }
+ al, gas, vmErr, err = ec.CreateAccessList(context.Background(), msg)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if vmErr == "" {
+ t.Fatalf("wanted vmErr, got none")
+ }
+ if gas == 21000 {
+ t.Fatalf("unexpected gas used: %v", gas)
+ }
+ if len(*al) != 1 || al.StorageKeys() != 1 {
+ t.Fatalf("unexpected length of accesslist: %v", len(*al))
+ }
+ // address changes between calls, so we can't test for it.
+ if (*al)[0].Address == common.HexToAddress("0x0") {
+ t.Fatalf("unexpected address: %v", (*al)[0].Address)
+ }
+ if (*al)[0].StorageKeys[0] != common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000081") {
+ t.Fatalf("unexpected storage key: %v", (*al)[0].StorageKeys[0])
+ }
+}
+
+func testGetProof(t *testing.T, client *rpc.Client) {
+ ec := New(client)
+ ethcl := ethclient.NewClient(client)
+ result, err := ec.GetProof(context.Background(), testAddr, []string{}, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(result.Address[:], testAddr[:]) {
+ t.Fatalf("unexpected address, want: %v got: %v", testAddr, result.Address)
+ }
+ // test nonce
+ nonce, _ := ethcl.NonceAt(context.Background(), result.Address, nil)
+ if result.Nonce != nonce {
+ t.Fatalf("invalid nonce, want: %v got: %v", nonce, result.Nonce)
+ }
+ // test balance
+ balance, _ := ethcl.BalanceAt(context.Background(), result.Address, nil)
+ if result.Balance.Cmp(balance) != 0 {
+ t.Fatalf("invalid balance, want: %v got: %v", balance, result.Balance)
+ }
+}
+
+func testGCStats(t *testing.T, client *rpc.Client) {
+ ec := New(client)
+ _, err := ec.GCStats(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func testMemStats(t *testing.T, client *rpc.Client) {
+ ec := New(client)
+ stats, err := ec.MemStats(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if stats.Alloc == 0 {
+ t.Fatal("Invalid mem stats retrieved")
+ }
+}
+
+func testGetNodeInfo(t *testing.T, client *rpc.Client) {
+ ec := New(client)
+ info, err := ec.GetNodeInfo(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if info.Name == "" {
+ t.Fatal("Invalid node info retrieved")
+ }
+}
+
+func testSetHead(t *testing.T, client *rpc.Client) {
+ ec := New(client)
+ err := ec.SetHead(context.Background(), big.NewInt(0))
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func testSubscribePendingTransactions(t *testing.T, client *rpc.Client) {
+ ec := New(client)
+ ethcl := ethclient.NewClient(client)
+ // Subscribe to Transactions
+ ch := make(chan common.Hash)
+ ec.SubscribePendingTransactions(context.Background(), ch)
+ // Send a transaction
+ chainID, err := ethcl.ChainID(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Create transaction
+ tx := types.NewTransaction(0, common.Address{1}, big.NewInt(1), 22000, big.NewInt(1), nil)
+ signer := types.LatestSignerForChainID(chainID)
+ signature, err := crypto.Sign(signer.Hash(tx).Bytes(), testKey)
+ if err != nil {
+ t.Fatal(err)
+ }
+ signedTx, err := tx.WithSignature(signer, signature)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Send transaction
+ err = ethcl.SendTransaction(context.Background(), signedTx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Check that the transaction was send over the channel
+ hash := <-ch
+ if hash != signedTx.Hash() {
+ t.Fatalf("Invalid tx hash received, got %v, want %v", hash, signedTx.Hash())
+ }
+}
+
+func testCallContract(t *testing.T, client *rpc.Client) {
+ ec := New(client)
+ msg := ethereum.CallMsg{
+ From: testAddr,
+ To: &common.Address{},
+ Gas: 21000,
+ GasPrice: big.NewInt(1),
+ Value: big.NewInt(1),
+ }
+ // CallContract without override
+ if _, err := ec.CallContract(context.Background(), msg, big.NewInt(0), nil); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ // CallContract with override
+ override := OverrideAccount{
+ Nonce: 1,
+ }
+ mapAcc := make(map[common.Address]OverrideAccount)
+ mapAcc[testAddr] = override
+ if _, err := ec.CallContract(context.Background(), msg, big.NewInt(0), &mapAcc); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+}