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 }