github.com/ethersphere/bee/v2@v2.2.0/pkg/transaction/transaction_test.go (about)

     1  // Copyright 2020 The Swarm Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package transaction_test
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"errors"
    11  	"fmt"
    12  	"math/big"
    13  	"strings"
    14  	"testing"
    15  	"time"
    16  
    17  	"github.com/ethereum/go-ethereum"
    18  	"github.com/ethereum/go-ethereum/common"
    19  	"github.com/ethereum/go-ethereum/core/types"
    20  	"github.com/ethereum/go-ethereum/rpc"
    21  	"github.com/ethersphere/bee/v2/pkg/crypto"
    22  	signermock "github.com/ethersphere/bee/v2/pkg/crypto/mock"
    23  	"github.com/ethersphere/bee/v2/pkg/log"
    24  	"github.com/ethersphere/bee/v2/pkg/sctx"
    25  	storemock "github.com/ethersphere/bee/v2/pkg/statestore/mock"
    26  	"github.com/ethersphere/bee/v2/pkg/transaction"
    27  	"github.com/ethersphere/bee/v2/pkg/transaction/backendmock"
    28  	"github.com/ethersphere/bee/v2/pkg/transaction/monitormock"
    29  	"github.com/ethersphere/bee/v2/pkg/util/abiutil"
    30  	"github.com/ethersphere/bee/v2/pkg/util/testutil"
    31  )
    32  
    33  func nonceKey(sender common.Address) string {
    34  	return fmt.Sprintf("transaction_nonce_%x", sender)
    35  }
    36  
    37  func signerMockForTransaction(t *testing.T, signedTx *types.Transaction, sender common.Address, signerChainID *big.Int) crypto.Signer {
    38  	t.Helper()
    39  	return signermock.New(
    40  		signermock.WithSignTxFunc(func(transaction *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
    41  			if transaction.Type() != 2 {
    42  				t.Fatalf("wrong transaction type. wanted 2, got %d", transaction.Type())
    43  			}
    44  			if signedTx.To() == nil {
    45  				if transaction.To() != nil {
    46  					t.Fatalf("signing transaction with recipient. wanted nil, got %x", transaction.To())
    47  				}
    48  			} else {
    49  				if transaction.To() == nil || *transaction.To() != *signedTx.To() {
    50  					t.Fatalf("signing transactiono with wrong recipient. wanted %x, got %x", signedTx.To(), transaction.To())
    51  				}
    52  			}
    53  			if !bytes.Equal(transaction.Data(), signedTx.Data()) {
    54  				t.Fatalf("signing transaction with wrong data. wanted %x, got %x", signedTx.Data(), transaction.Data())
    55  			}
    56  			if transaction.Value().Cmp(signedTx.Value()) != 0 {
    57  				t.Fatalf("signing transaction with wrong value. wanted %d, got %d", signedTx.Value(), transaction.Value())
    58  			}
    59  			if chainID.Cmp(signerChainID) != 0 {
    60  				t.Fatalf("signing transaction with wrong chainID. wanted %d, got %d", signerChainID, transaction.ChainId())
    61  			}
    62  			if transaction.Gas() != signedTx.Gas() {
    63  				t.Fatalf("signing transaction with wrong gas. wanted %d, got %d", signedTx.Gas(), transaction.Gas())
    64  			}
    65  			if transaction.GasPrice().Cmp(signedTx.GasPrice()) != 0 {
    66  				t.Fatalf("signing transaction with wrong gasprice. wanted %d, got %d", signedTx.GasPrice(), transaction.GasPrice())
    67  			}
    68  
    69  			if transaction.Nonce() != signedTx.Nonce() {
    70  				t.Fatalf("signing transaction with wrong nonce. wanted %d, got %d", signedTx.Nonce(), transaction.Nonce())
    71  			}
    72  
    73  			return signedTx, nil
    74  		}),
    75  		signermock.WithEthereumAddressFunc(func() (common.Address, error) {
    76  			return sender, nil
    77  		}),
    78  	)
    79  }
    80  
    81  func TestTransactionSend(t *testing.T) {
    82  	t.Parallel()
    83  
    84  	logger := log.Noop
    85  	sender := common.HexToAddress("0xddff")
    86  	recipient := common.HexToAddress("0xabcd")
    87  	txData := common.Hex2Bytes("0xabcdee")
    88  	value := big.NewInt(1)
    89  	suggestedGasPrice := big.NewInt(1000)
    90  	suggestedGasTip := big.NewInt(100)
    91  	defaultGasFee := big.NewInt(0).Add(suggestedGasPrice, suggestedGasTip)
    92  	estimatedGasLimit := uint64(3)
    93  	nonce := uint64(2)
    94  	chainID := big.NewInt(5)
    95  
    96  	t.Run("send", func(t *testing.T) {
    97  		t.Parallel()
    98  
    99  		signedTx := types.NewTx(&types.DynamicFeeTx{
   100  			ChainID:   chainID,
   101  			Nonce:     nonce,
   102  			To:        &recipient,
   103  			Value:     value,
   104  			Gas:       estimatedGasLimit,
   105  			GasFeeCap: defaultGasFee,
   106  			GasTipCap: suggestedGasTip,
   107  			Data:      txData,
   108  		})
   109  		request := &transaction.TxRequest{
   110  			To:    &recipient,
   111  			Data:  txData,
   112  			Value: value,
   113  		}
   114  		store := storemock.NewStateStore()
   115  		err := store.Put(nonceKey(sender), nonce)
   116  		if err != nil {
   117  			t.Fatal(err)
   118  		}
   119  
   120  		transactionService, err := transaction.NewService(logger, sender,
   121  			backendmock.New(
   122  				backendmock.WithSendTransactionFunc(func(ctx context.Context, tx *types.Transaction) error {
   123  					if tx != signedTx {
   124  						t.Fatal("not sending signed transaction")
   125  					}
   126  					return nil
   127  				}),
   128  				backendmock.WithEstimateGasFunc(func(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error) {
   129  					if !bytes.Equal(call.To.Bytes(), recipient.Bytes()) {
   130  						t.Fatalf("estimating with wrong recipient. wanted %x, got %x", recipient, call.To)
   131  					}
   132  					if !bytes.Equal(call.Data, txData) {
   133  						t.Fatal("estimating with wrong data")
   134  					}
   135  					return estimatedGasLimit, nil
   136  				}),
   137  				backendmock.WithSuggestGasPriceFunc(func(ctx context.Context) (*big.Int, error) {
   138  					return suggestedGasPrice, nil
   139  				}),
   140  				backendmock.WithPendingNonceAtFunc(func(ctx context.Context, account common.Address) (uint64, error) {
   141  					return nonce - 1, nil
   142  				}),
   143  				backendmock.WithSuggestGasTipCapFunc(func(ctx context.Context) (*big.Int, error) {
   144  					return suggestedGasTip, nil
   145  				}),
   146  			),
   147  			signerMockForTransaction(t, signedTx, sender, chainID),
   148  			store,
   149  			chainID,
   150  			monitormock.New(
   151  				monitormock.WithWatchTransactionFunc(func(txHash common.Hash, nonce uint64) (<-chan types.Receipt, <-chan error, error) {
   152  					return nil, nil, nil
   153  				}),
   154  			),
   155  		)
   156  		if err != nil {
   157  			t.Fatal(err)
   158  		}
   159  		testutil.CleanupCloser(t, transactionService)
   160  
   161  		txHash, err := transactionService.Send(context.Background(), request, 0)
   162  		if err != nil {
   163  			t.Fatal(err)
   164  		}
   165  
   166  		if !bytes.Equal(txHash.Bytes(), signedTx.Hash().Bytes()) {
   167  			t.Fatal("returning wrong transaction hash")
   168  		}
   169  
   170  		var storedNonce uint64
   171  		err = store.Get(nonceKey(sender), &storedNonce)
   172  		if err != nil {
   173  			t.Fatal(err)
   174  		}
   175  		if storedNonce != nonce+1 {
   176  			t.Fatalf("nonce not stored correctly: want %d, got %d", nonce+1, storedNonce)
   177  		}
   178  
   179  		storedTransaction, err := transactionService.StoredTransaction(txHash)
   180  		if err != nil {
   181  			t.Fatal(err)
   182  		}
   183  
   184  		if storedTransaction.To == nil || *storedTransaction.To != recipient {
   185  			t.Fatalf("got wrong recipient in stored transaction. wanted %x, got %x", recipient, storedTransaction.To)
   186  		}
   187  
   188  		if !bytes.Equal(storedTransaction.Data, request.Data) {
   189  			t.Fatalf("got wrong data in stored transaction. wanted %x, got %x", request.Data, storedTransaction.Data)
   190  		}
   191  
   192  		if storedTransaction.Description != request.Description {
   193  			t.Fatalf("got wrong description in stored transaction. wanted %x, got %x", request.Description, storedTransaction.Description)
   194  		}
   195  
   196  		if storedTransaction.GasLimit != estimatedGasLimit {
   197  			t.Fatalf("got wrong gas limit in stored transaction. wanted %d, got %d", estimatedGasLimit, storedTransaction.GasLimit)
   198  		}
   199  
   200  		if defaultGasFee.Cmp(storedTransaction.GasPrice) != 0 {
   201  			t.Fatalf("got wrong gas price in stored transaction. wanted %d, got %d", defaultGasFee, storedTransaction.GasPrice)
   202  		}
   203  
   204  		if storedTransaction.Nonce != nonce {
   205  			t.Fatalf("got wrong nonce in stored transaction. wanted %d, got %d", nonce, storedTransaction.Nonce)
   206  		}
   207  
   208  		pending, err := transactionService.PendingTransactions()
   209  		if err != nil {
   210  			t.Fatal(err)
   211  		}
   212  		if len(pending) != 1 {
   213  			t.Fatalf("expected one pending transaction, got %d", len(pending))
   214  		}
   215  
   216  		if pending[0] != txHash {
   217  			t.Fatalf("got wrong pending transaction. wanted %x, got %x", txHash, pending[0])
   218  		}
   219  	})
   220  
   221  	t.Run("send with estimate error", func(t *testing.T) {
   222  		t.Parallel()
   223  
   224  		signedTx := types.NewTx(&types.DynamicFeeTx{
   225  			ChainID:   chainID,
   226  			Nonce:     nonce,
   227  			To:        &recipient,
   228  			Value:     value,
   229  			Gas:       estimatedGasLimit,
   230  			GasFeeCap: defaultGasFee,
   231  			GasTipCap: suggestedGasTip,
   232  			Data:      txData,
   233  		})
   234  		request := &transaction.TxRequest{
   235  			To:                   &recipient,
   236  			Data:                 txData,
   237  			Value:                value,
   238  			MinEstimatedGasLimit: estimatedGasLimit,
   239  		}
   240  		store := storemock.NewStateStore()
   241  		err := store.Put(nonceKey(sender), nonce)
   242  		if err != nil {
   243  			t.Fatal(err)
   244  		}
   245  
   246  		transactionService, err := transaction.NewService(logger, sender,
   247  			backendmock.New(
   248  				backendmock.WithSendTransactionFunc(func(ctx context.Context, tx *types.Transaction) error {
   249  					if tx != signedTx {
   250  						t.Fatal("not sending signed transaction")
   251  					}
   252  					return nil
   253  				}),
   254  				backendmock.WithEstimateGasFunc(func(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error) {
   255  					return 0, errors.New("estimate failure")
   256  				}),
   257  				backendmock.WithSuggestGasPriceFunc(func(ctx context.Context) (*big.Int, error) {
   258  					return suggestedGasPrice, nil
   259  				}),
   260  				backendmock.WithPendingNonceAtFunc(func(ctx context.Context, account common.Address) (uint64, error) {
   261  					return nonce - 1, nil
   262  				}),
   263  				backendmock.WithSuggestGasTipCapFunc(func(ctx context.Context) (*big.Int, error) {
   264  					return suggestedGasTip, nil
   265  				}),
   266  			),
   267  			signerMockForTransaction(t, signedTx, sender, chainID),
   268  			store,
   269  			chainID,
   270  			monitormock.New(
   271  				monitormock.WithWatchTransactionFunc(func(txHash common.Hash, nonce uint64) (<-chan types.Receipt, <-chan error, error) {
   272  					return nil, nil, nil
   273  				}),
   274  			),
   275  		)
   276  		if err != nil {
   277  			t.Fatal(err)
   278  		}
   279  		testutil.CleanupCloser(t, transactionService)
   280  
   281  		txHash, err := transactionService.Send(context.Background(), request, 0)
   282  		if err != nil {
   283  			t.Fatal(err)
   284  		}
   285  
   286  		if !bytes.Equal(txHash.Bytes(), signedTx.Hash().Bytes()) {
   287  			t.Fatal("returning wrong transaction hash")
   288  		}
   289  
   290  		var storedNonce uint64
   291  		err = store.Get(nonceKey(sender), &storedNonce)
   292  		if err != nil {
   293  			t.Fatal(err)
   294  		}
   295  		if storedNonce != nonce+1 {
   296  			t.Fatalf("nonce not stored correctly: want %d, got %d", nonce+1, storedNonce)
   297  		}
   298  
   299  		storedTransaction, err := transactionService.StoredTransaction(txHash)
   300  		if err != nil {
   301  			t.Fatal(err)
   302  		}
   303  
   304  		if storedTransaction.To == nil || *storedTransaction.To != recipient {
   305  			t.Fatalf("got wrong recipient in stored transaction. wanted %x, got %x", recipient, storedTransaction.To)
   306  		}
   307  
   308  		if !bytes.Equal(storedTransaction.Data, request.Data) {
   309  			t.Fatalf("got wrong data in stored transaction. wanted %x, got %x", request.Data, storedTransaction.Data)
   310  		}
   311  
   312  		if storedTransaction.Description != request.Description {
   313  			t.Fatalf("got wrong description in stored transaction. wanted %x, got %x", request.Description, storedTransaction.Description)
   314  		}
   315  
   316  		if storedTransaction.GasLimit != estimatedGasLimit {
   317  			t.Fatalf("got wrong gas limit in stored transaction. wanted %d, got %d", estimatedGasLimit, storedTransaction.GasLimit)
   318  		}
   319  
   320  		if defaultGasFee.Cmp(storedTransaction.GasPrice) != 0 {
   321  			t.Fatalf("got wrong gas price in stored transaction. wanted %d, got %d", defaultGasFee, storedTransaction.GasPrice)
   322  		}
   323  
   324  		if storedTransaction.Nonce != nonce {
   325  			t.Fatalf("got wrong nonce in stored transaction. wanted %d, got %d", nonce, storedTransaction.Nonce)
   326  		}
   327  
   328  		pending, err := transactionService.PendingTransactions()
   329  		if err != nil {
   330  			t.Fatal(err)
   331  		}
   332  		if len(pending) != 1 {
   333  			t.Fatalf("expected one pending transaction, got %d", len(pending))
   334  		}
   335  
   336  		if pending[0] != txHash {
   337  			t.Fatalf("got wrong pending transaction. wanted %x, got %x", txHash, pending[0])
   338  		}
   339  	})
   340  
   341  	t.Run("sendWithBoost", func(t *testing.T) {
   342  		t.Parallel()
   343  
   344  		tip := big.NewInt(0).Div(new(big.Int).Mul(suggestedGasTip, big.NewInt(15)), big.NewInt(10))
   345  		fee := big.NewInt(0).Div(new(big.Int).Mul(suggestedGasPrice, big.NewInt(15)), big.NewInt(10))
   346  		fee = fee.Add(fee, tip)
   347  		// tip is the same as suggestedGasPrice and boost is 50%
   348  		// so final gas price will be 2.5x suggestedGasPrice
   349  
   350  		signedTx := types.NewTx(&types.DynamicFeeTx{
   351  			ChainID:   chainID,
   352  			Nonce:     nonce,
   353  			To:        &recipient,
   354  			Value:     value,
   355  			Gas:       estimatedGasLimit,
   356  			GasFeeCap: fee,
   357  			GasTipCap: tip,
   358  			Data:      txData,
   359  		})
   360  		request := &transaction.TxRequest{
   361  			To:    &recipient,
   362  			Data:  txData,
   363  			Value: value,
   364  		}
   365  		store := storemock.NewStateStore()
   366  		err := store.Put(nonceKey(sender), nonce)
   367  		if err != nil {
   368  			t.Fatal(err)
   369  		}
   370  
   371  		transactionService, err := transaction.NewService(logger, sender,
   372  			backendmock.New(
   373  				backendmock.WithSendTransactionFunc(func(ctx context.Context, tx *types.Transaction) error {
   374  					if tx != signedTx {
   375  						t.Fatal("not sending signed transaction")
   376  					}
   377  					return nil
   378  				}),
   379  				backendmock.WithEstimateGasFunc(func(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error) {
   380  					if !bytes.Equal(call.To.Bytes(), recipient.Bytes()) {
   381  						t.Fatalf("estimating with wrong recipient. wanted %x, got %x", recipient, call.To)
   382  					}
   383  					if !bytes.Equal(call.Data, txData) {
   384  						t.Fatal("estimating with wrong data")
   385  					}
   386  					return estimatedGasLimit, nil
   387  				}),
   388  				backendmock.WithSuggestGasPriceFunc(func(ctx context.Context) (*big.Int, error) {
   389  					return suggestedGasPrice, nil
   390  				}),
   391  				backendmock.WithPendingNonceAtFunc(func(ctx context.Context, account common.Address) (uint64, error) {
   392  					return nonce - 1, nil
   393  				}),
   394  				backendmock.WithSuggestGasTipCapFunc(func(ctx context.Context) (*big.Int, error) {
   395  					return suggestedGasTip, nil
   396  				}),
   397  			),
   398  			signerMockForTransaction(t, signedTx, sender, chainID),
   399  			store,
   400  			chainID,
   401  			monitormock.New(
   402  				monitormock.WithWatchTransactionFunc(func(txHash common.Hash, nonce uint64) (<-chan types.Receipt, <-chan error, error) {
   403  					return nil, nil, nil
   404  				}),
   405  			),
   406  		)
   407  		if err != nil {
   408  			t.Fatal(err)
   409  		}
   410  		testutil.CleanupCloser(t, transactionService)
   411  
   412  		txHash, err := transactionService.Send(context.Background(), request, 50)
   413  		if err != nil {
   414  			t.Fatal(err)
   415  		}
   416  
   417  		if !bytes.Equal(txHash.Bytes(), signedTx.Hash().Bytes()) {
   418  			t.Fatal("returning wrong transaction hash")
   419  		}
   420  
   421  		var storedNonce uint64
   422  		err = store.Get(nonceKey(sender), &storedNonce)
   423  		if err != nil {
   424  			t.Fatal(err)
   425  		}
   426  		if storedNonce != nonce+1 {
   427  			t.Fatalf("nonce not stored correctly: want %d, got %d", nonce+1, storedNonce)
   428  		}
   429  
   430  		storedTransaction, err := transactionService.StoredTransaction(txHash)
   431  		if err != nil {
   432  			t.Fatal(err)
   433  		}
   434  
   435  		if storedTransaction.To == nil || *storedTransaction.To != recipient {
   436  			t.Fatalf("got wrong recipient in stored transaction. wanted %x, got %x", recipient, storedTransaction.To)
   437  		}
   438  
   439  		if !bytes.Equal(storedTransaction.Data, request.Data) {
   440  			t.Fatalf("got wrong data in stored transaction. wanted %x, got %x", request.Data, storedTransaction.Data)
   441  		}
   442  
   443  		if storedTransaction.Description != request.Description {
   444  			t.Fatalf("got wrong description in stored transaction. wanted %x, got %x", request.Description, storedTransaction.Description)
   445  		}
   446  
   447  		if storedTransaction.GasLimit != estimatedGasLimit {
   448  			t.Fatalf("got wrong gas limit in stored transaction. wanted %d, got %d", estimatedGasLimit, storedTransaction.GasLimit)
   449  		}
   450  
   451  		if fee.Cmp(storedTransaction.GasPrice) != 0 {
   452  			t.Fatalf("got wrong gas price in stored transaction. wanted %d, got %d", fee, storedTransaction.GasPrice)
   453  		}
   454  
   455  		if storedTransaction.Nonce != nonce {
   456  			t.Fatalf("got wrong nonce in stored transaction. wanted %d, got %d", nonce, storedTransaction.Nonce)
   457  		}
   458  
   459  		pending, err := transactionService.PendingTransactions()
   460  		if err != nil {
   461  			t.Fatal(err)
   462  		}
   463  		if len(pending) != 1 {
   464  			t.Fatalf("expected one pending transaction, got %d", len(pending))
   465  		}
   466  
   467  		if pending[0] != txHash {
   468  			t.Fatalf("got wrong pending transaction. wanted %x, got %x", txHash, pending[0])
   469  		}
   470  	})
   471  
   472  	t.Run("send_no_nonce", func(t *testing.T) {
   473  		t.Parallel()
   474  
   475  		signedTx := types.NewTx(&types.DynamicFeeTx{
   476  			ChainID:   chainID,
   477  			Nonce:     nonce,
   478  			To:        &recipient,
   479  			Value:     value,
   480  			Gas:       estimatedGasLimit,
   481  			GasTipCap: suggestedGasTip,
   482  			GasFeeCap: defaultGasFee,
   483  			Data:      txData,
   484  		})
   485  		request := &transaction.TxRequest{
   486  			To:    &recipient,
   487  			Data:  txData,
   488  			Value: value,
   489  		}
   490  		store := storemock.NewStateStore()
   491  
   492  		transactionService, err := transaction.NewService(logger, sender,
   493  			backendmock.New(
   494  				backendmock.WithSendTransactionFunc(func(ctx context.Context, tx *types.Transaction) error {
   495  					if tx != signedTx {
   496  						t.Fatal("not sending signed transaction")
   497  					}
   498  					return nil
   499  				}),
   500  				backendmock.WithEstimateGasFunc(func(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error) {
   501  					if !bytes.Equal(call.To.Bytes(), recipient.Bytes()) {
   502  						t.Fatalf("estimating with wrong recipient. wanted %x, got %x", recipient, call.To)
   503  					}
   504  					if !bytes.Equal(call.Data, txData) {
   505  						t.Fatal("estimating with wrong data")
   506  					}
   507  					return estimatedGasLimit, nil
   508  				}),
   509  				backendmock.WithSuggestGasPriceFunc(func(ctx context.Context) (*big.Int, error) {
   510  					return suggestedGasPrice, nil
   511  				}),
   512  				backendmock.WithPendingNonceAtFunc(func(ctx context.Context, account common.Address) (uint64, error) {
   513  					return nonce, nil
   514  				}),
   515  				backendmock.WithSuggestGasTipCapFunc(func(ctx context.Context) (*big.Int, error) {
   516  					return suggestedGasTip, nil
   517  				}),
   518  			),
   519  			signerMockForTransaction(t, signedTx, sender, chainID),
   520  			store,
   521  			chainID,
   522  			monitormock.New(),
   523  		)
   524  		if err != nil {
   525  			t.Fatal(err)
   526  		}
   527  		testutil.CleanupCloser(t, transactionService)
   528  
   529  		txHash, err := transactionService.Send(context.Background(), request, 0)
   530  		if err != nil {
   531  			t.Fatal(err)
   532  		}
   533  
   534  		if !bytes.Equal(txHash.Bytes(), signedTx.Hash().Bytes()) {
   535  			t.Fatal("returning wrong transaction hash")
   536  		}
   537  
   538  		var storedNonce uint64
   539  		err = store.Get(nonceKey(sender), &storedNonce)
   540  		if err != nil {
   541  			t.Fatal(err)
   542  		}
   543  		if storedNonce != nonce+1 {
   544  			t.Fatalf("did not store nonce correctly. wanted %d, got %d", nonce+1, storedNonce)
   545  		}
   546  	})
   547  
   548  	t.Run("send_skipped_nonce", func(t *testing.T) {
   549  		t.Parallel()
   550  
   551  		nextNonce := nonce + 5
   552  		signedTx := types.NewTx(&types.DynamicFeeTx{
   553  			ChainID:   chainID,
   554  			Nonce:     nextNonce,
   555  			To:        &recipient,
   556  			Value:     value,
   557  			Gas:       estimatedGasLimit,
   558  			GasTipCap: suggestedGasTip,
   559  			GasFeeCap: defaultGasFee,
   560  			Data:      txData,
   561  		})
   562  		request := &transaction.TxRequest{
   563  			To:    &recipient,
   564  			Data:  txData,
   565  			Value: value,
   566  		}
   567  		store := storemock.NewStateStore()
   568  		err := store.Put(nonceKey(sender), nonce)
   569  		if err != nil {
   570  			t.Fatal(err)
   571  		}
   572  
   573  		transactionService, err := transaction.NewService(logger, sender,
   574  			backendmock.New(
   575  				backendmock.WithSendTransactionFunc(func(ctx context.Context, tx *types.Transaction) error {
   576  					if tx != signedTx {
   577  						t.Fatal("not sending signed transaction")
   578  					}
   579  					return nil
   580  				}),
   581  				backendmock.WithEstimateGasFunc(func(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error) {
   582  					if !bytes.Equal(call.To.Bytes(), recipient.Bytes()) {
   583  						t.Fatalf("estimating with wrong recipient. wanted %x, got %x", recipient, call.To)
   584  					}
   585  					if !bytes.Equal(call.Data, txData) {
   586  						t.Fatal("estimating with wrong data")
   587  					}
   588  					return estimatedGasLimit, nil
   589  				}),
   590  				backendmock.WithSuggestGasPriceFunc(func(ctx context.Context) (*big.Int, error) {
   591  					return suggestedGasPrice, nil
   592  				}),
   593  				backendmock.WithPendingNonceAtFunc(func(ctx context.Context, account common.Address) (uint64, error) {
   594  					return nextNonce, nil
   595  				}),
   596  				backendmock.WithSuggestGasTipCapFunc(func(ctx context.Context) (*big.Int, error) {
   597  					return suggestedGasTip, nil
   598  				}),
   599  			),
   600  			signerMockForTransaction(t, signedTx, sender, chainID),
   601  			store,
   602  			chainID,
   603  			monitormock.New(),
   604  		)
   605  		if err != nil {
   606  			t.Fatal(err)
   607  		}
   608  
   609  		txHash, err := transactionService.Send(context.Background(), request, 0)
   610  		if err != nil {
   611  			t.Fatal(err)
   612  		}
   613  
   614  		if !bytes.Equal(txHash.Bytes(), signedTx.Hash().Bytes()) {
   615  			t.Fatal("returning wrong transaction hash")
   616  		}
   617  
   618  		var storedNonce uint64
   619  		err = store.Get(nonceKey(sender), &storedNonce)
   620  		if err != nil {
   621  			t.Fatal(err)
   622  		}
   623  		if storedNonce != nextNonce+1 {
   624  			t.Fatalf("did not store nonce correctly. wanted %d, got %d", nextNonce+1, storedNonce)
   625  		}
   626  	})
   627  }
   628  
   629  func TestTransactionWaitForReceipt(t *testing.T) {
   630  	t.Parallel()
   631  
   632  	logger := log.Noop
   633  	sender := common.HexToAddress("0xddff")
   634  	txHash := common.HexToHash("0xabcdee")
   635  	chainID := big.NewInt(5)
   636  	nonce := uint64(10)
   637  
   638  	store := storemock.NewStateStore()
   639  	testutil.CleanupCloser(t, store)
   640  
   641  	err := store.Put(transaction.StoredTransactionKey(txHash), transaction.StoredTransaction{
   642  		Nonce: nonce,
   643  	})
   644  	if err != nil {
   645  		t.Fatal(err)
   646  	}
   647  
   648  	transactionService, err := transaction.NewService(logger, sender,
   649  		backendmock.New(
   650  			backendmock.WithTransactionReceiptFunc(func(ctx context.Context, txHash common.Hash) (*types.Receipt, error) {
   651  				return &types.Receipt{
   652  					TxHash: txHash,
   653  				}, nil
   654  			}),
   655  		),
   656  		signermock.New(),
   657  		store,
   658  		chainID,
   659  		monitormock.New(
   660  			monitormock.WithWatchTransactionFunc(func(txh common.Hash, n uint64) (<-chan types.Receipt, <-chan error, error) {
   661  				if nonce != n {
   662  					return nil, nil, fmt.Errorf("nonce mismatch. wanted %d, got %d", nonce, n)
   663  				}
   664  				if txHash != txh {
   665  					return nil, nil, fmt.Errorf("hash mismatch. wanted %x, got %x", txHash, txh)
   666  				}
   667  				receiptC := make(chan types.Receipt, 1)
   668  				receiptC <- types.Receipt{
   669  					TxHash: txHash,
   670  				}
   671  				return receiptC, nil, nil
   672  			}),
   673  		),
   674  	)
   675  	if err != nil {
   676  		t.Fatal(err)
   677  	}
   678  	testutil.CleanupCloser(t, transactionService)
   679  
   680  	receipt, err := transactionService.WaitForReceipt(context.Background(), txHash)
   681  	if err != nil {
   682  		t.Fatal(err)
   683  	}
   684  
   685  	if receipt.TxHash != txHash {
   686  		t.Fatal("got wrong receipt")
   687  	}
   688  }
   689  
   690  func TestTransactionResend(t *testing.T) {
   691  	t.Parallel()
   692  
   693  	logger := log.Noop
   694  	sender := common.HexToAddress("0xddff")
   695  	recipient := common.HexToAddress("0xbbbddd")
   696  	chainID := big.NewInt(5)
   697  	nonce := uint64(10)
   698  	data := []byte{1, 2, 3, 4}
   699  	gasPrice := big.NewInt(1000)
   700  	gasTip := big.NewInt(100)
   701  	gasFee := big.NewInt(1100)
   702  	gasLimit := uint64(100000)
   703  	value := big.NewInt(0)
   704  
   705  	store := storemock.NewStateStore()
   706  	testutil.CleanupCloser(t, store)
   707  
   708  	signedTx := types.NewTx(&types.DynamicFeeTx{
   709  		ChainID:   chainID,
   710  		Nonce:     nonce,
   711  		To:        &recipient,
   712  		Value:     value,
   713  		Gas:       gasLimit,
   714  		GasTipCap: gasTip,
   715  		GasFeeCap: gasFee,
   716  		Data:      data,
   717  	})
   718  
   719  	err := store.Put(transaction.StoredTransactionKey(signedTx.Hash()), transaction.StoredTransaction{
   720  		Nonce:    nonce,
   721  		To:       &recipient,
   722  		Data:     data,
   723  		GasPrice: gasFee,
   724  		GasLimit: gasLimit,
   725  		Value:    value,
   726  	})
   727  	if err != nil {
   728  		t.Fatal(err)
   729  	}
   730  
   731  	transactionService, err := transaction.NewService(logger, sender,
   732  		backendmock.New(
   733  			backendmock.WithSendTransactionFunc(func(ctx context.Context, tx *types.Transaction) error {
   734  				if tx != signedTx {
   735  					t.Fatal("not sending signed transaction")
   736  				}
   737  				return nil
   738  			}),
   739  			backendmock.WithSuggestGasPriceFunc(func(ctx context.Context) (*big.Int, error) {
   740  				return gasPrice, nil
   741  			}),
   742  			backendmock.WithSuggestGasTipCapFunc(func(ctx context.Context) (*big.Int, error) {
   743  				return gasTip, nil
   744  			}),
   745  		),
   746  		signerMockForTransaction(t, signedTx, recipient, chainID),
   747  		store,
   748  		chainID,
   749  		monitormock.New(),
   750  	)
   751  	if err != nil {
   752  		t.Fatal(err)
   753  	}
   754  	testutil.CleanupCloser(t, transactionService)
   755  
   756  	err = transactionService.ResendTransaction(context.Background(), signedTx.Hash())
   757  	if err != nil {
   758  		t.Fatal(err)
   759  	}
   760  }
   761  
   762  func TestTransactionCancel(t *testing.T) {
   763  	t.Parallel()
   764  
   765  	logger := log.Noop
   766  	sender := common.HexToAddress("0xddff")
   767  	recipient := common.HexToAddress("0xbbbddd")
   768  	chainID := big.NewInt(5)
   769  	nonce := uint64(10)
   770  	data := []byte{1, 2, 3, 4}
   771  	gasPrice := big.NewInt(1000)
   772  	gasTip := big.NewInt(100)
   773  	gasFee := big.NewInt(1100)
   774  	gasLimit := uint64(100000)
   775  	value := big.NewInt(0)
   776  
   777  	store := storemock.NewStateStore()
   778  	testutil.CleanupCloser(t, store)
   779  
   780  	signedTx := types.NewTx(&types.DynamicFeeTx{
   781  		ChainID:   chainID,
   782  		Nonce:     nonce,
   783  		To:        &recipient,
   784  		Value:     value,
   785  		Gas:       gasLimit,
   786  		GasFeeCap: gasFee,
   787  		GasTipCap: gasTip,
   788  		Data:      data,
   789  	})
   790  	err := store.Put(transaction.StoredTransactionKey(signedTx.Hash()), transaction.StoredTransaction{
   791  		Nonce:     nonce,
   792  		To:        &recipient,
   793  		Data:      data,
   794  		GasPrice:  gasFee,
   795  		GasLimit:  gasLimit,
   796  		GasFeeCap: gasFee,
   797  		GasTipCap: gasTip,
   798  		Value:     value,
   799  	})
   800  	if err != nil {
   801  		t.Fatal(err)
   802  	}
   803  
   804  	gasTipCap := new(big.Int).Div(new(big.Int).Mul(big.NewInt(int64(10)+100), gasTip), big.NewInt(100))
   805  	gasFeeCap := new(big.Int).Add(gasFee, gasTipCap)
   806  
   807  	t.Run("ok", func(t *testing.T) {
   808  		t.Parallel()
   809  
   810  		cancelTx := types.NewTx(&types.DynamicFeeTx{
   811  			ChainID:   chainID,
   812  			Nonce:     nonce,
   813  			To:        &recipient,
   814  			Value:     big.NewInt(0),
   815  			Gas:       21000,
   816  			GasTipCap: gasTipCap,
   817  			GasFeeCap: gasFeeCap,
   818  			Data:      []byte{},
   819  		})
   820  
   821  		transactionService, err := transaction.NewService(logger, sender,
   822  			backendmock.New(
   823  				backendmock.WithSendTransactionFunc(func(ctx context.Context, tx *types.Transaction) error {
   824  					if tx != cancelTx {
   825  						t.Fatal("not sending signed transaction")
   826  					}
   827  					return nil
   828  				}),
   829  				backendmock.WithSuggestGasPriceFunc(func(ctx context.Context) (*big.Int, error) {
   830  					return gasPrice, nil
   831  				}),
   832  				backendmock.WithSuggestGasTipCapFunc(func(ctx context.Context) (*big.Int, error) {
   833  					return gasTip, nil
   834  				}),
   835  			),
   836  			signerMockForTransaction(t, cancelTx, recipient, chainID),
   837  			store,
   838  			chainID,
   839  			monitormock.New(),
   840  		)
   841  		if err != nil {
   842  			t.Fatal(err)
   843  		}
   844  		testutil.CleanupCloser(t, transactionService)
   845  
   846  		cancelTxHash, err := transactionService.CancelTransaction(context.Background(), signedTx.Hash())
   847  		if err != nil {
   848  			t.Fatal(err)
   849  		}
   850  
   851  		if cancelTx.Hash() != cancelTxHash {
   852  			t.Fatalf("returned wrong hash. wanted %v, got %v", cancelTx.Hash(), cancelTxHash)
   853  		}
   854  	})
   855  
   856  	t.Run("custom gas price", func(t *testing.T) {
   857  		t.Parallel()
   858  
   859  		customGasPrice := big.NewInt(5)
   860  
   861  		cancelTx := types.NewTx(&types.DynamicFeeTx{
   862  			ChainID:   chainID,
   863  			Nonce:     nonce,
   864  			To:        &recipient,
   865  			Value:     big.NewInt(0),
   866  			Gas:       21000,
   867  			GasFeeCap: gasFeeCap,
   868  			GasTipCap: gasTip,
   869  			Data:      []byte{},
   870  		})
   871  
   872  		transactionService, err := transaction.NewService(logger, sender,
   873  			backendmock.New(
   874  				backendmock.WithSendTransactionFunc(func(ctx context.Context, tx *types.Transaction) error {
   875  					if tx != cancelTx {
   876  						t.Fatal("not sending signed transaction")
   877  					}
   878  					return nil
   879  				}),
   880  				backendmock.WithSuggestGasPriceFunc(func(ctx context.Context) (*big.Int, error) {
   881  					return gasPrice, nil
   882  				}),
   883  				backendmock.WithSuggestGasTipCapFunc(func(ctx context.Context) (*big.Int, error) {
   884  					return gasTip, nil
   885  				}),
   886  			),
   887  			signerMockForTransaction(t, cancelTx, recipient, chainID),
   888  			store,
   889  			chainID,
   890  			monitormock.New(),
   891  		)
   892  		if err != nil {
   893  			t.Fatal(err)
   894  		}
   895  		testutil.CleanupCloser(t, transactionService)
   896  
   897  		ctx := sctx.SetGasPrice(context.Background(), customGasPrice)
   898  		cancelTxHash, err := transactionService.CancelTransaction(ctx, signedTx.Hash())
   899  		if err != nil {
   900  			t.Fatal(err)
   901  		}
   902  
   903  		if cancelTx.Hash() != cancelTxHash {
   904  			t.Fatalf("returned wrong hash. wanted %v, got %v", cancelTx.Hash(), cancelTxHash)
   905  		}
   906  	})
   907  }
   908  
   909  // rpcAPIError is a copy of engine.EngineAPIError from go-ethereum pkg.
   910  type rpcAPIError struct {
   911  	code int
   912  	msg  string
   913  	err  string
   914  }
   915  
   916  func (e *rpcAPIError) ErrorCode() int         { return e.code }
   917  func (e *rpcAPIError) Error() string          { return e.msg }
   918  func (e *rpcAPIError) ErrorData() interface{} { return e.err }
   919  
   920  var _ rpc.DataError = (*rpcAPIError)(nil)
   921  
   922  func TestTransactionService_UnwrapABIError(t *testing.T) {
   923  	t.Parallel()
   924  
   925  	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
   926  	defer cancel()
   927  
   928  	var (
   929  		sender    = common.HexToAddress("0xddff")
   930  		recipient = common.HexToAddress("0xbbbddd")
   931  		chainID   = big.NewInt(5)
   932  		nonce     = uint64(10)
   933  		gasTip    = big.NewInt(100)
   934  		gasFee    = big.NewInt(1100)
   935  		txData    = common.Hex2Bytes("0xabcdee")
   936  		value     = big.NewInt(1)
   937  
   938  		// This is the ABI of the following contract: https://sepolia.etherscan.io/address/0xd29d9e385f19d888557cd609006bb1934cb5d1e2#code
   939  		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"}]`)
   940  		rpcAPIErr   = &rpcAPIError{
   941  			code: 3,
   942  			msg:  "execution reverted",
   943  			err:  "0xcf4791810000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006f", // This is the ABI encoded error form the following failed transaction: https://sepolia.etherscan.io/tx/0x74a2577db1c325c41e38977aa1eb32ab03dfa17cc1fa0649e84f3d8c0f0882ee
   944  		}
   945  	)
   946  
   947  	gasTipCap := new(big.Int).Div(new(big.Int).Mul(big.NewInt(int64(10)+100), gasTip), big.NewInt(100))
   948  	gasFeeCap := new(big.Int).Add(gasFee, gasTipCap)
   949  
   950  	signedTx := types.NewTx(&types.DynamicFeeTx{
   951  		ChainID:   chainID,
   952  		Nonce:     nonce,
   953  		To:        &recipient,
   954  		Value:     value,
   955  		Gas:       21000,
   956  		GasTipCap: gasTipCap,
   957  		GasFeeCap: gasFeeCap,
   958  		Data:      txData,
   959  	})
   960  	request := &transaction.TxRequest{
   961  		To:    &recipient,
   962  		Data:  txData,
   963  		Value: value,
   964  	}
   965  
   966  	transactionService, err := transaction.NewService(log.Noop, sender,
   967  		backendmock.New(
   968  			backendmock.WithCallContractFunc(func(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) {
   969  				return nil, rpcAPIErr
   970  			}),
   971  		),
   972  		signerMockForTransaction(t, signedTx, recipient, chainID),
   973  		storemock.NewStateStore(),
   974  		chainID,
   975  		monitormock.New(),
   976  	)
   977  	if err != nil {
   978  		t.Fatal(err)
   979  	}
   980  	testutil.CleanupCloser(t, transactionService)
   981  
   982  	originErr := errors.New("origin error")
   983  	wrappedErr := transactionService.UnwrapABIError(ctx, request, originErr, contractABI.Errors)
   984  	if !errors.Is(wrappedErr, originErr) {
   985  		t.Fatal("origin error not wrapped")
   986  	}
   987  	if !strings.Contains(wrappedErr.Error(), rpcAPIErr.Error()) {
   988  		t.Fatal("wrapped error without rpc api main error")
   989  	}
   990  	if !strings.Contains(wrappedErr.Error(), "InsufficientBalance(available=0,required=111)") {
   991  		t.Fatal("wrapped error without rpc api error data")
   992  	}
   993  }