1use async_trait::async_trait;
8use chrono::Utc;
9use eyre::Result;
10use std::sync::Arc;
11use tracing::{debug, error, info, warn};
12
13use crate::{
14 constants::{
15 matches_known_transaction, ALREADY_SUBMITTED_PATTERNS, DEFAULT_EVM_GAS_LIMIT_ESTIMATION,
16 GAS_LIMIT_BUFFER_MULTIPLIER, MAX_NONCE_TOO_HIGH_RETRIES, NONCE_TOO_HIGH_PATTERNS,
17 },
18 domain::{
19 evm::is_noop,
20 transaction::{
21 evm::{ensure_status, ensure_status_one_of, PriceCalculator, PriceCalculatorTrait},
22 Transaction,
23 },
24 EvmTransactionValidationError, EvmTransactionValidator,
25 },
26 jobs::{
27 JobProducer, JobProducerTrait, RelayerHealthCheck, StatusCheckContext, TransactionSend,
28 TransactionStatusCheck,
29 },
30 models::{
31 produce_transaction_update_notification_payload, EvmNetwork, EvmTransactionData,
32 NetworkRepoModel, NetworkTransactionData, NetworkTransactionRequest, NetworkType,
33 RelayerEvmPolicy, RelayerRepoModel, TransactionError, TransactionMetadata,
34 TransactionRepoModel, TransactionStatus, TransactionUpdateRequest,
35 },
36 repositories::{
37 NetworkRepository, NetworkRepositoryStorage, RelayerRepository, RelayerRepositoryStorage,
38 Repository, TransactionCounterRepositoryStorage, TransactionCounterTrait,
39 TransactionRepository, TransactionRepositoryStorage,
40 },
41 services::{
42 gas::evm_gas_price::EvmGasPriceService,
43 provider::{EvmProvider, EvmProviderTrait},
44 signer::{EvmSigner, Signer},
45 },
46 utils::{calculate_scheduled_timestamp, get_evm_default_gas_limit_for_tx},
47};
48
49use super::PriceParams;
50
51pub(super) const TX_NONCE_RECONCILE_TRIGGER: &str = "tx_nonce_reconcile_trigger";
55
56#[derive(Debug, Clone, PartialEq)]
67pub(super) enum SubmissionErrorKind {
68 NonceTooLow,
69 AlreadyKnown,
70 ReplacementUnderpriced,
71 NonceTooHigh,
72 Other(String),
73}
74
75#[allow(dead_code)]
76pub struct EvmRelayerTransaction<P, RR, NR, TR, J, S, TCR, PC>
77where
78 P: EvmProviderTrait,
79 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
80 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
81 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
82 J: JobProducerTrait + Send + Sync + 'static,
83 S: Signer + Send + Sync + 'static,
84 TCR: TransactionCounterTrait + Send + Sync + 'static,
85 PC: PriceCalculatorTrait,
86{
87 provider: P,
88 relayer_repository: Arc<RR>,
89 network_repository: Arc<NR>,
90 transaction_repository: Arc<TR>,
91 job_producer: Arc<J>,
92 signer: S,
93 relayer: RelayerRepoModel,
94 transaction_counter_service: Arc<TCR>,
95 price_calculator: PC,
96}
97
98#[allow(dead_code, clippy::too_many_arguments)]
99impl<P, RR, NR, TR, J, S, TCR, PC> EvmRelayerTransaction<P, RR, NR, TR, J, S, TCR, PC>
100where
101 P: EvmProviderTrait,
102 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
103 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
104 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
105 J: JobProducerTrait + Send + Sync + 'static,
106 S: Signer + Send + Sync + 'static,
107 TCR: TransactionCounterTrait + Send + Sync + 'static,
108 PC: PriceCalculatorTrait,
109{
110 pub fn new(
127 relayer: RelayerRepoModel,
128 provider: P,
129 relayer_repository: Arc<RR>,
130 network_repository: Arc<NR>,
131 transaction_repository: Arc<TR>,
132 transaction_counter_service: Arc<TCR>,
133 job_producer: Arc<J>,
134 price_calculator: PC,
135 signer: S,
136 ) -> Result<Self, TransactionError> {
137 Ok(Self {
138 relayer,
139 provider,
140 relayer_repository,
141 network_repository,
142 transaction_repository,
143 transaction_counter_service,
144 job_producer,
145 price_calculator,
146 signer,
147 })
148 }
149
150 pub fn provider(&self) -> &P {
152 &self.provider
153 }
154
155 pub fn relayer(&self) -> &RelayerRepoModel {
157 &self.relayer
158 }
159
160 pub fn network_repository(&self) -> &NR {
162 &self.network_repository
163 }
164
165 pub fn job_producer(&self) -> &J {
167 &self.job_producer
168 }
169
170 pub fn transaction_repository(&self) -> &TR {
171 &self.transaction_repository
172 }
173
174 fn classify_submission_error(error: &impl std::fmt::Display) -> SubmissionErrorKind {
181 let original = error.to_string();
182 let error_msg = original.to_lowercase();
183
184 for pattern in ALREADY_SUBMITTED_PATTERNS {
187 if error_msg.contains(pattern) {
188 return match *pattern {
189 "nonce too low" | "nonce is too low" => SubmissionErrorKind::NonceTooLow,
190 "replacement transaction underpriced" => {
191 SubmissionErrorKind::ReplacementUnderpriced
192 }
193 _ => SubmissionErrorKind::AlreadyKnown,
195 };
196 }
197 }
198
199 if matches_known_transaction(&error_msg) {
202 return SubmissionErrorKind::AlreadyKnown;
203 }
204
205 for pattern in NONCE_TOO_HIGH_PATTERNS {
208 if error_msg.contains(pattern) {
209 return SubmissionErrorKind::NonceTooHigh;
210 }
211 }
212
213 SubmissionErrorKind::Other(original)
214 }
215
216 fn is_already_submitted_error(error: &impl std::fmt::Display) -> bool {
219 matches!(
220 Self::classify_submission_error(error),
221 SubmissionErrorKind::NonceTooLow
222 | SubmissionErrorKind::AlreadyKnown
223 | SubmissionErrorKind::ReplacementUnderpriced
224 )
225 }
226
227 pub(super) async fn schedule_status_check(
231 &self,
232 tx: &TransactionRepoModel,
233 delay_seconds: Option<i64>,
234 metadata: Option<std::collections::HashMap<String, String>>,
235 ) -> Result<(), TransactionError> {
236 let delay = delay_seconds.map(calculate_scheduled_timestamp);
237 let mut job = TransactionStatusCheck::new(
238 tx.id.clone(),
239 tx.relayer_id.clone(),
240 crate::models::NetworkType::Evm,
241 );
242 if let Some(meta) = metadata {
243 job = job.with_metadata(meta);
244 }
245 self.job_producer()
246 .produce_check_transaction_status_job(job, delay)
247 .await
248 .map_err(|e| {
249 TransactionError::UnexpectedError(format!("Failed to schedule status check: {e}"))
250 })
251 }
252
253 pub(super) async fn schedule_nonce_recovery_status_check(
260 &self,
261 tx: &TransactionRepoModel,
262 error_kind: &SubmissionErrorKind,
263 ) -> Result<(), TransactionError> {
264 let mut metadata = std::collections::HashMap::new();
265 metadata.insert(
266 TX_NONCE_RECONCILE_TRIGGER.to_string(),
267 format!("{error_kind:?}"),
268 );
269 self.schedule_status_check(tx, None, Some(metadata)).await
270 }
271
272 pub(super) async fn schedule_relayer_nonce_health_job(
278 &self,
279 tx: &TransactionRepoModel,
280 ) -> Result<(), TransactionError> {
281 let nonce_hint = tx
284 .network_data
285 .get_evm_transaction_data()
286 .ok()
287 .and_then(|d| d.nonce);
288 let job = match nonce_hint {
289 Some(nonce) => RelayerHealthCheck::nonce_health_with_hint(tx.relayer_id.clone(), nonce),
290 None => RelayerHealthCheck::nonce_health(tx.relayer_id.clone()),
291 };
292
293 self.job_producer()
294 .produce_relayer_health_check_job(job, None)
295 .await
296 .map_err(|e| {
297 TransactionError::UnexpectedError(format!(
298 "Failed to schedule nonce health check: {e}"
299 ))
300 })
301 }
302
303 pub(super) async fn handle_nonce_too_high(&self, tx: &TransactionRepoModel, context: &str) {
306 let retry_count = tx
307 .metadata
308 .as_ref()
309 .map(|m| m.nonce_too_high_retries)
310 .unwrap_or(0);
311
312 let new_count = retry_count + 1;
313
314 let update = TransactionUpdateRequest {
316 metadata: Some(TransactionMetadata {
317 nonce_too_high_retries: new_count,
318 ..tx.metadata.clone().unwrap_or_default()
319 }),
320 status_reason: Some(format!("Nonce too high (attempt {new_count})")),
321 ..Default::default()
322 };
323 if let Err(update_err) = self
324 .transaction_repository
325 .partial_update(tx.id.clone(), update)
326 .await
327 {
328 warn!(
329 tx_id = %tx.id,
330 error = %update_err,
331 "failed to persist nonce_too_high_retries metadata {context}"
332 );
333 }
334
335 if new_count >= MAX_NONCE_TOO_HIGH_RETRIES {
336 warn!(
337 tx_id = %tx.id,
338 "nonce too high after {} attempts {context}, scheduling nonce health check",
339 new_count
340 );
341 if let Err(schedule_err) = self.schedule_relayer_nonce_health_job(tx).await {
342 error!(
343 tx_id = %tx.id,
344 error = %schedule_err,
345 "failed to schedule nonce health check {context}"
346 );
347 }
348 } else {
349 warn!(
350 tx_id = %tx.id,
351 "nonce too high {context} (attempt {}/{}), status checker will retry",
352 new_count,
353 MAX_NONCE_TOO_HIGH_RETRIES
354 );
355 }
356 }
357
358 pub(super) async fn send_transaction_submit_job(
360 &self,
361 tx: &TransactionRepoModel,
362 ) -> Result<(), TransactionError> {
363 debug!(
364 tx_id = %tx.id,
365 relayer_id = %tx.relayer_id,
366 "enqueueing submit transaction job"
367 );
368 let job = TransactionSend::submit(tx.id.clone(), tx.relayer_id.clone());
369
370 self.job_producer()
371 .produce_submit_transaction_job(job, None)
372 .await
373 .map_err(|e| {
374 TransactionError::UnexpectedError(format!("Failed to produce submit job: {e}"))
375 })
376 }
377
378 pub(super) async fn send_transaction_resubmit_job(
380 &self,
381 tx: &TransactionRepoModel,
382 ) -> Result<(), TransactionError> {
383 debug!(
384 tx_id = %tx.id,
385 relayer_id = %tx.relayer_id,
386 "enqueueing resubmit transaction job"
387 );
388 let job = TransactionSend::resubmit(tx.id.clone(), tx.relayer_id.clone());
389
390 self.job_producer()
391 .produce_submit_transaction_job(job, None)
392 .await
393 .map_err(|e| {
394 TransactionError::UnexpectedError(format!("Failed to produce resubmit job: {e}"))
395 })
396 }
397
398 pub(super) async fn send_transaction_resend_job(
400 &self,
401 tx: &TransactionRepoModel,
402 ) -> Result<(), TransactionError> {
403 debug!(
404 tx_id = %tx.id,
405 relayer_id = %tx.relayer_id,
406 "enqueueing resend transaction job"
407 );
408 let job = TransactionSend::resend(tx.id.clone(), tx.relayer_id.clone());
409
410 self.job_producer()
411 .produce_submit_transaction_job(job, None)
412 .await
413 .map_err(|e| {
414 TransactionError::UnexpectedError(format!("Failed to produce resend job: {e}"))
415 })
416 }
417
418 pub(super) async fn send_transaction_request_job(
420 &self,
421 tx: &TransactionRepoModel,
422 ) -> Result<(), TransactionError> {
423 use crate::jobs::TransactionRequest;
424
425 let job = TransactionRequest::new(tx.id.clone(), tx.relayer_id.clone());
426
427 self.job_producer()
428 .produce_transaction_request_job(job, None)
429 .await
430 .map_err(|e| {
431 TransactionError::UnexpectedError(format!("Failed to produce request job: {e}"))
432 })
433 }
434
435 pub(super) async fn update_transaction_status(
437 &self,
438 tx: TransactionRepoModel,
439 new_status: TransactionStatus,
440 status_reason: Option<String>,
441 ) -> Result<TransactionRepoModel, TransactionError> {
442 let confirmed_at = if new_status == TransactionStatus::Confirmed {
443 Some(Utc::now().to_rfc3339())
444 } else {
445 None
446 };
447
448 let update_request = TransactionUpdateRequest {
449 status: Some(new_status),
450 confirmed_at,
451 status_reason,
452 ..Default::default()
453 };
454
455 let updated_tx = self
456 .transaction_repository()
457 .partial_update(tx.id.clone(), update_request)
458 .await?;
459
460 if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
461 error!(
462 tx_id = %updated_tx.id,
463 status = ?updated_tx.status,
464 "sending transaction update notification failed: {:?}",
465 e
466 );
467 }
468 Ok(updated_tx)
469 }
470
471 pub(super) async fn send_transaction_update_notification(
476 &self,
477 tx: &TransactionRepoModel,
478 ) -> Result<(), eyre::Report> {
479 if let Some(notification_id) = &self.relayer().notification_id {
480 self.job_producer()
481 .produce_send_notification_job(
482 produce_transaction_update_notification_payload(notification_id, tx),
483 None,
484 )
485 .await?;
486 }
487 Ok(())
488 }
489
490 async fn mark_transaction_as_failed(
504 &self,
505 tx: &TransactionRepoModel,
506 reason: String,
507 error_context: &str,
508 ) -> Result<TransactionRepoModel, TransactionError> {
509 let update = TransactionUpdateRequest {
510 status: Some(TransactionStatus::Failed),
511 status_reason: Some(reason.clone()),
512 ..Default::default()
513 };
514
515 let updated_tx = self
516 .transaction_repository
517 .partial_update(tx.id.clone(), update)
518 .await?;
519
520 if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
521 error!(
522 tx_id = %updated_tx.id,
523 status = ?TransactionStatus::Failed,
524 "sending transaction update notification failed for {}: {:?}",
525 error_context,
526 e
527 );
528 }
529
530 Ok(updated_tx)
531 }
532
533 async fn ensure_sufficient_balance(
545 &self,
546 total_cost: crate::models::U256,
547 ) -> Result<(), TransactionError> {
548 EvmTransactionValidator::validate_sufficient_relayer_balance(
549 total_cost,
550 &self.relayer().address,
551 &self.relayer().policies.get_evm_policy(),
552 &self.provider,
553 )
554 .await
555 .map_err(|validation_error| match validation_error {
556 EvmTransactionValidationError::InsufficientBalance(msg) => {
558 TransactionError::InsufficientBalance(msg)
559 }
560 EvmTransactionValidationError::ProviderError(msg) => {
562 TransactionError::UnexpectedError(format!("Failed to check balance: {msg}"))
563 }
564 EvmTransactionValidationError::ValidationError(msg) => {
566 TransactionError::UnexpectedError(format!("Balance validation error: {msg}"))
567 }
568 })
569 }
570
571 async fn estimate_tx_gas_limit(
579 &self,
580 evm_data: &EvmTransactionData,
581 relayer_policy: &RelayerEvmPolicy,
582 ) -> Result<u64, TransactionError> {
583 if !relayer_policy
584 .gas_limit_estimation
585 .unwrap_or(DEFAULT_EVM_GAS_LIMIT_ESTIMATION)
586 {
587 warn!("gas limit estimation is disabled for relayer");
588 return Err(TransactionError::UnexpectedError(
589 "Gas limit estimation is disabled".to_string(),
590 ));
591 }
592
593 let estimated_gas = self.provider.estimate_gas(evm_data).await.map_err(|e| {
594 warn!(error = ?e, tx_data = ?evm_data, "failed to estimate gas");
595 TransactionError::UnexpectedError(format!("Failed to estimate gas: {e}"))
596 })?;
597
598 Ok(estimated_gas * GAS_LIMIT_BUFFER_MULTIPLIER / 100)
599 }
600}
601
602#[async_trait]
603impl<P, RR, NR, TR, J, S, TCR, PC> Transaction
604 for EvmRelayerTransaction<P, RR, NR, TR, J, S, TCR, PC>
605where
606 P: EvmProviderTrait + Send + Sync + 'static,
607 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
608 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
609 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
610 J: JobProducerTrait + Send + Sync + 'static,
611 S: Signer + Send + Sync + 'static,
612 TCR: TransactionCounterTrait + Send + Sync + 'static,
613 PC: PriceCalculatorTrait + Send + Sync + 'static,
614{
615 async fn prepare_transaction(
625 &self,
626 tx: TransactionRepoModel,
627 ) -> Result<TransactionRepoModel, TransactionError> {
628 debug!(
629 tx_id = %tx.id,
630 relayer_id = %tx.relayer_id,
631 status = ?tx.status,
632 "preparing transaction"
633 );
634
635 if let Err(e) = ensure_status(&tx, TransactionStatus::Pending, Some("prepare_transaction"))
638 {
639 warn!(
640 tx_id = %tx.id,
641 status = ?tx.status,
642 error = %e,
643 "transaction not in Pending status, skipping preparation"
644 );
645 return Ok(tx);
646 }
647
648 let mut evm_data = tx.network_data.get_evm_transaction_data()?;
649 let relayer = self.relayer();
650
651 if evm_data.gas_limit.is_none() {
652 match self
653 .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
654 .await
655 {
656 Ok(estimated_gas_limit) => {
657 evm_data.gas_limit = Some(estimated_gas_limit);
658 }
659 Err(estimation_error) => {
660 error!(
661 tx_id = %tx.id,
662 relayer_id = %tx.relayer_id,
663 error = ?estimation_error,
664 "failed to estimate gas limit"
665 );
666
667 let default_gas_limit = get_evm_default_gas_limit_for_tx(&evm_data);
668 debug!(
669 tx_id = %tx.id,
670 gas_limit = %default_gas_limit,
671 "fallback to default gas limit"
672 );
673 evm_data.gas_limit = Some(default_gas_limit);
674 }
675 }
676 } else {
677 let block = self.provider.get_block_by_number().await;
679 if let Ok(block) = block {
680 let block_gas_limit = block.header.gas_limit;
681 if let Some(gas_limit) = evm_data.gas_limit {
682 if gas_limit > block_gas_limit {
683 let reason = format!(
684 "Transaction gas limit ({gas_limit}) exceeds block gas limit ({block_gas_limit})",
685 );
686 warn!(
687 tx_id = %tx.id,
688 tx_gas_limit = %gas_limit,
689 block_gas_limit = %block_gas_limit,
690 "transaction gas limit exceeds block gas limit"
691 );
692
693 let updated_tx = self
694 .mark_transaction_as_failed(
695 &tx,
696 reason,
697 "gas limit exceeds block gas limit",
698 )
699 .await?;
700 return Ok(updated_tx);
701 }
702 }
703 }
704 }
705
706 let price_params: PriceParams = self
708 .price_calculator
709 .get_transaction_price_params(&evm_data, relayer)
710 .await?;
711
712 debug!(
713 tx_id = %tx.id,
714 relayer_id = %tx.relayer_id,
715 gas_price = ?price_params.gas_price,
716 "gas price"
717 );
718
719 if let Err(balance_error) = self
721 .ensure_sufficient_balance(price_params.total_cost)
722 .await
723 {
724 match &balance_error {
726 TransactionError::InsufficientBalance(_) => {
727 warn!(
728 tx_id = %tx.id,
729 relayer_id = %tx.relayer_id,
730 error = %balance_error,
731 "insufficient balance for transaction"
732 );
733
734 let updated_tx = self
735 .mark_transaction_as_failed(
736 &tx,
737 balance_error.to_string(),
738 "insufficient balance",
739 )
740 .await?;
741
742 return Ok(updated_tx);
744 }
745 _ => {
748 debug!(error = %balance_error, "failed to check balance, will retry");
749 return Err(balance_error);
750 }
751 }
752 }
753
754 let tx_with_nonce = if let Some(existing_nonce) = evm_data.nonce {
756 debug!(
757 nonce = existing_nonce,
758 "transaction already has nonce assigned, reusing for retry"
759 );
760 tx
766 } else {
767 let new_nonce = self
769 .transaction_counter_service
770 .get_and_increment(&self.relayer.id, &self.relayer.address)
771 .await
772 .map_err(|e| TransactionError::UnexpectedError(e.to_string()))?;
773
774 debug!(nonce = new_nonce, "assigned new nonce to transaction");
775
776 let updated_evm_data = evm_data
777 .with_price_params(price_params.clone())
778 .with_nonce(new_nonce);
779
780 let presign_update = TransactionUpdateRequest {
783 network_data: Some(NetworkTransactionData::Evm(updated_evm_data.clone())),
784 priced_at: Some(Utc::now().to_rfc3339()),
785 ..Default::default()
786 };
787
788 self.transaction_repository
789 .partial_update(tx.id.clone(), presign_update)
790 .await?
791 };
792
793 let updated_evm_data = tx_with_nonce
795 .network_data
796 .get_evm_transaction_data()?
797 .with_price_params(price_params.clone());
798
799 let sig_result = self
801 .signer
802 .sign_transaction(NetworkTransactionData::Evm(updated_evm_data.clone()))
803 .await?;
804
805 let updated_evm_data =
806 updated_evm_data.with_signed_transaction_data(sig_result.into_evm()?);
807
808 let mut hashes = tx_with_nonce.hashes.clone();
810 if let Some(hash) = updated_evm_data.hash.clone() {
811 hashes.push(hash);
812 }
813
814 let postsign_update = TransactionUpdateRequest {
816 status: Some(TransactionStatus::Sent),
817 network_data: Some(NetworkTransactionData::Evm(updated_evm_data)),
818 hashes: Some(hashes),
819 ..Default::default()
820 };
821
822 let updated_tx = self
823 .transaction_repository
824 .partial_update(tx_with_nonce.id.clone(), postsign_update)
825 .await?;
826
827 debug!(
828 tx_id = %updated_tx.id,
829 relayer_id = %updated_tx.relayer_id,
830 status = ?updated_tx.status,
831 "transaction status updated to Sent"
832 );
833
834 self.job_producer
836 .produce_submit_transaction_job(
837 TransactionSend::submit(updated_tx.id.clone(), updated_tx.relayer_id.clone()),
838 None,
839 )
840 .await?;
841
842 if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
843 error!(
844 tx_id = %updated_tx.id,
845 relayer_id = %updated_tx.relayer_id,
846 status = ?TransactionStatus::Sent,
847 error = %e,
848 "sending transaction update notification failed after prepare"
849 );
850 }
851
852 Ok(updated_tx)
853 }
854
855 async fn submit_transaction(
865 &self,
866 tx: TransactionRepoModel,
867 ) -> Result<TransactionRepoModel, TransactionError> {
868 debug!(
869 tx_id = %tx.id,
870 relayer_id = %tx.relayer_id,
871 status = ?tx.status,
872 "submitting transaction"
873 );
874
875 if let Err(e) = ensure_status_one_of(
878 &tx,
879 &[TransactionStatus::Sent, TransactionStatus::Submitted],
880 Some("submit_transaction"),
881 ) {
882 warn!(
883 tx_id = %tx.id,
884 status = ?tx.status,
885 error = %e,
886 "transaction not in expected status for submission, skipping"
887 );
888 return Ok(tx);
889 }
890
891 let evm_tx_data = tx.network_data.get_evm_transaction_data()?;
892 let raw_tx = evm_tx_data.raw.as_ref().ok_or_else(|| {
893 TransactionError::InvalidType("Raw transaction data is missing".to_string())
894 })?;
895
896 match self.provider.send_raw_transaction(raw_tx).await {
899 Ok(_) => {
900 }
902 Err(e) => {
903 let error_kind = Self::classify_submission_error(&e);
904
905 match (&tx.status, &error_kind) {
906 (_, SubmissionErrorKind::AlreadyKnown)
910 | (_, SubmissionErrorKind::ReplacementUnderpriced) => {
911 warn!(
912 tx_id = %tx.id,
913 error = %e,
914 error_kind = ?error_kind,
915 "transaction appears to be already submitted based on RPC error - treating as success"
916 );
917 }
919 (_, SubmissionErrorKind::NonceTooLow) => {
925 warn!(
926 tx_id = %tx.id,
927 status = ?tx.status,
928 error = %e,
929 error_kind = ?error_kind,
930 "nonce error during submission - scheduling nonce recovery"
931 );
932
933 let reason = format!("Nonce error during submission: {error_kind:?}");
935 let update = TransactionUpdateRequest {
936 status_reason: Some(reason),
937 ..Default::default()
938 };
939 if let Err(update_err) = self
940 .transaction_repository
941 .partial_update(tx.id.clone(), update)
942 .await
943 {
944 warn!(
945 tx_id = %tx.id,
946 error = %update_err,
947 "failed to persist status_reason for nonce error"
948 );
949 }
950
951 if let Err(schedule_err) = self
953 .schedule_nonce_recovery_status_check(&tx, &error_kind)
954 .await
955 {
956 error!(
957 tx_id = %tx.id,
958 error = %schedule_err,
959 "failed to schedule nonce recovery status check"
960 );
961 }
962
963 return Ok(tx);
965 }
966 (_, SubmissionErrorKind::NonceTooHigh) => {
970 self.handle_nonce_too_high(&tx, "during submission").await;
971 return Ok(tx);
973 }
974 _ => {
976 return Err(e.into());
977 }
978 }
979 }
980 }
981
982 let metadata_reset = tx
986 .metadata
987 .as_ref()
988 .and_then(|m| m.with_nonce_retries_reset());
989 let update = TransactionUpdateRequest {
990 status: Some(TransactionStatus::Submitted),
991 sent_at: Some(Utc::now().to_rfc3339()),
992 metadata: metadata_reset,
993 ..Default::default()
994 };
995
996 let updated_tx = match self
997 .transaction_repository
998 .partial_update(tx.id.clone(), update)
999 .await
1000 {
1001 Ok(tx) => tx,
1002 Err(e) => {
1003 error!(
1004 tx_id = %tx.id,
1005 relayer_id = %tx.relayer_id,
1006 error = %e,
1007 "CRITICAL: transaction sent to blockchain but failed to update database - transaction may not be tracked correctly"
1008 );
1009 tx
1012 }
1013 };
1014
1015 if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
1016 error!(
1017 tx_id = %updated_tx.id,
1018 relayer_id = %updated_tx.relayer_id,
1019 status = ?TransactionStatus::Submitted,
1020 error = %e,
1021 "sending transaction update notification failed after submit",
1022 );
1023 }
1024
1025 Ok(updated_tx)
1026 }
1027
1028 async fn handle_transaction_status(
1038 &self,
1039 tx: TransactionRepoModel,
1040 context: Option<StatusCheckContext>,
1041 ) -> Result<TransactionRepoModel, TransactionError> {
1042 self.handle_status_impl(tx, context).await
1043 }
1044 async fn resubmit_transaction(
1054 &self,
1055 tx: TransactionRepoModel,
1056 ) -> Result<TransactionRepoModel, TransactionError> {
1057 debug!(
1058 tx_id = %tx.id,
1059 relayer_id = %tx.relayer_id,
1060 status = ?tx.status,
1061 "resubmitting transaction"
1062 );
1063
1064 if let Err(e) = ensure_status_one_of(
1066 &tx,
1067 &[TransactionStatus::Sent, TransactionStatus::Submitted],
1068 Some("resubmit_transaction"),
1069 ) {
1070 warn!(
1071 tx_id = %tx.id,
1072 status = ?tx.status,
1073 error = %e,
1074 "transaction not in expected status for resubmission, skipping"
1075 );
1076 return Ok(tx);
1077 }
1078
1079 let evm_data = tx.network_data.get_evm_transaction_data()?;
1080
1081 let bumped_price_params = self
1084 .price_calculator
1085 .calculate_bumped_gas_price(&evm_data, self.relayer(), is_noop(&evm_data))
1086 .await?;
1087
1088 if !bumped_price_params.is_min_bumped.is_some_and(|b| b) {
1089 warn!(
1090 tx_id = %tx.id,
1091 relayer_id = %tx.relayer_id,
1092 price_params = ?bumped_price_params,
1093 "bumped gas price does not meet minimum requirement, skipping resubmission"
1094 );
1095 return Ok(tx);
1096 }
1097
1098 self.ensure_sufficient_balance(bumped_price_params.total_cost)
1100 .await?;
1101
1102 let updated_evm_data = evm_data.with_price_params(bumped_price_params.clone());
1104
1105 let sig_result = self
1107 .signer
1108 .sign_transaction(NetworkTransactionData::Evm(updated_evm_data.clone()))
1109 .await?;
1110
1111 let final_evm_data = updated_evm_data.with_signed_transaction_data(sig_result.into_evm()?);
1112
1113 let raw_tx = final_evm_data.raw.as_ref().ok_or_else(|| {
1114 TransactionError::InvalidType("Raw transaction data is missing".to_string())
1115 })?;
1116
1117 let was_already_submitted = match self.provider.send_raw_transaction(raw_tx).await {
1119 Ok(_) => {
1120 false
1122 }
1123 Err(e) => {
1124 let error_kind = Self::classify_submission_error(&e);
1125
1126 match &error_kind {
1127 SubmissionErrorKind::AlreadyKnown
1129 | SubmissionErrorKind::ReplacementUnderpriced => {
1130 warn!(
1131 tx_id = %tx.id,
1132 error = %e,
1133 error_kind = ?error_kind,
1134 "resubmission indicates transaction already in mempool/mined - keeping original hash"
1135 );
1136 true
1137 }
1138 SubmissionErrorKind::NonceTooLow => {
1142 warn!(
1143 tx_id = %tx.id,
1144 error = %e,
1145 "resubmission got nonce too low - scheduling nonce recovery"
1146 );
1147 if let Err(schedule_err) = self
1148 .schedule_nonce_recovery_status_check(&tx, &error_kind)
1149 .await
1150 {
1151 error!(
1152 tx_id = %tx.id,
1153 error = %schedule_err,
1154 "failed to schedule nonce recovery status check during resubmission"
1155 );
1156 }
1157 true
1158 }
1159 SubmissionErrorKind::NonceTooHigh => {
1161 self.handle_nonce_too_high(&tx, "during resubmission").await;
1162 return Ok(tx);
1164 }
1165 _ => {
1167 return Err(e.into());
1168 }
1169 }
1170 }
1171 };
1172
1173 let metadata_reset = tx
1175 .metadata
1176 .as_ref()
1177 .and_then(|m| m.with_nonce_retries_reset());
1178
1179 let update = if was_already_submitted {
1181 TransactionUpdateRequest {
1183 status: Some(TransactionStatus::Submitted),
1184 metadata: metadata_reset,
1185 ..Default::default()
1186 }
1187 } else {
1188 let mut hashes = tx.hashes.clone();
1190 if let Some(hash) = final_evm_data.hash.clone() {
1191 hashes.push(hash);
1192 }
1193
1194 TransactionUpdateRequest {
1195 network_data: Some(NetworkTransactionData::Evm(final_evm_data)),
1196 hashes: Some(hashes),
1197 status: Some(TransactionStatus::Submitted),
1198 priced_at: Some(Utc::now().to_rfc3339()),
1199 sent_at: Some(Utc::now().to_rfc3339()),
1200 metadata: metadata_reset,
1201 ..Default::default()
1202 }
1203 };
1204
1205 let updated_tx = match self
1206 .transaction_repository
1207 .partial_update(tx.id.clone(), update)
1208 .await
1209 {
1210 Ok(tx) => tx,
1211 Err(e) => {
1212 error!(
1213 error = %e,
1214 tx_id = %tx.id,
1215 "CRITICAL: resubmitted transaction sent to blockchain but failed to update database"
1216 );
1217 tx
1219 }
1220 };
1221
1222 Ok(updated_tx)
1223 }
1224
1225 async fn cancel_transaction(
1235 &self,
1236 tx: TransactionRepoModel,
1237 ) -> Result<TransactionRepoModel, TransactionError> {
1238 info!(tx_id = %tx.id, status = ?tx.status, "cancelling transaction");
1239
1240 ensure_status_one_of(
1242 &tx,
1243 &[
1244 TransactionStatus::Pending,
1245 TransactionStatus::Sent,
1246 TransactionStatus::Submitted,
1247 ],
1248 Some("cancel_transaction"),
1249 )?;
1250
1251 if tx.status == TransactionStatus::Pending {
1253 debug!("transaction is in pending state, updating status to canceled");
1254 return self
1255 .update_transaction_status(
1256 tx,
1257 TransactionStatus::Canceled,
1258 Some("Transaction canceled by user".to_string()),
1259 )
1260 .await;
1261 }
1262
1263 let update = self
1269 .prepare_noop_update_request(
1270 &tx,
1271 true,
1272 Some("Transaction canceled by user, replacing with NOOP".to_string()),
1273 )
1274 .await?;
1275
1276 let updated_tx = self
1277 .transaction_repository()
1278 .partial_update(tx.id.clone(), update)
1279 .await?;
1280
1281 self.send_transaction_resubmit_job(&updated_tx).await?;
1283
1284 if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
1286 error!(
1287 tx_id = %updated_tx.id,
1288 status = ?updated_tx.status,
1289 "sending transaction update notification failed after cancel: {:?}",
1290 e
1291 );
1292 }
1293
1294 debug!("original transaction updated with cancellation data");
1295 Ok(updated_tx)
1296 }
1297
1298 async fn replace_transaction(
1309 &self,
1310 old_tx: TransactionRepoModel,
1311 new_tx_request: NetworkTransactionRequest,
1312 ) -> Result<TransactionRepoModel, TransactionError> {
1313 debug!("replacing transaction");
1314
1315 ensure_status_one_of(
1317 &old_tx,
1318 &[
1319 TransactionStatus::Pending,
1320 TransactionStatus::Sent,
1321 TransactionStatus::Submitted,
1322 ],
1323 Some("replace_transaction"),
1324 )?;
1325
1326 let old_evm_data = old_tx.network_data.get_evm_transaction_data()?;
1328 let new_evm_request = match new_tx_request {
1329 NetworkTransactionRequest::Evm(evm_req) => evm_req,
1330 _ => {
1331 return Err(TransactionError::InvalidType(
1332 "New transaction request must be EVM type".to_string(),
1333 ))
1334 }
1335 };
1336
1337 let network_repo_model = self
1338 .network_repository()
1339 .get_by_chain_id(NetworkType::Evm, old_evm_data.chain_id)
1340 .await
1341 .map_err(|e| {
1342 TransactionError::NetworkConfiguration(format!(
1343 "Failed to get network by chain_id {}: {}",
1344 old_evm_data.chain_id, e
1345 ))
1346 })?
1347 .ok_or_else(|| {
1348 TransactionError::NetworkConfiguration(format!(
1349 "Network with chain_id {} not found",
1350 old_evm_data.chain_id
1351 ))
1352 })?;
1353
1354 let network = EvmNetwork::try_from(network_repo_model).map_err(|e| {
1355 TransactionError::NetworkConfiguration(format!("Failed to convert network model: {e}"))
1356 })?;
1357
1358 let updated_evm_data = EvmTransactionData::for_replacement(&old_evm_data, &new_evm_request);
1360
1361 let price_params = super::replacement::determine_replacement_pricing(
1363 &old_evm_data,
1364 &updated_evm_data,
1365 self.relayer(),
1366 &self.price_calculator,
1367 network.lacks_mempool(),
1368 )
1369 .await?;
1370
1371 debug!(price_params = ?price_params, "replacement price params");
1372
1373 let evm_data_with_price_params = updated_evm_data.with_price_params(price_params.clone());
1375
1376 self.ensure_sufficient_balance(price_params.total_cost)
1378 .await?;
1379
1380 let sig_result = self
1381 .signer
1382 .sign_transaction(NetworkTransactionData::Evm(
1383 evm_data_with_price_params.clone(),
1384 ))
1385 .await?;
1386
1387 let final_evm_data =
1388 evm_data_with_price_params.with_signed_transaction_data(sig_result.into_evm()?);
1389
1390 let updated_tx = self
1392 .transaction_repository
1393 .update_network_data(
1394 old_tx.id.clone(),
1395 NetworkTransactionData::Evm(final_evm_data),
1396 )
1397 .await?;
1398
1399 self.send_transaction_resubmit_job(&updated_tx).await?;
1400
1401 if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
1403 error!(
1404 tx_id = %updated_tx.id,
1405 status = ?updated_tx.status,
1406 "sending transaction update notification failed after replace: {:?}",
1407 e
1408 );
1409 }
1410
1411 Ok(updated_tx)
1412 }
1413
1414 async fn sign_transaction(
1424 &self,
1425 tx: TransactionRepoModel,
1426 ) -> Result<TransactionRepoModel, TransactionError> {
1427 Ok(tx)
1428 }
1429
1430 async fn validate_transaction(
1440 &self,
1441 _tx: TransactionRepoModel,
1442 ) -> Result<bool, TransactionError> {
1443 Ok(true)
1444 }
1445}
1446pub type DefaultEvmTransaction = EvmRelayerTransaction<
1455 EvmProvider,
1456 RelayerRepositoryStorage,
1457 NetworkRepositoryStorage,
1458 TransactionRepositoryStorage,
1459 JobProducer,
1460 EvmSigner,
1461 TransactionCounterRepositoryStorage,
1462 PriceCalculator<EvmGasPriceService<EvmProvider>>,
1463>;
1464#[cfg(test)]
1465mod tests {
1466
1467 use super::*;
1468 use crate::{
1469 domain::evm::price_calculator::PriceParams,
1470 jobs::MockJobProducerTrait,
1471 models::{
1472 evm::Speed, EvmTransactionData, EvmTransactionRequest, NetworkType,
1473 RelayerNetworkPolicy, U256,
1474 },
1475 repositories::{
1476 MockNetworkRepository, MockRelayerRepository, MockTransactionCounterTrait,
1477 MockTransactionRepository,
1478 },
1479 services::{provider::MockEvmProviderTrait, signer::MockSigner},
1480 };
1481 use chrono::Utc;
1482 use futures::future::ready;
1483 use mockall::{mock, predicate::*};
1484
1485 mock! {
1487 pub PriceCalculator {}
1488 #[async_trait]
1489 impl PriceCalculatorTrait for PriceCalculator {
1490 async fn get_transaction_price_params(
1491 &self,
1492 tx_data: &EvmTransactionData,
1493 relayer: &RelayerRepoModel
1494 ) -> Result<PriceParams, TransactionError>;
1495
1496 async fn calculate_bumped_gas_price(
1497 &self,
1498 tx: &EvmTransactionData,
1499 relayer: &RelayerRepoModel,
1500 force_bump: bool,
1501 ) -> Result<PriceParams, TransactionError>;
1502 }
1503 }
1504
1505 fn create_test_relayer() -> RelayerRepoModel {
1507 create_test_relayer_with_policy(crate::models::RelayerEvmPolicy {
1508 min_balance: Some(100000000000000000u128), gas_limit_estimation: Some(true),
1510 gas_price_cap: Some(100000000000), whitelist_receivers: Some(vec!["0xRecipient".to_string()]),
1512 eip1559_pricing: Some(false),
1513 private_transactions: Some(false),
1514 })
1515 }
1516
1517 fn create_test_relayer_with_policy(evm_policy: RelayerEvmPolicy) -> RelayerRepoModel {
1518 RelayerRepoModel {
1519 id: "test-relayer-id".to_string(),
1520 name: "Test Relayer".to_string(),
1521 network: "1".to_string(), address: "0xSender".to_string(),
1523 paused: false,
1524 system_disabled: false,
1525 signer_id: "test-signer-id".to_string(),
1526 notification_id: Some("test-notification-id".to_string()),
1527 policies: RelayerNetworkPolicy::Evm(evm_policy),
1528 network_type: NetworkType::Evm,
1529 custom_rpc_urls: None,
1530 ..Default::default()
1531 }
1532 }
1533
1534 fn create_test_transaction() -> TransactionRepoModel {
1536 TransactionRepoModel {
1537 id: "test-tx-id".to_string(),
1538 relayer_id: "test-relayer-id".to_string(),
1539 status: TransactionStatus::Pending,
1540 status_reason: None,
1541 created_at: Utc::now().to_rfc3339(),
1542 sent_at: None,
1543 confirmed_at: None,
1544 valid_until: None,
1545 delete_at: None,
1546 network_type: NetworkType::Evm,
1547 network_data: NetworkTransactionData::Evm(EvmTransactionData {
1548 chain_id: 1,
1549 from: "0xSender".to_string(),
1550 to: Some("0xRecipient".to_string()),
1551 value: U256::from(1000000000000000000u64), data: Some("0xData".to_string()),
1553 gas_limit: Some(21000),
1554 gas_price: Some(20000000000), max_fee_per_gas: None,
1556 max_priority_fee_per_gas: None,
1557 nonce: None,
1558 signature: None,
1559 hash: None,
1560 speed: Some(Speed::Fast),
1561 raw: None,
1562 }),
1563 priced_at: None,
1564 hashes: Vec::new(),
1565 noop_count: None,
1566 is_canceled: Some(false),
1567 metadata: None,
1568 }
1569 }
1570
1571 #[tokio::test]
1572 async fn test_prepare_transaction_with_sufficient_balance() {
1573 let mut mock_transaction = MockTransactionRepository::new();
1574 let mock_relayer = MockRelayerRepository::new();
1575 let mut mock_provider = MockEvmProviderTrait::new();
1576 let mut mock_signer = MockSigner::new();
1577 let mut mock_job_producer = MockJobProducerTrait::new();
1578 let mut mock_price_calculator = MockPriceCalculator::new();
1579 let mut counter_service = MockTransactionCounterTrait::new();
1580
1581 let relayer = create_test_relayer();
1582 let test_tx = create_test_transaction();
1583
1584 counter_service
1585 .expect_get_and_increment()
1586 .returning(|_, _| Box::pin(ready(Ok(42))));
1587
1588 let price_params = PriceParams {
1589 gas_price: Some(30000000000),
1590 max_fee_per_gas: None,
1591 max_priority_fee_per_gas: None,
1592 is_min_bumped: None,
1593 extra_fee: None,
1594 total_cost: U256::from(630000000000000u64),
1595 };
1596 mock_price_calculator
1597 .expect_get_transaction_price_params()
1598 .returning(move |_, _| Ok(price_params.clone()));
1599
1600 mock_signer.expect_sign_transaction().returning(|_| {
1601 Box::pin(ready(Ok(
1602 crate::domain::relayer::SignTransactionResponse::Evm(
1603 crate::domain::relayer::SignTransactionResponseEvm {
1604 hash: "0xtx_hash".to_string(),
1605 signature: crate::models::EvmTransactionDataSignature {
1606 r: "r".to_string(),
1607 s: "s".to_string(),
1608 v: 1,
1609 sig: "0xsignature".to_string(),
1610 },
1611 raw: vec![1, 2, 3],
1612 },
1613 ),
1614 )))
1615 });
1616
1617 mock_provider
1618 .expect_get_balance()
1619 .with(eq("0xSender"))
1620 .returning(|_| Box::pin(ready(Ok(U256::from(1000000000000000000u64)))));
1621
1622 mock_provider
1624 .expect_get_block_by_number()
1625 .times(1)
1626 .returning(|| {
1627 Box::pin(async {
1628 use alloy::{network::AnyRpcBlock, rpc::types::Block};
1629 let mut block: Block = Block::default();
1630 block.header.gas_limit = 30_000_000u64;
1632 Ok(AnyRpcBlock::from(block))
1633 })
1634 });
1635
1636 let test_tx_clone = test_tx.clone();
1637 mock_transaction
1638 .expect_partial_update()
1639 .returning(move |_, update| {
1640 let mut updated_tx = test_tx_clone.clone();
1641 if let Some(status) = &update.status {
1642 updated_tx.status = status.clone();
1643 }
1644 if let Some(network_data) = &update.network_data {
1645 updated_tx.network_data = network_data.clone();
1646 }
1647 if let Some(hashes) = &update.hashes {
1648 updated_tx.hashes = hashes.clone();
1649 }
1650 Ok(updated_tx)
1651 });
1652
1653 mock_job_producer
1654 .expect_produce_submit_transaction_job()
1655 .returning(|_, _| Box::pin(ready(Ok(()))));
1656 mock_job_producer
1657 .expect_produce_send_notification_job()
1658 .returning(|_, _| Box::pin(ready(Ok(()))));
1659
1660 let mock_network = MockNetworkRepository::new();
1661
1662 let evm_transaction = EvmRelayerTransaction {
1663 relayer: relayer.clone(),
1664 provider: mock_provider,
1665 relayer_repository: Arc::new(mock_relayer),
1666 network_repository: Arc::new(mock_network),
1667 transaction_repository: Arc::new(mock_transaction),
1668 transaction_counter_service: Arc::new(counter_service),
1669 job_producer: Arc::new(mock_job_producer),
1670 price_calculator: mock_price_calculator,
1671 signer: mock_signer,
1672 };
1673
1674 let result = evm_transaction.prepare_transaction(test_tx.clone()).await;
1675 assert!(result.is_ok());
1676 let prepared_tx = result.unwrap();
1677 assert_eq!(prepared_tx.status, TransactionStatus::Sent);
1678 assert!(!prepared_tx.hashes.is_empty());
1679 }
1680
1681 #[tokio::test]
1682 async fn test_prepare_transaction_with_insufficient_balance() {
1683 let mut mock_transaction = MockTransactionRepository::new();
1684 let mock_relayer = MockRelayerRepository::new();
1685 let mut mock_provider = MockEvmProviderTrait::new();
1686 let mut mock_signer = MockSigner::new();
1687 let mut mock_job_producer = MockJobProducerTrait::new();
1688 let mut mock_price_calculator = MockPriceCalculator::new();
1689 let mut counter_service = MockTransactionCounterTrait::new();
1690
1691 let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
1692 gas_limit_estimation: Some(false),
1693 min_balance: Some(100000000000000000u128),
1694 ..Default::default()
1695 });
1696 let test_tx = create_test_transaction();
1697
1698 counter_service
1699 .expect_get_and_increment()
1700 .returning(|_, _| Box::pin(ready(Ok(42))));
1701
1702 let price_params = PriceParams {
1703 gas_price: Some(30000000000),
1704 max_fee_per_gas: None,
1705 max_priority_fee_per_gas: None,
1706 is_min_bumped: None,
1707 extra_fee: None,
1708 total_cost: U256::from(630000000000000u64),
1709 };
1710 mock_price_calculator
1711 .expect_get_transaction_price_params()
1712 .returning(move |_, _| Ok(price_params.clone()));
1713
1714 mock_signer.expect_sign_transaction().returning(|_| {
1715 Box::pin(ready(Ok(
1716 crate::domain::relayer::SignTransactionResponse::Evm(
1717 crate::domain::relayer::SignTransactionResponseEvm {
1718 hash: "0xtx_hash".to_string(),
1719 signature: crate::models::EvmTransactionDataSignature {
1720 r: "r".to_string(),
1721 s: "s".to_string(),
1722 v: 1,
1723 sig: "0xsignature".to_string(),
1724 },
1725 raw: vec![1, 2, 3],
1726 },
1727 ),
1728 )))
1729 });
1730
1731 mock_provider
1732 .expect_get_balance()
1733 .with(eq("0xSender"))
1734 .returning(|_| Box::pin(ready(Ok(U256::from(90000000000000000u64)))));
1735
1736 mock_provider
1738 .expect_get_block_by_number()
1739 .times(1)
1740 .returning(|| {
1741 Box::pin(async {
1742 use alloy::{network::AnyRpcBlock, rpc::types::Block};
1743 let mut block: Block = Block::default();
1744 block.header.gas_limit = 30_000_000u64;
1746 Ok(AnyRpcBlock::from(block))
1747 })
1748 });
1749
1750 let test_tx_clone = test_tx.clone();
1751 mock_transaction
1752 .expect_partial_update()
1753 .withf(move |id, update| {
1754 id == "test-tx-id" && update.status == Some(TransactionStatus::Failed)
1755 })
1756 .returning(move |_, update| {
1757 let mut updated_tx = test_tx_clone.clone();
1758 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1759 updated_tx.status_reason = update.status_reason.clone();
1760 Ok(updated_tx)
1761 });
1762
1763 mock_job_producer
1764 .expect_produce_send_notification_job()
1765 .returning(|_, _| Box::pin(ready(Ok(()))));
1766
1767 let mock_network = MockNetworkRepository::new();
1768
1769 let evm_transaction = EvmRelayerTransaction {
1770 relayer: relayer.clone(),
1771 provider: mock_provider,
1772 relayer_repository: Arc::new(mock_relayer),
1773 network_repository: Arc::new(mock_network),
1774 transaction_repository: Arc::new(mock_transaction),
1775 transaction_counter_service: Arc::new(counter_service),
1776 job_producer: Arc::new(mock_job_producer),
1777 price_calculator: mock_price_calculator,
1778 signer: mock_signer,
1779 };
1780
1781 let result = evm_transaction.prepare_transaction(test_tx.clone()).await;
1782 assert!(result.is_ok(), "Expected Ok, got: {result:?}");
1783
1784 let updated_tx = result.unwrap();
1785 assert_eq!(
1786 updated_tx.status,
1787 TransactionStatus::Failed,
1788 "Transaction should be marked as Failed"
1789 );
1790 assert!(
1791 updated_tx.status_reason.is_some(),
1792 "Status reason should be set"
1793 );
1794 assert!(
1795 updated_tx
1796 .status_reason
1797 .as_ref()
1798 .unwrap()
1799 .to_lowercase()
1800 .contains("insufficient balance"),
1801 "Status reason should contain insufficient balance error, got: {:?}",
1802 updated_tx.status_reason
1803 );
1804 }
1805
1806 #[tokio::test]
1807 async fn test_prepare_transaction_with_gas_limit_exceeding_block_limit() {
1808 let mut mock_transaction = MockTransactionRepository::new();
1809 let mock_relayer = MockRelayerRepository::new();
1810 let mut mock_provider = MockEvmProviderTrait::new();
1811 let mock_signer = MockSigner::new();
1812 let mut mock_job_producer = MockJobProducerTrait::new();
1813 let mock_price_calculator = MockPriceCalculator::new();
1814 let mut counter_service = MockTransactionCounterTrait::new();
1815
1816 let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
1817 gas_limit_estimation: Some(false), min_balance: Some(100000000000000000u128),
1819 ..Default::default()
1820 });
1821
1822 let mut test_tx = create_test_transaction();
1824 if let NetworkTransactionData::Evm(ref mut evm_data) = test_tx.network_data {
1825 evm_data.gas_limit = Some(30_000_001); }
1827
1828 counter_service
1829 .expect_get_and_increment()
1830 .returning(|_, _| Box::pin(ready(Ok(42))));
1831
1832 mock_provider
1834 .expect_get_block_by_number()
1835 .times(1)
1836 .returning(|| {
1837 Box::pin(async {
1838 use alloy::{network::AnyRpcBlock, rpc::types::Block};
1839 let mut block: Block = Block::default();
1840 block.header.gas_limit = 30_000_000u64;
1842 Ok(AnyRpcBlock::from(block))
1843 })
1844 });
1845
1846 let test_tx_clone = test_tx.clone();
1848 mock_transaction
1849 .expect_partial_update()
1850 .withf(move |id, update| {
1851 id == "test-tx-id"
1852 && update.status == Some(TransactionStatus::Failed)
1853 && update.status_reason.is_some()
1854 && update
1855 .status_reason
1856 .as_ref()
1857 .unwrap()
1858 .contains("exceeds block gas limit")
1859 })
1860 .returning(move |_, update| {
1861 let mut updated_tx = test_tx_clone.clone();
1862 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1863 updated_tx.status_reason = update.status_reason.clone();
1864 Ok(updated_tx)
1865 });
1866
1867 mock_job_producer
1868 .expect_produce_send_notification_job()
1869 .returning(|_, _| Box::pin(ready(Ok(()))));
1870
1871 let mock_network = MockNetworkRepository::new();
1872
1873 let evm_transaction = EvmRelayerTransaction {
1874 relayer: relayer.clone(),
1875 provider: mock_provider,
1876 relayer_repository: Arc::new(mock_relayer),
1877 network_repository: Arc::new(mock_network),
1878 transaction_repository: Arc::new(mock_transaction),
1879 transaction_counter_service: Arc::new(counter_service),
1880 job_producer: Arc::new(mock_job_producer),
1881 price_calculator: mock_price_calculator,
1882 signer: mock_signer,
1883 };
1884
1885 let result = evm_transaction.prepare_transaction(test_tx.clone()).await;
1886 assert!(result.is_ok(), "Expected Ok, got: {result:?}");
1887
1888 let updated_tx = result.unwrap();
1889 assert_eq!(
1890 updated_tx.status,
1891 TransactionStatus::Failed,
1892 "Transaction should be marked as Failed"
1893 );
1894 assert!(
1895 updated_tx.status_reason.is_some(),
1896 "Status reason should be set"
1897 );
1898 assert!(
1899 updated_tx
1900 .status_reason
1901 .as_ref()
1902 .unwrap()
1903 .contains("exceeds block gas limit"),
1904 "Status reason should mention gas limit exceeds block gas limit, got: {:?}",
1905 updated_tx.status_reason
1906 );
1907 assert!(
1908 updated_tx
1909 .status_reason
1910 .as_ref()
1911 .unwrap()
1912 .contains("30000001"),
1913 "Status reason should contain transaction gas limit, got: {:?}",
1914 updated_tx.status_reason
1915 );
1916 assert!(
1917 updated_tx
1918 .status_reason
1919 .as_ref()
1920 .unwrap()
1921 .contains("30000000"),
1922 "Status reason should contain block gas limit, got: {:?}",
1923 updated_tx.status_reason
1924 );
1925 }
1926
1927 #[tokio::test]
1928 async fn test_prepare_transaction_with_gas_limit_within_block_limit() {
1929 let mut mock_transaction = MockTransactionRepository::new();
1930 let mock_relayer = MockRelayerRepository::new();
1931 let mut mock_provider = MockEvmProviderTrait::new();
1932 let mut mock_signer = MockSigner::new();
1933 let mut mock_job_producer = MockJobProducerTrait::new();
1934 let mut mock_price_calculator = MockPriceCalculator::new();
1935 let mut counter_service = MockTransactionCounterTrait::new();
1936
1937 let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
1938 gas_limit_estimation: Some(false), min_balance: Some(100000000000000000u128),
1940 ..Default::default()
1941 });
1942
1943 let mut test_tx = create_test_transaction();
1945 if let NetworkTransactionData::Evm(ref mut evm_data) = test_tx.network_data {
1946 evm_data.gas_limit = Some(21_000); }
1948
1949 counter_service
1950 .expect_get_and_increment()
1951 .returning(|_, _| Box::pin(ready(Ok(42))));
1952
1953 let price_params = PriceParams {
1954 gas_price: Some(30000000000),
1955 max_fee_per_gas: None,
1956 max_priority_fee_per_gas: None,
1957 is_min_bumped: None,
1958 extra_fee: None,
1959 total_cost: U256::from(630000000000000u64),
1960 };
1961 mock_price_calculator
1962 .expect_get_transaction_price_params()
1963 .returning(move |_, _| Ok(price_params.clone()));
1964
1965 mock_signer.expect_sign_transaction().returning(|_| {
1966 Box::pin(ready(Ok(
1967 crate::domain::relayer::SignTransactionResponse::Evm(
1968 crate::domain::relayer::SignTransactionResponseEvm {
1969 hash: "0xtx_hash".to_string(),
1970 signature: crate::models::EvmTransactionDataSignature {
1971 r: "r".to_string(),
1972 s: "s".to_string(),
1973 v: 1,
1974 sig: "0xsignature".to_string(),
1975 },
1976 raw: vec![1, 2, 3],
1977 },
1978 ),
1979 )))
1980 });
1981
1982 mock_provider
1983 .expect_get_balance()
1984 .with(eq("0xSender"))
1985 .returning(|_| Box::pin(ready(Ok(U256::from(1000000000000000000u64)))));
1986
1987 mock_provider
1989 .expect_get_block_by_number()
1990 .times(1)
1991 .returning(|| {
1992 Box::pin(async {
1993 use alloy::{network::AnyRpcBlock, rpc::types::Block};
1994 let mut block: Block = Block::default();
1995 block.header.gas_limit = 30_000_000u64;
1997 Ok(AnyRpcBlock::from(block))
1998 })
1999 });
2000
2001 let test_tx_clone = test_tx.clone();
2002 mock_transaction
2003 .expect_partial_update()
2004 .returning(move |_, update| {
2005 let mut updated_tx = test_tx_clone.clone();
2006 if let Some(status) = &update.status {
2007 updated_tx.status = status.clone();
2008 }
2009 if let Some(network_data) = &update.network_data {
2010 updated_tx.network_data = network_data.clone();
2011 }
2012 if let Some(hashes) = &update.hashes {
2013 updated_tx.hashes = hashes.clone();
2014 }
2015 Ok(updated_tx)
2016 });
2017
2018 mock_job_producer
2019 .expect_produce_submit_transaction_job()
2020 .returning(|_, _| Box::pin(ready(Ok(()))));
2021 mock_job_producer
2022 .expect_produce_send_notification_job()
2023 .returning(|_, _| Box::pin(ready(Ok(()))));
2024
2025 let mock_network = MockNetworkRepository::new();
2026
2027 let evm_transaction = EvmRelayerTransaction {
2028 relayer: relayer.clone(),
2029 provider: mock_provider,
2030 relayer_repository: Arc::new(mock_relayer),
2031 network_repository: Arc::new(mock_network),
2032 transaction_repository: Arc::new(mock_transaction),
2033 transaction_counter_service: Arc::new(counter_service),
2034 job_producer: Arc::new(mock_job_producer),
2035 price_calculator: mock_price_calculator,
2036 signer: mock_signer,
2037 };
2038
2039 let result = evm_transaction.prepare_transaction(test_tx.clone()).await;
2040 assert!(result.is_ok(), "Expected Ok, got: {result:?}");
2041
2042 let prepared_tx = result.unwrap();
2043 assert_eq!(prepared_tx.status, TransactionStatus::Sent);
2045 assert!(!prepared_tx.hashes.is_empty());
2046 }
2047
2048 #[tokio::test]
2049 async fn test_cancel_transaction() {
2050 {
2052 let mut mock_transaction = MockTransactionRepository::new();
2054 let mock_relayer = MockRelayerRepository::new();
2055 let mock_provider = MockEvmProviderTrait::new();
2056 let mock_signer = MockSigner::new();
2057 let mut mock_job_producer = MockJobProducerTrait::new();
2058 let mock_price_calculator = MockPriceCalculator::new();
2059 let counter_service = MockTransactionCounterTrait::new();
2060
2061 let relayer = create_test_relayer();
2063 let mut test_tx = create_test_transaction();
2064 test_tx.status = TransactionStatus::Pending;
2065
2066 let test_tx_clone = test_tx.clone();
2068 mock_transaction
2069 .expect_partial_update()
2070 .withf(move |id, update| {
2071 id == "test-tx-id" && update.status == Some(TransactionStatus::Canceled)
2072 })
2073 .returning(move |_, update| {
2074 let mut updated_tx = test_tx_clone.clone();
2075 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2076 Ok(updated_tx)
2077 });
2078
2079 mock_job_producer
2081 .expect_produce_send_notification_job()
2082 .returning(|_, _| Box::pin(ready(Ok(()))));
2083
2084 let mock_network = MockNetworkRepository::new();
2085
2086 let evm_transaction = EvmRelayerTransaction {
2088 relayer: relayer.clone(),
2089 provider: mock_provider,
2090 relayer_repository: Arc::new(mock_relayer),
2091 network_repository: Arc::new(mock_network),
2092 transaction_repository: Arc::new(mock_transaction),
2093 transaction_counter_service: Arc::new(counter_service),
2094 job_producer: Arc::new(mock_job_producer),
2095 price_calculator: mock_price_calculator,
2096 signer: mock_signer,
2097 };
2098
2099 let result = evm_transaction.cancel_transaction(test_tx.clone()).await;
2101 assert!(result.is_ok());
2102 let cancelled_tx = result.unwrap();
2103 assert_eq!(cancelled_tx.id, "test-tx-id");
2104 assert_eq!(cancelled_tx.status, TransactionStatus::Canceled);
2105 }
2106
2107 {
2109 let mut mock_transaction = MockTransactionRepository::new();
2111 let mock_relayer = MockRelayerRepository::new();
2112 let mock_provider = MockEvmProviderTrait::new();
2113 let mut mock_signer = MockSigner::new();
2114 let mut mock_job_producer = MockJobProducerTrait::new();
2115 let mut mock_price_calculator = MockPriceCalculator::new();
2116 let counter_service = MockTransactionCounterTrait::new();
2117
2118 let relayer = create_test_relayer();
2120 let mut test_tx = create_test_transaction();
2121 test_tx.status = TransactionStatus::Submitted;
2122 test_tx.sent_at = Some(Utc::now().to_rfc3339());
2123 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
2124 nonce: Some(42),
2125 hash: Some("0xoriginal_hash".to_string()),
2126 ..test_tx.network_data.get_evm_transaction_data().unwrap()
2127 });
2128
2129 mock_price_calculator
2131 .expect_get_transaction_price_params()
2132 .return_once(move |_, _| {
2133 Ok(PriceParams {
2134 gas_price: Some(40000000000), max_fee_per_gas: None,
2136 max_priority_fee_per_gas: None,
2137 is_min_bumped: Some(true),
2138 extra_fee: Some(U256::ZERO),
2139 total_cost: U256::ZERO,
2140 })
2141 });
2142
2143 mock_signer.expect_sign_transaction().returning(|_| {
2145 Box::pin(ready(Ok(
2146 crate::domain::relayer::SignTransactionResponse::Evm(
2147 crate::domain::relayer::SignTransactionResponseEvm {
2148 hash: "0xcancellation_hash".to_string(),
2149 signature: crate::models::EvmTransactionDataSignature {
2150 r: "r".to_string(),
2151 s: "s".to_string(),
2152 v: 1,
2153 sig: "0xsignature".to_string(),
2154 },
2155 raw: vec![1, 2, 3],
2156 },
2157 ),
2158 )))
2159 });
2160
2161 let test_tx_clone = test_tx.clone();
2163 mock_transaction
2164 .expect_partial_update()
2165 .returning(move |tx_id, update| {
2166 let mut updated_tx = test_tx_clone.clone();
2167 updated_tx.id = tx_id;
2168 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2169 updated_tx.network_data =
2170 update.network_data.unwrap_or(updated_tx.network_data);
2171 updated_tx.is_canceled = update.is_canceled.or(updated_tx.is_canceled);
2172 if let Some(hashes) = update.hashes {
2173 updated_tx.hashes = hashes;
2174 }
2175 Ok(updated_tx)
2176 });
2177
2178 mock_job_producer
2180 .expect_produce_submit_transaction_job()
2181 .returning(|_, _| Box::pin(ready(Ok(()))));
2182 mock_job_producer
2183 .expect_produce_send_notification_job()
2184 .returning(|_, _| Box::pin(ready(Ok(()))));
2185
2186 let mut mock_network = MockNetworkRepository::new();
2188 mock_network
2189 .expect_get_by_chain_id()
2190 .with(eq(NetworkType::Evm), eq(1))
2191 .returning(|_, _| {
2192 use crate::config::{EvmNetworkConfig, NetworkConfigCommon};
2193 use crate::models::{NetworkConfigData, NetworkRepoModel, RpcConfig};
2194
2195 let config = EvmNetworkConfig {
2196 common: NetworkConfigCommon {
2197 network: "mainnet".to_string(),
2198 from: None,
2199 rpc_urls: Some(vec![RpcConfig::new(
2200 "https://rpc.example.com".to_string(),
2201 )]),
2202 explorer_urls: None,
2203 average_blocktime_ms: Some(12000),
2204 is_testnet: Some(false),
2205 tags: Some(vec!["mainnet".to_string()]),
2206 },
2207 chain_id: Some(1),
2208 required_confirmations: Some(12),
2209 features: Some(vec!["eip1559".to_string()]),
2210 symbol: Some("ETH".to_string()),
2211 gas_price_cache: None,
2212 };
2213 Ok(Some(NetworkRepoModel {
2214 id: "evm:mainnet".to_string(),
2215 name: "mainnet".to_string(),
2216 network_type: NetworkType::Evm,
2217 config: NetworkConfigData::Evm(config),
2218 }))
2219 });
2220
2221 let evm_transaction = EvmRelayerTransaction {
2223 relayer: relayer.clone(),
2224 provider: mock_provider,
2225 relayer_repository: Arc::new(mock_relayer),
2226 network_repository: Arc::new(mock_network),
2227 transaction_repository: Arc::new(mock_transaction),
2228 transaction_counter_service: Arc::new(counter_service),
2229 job_producer: Arc::new(mock_job_producer),
2230 price_calculator: mock_price_calculator,
2231 signer: mock_signer,
2232 };
2233
2234 let result = evm_transaction.cancel_transaction(test_tx.clone()).await;
2236 assert!(result.is_ok());
2237 let cancelled_tx = result.unwrap();
2238
2239 assert_eq!(cancelled_tx.id, "test-tx-id");
2241 assert_eq!(cancelled_tx.status, TransactionStatus::Submitted);
2244 assert_eq!(cancelled_tx.is_canceled, Some(true));
2245
2246 if let NetworkTransactionData::Evm(evm_data) = &cancelled_tx.network_data {
2248 assert_eq!(evm_data.nonce, Some(42)); } else {
2250 panic!("Expected EVM transaction data");
2251 }
2252 }
2253
2254 {
2256 let mock_transaction = MockTransactionRepository::new();
2258 let mock_relayer = MockRelayerRepository::new();
2259 let mock_provider = MockEvmProviderTrait::new();
2260 let mock_signer = MockSigner::new();
2261 let mock_job_producer = MockJobProducerTrait::new();
2262 let mock_price_calculator = MockPriceCalculator::new();
2263 let counter_service = MockTransactionCounterTrait::new();
2264
2265 let relayer = create_test_relayer();
2267 let mut test_tx = create_test_transaction();
2268 test_tx.status = TransactionStatus::Confirmed;
2269
2270 let mock_network = MockNetworkRepository::new();
2271
2272 let evm_transaction = EvmRelayerTransaction {
2274 relayer: relayer.clone(),
2275 provider: mock_provider,
2276 relayer_repository: Arc::new(mock_relayer),
2277 network_repository: Arc::new(mock_network),
2278 transaction_repository: Arc::new(mock_transaction),
2279 transaction_counter_service: Arc::new(counter_service),
2280 job_producer: Arc::new(mock_job_producer),
2281 price_calculator: mock_price_calculator,
2282 signer: mock_signer,
2283 };
2284
2285 let result = evm_transaction.cancel_transaction(test_tx.clone()).await;
2287 assert!(result.is_err());
2288 if let Err(TransactionError::ValidationError(msg)) = result {
2289 assert!(msg.contains("Invalid transaction state for cancel_transaction"));
2290 } else {
2291 panic!("Expected ValidationError");
2292 }
2293 }
2294 }
2295
2296 #[tokio::test]
2297 async fn test_replace_transaction() {
2298 {
2300 let mut mock_transaction = MockTransactionRepository::new();
2302 let mock_relayer = MockRelayerRepository::new();
2303 let mut mock_provider = MockEvmProviderTrait::new();
2304 let mut mock_signer = MockSigner::new();
2305 let mut mock_job_producer = MockJobProducerTrait::new();
2306 let mut mock_price_calculator = MockPriceCalculator::new();
2307 let counter_service = MockTransactionCounterTrait::new();
2308
2309 let relayer = create_test_relayer();
2311 let mut test_tx = create_test_transaction();
2312 test_tx.status = TransactionStatus::Submitted;
2313 test_tx.sent_at = Some(Utc::now().to_rfc3339());
2314
2315 mock_price_calculator
2317 .expect_get_transaction_price_params()
2318 .return_once(move |_, _| {
2319 Ok(PriceParams {
2320 gas_price: Some(40000000000), max_fee_per_gas: None,
2322 max_priority_fee_per_gas: None,
2323 is_min_bumped: Some(true),
2324 extra_fee: Some(U256::ZERO),
2325 total_cost: U256::from(2001000000000000000u64), })
2327 });
2328
2329 mock_signer.expect_sign_transaction().returning(|_| {
2331 Box::pin(ready(Ok(
2332 crate::domain::relayer::SignTransactionResponse::Evm(
2333 crate::domain::relayer::SignTransactionResponseEvm {
2334 hash: "0xreplacement_hash".to_string(),
2335 signature: crate::models::EvmTransactionDataSignature {
2336 r: "r".to_string(),
2337 s: "s".to_string(),
2338 v: 1,
2339 sig: "0xsignature".to_string(),
2340 },
2341 raw: vec![1, 2, 3],
2342 },
2343 ),
2344 )))
2345 });
2346
2347 mock_provider
2349 .expect_get_balance()
2350 .with(eq("0xSender"))
2351 .returning(|_| Box::pin(ready(Ok(U256::from(3000000000000000000u64)))));
2352
2353 let test_tx_clone = test_tx.clone();
2355 mock_transaction
2356 .expect_update_network_data()
2357 .returning(move |tx_id, network_data| {
2358 let mut updated_tx = test_tx_clone.clone();
2359 updated_tx.id = tx_id;
2360 updated_tx.network_data = network_data;
2361 Ok(updated_tx)
2362 });
2363
2364 mock_job_producer
2366 .expect_produce_submit_transaction_job()
2367 .returning(|_, _| Box::pin(ready(Ok(()))));
2368 mock_job_producer
2369 .expect_produce_send_notification_job()
2370 .returning(|_, _| Box::pin(ready(Ok(()))));
2371
2372 let mut mock_network = MockNetworkRepository::new();
2374 mock_network
2375 .expect_get_by_chain_id()
2376 .with(eq(NetworkType::Evm), eq(1))
2377 .returning(|_, _| {
2378 use crate::config::{EvmNetworkConfig, NetworkConfigCommon};
2379 use crate::models::{NetworkConfigData, NetworkRepoModel};
2380
2381 let config = EvmNetworkConfig {
2382 common: NetworkConfigCommon {
2383 network: "mainnet".to_string(),
2384 from: None,
2385 rpc_urls: Some(vec![crate::models::RpcConfig::new(
2386 "https://rpc.example.com".to_string(),
2387 )]),
2388 explorer_urls: None,
2389 average_blocktime_ms: Some(12000),
2390 is_testnet: Some(false),
2391 tags: Some(vec!["mainnet".to_string()]), },
2393 chain_id: Some(1),
2394 required_confirmations: Some(12),
2395 features: Some(vec!["eip1559".to_string()]),
2396 symbol: Some("ETH".to_string()),
2397 gas_price_cache: None,
2398 };
2399 Ok(Some(NetworkRepoModel {
2400 id: "evm:mainnet".to_string(),
2401 name: "mainnet".to_string(),
2402 network_type: NetworkType::Evm,
2403 config: NetworkConfigData::Evm(config),
2404 }))
2405 });
2406
2407 let evm_transaction = EvmRelayerTransaction {
2409 relayer: relayer.clone(),
2410 provider: mock_provider,
2411 relayer_repository: Arc::new(mock_relayer),
2412 network_repository: Arc::new(mock_network),
2413 transaction_repository: Arc::new(mock_transaction),
2414 transaction_counter_service: Arc::new(counter_service),
2415 job_producer: Arc::new(mock_job_producer),
2416 price_calculator: mock_price_calculator,
2417 signer: mock_signer,
2418 };
2419
2420 let replacement_request = NetworkTransactionRequest::Evm(EvmTransactionRequest {
2422 to: Some("0xNewRecipient".to_string()),
2423 value: U256::from(2000000000000000000u64), data: Some("0xNewData".to_string()),
2425 gas_limit: Some(25000),
2426 gas_price: None, max_fee_per_gas: None,
2428 max_priority_fee_per_gas: None,
2429 speed: Some(Speed::Fast),
2430 valid_until: None,
2431 });
2432
2433 let result = evm_transaction
2435 .replace_transaction(test_tx.clone(), replacement_request)
2436 .await;
2437 if let Err(ref e) = result {
2438 eprintln!("Replace transaction failed with error: {e:?}");
2439 }
2440 assert!(result.is_ok());
2441 let replaced_tx = result.unwrap();
2442
2443 assert_eq!(replaced_tx.id, "test-tx-id");
2445
2446 if let NetworkTransactionData::Evm(evm_data) = &replaced_tx.network_data {
2448 assert_eq!(evm_data.to, Some("0xNewRecipient".to_string()));
2449 assert_eq!(evm_data.value, U256::from(2000000000000000000u64));
2450 assert_eq!(evm_data.gas_price, Some(40000000000));
2451 assert_eq!(evm_data.gas_limit, Some(25000));
2452 assert!(evm_data.hash.is_some());
2453 assert!(evm_data.raw.is_some());
2454 } else {
2455 panic!("Expected EVM transaction data");
2456 }
2457 }
2458
2459 {
2461 let mock_transaction = MockTransactionRepository::new();
2463 let mock_relayer = MockRelayerRepository::new();
2464 let mock_provider = MockEvmProviderTrait::new();
2465 let mock_signer = MockSigner::new();
2466 let mock_job_producer = MockJobProducerTrait::new();
2467 let mock_price_calculator = MockPriceCalculator::new();
2468 let counter_service = MockTransactionCounterTrait::new();
2469
2470 let relayer = create_test_relayer();
2472 let mut test_tx = create_test_transaction();
2473 test_tx.status = TransactionStatus::Confirmed;
2474
2475 let mock_network = MockNetworkRepository::new();
2476
2477 let evm_transaction = EvmRelayerTransaction {
2479 relayer: relayer.clone(),
2480 provider: mock_provider,
2481 relayer_repository: Arc::new(mock_relayer),
2482 network_repository: Arc::new(mock_network),
2483 transaction_repository: Arc::new(mock_transaction),
2484 transaction_counter_service: Arc::new(counter_service),
2485 job_producer: Arc::new(mock_job_producer),
2486 price_calculator: mock_price_calculator,
2487 signer: mock_signer,
2488 };
2489
2490 let replacement_request = NetworkTransactionRequest::Evm(EvmTransactionRequest {
2492 to: Some("0xNewRecipient".to_string()),
2493 value: U256::from(1000000000000000000u64),
2494 data: Some("0xData".to_string()),
2495 gas_limit: Some(21000),
2496 gas_price: Some(30000000000),
2497 max_fee_per_gas: None,
2498 max_priority_fee_per_gas: None,
2499 speed: Some(Speed::Fast),
2500 valid_until: None,
2501 });
2502
2503 let result = evm_transaction
2505 .replace_transaction(test_tx.clone(), replacement_request)
2506 .await;
2507 assert!(result.is_err());
2508 if let Err(TransactionError::ValidationError(msg)) = result {
2509 assert!(msg.contains("Invalid transaction state for replace_transaction"));
2510 } else {
2511 panic!("Expected ValidationError");
2512 }
2513 }
2514 }
2515
2516 #[tokio::test]
2517 async fn test_estimate_tx_gas_limit_success() {
2518 let mock_transaction = MockTransactionRepository::new();
2519 let mock_relayer = MockRelayerRepository::new();
2520 let mut mock_provider = MockEvmProviderTrait::new();
2521 let mock_signer = MockSigner::new();
2522 let mock_job_producer = MockJobProducerTrait::new();
2523 let mock_price_calculator = MockPriceCalculator::new();
2524 let counter_service = MockTransactionCounterTrait::new();
2525 let mock_network = MockNetworkRepository::new();
2526
2527 let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2529 gas_limit_estimation: Some(true),
2530 ..Default::default()
2531 });
2532 let evm_data = EvmTransactionData {
2533 from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
2534 to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
2535 value: U256::from(1000000000000000000u128),
2536 data: Some("0x".to_string()),
2537 gas_limit: None,
2538 gas_price: Some(20_000_000_000),
2539 nonce: Some(1),
2540 chain_id: 1,
2541 hash: None,
2542 signature: None,
2543 speed: Some(Speed::Average),
2544 max_fee_per_gas: None,
2545 max_priority_fee_per_gas: None,
2546 raw: None,
2547 };
2548
2549 mock_provider
2551 .expect_estimate_gas()
2552 .times(1)
2553 .returning(|_| Box::pin(async { Ok(21000) }));
2554
2555 let transaction = EvmRelayerTransaction::new(
2556 relayer.clone(),
2557 mock_provider,
2558 Arc::new(mock_relayer),
2559 Arc::new(mock_network),
2560 Arc::new(mock_transaction),
2561 Arc::new(counter_service),
2562 Arc::new(mock_job_producer),
2563 mock_price_calculator,
2564 mock_signer,
2565 )
2566 .unwrap();
2567
2568 let result = transaction
2569 .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
2570 .await;
2571
2572 assert!(result.is_ok());
2573 assert_eq!(result.unwrap(), 23100);
2575 }
2576
2577 #[tokio::test]
2578 async fn test_estimate_tx_gas_limit_disabled() {
2579 let mock_transaction = MockTransactionRepository::new();
2580 let mock_relayer = MockRelayerRepository::new();
2581 let mut mock_provider = MockEvmProviderTrait::new();
2582 let mock_signer = MockSigner::new();
2583 let mock_job_producer = MockJobProducerTrait::new();
2584 let mock_price_calculator = MockPriceCalculator::new();
2585 let counter_service = MockTransactionCounterTrait::new();
2586 let mock_network = MockNetworkRepository::new();
2587
2588 let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2590 gas_limit_estimation: Some(false),
2591 ..Default::default()
2592 });
2593
2594 let evm_data = EvmTransactionData {
2595 from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
2596 to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
2597 value: U256::from(1000000000000000000u128),
2598 data: Some("0x".to_string()),
2599 gas_limit: None,
2600 gas_price: Some(20_000_000_000),
2601 nonce: Some(1),
2602 chain_id: 1,
2603 hash: None,
2604 signature: None,
2605 speed: Some(Speed::Average),
2606 max_fee_per_gas: None,
2607 max_priority_fee_per_gas: None,
2608 raw: None,
2609 };
2610
2611 mock_provider.expect_estimate_gas().times(0);
2613
2614 let transaction = EvmRelayerTransaction::new(
2615 relayer.clone(),
2616 mock_provider,
2617 Arc::new(mock_relayer),
2618 Arc::new(mock_network),
2619 Arc::new(mock_transaction),
2620 Arc::new(counter_service),
2621 Arc::new(mock_job_producer),
2622 mock_price_calculator,
2623 mock_signer,
2624 )
2625 .unwrap();
2626
2627 let result = transaction
2628 .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
2629 .await;
2630
2631 assert!(result.is_err());
2632 assert!(matches!(
2633 result.unwrap_err(),
2634 TransactionError::UnexpectedError(_)
2635 ));
2636 }
2637
2638 #[tokio::test]
2639 async fn test_estimate_tx_gas_limit_default_enabled() {
2640 let mock_transaction = MockTransactionRepository::new();
2641 let mock_relayer = MockRelayerRepository::new();
2642 let mut mock_provider = MockEvmProviderTrait::new();
2643 let mock_signer = MockSigner::new();
2644 let mock_job_producer = MockJobProducerTrait::new();
2645 let mock_price_calculator = MockPriceCalculator::new();
2646 let counter_service = MockTransactionCounterTrait::new();
2647 let mock_network = MockNetworkRepository::new();
2648
2649 let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2650 gas_limit_estimation: None, ..Default::default()
2652 });
2653
2654 let evm_data = EvmTransactionData {
2655 from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
2656 to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
2657 value: U256::from(1000000000000000000u128),
2658 data: Some("0x".to_string()),
2659 gas_limit: None,
2660 gas_price: Some(20_000_000_000),
2661 nonce: Some(1),
2662 chain_id: 1,
2663 hash: None,
2664 signature: None,
2665 speed: Some(Speed::Average),
2666 max_fee_per_gas: None,
2667 max_priority_fee_per_gas: None,
2668 raw: None,
2669 };
2670
2671 mock_provider
2673 .expect_estimate_gas()
2674 .times(1)
2675 .returning(|_| Box::pin(async { Ok(50000) }));
2676
2677 let transaction = EvmRelayerTransaction::new(
2678 relayer.clone(),
2679 mock_provider,
2680 Arc::new(mock_relayer),
2681 Arc::new(mock_network),
2682 Arc::new(mock_transaction),
2683 Arc::new(counter_service),
2684 Arc::new(mock_job_producer),
2685 mock_price_calculator,
2686 mock_signer,
2687 )
2688 .unwrap();
2689
2690 let result = transaction
2691 .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
2692 .await;
2693
2694 assert!(result.is_ok());
2695 assert_eq!(result.unwrap(), 55000);
2697 }
2698
2699 #[tokio::test]
2700 async fn test_estimate_tx_gas_limit_provider_error() {
2701 let mock_transaction = MockTransactionRepository::new();
2702 let mock_relayer = MockRelayerRepository::new();
2703 let mut mock_provider = MockEvmProviderTrait::new();
2704 let mock_signer = MockSigner::new();
2705 let mock_job_producer = MockJobProducerTrait::new();
2706 let mock_price_calculator = MockPriceCalculator::new();
2707 let counter_service = MockTransactionCounterTrait::new();
2708 let mock_network = MockNetworkRepository::new();
2709
2710 let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2711 gas_limit_estimation: Some(true),
2712 ..Default::default()
2713 });
2714
2715 let evm_data = EvmTransactionData {
2716 from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
2717 to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
2718 value: U256::from(1000000000000000000u128),
2719 data: Some("0x".to_string()),
2720 gas_limit: None,
2721 gas_price: Some(20_000_000_000),
2722 nonce: Some(1),
2723 chain_id: 1,
2724 hash: None,
2725 signature: None,
2726 speed: Some(Speed::Average),
2727 max_fee_per_gas: None,
2728 max_priority_fee_per_gas: None,
2729 raw: None,
2730 };
2731
2732 mock_provider.expect_estimate_gas().times(1).returning(|_| {
2734 Box::pin(async {
2735 Err(crate::services::provider::ProviderError::Other(
2736 "RPC error".to_string(),
2737 ))
2738 })
2739 });
2740
2741 let transaction = EvmRelayerTransaction::new(
2742 relayer.clone(),
2743 mock_provider,
2744 Arc::new(mock_relayer),
2745 Arc::new(mock_network),
2746 Arc::new(mock_transaction),
2747 Arc::new(counter_service),
2748 Arc::new(mock_job_producer),
2749 mock_price_calculator,
2750 mock_signer,
2751 )
2752 .unwrap();
2753
2754 let result = transaction
2755 .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
2756 .await;
2757
2758 assert!(result.is_err());
2759 assert!(matches!(
2760 result.unwrap_err(),
2761 TransactionError::UnexpectedError(_)
2762 ));
2763 }
2764
2765 #[tokio::test]
2766 async fn test_prepare_transaction_uses_gas_estimation_and_stores_result() {
2767 let mut mock_transaction = MockTransactionRepository::new();
2768 let mock_relayer = MockRelayerRepository::new();
2769 let mut mock_provider = MockEvmProviderTrait::new();
2770 let mut mock_signer = MockSigner::new();
2771 let mut mock_job_producer = MockJobProducerTrait::new();
2772 let mut mock_price_calculator = MockPriceCalculator::new();
2773 let mut counter_service = MockTransactionCounterTrait::new();
2774 let mock_network = MockNetworkRepository::new();
2775
2776 let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2778 gas_limit_estimation: Some(true),
2779 min_balance: Some(100000000000000000u128),
2780 ..Default::default()
2781 });
2782
2783 let mut test_tx = create_test_transaction();
2785 if let NetworkTransactionData::Evm(ref mut evm_data) = test_tx.network_data {
2786 evm_data.gas_limit = None; evm_data.nonce = None; }
2789
2790 const PROVIDER_GAS_ESTIMATE: u64 = 45000;
2792 const EXPECTED_GAS_WITH_BUFFER: u64 = 49500; mock_provider
2796 .expect_estimate_gas()
2797 .times(1)
2798 .returning(move |_| Box::pin(async move { Ok(PROVIDER_GAS_ESTIMATE) }));
2799
2800 mock_provider
2802 .expect_get_balance()
2803 .times(1)
2804 .returning(|_| Box::pin(async { Ok(U256::from(2000000000000000000u128)) })); let price_params = PriceParams {
2807 gas_price: Some(20_000_000_000), max_fee_per_gas: None,
2809 max_priority_fee_per_gas: None,
2810 is_min_bumped: None,
2811 extra_fee: None,
2812 total_cost: U256::from(1900000000000000000u128), };
2814
2815 mock_price_calculator
2817 .expect_get_transaction_price_params()
2818 .returning(move |_, _| Ok(price_params.clone()));
2819
2820 counter_service
2822 .expect_get_and_increment()
2823 .times(1)
2824 .returning(|_, _| Box::pin(async { Ok(42) }));
2825
2826 mock_signer.expect_sign_transaction().returning(|_| {
2828 Box::pin(ready(Ok(
2829 crate::domain::relayer::SignTransactionResponse::Evm(
2830 crate::domain::relayer::SignTransactionResponseEvm {
2831 hash: "0xhash".to_string(),
2832 signature: crate::models::EvmTransactionDataSignature {
2833 r: "r".to_string(),
2834 s: "s".to_string(),
2835 v: 1,
2836 sig: "0xsignature".to_string(),
2837 },
2838 raw: vec![1, 2, 3],
2839 },
2840 ),
2841 )))
2842 });
2843
2844 mock_job_producer
2846 .expect_produce_submit_transaction_job()
2847 .returning(|_, _| Box::pin(async { Ok(()) }));
2848
2849 mock_job_producer
2850 .expect_produce_send_notification_job()
2851 .returning(|_, _| Box::pin(ready(Ok(()))));
2852
2853 let expected_gas_limit = EXPECTED_GAS_WITH_BUFFER;
2858
2859 let test_tx_clone = test_tx.clone();
2860 mock_transaction
2861 .expect_partial_update()
2862 .times(2)
2863 .returning(move |_, update| {
2864 let mut updated_tx = test_tx_clone.clone();
2865
2866 if let Some(status) = &update.status {
2868 updated_tx.status = status.clone();
2869 }
2870 if let Some(network_data) = &update.network_data {
2871 updated_tx.network_data = network_data.clone();
2872 } else {
2873 if let NetworkTransactionData::Evm(ref mut evm_data) = updated_tx.network_data {
2875 if evm_data.gas_limit.is_none() {
2876 evm_data.gas_limit = Some(expected_gas_limit);
2877 }
2878 }
2879 }
2880 if let Some(hashes) = &update.hashes {
2881 updated_tx.hashes = hashes.clone();
2882 }
2883
2884 Ok(updated_tx)
2885 });
2886
2887 let transaction = EvmRelayerTransaction::new(
2888 relayer.clone(),
2889 mock_provider,
2890 Arc::new(mock_relayer),
2891 Arc::new(mock_network),
2892 Arc::new(mock_transaction),
2893 Arc::new(counter_service),
2894 Arc::new(mock_job_producer),
2895 mock_price_calculator,
2896 mock_signer,
2897 )
2898 .unwrap();
2899
2900 let result = transaction.prepare_transaction(test_tx).await;
2902
2903 assert!(result.is_ok(), "prepare_transaction should succeed");
2905 let prepared_tx = result.unwrap();
2906
2907 if let NetworkTransactionData::Evm(evm_data) = prepared_tx.network_data {
2909 assert_eq!(evm_data.gas_limit, Some(EXPECTED_GAS_WITH_BUFFER));
2910 } else {
2911 panic!("Expected EVM network data");
2912 }
2913 }
2914
2915 #[tokio::test]
2916 async fn test_prepare_transaction_estimates_gas_for_contract_creation() {
2917 let mut mock_transaction = MockTransactionRepository::new();
2918 let mock_relayer = MockRelayerRepository::new();
2919 let mut mock_provider = MockEvmProviderTrait::new();
2920 let mut mock_signer = MockSigner::new();
2921 let mut mock_job_producer = MockJobProducerTrait::new();
2922 let mut mock_price_calculator = MockPriceCalculator::new();
2923 let mut counter_service = MockTransactionCounterTrait::new();
2924 let mock_network = MockNetworkRepository::new();
2925
2926 let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2927 gas_limit_estimation: Some(true),
2928 min_balance: Some(100000000000000000u128),
2929 ..Default::default()
2930 });
2931
2932 let mut test_tx = create_test_transaction();
2933 if let NetworkTransactionData::Evm(ref mut evm_data) = test_tx.network_data {
2934 evm_data.to = None;
2935 evm_data.data = Some("0x6080604052348015600f57600080fd5b".to_string());
2936 evm_data.gas_limit = None;
2937 evm_data.nonce = None;
2938 }
2939
2940 const PROVIDER_GAS_ESTIMATE: u64 = 1500000;
2941 const EXPECTED_GAS_WITH_BUFFER: u64 = 1650000;
2942
2943 mock_provider
2944 .expect_estimate_gas()
2945 .withf(|tx| tx.to.is_none())
2946 .times(1)
2947 .returning(move |_| Box::pin(async move { Ok(PROVIDER_GAS_ESTIMATE) }));
2948
2949 mock_provider
2950 .expect_get_balance()
2951 .times(1)
2952 .returning(|_| Box::pin(async { Ok(U256::from(2000000000000000000u128)) }));
2953
2954 let price_params = PriceParams {
2955 gas_price: Some(20_000_000_000),
2956 max_fee_per_gas: None,
2957 max_priority_fee_per_gas: None,
2958 is_min_bumped: None,
2959 extra_fee: None,
2960 total_cost: U256::from(1900000000000000000u128),
2961 };
2962
2963 mock_price_calculator
2964 .expect_get_transaction_price_params()
2965 .returning(move |_, _| Ok(price_params.clone()));
2966
2967 counter_service
2968 .expect_get_and_increment()
2969 .times(1)
2970 .returning(|_, _| Box::pin(async { Ok(42) }));
2971
2972 mock_signer.expect_sign_transaction().returning(|_| {
2973 Box::pin(ready(Ok(
2974 crate::domain::relayer::SignTransactionResponse::Evm(
2975 crate::domain::relayer::SignTransactionResponseEvm {
2976 hash: "0xhash".to_string(),
2977 signature: crate::models::EvmTransactionDataSignature {
2978 r: "r".to_string(),
2979 s: "s".to_string(),
2980 v: 1,
2981 sig: "0xsignature".to_string(),
2982 },
2983 raw: vec![1, 2, 3],
2984 },
2985 ),
2986 )))
2987 });
2988
2989 mock_job_producer
2990 .expect_produce_submit_transaction_job()
2991 .returning(|_, _| Box::pin(async { Ok(()) }));
2992
2993 mock_job_producer
2994 .expect_produce_send_notification_job()
2995 .returning(|_, _| Box::pin(ready(Ok(()))));
2996
2997 let expected_gas_limit = EXPECTED_GAS_WITH_BUFFER;
2998 let test_tx_clone = test_tx.clone();
2999 mock_transaction
3000 .expect_partial_update()
3001 .times(2)
3002 .returning(move |_, update| {
3003 let mut updated_tx = test_tx_clone.clone();
3004
3005 if let Some(status) = &update.status {
3006 updated_tx.status = status.clone();
3007 }
3008 if let Some(network_data) = &update.network_data {
3009 updated_tx.network_data = network_data.clone();
3010 } else if let NetworkTransactionData::Evm(ref mut evm_data) =
3011 updated_tx.network_data
3012 {
3013 if evm_data.gas_limit.is_none() {
3014 evm_data.gas_limit = Some(expected_gas_limit);
3015 }
3016 }
3017 if let Some(hashes) = &update.hashes {
3018 updated_tx.hashes = hashes.clone();
3019 }
3020
3021 Ok(updated_tx)
3022 });
3023
3024 let transaction = EvmRelayerTransaction::new(
3025 relayer,
3026 mock_provider,
3027 Arc::new(mock_relayer),
3028 Arc::new(mock_network),
3029 Arc::new(mock_transaction),
3030 Arc::new(counter_service),
3031 Arc::new(mock_job_producer),
3032 mock_price_calculator,
3033 mock_signer,
3034 )
3035 .unwrap();
3036
3037 let result = transaction.prepare_transaction(test_tx).await;
3038
3039 assert!(result.is_ok(), "prepare_transaction should succeed");
3040 let prepared_tx = result.unwrap();
3041
3042 if let NetworkTransactionData::Evm(evm_data) = prepared_tx.network_data {
3043 assert_eq!(evm_data.to, None);
3044 assert_eq!(evm_data.gas_limit, Some(EXPECTED_GAS_WITH_BUFFER));
3045 } else {
3046 panic!("Expected EVM network data");
3047 }
3048 }
3049
3050 #[test]
3051 fn test_is_already_submitted_error_detection() {
3052 assert!(DefaultEvmTransaction::is_already_submitted_error(
3054 &"already known"
3055 ));
3056 assert!(DefaultEvmTransaction::is_already_submitted_error(
3057 &"Transaction already known"
3058 ));
3059 assert!(DefaultEvmTransaction::is_already_submitted_error(
3060 &"Error: already known"
3061 ));
3062
3063 assert!(DefaultEvmTransaction::is_already_submitted_error(
3065 &"nonce too low"
3066 ));
3067 assert!(DefaultEvmTransaction::is_already_submitted_error(
3068 &"Nonce Too Low"
3069 ));
3070 assert!(DefaultEvmTransaction::is_already_submitted_error(
3071 &"Error: nonce too low"
3072 ));
3073
3074 assert!(DefaultEvmTransaction::is_already_submitted_error(
3076 &"nonce is too low"
3077 ));
3078 assert!(DefaultEvmTransaction::is_already_submitted_error(
3079 &"Error: nonce is too low"
3080 ));
3081
3082 assert!(DefaultEvmTransaction::is_already_submitted_error(
3084 &"known transaction"
3085 ));
3086 assert!(DefaultEvmTransaction::is_already_submitted_error(
3087 &"Known Transaction"
3088 ));
3089
3090 assert!(DefaultEvmTransaction::is_already_submitted_error(
3092 &"replacement transaction underpriced"
3093 ));
3094 assert!(DefaultEvmTransaction::is_already_submitted_error(
3095 &"Replacement Transaction Underpriced"
3096 ));
3097
3098 assert!(DefaultEvmTransaction::is_already_submitted_error(
3100 &"same hash was already imported"
3101 ));
3102
3103 assert!(!DefaultEvmTransaction::is_already_submitted_error(
3105 &"insufficient funds"
3106 ));
3107 assert!(!DefaultEvmTransaction::is_already_submitted_error(
3108 &"execution reverted"
3109 ));
3110 assert!(!DefaultEvmTransaction::is_already_submitted_error(
3111 &"gas too low"
3112 ));
3113 assert!(!DefaultEvmTransaction::is_already_submitted_error(
3114 &"timeout"
3115 ));
3116 assert!(!DefaultEvmTransaction::is_already_submitted_error(
3118 &"Unknown transaction status"
3119 ));
3120 }
3121
3122 #[tokio::test]
3125 async fn test_submit_transaction_already_known_error_from_sent() {
3126 let mut mock_transaction = MockTransactionRepository::new();
3127 let mock_relayer = MockRelayerRepository::new();
3128 let mut mock_provider = MockEvmProviderTrait::new();
3129 let mock_signer = MockSigner::new();
3130 let mut mock_job_producer = MockJobProducerTrait::new();
3131 let mock_price_calculator = MockPriceCalculator::new();
3132 let counter_service = MockTransactionCounterTrait::new();
3133 let mock_network = MockNetworkRepository::new();
3134
3135 let relayer = create_test_relayer();
3136 let mut test_tx = create_test_transaction();
3137 test_tx.status = TransactionStatus::Sent;
3138 test_tx.sent_at = Some(Utc::now().to_rfc3339());
3139 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3140 nonce: Some(42),
3141 hash: Some("0xhash".to_string()),
3142 raw: Some(vec![1, 2, 3]),
3143 ..test_tx.network_data.get_evm_transaction_data().unwrap()
3144 });
3145
3146 mock_provider
3148 .expect_send_raw_transaction()
3149 .times(1)
3150 .returning(|_| {
3151 Box::pin(async {
3152 Err(crate::services::provider::ProviderError::Other(
3153 "already known: transaction already in mempool".to_string(),
3154 ))
3155 })
3156 });
3157
3158 let test_tx_clone = test_tx.clone();
3160 mock_transaction
3161 .expect_partial_update()
3162 .times(1)
3163 .withf(|_, update| update.status == Some(TransactionStatus::Submitted))
3164 .returning(move |_, update| {
3165 let mut updated_tx = test_tx_clone.clone();
3166 updated_tx.status = update.status.unwrap();
3167 updated_tx.sent_at = update.sent_at.clone();
3168 Ok(updated_tx)
3169 });
3170
3171 mock_job_producer
3172 .expect_produce_send_notification_job()
3173 .times(1)
3174 .returning(|_, _| Box::pin(ready(Ok(()))));
3175
3176 let evm_transaction = EvmRelayerTransaction {
3177 relayer: relayer.clone(),
3178 provider: mock_provider,
3179 relayer_repository: Arc::new(mock_relayer),
3180 network_repository: Arc::new(mock_network),
3181 transaction_repository: Arc::new(mock_transaction),
3182 transaction_counter_service: Arc::new(counter_service),
3183 job_producer: Arc::new(mock_job_producer),
3184 price_calculator: mock_price_calculator,
3185 signer: mock_signer,
3186 };
3187
3188 let result = evm_transaction.submit_transaction(test_tx).await;
3189 assert!(result.is_ok());
3190 let updated_tx = result.unwrap();
3191 assert_eq!(updated_tx.status, TransactionStatus::Submitted);
3192 }
3193
3194 #[tokio::test]
3196 async fn test_submit_transaction_real_error_fails() {
3197 let mock_transaction = MockTransactionRepository::new();
3198 let mock_relayer = MockRelayerRepository::new();
3199 let mut mock_provider = MockEvmProviderTrait::new();
3200 let mock_signer = MockSigner::new();
3201 let mock_job_producer = MockJobProducerTrait::new();
3202 let mock_price_calculator = MockPriceCalculator::new();
3203 let counter_service = MockTransactionCounterTrait::new();
3204 let mock_network = MockNetworkRepository::new();
3205
3206 let relayer = create_test_relayer();
3207 let mut test_tx = create_test_transaction();
3208 test_tx.status = TransactionStatus::Sent;
3209 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3210 raw: Some(vec![1, 2, 3]),
3211 ..test_tx.network_data.get_evm_transaction_data().unwrap()
3212 });
3213
3214 mock_provider
3216 .expect_send_raw_transaction()
3217 .times(1)
3218 .returning(|_| {
3219 Box::pin(async {
3220 Err(crate::services::provider::ProviderError::Other(
3221 "insufficient funds for gas * price + value".to_string(),
3222 ))
3223 })
3224 });
3225
3226 let evm_transaction = EvmRelayerTransaction {
3227 relayer: relayer.clone(),
3228 provider: mock_provider,
3229 relayer_repository: Arc::new(mock_relayer),
3230 network_repository: Arc::new(mock_network),
3231 transaction_repository: Arc::new(mock_transaction),
3232 transaction_counter_service: Arc::new(counter_service),
3233 job_producer: Arc::new(mock_job_producer),
3234 price_calculator: mock_price_calculator,
3235 signer: mock_signer,
3236 };
3237
3238 let result = evm_transaction.submit_transaction(test_tx).await;
3239 assert!(result.is_err());
3240 }
3241
3242 #[tokio::test]
3245 async fn test_resubmit_transaction_already_submitted_preserves_hash() {
3246 let mut mock_transaction = MockTransactionRepository::new();
3247 let mock_relayer = MockRelayerRepository::new();
3248 let mut mock_provider = MockEvmProviderTrait::new();
3249 let mut mock_signer = MockSigner::new();
3250 let mock_job_producer = MockJobProducerTrait::new();
3251 let mut mock_price_calculator = MockPriceCalculator::new();
3252 let counter_service = MockTransactionCounterTrait::new();
3253 let mock_network = MockNetworkRepository::new();
3254
3255 let relayer = create_test_relayer();
3256 let mut test_tx = create_test_transaction();
3257 test_tx.status = TransactionStatus::Submitted;
3258 test_tx.sent_at = Some(Utc::now().to_rfc3339());
3259 let original_hash = "0xoriginal_hash".to_string();
3260 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3261 nonce: Some(42),
3262 hash: Some(original_hash.clone()),
3263 raw: Some(vec![1, 2, 3]),
3264 ..test_tx.network_data.get_evm_transaction_data().unwrap()
3265 });
3266 test_tx.hashes = vec![original_hash.clone()];
3267
3268 mock_price_calculator
3270 .expect_calculate_bumped_gas_price()
3271 .times(1)
3272 .returning(|_, _, _| {
3273 Ok(PriceParams {
3274 gas_price: Some(25000000000), max_fee_per_gas: None,
3276 max_priority_fee_per_gas: None,
3277 is_min_bumped: Some(true),
3278 extra_fee: None,
3279 total_cost: U256::from(525000000000000u64),
3280 })
3281 });
3282
3283 mock_provider
3285 .expect_get_balance()
3286 .times(1)
3287 .returning(|_| Box::pin(async { Ok(U256::from(1000000000000000000u64)) }));
3288
3289 mock_signer
3291 .expect_sign_transaction()
3292 .times(1)
3293 .returning(|_| {
3294 Box::pin(ready(Ok(
3295 crate::domain::relayer::SignTransactionResponse::Evm(
3296 crate::domain::relayer::SignTransactionResponseEvm {
3297 hash: "0xnew_hash_that_should_not_be_saved".to_string(),
3298 signature: crate::models::EvmTransactionDataSignature {
3299 r: "r".to_string(),
3300 s: "s".to_string(),
3301 v: 1,
3302 sig: "0xsignature".to_string(),
3303 },
3304 raw: vec![4, 5, 6],
3305 },
3306 ),
3307 )))
3308 });
3309
3310 mock_provider
3312 .expect_send_raw_transaction()
3313 .times(1)
3314 .returning(|_| {
3315 Box::pin(async {
3316 Err(crate::services::provider::ProviderError::Other(
3317 "already known: transaction with same nonce already in mempool".to_string(),
3318 ))
3319 })
3320 });
3321
3322 let test_tx_clone = test_tx.clone();
3324 mock_transaction
3325 .expect_partial_update()
3326 .times(1)
3327 .withf(|_, update| {
3328 update.status == Some(TransactionStatus::Submitted)
3330 && update.network_data.is_none()
3331 && update.hashes.is_none()
3332 })
3333 .returning(move |_, _| {
3334 let mut updated_tx = test_tx_clone.clone();
3335 updated_tx.status = TransactionStatus::Submitted;
3336 Ok(updated_tx)
3338 });
3339
3340 let evm_transaction = EvmRelayerTransaction {
3341 relayer: relayer.clone(),
3342 provider: mock_provider,
3343 relayer_repository: Arc::new(mock_relayer),
3344 network_repository: Arc::new(mock_network),
3345 transaction_repository: Arc::new(mock_transaction),
3346 transaction_counter_service: Arc::new(counter_service),
3347 job_producer: Arc::new(mock_job_producer),
3348 price_calculator: mock_price_calculator,
3349 signer: mock_signer,
3350 };
3351
3352 let result = evm_transaction.resubmit_transaction(test_tx.clone()).await;
3353 assert!(result.is_ok());
3354 let updated_tx = result.unwrap();
3355
3356 if let NetworkTransactionData::Evm(evm_data) = &updated_tx.network_data {
3358 assert_eq!(evm_data.hash, Some(original_hash));
3359 } else {
3360 panic!("Expected EVM network data");
3361 }
3362 }
3363
3364 #[tokio::test]
3367 async fn test_submit_transaction_db_failure_after_blockchain_success() {
3368 let mut mock_transaction = MockTransactionRepository::new();
3369 let mock_relayer = MockRelayerRepository::new();
3370 let mut mock_provider = MockEvmProviderTrait::new();
3371 let mock_signer = MockSigner::new();
3372 let mut mock_job_producer = MockJobProducerTrait::new();
3373 let mock_price_calculator = MockPriceCalculator::new();
3374 let counter_service = MockTransactionCounterTrait::new();
3375 let mock_network = MockNetworkRepository::new();
3376
3377 let relayer = create_test_relayer();
3378 let mut test_tx = create_test_transaction();
3379 test_tx.status = TransactionStatus::Sent;
3380 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3381 raw: Some(vec![1, 2, 3]),
3382 ..test_tx.network_data.get_evm_transaction_data().unwrap()
3383 });
3384
3385 mock_provider
3387 .expect_send_raw_transaction()
3388 .times(1)
3389 .returning(|_| Box::pin(async { Ok("0xsubmitted_hash".to_string()) }));
3390
3391 mock_transaction
3393 .expect_partial_update()
3394 .times(1)
3395 .returning(|_, _| {
3396 Err(crate::models::RepositoryError::UnexpectedError(
3397 "Redis timeout".to_string(),
3398 ))
3399 });
3400
3401 mock_job_producer
3403 .expect_produce_send_notification_job()
3404 .times(1)
3405 .returning(|_, _| Box::pin(ready(Ok(()))));
3406
3407 let evm_transaction = EvmRelayerTransaction {
3408 relayer: relayer.clone(),
3409 provider: mock_provider,
3410 relayer_repository: Arc::new(mock_relayer),
3411 network_repository: Arc::new(mock_network),
3412 transaction_repository: Arc::new(mock_transaction),
3413 transaction_counter_service: Arc::new(counter_service),
3414 job_producer: Arc::new(mock_job_producer),
3415 price_calculator: mock_price_calculator,
3416 signer: mock_signer,
3417 };
3418
3419 let result = evm_transaction.submit_transaction(test_tx.clone()).await;
3420 assert!(result.is_ok());
3422 let returned_tx = result.unwrap();
3423 assert_eq!(returned_tx.id, test_tx.id);
3425 assert_eq!(returned_tx.status, TransactionStatus::Sent); }
3427
3428 #[tokio::test]
3430 async fn test_send_transaction_resend_job_success() {
3431 let mock_transaction = MockTransactionRepository::new();
3432 let mock_relayer = MockRelayerRepository::new();
3433 let mock_provider = MockEvmProviderTrait::new();
3434 let mock_signer = MockSigner::new();
3435 let mut mock_job_producer = MockJobProducerTrait::new();
3436 let mock_price_calculator = MockPriceCalculator::new();
3437 let counter_service = MockTransactionCounterTrait::new();
3438 let mock_network = MockNetworkRepository::new();
3439
3440 let relayer = create_test_relayer();
3441 let test_tx = create_test_transaction();
3442
3443 mock_job_producer
3445 .expect_produce_submit_transaction_job()
3446 .times(1)
3447 .withf(|job, delay| {
3448 job.transaction_id == "test-tx-id"
3450 && job.relayer_id == "test-relayer-id"
3451 && matches!(job.command, crate::jobs::TransactionCommand::Resend)
3452 && delay.is_none()
3453 })
3454 .returning(|_, _| Box::pin(ready(Ok(()))));
3455
3456 let evm_transaction = EvmRelayerTransaction {
3457 relayer: relayer.clone(),
3458 provider: mock_provider,
3459 relayer_repository: Arc::new(mock_relayer),
3460 network_repository: Arc::new(mock_network),
3461 transaction_repository: Arc::new(mock_transaction),
3462 transaction_counter_service: Arc::new(counter_service),
3463 job_producer: Arc::new(mock_job_producer),
3464 price_calculator: mock_price_calculator,
3465 signer: mock_signer,
3466 };
3467
3468 let result = evm_transaction.send_transaction_resend_job(&test_tx).await;
3469 assert!(result.is_ok());
3470 }
3471
3472 #[tokio::test]
3474 async fn test_send_transaction_resend_job_failure() {
3475 let mock_transaction = MockTransactionRepository::new();
3476 let mock_relayer = MockRelayerRepository::new();
3477 let mock_provider = MockEvmProviderTrait::new();
3478 let mock_signer = MockSigner::new();
3479 let mut mock_job_producer = MockJobProducerTrait::new();
3480 let mock_price_calculator = MockPriceCalculator::new();
3481 let counter_service = MockTransactionCounterTrait::new();
3482 let mock_network = MockNetworkRepository::new();
3483
3484 let relayer = create_test_relayer();
3485 let test_tx = create_test_transaction();
3486
3487 mock_job_producer
3489 .expect_produce_submit_transaction_job()
3490 .times(1)
3491 .returning(|_, _| {
3492 Box::pin(ready(Err(crate::jobs::JobProducerError::QueueError(
3493 "Job queue is full".to_string(),
3494 ))))
3495 });
3496
3497 let evm_transaction = EvmRelayerTransaction {
3498 relayer: relayer.clone(),
3499 provider: mock_provider,
3500 relayer_repository: Arc::new(mock_relayer),
3501 network_repository: Arc::new(mock_network),
3502 transaction_repository: Arc::new(mock_transaction),
3503 transaction_counter_service: Arc::new(counter_service),
3504 job_producer: Arc::new(mock_job_producer),
3505 price_calculator: mock_price_calculator,
3506 signer: mock_signer,
3507 };
3508
3509 let result = evm_transaction.send_transaction_resend_job(&test_tx).await;
3510 assert!(result.is_err());
3511 let err = result.unwrap_err();
3512 match err {
3513 TransactionError::UnexpectedError(msg) => {
3514 assert!(msg.contains("Failed to produce resend job"));
3515 }
3516 _ => panic!("Expected UnexpectedError"),
3517 }
3518 }
3519
3520 #[tokio::test]
3522 async fn test_send_transaction_request_job_success() {
3523 let mock_transaction = MockTransactionRepository::new();
3524 let mock_relayer = MockRelayerRepository::new();
3525 let mock_provider = MockEvmProviderTrait::new();
3526 let mock_signer = MockSigner::new();
3527 let mut mock_job_producer = MockJobProducerTrait::new();
3528 let mock_price_calculator = MockPriceCalculator::new();
3529 let counter_service = MockTransactionCounterTrait::new();
3530 let mock_network = MockNetworkRepository::new();
3531
3532 let relayer = create_test_relayer();
3533 let test_tx = create_test_transaction();
3534
3535 mock_job_producer
3537 .expect_produce_transaction_request_job()
3538 .times(1)
3539 .withf(|job, delay| {
3540 job.transaction_id == "test-tx-id"
3542 && job.relayer_id == "test-relayer-id"
3543 && delay.is_none()
3544 })
3545 .returning(|_, _| Box::pin(ready(Ok(()))));
3546
3547 let evm_transaction = EvmRelayerTransaction {
3548 relayer: relayer.clone(),
3549 provider: mock_provider,
3550 relayer_repository: Arc::new(mock_relayer),
3551 network_repository: Arc::new(mock_network),
3552 transaction_repository: Arc::new(mock_transaction),
3553 transaction_counter_service: Arc::new(counter_service),
3554 job_producer: Arc::new(mock_job_producer),
3555 price_calculator: mock_price_calculator,
3556 signer: mock_signer,
3557 };
3558
3559 let result = evm_transaction.send_transaction_request_job(&test_tx).await;
3560 assert!(result.is_ok());
3561 }
3562
3563 #[tokio::test]
3565 async fn test_send_transaction_request_job_failure() {
3566 let mock_transaction = MockTransactionRepository::new();
3567 let mock_relayer = MockRelayerRepository::new();
3568 let mock_provider = MockEvmProviderTrait::new();
3569 let mock_signer = MockSigner::new();
3570 let mut mock_job_producer = MockJobProducerTrait::new();
3571 let mock_price_calculator = MockPriceCalculator::new();
3572 let counter_service = MockTransactionCounterTrait::new();
3573 let mock_network = MockNetworkRepository::new();
3574
3575 let relayer = create_test_relayer();
3576 let test_tx = create_test_transaction();
3577
3578 mock_job_producer
3580 .expect_produce_transaction_request_job()
3581 .times(1)
3582 .returning(|_, _| {
3583 Box::pin(ready(Err(crate::jobs::JobProducerError::QueueError(
3584 "Redis connection failed".to_string(),
3585 ))))
3586 });
3587
3588 let evm_transaction = EvmRelayerTransaction {
3589 relayer: relayer.clone(),
3590 provider: mock_provider,
3591 relayer_repository: Arc::new(mock_relayer),
3592 network_repository: Arc::new(mock_network),
3593 transaction_repository: Arc::new(mock_transaction),
3594 transaction_counter_service: Arc::new(counter_service),
3595 job_producer: Arc::new(mock_job_producer),
3596 price_calculator: mock_price_calculator,
3597 signer: mock_signer,
3598 };
3599
3600 let result = evm_transaction.send_transaction_request_job(&test_tx).await;
3601 assert!(result.is_err());
3602 let err = result.unwrap_err();
3603 match err {
3604 TransactionError::UnexpectedError(msg) => {
3605 assert!(msg.contains("Failed to produce request job"));
3606 }
3607 _ => panic!("Expected UnexpectedError"),
3608 }
3609 }
3610
3611 #[tokio::test]
3613 async fn test_resubmit_transaction_sent_to_submitted() {
3614 let mut mock_transaction = MockTransactionRepository::new();
3615 let mock_relayer = MockRelayerRepository::new();
3616 let mut mock_provider = MockEvmProviderTrait::new();
3617 let mut mock_signer = MockSigner::new();
3618 let mock_job_producer = MockJobProducerTrait::new();
3619 let mut mock_price_calculator = MockPriceCalculator::new();
3620 let counter_service = MockTransactionCounterTrait::new();
3621 let mock_network = MockNetworkRepository::new();
3622
3623 let relayer = create_test_relayer();
3624 let mut test_tx = create_test_transaction();
3625 test_tx.status = TransactionStatus::Sent;
3626 test_tx.sent_at = Some(Utc::now().to_rfc3339());
3627 let original_hash = "0xoriginal_hash".to_string();
3628 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3629 nonce: Some(42),
3630 hash: Some(original_hash.clone()),
3631 raw: Some(vec![1, 2, 3]),
3632 gas_price: Some(20000000000), ..test_tx.network_data.get_evm_transaction_data().unwrap()
3634 });
3635 test_tx.hashes = vec![original_hash.clone()];
3636
3637 mock_price_calculator
3639 .expect_calculate_bumped_gas_price()
3640 .times(1)
3641 .returning(|_, _, _| {
3642 Ok(PriceParams {
3643 gas_price: Some(25000000000), max_fee_per_gas: None,
3645 max_priority_fee_per_gas: None,
3646 is_min_bumped: Some(true),
3647 extra_fee: None,
3648 total_cost: U256::from(525000000000000u64),
3649 })
3650 });
3651
3652 mock_provider
3654 .expect_get_balance()
3655 .returning(|_| Box::pin(ready(Ok(U256::from(1000000000000000000u64)))));
3656
3657 mock_signer.expect_sign_transaction().returning(|_| {
3659 Box::pin(ready(Ok(
3660 crate::domain::relayer::SignTransactionResponse::Evm(
3661 crate::domain::relayer::SignTransactionResponseEvm {
3662 hash: "0xnew_hash".to_string(),
3663 signature: crate::models::EvmTransactionDataSignature {
3664 r: "r".to_string(),
3665 s: "s".to_string(),
3666 v: 1,
3667 sig: "0xsignature".to_string(),
3668 },
3669 raw: vec![4, 5, 6],
3670 },
3671 ),
3672 )))
3673 });
3674
3675 mock_provider
3677 .expect_send_raw_transaction()
3678 .times(1)
3679 .returning(|_| Box::pin(async { Ok("0xnew_hash".to_string()) }));
3680
3681 let test_tx_clone = test_tx.clone();
3683 mock_transaction
3684 .expect_partial_update()
3685 .times(1)
3686 .withf(|_, update| {
3687 update.status == Some(TransactionStatus::Submitted)
3688 && update.sent_at.is_some()
3689 && update.priced_at.is_some()
3690 && update.hashes.is_some()
3691 })
3692 .returning(move |_, update| {
3693 let mut updated_tx = test_tx_clone.clone();
3694 updated_tx.status = update.status.unwrap();
3695 updated_tx.sent_at = update.sent_at.clone();
3696 updated_tx.priced_at = update.priced_at.clone();
3697 if let Some(hashes) = update.hashes.clone() {
3698 updated_tx.hashes = hashes;
3699 }
3700 if let Some(network_data) = update.network_data.clone() {
3701 updated_tx.network_data = network_data;
3702 }
3703 Ok(updated_tx)
3704 });
3705
3706 let evm_transaction = EvmRelayerTransaction {
3707 relayer: relayer.clone(),
3708 provider: mock_provider,
3709 relayer_repository: Arc::new(mock_relayer),
3710 network_repository: Arc::new(mock_network),
3711 transaction_repository: Arc::new(mock_transaction),
3712 transaction_counter_service: Arc::new(counter_service),
3713 job_producer: Arc::new(mock_job_producer),
3714 price_calculator: mock_price_calculator,
3715 signer: mock_signer,
3716 };
3717
3718 let result = evm_transaction.resubmit_transaction(test_tx.clone()).await;
3719 assert!(result.is_ok(), "Expected Ok, got: {result:?}");
3720 let updated_tx = result.unwrap();
3721 assert_eq!(
3722 updated_tx.status,
3723 TransactionStatus::Submitted,
3724 "Transaction status should transition from Sent to Submitted"
3725 );
3726 }
3727
3728 #[test]
3729 fn test_classify_submission_error_nonce_too_low() {
3730 assert_eq!(
3731 DefaultEvmTransaction::classify_submission_error(&"nonce too low"),
3732 SubmissionErrorKind::NonceTooLow
3733 );
3734 assert_eq!(
3735 DefaultEvmTransaction::classify_submission_error(&"Nonce Too Low"),
3736 SubmissionErrorKind::NonceTooLow
3737 );
3738 assert_eq!(
3739 DefaultEvmTransaction::classify_submission_error(&"nonce is too low"),
3740 SubmissionErrorKind::NonceTooLow
3741 );
3742 }
3743
3744 #[test]
3745 fn test_classify_submission_error_already_known() {
3746 assert_eq!(
3747 DefaultEvmTransaction::classify_submission_error(&"already known"),
3748 SubmissionErrorKind::AlreadyKnown
3749 );
3750 assert_eq!(
3751 DefaultEvmTransaction::classify_submission_error(&"known transaction"),
3752 SubmissionErrorKind::AlreadyKnown
3753 );
3754 assert_eq!(
3755 DefaultEvmTransaction::classify_submission_error(&"same hash was already imported"),
3756 SubmissionErrorKind::AlreadyKnown
3757 );
3758 assert!(matches!(
3760 DefaultEvmTransaction::classify_submission_error(&"unknown transaction"),
3761 SubmissionErrorKind::Other(_)
3762 ));
3763 }
3764
3765 #[test]
3766 fn test_classify_submission_error_replacement_underpriced() {
3767 assert_eq!(
3768 DefaultEvmTransaction::classify_submission_error(
3769 &"replacement transaction underpriced"
3770 ),
3771 SubmissionErrorKind::ReplacementUnderpriced
3772 );
3773 }
3774
3775 #[test]
3776 fn test_classify_submission_error_other() {
3777 assert!(matches!(
3778 DefaultEvmTransaction::classify_submission_error(&"execution reverted"),
3779 SubmissionErrorKind::Other(_)
3780 ));
3781 assert!(matches!(
3782 DefaultEvmTransaction::classify_submission_error(&"gas too low"),
3783 SubmissionErrorKind::Other(_)
3784 ));
3785 assert!(matches!(
3787 DefaultEvmTransaction::classify_submission_error(
3788 &"insufficient funds for gas * price + value"
3789 ),
3790 SubmissionErrorKind::Other(_)
3791 ));
3792 }
3793
3794 #[test]
3795 fn test_classify_submission_error_nonce_too_high() {
3796 assert_eq!(
3797 DefaultEvmTransaction::classify_submission_error(&"nonce too high"),
3798 SubmissionErrorKind::NonceTooHigh
3799 );
3800 assert_eq!(
3801 DefaultEvmTransaction::classify_submission_error(&"exceeds next nonce"),
3802 SubmissionErrorKind::NonceTooHigh
3803 );
3804 assert_eq!(
3805 DefaultEvmTransaction::classify_submission_error(&"nonce too far in the future"),
3806 SubmissionErrorKind::NonceTooHigh
3807 );
3808 assert_eq!(
3809 DefaultEvmTransaction::classify_submission_error(&"nonce out of range"),
3810 SubmissionErrorKind::NonceTooHigh
3811 );
3812 }
3813
3814 #[test]
3815 fn test_classify_submission_error_nonce_too_high_case_insensitive() {
3816 assert_eq!(
3817 DefaultEvmTransaction::classify_submission_error(&"Nonce Too High"),
3818 SubmissionErrorKind::NonceTooHigh
3819 );
3820 assert_eq!(
3821 DefaultEvmTransaction::classify_submission_error(&"NONCE OUT OF RANGE"),
3822 SubmissionErrorKind::NonceTooHigh
3823 );
3824 assert_eq!(
3825 DefaultEvmTransaction::classify_submission_error(&"Exceeds Next Nonce"),
3826 SubmissionErrorKind::NonceTooHigh
3827 );
3828 }
3829
3830 #[tokio::test]
3833 async fn test_submit_transaction_nonce_too_low_on_submitted_schedules_recovery() {
3834 let mut mock_transaction = MockTransactionRepository::new();
3835 let mock_relayer = MockRelayerRepository::new();
3836 let mut mock_provider = MockEvmProviderTrait::new();
3837 let mock_signer = MockSigner::new();
3838 let mut mock_job_producer = MockJobProducerTrait::new();
3839 let mock_price_calculator = MockPriceCalculator::new();
3840 let counter_service = MockTransactionCounterTrait::new();
3841 let mock_network = MockNetworkRepository::new();
3842
3843 let relayer = create_test_relayer();
3844 let mut test_tx = create_test_transaction();
3845 test_tx.status = TransactionStatus::Submitted;
3846 test_tx.sent_at = Some(Utc::now().to_rfc3339());
3847 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3848 nonce: Some(42),
3849 hash: Some("0xhash".to_string()),
3850 raw: Some(vec![1, 2, 3]),
3851 ..test_tx.network_data.get_evm_transaction_data().unwrap()
3852 });
3853
3854 mock_provider
3856 .expect_send_raw_transaction()
3857 .times(1)
3858 .returning(|_| {
3859 Box::pin(async {
3860 Err(crate::services::provider::ProviderError::Other(
3861 "nonce too low".to_string(),
3862 ))
3863 })
3864 });
3865
3866 let test_tx_clone = test_tx.clone();
3868 mock_transaction
3869 .expect_partial_update()
3870 .times(1)
3871 .withf(|_, update| update.status_reason.is_some())
3872 .returning(move |_, _| Ok(test_tx_clone.clone()));
3873
3874 mock_job_producer
3876 .expect_produce_check_transaction_status_job()
3877 .times(1)
3878 .withf(|job, _| {
3879 job.metadata
3880 .as_ref()
3881 .map(|m| m.contains_key(TX_NONCE_RECONCILE_TRIGGER))
3882 .unwrap_or(false)
3883 })
3884 .returning(|_, _| Box::pin(ready(Ok(()))));
3885
3886 let evm_transaction = EvmRelayerTransaction {
3887 relayer: relayer.clone(),
3888 provider: mock_provider,
3889 relayer_repository: Arc::new(mock_relayer),
3890 network_repository: Arc::new(mock_network),
3891 transaction_repository: Arc::new(mock_transaction),
3892 transaction_counter_service: Arc::new(counter_service),
3893 job_producer: Arc::new(mock_job_producer),
3894 price_calculator: mock_price_calculator,
3895 signer: mock_signer,
3896 };
3897
3898 let result = evm_transaction.submit_transaction(test_tx).await;
3900 assert!(
3901 result.is_ok(),
3902 "Expected Ok on nonce error for non-Sent tx, got: {result:?}"
3903 );
3904 }
3905
3906 #[tokio::test]
3911 async fn test_submit_transaction_nonce_too_low_on_sent_schedules_recovery() {
3912 let mut mock_transaction = MockTransactionRepository::new();
3913 let mock_relayer = MockRelayerRepository::new();
3914 let mut mock_provider = MockEvmProviderTrait::new();
3915 let mock_signer = MockSigner::new();
3916 let mut mock_job_producer = MockJobProducerTrait::new();
3917 let mock_price_calculator = MockPriceCalculator::new();
3918 let counter_service = MockTransactionCounterTrait::new();
3919 let mock_network = MockNetworkRepository::new();
3920
3921 let relayer = create_test_relayer();
3922 let mut test_tx = create_test_transaction();
3923 test_tx.status = TransactionStatus::Sent;
3924 test_tx.sent_at = Some(Utc::now().to_rfc3339());
3925 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3926 nonce: Some(42),
3927 hash: Some("0xhash".to_string()),
3928 raw: Some(vec![1, 2, 3]),
3929 ..test_tx.network_data.get_evm_transaction_data().unwrap()
3930 });
3931
3932 mock_provider
3934 .expect_send_raw_transaction()
3935 .times(1)
3936 .returning(|_| {
3937 Box::pin(async {
3938 Err(crate::services::provider::ProviderError::Other(
3939 "nonce too low".to_string(),
3940 ))
3941 })
3942 });
3943
3944 let test_tx_clone = test_tx.clone();
3946 mock_transaction
3947 .expect_partial_update()
3948 .times(1)
3949 .withf(|_, update| update.status_reason.is_some())
3950 .returning(move |_, _| Ok(test_tx_clone.clone()));
3951
3952 mock_job_producer
3954 .expect_produce_check_transaction_status_job()
3955 .times(1)
3956 .withf(|job, _| {
3957 job.metadata
3958 .as_ref()
3959 .map(|m| m.contains_key(TX_NONCE_RECONCILE_TRIGGER))
3960 .unwrap_or(false)
3961 })
3962 .returning(|_, _| Box::pin(ready(Ok(()))));
3963
3964 let evm_transaction = EvmRelayerTransaction {
3965 relayer: relayer.clone(),
3966 provider: mock_provider,
3967 relayer_repository: Arc::new(mock_relayer),
3968 network_repository: Arc::new(mock_network),
3969 transaction_repository: Arc::new(mock_transaction),
3970 transaction_counter_service: Arc::new(counter_service),
3971 job_producer: Arc::new(mock_job_producer),
3972 price_calculator: mock_price_calculator,
3973 signer: mock_signer,
3974 };
3975
3976 let result = evm_transaction.submit_transaction(test_tx.clone()).await;
3978 assert!(
3979 result.is_ok(),
3980 "Expected Ok on nonce too low for Sent tx, got: {result:?}"
3981 );
3982 let returned_tx = result.unwrap();
3984 assert_eq!(returned_tx.status, TransactionStatus::Sent);
3985 }
3986
3987 #[tokio::test]
3989 async fn test_resubmit_transaction_nonce_too_low_schedules_recovery() {
3990 let mut mock_transaction = MockTransactionRepository::new();
3991 let mock_relayer = MockRelayerRepository::new();
3992 let mut mock_provider = MockEvmProviderTrait::new();
3993 let mut mock_signer = MockSigner::new();
3994 let mut mock_job_producer = MockJobProducerTrait::new();
3995 let mut mock_price_calculator = MockPriceCalculator::new();
3996 let counter_service = MockTransactionCounterTrait::new();
3997 let mock_network = MockNetworkRepository::new();
3998
3999 let relayer = create_test_relayer();
4000 let mut test_tx = create_test_transaction();
4001 test_tx.status = TransactionStatus::Submitted;
4002 test_tx.sent_at = Some(Utc::now().to_rfc3339());
4003 let original_hash = "0xoriginal_hash".to_string();
4004 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
4005 nonce: Some(42),
4006 hash: Some(original_hash.clone()),
4007 raw: Some(vec![1, 2, 3]),
4008 ..test_tx.network_data.get_evm_transaction_data().unwrap()
4009 });
4010 test_tx.hashes = vec![original_hash.clone()];
4011
4012 mock_price_calculator
4014 .expect_calculate_bumped_gas_price()
4015 .times(1)
4016 .returning(|_, _, _| {
4017 Ok(PriceParams {
4018 gas_price: Some(25000000000),
4019 max_fee_per_gas: None,
4020 max_priority_fee_per_gas: None,
4021 is_min_bumped: Some(true),
4022 extra_fee: None,
4023 total_cost: U256::from(525000000000000u64),
4024 })
4025 });
4026
4027 mock_provider
4029 .expect_get_balance()
4030 .times(1)
4031 .returning(|_| Box::pin(async { Ok(U256::from(1000000000000000000u64)) }));
4032
4033 mock_signer
4035 .expect_sign_transaction()
4036 .times(1)
4037 .returning(|_| {
4038 Box::pin(ready(Ok(
4039 crate::domain::relayer::SignTransactionResponse::Evm(
4040 crate::domain::relayer::SignTransactionResponseEvm {
4041 hash: "0xnew_hash".to_string(),
4042 signature: crate::models::EvmTransactionDataSignature {
4043 r: "r".to_string(),
4044 s: "s".to_string(),
4045 v: 1,
4046 sig: "0xsignature".to_string(),
4047 },
4048 raw: vec![4, 5, 6],
4049 },
4050 ),
4051 )))
4052 });
4053
4054 mock_provider
4056 .expect_send_raw_transaction()
4057 .times(1)
4058 .returning(|_| {
4059 Box::pin(async {
4060 Err(crate::services::provider::ProviderError::Other(
4061 "nonce too low".to_string(),
4062 ))
4063 })
4064 });
4065
4066 mock_job_producer
4068 .expect_produce_check_transaction_status_job()
4069 .times(1)
4070 .withf(|job, _| {
4071 job.metadata
4072 .as_ref()
4073 .map(|m| m.contains_key(TX_NONCE_RECONCILE_TRIGGER))
4074 .unwrap_or(false)
4075 })
4076 .returning(|_, _| Box::pin(ready(Ok(()))));
4077
4078 let test_tx_clone = test_tx.clone();
4080 mock_transaction
4081 .expect_partial_update()
4082 .times(1)
4083 .withf(|_, update| {
4084 update.status == Some(TransactionStatus::Submitted)
4085 && update.network_data.is_none()
4086 && update.hashes.is_none()
4087 })
4088 .returning(move |_, _| {
4089 let mut updated_tx = test_tx_clone.clone();
4090 updated_tx.status = TransactionStatus::Submitted;
4091 Ok(updated_tx)
4092 });
4093
4094 let evm_transaction = EvmRelayerTransaction {
4095 relayer: relayer.clone(),
4096 provider: mock_provider,
4097 relayer_repository: Arc::new(mock_relayer),
4098 network_repository: Arc::new(mock_network),
4099 transaction_repository: Arc::new(mock_transaction),
4100 transaction_counter_service: Arc::new(counter_service),
4101 job_producer: Arc::new(mock_job_producer),
4102 price_calculator: mock_price_calculator,
4103 signer: mock_signer,
4104 };
4105
4106 let result = evm_transaction.resubmit_transaction(test_tx.clone()).await;
4107 assert!(result.is_ok());
4108 let updated_tx = result.unwrap();
4109 if let NetworkTransactionData::Evm(evm_data) = &updated_tx.network_data {
4111 assert_eq!(evm_data.hash, Some(original_hash));
4112 } else {
4113 panic!("Expected EVM network data");
4114 }
4115 }
4116
4117 #[tokio::test]
4119 async fn test_submit_transaction_nonce_too_high_increments_retry_counter() {
4120 let mut mock_transaction = MockTransactionRepository::new();
4121 let mock_relayer = MockRelayerRepository::new();
4122 let mut mock_provider = MockEvmProviderTrait::new();
4123 let mock_signer = MockSigner::new();
4124 let mock_job_producer = MockJobProducerTrait::new();
4125 let mock_price_calculator = MockPriceCalculator::new();
4126 let counter_service = MockTransactionCounterTrait::new();
4127 let mock_network = MockNetworkRepository::new();
4128
4129 let relayer = create_test_relayer();
4130 let mut test_tx = create_test_transaction();
4131 test_tx.status = TransactionStatus::Sent;
4132 test_tx.sent_at = Some(Utc::now().to_rfc3339());
4133 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
4134 nonce: Some(10),
4135 hash: Some("0xhash".to_string()),
4136 raw: Some(vec![1, 2, 3]),
4137 ..test_tx.network_data.get_evm_transaction_data().unwrap()
4138 });
4139 test_tx.metadata = Some(crate::models::TransactionMetadata {
4141 nonce_too_high_retries: 0,
4142 ..Default::default()
4143 });
4144
4145 mock_provider
4147 .expect_send_raw_transaction()
4148 .times(1)
4149 .returning(|_| {
4150 Box::pin(async {
4151 Err(crate::services::provider::ProviderError::Other(
4152 "nonce too high".to_string(),
4153 ))
4154 })
4155 });
4156
4157 let test_tx_clone = test_tx.clone();
4159 mock_transaction
4160 .expect_partial_update()
4161 .times(1)
4162 .withf(|_, update| {
4163 update
4164 .metadata
4165 .as_ref()
4166 .map(|m| m.nonce_too_high_retries == 1)
4167 .unwrap_or(false)
4168 })
4169 .returning(move |_, _| Ok(test_tx_clone.clone()));
4170
4171 let evm_transaction = EvmRelayerTransaction {
4172 relayer: relayer.clone(),
4173 provider: mock_provider,
4174 relayer_repository: Arc::new(mock_relayer),
4175 network_repository: Arc::new(mock_network),
4176 transaction_repository: Arc::new(mock_transaction),
4177 transaction_counter_service: Arc::new(counter_service),
4178 job_producer: Arc::new(mock_job_producer),
4179 price_calculator: mock_price_calculator,
4180 signer: mock_signer,
4181 };
4182
4183 let result = evm_transaction.submit_transaction(test_tx).await;
4185 assert!(
4186 result.is_ok(),
4187 "Expected Ok on nonce too high, got: {result:?}"
4188 );
4189 }
4190
4191 #[tokio::test]
4194 async fn test_submit_transaction_nonce_too_high_schedules_health_job_at_threshold() {
4195 let mut mock_transaction = MockTransactionRepository::new();
4196 let mock_relayer = MockRelayerRepository::new();
4197 let mut mock_provider = MockEvmProviderTrait::new();
4198 let mock_signer = MockSigner::new();
4199 let mut mock_job_producer = MockJobProducerTrait::new();
4200 let mock_price_calculator = MockPriceCalculator::new();
4201 let counter_service = MockTransactionCounterTrait::new();
4202 let mock_network = MockNetworkRepository::new();
4203
4204 let relayer = create_test_relayer();
4205 let mut test_tx = create_test_transaction();
4206 test_tx.status = TransactionStatus::Sent;
4207 test_tx.sent_at = Some(Utc::now().to_rfc3339());
4208 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
4209 nonce: Some(10),
4210 hash: Some("0xhash".to_string()),
4211 raw: Some(vec![1, 2, 3]),
4212 ..test_tx.network_data.get_evm_transaction_data().unwrap()
4213 });
4214 test_tx.metadata = Some(crate::models::TransactionMetadata {
4216 nonce_too_high_retries: 2,
4217 ..Default::default()
4218 });
4219
4220 mock_provider
4222 .expect_send_raw_transaction()
4223 .times(1)
4224 .returning(|_| {
4225 Box::pin(async {
4226 Err(crate::services::provider::ProviderError::Other(
4227 "nonce too high".to_string(),
4228 ))
4229 })
4230 });
4231
4232 let test_tx_clone = test_tx.clone();
4234 mock_transaction
4235 .expect_partial_update()
4236 .times(1)
4237 .withf(|_, update| {
4238 update
4239 .metadata
4240 .as_ref()
4241 .map(|m| m.nonce_too_high_retries == 3)
4242 .unwrap_or(false)
4243 })
4244 .returning(move |_, _| Ok(test_tx_clone.clone()));
4245
4246 mock_job_producer
4248 .expect_produce_relayer_health_check_job()
4249 .times(1)
4250 .returning(|_, _| Box::pin(ready(Ok(()))));
4251
4252 let evm_transaction = EvmRelayerTransaction {
4253 relayer: relayer.clone(),
4254 provider: mock_provider,
4255 relayer_repository: Arc::new(mock_relayer),
4256 network_repository: Arc::new(mock_network),
4257 transaction_repository: Arc::new(mock_transaction),
4258 transaction_counter_service: Arc::new(counter_service),
4259 job_producer: Arc::new(mock_job_producer),
4260 price_calculator: mock_price_calculator,
4261 signer: mock_signer,
4262 };
4263
4264 let result = evm_transaction.submit_transaction(test_tx).await;
4266 assert!(
4267 result.is_ok(),
4268 "Expected Ok on nonce too high at threshold, got: {result:?}"
4269 );
4270 }
4271
4272 #[tokio::test]
4274 async fn test_resubmit_transaction_nonce_too_high_returns_ok() {
4275 let mut mock_transaction = MockTransactionRepository::new();
4276 let mock_relayer = MockRelayerRepository::new();
4277 let mut mock_provider = MockEvmProviderTrait::new();
4278 let mut mock_signer = MockSigner::new();
4279 let mock_job_producer = MockJobProducerTrait::new();
4280 let mut mock_price_calculator = MockPriceCalculator::new();
4281 let counter_service = MockTransactionCounterTrait::new();
4282 let mock_network = MockNetworkRepository::new();
4283
4284 let relayer = create_test_relayer();
4285 let mut test_tx = create_test_transaction();
4286 test_tx.status = TransactionStatus::Submitted;
4287 test_tx.sent_at = Some(Utc::now().to_rfc3339());
4288 let original_hash = "0xoriginal_hash".to_string();
4289 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
4290 nonce: Some(10),
4291 hash: Some(original_hash.clone()),
4292 raw: Some(vec![1, 2, 3]),
4293 ..test_tx.network_data.get_evm_transaction_data().unwrap()
4294 });
4295 test_tx.hashes = vec![original_hash.clone()];
4296 test_tx.metadata = Some(crate::models::TransactionMetadata {
4298 nonce_too_high_retries: 0,
4299 ..Default::default()
4300 });
4301
4302 mock_price_calculator
4304 .expect_calculate_bumped_gas_price()
4305 .times(1)
4306 .returning(|_, _, _| {
4307 Ok(PriceParams {
4308 gas_price: Some(25000000000),
4309 max_fee_per_gas: None,
4310 max_priority_fee_per_gas: None,
4311 is_min_bumped: Some(true),
4312 extra_fee: None,
4313 total_cost: U256::from(525000000000000u64),
4314 })
4315 });
4316
4317 mock_provider
4319 .expect_get_balance()
4320 .times(1)
4321 .returning(|_| Box::pin(async { Ok(U256::from(1000000000000000000u64)) }));
4322
4323 mock_signer
4325 .expect_sign_transaction()
4326 .times(1)
4327 .returning(|_| {
4328 Box::pin(ready(Ok(
4329 crate::domain::relayer::SignTransactionResponse::Evm(
4330 crate::domain::relayer::SignTransactionResponseEvm {
4331 hash: "0xnew_hash".to_string(),
4332 signature: crate::models::EvmTransactionDataSignature {
4333 r: "r".to_string(),
4334 s: "s".to_string(),
4335 v: 1,
4336 sig: "0xsignature".to_string(),
4337 },
4338 raw: vec![4, 5, 6],
4339 },
4340 ),
4341 )))
4342 });
4343
4344 mock_provider
4346 .expect_send_raw_transaction()
4347 .times(1)
4348 .returning(|_| {
4349 Box::pin(async {
4350 Err(crate::services::provider::ProviderError::Other(
4351 "nonce too high".to_string(),
4352 ))
4353 })
4354 });
4355
4356 let test_tx_clone = test_tx.clone();
4358 mock_transaction
4359 .expect_partial_update()
4360 .times(1)
4361 .withf(|_, update| {
4362 update
4363 .metadata
4364 .as_ref()
4365 .map(|m| m.nonce_too_high_retries == 1)
4366 .unwrap_or(false)
4367 })
4368 .returning(move |_, _| Ok(test_tx_clone.clone()));
4369
4370 let evm_transaction = EvmRelayerTransaction {
4371 relayer: relayer.clone(),
4372 provider: mock_provider,
4373 relayer_repository: Arc::new(mock_relayer),
4374 network_repository: Arc::new(mock_network),
4375 transaction_repository: Arc::new(mock_transaction),
4376 transaction_counter_service: Arc::new(counter_service),
4377 job_producer: Arc::new(mock_job_producer),
4378 price_calculator: mock_price_calculator,
4379 signer: mock_signer,
4380 };
4381
4382 let result = evm_transaction.resubmit_transaction(test_tx.clone()).await;
4384 assert!(
4385 result.is_ok(),
4386 "Expected Ok on nonce too high during resubmit, got: {result:?}"
4387 );
4388 let returned_tx = result.unwrap();
4389 assert_eq!(returned_tx.status, TransactionStatus::Submitted);
4391 }
4392}