github.com/status-im/status-go@v1.1.0/transactions/transactor_test.go (about)

     1  package transactions
     2  
     3  import (
     4  	"fmt"
     5  	"math/big"
     6  	"reflect"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/golang/mock/gomock"
    11  	"github.com/stretchr/testify/suite"
    12  
    13  	"github.com/ethereum/go-ethereum/common"
    14  	"github.com/ethereum/go-ethereum/common/hexutil"
    15  	gethtypes "github.com/ethereum/go-ethereum/core/types"
    16  	gethcrypto "github.com/ethereum/go-ethereum/crypto"
    17  	gethparams "github.com/ethereum/go-ethereum/params"
    18  	"github.com/ethereum/go-ethereum/rlp"
    19  	gethrpc "github.com/ethereum/go-ethereum/rpc"
    20  
    21  	"github.com/status-im/status-go/account"
    22  	"github.com/status-im/status-go/eth-node/crypto"
    23  	"github.com/status-im/status-go/eth-node/types"
    24  	"github.com/status-im/status-go/params"
    25  	"github.com/status-im/status-go/rpc"
    26  	wallet_common "github.com/status-im/status-go/services/wallet/common"
    27  	"github.com/status-im/status-go/sqlite"
    28  	"github.com/status-im/status-go/t/utils"
    29  	"github.com/status-im/status-go/transactions/fake"
    30  )
    31  
    32  func TestTransactorSuite(t *testing.T) {
    33  	utils.Init()
    34  	suite.Run(t, new(TransactorSuite))
    35  }
    36  
    37  type TransactorSuite struct {
    38  	suite.Suite
    39  	server            *gethrpc.Server
    40  	client            *gethrpc.Client
    41  	txServiceMockCtrl *gomock.Controller
    42  	txServiceMock     *fake.MockPublicTransactionPoolAPI
    43  	nodeConfig        *params.NodeConfig
    44  
    45  	manager *Transactor
    46  }
    47  
    48  func (s *TransactorSuite) SetupTest() {
    49  	s.txServiceMockCtrl = gomock.NewController(s.T())
    50  
    51  	s.server, s.txServiceMock = fake.NewTestServer(s.txServiceMockCtrl)
    52  	s.client = gethrpc.DialInProc(s.server)
    53  
    54  	// expected by simulated backend
    55  	chainID := gethparams.AllEthashProtocolChanges.ChainID.Uint64()
    56  	db, err := sqlite.OpenUnecryptedDB(sqlite.InMemoryPath) // dummy to make rpc.Client happy
    57  	s.Require().NoError(err)
    58  	rpcClient, _ := rpc.NewClient(s.client, chainID, params.UpstreamRPCConfig{}, nil, db, nil)
    59  	rpcClient.UpstreamChainID = chainID
    60  	nodeConfig, err := utils.MakeTestNodeConfigWithDataDir("", "/tmp", chainID)
    61  	s.Require().NoError(err)
    62  	s.nodeConfig = nodeConfig
    63  
    64  	s.manager = NewTransactor()
    65  	s.manager.sendTxTimeout = time.Second
    66  	s.manager.SetNetworkID(chainID)
    67  	s.manager.SetRPC(rpcClient, time.Second)
    68  }
    69  
    70  func (s *TransactorSuite) TearDownTest() {
    71  	s.txServiceMockCtrl.Finish()
    72  	s.server.Stop()
    73  	s.client.Close()
    74  }
    75  
    76  var (
    77  	testGas      = hexutil.Uint64(defaultGas + 1)
    78  	testGasPrice = (*hexutil.Big)(big.NewInt(10))
    79  	testNonce    = hexutil.Uint64(10)
    80  )
    81  
    82  func (s *TransactorSuite) setupTransactionPoolAPI(args SendTxArgs, returnNonce, resultNonce hexutil.Uint64, account *account.SelectedExtKey, txErr error) {
    83  	// Expect calls to gas functions only if there are no user defined values.
    84  	// And also set the expected gas and gas price for RLP encoding the expected tx.
    85  	var usedGas hexutil.Uint64
    86  	var usedGasPrice *big.Int
    87  	s.txServiceMock.EXPECT().GetTransactionCount(gomock.Any(), gomock.Eq(common.Address(account.Address)), gethrpc.PendingBlockNumber).Return(&returnNonce, nil)
    88  	if !args.IsDynamicFeeTx() {
    89  		if args.GasPrice == nil {
    90  			usedGasPrice = (*big.Int)(testGasPrice)
    91  			s.txServiceMock.EXPECT().GasPrice(gomock.Any()).Return(testGasPrice, nil)
    92  		} else {
    93  			usedGasPrice = (*big.Int)(args.GasPrice)
    94  		}
    95  	}
    96  
    97  	if args.Gas == nil {
    98  		s.txServiceMock.EXPECT().EstimateGas(gomock.Any(), gomock.Any()).Return(testGas, nil)
    99  		usedGas = testGas
   100  	} else {
   101  		usedGas = *args.Gas
   102  	}
   103  	// Prepare the transaction and RLP encode it.
   104  	data := s.rlpEncodeTx(args, s.nodeConfig, account, &resultNonce, usedGas, usedGasPrice)
   105  	// Expect the RLP encoded transaction.
   106  	s.txServiceMock.EXPECT().SendRawTransaction(gomock.Any(), data).Return(common.Hash{}, txErr)
   107  }
   108  
   109  func (s *TransactorSuite) rlpEncodeTx(args SendTxArgs, config *params.NodeConfig, account *account.SelectedExtKey, nonce *hexutil.Uint64, gas hexutil.Uint64, gasPrice *big.Int) hexutil.Bytes {
   110  	var txData gethtypes.TxData
   111  	to := common.Address(*args.To)
   112  	if args.IsDynamicFeeTx() {
   113  		gasTipCap := (*big.Int)(args.MaxPriorityFeePerGas)
   114  		gasFeeCap := (*big.Int)(args.MaxFeePerGas)
   115  
   116  		txData = &gethtypes.DynamicFeeTx{
   117  			Nonce:     uint64(*nonce),
   118  			Gas:       uint64(gas),
   119  			GasTipCap: gasTipCap,
   120  			GasFeeCap: gasFeeCap,
   121  			To:        &to,
   122  			Value:     args.Value.ToInt(),
   123  			Data:      args.GetInput(),
   124  		}
   125  	} else {
   126  		txData = &gethtypes.LegacyTx{
   127  			Nonce:    uint64(*nonce),
   128  			GasPrice: gasPrice,
   129  			Gas:      uint64(gas),
   130  			To:       &to,
   131  			Value:    args.Value.ToInt(),
   132  			Data:     args.GetInput(),
   133  		}
   134  	}
   135  
   136  	newTx := gethtypes.NewTx(txData)
   137  	chainID := big.NewInt(int64(s.nodeConfig.NetworkID))
   138  
   139  	signedTx, err := gethtypes.SignTx(newTx, gethtypes.NewLondonSigner(chainID), account.AccountKey.PrivateKey)
   140  	s.NoError(err)
   141  	data, err := signedTx.MarshalBinary()
   142  	s.NoError(err)
   143  	return hexutil.Bytes(data)
   144  }
   145  
   146  func (s *TransactorSuite) TestGasValues() {
   147  	key, _ := gethcrypto.GenerateKey()
   148  	selectedAccount := &account.SelectedExtKey{
   149  		Address:    account.FromAddress(utils.TestConfig.Account1.WalletAddress),
   150  		AccountKey: &types.Key{PrivateKey: key},
   151  	}
   152  	testCases := []struct {
   153  		name                 string
   154  		gas                  *hexutil.Uint64
   155  		gasPrice             *hexutil.Big
   156  		maxFeePerGas         *hexutil.Big
   157  		maxPriorityFeePerGas *hexutil.Big
   158  	}{
   159  		{
   160  			"noGasDef",
   161  			nil,
   162  			nil,
   163  			nil,
   164  			nil,
   165  		},
   166  		{
   167  			"gasDefined",
   168  			&testGas,
   169  			nil,
   170  			nil,
   171  			nil,
   172  		},
   173  		{
   174  			"gasPriceDefined",
   175  			nil,
   176  			testGasPrice,
   177  			nil,
   178  			nil,
   179  		},
   180  		{
   181  			"nilSignTransactionSpecificArgs",
   182  			nil,
   183  			nil,
   184  			nil,
   185  			nil,
   186  		},
   187  
   188  		{
   189  			"maxFeeAndPriorityset",
   190  			nil,
   191  			nil,
   192  			testGasPrice,
   193  			testGasPrice,
   194  		},
   195  	}
   196  
   197  	for _, testCase := range testCases {
   198  		s.T().Run(testCase.name, func(t *testing.T) {
   199  			s.SetupTest()
   200  			args := SendTxArgs{
   201  				From:                 account.FromAddress(utils.TestConfig.Account1.WalletAddress),
   202  				To:                   account.ToAddress(utils.TestConfig.Account2.WalletAddress),
   203  				Gas:                  testCase.gas,
   204  				GasPrice:             testCase.gasPrice,
   205  				MaxFeePerGas:         testCase.maxFeePerGas,
   206  				MaxPriorityFeePerGas: testCase.maxPriorityFeePerGas,
   207  			}
   208  			s.setupTransactionPoolAPI(args, testNonce, testNonce, selectedAccount, nil)
   209  
   210  			hash, _, err := s.manager.SendTransaction(args, selectedAccount, -1)
   211  			s.NoError(err)
   212  			s.False(reflect.DeepEqual(hash, common.Hash{}))
   213  		})
   214  	}
   215  }
   216  
   217  func (s *TransactorSuite) setupBuildTransactionMocks(args SendTxArgs, account *account.SelectedExtKey) {
   218  	s.txServiceMock.EXPECT().GetTransactionCount(gomock.Any(), gomock.Eq(common.Address(account.Address)), gethrpc.PendingBlockNumber).Return(&testNonce, nil)
   219  
   220  	if !args.IsDynamicFeeTx() && args.GasPrice == nil {
   221  		s.txServiceMock.EXPECT().GasPrice(gomock.Any()).Return(testGasPrice, nil)
   222  	}
   223  
   224  	if args.Gas == nil {
   225  		s.txServiceMock.EXPECT().EstimateGas(gomock.Any(), gomock.Any()).Return(testGas, nil)
   226  	}
   227  }
   228  
   229  func (s *TransactorSuite) TestBuildAndValidateTransaction() {
   230  	key, _ := gethcrypto.GenerateKey()
   231  	selectedAccount := &account.SelectedExtKey{
   232  		Address:    account.FromAddress(utils.TestConfig.Account1.WalletAddress),
   233  		AccountKey: &types.Key{PrivateKey: key},
   234  	}
   235  
   236  	chainID := s.nodeConfig.NetworkID
   237  	fromAddress := account.FromAddress(utils.TestConfig.Account1.WalletAddress)
   238  	toAddress := account.ToAddress(utils.TestConfig.Account2.WalletAddress)
   239  	value := (*hexutil.Big)(big.NewInt(10))
   240  
   241  	expectedGasPrice := (*big.Int)(testGasPrice)
   242  	expectedGas := uint64(testGas)
   243  	expectedNonce := uint64(testNonce)
   244  
   245  	s.T().Run("DynamicFeeTransaction", func(t *testing.T) {
   246  		s.SetupTest()
   247  
   248  		gas := hexutil.Uint64(21000)
   249  		args := SendTxArgs{
   250  			From:                 fromAddress,
   251  			To:                   toAddress,
   252  			Gas:                  &gas,
   253  			Value:                value,
   254  			MaxFeePerGas:         testGasPrice,
   255  			MaxPriorityFeePerGas: testGasPrice,
   256  		}
   257  		s.setupBuildTransactionMocks(args, selectedAccount)
   258  
   259  		tx, _, err := s.manager.ValidateAndBuildTransaction(chainID, args, -1)
   260  		s.NoError(err)
   261  		s.Equal(tx.Gas(), uint64(gas), "The gas shouldn't be estimated, but should use the gas from the Tx")
   262  		s.Equal(tx.GasFeeCap(), expectedGasPrice, "The maxFeePerGas should be the same as in the original Tx")
   263  		s.Equal(tx.GasTipCap(), expectedGasPrice, "The maxPriorityFeePerGas should be the same as in the original Tx")
   264  		s.Equal(tx.Type(), uint8(gethtypes.DynamicFeeTxType), "The transaction type should be DynamicFeeTxType")
   265  	})
   266  
   267  	s.T().Run("DynamicFeeTransaction with gas estimation", func(t *testing.T) {
   268  		s.SetupTest()
   269  		args := SendTxArgs{
   270  			From:                 fromAddress,
   271  			To:                   toAddress,
   272  			Value:                value,
   273  			MaxFeePerGas:         testGasPrice,
   274  			MaxPriorityFeePerGas: testGasPrice,
   275  		}
   276  		s.setupBuildTransactionMocks(args, selectedAccount)
   277  
   278  		tx, _, err := s.manager.ValidateAndBuildTransaction(chainID, args, -1)
   279  		s.NoError(err)
   280  		s.Equal(tx.Gas(), expectedGas, "The gas should be estimated if not present in the original Tx")
   281  		s.Equal(tx.Nonce(), expectedNonce, "The nonce should be added if not present in the original Tx")
   282  		s.Equal(tx.GasFeeCap(), expectedGasPrice, "The maxFeePerGas should be the same as in the original Tx")
   283  		s.Equal(tx.GasTipCap(), expectedGasPrice, "The maxPriorityFeePerGas should be the same as in the original Tx")
   284  		s.Equal(tx.Type(), uint8(gethtypes.DynamicFeeTxType), "The transaction type should be DynamicFeeTxType")
   285  	})
   286  
   287  	s.T().Run("LegacyTransaction", func(t *testing.T) {
   288  		s.SetupTest()
   289  
   290  		gas := hexutil.Uint64(21000)
   291  		gasPrice := (*hexutil.Big)(big.NewInt(10))
   292  		args := SendTxArgs{
   293  			From:     fromAddress,
   294  			To:       toAddress,
   295  			Value:    value,
   296  			Gas:      &gas,
   297  			GasPrice: gasPrice,
   298  		}
   299  		s.setupBuildTransactionMocks(args, selectedAccount)
   300  
   301  		tx, _, err := s.manager.ValidateAndBuildTransaction(chainID, args, -1)
   302  		s.NoError(err)
   303  		s.Equal(tx.Gas(), uint64(gas), "The gas shouldn't be estimated, but should use the gas from the Tx")
   304  		s.Equal(tx.GasPrice(), expectedGasPrice, "The gasPrice should be the same as in the original Tx")
   305  		s.Equal(tx.Type(), uint8(gethtypes.LegacyTxType), "The transaction type should be LegacyTxType")
   306  	})
   307  	s.T().Run("LegacyTransaction without gas estimation", func(t *testing.T) {
   308  		s.SetupTest()
   309  
   310  		args := SendTxArgs{
   311  			From:  fromAddress,
   312  			To:    toAddress,
   313  			Value: value,
   314  		}
   315  		s.setupBuildTransactionMocks(args, selectedAccount)
   316  
   317  		tx, _, err := s.manager.ValidateAndBuildTransaction(chainID, args, -1)
   318  		s.NoError(err)
   319  		s.Equal(tx.Gas(), expectedGas, "The gas should be estimated if not present in the original Tx")
   320  		s.Equal(tx.GasPrice(), expectedGasPrice, "The gasPrice should be estimated if not present in the original Tx")
   321  		s.Equal(tx.Type(), uint8(gethtypes.LegacyTxType), "The transaction type should be LegacyTxType")
   322  	})
   323  }
   324  
   325  func (s *TransactorSuite) TestArgsValidation() {
   326  	args := SendTxArgs{
   327  		From:  account.FromAddress(utils.TestConfig.Account1.WalletAddress),
   328  		To:    account.ToAddress(utils.TestConfig.Account2.WalletAddress),
   329  		Data:  types.HexBytes([]byte{0x01, 0x02}),
   330  		Input: types.HexBytes([]byte{0x02, 0x01}),
   331  	}
   332  	s.False(args.Valid())
   333  	selectedAccount := &account.SelectedExtKey{
   334  		Address: account.FromAddress(utils.TestConfig.Account1.WalletAddress),
   335  	}
   336  	_, _, err := s.manager.SendTransaction(args, selectedAccount, -1)
   337  	s.EqualError(err, ErrInvalidSendTxArgs.Error())
   338  }
   339  
   340  func (s *TransactorSuite) TestAccountMismatch() {
   341  	args := SendTxArgs{
   342  		From: account.FromAddress(utils.TestConfig.Account1.WalletAddress),
   343  		To:   account.ToAddress(utils.TestConfig.Account2.WalletAddress),
   344  	}
   345  
   346  	var err error
   347  
   348  	// missing account
   349  	_, _, err = s.manager.SendTransaction(args, nil, -1)
   350  	s.EqualError(err, account.ErrNoAccountSelected.Error())
   351  
   352  	// mismatched accounts
   353  	selectedAccount := &account.SelectedExtKey{
   354  		Address: account.FromAddress(utils.TestConfig.Account2.WalletAddress),
   355  	}
   356  	_, _, err = s.manager.SendTransaction(args, selectedAccount, -1)
   357  	s.EqualError(err, ErrInvalidTxSender.Error())
   358  }
   359  
   360  func (s *TransactorSuite) TestSendTransactionWithSignature() {
   361  	privKey, err := crypto.GenerateKey()
   362  	s.Require().NoError(err)
   363  	address := crypto.PubkeyToAddress(privKey.PublicKey)
   364  
   365  	scenarios := []struct {
   366  		nonceFromNetwork hexutil.Uint64
   367  		txNonce          hexutil.Uint64
   368  		expectError      bool
   369  	}{
   370  		{
   371  			nonceFromNetwork: hexutil.Uint64(0),
   372  			txNonce:          hexutil.Uint64(0),
   373  			expectError:      false,
   374  		},
   375  		{
   376  			nonceFromNetwork: hexutil.Uint64(0),
   377  			txNonce:          hexutil.Uint64(1),
   378  			expectError:      true,
   379  		},
   380  	}
   381  
   382  	for _, localScenario := range scenarios {
   383  		// to satisfy gosec: C601 checks
   384  		scenario := localScenario
   385  		desc := fmt.Sprintf("nonceFromNetwork: %d, tx nonce: %d, expect error: %v", scenario.nonceFromNetwork, scenario.txNonce, scenario.expectError)
   386  		s.T().Run(desc, func(t *testing.T) {
   387  			nonce := scenario.txNonce
   388  			from := address
   389  			to := address
   390  			value := (*hexutil.Big)(big.NewInt(10))
   391  			gas := hexutil.Uint64(21000)
   392  			gasPrice := (*hexutil.Big)(big.NewInt(2000000000))
   393  			data := []byte{}
   394  			chainID := big.NewInt(int64(s.nodeConfig.NetworkID))
   395  			args := SendTxArgs{
   396  				From:     from,
   397  				To:       &to,
   398  				Gas:      &gas,
   399  				GasPrice: gasPrice,
   400  				Value:    value,
   401  				Nonce:    &nonce,
   402  				Data:     nil,
   403  			}
   404  
   405  			// simulate transaction signed externally
   406  			signer := gethtypes.NewLondonSigner(chainID)
   407  			tx := gethtypes.NewTransaction(uint64(nonce), common.Address(to), (*big.Int)(value), uint64(gas), (*big.Int)(gasPrice), data)
   408  			hash := signer.Hash(tx)
   409  			sig, err := gethcrypto.Sign(hash[:], privKey)
   410  			s.Require().NoError(err)
   411  			txWithSig, err := tx.WithSignature(signer, sig)
   412  			s.Require().NoError(err)
   413  			expectedEncodedTx, err := rlp.EncodeToBytes(txWithSig)
   414  			s.Require().NoError(err)
   415  
   416  			s.txServiceMock.EXPECT().
   417  				GetTransactionCount(gomock.Any(), common.Address(address), gethrpc.PendingBlockNumber).
   418  				Return(&scenario.nonceFromNetwork, nil)
   419  
   420  			if !scenario.expectError {
   421  				s.txServiceMock.EXPECT().
   422  					SendRawTransaction(gomock.Any(), hexutil.Bytes(expectedEncodedTx)).
   423  					Return(common.Hash{}, nil)
   424  			}
   425  
   426  			tx, err = s.manager.BuildTransactionWithSignature(s.nodeConfig.NetworkID, args, sig)
   427  			if scenario.expectError {
   428  				s.Error(err)
   429  			} else {
   430  				s.NoError(err)
   431  
   432  				_, err = s.manager.SendTransactionWithSignature(common.Address(args.From), args.Symbol, args.MultiTransactionID, tx)
   433  				if scenario.expectError {
   434  					s.Error(err)
   435  				} else {
   436  					s.NoError(err)
   437  				}
   438  			}
   439  		})
   440  	}
   441  }
   442  
   443  func (s *TransactorSuite) TestSendTransactionWithSignature_InvalidSignature() {
   444  	args := SendTxArgs{}
   445  	_, err := s.manager.BuildTransactionWithSignature(1, args, []byte{})
   446  	s.Equal(ErrInvalidSignatureSize, err)
   447  }
   448  
   449  func (s *TransactorSuite) TestHashTransaction() {
   450  	privKey, err := crypto.GenerateKey()
   451  	s.Require().NoError(err)
   452  	address := crypto.PubkeyToAddress(privKey.PublicKey)
   453  
   454  	remoteNonce := hexutil.Uint64(1)
   455  	txNonce := hexutil.Uint64(0)
   456  	from := address
   457  	to := address
   458  	value := (*hexutil.Big)(big.NewInt(10))
   459  	gas := hexutil.Uint64(21000)
   460  	gasPrice := (*hexutil.Big)(big.NewInt(2000000000))
   461  
   462  	args := SendTxArgs{
   463  		From:     from,
   464  		To:       &to,
   465  		Gas:      &gas,
   466  		GasPrice: gasPrice,
   467  		Value:    value,
   468  		Nonce:    &txNonce,
   469  		Data:     nil,
   470  	}
   471  
   472  	s.txServiceMock.EXPECT().
   473  		GetTransactionCount(gomock.Any(), common.Address(address), gethrpc.PendingBlockNumber).
   474  		Return(&remoteNonce, nil)
   475  
   476  	newArgs, hash, err := s.manager.HashTransaction(args)
   477  	s.Require().NoError(err)
   478  	// args should be updated with the right nonce
   479  	s.NotEqual(*args.Nonce, *newArgs.Nonce)
   480  	s.Equal(remoteNonce, *newArgs.Nonce)
   481  
   482  	s.NotEqual(common.Hash{}, hash)
   483  }
   484  
   485  func (s *TransactorSuite) TestStoreAndTrackPendingTx() {
   486  	s.Nil(s.manager.pendingTracker)
   487  
   488  	// Empty tracker doesn't produce error
   489  	err := s.manager.StoreAndTrackPendingTx(common.Address{}, "", 0, wallet_common.MultiTransactionIDType(0), nil)
   490  	s.NoError(err)
   491  }