package transactions import ( "fmt" "math/big" "reflect" "testing" "time" "github.com/golang/mock/gomock" "github.com/stretchr/testify/suite" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" gethtypes "github.com/ethereum/go-ethereum/core/types" gethcrypto "github.com/ethereum/go-ethereum/crypto" gethparams "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" gethrpc "github.com/ethereum/go-ethereum/rpc" "github.com/status-im/status-go/account" "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/params" "github.com/status-im/status-go/rpc" wallet_common "github.com/status-im/status-go/services/wallet/common" "github.com/status-im/status-go/sqlite" "github.com/status-im/status-go/t/utils" "github.com/status-im/status-go/transactions/fake" ) func TestTransactorSuite(t *testing.T) { utils.Init() suite.Run(t, new(TransactorSuite)) } type TransactorSuite struct { suite.Suite server *gethrpc.Server client *gethrpc.Client txServiceMockCtrl *gomock.Controller txServiceMock *fake.MockPublicTransactionPoolAPI nodeConfig *params.NodeConfig manager *Transactor } func (s *TransactorSuite) SetupTest() { s.txServiceMockCtrl = gomock.NewController(s.T()) s.server, s.txServiceMock = fake.NewTestServer(s.txServiceMockCtrl) s.client = gethrpc.DialInProc(s.server) // expected by simulated backend chainID := gethparams.AllEthashProtocolChanges.ChainID.Uint64() db, err := sqlite.OpenUnecryptedDB(sqlite.InMemoryPath) // dummy to make rpc.Client happy s.Require().NoError(err) rpcClient, _ := rpc.NewClient(s.client, chainID, params.UpstreamRPCConfig{}, nil, db, nil) rpcClient.UpstreamChainID = chainID nodeConfig, err := utils.MakeTestNodeConfigWithDataDir("", "/tmp", chainID) s.Require().NoError(err) s.nodeConfig = nodeConfig s.manager = NewTransactor() s.manager.sendTxTimeout = time.Second s.manager.SetNetworkID(chainID) s.manager.SetRPC(rpcClient, time.Second) } func (s *TransactorSuite) TearDownTest() { s.txServiceMockCtrl.Finish() s.server.Stop() s.client.Close() } var ( testGas = hexutil.Uint64(defaultGas + 1) testGasPrice = (*hexutil.Big)(big.NewInt(10)) testNonce = hexutil.Uint64(10) ) func (s *TransactorSuite) setupTransactionPoolAPI(args SendTxArgs, returnNonce, resultNonce hexutil.Uint64, account *account.SelectedExtKey, txErr error) { // Expect calls to gas functions only if there are no user defined values. // And also set the expected gas and gas price for RLP encoding the expected tx. var usedGas hexutil.Uint64 var usedGasPrice *big.Int s.txServiceMock.EXPECT().GetTransactionCount(gomock.Any(), gomock.Eq(common.Address(account.Address)), gethrpc.PendingBlockNumber).Return(&returnNonce, nil) if !args.IsDynamicFeeTx() { if args.GasPrice == nil { usedGasPrice = (*big.Int)(testGasPrice) s.txServiceMock.EXPECT().GasPrice(gomock.Any()).Return(testGasPrice, nil) } else { usedGasPrice = (*big.Int)(args.GasPrice) } } if args.Gas == nil { s.txServiceMock.EXPECT().EstimateGas(gomock.Any(), gomock.Any()).Return(testGas, nil) usedGas = testGas } else { usedGas = *args.Gas } // Prepare the transaction and RLP encode it. data := s.rlpEncodeTx(args, s.nodeConfig, account, &resultNonce, usedGas, usedGasPrice) // Expect the RLP encoded transaction. s.txServiceMock.EXPECT().SendRawTransaction(gomock.Any(), data).Return(common.Hash{}, txErr) } func (s *TransactorSuite) rlpEncodeTx(args SendTxArgs, config *params.NodeConfig, account *account.SelectedExtKey, nonce *hexutil.Uint64, gas hexutil.Uint64, gasPrice *big.Int) hexutil.Bytes { var txData gethtypes.TxData to := common.Address(*args.To) if args.IsDynamicFeeTx() { gasTipCap := (*big.Int)(args.MaxPriorityFeePerGas) gasFeeCap := (*big.Int)(args.MaxFeePerGas) txData = &gethtypes.DynamicFeeTx{ Nonce: uint64(*nonce), Gas: uint64(gas), GasTipCap: gasTipCap, GasFeeCap: gasFeeCap, To: &to, Value: args.Value.ToInt(), Data: args.GetInput(), } } else { txData = &gethtypes.LegacyTx{ Nonce: uint64(*nonce), GasPrice: gasPrice, Gas: uint64(gas), To: &to, Value: args.Value.ToInt(), Data: args.GetInput(), } } newTx := gethtypes.NewTx(txData) chainID := big.NewInt(int64(s.nodeConfig.NetworkID)) signedTx, err := gethtypes.SignTx(newTx, gethtypes.NewLondonSigner(chainID), account.AccountKey.PrivateKey) s.NoError(err) data, err := signedTx.MarshalBinary() s.NoError(err) return hexutil.Bytes(data) } func (s *TransactorSuite) TestGasValues() { key, _ := gethcrypto.GenerateKey() selectedAccount := &account.SelectedExtKey{ Address: account.FromAddress(utils.TestConfig.Account1.WalletAddress), AccountKey: &types.Key{PrivateKey: key}, } testCases := []struct { name string gas *hexutil.Uint64 gasPrice *hexutil.Big maxFeePerGas *hexutil.Big maxPriorityFeePerGas *hexutil.Big }{ { "noGasDef", nil, nil, nil, nil, }, { "gasDefined", &testGas, nil, nil, nil, }, { "gasPriceDefined", nil, testGasPrice, nil, nil, }, { "nilSignTransactionSpecificArgs", nil, nil, nil, nil, }, { "maxFeeAndPriorityset", nil, nil, testGasPrice, testGasPrice, }, } for _, testCase := range testCases { s.T().Run(testCase.name, func(t *testing.T) { s.SetupTest() args := SendTxArgs{ From: account.FromAddress(utils.TestConfig.Account1.WalletAddress), To: account.ToAddress(utils.TestConfig.Account2.WalletAddress), Gas: testCase.gas, GasPrice: testCase.gasPrice, MaxFeePerGas: testCase.maxFeePerGas, MaxPriorityFeePerGas: testCase.maxPriorityFeePerGas, } s.setupTransactionPoolAPI(args, testNonce, testNonce, selectedAccount, nil) hash, err := s.manager.SendTransaction(args, selectedAccount) s.NoError(err) s.False(reflect.DeepEqual(hash, common.Hash{})) }) } } func (s *TransactorSuite) setupBuildTransactionMocks(args SendTxArgs, account *account.SelectedExtKey) { s.txServiceMock.EXPECT().GetTransactionCount(gomock.Any(), gomock.Eq(common.Address(account.Address)), gethrpc.PendingBlockNumber).Return(&testNonce, nil) if !args.IsDynamicFeeTx() && args.GasPrice == nil { s.txServiceMock.EXPECT().GasPrice(gomock.Any()).Return(testGasPrice, nil) } if args.Gas == nil { s.txServiceMock.EXPECT().EstimateGas(gomock.Any(), gomock.Any()).Return(testGas, nil) } } func (s *TransactorSuite) TestBuildAndValidateTransaction() { key, _ := gethcrypto.GenerateKey() selectedAccount := &account.SelectedExtKey{ Address: account.FromAddress(utils.TestConfig.Account1.WalletAddress), AccountKey: &types.Key{PrivateKey: key}, } chainID := s.nodeConfig.NetworkID fromAddress := account.FromAddress(utils.TestConfig.Account1.WalletAddress) toAddress := account.ToAddress(utils.TestConfig.Account2.WalletAddress) value := (*hexutil.Big)(big.NewInt(10)) expectedGasPrice := (*big.Int)(testGasPrice) expectedGas := uint64(testGas) expectedNonce := uint64(testNonce) s.T().Run("DynamicFeeTransaction", func(t *testing.T) { s.SetupTest() gas := hexutil.Uint64(21000) args := SendTxArgs{ From: fromAddress, To: toAddress, Gas: &gas, Value: value, MaxFeePerGas: testGasPrice, MaxPriorityFeePerGas: testGasPrice, } s.setupBuildTransactionMocks(args, selectedAccount) tx, err := s.manager.ValidateAndBuildTransaction(chainID, args) s.NoError(err) s.Equal(tx.Gas(), uint64(gas), "The gas shouldn't be estimated, but should use the gas from the Tx") s.Equal(tx.GasFeeCap(), expectedGasPrice, "The maxFeePerGas should be the same as in the original Tx") s.Equal(tx.GasTipCap(), expectedGasPrice, "The maxPriorityFeePerGas should be the same as in the original Tx") s.Equal(tx.Type(), uint8(gethtypes.DynamicFeeTxType), "The transaction type should be DynamicFeeTxType") }) s.T().Run("DynamicFeeTransaction with gas estimation", func(t *testing.T) { s.SetupTest() args := SendTxArgs{ From: fromAddress, To: toAddress, Value: value, MaxFeePerGas: testGasPrice, MaxPriorityFeePerGas: testGasPrice, } s.setupBuildTransactionMocks(args, selectedAccount) tx, err := s.manager.ValidateAndBuildTransaction(chainID, args) s.NoError(err) s.Equal(tx.Gas(), expectedGas, "The gas should be estimated if not present in the original Tx") s.Equal(tx.Nonce(), expectedNonce, "The nonce should be added if not present in the original Tx") s.Equal(tx.GasFeeCap(), expectedGasPrice, "The maxFeePerGas should be the same as in the original Tx") s.Equal(tx.GasTipCap(), expectedGasPrice, "The maxPriorityFeePerGas should be the same as in the original Tx") s.Equal(tx.Type(), uint8(gethtypes.DynamicFeeTxType), "The transaction type should be DynamicFeeTxType") }) s.T().Run("LegacyTransaction", func(t *testing.T) { s.SetupTest() gas := hexutil.Uint64(21000) gasPrice := (*hexutil.Big)(big.NewInt(10)) args := SendTxArgs{ From: fromAddress, To: toAddress, Value: value, Gas: &gas, GasPrice: gasPrice, } s.setupBuildTransactionMocks(args, selectedAccount) tx, err := s.manager.ValidateAndBuildTransaction(chainID, args) s.NoError(err) s.Equal(tx.Gas(), uint64(gas), "The gas shouldn't be estimated, but should use the gas from the Tx") s.Equal(tx.GasPrice(), expectedGasPrice, "The gasPrice should be the same as in the original Tx") s.Equal(tx.Type(), uint8(gethtypes.LegacyTxType), "The transaction type should be LegacyTxType") }) s.T().Run("LegacyTransaction without gas estimation", func(t *testing.T) { s.SetupTest() args := SendTxArgs{ From: fromAddress, To: toAddress, Value: value, } s.setupBuildTransactionMocks(args, selectedAccount) tx, err := s.manager.ValidateAndBuildTransaction(chainID, args) s.NoError(err) s.Equal(tx.Gas(), expectedGas, "The gas should be estimated if not present in the original Tx") s.Equal(tx.GasPrice(), expectedGasPrice, "The gasPrice should be estimated if not present in the original Tx") s.Equal(tx.Type(), uint8(gethtypes.LegacyTxType), "The transaction type should be LegacyTxType") }) } func (s *TransactorSuite) TestArgsValidation() { args := SendTxArgs{ From: account.FromAddress(utils.TestConfig.Account1.WalletAddress), To: account.ToAddress(utils.TestConfig.Account2.WalletAddress), Data: types.HexBytes([]byte{0x01, 0x02}), Input: types.HexBytes([]byte{0x02, 0x01}), } s.False(args.Valid()) selectedAccount := &account.SelectedExtKey{ Address: account.FromAddress(utils.TestConfig.Account1.WalletAddress), } _, err := s.manager.SendTransaction(args, selectedAccount) s.EqualError(err, ErrInvalidSendTxArgs.Error()) } func (s *TransactorSuite) TestAccountMismatch() { args := SendTxArgs{ From: account.FromAddress(utils.TestConfig.Account1.WalletAddress), To: account.ToAddress(utils.TestConfig.Account2.WalletAddress), } var err error // missing account _, err = s.manager.SendTransaction(args, nil) s.EqualError(err, account.ErrNoAccountSelected.Error()) // mismatched accounts selectedAccount := &account.SelectedExtKey{ Address: account.FromAddress(utils.TestConfig.Account2.WalletAddress), } _, err = s.manager.SendTransaction(args, selectedAccount) s.EqualError(err, ErrInvalidTxSender.Error()) } func (s *TransactorSuite) TestSendTransactionWithSignature() { privKey, err := crypto.GenerateKey() s.Require().NoError(err) address := crypto.PubkeyToAddress(privKey.PublicKey) scenarios := []struct { nonceFromNetwork hexutil.Uint64 txNonce hexutil.Uint64 expectError bool }{ { nonceFromNetwork: hexutil.Uint64(0), txNonce: hexutil.Uint64(0), expectError: false, }, { nonceFromNetwork: hexutil.Uint64(0), txNonce: hexutil.Uint64(1), expectError: true, }, } for _, localScenario := range scenarios { // to satisfy gosec: C601 checks scenario := localScenario desc := fmt.Sprintf("nonceFromNetwork: %d, tx nonce: %d, expect error: %v", scenario.nonceFromNetwork, scenario.txNonce, scenario.expectError) s.T().Run(desc, func(t *testing.T) { nonce := scenario.txNonce from := address to := address value := (*hexutil.Big)(big.NewInt(10)) gas := hexutil.Uint64(21000) gasPrice := (*hexutil.Big)(big.NewInt(2000000000)) data := []byte{} chainID := big.NewInt(int64(s.nodeConfig.NetworkID)) args := SendTxArgs{ From: from, To: &to, Gas: &gas, GasPrice: gasPrice, Value: value, Nonce: &nonce, Data: nil, } // simulate transaction signed externally signer := gethtypes.NewLondonSigner(chainID) tx := gethtypes.NewTransaction(uint64(nonce), common.Address(to), (*big.Int)(value), uint64(gas), (*big.Int)(gasPrice), data) hash := signer.Hash(tx) sig, err := gethcrypto.Sign(hash[:], privKey) s.Require().NoError(err) txWithSig, err := tx.WithSignature(signer, sig) s.Require().NoError(err) expectedEncodedTx, err := rlp.EncodeToBytes(txWithSig) s.Require().NoError(err) s.txServiceMock.EXPECT(). GetTransactionCount(gomock.Any(), common.Address(address), gethrpc.PendingBlockNumber). Return(&scenario.nonceFromNetwork, nil) if !scenario.expectError { s.txServiceMock.EXPECT(). SendRawTransaction(gomock.Any(), hexutil.Bytes(expectedEncodedTx)). Return(common.Hash{}, nil) } tx, err = s.manager.BuildTransactionWithSignature(s.nodeConfig.NetworkID, args, sig) if scenario.expectError { s.Error(err) } else { s.NoError(err) _, err = s.manager.SendTransactionWithSignature(common.Address(args.From), args.Symbol, args.MultiTransactionID, tx) if scenario.expectError { s.Error(err) } else { s.NoError(err) } } }) } } func (s *TransactorSuite) TestSendTransactionWithSignature_InvalidSignature() { args := SendTxArgs{} _, err := s.manager.BuildTransactionWithSignature(1, args, []byte{}) s.Equal(ErrInvalidSignatureSize, err) } func (s *TransactorSuite) TestHashTransaction() { privKey, err := crypto.GenerateKey() s.Require().NoError(err) address := crypto.PubkeyToAddress(privKey.PublicKey) remoteNonce := hexutil.Uint64(1) txNonce := hexutil.Uint64(0) from := address to := address value := (*hexutil.Big)(big.NewInt(10)) gas := hexutil.Uint64(21000) gasPrice := (*hexutil.Big)(big.NewInt(2000000000)) args := SendTxArgs{ From: from, To: &to, Gas: &gas, GasPrice: gasPrice, Value: value, Nonce: &txNonce, Data: nil, } s.txServiceMock.EXPECT(). GetTransactionCount(gomock.Any(), common.Address(address), gethrpc.PendingBlockNumber). Return(&remoteNonce, nil) newArgs, hash, err := s.manager.HashTransaction(args) s.Require().NoError(err) // args should be updated with the right nonce s.NotEqual(*args.Nonce, *newArgs.Nonce) s.Equal(remoteNonce, *newArgs.Nonce) s.NotEqual(common.Hash{}, hash) } func (s *TransactorSuite) TestStoreAndTrackPendingTx() { s.Nil(s.manager.pendingTracker) // Empty tracker doesn't produce error err := s.manager.StoreAndTrackPendingTx(common.Address{}, "", 0, wallet_common.MultiTransactionIDType(0), nil) s.NoError(err) }