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 }