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
1264 .prepare_noop_update_request(
1265 &tx,
1266 true,
1267 Some("Transaction canceled by user, replacing with NOOP".to_string()),
1268 )
1269 .await?;
1270 let updated_tx = self
1271 .transaction_repository()
1272 .partial_update(tx.id.clone(), update)
1273 .await?;
1274
1275 self.send_transaction_resubmit_job(&updated_tx).await?;
1277
1278 if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
1280 error!(
1281 tx_id = %updated_tx.id,
1282 status = ?updated_tx.status,
1283 "sending transaction update notification failed after cancel: {:?}",
1284 e
1285 );
1286 }
1287
1288 debug!("original transaction updated with cancellation data");
1289 Ok(updated_tx)
1290 }
1291
1292 async fn replace_transaction(
1303 &self,
1304 old_tx: TransactionRepoModel,
1305 new_tx_request: NetworkTransactionRequest,
1306 ) -> Result<TransactionRepoModel, TransactionError> {
1307 debug!("replacing transaction");
1308
1309 ensure_status_one_of(
1311 &old_tx,
1312 &[
1313 TransactionStatus::Pending,
1314 TransactionStatus::Sent,
1315 TransactionStatus::Submitted,
1316 ],
1317 Some("replace_transaction"),
1318 )?;
1319
1320 let old_evm_data = old_tx.network_data.get_evm_transaction_data()?;
1322 let new_evm_request = match new_tx_request {
1323 NetworkTransactionRequest::Evm(evm_req) => evm_req,
1324 _ => {
1325 return Err(TransactionError::InvalidType(
1326 "New transaction request must be EVM type".to_string(),
1327 ))
1328 }
1329 };
1330
1331 let network_repo_model = self
1332 .network_repository()
1333 .get_by_chain_id(NetworkType::Evm, old_evm_data.chain_id)
1334 .await
1335 .map_err(|e| {
1336 TransactionError::NetworkConfiguration(format!(
1337 "Failed to get network by chain_id {}: {}",
1338 old_evm_data.chain_id, e
1339 ))
1340 })?
1341 .ok_or_else(|| {
1342 TransactionError::NetworkConfiguration(format!(
1343 "Network with chain_id {} not found",
1344 old_evm_data.chain_id
1345 ))
1346 })?;
1347
1348 let network = EvmNetwork::try_from(network_repo_model).map_err(|e| {
1349 TransactionError::NetworkConfiguration(format!("Failed to convert network model: {e}"))
1350 })?;
1351
1352 let updated_evm_data = EvmTransactionData::for_replacement(&old_evm_data, &new_evm_request);
1354
1355 let price_params = super::replacement::determine_replacement_pricing(
1357 &old_evm_data,
1358 &updated_evm_data,
1359 self.relayer(),
1360 &self.price_calculator,
1361 network.lacks_mempool(),
1362 )
1363 .await?;
1364
1365 debug!(price_params = ?price_params, "replacement price params");
1366
1367 let evm_data_with_price_params = updated_evm_data.with_price_params(price_params.clone());
1369
1370 self.ensure_sufficient_balance(price_params.total_cost)
1372 .await?;
1373
1374 let sig_result = self
1375 .signer
1376 .sign_transaction(NetworkTransactionData::Evm(
1377 evm_data_with_price_params.clone(),
1378 ))
1379 .await?;
1380
1381 let final_evm_data =
1382 evm_data_with_price_params.with_signed_transaction_data(sig_result.into_evm()?);
1383
1384 let updated_tx = self
1386 .transaction_repository
1387 .update_network_data(
1388 old_tx.id.clone(),
1389 NetworkTransactionData::Evm(final_evm_data),
1390 )
1391 .await?;
1392
1393 self.send_transaction_resubmit_job(&updated_tx).await?;
1394
1395 if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
1397 error!(
1398 tx_id = %updated_tx.id,
1399 status = ?updated_tx.status,
1400 "sending transaction update notification failed after replace: {:?}",
1401 e
1402 );
1403 }
1404
1405 Ok(updated_tx)
1406 }
1407
1408 async fn sign_transaction(
1418 &self,
1419 tx: TransactionRepoModel,
1420 ) -> Result<TransactionRepoModel, TransactionError> {
1421 Ok(tx)
1422 }
1423
1424 async fn validate_transaction(
1434 &self,
1435 _tx: TransactionRepoModel,
1436 ) -> Result<bool, TransactionError> {
1437 Ok(true)
1438 }
1439}
1440pub type DefaultEvmTransaction = EvmRelayerTransaction<
1449 EvmProvider,
1450 RelayerRepositoryStorage,
1451 NetworkRepositoryStorage,
1452 TransactionRepositoryStorage,
1453 JobProducer,
1454 EvmSigner,
1455 TransactionCounterRepositoryStorage,
1456 PriceCalculator<EvmGasPriceService<EvmProvider>>,
1457>;
1458#[cfg(test)]
1459mod tests {
1460
1461 use super::*;
1462 use crate::{
1463 domain::evm::price_calculator::PriceParams,
1464 jobs::MockJobProducerTrait,
1465 models::{
1466 evm::Speed, EvmTransactionData, EvmTransactionRequest, NetworkType,
1467 RelayerNetworkPolicy, U256,
1468 },
1469 repositories::{
1470 MockNetworkRepository, MockRelayerRepository, MockTransactionCounterTrait,
1471 MockTransactionRepository,
1472 },
1473 services::{provider::MockEvmProviderTrait, signer::MockSigner},
1474 };
1475 use chrono::Utc;
1476 use futures::future::ready;
1477 use mockall::{mock, predicate::*};
1478
1479 mock! {
1481 pub PriceCalculator {}
1482 #[async_trait]
1483 impl PriceCalculatorTrait for PriceCalculator {
1484 async fn get_transaction_price_params(
1485 &self,
1486 tx_data: &EvmTransactionData,
1487 relayer: &RelayerRepoModel
1488 ) -> Result<PriceParams, TransactionError>;
1489
1490 async fn calculate_bumped_gas_price(
1491 &self,
1492 tx: &EvmTransactionData,
1493 relayer: &RelayerRepoModel,
1494 force_bump: bool,
1495 ) -> Result<PriceParams, TransactionError>;
1496 }
1497 }
1498
1499 fn create_test_relayer() -> RelayerRepoModel {
1501 create_test_relayer_with_policy(crate::models::RelayerEvmPolicy {
1502 include_revert_data: None,
1503 min_balance: Some(100000000000000000u128), gas_limit_estimation: Some(true),
1505 gas_price_cap: Some(100000000000), whitelist_receivers: Some(vec!["0xRecipient".to_string()]),
1507 eip1559_pricing: Some(false),
1508 private_transactions: Some(false),
1509 })
1510 }
1511
1512 fn create_test_relayer_with_policy(evm_policy: RelayerEvmPolicy) -> RelayerRepoModel {
1513 RelayerRepoModel {
1514 id: "test-relayer-id".to_string(),
1515 name: "Test Relayer".to_string(),
1516 network: "1".to_string(), address: "0xSender".to_string(),
1518 paused: false,
1519 system_disabled: false,
1520 signer_id: "test-signer-id".to_string(),
1521 notification_id: Some("test-notification-id".to_string()),
1522 policies: RelayerNetworkPolicy::Evm(evm_policy),
1523 network_type: NetworkType::Evm,
1524 custom_rpc_urls: None,
1525 ..Default::default()
1526 }
1527 }
1528
1529 fn create_test_transaction() -> TransactionRepoModel {
1531 TransactionRepoModel {
1532 id: "test-tx-id".to_string(),
1533 relayer_id: "test-relayer-id".to_string(),
1534 status: TransactionStatus::Pending,
1535 status_reason: None,
1536 created_at: Utc::now().to_rfc3339(),
1537 sent_at: None,
1538 confirmed_at: None,
1539 valid_until: None,
1540 delete_at: None,
1541 network_type: NetworkType::Evm,
1542 network_data: NetworkTransactionData::Evm(EvmTransactionData {
1543 chain_id: 1,
1544 from: "0xSender".to_string(),
1545 to: Some("0xRecipient".to_string()),
1546 value: U256::from(1000000000000000000u64), data: Some("0xData".to_string()),
1548 gas_limit: Some(21000),
1549 gas_price: Some(20000000000), max_fee_per_gas: None,
1551 max_priority_fee_per_gas: None,
1552 nonce: None,
1553 signature: None,
1554 hash: None,
1555 speed: Some(Speed::Fast),
1556 raw: None,
1557 }),
1558 priced_at: None,
1559 hashes: Vec::new(),
1560 noop_count: None,
1561 is_canceled: Some(false),
1562 metadata: None,
1563 }
1564 }
1565
1566 #[tokio::test]
1567 async fn test_prepare_transaction_with_sufficient_balance() {
1568 let mut mock_transaction = MockTransactionRepository::new();
1569 let mock_relayer = MockRelayerRepository::new();
1570 let mut mock_provider = MockEvmProviderTrait::new();
1571 let mut mock_signer = MockSigner::new();
1572 let mut mock_job_producer = MockJobProducerTrait::new();
1573 let mut mock_price_calculator = MockPriceCalculator::new();
1574 let mut counter_service = MockTransactionCounterTrait::new();
1575
1576 let relayer = create_test_relayer();
1577 let test_tx = create_test_transaction();
1578
1579 counter_service
1580 .expect_get_and_increment()
1581 .returning(|_, _| Box::pin(ready(Ok(42))));
1582
1583 let price_params = PriceParams {
1584 gas_price: Some(30000000000),
1585 max_fee_per_gas: None,
1586 max_priority_fee_per_gas: None,
1587 is_min_bumped: None,
1588 extra_fee: None,
1589 total_cost: U256::from(630000000000000u64),
1590 };
1591 mock_price_calculator
1592 .expect_get_transaction_price_params()
1593 .returning(move |_, _| Ok(price_params.clone()));
1594
1595 mock_signer.expect_sign_transaction().returning(|_| {
1596 Box::pin(ready(Ok(
1597 crate::domain::relayer::SignTransactionResponse::Evm(
1598 crate::domain::relayer::SignTransactionResponseEvm {
1599 hash: "0xtx_hash".to_string(),
1600 signature: crate::models::EvmTransactionDataSignature {
1601 r: "r".to_string(),
1602 s: "s".to_string(),
1603 v: 1,
1604 sig: "0xsignature".to_string(),
1605 },
1606 raw: vec![1, 2, 3],
1607 },
1608 ),
1609 )))
1610 });
1611
1612 mock_provider
1613 .expect_get_balance()
1614 .with(eq("0xSender"))
1615 .returning(|_| Box::pin(ready(Ok(U256::from(1000000000000000000u64)))));
1616
1617 mock_provider
1619 .expect_get_block_by_number()
1620 .times(1)
1621 .returning(|| {
1622 Box::pin(async {
1623 use alloy::{network::AnyRpcBlock, rpc::types::Block};
1624 let mut block: Block = Block::default();
1625 block.header.gas_limit = 30_000_000u64;
1627 Ok(AnyRpcBlock::from(block))
1628 })
1629 });
1630
1631 let test_tx_clone = test_tx.clone();
1632 mock_transaction
1633 .expect_partial_update()
1634 .returning(move |_, update| {
1635 let mut updated_tx = test_tx_clone.clone();
1636 if let Some(status) = &update.status {
1637 updated_tx.status = status.clone();
1638 }
1639 if let Some(network_data) = &update.network_data {
1640 updated_tx.network_data = network_data.clone();
1641 }
1642 if let Some(hashes) = &update.hashes {
1643 updated_tx.hashes = hashes.clone();
1644 }
1645 Ok(updated_tx)
1646 });
1647
1648 mock_job_producer
1649 .expect_produce_submit_transaction_job()
1650 .returning(|_, _| Box::pin(ready(Ok(()))));
1651 mock_job_producer
1652 .expect_produce_send_notification_job()
1653 .returning(|_, _| Box::pin(ready(Ok(()))));
1654
1655 let mock_network = MockNetworkRepository::new();
1656
1657 let evm_transaction = EvmRelayerTransaction {
1658 relayer: relayer.clone(),
1659 provider: mock_provider,
1660 relayer_repository: Arc::new(mock_relayer),
1661 network_repository: Arc::new(mock_network),
1662 transaction_repository: Arc::new(mock_transaction),
1663 transaction_counter_service: Arc::new(counter_service),
1664 job_producer: Arc::new(mock_job_producer),
1665 price_calculator: mock_price_calculator,
1666 signer: mock_signer,
1667 };
1668
1669 let result = evm_transaction.prepare_transaction(test_tx.clone()).await;
1670 assert!(result.is_ok());
1671 let prepared_tx = result.unwrap();
1672 assert_eq!(prepared_tx.status, TransactionStatus::Sent);
1673 assert!(!prepared_tx.hashes.is_empty());
1674 }
1675
1676 #[tokio::test]
1677 async fn test_prepare_transaction_with_insufficient_balance() {
1678 let mut mock_transaction = MockTransactionRepository::new();
1679 let mock_relayer = MockRelayerRepository::new();
1680 let mut mock_provider = MockEvmProviderTrait::new();
1681 let mut mock_signer = MockSigner::new();
1682 let mut mock_job_producer = MockJobProducerTrait::new();
1683 let mut mock_price_calculator = MockPriceCalculator::new();
1684 let mut counter_service = MockTransactionCounterTrait::new();
1685
1686 let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
1687 gas_limit_estimation: Some(false),
1688 min_balance: Some(100000000000000000u128),
1689 ..Default::default()
1690 });
1691 let test_tx = create_test_transaction();
1692
1693 counter_service
1694 .expect_get_and_increment()
1695 .returning(|_, _| Box::pin(ready(Ok(42))));
1696
1697 let price_params = PriceParams {
1698 gas_price: Some(30000000000),
1699 max_fee_per_gas: None,
1700 max_priority_fee_per_gas: None,
1701 is_min_bumped: None,
1702 extra_fee: None,
1703 total_cost: U256::from(630000000000000u64),
1704 };
1705 mock_price_calculator
1706 .expect_get_transaction_price_params()
1707 .returning(move |_, _| Ok(price_params.clone()));
1708
1709 mock_signer.expect_sign_transaction().returning(|_| {
1710 Box::pin(ready(Ok(
1711 crate::domain::relayer::SignTransactionResponse::Evm(
1712 crate::domain::relayer::SignTransactionResponseEvm {
1713 hash: "0xtx_hash".to_string(),
1714 signature: crate::models::EvmTransactionDataSignature {
1715 r: "r".to_string(),
1716 s: "s".to_string(),
1717 v: 1,
1718 sig: "0xsignature".to_string(),
1719 },
1720 raw: vec![1, 2, 3],
1721 },
1722 ),
1723 )))
1724 });
1725
1726 mock_provider
1727 .expect_get_balance()
1728 .with(eq("0xSender"))
1729 .returning(|_| Box::pin(ready(Ok(U256::from(90000000000000000u64)))));
1730
1731 mock_provider
1733 .expect_get_block_by_number()
1734 .times(1)
1735 .returning(|| {
1736 Box::pin(async {
1737 use alloy::{network::AnyRpcBlock, rpc::types::Block};
1738 let mut block: Block = Block::default();
1739 block.header.gas_limit = 30_000_000u64;
1741 Ok(AnyRpcBlock::from(block))
1742 })
1743 });
1744
1745 let test_tx_clone = test_tx.clone();
1746 mock_transaction
1747 .expect_partial_update()
1748 .withf(move |id, update| {
1749 id == "test-tx-id" && update.status == Some(TransactionStatus::Failed)
1750 })
1751 .returning(move |_, update| {
1752 let mut updated_tx = test_tx_clone.clone();
1753 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1754 updated_tx.status_reason = update.status_reason.clone();
1755 Ok(updated_tx)
1756 });
1757
1758 mock_job_producer
1759 .expect_produce_send_notification_job()
1760 .returning(|_, _| Box::pin(ready(Ok(()))));
1761
1762 let mock_network = MockNetworkRepository::new();
1763
1764 let evm_transaction = EvmRelayerTransaction {
1765 relayer: relayer.clone(),
1766 provider: mock_provider,
1767 relayer_repository: Arc::new(mock_relayer),
1768 network_repository: Arc::new(mock_network),
1769 transaction_repository: Arc::new(mock_transaction),
1770 transaction_counter_service: Arc::new(counter_service),
1771 job_producer: Arc::new(mock_job_producer),
1772 price_calculator: mock_price_calculator,
1773 signer: mock_signer,
1774 };
1775
1776 let result = evm_transaction.prepare_transaction(test_tx.clone()).await;
1777 assert!(result.is_ok(), "Expected Ok, got: {result:?}");
1778
1779 let updated_tx = result.unwrap();
1780 assert_eq!(
1781 updated_tx.status,
1782 TransactionStatus::Failed,
1783 "Transaction should be marked as Failed"
1784 );
1785 assert!(
1786 updated_tx.status_reason.is_some(),
1787 "Status reason should be set"
1788 );
1789 assert!(
1790 updated_tx
1791 .status_reason
1792 .as_ref()
1793 .unwrap()
1794 .to_lowercase()
1795 .contains("insufficient balance"),
1796 "Status reason should contain insufficient balance error, got: {:?}",
1797 updated_tx.status_reason
1798 );
1799 }
1800
1801 #[tokio::test]
1802 async fn test_prepare_transaction_with_gas_limit_exceeding_block_limit() {
1803 let mut mock_transaction = MockTransactionRepository::new();
1804 let mock_relayer = MockRelayerRepository::new();
1805 let mut mock_provider = MockEvmProviderTrait::new();
1806 let mock_signer = MockSigner::new();
1807 let mut mock_job_producer = MockJobProducerTrait::new();
1808 let mock_price_calculator = MockPriceCalculator::new();
1809 let mut counter_service = MockTransactionCounterTrait::new();
1810
1811 let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
1812 gas_limit_estimation: Some(false), min_balance: Some(100000000000000000u128),
1814 ..Default::default()
1815 });
1816
1817 let mut test_tx = create_test_transaction();
1819 if let NetworkTransactionData::Evm(ref mut evm_data) = test_tx.network_data {
1820 evm_data.gas_limit = Some(30_000_001); }
1822
1823 counter_service
1824 .expect_get_and_increment()
1825 .returning(|_, _| Box::pin(ready(Ok(42))));
1826
1827 mock_provider
1829 .expect_get_block_by_number()
1830 .times(1)
1831 .returning(|| {
1832 Box::pin(async {
1833 use alloy::{network::AnyRpcBlock, rpc::types::Block};
1834 let mut block: Block = Block::default();
1835 block.header.gas_limit = 30_000_000u64;
1837 Ok(AnyRpcBlock::from(block))
1838 })
1839 });
1840
1841 let test_tx_clone = test_tx.clone();
1843 mock_transaction
1844 .expect_partial_update()
1845 .withf(move |id, update| {
1846 id == "test-tx-id"
1847 && update.status == Some(TransactionStatus::Failed)
1848 && update.status_reason.is_some()
1849 && update
1850 .status_reason
1851 .as_ref()
1852 .unwrap()
1853 .contains("exceeds block gas limit")
1854 })
1855 .returning(move |_, update| {
1856 let mut updated_tx = test_tx_clone.clone();
1857 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1858 updated_tx.status_reason = update.status_reason.clone();
1859 Ok(updated_tx)
1860 });
1861
1862 mock_job_producer
1863 .expect_produce_send_notification_job()
1864 .returning(|_, _| Box::pin(ready(Ok(()))));
1865
1866 let mock_network = MockNetworkRepository::new();
1867
1868 let evm_transaction = EvmRelayerTransaction {
1869 relayer: relayer.clone(),
1870 provider: mock_provider,
1871 relayer_repository: Arc::new(mock_relayer),
1872 network_repository: Arc::new(mock_network),
1873 transaction_repository: Arc::new(mock_transaction),
1874 transaction_counter_service: Arc::new(counter_service),
1875 job_producer: Arc::new(mock_job_producer),
1876 price_calculator: mock_price_calculator,
1877 signer: mock_signer,
1878 };
1879
1880 let result = evm_transaction.prepare_transaction(test_tx.clone()).await;
1881 assert!(result.is_ok(), "Expected Ok, got: {result:?}");
1882
1883 let updated_tx = result.unwrap();
1884 assert_eq!(
1885 updated_tx.status,
1886 TransactionStatus::Failed,
1887 "Transaction should be marked as Failed"
1888 );
1889 assert!(
1890 updated_tx.status_reason.is_some(),
1891 "Status reason should be set"
1892 );
1893 assert!(
1894 updated_tx
1895 .status_reason
1896 .as_ref()
1897 .unwrap()
1898 .contains("exceeds block gas limit"),
1899 "Status reason should mention gas limit exceeds block gas limit, got: {:?}",
1900 updated_tx.status_reason
1901 );
1902 assert!(
1903 updated_tx
1904 .status_reason
1905 .as_ref()
1906 .unwrap()
1907 .contains("30000001"),
1908 "Status reason should contain transaction gas limit, got: {:?}",
1909 updated_tx.status_reason
1910 );
1911 assert!(
1912 updated_tx
1913 .status_reason
1914 .as_ref()
1915 .unwrap()
1916 .contains("30000000"),
1917 "Status reason should contain block gas limit, got: {:?}",
1918 updated_tx.status_reason
1919 );
1920 }
1921
1922 #[tokio::test]
1923 async fn test_prepare_transaction_with_gas_limit_within_block_limit() {
1924 let mut mock_transaction = MockTransactionRepository::new();
1925 let mock_relayer = MockRelayerRepository::new();
1926 let mut mock_provider = MockEvmProviderTrait::new();
1927 let mut mock_signer = MockSigner::new();
1928 let mut mock_job_producer = MockJobProducerTrait::new();
1929 let mut mock_price_calculator = MockPriceCalculator::new();
1930 let mut counter_service = MockTransactionCounterTrait::new();
1931
1932 let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
1933 gas_limit_estimation: Some(false), min_balance: Some(100000000000000000u128),
1935 ..Default::default()
1936 });
1937
1938 let mut test_tx = create_test_transaction();
1940 if let NetworkTransactionData::Evm(ref mut evm_data) = test_tx.network_data {
1941 evm_data.gas_limit = Some(21_000); }
1943
1944 counter_service
1945 .expect_get_and_increment()
1946 .returning(|_, _| Box::pin(ready(Ok(42))));
1947
1948 let price_params = PriceParams {
1949 gas_price: Some(30000000000),
1950 max_fee_per_gas: None,
1951 max_priority_fee_per_gas: None,
1952 is_min_bumped: None,
1953 extra_fee: None,
1954 total_cost: U256::from(630000000000000u64),
1955 };
1956 mock_price_calculator
1957 .expect_get_transaction_price_params()
1958 .returning(move |_, _| Ok(price_params.clone()));
1959
1960 mock_signer.expect_sign_transaction().returning(|_| {
1961 Box::pin(ready(Ok(
1962 crate::domain::relayer::SignTransactionResponse::Evm(
1963 crate::domain::relayer::SignTransactionResponseEvm {
1964 hash: "0xtx_hash".to_string(),
1965 signature: crate::models::EvmTransactionDataSignature {
1966 r: "r".to_string(),
1967 s: "s".to_string(),
1968 v: 1,
1969 sig: "0xsignature".to_string(),
1970 },
1971 raw: vec![1, 2, 3],
1972 },
1973 ),
1974 )))
1975 });
1976
1977 mock_provider
1978 .expect_get_balance()
1979 .with(eq("0xSender"))
1980 .returning(|_| Box::pin(ready(Ok(U256::from(1000000000000000000u64)))));
1981
1982 mock_provider
1984 .expect_get_block_by_number()
1985 .times(1)
1986 .returning(|| {
1987 Box::pin(async {
1988 use alloy::{network::AnyRpcBlock, rpc::types::Block};
1989 let mut block: Block = Block::default();
1990 block.header.gas_limit = 30_000_000u64;
1992 Ok(AnyRpcBlock::from(block))
1993 })
1994 });
1995
1996 let test_tx_clone = test_tx.clone();
1997 mock_transaction
1998 .expect_partial_update()
1999 .returning(move |_, update| {
2000 let mut updated_tx = test_tx_clone.clone();
2001 if let Some(status) = &update.status {
2002 updated_tx.status = status.clone();
2003 }
2004 if let Some(network_data) = &update.network_data {
2005 updated_tx.network_data = network_data.clone();
2006 }
2007 if let Some(hashes) = &update.hashes {
2008 updated_tx.hashes = hashes.clone();
2009 }
2010 Ok(updated_tx)
2011 });
2012
2013 mock_job_producer
2014 .expect_produce_submit_transaction_job()
2015 .returning(|_, _| Box::pin(ready(Ok(()))));
2016 mock_job_producer
2017 .expect_produce_send_notification_job()
2018 .returning(|_, _| Box::pin(ready(Ok(()))));
2019
2020 let mock_network = MockNetworkRepository::new();
2021
2022 let evm_transaction = EvmRelayerTransaction {
2023 relayer: relayer.clone(),
2024 provider: mock_provider,
2025 relayer_repository: Arc::new(mock_relayer),
2026 network_repository: Arc::new(mock_network),
2027 transaction_repository: Arc::new(mock_transaction),
2028 transaction_counter_service: Arc::new(counter_service),
2029 job_producer: Arc::new(mock_job_producer),
2030 price_calculator: mock_price_calculator,
2031 signer: mock_signer,
2032 };
2033
2034 let result = evm_transaction.prepare_transaction(test_tx.clone()).await;
2035 assert!(result.is_ok(), "Expected Ok, got: {result:?}");
2036
2037 let prepared_tx = result.unwrap();
2038 assert_eq!(prepared_tx.status, TransactionStatus::Sent);
2040 assert!(!prepared_tx.hashes.is_empty());
2041 }
2042
2043 #[tokio::test]
2044 async fn test_cancel_transaction() {
2045 {
2047 let mut mock_transaction = MockTransactionRepository::new();
2049 let mock_relayer = MockRelayerRepository::new();
2050 let mock_provider = MockEvmProviderTrait::new();
2051 let mock_signer = MockSigner::new();
2052 let mut mock_job_producer = MockJobProducerTrait::new();
2053 let mock_price_calculator = MockPriceCalculator::new();
2054 let counter_service = MockTransactionCounterTrait::new();
2055
2056 let relayer = create_test_relayer();
2058 let mut test_tx = create_test_transaction();
2059 test_tx.status = TransactionStatus::Pending;
2060
2061 let test_tx_clone = test_tx.clone();
2063 mock_transaction
2064 .expect_partial_update()
2065 .withf(move |id, update| {
2066 id == "test-tx-id" && update.status == Some(TransactionStatus::Canceled)
2067 })
2068 .returning(move |_, update| {
2069 let mut updated_tx = test_tx_clone.clone();
2070 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2071 Ok(updated_tx)
2072 });
2073
2074 mock_job_producer
2076 .expect_produce_send_notification_job()
2077 .returning(|_, _| Box::pin(ready(Ok(()))));
2078
2079 let mock_network = MockNetworkRepository::new();
2080
2081 let evm_transaction = EvmRelayerTransaction {
2083 relayer: relayer.clone(),
2084 provider: mock_provider,
2085 relayer_repository: Arc::new(mock_relayer),
2086 network_repository: Arc::new(mock_network),
2087 transaction_repository: Arc::new(mock_transaction),
2088 transaction_counter_service: Arc::new(counter_service),
2089 job_producer: Arc::new(mock_job_producer),
2090 price_calculator: mock_price_calculator,
2091 signer: mock_signer,
2092 };
2093
2094 let result = evm_transaction.cancel_transaction(test_tx.clone()).await;
2096 assert!(result.is_ok());
2097 let cancelled_tx = result.unwrap();
2098 assert_eq!(cancelled_tx.id, "test-tx-id");
2099 assert_eq!(cancelled_tx.status, TransactionStatus::Canceled);
2100 }
2101
2102 {
2104 let mut mock_transaction = MockTransactionRepository::new();
2106 let mock_relayer = MockRelayerRepository::new();
2107 let mock_provider = MockEvmProviderTrait::new();
2108 let mut mock_signer = MockSigner::new();
2109 let mut mock_job_producer = MockJobProducerTrait::new();
2110 let mut mock_price_calculator = MockPriceCalculator::new();
2111 let counter_service = MockTransactionCounterTrait::new();
2112
2113 let relayer = create_test_relayer();
2115 let mut test_tx = create_test_transaction();
2116 test_tx.status = TransactionStatus::Submitted;
2117 test_tx.sent_at = Some(Utc::now().to_rfc3339());
2118 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
2119 nonce: Some(42),
2120 hash: Some("0xoriginal_hash".to_string()),
2121 ..test_tx.network_data.get_evm_transaction_data().unwrap()
2122 });
2123
2124 mock_price_calculator
2126 .expect_get_transaction_price_params()
2127 .return_once(move |_, _| {
2128 Ok(PriceParams {
2129 gas_price: Some(40000000000), max_fee_per_gas: None,
2131 max_priority_fee_per_gas: None,
2132 is_min_bumped: Some(true),
2133 extra_fee: Some(U256::ZERO),
2134 total_cost: U256::ZERO,
2135 })
2136 });
2137
2138 mock_signer.expect_sign_transaction().returning(|_| {
2140 Box::pin(ready(Ok(
2141 crate::domain::relayer::SignTransactionResponse::Evm(
2142 crate::domain::relayer::SignTransactionResponseEvm {
2143 hash: "0xcancellation_hash".to_string(),
2144 signature: crate::models::EvmTransactionDataSignature {
2145 r: "r".to_string(),
2146 s: "s".to_string(),
2147 v: 1,
2148 sig: "0xsignature".to_string(),
2149 },
2150 raw: vec![1, 2, 3],
2151 },
2152 ),
2153 )))
2154 });
2155
2156 let test_tx_clone = test_tx.clone();
2158 mock_transaction
2159 .expect_partial_update()
2160 .returning(move |tx_id, update| {
2161 let mut updated_tx = test_tx_clone.clone();
2162 updated_tx.id = tx_id;
2163 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2164 updated_tx.network_data =
2165 update.network_data.unwrap_or(updated_tx.network_data);
2166 if let Some(hashes) = update.hashes {
2167 updated_tx.hashes = hashes;
2168 }
2169 Ok(updated_tx)
2170 });
2171
2172 mock_job_producer
2174 .expect_produce_submit_transaction_job()
2175 .returning(|_, _| Box::pin(ready(Ok(()))));
2176 mock_job_producer
2177 .expect_produce_send_notification_job()
2178 .returning(|_, _| Box::pin(ready(Ok(()))));
2179
2180 let mut mock_network = MockNetworkRepository::new();
2182 mock_network
2183 .expect_get_by_chain_id()
2184 .with(eq(NetworkType::Evm), eq(1))
2185 .returning(|_, _| {
2186 use crate::config::{EvmNetworkConfig, NetworkConfigCommon};
2187 use crate::models::{NetworkConfigData, NetworkRepoModel, RpcConfig};
2188
2189 let config = EvmNetworkConfig {
2190 common: NetworkConfigCommon {
2191 network: "mainnet".to_string(),
2192 from: None,
2193 rpc_urls: Some(vec![RpcConfig::new(
2194 "https://rpc.example.com".to_string(),
2195 )]),
2196 explorer_urls: None,
2197 average_blocktime_ms: Some(12000),
2198 is_testnet: Some(false),
2199 tags: Some(vec!["mainnet".to_string()]),
2200 },
2201 chain_id: Some(1),
2202 required_confirmations: Some(12),
2203 features: Some(vec!["eip1559".to_string()]),
2204 symbol: Some("ETH".to_string()),
2205 gas_price_cache: None,
2206 };
2207 Ok(Some(NetworkRepoModel {
2208 id: "evm:mainnet".to_string(),
2209 name: "mainnet".to_string(),
2210 network_type: NetworkType::Evm,
2211 config: NetworkConfigData::Evm(config),
2212 }))
2213 });
2214
2215 let evm_transaction = EvmRelayerTransaction {
2217 relayer: relayer.clone(),
2218 provider: mock_provider,
2219 relayer_repository: Arc::new(mock_relayer),
2220 network_repository: Arc::new(mock_network),
2221 transaction_repository: Arc::new(mock_transaction),
2222 transaction_counter_service: Arc::new(counter_service),
2223 job_producer: Arc::new(mock_job_producer),
2224 price_calculator: mock_price_calculator,
2225 signer: mock_signer,
2226 };
2227
2228 let result = evm_transaction.cancel_transaction(test_tx.clone()).await;
2230 assert!(result.is_ok());
2231 let cancelled_tx = result.unwrap();
2232
2233 assert_eq!(cancelled_tx.id, "test-tx-id");
2235 assert_eq!(cancelled_tx.status, TransactionStatus::Submitted);
2236
2237 if let NetworkTransactionData::Evm(evm_data) = &cancelled_tx.network_data {
2239 assert_eq!(evm_data.nonce, Some(42)); } else {
2241 panic!("Expected EVM transaction data");
2242 }
2243 }
2244
2245 {
2247 let mock_transaction = MockTransactionRepository::new();
2249 let mock_relayer = MockRelayerRepository::new();
2250 let mock_provider = MockEvmProviderTrait::new();
2251 let mock_signer = MockSigner::new();
2252 let mock_job_producer = MockJobProducerTrait::new();
2253 let mock_price_calculator = MockPriceCalculator::new();
2254 let counter_service = MockTransactionCounterTrait::new();
2255
2256 let relayer = create_test_relayer();
2258 let mut test_tx = create_test_transaction();
2259 test_tx.status = TransactionStatus::Confirmed;
2260
2261 let mock_network = MockNetworkRepository::new();
2262
2263 let evm_transaction = EvmRelayerTransaction {
2265 relayer: relayer.clone(),
2266 provider: mock_provider,
2267 relayer_repository: Arc::new(mock_relayer),
2268 network_repository: Arc::new(mock_network),
2269 transaction_repository: Arc::new(mock_transaction),
2270 transaction_counter_service: Arc::new(counter_service),
2271 job_producer: Arc::new(mock_job_producer),
2272 price_calculator: mock_price_calculator,
2273 signer: mock_signer,
2274 };
2275
2276 let result = evm_transaction.cancel_transaction(test_tx.clone()).await;
2278 assert!(result.is_err());
2279 if let Err(TransactionError::ValidationError(msg)) = result {
2280 assert!(msg.contains("Invalid transaction state for cancel_transaction"));
2281 } else {
2282 panic!("Expected ValidationError");
2283 }
2284 }
2285 }
2286
2287 #[tokio::test]
2288 async fn test_replace_transaction() {
2289 {
2291 let mut mock_transaction = MockTransactionRepository::new();
2293 let mock_relayer = MockRelayerRepository::new();
2294 let mut mock_provider = MockEvmProviderTrait::new();
2295 let mut mock_signer = MockSigner::new();
2296 let mut mock_job_producer = MockJobProducerTrait::new();
2297 let mut mock_price_calculator = MockPriceCalculator::new();
2298 let counter_service = MockTransactionCounterTrait::new();
2299
2300 let relayer = create_test_relayer();
2302 let mut test_tx = create_test_transaction();
2303 test_tx.status = TransactionStatus::Submitted;
2304 test_tx.sent_at = Some(Utc::now().to_rfc3339());
2305
2306 mock_price_calculator
2308 .expect_get_transaction_price_params()
2309 .return_once(move |_, _| {
2310 Ok(PriceParams {
2311 gas_price: Some(40000000000), max_fee_per_gas: None,
2313 max_priority_fee_per_gas: None,
2314 is_min_bumped: Some(true),
2315 extra_fee: Some(U256::ZERO),
2316 total_cost: U256::from(2001000000000000000u64), })
2318 });
2319
2320 mock_signer.expect_sign_transaction().returning(|_| {
2322 Box::pin(ready(Ok(
2323 crate::domain::relayer::SignTransactionResponse::Evm(
2324 crate::domain::relayer::SignTransactionResponseEvm {
2325 hash: "0xreplacement_hash".to_string(),
2326 signature: crate::models::EvmTransactionDataSignature {
2327 r: "r".to_string(),
2328 s: "s".to_string(),
2329 v: 1,
2330 sig: "0xsignature".to_string(),
2331 },
2332 raw: vec![1, 2, 3],
2333 },
2334 ),
2335 )))
2336 });
2337
2338 mock_provider
2340 .expect_get_balance()
2341 .with(eq("0xSender"))
2342 .returning(|_| Box::pin(ready(Ok(U256::from(3000000000000000000u64)))));
2343
2344 let test_tx_clone = test_tx.clone();
2346 mock_transaction
2347 .expect_update_network_data()
2348 .returning(move |tx_id, network_data| {
2349 let mut updated_tx = test_tx_clone.clone();
2350 updated_tx.id = tx_id;
2351 updated_tx.network_data = network_data;
2352 Ok(updated_tx)
2353 });
2354
2355 mock_job_producer
2357 .expect_produce_submit_transaction_job()
2358 .returning(|_, _| Box::pin(ready(Ok(()))));
2359 mock_job_producer
2360 .expect_produce_send_notification_job()
2361 .returning(|_, _| Box::pin(ready(Ok(()))));
2362
2363 let mut mock_network = MockNetworkRepository::new();
2365 mock_network
2366 .expect_get_by_chain_id()
2367 .with(eq(NetworkType::Evm), eq(1))
2368 .returning(|_, _| {
2369 use crate::config::{EvmNetworkConfig, NetworkConfigCommon};
2370 use crate::models::{NetworkConfigData, NetworkRepoModel};
2371
2372 let config = EvmNetworkConfig {
2373 common: NetworkConfigCommon {
2374 network: "mainnet".to_string(),
2375 from: None,
2376 rpc_urls: Some(vec![crate::models::RpcConfig::new(
2377 "https://rpc.example.com".to_string(),
2378 )]),
2379 explorer_urls: None,
2380 average_blocktime_ms: Some(12000),
2381 is_testnet: Some(false),
2382 tags: Some(vec!["mainnet".to_string()]), },
2384 chain_id: Some(1),
2385 required_confirmations: Some(12),
2386 features: Some(vec!["eip1559".to_string()]),
2387 symbol: Some("ETH".to_string()),
2388 gas_price_cache: None,
2389 };
2390 Ok(Some(NetworkRepoModel {
2391 id: "evm:mainnet".to_string(),
2392 name: "mainnet".to_string(),
2393 network_type: NetworkType::Evm,
2394 config: NetworkConfigData::Evm(config),
2395 }))
2396 });
2397
2398 let evm_transaction = EvmRelayerTransaction {
2400 relayer: relayer.clone(),
2401 provider: mock_provider,
2402 relayer_repository: Arc::new(mock_relayer),
2403 network_repository: Arc::new(mock_network),
2404 transaction_repository: Arc::new(mock_transaction),
2405 transaction_counter_service: Arc::new(counter_service),
2406 job_producer: Arc::new(mock_job_producer),
2407 price_calculator: mock_price_calculator,
2408 signer: mock_signer,
2409 };
2410
2411 let replacement_request = NetworkTransactionRequest::Evm(EvmTransactionRequest {
2413 to: Some("0xNewRecipient".to_string()),
2414 value: U256::from(2000000000000000000u64), data: Some("0xNewData".to_string()),
2416 gas_limit: Some(25000),
2417 gas_price: None, max_fee_per_gas: None,
2419 max_priority_fee_per_gas: None,
2420 speed: Some(Speed::Fast),
2421 valid_until: None,
2422 });
2423
2424 let result = evm_transaction
2426 .replace_transaction(test_tx.clone(), replacement_request)
2427 .await;
2428 if let Err(ref e) = result {
2429 eprintln!("Replace transaction failed with error: {e:?}");
2430 }
2431 assert!(result.is_ok());
2432 let replaced_tx = result.unwrap();
2433
2434 assert_eq!(replaced_tx.id, "test-tx-id");
2436
2437 if let NetworkTransactionData::Evm(evm_data) = &replaced_tx.network_data {
2439 assert_eq!(evm_data.to, Some("0xNewRecipient".to_string()));
2440 assert_eq!(evm_data.value, U256::from(2000000000000000000u64));
2441 assert_eq!(evm_data.gas_price, Some(40000000000));
2442 assert_eq!(evm_data.gas_limit, Some(25000));
2443 assert!(evm_data.hash.is_some());
2444 assert!(evm_data.raw.is_some());
2445 } else {
2446 panic!("Expected EVM transaction data");
2447 }
2448 }
2449
2450 {
2452 let mock_transaction = MockTransactionRepository::new();
2454 let mock_relayer = MockRelayerRepository::new();
2455 let mock_provider = MockEvmProviderTrait::new();
2456 let mock_signer = MockSigner::new();
2457 let mock_job_producer = MockJobProducerTrait::new();
2458 let mock_price_calculator = MockPriceCalculator::new();
2459 let counter_service = MockTransactionCounterTrait::new();
2460
2461 let relayer = create_test_relayer();
2463 let mut test_tx = create_test_transaction();
2464 test_tx.status = TransactionStatus::Confirmed;
2465
2466 let mock_network = MockNetworkRepository::new();
2467
2468 let evm_transaction = EvmRelayerTransaction {
2470 relayer: relayer.clone(),
2471 provider: mock_provider,
2472 relayer_repository: Arc::new(mock_relayer),
2473 network_repository: Arc::new(mock_network),
2474 transaction_repository: Arc::new(mock_transaction),
2475 transaction_counter_service: Arc::new(counter_service),
2476 job_producer: Arc::new(mock_job_producer),
2477 price_calculator: mock_price_calculator,
2478 signer: mock_signer,
2479 };
2480
2481 let replacement_request = NetworkTransactionRequest::Evm(EvmTransactionRequest {
2483 to: Some("0xNewRecipient".to_string()),
2484 value: U256::from(1000000000000000000u64),
2485 data: Some("0xData".to_string()),
2486 gas_limit: Some(21000),
2487 gas_price: Some(30000000000),
2488 max_fee_per_gas: None,
2489 max_priority_fee_per_gas: None,
2490 speed: Some(Speed::Fast),
2491 valid_until: None,
2492 });
2493
2494 let result = evm_transaction
2496 .replace_transaction(test_tx.clone(), replacement_request)
2497 .await;
2498 assert!(result.is_err());
2499 if let Err(TransactionError::ValidationError(msg)) = result {
2500 assert!(msg.contains("Invalid transaction state for replace_transaction"));
2501 } else {
2502 panic!("Expected ValidationError");
2503 }
2504 }
2505 }
2506
2507 #[tokio::test]
2508 async fn test_estimate_tx_gas_limit_success() {
2509 let mock_transaction = MockTransactionRepository::new();
2510 let mock_relayer = MockRelayerRepository::new();
2511 let mut mock_provider = MockEvmProviderTrait::new();
2512 let mock_signer = MockSigner::new();
2513 let mock_job_producer = MockJobProducerTrait::new();
2514 let mock_price_calculator = MockPriceCalculator::new();
2515 let counter_service = MockTransactionCounterTrait::new();
2516 let mock_network = MockNetworkRepository::new();
2517
2518 let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2520 gas_limit_estimation: Some(true),
2521 ..Default::default()
2522 });
2523 let evm_data = EvmTransactionData {
2524 from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
2525 to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
2526 value: U256::from(1000000000000000000u128),
2527 data: Some("0x".to_string()),
2528 gas_limit: None,
2529 gas_price: Some(20_000_000_000),
2530 nonce: Some(1),
2531 chain_id: 1,
2532 hash: None,
2533 signature: None,
2534 speed: Some(Speed::Average),
2535 max_fee_per_gas: None,
2536 max_priority_fee_per_gas: None,
2537 raw: None,
2538 };
2539
2540 mock_provider
2542 .expect_estimate_gas()
2543 .times(1)
2544 .returning(|_| Box::pin(async { Ok(21000) }));
2545
2546 let transaction = EvmRelayerTransaction::new(
2547 relayer.clone(),
2548 mock_provider,
2549 Arc::new(mock_relayer),
2550 Arc::new(mock_network),
2551 Arc::new(mock_transaction),
2552 Arc::new(counter_service),
2553 Arc::new(mock_job_producer),
2554 mock_price_calculator,
2555 mock_signer,
2556 )
2557 .unwrap();
2558
2559 let result = transaction
2560 .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
2561 .await;
2562
2563 assert!(result.is_ok());
2564 assert_eq!(result.unwrap(), 23100);
2566 }
2567
2568 #[tokio::test]
2569 async fn test_estimate_tx_gas_limit_disabled() {
2570 let mock_transaction = MockTransactionRepository::new();
2571 let mock_relayer = MockRelayerRepository::new();
2572 let mut mock_provider = MockEvmProviderTrait::new();
2573 let mock_signer = MockSigner::new();
2574 let mock_job_producer = MockJobProducerTrait::new();
2575 let mock_price_calculator = MockPriceCalculator::new();
2576 let counter_service = MockTransactionCounterTrait::new();
2577 let mock_network = MockNetworkRepository::new();
2578
2579 let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2581 gas_limit_estimation: Some(false),
2582 ..Default::default()
2583 });
2584
2585 let evm_data = EvmTransactionData {
2586 from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
2587 to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
2588 value: U256::from(1000000000000000000u128),
2589 data: Some("0x".to_string()),
2590 gas_limit: None,
2591 gas_price: Some(20_000_000_000),
2592 nonce: Some(1),
2593 chain_id: 1,
2594 hash: None,
2595 signature: None,
2596 speed: Some(Speed::Average),
2597 max_fee_per_gas: None,
2598 max_priority_fee_per_gas: None,
2599 raw: None,
2600 };
2601
2602 mock_provider.expect_estimate_gas().times(0);
2604
2605 let transaction = EvmRelayerTransaction::new(
2606 relayer.clone(),
2607 mock_provider,
2608 Arc::new(mock_relayer),
2609 Arc::new(mock_network),
2610 Arc::new(mock_transaction),
2611 Arc::new(counter_service),
2612 Arc::new(mock_job_producer),
2613 mock_price_calculator,
2614 mock_signer,
2615 )
2616 .unwrap();
2617
2618 let result = transaction
2619 .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
2620 .await;
2621
2622 assert!(result.is_err());
2623 assert!(matches!(
2624 result.unwrap_err(),
2625 TransactionError::UnexpectedError(_)
2626 ));
2627 }
2628
2629 #[tokio::test]
2630 async fn test_estimate_tx_gas_limit_default_enabled() {
2631 let mock_transaction = MockTransactionRepository::new();
2632 let mock_relayer = MockRelayerRepository::new();
2633 let mut mock_provider = MockEvmProviderTrait::new();
2634 let mock_signer = MockSigner::new();
2635 let mock_job_producer = MockJobProducerTrait::new();
2636 let mock_price_calculator = MockPriceCalculator::new();
2637 let counter_service = MockTransactionCounterTrait::new();
2638 let mock_network = MockNetworkRepository::new();
2639
2640 let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2641 gas_limit_estimation: None, ..Default::default()
2643 });
2644
2645 let evm_data = EvmTransactionData {
2646 from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
2647 to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
2648 value: U256::from(1000000000000000000u128),
2649 data: Some("0x".to_string()),
2650 gas_limit: None,
2651 gas_price: Some(20_000_000_000),
2652 nonce: Some(1),
2653 chain_id: 1,
2654 hash: None,
2655 signature: None,
2656 speed: Some(Speed::Average),
2657 max_fee_per_gas: None,
2658 max_priority_fee_per_gas: None,
2659 raw: None,
2660 };
2661
2662 mock_provider
2664 .expect_estimate_gas()
2665 .times(1)
2666 .returning(|_| Box::pin(async { Ok(50000) }));
2667
2668 let transaction = EvmRelayerTransaction::new(
2669 relayer.clone(),
2670 mock_provider,
2671 Arc::new(mock_relayer),
2672 Arc::new(mock_network),
2673 Arc::new(mock_transaction),
2674 Arc::new(counter_service),
2675 Arc::new(mock_job_producer),
2676 mock_price_calculator,
2677 mock_signer,
2678 )
2679 .unwrap();
2680
2681 let result = transaction
2682 .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
2683 .await;
2684
2685 assert!(result.is_ok());
2686 assert_eq!(result.unwrap(), 55000);
2688 }
2689
2690 #[tokio::test]
2691 async fn test_estimate_tx_gas_limit_provider_error() {
2692 let mock_transaction = MockTransactionRepository::new();
2693 let mock_relayer = MockRelayerRepository::new();
2694 let mut mock_provider = MockEvmProviderTrait::new();
2695 let mock_signer = MockSigner::new();
2696 let mock_job_producer = MockJobProducerTrait::new();
2697 let mock_price_calculator = MockPriceCalculator::new();
2698 let counter_service = MockTransactionCounterTrait::new();
2699 let mock_network = MockNetworkRepository::new();
2700
2701 let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2702 gas_limit_estimation: Some(true),
2703 ..Default::default()
2704 });
2705
2706 let evm_data = EvmTransactionData {
2707 from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
2708 to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
2709 value: U256::from(1000000000000000000u128),
2710 data: Some("0x".to_string()),
2711 gas_limit: None,
2712 gas_price: Some(20_000_000_000),
2713 nonce: Some(1),
2714 chain_id: 1,
2715 hash: None,
2716 signature: None,
2717 speed: Some(Speed::Average),
2718 max_fee_per_gas: None,
2719 max_priority_fee_per_gas: None,
2720 raw: None,
2721 };
2722
2723 mock_provider.expect_estimate_gas().times(1).returning(|_| {
2725 Box::pin(async {
2726 Err(crate::services::provider::ProviderError::Other(
2727 "RPC error".to_string(),
2728 ))
2729 })
2730 });
2731
2732 let transaction = EvmRelayerTransaction::new(
2733 relayer.clone(),
2734 mock_provider,
2735 Arc::new(mock_relayer),
2736 Arc::new(mock_network),
2737 Arc::new(mock_transaction),
2738 Arc::new(counter_service),
2739 Arc::new(mock_job_producer),
2740 mock_price_calculator,
2741 mock_signer,
2742 )
2743 .unwrap();
2744
2745 let result = transaction
2746 .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
2747 .await;
2748
2749 assert!(result.is_err());
2750 assert!(matches!(
2751 result.unwrap_err(),
2752 TransactionError::UnexpectedError(_)
2753 ));
2754 }
2755
2756 #[tokio::test]
2757 async fn test_prepare_transaction_uses_gas_estimation_and_stores_result() {
2758 let mut mock_transaction = MockTransactionRepository::new();
2759 let mock_relayer = MockRelayerRepository::new();
2760 let mut mock_provider = MockEvmProviderTrait::new();
2761 let mut mock_signer = MockSigner::new();
2762 let mut mock_job_producer = MockJobProducerTrait::new();
2763 let mut mock_price_calculator = MockPriceCalculator::new();
2764 let mut counter_service = MockTransactionCounterTrait::new();
2765 let mock_network = MockNetworkRepository::new();
2766
2767 let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2769 gas_limit_estimation: Some(true),
2770 min_balance: Some(100000000000000000u128),
2771 ..Default::default()
2772 });
2773
2774 let mut test_tx = create_test_transaction();
2776 if let NetworkTransactionData::Evm(ref mut evm_data) = test_tx.network_data {
2777 evm_data.gas_limit = None; evm_data.nonce = None; }
2780
2781 const PROVIDER_GAS_ESTIMATE: u64 = 45000;
2783 const EXPECTED_GAS_WITH_BUFFER: u64 = 49500; mock_provider
2787 .expect_estimate_gas()
2788 .times(1)
2789 .returning(move |_| Box::pin(async move { Ok(PROVIDER_GAS_ESTIMATE) }));
2790
2791 mock_provider
2793 .expect_get_balance()
2794 .times(1)
2795 .returning(|_| Box::pin(async { Ok(U256::from(2000000000000000000u128)) })); let price_params = PriceParams {
2798 gas_price: Some(20_000_000_000), max_fee_per_gas: None,
2800 max_priority_fee_per_gas: None,
2801 is_min_bumped: None,
2802 extra_fee: None,
2803 total_cost: U256::from(1900000000000000000u128), };
2805
2806 mock_price_calculator
2808 .expect_get_transaction_price_params()
2809 .returning(move |_, _| Ok(price_params.clone()));
2810
2811 counter_service
2813 .expect_get_and_increment()
2814 .times(1)
2815 .returning(|_, _| Box::pin(async { Ok(42) }));
2816
2817 mock_signer.expect_sign_transaction().returning(|_| {
2819 Box::pin(ready(Ok(
2820 crate::domain::relayer::SignTransactionResponse::Evm(
2821 crate::domain::relayer::SignTransactionResponseEvm {
2822 hash: "0xhash".to_string(),
2823 signature: crate::models::EvmTransactionDataSignature {
2824 r: "r".to_string(),
2825 s: "s".to_string(),
2826 v: 1,
2827 sig: "0xsignature".to_string(),
2828 },
2829 raw: vec![1, 2, 3],
2830 },
2831 ),
2832 )))
2833 });
2834
2835 mock_job_producer
2837 .expect_produce_submit_transaction_job()
2838 .returning(|_, _| Box::pin(async { Ok(()) }));
2839
2840 mock_job_producer
2841 .expect_produce_send_notification_job()
2842 .returning(|_, _| Box::pin(ready(Ok(()))));
2843
2844 let expected_gas_limit = EXPECTED_GAS_WITH_BUFFER;
2849
2850 let test_tx_clone = test_tx.clone();
2851 mock_transaction
2852 .expect_partial_update()
2853 .times(2)
2854 .returning(move |_, update| {
2855 let mut updated_tx = test_tx_clone.clone();
2856
2857 if let Some(status) = &update.status {
2859 updated_tx.status = status.clone();
2860 }
2861 if let Some(network_data) = &update.network_data {
2862 updated_tx.network_data = network_data.clone();
2863 } else {
2864 if let NetworkTransactionData::Evm(ref mut evm_data) = updated_tx.network_data {
2866 if evm_data.gas_limit.is_none() {
2867 evm_data.gas_limit = Some(expected_gas_limit);
2868 }
2869 }
2870 }
2871 if let Some(hashes) = &update.hashes {
2872 updated_tx.hashes = hashes.clone();
2873 }
2874
2875 Ok(updated_tx)
2876 });
2877
2878 let transaction = EvmRelayerTransaction::new(
2879 relayer.clone(),
2880 mock_provider,
2881 Arc::new(mock_relayer),
2882 Arc::new(mock_network),
2883 Arc::new(mock_transaction),
2884 Arc::new(counter_service),
2885 Arc::new(mock_job_producer),
2886 mock_price_calculator,
2887 mock_signer,
2888 )
2889 .unwrap();
2890
2891 let result = transaction.prepare_transaction(test_tx).await;
2893
2894 assert!(result.is_ok(), "prepare_transaction should succeed");
2896 let prepared_tx = result.unwrap();
2897
2898 if let NetworkTransactionData::Evm(evm_data) = prepared_tx.network_data {
2900 assert_eq!(evm_data.gas_limit, Some(EXPECTED_GAS_WITH_BUFFER));
2901 } else {
2902 panic!("Expected EVM network data");
2903 }
2904 }
2905
2906 #[tokio::test]
2907 async fn test_prepare_transaction_estimates_gas_for_contract_creation() {
2908 let mut mock_transaction = MockTransactionRepository::new();
2909 let mock_relayer = MockRelayerRepository::new();
2910 let mut mock_provider = MockEvmProviderTrait::new();
2911 let mut mock_signer = MockSigner::new();
2912 let mut mock_job_producer = MockJobProducerTrait::new();
2913 let mut mock_price_calculator = MockPriceCalculator::new();
2914 let mut counter_service = MockTransactionCounterTrait::new();
2915 let mock_network = MockNetworkRepository::new();
2916
2917 let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2918 gas_limit_estimation: Some(true),
2919 min_balance: Some(100000000000000000u128),
2920 ..Default::default()
2921 });
2922
2923 let mut test_tx = create_test_transaction();
2924 if let NetworkTransactionData::Evm(ref mut evm_data) = test_tx.network_data {
2925 evm_data.to = None;
2926 evm_data.data = Some("0x6080604052348015600f57600080fd5b".to_string());
2927 evm_data.gas_limit = None;
2928 evm_data.nonce = None;
2929 }
2930
2931 const PROVIDER_GAS_ESTIMATE: u64 = 1500000;
2932 const EXPECTED_GAS_WITH_BUFFER: u64 = 1650000;
2933
2934 mock_provider
2935 .expect_estimate_gas()
2936 .withf(|tx| tx.to.is_none())
2937 .times(1)
2938 .returning(move |_| Box::pin(async move { Ok(PROVIDER_GAS_ESTIMATE) }));
2939
2940 mock_provider
2941 .expect_get_balance()
2942 .times(1)
2943 .returning(|_| Box::pin(async { Ok(U256::from(2000000000000000000u128)) }));
2944
2945 let price_params = PriceParams {
2946 gas_price: Some(20_000_000_000),
2947 max_fee_per_gas: None,
2948 max_priority_fee_per_gas: None,
2949 is_min_bumped: None,
2950 extra_fee: None,
2951 total_cost: U256::from(1900000000000000000u128),
2952 };
2953
2954 mock_price_calculator
2955 .expect_get_transaction_price_params()
2956 .returning(move |_, _| Ok(price_params.clone()));
2957
2958 counter_service
2959 .expect_get_and_increment()
2960 .times(1)
2961 .returning(|_, _| Box::pin(async { Ok(42) }));
2962
2963 mock_signer.expect_sign_transaction().returning(|_| {
2964 Box::pin(ready(Ok(
2965 crate::domain::relayer::SignTransactionResponse::Evm(
2966 crate::domain::relayer::SignTransactionResponseEvm {
2967 hash: "0xhash".to_string(),
2968 signature: crate::models::EvmTransactionDataSignature {
2969 r: "r".to_string(),
2970 s: "s".to_string(),
2971 v: 1,
2972 sig: "0xsignature".to_string(),
2973 },
2974 raw: vec![1, 2, 3],
2975 },
2976 ),
2977 )))
2978 });
2979
2980 mock_job_producer
2981 .expect_produce_submit_transaction_job()
2982 .returning(|_, _| Box::pin(async { Ok(()) }));
2983
2984 mock_job_producer
2985 .expect_produce_send_notification_job()
2986 .returning(|_, _| Box::pin(ready(Ok(()))));
2987
2988 let expected_gas_limit = EXPECTED_GAS_WITH_BUFFER;
2989 let test_tx_clone = test_tx.clone();
2990 mock_transaction
2991 .expect_partial_update()
2992 .times(2)
2993 .returning(move |_, update| {
2994 let mut updated_tx = test_tx_clone.clone();
2995
2996 if let Some(status) = &update.status {
2997 updated_tx.status = status.clone();
2998 }
2999 if let Some(network_data) = &update.network_data {
3000 updated_tx.network_data = network_data.clone();
3001 } else if let NetworkTransactionData::Evm(ref mut evm_data) =
3002 updated_tx.network_data
3003 {
3004 if evm_data.gas_limit.is_none() {
3005 evm_data.gas_limit = Some(expected_gas_limit);
3006 }
3007 }
3008 if let Some(hashes) = &update.hashes {
3009 updated_tx.hashes = hashes.clone();
3010 }
3011
3012 Ok(updated_tx)
3013 });
3014
3015 let transaction = EvmRelayerTransaction::new(
3016 relayer,
3017 mock_provider,
3018 Arc::new(mock_relayer),
3019 Arc::new(mock_network),
3020 Arc::new(mock_transaction),
3021 Arc::new(counter_service),
3022 Arc::new(mock_job_producer),
3023 mock_price_calculator,
3024 mock_signer,
3025 )
3026 .unwrap();
3027
3028 let result = transaction.prepare_transaction(test_tx).await;
3029
3030 assert!(result.is_ok(), "prepare_transaction should succeed");
3031 let prepared_tx = result.unwrap();
3032
3033 if let NetworkTransactionData::Evm(evm_data) = prepared_tx.network_data {
3034 assert_eq!(evm_data.to, None);
3035 assert_eq!(evm_data.gas_limit, Some(EXPECTED_GAS_WITH_BUFFER));
3036 } else {
3037 panic!("Expected EVM network data");
3038 }
3039 }
3040
3041 #[test]
3042 fn test_is_already_submitted_error_detection() {
3043 assert!(DefaultEvmTransaction::is_already_submitted_error(
3045 &"already known"
3046 ));
3047 assert!(DefaultEvmTransaction::is_already_submitted_error(
3048 &"Transaction already known"
3049 ));
3050 assert!(DefaultEvmTransaction::is_already_submitted_error(
3051 &"Error: already known"
3052 ));
3053
3054 assert!(DefaultEvmTransaction::is_already_submitted_error(
3056 &"nonce too low"
3057 ));
3058 assert!(DefaultEvmTransaction::is_already_submitted_error(
3059 &"Nonce Too Low"
3060 ));
3061 assert!(DefaultEvmTransaction::is_already_submitted_error(
3062 &"Error: nonce too low"
3063 ));
3064
3065 assert!(DefaultEvmTransaction::is_already_submitted_error(
3067 &"nonce is too low"
3068 ));
3069 assert!(DefaultEvmTransaction::is_already_submitted_error(
3070 &"Error: nonce is too low"
3071 ));
3072
3073 assert!(DefaultEvmTransaction::is_already_submitted_error(
3075 &"known transaction"
3076 ));
3077 assert!(DefaultEvmTransaction::is_already_submitted_error(
3078 &"Known Transaction"
3079 ));
3080
3081 assert!(DefaultEvmTransaction::is_already_submitted_error(
3083 &"replacement transaction underpriced"
3084 ));
3085 assert!(DefaultEvmTransaction::is_already_submitted_error(
3086 &"Replacement Transaction Underpriced"
3087 ));
3088
3089 assert!(DefaultEvmTransaction::is_already_submitted_error(
3091 &"same hash was already imported"
3092 ));
3093
3094 assert!(!DefaultEvmTransaction::is_already_submitted_error(
3096 &"insufficient funds"
3097 ));
3098 assert!(!DefaultEvmTransaction::is_already_submitted_error(
3099 &"execution reverted"
3100 ));
3101 assert!(!DefaultEvmTransaction::is_already_submitted_error(
3102 &"gas too low"
3103 ));
3104 assert!(!DefaultEvmTransaction::is_already_submitted_error(
3105 &"timeout"
3106 ));
3107 assert!(!DefaultEvmTransaction::is_already_submitted_error(
3109 &"Unknown transaction status"
3110 ));
3111 }
3112
3113 #[tokio::test]
3116 async fn test_submit_transaction_already_known_error_from_sent() {
3117 let mut mock_transaction = MockTransactionRepository::new();
3118 let mock_relayer = MockRelayerRepository::new();
3119 let mut mock_provider = MockEvmProviderTrait::new();
3120 let mock_signer = MockSigner::new();
3121 let mut mock_job_producer = MockJobProducerTrait::new();
3122 let mock_price_calculator = MockPriceCalculator::new();
3123 let counter_service = MockTransactionCounterTrait::new();
3124 let mock_network = MockNetworkRepository::new();
3125
3126 let relayer = create_test_relayer();
3127 let mut test_tx = create_test_transaction();
3128 test_tx.status = TransactionStatus::Sent;
3129 test_tx.sent_at = Some(Utc::now().to_rfc3339());
3130 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3131 nonce: Some(42),
3132 hash: Some("0xhash".to_string()),
3133 raw: Some(vec![1, 2, 3]),
3134 ..test_tx.network_data.get_evm_transaction_data().unwrap()
3135 });
3136
3137 mock_provider
3139 .expect_send_raw_transaction()
3140 .times(1)
3141 .returning(|_| {
3142 Box::pin(async {
3143 Err(crate::services::provider::ProviderError::Other(
3144 "already known: transaction already in mempool".to_string(),
3145 ))
3146 })
3147 });
3148
3149 let test_tx_clone = test_tx.clone();
3151 mock_transaction
3152 .expect_partial_update()
3153 .times(1)
3154 .withf(|_, update| update.status == Some(TransactionStatus::Submitted))
3155 .returning(move |_, update| {
3156 let mut updated_tx = test_tx_clone.clone();
3157 updated_tx.status = update.status.unwrap();
3158 updated_tx.sent_at = update.sent_at.clone();
3159 Ok(updated_tx)
3160 });
3161
3162 mock_job_producer
3163 .expect_produce_send_notification_job()
3164 .times(1)
3165 .returning(|_, _| Box::pin(ready(Ok(()))));
3166
3167 let evm_transaction = EvmRelayerTransaction {
3168 relayer: relayer.clone(),
3169 provider: mock_provider,
3170 relayer_repository: Arc::new(mock_relayer),
3171 network_repository: Arc::new(mock_network),
3172 transaction_repository: Arc::new(mock_transaction),
3173 transaction_counter_service: Arc::new(counter_service),
3174 job_producer: Arc::new(mock_job_producer),
3175 price_calculator: mock_price_calculator,
3176 signer: mock_signer,
3177 };
3178
3179 let result = evm_transaction.submit_transaction(test_tx).await;
3180 assert!(result.is_ok());
3181 let updated_tx = result.unwrap();
3182 assert_eq!(updated_tx.status, TransactionStatus::Submitted);
3183 }
3184
3185 #[tokio::test]
3187 async fn test_submit_transaction_real_error_fails() {
3188 let mock_transaction = MockTransactionRepository::new();
3189 let mock_relayer = MockRelayerRepository::new();
3190 let mut mock_provider = MockEvmProviderTrait::new();
3191 let mock_signer = MockSigner::new();
3192 let mock_job_producer = MockJobProducerTrait::new();
3193 let mock_price_calculator = MockPriceCalculator::new();
3194 let counter_service = MockTransactionCounterTrait::new();
3195 let mock_network = MockNetworkRepository::new();
3196
3197 let relayer = create_test_relayer();
3198 let mut test_tx = create_test_transaction();
3199 test_tx.status = TransactionStatus::Sent;
3200 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3201 raw: Some(vec![1, 2, 3]),
3202 ..test_tx.network_data.get_evm_transaction_data().unwrap()
3203 });
3204
3205 mock_provider
3207 .expect_send_raw_transaction()
3208 .times(1)
3209 .returning(|_| {
3210 Box::pin(async {
3211 Err(crate::services::provider::ProviderError::Other(
3212 "insufficient funds for gas * price + value".to_string(),
3213 ))
3214 })
3215 });
3216
3217 let evm_transaction = EvmRelayerTransaction {
3218 relayer: relayer.clone(),
3219 provider: mock_provider,
3220 relayer_repository: Arc::new(mock_relayer),
3221 network_repository: Arc::new(mock_network),
3222 transaction_repository: Arc::new(mock_transaction),
3223 transaction_counter_service: Arc::new(counter_service),
3224 job_producer: Arc::new(mock_job_producer),
3225 price_calculator: mock_price_calculator,
3226 signer: mock_signer,
3227 };
3228
3229 let result = evm_transaction.submit_transaction(test_tx).await;
3230 assert!(result.is_err());
3231 }
3232
3233 #[tokio::test]
3236 async fn test_resubmit_transaction_already_submitted_preserves_hash() {
3237 let mut mock_transaction = MockTransactionRepository::new();
3238 let mock_relayer = MockRelayerRepository::new();
3239 let mut mock_provider = MockEvmProviderTrait::new();
3240 let mut mock_signer = MockSigner::new();
3241 let mock_job_producer = MockJobProducerTrait::new();
3242 let mut mock_price_calculator = MockPriceCalculator::new();
3243 let counter_service = MockTransactionCounterTrait::new();
3244 let mock_network = MockNetworkRepository::new();
3245
3246 let relayer = create_test_relayer();
3247 let mut test_tx = create_test_transaction();
3248 test_tx.status = TransactionStatus::Submitted;
3249 test_tx.sent_at = Some(Utc::now().to_rfc3339());
3250 let original_hash = "0xoriginal_hash".to_string();
3251 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3252 nonce: Some(42),
3253 hash: Some(original_hash.clone()),
3254 raw: Some(vec![1, 2, 3]),
3255 ..test_tx.network_data.get_evm_transaction_data().unwrap()
3256 });
3257 test_tx.hashes = vec![original_hash.clone()];
3258
3259 mock_price_calculator
3261 .expect_calculate_bumped_gas_price()
3262 .times(1)
3263 .returning(|_, _, _| {
3264 Ok(PriceParams {
3265 gas_price: Some(25000000000), max_fee_per_gas: None,
3267 max_priority_fee_per_gas: None,
3268 is_min_bumped: Some(true),
3269 extra_fee: None,
3270 total_cost: U256::from(525000000000000u64),
3271 })
3272 });
3273
3274 mock_provider
3276 .expect_get_balance()
3277 .times(1)
3278 .returning(|_| Box::pin(async { Ok(U256::from(1000000000000000000u64)) }));
3279
3280 mock_signer
3282 .expect_sign_transaction()
3283 .times(1)
3284 .returning(|_| {
3285 Box::pin(ready(Ok(
3286 crate::domain::relayer::SignTransactionResponse::Evm(
3287 crate::domain::relayer::SignTransactionResponseEvm {
3288 hash: "0xnew_hash_that_should_not_be_saved".to_string(),
3289 signature: crate::models::EvmTransactionDataSignature {
3290 r: "r".to_string(),
3291 s: "s".to_string(),
3292 v: 1,
3293 sig: "0xsignature".to_string(),
3294 },
3295 raw: vec![4, 5, 6],
3296 },
3297 ),
3298 )))
3299 });
3300
3301 mock_provider
3303 .expect_send_raw_transaction()
3304 .times(1)
3305 .returning(|_| {
3306 Box::pin(async {
3307 Err(crate::services::provider::ProviderError::Other(
3308 "already known: transaction with same nonce already in mempool".to_string(),
3309 ))
3310 })
3311 });
3312
3313 let test_tx_clone = test_tx.clone();
3315 mock_transaction
3316 .expect_partial_update()
3317 .times(1)
3318 .withf(|_, update| {
3319 update.status == Some(TransactionStatus::Submitted)
3321 && update.network_data.is_none()
3322 && update.hashes.is_none()
3323 })
3324 .returning(move |_, _| {
3325 let mut updated_tx = test_tx_clone.clone();
3326 updated_tx.status = TransactionStatus::Submitted;
3327 Ok(updated_tx)
3329 });
3330
3331 let evm_transaction = EvmRelayerTransaction {
3332 relayer: relayer.clone(),
3333 provider: mock_provider,
3334 relayer_repository: Arc::new(mock_relayer),
3335 network_repository: Arc::new(mock_network),
3336 transaction_repository: Arc::new(mock_transaction),
3337 transaction_counter_service: Arc::new(counter_service),
3338 job_producer: Arc::new(mock_job_producer),
3339 price_calculator: mock_price_calculator,
3340 signer: mock_signer,
3341 };
3342
3343 let result = evm_transaction.resubmit_transaction(test_tx.clone()).await;
3344 assert!(result.is_ok());
3345 let updated_tx = result.unwrap();
3346
3347 if let NetworkTransactionData::Evm(evm_data) = &updated_tx.network_data {
3349 assert_eq!(evm_data.hash, Some(original_hash));
3350 } else {
3351 panic!("Expected EVM network data");
3352 }
3353 }
3354
3355 #[tokio::test]
3358 async fn test_submit_transaction_db_failure_after_blockchain_success() {
3359 let mut mock_transaction = MockTransactionRepository::new();
3360 let mock_relayer = MockRelayerRepository::new();
3361 let mut mock_provider = MockEvmProviderTrait::new();
3362 let mock_signer = MockSigner::new();
3363 let mut mock_job_producer = MockJobProducerTrait::new();
3364 let mock_price_calculator = MockPriceCalculator::new();
3365 let counter_service = MockTransactionCounterTrait::new();
3366 let mock_network = MockNetworkRepository::new();
3367
3368 let relayer = create_test_relayer();
3369 let mut test_tx = create_test_transaction();
3370 test_tx.status = TransactionStatus::Sent;
3371 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3372 raw: Some(vec![1, 2, 3]),
3373 ..test_tx.network_data.get_evm_transaction_data().unwrap()
3374 });
3375
3376 mock_provider
3378 .expect_send_raw_transaction()
3379 .times(1)
3380 .returning(|_| Box::pin(async { Ok("0xsubmitted_hash".to_string()) }));
3381
3382 mock_transaction
3384 .expect_partial_update()
3385 .times(1)
3386 .returning(|_, _| {
3387 Err(crate::models::RepositoryError::UnexpectedError(
3388 "Redis timeout".to_string(),
3389 ))
3390 });
3391
3392 mock_job_producer
3394 .expect_produce_send_notification_job()
3395 .times(1)
3396 .returning(|_, _| Box::pin(ready(Ok(()))));
3397
3398 let evm_transaction = EvmRelayerTransaction {
3399 relayer: relayer.clone(),
3400 provider: mock_provider,
3401 relayer_repository: Arc::new(mock_relayer),
3402 network_repository: Arc::new(mock_network),
3403 transaction_repository: Arc::new(mock_transaction),
3404 transaction_counter_service: Arc::new(counter_service),
3405 job_producer: Arc::new(mock_job_producer),
3406 price_calculator: mock_price_calculator,
3407 signer: mock_signer,
3408 };
3409
3410 let result = evm_transaction.submit_transaction(test_tx.clone()).await;
3411 assert!(result.is_ok());
3413 let returned_tx = result.unwrap();
3414 assert_eq!(returned_tx.id, test_tx.id);
3416 assert_eq!(returned_tx.status, TransactionStatus::Sent); }
3418
3419 #[tokio::test]
3421 async fn test_send_transaction_resend_job_success() {
3422 let mock_transaction = MockTransactionRepository::new();
3423 let mock_relayer = MockRelayerRepository::new();
3424 let mock_provider = MockEvmProviderTrait::new();
3425 let mock_signer = MockSigner::new();
3426 let mut mock_job_producer = MockJobProducerTrait::new();
3427 let mock_price_calculator = MockPriceCalculator::new();
3428 let counter_service = MockTransactionCounterTrait::new();
3429 let mock_network = MockNetworkRepository::new();
3430
3431 let relayer = create_test_relayer();
3432 let test_tx = create_test_transaction();
3433
3434 mock_job_producer
3436 .expect_produce_submit_transaction_job()
3437 .times(1)
3438 .withf(|job, delay| {
3439 job.transaction_id == "test-tx-id"
3441 && job.relayer_id == "test-relayer-id"
3442 && matches!(job.command, crate::jobs::TransactionCommand::Resend)
3443 && delay.is_none()
3444 })
3445 .returning(|_, _| Box::pin(ready(Ok(()))));
3446
3447 let evm_transaction = EvmRelayerTransaction {
3448 relayer: relayer.clone(),
3449 provider: mock_provider,
3450 relayer_repository: Arc::new(mock_relayer),
3451 network_repository: Arc::new(mock_network),
3452 transaction_repository: Arc::new(mock_transaction),
3453 transaction_counter_service: Arc::new(counter_service),
3454 job_producer: Arc::new(mock_job_producer),
3455 price_calculator: mock_price_calculator,
3456 signer: mock_signer,
3457 };
3458
3459 let result = evm_transaction.send_transaction_resend_job(&test_tx).await;
3460 assert!(result.is_ok());
3461 }
3462
3463 #[tokio::test]
3465 async fn test_send_transaction_resend_job_failure() {
3466 let mock_transaction = MockTransactionRepository::new();
3467 let mock_relayer = MockRelayerRepository::new();
3468 let mock_provider = MockEvmProviderTrait::new();
3469 let mock_signer = MockSigner::new();
3470 let mut mock_job_producer = MockJobProducerTrait::new();
3471 let mock_price_calculator = MockPriceCalculator::new();
3472 let counter_service = MockTransactionCounterTrait::new();
3473 let mock_network = MockNetworkRepository::new();
3474
3475 let relayer = create_test_relayer();
3476 let test_tx = create_test_transaction();
3477
3478 mock_job_producer
3480 .expect_produce_submit_transaction_job()
3481 .times(1)
3482 .returning(|_, _| {
3483 Box::pin(ready(Err(crate::jobs::JobProducerError::QueueError(
3484 "Job queue is full".to_string(),
3485 ))))
3486 });
3487
3488 let evm_transaction = EvmRelayerTransaction {
3489 relayer: relayer.clone(),
3490 provider: mock_provider,
3491 relayer_repository: Arc::new(mock_relayer),
3492 network_repository: Arc::new(mock_network),
3493 transaction_repository: Arc::new(mock_transaction),
3494 transaction_counter_service: Arc::new(counter_service),
3495 job_producer: Arc::new(mock_job_producer),
3496 price_calculator: mock_price_calculator,
3497 signer: mock_signer,
3498 };
3499
3500 let result = evm_transaction.send_transaction_resend_job(&test_tx).await;
3501 assert!(result.is_err());
3502 let err = result.unwrap_err();
3503 match err {
3504 TransactionError::UnexpectedError(msg) => {
3505 assert!(msg.contains("Failed to produce resend job"));
3506 }
3507 _ => panic!("Expected UnexpectedError"),
3508 }
3509 }
3510
3511 #[tokio::test]
3513 async fn test_send_transaction_request_job_success() {
3514 let mock_transaction = MockTransactionRepository::new();
3515 let mock_relayer = MockRelayerRepository::new();
3516 let mock_provider = MockEvmProviderTrait::new();
3517 let mock_signer = MockSigner::new();
3518 let mut mock_job_producer = MockJobProducerTrait::new();
3519 let mock_price_calculator = MockPriceCalculator::new();
3520 let counter_service = MockTransactionCounterTrait::new();
3521 let mock_network = MockNetworkRepository::new();
3522
3523 let relayer = create_test_relayer();
3524 let test_tx = create_test_transaction();
3525
3526 mock_job_producer
3528 .expect_produce_transaction_request_job()
3529 .times(1)
3530 .withf(|job, delay| {
3531 job.transaction_id == "test-tx-id"
3533 && job.relayer_id == "test-relayer-id"
3534 && delay.is_none()
3535 })
3536 .returning(|_, _| Box::pin(ready(Ok(()))));
3537
3538 let evm_transaction = EvmRelayerTransaction {
3539 relayer: relayer.clone(),
3540 provider: mock_provider,
3541 relayer_repository: Arc::new(mock_relayer),
3542 network_repository: Arc::new(mock_network),
3543 transaction_repository: Arc::new(mock_transaction),
3544 transaction_counter_service: Arc::new(counter_service),
3545 job_producer: Arc::new(mock_job_producer),
3546 price_calculator: mock_price_calculator,
3547 signer: mock_signer,
3548 };
3549
3550 let result = evm_transaction.send_transaction_request_job(&test_tx).await;
3551 assert!(result.is_ok());
3552 }
3553
3554 #[tokio::test]
3556 async fn test_send_transaction_request_job_failure() {
3557 let mock_transaction = MockTransactionRepository::new();
3558 let mock_relayer = MockRelayerRepository::new();
3559 let mock_provider = MockEvmProviderTrait::new();
3560 let mock_signer = MockSigner::new();
3561 let mut mock_job_producer = MockJobProducerTrait::new();
3562 let mock_price_calculator = MockPriceCalculator::new();
3563 let counter_service = MockTransactionCounterTrait::new();
3564 let mock_network = MockNetworkRepository::new();
3565
3566 let relayer = create_test_relayer();
3567 let test_tx = create_test_transaction();
3568
3569 mock_job_producer
3571 .expect_produce_transaction_request_job()
3572 .times(1)
3573 .returning(|_, _| {
3574 Box::pin(ready(Err(crate::jobs::JobProducerError::QueueError(
3575 "Redis connection failed".to_string(),
3576 ))))
3577 });
3578
3579 let evm_transaction = EvmRelayerTransaction {
3580 relayer: relayer.clone(),
3581 provider: mock_provider,
3582 relayer_repository: Arc::new(mock_relayer),
3583 network_repository: Arc::new(mock_network),
3584 transaction_repository: Arc::new(mock_transaction),
3585 transaction_counter_service: Arc::new(counter_service),
3586 job_producer: Arc::new(mock_job_producer),
3587 price_calculator: mock_price_calculator,
3588 signer: mock_signer,
3589 };
3590
3591 let result = evm_transaction.send_transaction_request_job(&test_tx).await;
3592 assert!(result.is_err());
3593 let err = result.unwrap_err();
3594 match err {
3595 TransactionError::UnexpectedError(msg) => {
3596 assert!(msg.contains("Failed to produce request job"));
3597 }
3598 _ => panic!("Expected UnexpectedError"),
3599 }
3600 }
3601
3602 #[tokio::test]
3604 async fn test_resubmit_transaction_sent_to_submitted() {
3605 let mut mock_transaction = MockTransactionRepository::new();
3606 let mock_relayer = MockRelayerRepository::new();
3607 let mut mock_provider = MockEvmProviderTrait::new();
3608 let mut mock_signer = MockSigner::new();
3609 let mock_job_producer = MockJobProducerTrait::new();
3610 let mut mock_price_calculator = MockPriceCalculator::new();
3611 let counter_service = MockTransactionCounterTrait::new();
3612 let mock_network = MockNetworkRepository::new();
3613
3614 let relayer = create_test_relayer();
3615 let mut test_tx = create_test_transaction();
3616 test_tx.status = TransactionStatus::Sent;
3617 test_tx.sent_at = Some(Utc::now().to_rfc3339());
3618 let original_hash = "0xoriginal_hash".to_string();
3619 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3620 nonce: Some(42),
3621 hash: Some(original_hash.clone()),
3622 raw: Some(vec![1, 2, 3]),
3623 gas_price: Some(20000000000), ..test_tx.network_data.get_evm_transaction_data().unwrap()
3625 });
3626 test_tx.hashes = vec![original_hash.clone()];
3627
3628 mock_price_calculator
3630 .expect_calculate_bumped_gas_price()
3631 .times(1)
3632 .returning(|_, _, _| {
3633 Ok(PriceParams {
3634 gas_price: Some(25000000000), max_fee_per_gas: None,
3636 max_priority_fee_per_gas: None,
3637 is_min_bumped: Some(true),
3638 extra_fee: None,
3639 total_cost: U256::from(525000000000000u64),
3640 })
3641 });
3642
3643 mock_provider
3645 .expect_get_balance()
3646 .returning(|_| Box::pin(ready(Ok(U256::from(1000000000000000000u64)))));
3647
3648 mock_signer.expect_sign_transaction().returning(|_| {
3650 Box::pin(ready(Ok(
3651 crate::domain::relayer::SignTransactionResponse::Evm(
3652 crate::domain::relayer::SignTransactionResponseEvm {
3653 hash: "0xnew_hash".to_string(),
3654 signature: crate::models::EvmTransactionDataSignature {
3655 r: "r".to_string(),
3656 s: "s".to_string(),
3657 v: 1,
3658 sig: "0xsignature".to_string(),
3659 },
3660 raw: vec![4, 5, 6],
3661 },
3662 ),
3663 )))
3664 });
3665
3666 mock_provider
3668 .expect_send_raw_transaction()
3669 .times(1)
3670 .returning(|_| Box::pin(async { Ok("0xnew_hash".to_string()) }));
3671
3672 let test_tx_clone = test_tx.clone();
3674 mock_transaction
3675 .expect_partial_update()
3676 .times(1)
3677 .withf(|_, update| {
3678 update.status == Some(TransactionStatus::Submitted)
3679 && update.sent_at.is_some()
3680 && update.priced_at.is_some()
3681 && update.hashes.is_some()
3682 })
3683 .returning(move |_, update| {
3684 let mut updated_tx = test_tx_clone.clone();
3685 updated_tx.status = update.status.unwrap();
3686 updated_tx.sent_at = update.sent_at.clone();
3687 updated_tx.priced_at = update.priced_at.clone();
3688 if let Some(hashes) = update.hashes.clone() {
3689 updated_tx.hashes = hashes;
3690 }
3691 if let Some(network_data) = update.network_data.clone() {
3692 updated_tx.network_data = network_data;
3693 }
3694 Ok(updated_tx)
3695 });
3696
3697 let evm_transaction = EvmRelayerTransaction {
3698 relayer: relayer.clone(),
3699 provider: mock_provider,
3700 relayer_repository: Arc::new(mock_relayer),
3701 network_repository: Arc::new(mock_network),
3702 transaction_repository: Arc::new(mock_transaction),
3703 transaction_counter_service: Arc::new(counter_service),
3704 job_producer: Arc::new(mock_job_producer),
3705 price_calculator: mock_price_calculator,
3706 signer: mock_signer,
3707 };
3708
3709 let result = evm_transaction.resubmit_transaction(test_tx.clone()).await;
3710 assert!(result.is_ok(), "Expected Ok, got: {result:?}");
3711 let updated_tx = result.unwrap();
3712 assert_eq!(
3713 updated_tx.status,
3714 TransactionStatus::Submitted,
3715 "Transaction status should transition from Sent to Submitted"
3716 );
3717 }
3718
3719 #[test]
3720 fn test_classify_submission_error_nonce_too_low() {
3721 assert_eq!(
3722 DefaultEvmTransaction::classify_submission_error(&"nonce too low"),
3723 SubmissionErrorKind::NonceTooLow
3724 );
3725 assert_eq!(
3726 DefaultEvmTransaction::classify_submission_error(&"Nonce Too Low"),
3727 SubmissionErrorKind::NonceTooLow
3728 );
3729 assert_eq!(
3730 DefaultEvmTransaction::classify_submission_error(&"nonce is too low"),
3731 SubmissionErrorKind::NonceTooLow
3732 );
3733 }
3734
3735 #[test]
3736 fn test_classify_submission_error_already_known() {
3737 assert_eq!(
3738 DefaultEvmTransaction::classify_submission_error(&"already known"),
3739 SubmissionErrorKind::AlreadyKnown
3740 );
3741 assert_eq!(
3742 DefaultEvmTransaction::classify_submission_error(&"known transaction"),
3743 SubmissionErrorKind::AlreadyKnown
3744 );
3745 assert_eq!(
3746 DefaultEvmTransaction::classify_submission_error(&"same hash was already imported"),
3747 SubmissionErrorKind::AlreadyKnown
3748 );
3749 assert!(matches!(
3751 DefaultEvmTransaction::classify_submission_error(&"unknown transaction"),
3752 SubmissionErrorKind::Other(_)
3753 ));
3754 }
3755
3756 #[test]
3757 fn test_classify_submission_error_replacement_underpriced() {
3758 assert_eq!(
3759 DefaultEvmTransaction::classify_submission_error(
3760 &"replacement transaction underpriced"
3761 ),
3762 SubmissionErrorKind::ReplacementUnderpriced
3763 );
3764 }
3765
3766 #[test]
3767 fn test_classify_submission_error_other() {
3768 assert!(matches!(
3769 DefaultEvmTransaction::classify_submission_error(&"execution reverted"),
3770 SubmissionErrorKind::Other(_)
3771 ));
3772 assert!(matches!(
3773 DefaultEvmTransaction::classify_submission_error(&"gas too low"),
3774 SubmissionErrorKind::Other(_)
3775 ));
3776 assert!(matches!(
3778 DefaultEvmTransaction::classify_submission_error(
3779 &"insufficient funds for gas * price + value"
3780 ),
3781 SubmissionErrorKind::Other(_)
3782 ));
3783 }
3784
3785 #[test]
3786 fn test_classify_submission_error_nonce_too_high() {
3787 assert_eq!(
3788 DefaultEvmTransaction::classify_submission_error(&"nonce too high"),
3789 SubmissionErrorKind::NonceTooHigh
3790 );
3791 assert_eq!(
3792 DefaultEvmTransaction::classify_submission_error(&"exceeds next nonce"),
3793 SubmissionErrorKind::NonceTooHigh
3794 );
3795 assert_eq!(
3796 DefaultEvmTransaction::classify_submission_error(&"nonce too far in the future"),
3797 SubmissionErrorKind::NonceTooHigh
3798 );
3799 assert_eq!(
3800 DefaultEvmTransaction::classify_submission_error(&"nonce out of range"),
3801 SubmissionErrorKind::NonceTooHigh
3802 );
3803 }
3804
3805 #[test]
3806 fn test_classify_submission_error_nonce_too_high_case_insensitive() {
3807 assert_eq!(
3808 DefaultEvmTransaction::classify_submission_error(&"Nonce Too High"),
3809 SubmissionErrorKind::NonceTooHigh
3810 );
3811 assert_eq!(
3812 DefaultEvmTransaction::classify_submission_error(&"NONCE OUT OF RANGE"),
3813 SubmissionErrorKind::NonceTooHigh
3814 );
3815 assert_eq!(
3816 DefaultEvmTransaction::classify_submission_error(&"Exceeds Next Nonce"),
3817 SubmissionErrorKind::NonceTooHigh
3818 );
3819 }
3820
3821 #[tokio::test]
3824 async fn test_submit_transaction_nonce_too_low_on_submitted_schedules_recovery() {
3825 let mut mock_transaction = MockTransactionRepository::new();
3826 let mock_relayer = MockRelayerRepository::new();
3827 let mut mock_provider = MockEvmProviderTrait::new();
3828 let mock_signer = MockSigner::new();
3829 let mut mock_job_producer = MockJobProducerTrait::new();
3830 let mock_price_calculator = MockPriceCalculator::new();
3831 let counter_service = MockTransactionCounterTrait::new();
3832 let mock_network = MockNetworkRepository::new();
3833
3834 let relayer = create_test_relayer();
3835 let mut test_tx = create_test_transaction();
3836 test_tx.status = TransactionStatus::Submitted;
3837 test_tx.sent_at = Some(Utc::now().to_rfc3339());
3838 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3839 nonce: Some(42),
3840 hash: Some("0xhash".to_string()),
3841 raw: Some(vec![1, 2, 3]),
3842 ..test_tx.network_data.get_evm_transaction_data().unwrap()
3843 });
3844
3845 mock_provider
3847 .expect_send_raw_transaction()
3848 .times(1)
3849 .returning(|_| {
3850 Box::pin(async {
3851 Err(crate::services::provider::ProviderError::Other(
3852 "nonce too low".to_string(),
3853 ))
3854 })
3855 });
3856
3857 let test_tx_clone = test_tx.clone();
3859 mock_transaction
3860 .expect_partial_update()
3861 .times(1)
3862 .withf(|_, update| update.status_reason.is_some())
3863 .returning(move |_, _| Ok(test_tx_clone.clone()));
3864
3865 mock_job_producer
3867 .expect_produce_check_transaction_status_job()
3868 .times(1)
3869 .withf(|job, _| {
3870 job.metadata
3871 .as_ref()
3872 .map(|m| m.contains_key(TX_NONCE_RECONCILE_TRIGGER))
3873 .unwrap_or(false)
3874 })
3875 .returning(|_, _| Box::pin(ready(Ok(()))));
3876
3877 let evm_transaction = EvmRelayerTransaction {
3878 relayer: relayer.clone(),
3879 provider: mock_provider,
3880 relayer_repository: Arc::new(mock_relayer),
3881 network_repository: Arc::new(mock_network),
3882 transaction_repository: Arc::new(mock_transaction),
3883 transaction_counter_service: Arc::new(counter_service),
3884 job_producer: Arc::new(mock_job_producer),
3885 price_calculator: mock_price_calculator,
3886 signer: mock_signer,
3887 };
3888
3889 let result = evm_transaction.submit_transaction(test_tx).await;
3891 assert!(
3892 result.is_ok(),
3893 "Expected Ok on nonce error for non-Sent tx, got: {result:?}"
3894 );
3895 }
3896
3897 #[tokio::test]
3902 async fn test_submit_transaction_nonce_too_low_on_sent_schedules_recovery() {
3903 let mut mock_transaction = MockTransactionRepository::new();
3904 let mock_relayer = MockRelayerRepository::new();
3905 let mut mock_provider = MockEvmProviderTrait::new();
3906 let mock_signer = MockSigner::new();
3907 let mut mock_job_producer = MockJobProducerTrait::new();
3908 let mock_price_calculator = MockPriceCalculator::new();
3909 let counter_service = MockTransactionCounterTrait::new();
3910 let mock_network = MockNetworkRepository::new();
3911
3912 let relayer = create_test_relayer();
3913 let mut test_tx = create_test_transaction();
3914 test_tx.status = TransactionStatus::Sent;
3915 test_tx.sent_at = Some(Utc::now().to_rfc3339());
3916 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3917 nonce: Some(42),
3918 hash: Some("0xhash".to_string()),
3919 raw: Some(vec![1, 2, 3]),
3920 ..test_tx.network_data.get_evm_transaction_data().unwrap()
3921 });
3922
3923 mock_provider
3925 .expect_send_raw_transaction()
3926 .times(1)
3927 .returning(|_| {
3928 Box::pin(async {
3929 Err(crate::services::provider::ProviderError::Other(
3930 "nonce too low".to_string(),
3931 ))
3932 })
3933 });
3934
3935 let test_tx_clone = test_tx.clone();
3937 mock_transaction
3938 .expect_partial_update()
3939 .times(1)
3940 .withf(|_, update| update.status_reason.is_some())
3941 .returning(move |_, _| Ok(test_tx_clone.clone()));
3942
3943 mock_job_producer
3945 .expect_produce_check_transaction_status_job()
3946 .times(1)
3947 .withf(|job, _| {
3948 job.metadata
3949 .as_ref()
3950 .map(|m| m.contains_key(TX_NONCE_RECONCILE_TRIGGER))
3951 .unwrap_or(false)
3952 })
3953 .returning(|_, _| Box::pin(ready(Ok(()))));
3954
3955 let evm_transaction = EvmRelayerTransaction {
3956 relayer: relayer.clone(),
3957 provider: mock_provider,
3958 relayer_repository: Arc::new(mock_relayer),
3959 network_repository: Arc::new(mock_network),
3960 transaction_repository: Arc::new(mock_transaction),
3961 transaction_counter_service: Arc::new(counter_service),
3962 job_producer: Arc::new(mock_job_producer),
3963 price_calculator: mock_price_calculator,
3964 signer: mock_signer,
3965 };
3966
3967 let result = evm_transaction.submit_transaction(test_tx.clone()).await;
3969 assert!(
3970 result.is_ok(),
3971 "Expected Ok on nonce too low for Sent tx, got: {result:?}"
3972 );
3973 let returned_tx = result.unwrap();
3975 assert_eq!(returned_tx.status, TransactionStatus::Sent);
3976 }
3977
3978 #[tokio::test]
3980 async fn test_resubmit_transaction_nonce_too_low_schedules_recovery() {
3981 let mut mock_transaction = MockTransactionRepository::new();
3982 let mock_relayer = MockRelayerRepository::new();
3983 let mut mock_provider = MockEvmProviderTrait::new();
3984 let mut mock_signer = MockSigner::new();
3985 let mut mock_job_producer = MockJobProducerTrait::new();
3986 let mut mock_price_calculator = MockPriceCalculator::new();
3987 let counter_service = MockTransactionCounterTrait::new();
3988 let mock_network = MockNetworkRepository::new();
3989
3990 let relayer = create_test_relayer();
3991 let mut test_tx = create_test_transaction();
3992 test_tx.status = TransactionStatus::Submitted;
3993 test_tx.sent_at = Some(Utc::now().to_rfc3339());
3994 let original_hash = "0xoriginal_hash".to_string();
3995 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3996 nonce: Some(42),
3997 hash: Some(original_hash.clone()),
3998 raw: Some(vec![1, 2, 3]),
3999 ..test_tx.network_data.get_evm_transaction_data().unwrap()
4000 });
4001 test_tx.hashes = vec![original_hash.clone()];
4002
4003 mock_price_calculator
4005 .expect_calculate_bumped_gas_price()
4006 .times(1)
4007 .returning(|_, _, _| {
4008 Ok(PriceParams {
4009 gas_price: Some(25000000000),
4010 max_fee_per_gas: None,
4011 max_priority_fee_per_gas: None,
4012 is_min_bumped: Some(true),
4013 extra_fee: None,
4014 total_cost: U256::from(525000000000000u64),
4015 })
4016 });
4017
4018 mock_provider
4020 .expect_get_balance()
4021 .times(1)
4022 .returning(|_| Box::pin(async { Ok(U256::from(1000000000000000000u64)) }));
4023
4024 mock_signer
4026 .expect_sign_transaction()
4027 .times(1)
4028 .returning(|_| {
4029 Box::pin(ready(Ok(
4030 crate::domain::relayer::SignTransactionResponse::Evm(
4031 crate::domain::relayer::SignTransactionResponseEvm {
4032 hash: "0xnew_hash".to_string(),
4033 signature: crate::models::EvmTransactionDataSignature {
4034 r: "r".to_string(),
4035 s: "s".to_string(),
4036 v: 1,
4037 sig: "0xsignature".to_string(),
4038 },
4039 raw: vec![4, 5, 6],
4040 },
4041 ),
4042 )))
4043 });
4044
4045 mock_provider
4047 .expect_send_raw_transaction()
4048 .times(1)
4049 .returning(|_| {
4050 Box::pin(async {
4051 Err(crate::services::provider::ProviderError::Other(
4052 "nonce too low".to_string(),
4053 ))
4054 })
4055 });
4056
4057 mock_job_producer
4059 .expect_produce_check_transaction_status_job()
4060 .times(1)
4061 .withf(|job, _| {
4062 job.metadata
4063 .as_ref()
4064 .map(|m| m.contains_key(TX_NONCE_RECONCILE_TRIGGER))
4065 .unwrap_or(false)
4066 })
4067 .returning(|_, _| Box::pin(ready(Ok(()))));
4068
4069 let test_tx_clone = test_tx.clone();
4071 mock_transaction
4072 .expect_partial_update()
4073 .times(1)
4074 .withf(|_, update| {
4075 update.status == Some(TransactionStatus::Submitted)
4076 && update.network_data.is_none()
4077 && update.hashes.is_none()
4078 })
4079 .returning(move |_, _| {
4080 let mut updated_tx = test_tx_clone.clone();
4081 updated_tx.status = TransactionStatus::Submitted;
4082 Ok(updated_tx)
4083 });
4084
4085 let evm_transaction = EvmRelayerTransaction {
4086 relayer: relayer.clone(),
4087 provider: mock_provider,
4088 relayer_repository: Arc::new(mock_relayer),
4089 network_repository: Arc::new(mock_network),
4090 transaction_repository: Arc::new(mock_transaction),
4091 transaction_counter_service: Arc::new(counter_service),
4092 job_producer: Arc::new(mock_job_producer),
4093 price_calculator: mock_price_calculator,
4094 signer: mock_signer,
4095 };
4096
4097 let result = evm_transaction.resubmit_transaction(test_tx.clone()).await;
4098 assert!(result.is_ok());
4099 let updated_tx = result.unwrap();
4100 if let NetworkTransactionData::Evm(evm_data) = &updated_tx.network_data {
4102 assert_eq!(evm_data.hash, Some(original_hash));
4103 } else {
4104 panic!("Expected EVM network data");
4105 }
4106 }
4107
4108 #[tokio::test]
4110 async fn test_submit_transaction_nonce_too_high_increments_retry_counter() {
4111 let mut mock_transaction = MockTransactionRepository::new();
4112 let mock_relayer = MockRelayerRepository::new();
4113 let mut mock_provider = MockEvmProviderTrait::new();
4114 let mock_signer = MockSigner::new();
4115 let mock_job_producer = MockJobProducerTrait::new();
4116 let mock_price_calculator = MockPriceCalculator::new();
4117 let counter_service = MockTransactionCounterTrait::new();
4118 let mock_network = MockNetworkRepository::new();
4119
4120 let relayer = create_test_relayer();
4121 let mut test_tx = create_test_transaction();
4122 test_tx.status = TransactionStatus::Sent;
4123 test_tx.sent_at = Some(Utc::now().to_rfc3339());
4124 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
4125 nonce: Some(10),
4126 hash: Some("0xhash".to_string()),
4127 raw: Some(vec![1, 2, 3]),
4128 ..test_tx.network_data.get_evm_transaction_data().unwrap()
4129 });
4130 test_tx.metadata = Some(crate::models::TransactionMetadata {
4132 nonce_too_high_retries: 0,
4133 ..Default::default()
4134 });
4135
4136 mock_provider
4138 .expect_send_raw_transaction()
4139 .times(1)
4140 .returning(|_| {
4141 Box::pin(async {
4142 Err(crate::services::provider::ProviderError::Other(
4143 "nonce too high".to_string(),
4144 ))
4145 })
4146 });
4147
4148 let test_tx_clone = test_tx.clone();
4150 mock_transaction
4151 .expect_partial_update()
4152 .times(1)
4153 .withf(|_, update| {
4154 update
4155 .metadata
4156 .as_ref()
4157 .map(|m| m.nonce_too_high_retries == 1)
4158 .unwrap_or(false)
4159 })
4160 .returning(move |_, _| Ok(test_tx_clone.clone()));
4161
4162 let evm_transaction = EvmRelayerTransaction {
4163 relayer: relayer.clone(),
4164 provider: mock_provider,
4165 relayer_repository: Arc::new(mock_relayer),
4166 network_repository: Arc::new(mock_network),
4167 transaction_repository: Arc::new(mock_transaction),
4168 transaction_counter_service: Arc::new(counter_service),
4169 job_producer: Arc::new(mock_job_producer),
4170 price_calculator: mock_price_calculator,
4171 signer: mock_signer,
4172 };
4173
4174 let result = evm_transaction.submit_transaction(test_tx).await;
4176 assert!(
4177 result.is_ok(),
4178 "Expected Ok on nonce too high, got: {result:?}"
4179 );
4180 }
4181
4182 #[tokio::test]
4185 async fn test_submit_transaction_nonce_too_high_schedules_health_job_at_threshold() {
4186 let mut mock_transaction = MockTransactionRepository::new();
4187 let mock_relayer = MockRelayerRepository::new();
4188 let mut mock_provider = MockEvmProviderTrait::new();
4189 let mock_signer = MockSigner::new();
4190 let mut mock_job_producer = MockJobProducerTrait::new();
4191 let mock_price_calculator = MockPriceCalculator::new();
4192 let counter_service = MockTransactionCounterTrait::new();
4193 let mock_network = MockNetworkRepository::new();
4194
4195 let relayer = create_test_relayer();
4196 let mut test_tx = create_test_transaction();
4197 test_tx.status = TransactionStatus::Sent;
4198 test_tx.sent_at = Some(Utc::now().to_rfc3339());
4199 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
4200 nonce: Some(10),
4201 hash: Some("0xhash".to_string()),
4202 raw: Some(vec![1, 2, 3]),
4203 ..test_tx.network_data.get_evm_transaction_data().unwrap()
4204 });
4205 test_tx.metadata = Some(crate::models::TransactionMetadata {
4207 nonce_too_high_retries: 2,
4208 ..Default::default()
4209 });
4210
4211 mock_provider
4213 .expect_send_raw_transaction()
4214 .times(1)
4215 .returning(|_| {
4216 Box::pin(async {
4217 Err(crate::services::provider::ProviderError::Other(
4218 "nonce too high".to_string(),
4219 ))
4220 })
4221 });
4222
4223 let test_tx_clone = test_tx.clone();
4225 mock_transaction
4226 .expect_partial_update()
4227 .times(1)
4228 .withf(|_, update| {
4229 update
4230 .metadata
4231 .as_ref()
4232 .map(|m| m.nonce_too_high_retries == 3)
4233 .unwrap_or(false)
4234 })
4235 .returning(move |_, _| Ok(test_tx_clone.clone()));
4236
4237 mock_job_producer
4239 .expect_produce_relayer_health_check_job()
4240 .times(1)
4241 .returning(|_, _| Box::pin(ready(Ok(()))));
4242
4243 let evm_transaction = EvmRelayerTransaction {
4244 relayer: relayer.clone(),
4245 provider: mock_provider,
4246 relayer_repository: Arc::new(mock_relayer),
4247 network_repository: Arc::new(mock_network),
4248 transaction_repository: Arc::new(mock_transaction),
4249 transaction_counter_service: Arc::new(counter_service),
4250 job_producer: Arc::new(mock_job_producer),
4251 price_calculator: mock_price_calculator,
4252 signer: mock_signer,
4253 };
4254
4255 let result = evm_transaction.submit_transaction(test_tx).await;
4257 assert!(
4258 result.is_ok(),
4259 "Expected Ok on nonce too high at threshold, got: {result:?}"
4260 );
4261 }
4262
4263 #[tokio::test]
4265 async fn test_resubmit_transaction_nonce_too_high_returns_ok() {
4266 let mut mock_transaction = MockTransactionRepository::new();
4267 let mock_relayer = MockRelayerRepository::new();
4268 let mut mock_provider = MockEvmProviderTrait::new();
4269 let mut mock_signer = MockSigner::new();
4270 let mock_job_producer = MockJobProducerTrait::new();
4271 let mut mock_price_calculator = MockPriceCalculator::new();
4272 let counter_service = MockTransactionCounterTrait::new();
4273 let mock_network = MockNetworkRepository::new();
4274
4275 let relayer = create_test_relayer();
4276 let mut test_tx = create_test_transaction();
4277 test_tx.status = TransactionStatus::Submitted;
4278 test_tx.sent_at = Some(Utc::now().to_rfc3339());
4279 let original_hash = "0xoriginal_hash".to_string();
4280 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
4281 nonce: Some(10),
4282 hash: Some(original_hash.clone()),
4283 raw: Some(vec![1, 2, 3]),
4284 ..test_tx.network_data.get_evm_transaction_data().unwrap()
4285 });
4286 test_tx.hashes = vec![original_hash.clone()];
4287 test_tx.metadata = Some(crate::models::TransactionMetadata {
4289 nonce_too_high_retries: 0,
4290 ..Default::default()
4291 });
4292
4293 mock_price_calculator
4295 .expect_calculate_bumped_gas_price()
4296 .times(1)
4297 .returning(|_, _, _| {
4298 Ok(PriceParams {
4299 gas_price: Some(25000000000),
4300 max_fee_per_gas: None,
4301 max_priority_fee_per_gas: None,
4302 is_min_bumped: Some(true),
4303 extra_fee: None,
4304 total_cost: U256::from(525000000000000u64),
4305 })
4306 });
4307
4308 mock_provider
4310 .expect_get_balance()
4311 .times(1)
4312 .returning(|_| Box::pin(async { Ok(U256::from(1000000000000000000u64)) }));
4313
4314 mock_signer
4316 .expect_sign_transaction()
4317 .times(1)
4318 .returning(|_| {
4319 Box::pin(ready(Ok(
4320 crate::domain::relayer::SignTransactionResponse::Evm(
4321 crate::domain::relayer::SignTransactionResponseEvm {
4322 hash: "0xnew_hash".to_string(),
4323 signature: crate::models::EvmTransactionDataSignature {
4324 r: "r".to_string(),
4325 s: "s".to_string(),
4326 v: 1,
4327 sig: "0xsignature".to_string(),
4328 },
4329 raw: vec![4, 5, 6],
4330 },
4331 ),
4332 )))
4333 });
4334
4335 mock_provider
4337 .expect_send_raw_transaction()
4338 .times(1)
4339 .returning(|_| {
4340 Box::pin(async {
4341 Err(crate::services::provider::ProviderError::Other(
4342 "nonce too high".to_string(),
4343 ))
4344 })
4345 });
4346
4347 let test_tx_clone = test_tx.clone();
4349 mock_transaction
4350 .expect_partial_update()
4351 .times(1)
4352 .withf(|_, update| {
4353 update
4354 .metadata
4355 .as_ref()
4356 .map(|m| m.nonce_too_high_retries == 1)
4357 .unwrap_or(false)
4358 })
4359 .returning(move |_, _| Ok(test_tx_clone.clone()));
4360
4361 let evm_transaction = EvmRelayerTransaction {
4362 relayer: relayer.clone(),
4363 provider: mock_provider,
4364 relayer_repository: Arc::new(mock_relayer),
4365 network_repository: Arc::new(mock_network),
4366 transaction_repository: Arc::new(mock_transaction),
4367 transaction_counter_service: Arc::new(counter_service),
4368 job_producer: Arc::new(mock_job_producer),
4369 price_calculator: mock_price_calculator,
4370 signer: mock_signer,
4371 };
4372
4373 let result = evm_transaction.resubmit_transaction(test_tx.clone()).await;
4375 assert!(
4376 result.is_ok(),
4377 "Expected Ok on nonce too high during resubmit, got: {result:?}"
4378 );
4379 let returned_tx = result.unwrap();
4380 assert_eq!(returned_tx.status, TransactionStatus::Submitted);
4382 }
4383}