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