1use alloy::network::ReceiptResponse;
6use chrono::{DateTime, Duration, Utc};
7use eyre::Result;
8use tracing::{debug, error, warn};
9
10use super::EvmRelayerTransaction;
11use super::{
12 ensure_status, get_age_since_status_change, has_enough_confirmations, is_noop,
13 is_too_early_to_resubmit, is_transaction_valid, make_noop, too_many_attempts,
14 too_many_noop_attempts,
15};
16use crate::constants::{
17 get_evm_min_age_for_hash_recovery, get_evm_pending_recovery_trigger_timeout,
18 get_evm_prepare_timeout, get_evm_resend_timeout, ARBITRUM_TIME_TO_RESUBMIT,
19 EVM_MIN_HASHES_FOR_RECOVERY,
20};
21use crate::domain::transaction::common::{
22 get_age_of_sent_at, is_final_state, is_pending_transaction,
23};
24use crate::domain::transaction::util::get_age_since_created;
25use crate::models::{EvmNetwork, NetworkRepoModel, NetworkType};
26use crate::repositories::{NetworkRepository, RelayerRepository};
27use crate::{
28 domain::transaction::evm::price_calculator::PriceCalculatorTrait,
29 jobs::{JobProducerTrait, StatusCheckContext},
30 models::{
31 NetworkTransactionData, RelayerRepoModel, TransactionError, TransactionRepoModel,
32 TransactionStatus, TransactionUpdateRequest,
33 },
34 repositories::{Repository, TransactionCounterTrait, TransactionRepository},
35 services::{provider::EvmProviderTrait, signer::Signer},
36 utils::{get_resubmit_timeout_for_speed, get_resubmit_timeout_with_backoff},
37};
38
39impl<P, RR, NR, TR, J, S, TCR, PC> EvmRelayerTransaction<P, RR, NR, TR, J, S, TCR, PC>
40where
41 P: EvmProviderTrait + Send + Sync,
42 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
43 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
44 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
45 J: JobProducerTrait + Send + Sync + 'static,
46 S: Signer + Send + Sync + 'static,
47 TCR: TransactionCounterTrait + Send + Sync + 'static,
48 PC: PriceCalculatorTrait + Send + Sync,
49{
50 pub(super) async fn check_transaction_status(
51 &self,
52 tx: &TransactionRepoModel,
53 ) -> Result<TransactionStatus, TransactionError> {
54 if is_final_state(&tx.status) {
56 return Ok(tx.status.clone());
57 }
58
59 match tx.status {
62 TransactionStatus::Pending | TransactionStatus::Sent => {
63 return Ok(tx.status.clone());
64 }
65 _ => {}
66 }
67
68 let evm_data = tx.network_data.get_evm_transaction_data()?;
69 let tx_hash = evm_data
70 .hash
71 .as_ref()
72 .ok_or(TransactionError::UnexpectedError(
73 "Transaction hash is missing".to_string(),
74 ))?;
75
76 let receipt_result = self.provider().get_transaction_receipt(tx_hash).await?;
77
78 if let Some(receipt) = receipt_result {
79 if !receipt.inner.status() {
80 return Ok(TransactionStatus::Failed);
81 }
82 let last_block_number = self.provider().get_block_number().await?;
83 let tx_block_number = receipt
84 .block_number
85 .ok_or(TransactionError::UnexpectedError(
86 "Transaction receipt missing block number".to_string(),
87 ))?;
88
89 let network_model = self
90 .network_repository()
91 .get_by_chain_id(NetworkType::Evm, evm_data.chain_id)
92 .await?
93 .ok_or(TransactionError::UnexpectedError(format!(
94 "Network with chain id {} not found",
95 evm_data.chain_id
96 )))?;
97
98 let network = EvmNetwork::try_from(network_model).map_err(|e| {
99 TransactionError::UnexpectedError(format!(
100 "Error converting network model to EvmNetwork: {e}"
101 ))
102 })?;
103
104 if !has_enough_confirmations(
105 tx_block_number,
106 last_block_number,
107 network.required_confirmations,
108 ) {
109 debug!(
110 tx_id = %tx.id,
111 relayer_id = %tx.relayer_id,
112 tx_hash = %tx_hash,
113 "transaction mined but not confirmed"
114 );
115 return Ok(TransactionStatus::Mined);
116 }
117 Ok(TransactionStatus::Confirmed)
118 } else {
119 debug!(
120 tx_id = %tx.id,
121 relayer_id = %tx.relayer_id,
122 tx_hash = %tx_hash,
123 "transaction not yet mined"
124 );
125
126 if tx.hashes.len() > 1 && self.should_try_hash_recovery(tx)? {
130 if let Some(recovered_tx) = self
131 .try_recover_with_historical_hashes(tx, &evm_data)
132 .await?
133 {
134 return Ok(recovered_tx.status);
136 }
137 }
138
139 Ok(TransactionStatus::Submitted)
140 }
141 }
142
143 pub(super) async fn should_resubmit(
145 &self,
146 tx: &TransactionRepoModel,
147 ) -> Result<bool, TransactionError> {
148 ensure_status(tx, TransactionStatus::Submitted, Some("should_resubmit"))?;
150
151 let evm_data = tx.network_data.get_evm_transaction_data()?;
152 let age = get_age_of_sent_at(tx)?;
153
154 let network_model = self
156 .network_repository()
157 .get_by_chain_id(NetworkType::Evm, evm_data.chain_id)
158 .await?
159 .ok_or(TransactionError::UnexpectedError(format!(
160 "Network with chain id {} not found",
161 evm_data.chain_id
162 )))?;
163
164 let network = EvmNetwork::try_from(network_model).map_err(|e| {
165 TransactionError::UnexpectedError(format!(
166 "Error converting network model to EvmNetwork: {e}"
167 ))
168 })?;
169
170 let timeout = match network.is_arbitrum() {
171 true => ARBITRUM_TIME_TO_RESUBMIT,
172 false => get_resubmit_timeout_for_speed(&evm_data.speed),
173 };
174
175 let timeout_with_backoff = match network.is_arbitrum() {
176 true => timeout, false => get_resubmit_timeout_with_backoff(timeout, tx.hashes.len()),
178 };
179
180 if age > Duration::milliseconds(timeout_with_backoff) {
181 debug!(
182 tx_id = %tx.id,
183 relayer_id = %tx.relayer_id,
184 age_ms = %age.num_milliseconds(),
185 "transaction has been pending for too long, resubmitting"
186 );
187 return Ok(true);
188 }
189 Ok(false)
190 }
191
192 pub(super) async fn should_noop(
202 &self,
203 tx: &TransactionRepoModel,
204 ) -> Result<(bool, Option<String>), TransactionError> {
205 if too_many_noop_attempts(tx) {
206 debug!("Transaction has too many NOOP attempts already");
207 return Ok((false, None));
208 }
209
210 let evm_data = tx.network_data.get_evm_transaction_data()?;
211 if is_noop(&evm_data) {
212 return Ok((false, None));
213 }
214
215 let network_model = self
216 .network_repository()
217 .get_by_chain_id(NetworkType::Evm, evm_data.chain_id)
218 .await?
219 .ok_or(TransactionError::UnexpectedError(format!(
220 "Network with chain id {} not found",
221 evm_data.chain_id
222 )))?;
223
224 let network = EvmNetwork::try_from(network_model).map_err(|e| {
225 TransactionError::UnexpectedError(format!(
226 "Error converting network model to EvmNetwork: {e}"
227 ))
228 })?;
229
230 if network.is_rollup() && too_many_attempts(tx) {
231 let reason =
232 "Rollup transaction has too many attempts. Replacing with NOOP.".to_string();
233 debug!(
234 tx_id = %tx.id,
235 relayer_id = %tx.relayer_id,
236 reason = %reason,
237 "replacing transaction with NOOP"
238 );
239 return Ok((true, Some(reason)));
240 }
241
242 if !is_transaction_valid(&tx.created_at, &tx.valid_until) {
243 let reason = "Transaction is expired. Replacing with NOOP.".to_string();
244 debug!(
245 tx_id = %tx.id,
246 relayer_id = %tx.relayer_id,
247 reason = %reason,
248 "replacing transaction with NOOP"
249 );
250 return Ok((true, Some(reason)));
251 }
252
253 if tx.status == TransactionStatus::Pending {
254 let created_at = &tx.created_at;
255 let created_time = DateTime::parse_from_rfc3339(created_at)
256 .map_err(|e| {
257 TransactionError::UnexpectedError(format!("Invalid created_at timestamp: {e}"))
258 })?
259 .with_timezone(&Utc);
260 let age = Utc::now().signed_duration_since(created_time);
261 if age > get_evm_prepare_timeout() {
262 let reason = format!(
263 "Transaction in Pending state for over {} minutes. Replacing with NOOP.",
264 get_evm_prepare_timeout().num_minutes()
265 );
266 debug!(
267 tx_id = %tx.id,
268 relayer_id = %tx.relayer_id,
269 reason = %reason,
270 "replacing transaction with NOOP"
271 );
272 return Ok((true, Some(reason)));
273 }
274 }
275
276 let latest_block = self.provider().get_block_by_number().await;
277 if let Ok(block) = latest_block {
278 let block_gas_limit = block.header.gas_limit;
279 if let Some(gas_limit) = evm_data.gas_limit {
280 if gas_limit > block_gas_limit {
281 let reason = format!(
282 "Transaction gas limit ({gas_limit}) exceeds block gas limit ({block_gas_limit}). Replacing with NOOP.",
283 );
284 warn!(
285 tx_id = %tx.id,
286 tx_gas_limit = %gas_limit,
287 block_gas_limit = %block_gas_limit,
288 "transaction gas limit exceeds block gas limit, replacing with NOOP"
289 );
290 return Ok((true, Some(reason)));
291 }
292 }
293 }
294
295 Ok((false, None))
296 }
297
298 pub(super) async fn update_transaction_status_if_needed(
300 &self,
301 tx: TransactionRepoModel,
302 new_status: TransactionStatus,
303 status_reason: Option<String>,
304 ) -> Result<TransactionRepoModel, TransactionError> {
305 if tx.status != new_status {
306 return self
307 .update_transaction_status(tx, new_status, status_reason)
308 .await;
309 }
310 Ok(tx)
311 }
312
313 pub(super) async fn prepare_noop_update_request(
315 &self,
316 tx: &TransactionRepoModel,
317 is_cancellation: bool,
318 reason: Option<String>,
319 ) -> Result<TransactionUpdateRequest, TransactionError> {
320 let mut evm_data = tx.network_data.get_evm_transaction_data()?;
321 let network_model = self
322 .network_repository()
323 .get_by_chain_id(NetworkType::Evm, evm_data.chain_id)
324 .await?
325 .ok_or(TransactionError::UnexpectedError(format!(
326 "Network with chain id {} not found",
327 evm_data.chain_id
328 )))?;
329
330 let network = EvmNetwork::try_from(network_model).map_err(|e| {
331 TransactionError::UnexpectedError(format!(
332 "Error converting network model to EvmNetwork: {e}"
333 ))
334 })?;
335
336 make_noop(&mut evm_data, &network, Some(self.provider())).await?;
337
338 let noop_count = tx.noop_count.unwrap_or(0) + 1;
339 let update_request = TransactionUpdateRequest {
340 network_data: Some(NetworkTransactionData::Evm(evm_data)),
341 noop_count: Some(noop_count),
342 status_reason: reason,
343 is_canceled: if is_cancellation {
344 Some(true)
345 } else {
346 tx.is_canceled
347 },
348 ..Default::default()
349 };
350 Ok(update_request)
351 }
352
353 async fn handle_submitted_state(
355 &self,
356 tx: TransactionRepoModel,
357 ) -> Result<TransactionRepoModel, TransactionError> {
358 if self.should_resubmit(&tx).await? {
359 let resubmitted_tx = self.handle_resubmission(tx).await?;
360 return Ok(resubmitted_tx);
361 }
362
363 self.update_transaction_status_if_needed(tx, TransactionStatus::Submitted, None)
364 .await
365 }
366
367 async fn handle_resubmission(
369 &self,
370 tx: TransactionRepoModel,
371 ) -> Result<TransactionRepoModel, TransactionError> {
372 debug!(
373 tx_id = %tx.id,
374 relayer_id = %tx.relayer_id,
375 status = ?tx.status,
376 "scheduling resubmit job for transaction"
377 );
378
379 let (should_noop, reason) = self.should_noop(&tx).await?;
381 let tx_to_process = if should_noop {
382 self.process_noop_transaction(&tx, reason).await?
383 } else {
384 tx
385 };
386
387 self.send_transaction_resubmit_job(&tx_to_process).await?;
388 Ok(tx_to_process)
389 }
390
391 async fn process_noop_transaction(
393 &self,
394 tx: &TransactionRepoModel,
395 reason: Option<String>,
396 ) -> Result<TransactionRepoModel, TransactionError> {
397 debug!(
398 tx_id = %tx.id,
399 relayer_id = %tx.relayer_id,
400 status = ?tx.status,
401 "preparing transaction NOOP before resubmission"
402 );
403 let update = self.prepare_noop_update_request(tx, false, reason).await?;
404 let updated_tx = self
405 .transaction_repository()
406 .partial_update(tx.id.clone(), update)
407 .await?;
408
409 let res = self.send_transaction_update_notification(&updated_tx).await;
410 if let Err(e) = res {
411 error!(
412 tx_id = %updated_tx.id,
413 relayer_id = %updated_tx.relayer_id,
414 status = ?updated_tx.status,
415 error = %e,
416 "sending transaction update notification failed for NOOP transaction"
417 );
418 }
419 Ok(updated_tx)
420 }
421
422 async fn handle_pending_state(
424 &self,
425 tx: TransactionRepoModel,
426 ) -> Result<TransactionRepoModel, TransactionError> {
427 let (should_noop, reason) = self.should_noop(&tx).await?;
428 if should_noop {
429 debug!(
432 tx_id = %tx.id,
433 relayer_id = %tx.relayer_id,
434 reason = %reason.as_ref().unwrap_or(&"unknown".to_string()),
435 "marking pending transaction as Failed (nonce not assigned, no NOOP needed)"
436 );
437 let update = TransactionUpdateRequest {
438 status: Some(TransactionStatus::Failed),
439 status_reason: reason,
440 ..Default::default()
441 };
442 let updated_tx = self
443 .transaction_repository()
444 .partial_update(tx.id.clone(), update)
445 .await?;
446
447 let res = self.send_transaction_update_notification(&updated_tx).await;
448 if let Err(e) = res {
449 error!(
450 tx_id = %updated_tx.id,
451 relayer_id = %updated_tx.relayer_id,
452 status = ?updated_tx.status,
453 error = %e,
454 "sending transaction update notification failed for Pending state NOOP"
455 );
456 }
457 return Ok(updated_tx);
458 }
459
460 let age = get_age_since_created(&tx)?;
462 if age > get_evm_pending_recovery_trigger_timeout() {
463 warn!(
464 tx_id = %tx.id,
465 relayer_id = %tx.relayer_id,
466 age_seconds = age.num_seconds(),
467 "transaction stuck in Pending, queuing prepare job"
468 );
469
470 self.send_transaction_request_job(&tx).await?;
472 }
473
474 Ok(tx)
475 }
476
477 async fn handle_mined_state(
479 &self,
480 tx: TransactionRepoModel,
481 ) -> Result<TransactionRepoModel, TransactionError> {
482 self.update_transaction_status_if_needed(tx, TransactionStatus::Mined, None)
483 .await
484 }
485
486 async fn handle_final_state(
488 &self,
489 tx: TransactionRepoModel,
490 status: TransactionStatus,
491 status_reason: Option<String>,
492 ) -> Result<TransactionRepoModel, TransactionError> {
493 self.update_transaction_status_if_needed(tx, status, status_reason)
494 .await
495 }
496
497 async fn mark_as_failed(
499 &self,
500 tx: TransactionRepoModel,
501 reason: String,
502 ) -> Result<TransactionRepoModel, TransactionError> {
503 warn!(
504 tx_id = %tx.id,
505 relayer_id = %tx.relayer_id,
506 reason = %reason,
507 "force-failing transaction due to circuit breaker"
508 );
509
510 let update = TransactionUpdateRequest {
511 status: Some(TransactionStatus::Failed),
512 status_reason: Some(reason),
513 ..Default::default()
514 };
515
516 let updated_tx = self
517 .transaction_repository()
518 .partial_update(tx.id.clone(), update)
519 .await?;
520
521 if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
523 error!(
524 tx_id = %updated_tx.id,
525 relayer_id = %updated_tx.relayer_id,
526 error = %e,
527 "failed to send notification for force-failed transaction"
528 );
529 }
530
531 Ok(updated_tx)
532 }
533
534 async fn handle_circuit_breaker_safely(
546 &self,
547 tx: TransactionRepoModel,
548 ctx: &StatusCheckContext,
549 ) -> Result<TransactionRepoModel, TransactionError> {
550 let reason = format!(
551 "Transaction status monitoring failed after {} consecutive errors (total: {}). \
552 Last status: {:?}.",
553 ctx.consecutive_failures, ctx.total_failures, tx.status
554 );
555
556 match tx.status {
557 TransactionStatus::Pending | TransactionStatus::Sent => {
558 debug!(
562 tx_id = %tx.id,
563 relayer_id = %tx.relayer_id,
564 status = ?tx.status,
565 "circuit breaker: transaction never broadcast - safe to mark as Failed"
566 );
567 self.mark_as_failed(tx, reason).await
568 }
569 TransactionStatus::Submitted => {
570 warn!(
576 tx_id = %tx.id,
577 relayer_id = %tx.relayer_id,
578 "circuit breaker: Submitted transaction - triggering NOOP to safely clear nonce"
579 );
580 let noop_reason = Some(format!(
581 "{reason}. Replacing with NOOP to clear nonce slot."
582 ));
583 let updated_tx = self.process_noop_transaction(&tx, noop_reason).await?;
584 self.send_transaction_resubmit_job(&updated_tx).await?;
585 Ok(updated_tx)
586 }
587 _ => {
588 debug!(
590 tx_id = %tx.id,
591 relayer_id = %tx.relayer_id,
592 status = ?tx.status,
593 "circuit breaker: unexpected status, returning transaction unchanged"
594 );
595 Ok(tx)
596 }
597 }
598 }
599
600 pub async fn handle_status_impl(
605 &self,
606 tx: TransactionRepoModel,
607 context: Option<StatusCheckContext>,
608 ) -> Result<TransactionRepoModel, TransactionError> {
609 debug!(
610 tx_id = %tx.id,
611 relayer_id = %tx.relayer_id,
612 status = ?tx.status,
613 "checking transaction status"
614 );
615
616 if is_final_state(&tx.status) {
618 debug!(
619 tx_id = %tx.id,
620 relayer_id = %tx.relayer_id,
621 status = ?tx.status,
622 "transaction already in final state"
623 );
624 return Ok(tx);
625 }
626
627 if let Some(ref ctx) = context {
631 let is_noop_tx = tx
632 .network_data
633 .get_evm_transaction_data()
634 .map(|data| is_noop(&data))
635 .unwrap_or(false);
636
637 if ctx.should_force_finalize() && !is_noop_tx {
638 warn!(
639 tx_id = %tx.id,
640 consecutive_failures = ctx.consecutive_failures,
641 total_failures = ctx.total_failures,
642 max_consecutive = ctx.max_consecutive_failures,
643 status = ?tx.status,
644 "circuit breaker triggered - handling safely based on transaction state"
645 );
646 return self.handle_circuit_breaker_safely(tx, ctx).await;
647 }
648
649 if ctx.should_force_finalize() && is_noop_tx {
650 debug!(
651 tx_id = %tx.id,
652 consecutive_failures = ctx.consecutive_failures,
653 relayer_id = %tx.relayer_id,
654 "circuit breaker would trigger but transaction is NOOP - continuing with normal status logic"
655 );
656 }
657 }
658
659 let status = self.check_transaction_status(&tx).await?;
664
665 debug!(
666 tx_id = %tx.id,
667 previous_status = ?tx.status,
668 new_status = ?status,
669 relayer_id = %tx.relayer_id,
670 "transaction status check completed"
671 );
672
673 let tx = if status != tx.status {
677 debug!(
678 tx_id = %tx.id,
679 old_status = ?tx.status,
680 new_status = ?status,
681 relayer_id = %tx.relayer_id,
682 "status changed during check, reloading transaction from DB to ensure fresh data"
683 );
684 self.transaction_repository()
685 .get_by_id(tx.id.clone())
686 .await?
687 } else {
688 tx
689 };
690
691 if is_too_early_to_resubmit(&tx)? && is_pending_transaction(&status) {
696 return self
698 .update_transaction_status_if_needed(tx, status, None)
699 .await;
700 }
701
702 match status {
704 TransactionStatus::Pending => self.handle_pending_state(tx).await,
705 TransactionStatus::Sent => self.handle_sent_state(tx).await,
706 TransactionStatus::Submitted => self.handle_submitted_state(tx).await,
707 TransactionStatus::Mined => self.handle_mined_state(tx).await,
708 TransactionStatus::Failed => {
709 let status_reason = if tx.status != TransactionStatus::Failed {
712 Some("Transaction reverted on-chain (receipt status: failed)".to_string())
713 } else {
714 None
715 };
716 self.handle_final_state(tx, status, status_reason).await
717 }
718 TransactionStatus::Confirmed
719 | TransactionStatus::Expired
720 | TransactionStatus::Canceled => self.handle_final_state(tx, status, None).await,
721 }
722 }
723
724 async fn handle_sent_state(
726 &self,
727 tx: TransactionRepoModel,
728 ) -> Result<TransactionRepoModel, TransactionError> {
729 debug!(
730 tx_id = %tx.id,
731 relayer_id = %tx.relayer_id,
732 "handling Sent state"
733 );
734
735 let (should_noop, reason) = self.should_noop(&tx).await?;
737 if should_noop {
738 debug!(
739 tx_id = %tx.id,
740 relayer_id = %tx.relayer_id,
741 "preparing NOOP for sent transaction"
742 );
743 let update = self.prepare_noop_update_request(&tx, false, reason).await?;
744 let updated_tx = self
745 .transaction_repository()
746 .partial_update(tx.id.clone(), update)
747 .await?;
748
749 self.send_transaction_submit_job(&updated_tx).await?;
750 let res = self.send_transaction_update_notification(&updated_tx).await;
751 if let Err(e) = res {
752 error!(
753 tx_id = %updated_tx.id,
754 relayer_id = %updated_tx.relayer_id,
755 status = ?updated_tx.status,
756 error = %e,
757 "sending transaction update notification failed for Sent state NOOP"
758 );
759 }
760 return Ok(updated_tx);
761 }
762
763 let age_since_sent = get_age_since_status_change(&tx)?;
766
767 if age_since_sent > get_evm_resend_timeout() {
768 warn!(
769 tx_id = %tx.id,
770 relayer_id = %tx.relayer_id,
771 age_seconds = age_since_sent.num_seconds(),
772 "transaction stuck in Sent, queuing resubmit job with repricing"
773 );
774
775 self.send_transaction_resubmit_job(&tx).await?;
777 }
778
779 self.update_transaction_status_if_needed(tx, TransactionStatus::Sent, None)
780 .await
781 }
782
783 fn should_try_hash_recovery(
790 &self,
791 tx: &TransactionRepoModel,
792 ) -> Result<bool, TransactionError> {
793 if tx.status != TransactionStatus::Submitted {
795 return Ok(false);
796 }
797
798 if tx.hashes.len() <= 1 {
800 return Ok(false);
801 }
802
803 let age = get_age_of_sent_at(tx)?;
805 let min_age_for_recovery = get_evm_min_age_for_hash_recovery();
806
807 if age < min_age_for_recovery {
808 return Ok(false);
809 }
810
811 if tx.hashes.len() < EVM_MIN_HASHES_FOR_RECOVERY {
814 return Ok(false);
815 }
816
817 Ok(true)
818 }
819
820 async fn try_recover_with_historical_hashes(
829 &self,
830 tx: &TransactionRepoModel,
831 evm_data: &crate::models::EvmTransactionData,
832 ) -> Result<Option<TransactionRepoModel>, TransactionError> {
833 warn!(
834 tx_id = %tx.id,
835 relayer_id = %tx.relayer_id,
836 current_hash = ?evm_data.hash,
837 total_hashes = %tx.hashes.len(),
838 "attempting hash recovery - checking historical hashes"
839 );
840
841 for (idx, historical_hash) in tx.hashes.iter().rev().enumerate() {
843 if Some(historical_hash) == evm_data.hash.as_ref() {
845 continue;
846 }
847
848 debug!(
849 tx_id = %tx.id,
850 relayer_id = %tx.relayer_id,
851 hash = %historical_hash,
852 index = %idx,
853 "checking historical hash"
854 );
855
856 match self
858 .provider()
859 .get_transaction_receipt(historical_hash)
860 .await
861 {
862 Ok(Some(receipt)) => {
863 warn!(
864 tx_id = %tx.id,
865 relayer_id = %tx.relayer_id,
866 mined_hash = %historical_hash,
867 wrong_hash = ?evm_data.hash,
868 block_number = ?receipt.block_number,
869 "RECOVERED: found mined transaction with historical hash - correcting database"
870 );
871
872 let updated_tx = self
875 .update_transaction_with_corrected_hash(
876 tx,
877 evm_data,
878 historical_hash,
879 TransactionStatus::Mined,
880 )
881 .await?;
882
883 return Ok(Some(updated_tx));
884 }
885 Ok(None) => {
886 continue;
888 }
889 Err(e) => {
890 warn!(
892 tx_id = %tx.id,
893 relayer_id = %tx.relayer_id,
894 hash = %historical_hash,
895 error = %e,
896 "error checking historical hash, continuing to next"
897 );
898 continue;
899 }
900 }
901 }
902
903 debug!(
905 tx_id = %tx.id,
906 relayer_id = %tx.relayer_id,
907 "hash recovery completed - no historical hashes found on-chain"
908 );
909 Ok(None)
910 }
911
912 async fn update_transaction_with_corrected_hash(
916 &self,
917 tx: &TransactionRepoModel,
918 evm_data: &crate::models::EvmTransactionData,
919 correct_hash: &str,
920 status: TransactionStatus,
921 ) -> Result<TransactionRepoModel, TransactionError> {
922 let mut corrected_data = evm_data.clone();
923 corrected_data.hash = Some(correct_hash.to_string());
924
925 let updated_tx = self
926 .transaction_repository()
927 .partial_update(
928 tx.id.clone(),
929 TransactionUpdateRequest {
930 network_data: Some(NetworkTransactionData::Evm(corrected_data)),
931 status: Some(status),
932 ..Default::default()
933 },
934 )
935 .await?;
936
937 if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
939 error!(
940 tx_id = %updated_tx.id,
941 relayer_id = %updated_tx.relayer_id,
942 error = %e,
943 "failed to send notification for hash recovery"
944 );
945 }
946
947 Ok(updated_tx)
948 }
949}
950
951#[cfg(test)]
952mod tests {
953 use crate::{
954 config::{EvmNetworkConfig, NetworkConfigCommon},
955 domain::transaction::evm::{EvmRelayerTransaction, MockPriceCalculatorTrait},
956 jobs::MockJobProducerTrait,
957 models::{
958 evm::Speed, EvmTransactionData, NetworkConfigData, NetworkRepoModel,
959 NetworkTransactionData, NetworkType, RelayerEvmPolicy, RelayerNetworkPolicy,
960 RelayerRepoModel, RpcConfig, TransactionReceipt, TransactionRepoModel,
961 TransactionStatus, U256,
962 },
963 repositories::{
964 MockNetworkRepository, MockRelayerRepository, MockTransactionCounterTrait,
965 MockTransactionRepository,
966 },
967 services::{provider::MockEvmProviderTrait, signer::MockSigner},
968 };
969 use alloy::{
970 consensus::{Eip658Value, Receipt, ReceiptWithBloom},
971 network::AnyReceiptEnvelope,
972 primitives::{b256, Address, BlockHash, Bloom, TxHash},
973 };
974 use chrono::{Duration, Utc};
975 use std::sync::Arc;
976
977 pub struct TestMocks {
979 pub provider: MockEvmProviderTrait,
980 pub relayer_repo: MockRelayerRepository,
981 pub network_repo: MockNetworkRepository,
982 pub tx_repo: MockTransactionRepository,
983 pub job_producer: MockJobProducerTrait,
984 pub signer: MockSigner,
985 pub counter: MockTransactionCounterTrait,
986 pub price_calc: MockPriceCalculatorTrait,
987 }
988
989 pub fn default_test_mocks() -> TestMocks {
992 TestMocks {
993 provider: MockEvmProviderTrait::new(),
994 relayer_repo: MockRelayerRepository::new(),
995 network_repo: MockNetworkRepository::new(),
996 tx_repo: MockTransactionRepository::new(),
997 job_producer: MockJobProducerTrait::new(),
998 signer: MockSigner::new(),
999 counter: MockTransactionCounterTrait::new(),
1000 price_calc: MockPriceCalculatorTrait::new(),
1001 }
1002 }
1003
1004 pub fn default_test_mocks_with_network() -> TestMocks {
1006 let mut mocks = default_test_mocks();
1007 mocks
1009 .network_repo
1010 .expect_get_by_chain_id()
1011 .returning(|network_type, chain_id| {
1012 if network_type == NetworkType::Evm && chain_id == 1 {
1013 Ok(Some(create_test_network_model()))
1014 } else {
1015 Ok(None)
1016 }
1017 });
1018 mocks
1019 }
1020
1021 pub fn create_test_network_model() -> NetworkRepoModel {
1023 let evm_config = EvmNetworkConfig {
1024 common: NetworkConfigCommon {
1025 network: "mainnet".to_string(),
1026 from: None,
1027 rpc_urls: Some(vec![RpcConfig::new("https://rpc.example.com".to_string())]),
1028 explorer_urls: Some(vec!["https://explorer.example.com".to_string()]),
1029 average_blocktime_ms: Some(12000),
1030 is_testnet: Some(false),
1031 tags: Some(vec!["mainnet".to_string()]),
1032 },
1033 chain_id: Some(1),
1034 required_confirmations: Some(12),
1035 features: Some(vec!["eip1559".to_string()]),
1036 symbol: Some("ETH".to_string()),
1037 gas_price_cache: None,
1038 };
1039 NetworkRepoModel {
1040 id: "evm:mainnet".to_string(),
1041 name: "mainnet".to_string(),
1042 network_type: NetworkType::Evm,
1043 config: NetworkConfigData::Evm(evm_config),
1044 }
1045 }
1046
1047 pub fn create_test_no_mempool_network_model() -> NetworkRepoModel {
1049 let evm_config = EvmNetworkConfig {
1050 common: NetworkConfigCommon {
1051 network: "arbitrum".to_string(),
1052 from: None,
1053 rpc_urls: Some(vec![crate::models::RpcConfig::new(
1054 "https://arb-rpc.example.com".to_string(),
1055 )]),
1056 explorer_urls: Some(vec!["https://arb-explorer.example.com".to_string()]),
1057 average_blocktime_ms: Some(1000),
1058 is_testnet: Some(false),
1059 tags: Some(vec![
1060 "arbitrum".to_string(),
1061 "rollup".to_string(),
1062 "no-mempool".to_string(),
1063 ]),
1064 },
1065 chain_id: Some(42161),
1066 required_confirmations: Some(12),
1067 features: Some(vec!["eip1559".to_string()]),
1068 symbol: Some("ETH".to_string()),
1069 gas_price_cache: None,
1070 };
1071 NetworkRepoModel {
1072 id: "evm:arbitrum".to_string(),
1073 name: "arbitrum".to_string(),
1074 network_type: NetworkType::Evm,
1075 config: NetworkConfigData::Evm(evm_config),
1076 }
1077 }
1078
1079 pub fn make_test_transaction(status: TransactionStatus) -> TransactionRepoModel {
1083 TransactionRepoModel {
1084 id: "test-tx-id".to_string(),
1085 relayer_id: "test-relayer-id".to_string(),
1086 status,
1087 status_reason: None,
1088 created_at: Utc::now().to_rfc3339(),
1089 sent_at: None,
1090 confirmed_at: None,
1091 valid_until: None,
1092 delete_at: None,
1093 network_type: NetworkType::Evm,
1094 network_data: NetworkTransactionData::Evm(EvmTransactionData {
1095 chain_id: 1,
1096 from: "0xSender".to_string(),
1097 to: Some("0xRecipient".to_string()),
1098 value: U256::from(0),
1099 data: Some("0xData".to_string()),
1100 gas_limit: Some(21000),
1101 gas_price: Some(20000000000),
1102 max_fee_per_gas: None,
1103 max_priority_fee_per_gas: None,
1104 nonce: None,
1105 signature: None,
1106 hash: None,
1107 speed: Some(Speed::Fast),
1108 raw: None,
1109 }),
1110 priced_at: None,
1111 hashes: Vec::new(),
1112 noop_count: None,
1113 is_canceled: Some(false),
1114 metadata: None,
1115 }
1116 }
1117
1118 pub fn make_test_evm_relayer_transaction(
1121 relayer: RelayerRepoModel,
1122 mocks: TestMocks,
1123 ) -> EvmRelayerTransaction<
1124 MockEvmProviderTrait,
1125 MockRelayerRepository,
1126 MockNetworkRepository,
1127 MockTransactionRepository,
1128 MockJobProducerTrait,
1129 MockSigner,
1130 MockTransactionCounterTrait,
1131 MockPriceCalculatorTrait,
1132 > {
1133 EvmRelayerTransaction::new(
1134 relayer,
1135 mocks.provider,
1136 Arc::new(mocks.relayer_repo),
1137 Arc::new(mocks.network_repo),
1138 Arc::new(mocks.tx_repo),
1139 Arc::new(mocks.counter),
1140 Arc::new(mocks.job_producer),
1141 mocks.price_calc,
1142 mocks.signer,
1143 )
1144 .unwrap()
1145 }
1146
1147 fn create_test_relayer() -> RelayerRepoModel {
1148 RelayerRepoModel {
1149 id: "test-relayer-id".to_string(),
1150 name: "Test Relayer".to_string(),
1151 paused: false,
1152 system_disabled: false,
1153 network: "test_network".to_string(),
1154 network_type: NetworkType::Evm,
1155 policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
1156 signer_id: "test_signer".to_string(),
1157 address: "0x".to_string(),
1158 notification_id: None,
1159 custom_rpc_urls: None,
1160 ..Default::default()
1161 }
1162 }
1163
1164 fn make_mock_receipt(status: bool, block_number: Option<u64>) -> TransactionReceipt {
1165 let tx_hash = TxHash::from(b256!(
1167 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
1168 ));
1169 let block_hash = BlockHash::from(b256!(
1170 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
1171 ));
1172 let from_address = Address::from([0x11; 20]);
1173
1174 TransactionReceipt {
1175 inner: alloy::rpc::types::TransactionReceipt {
1176 inner: AnyReceiptEnvelope {
1177 inner: ReceiptWithBloom {
1178 receipt: Receipt {
1179 status: Eip658Value::Eip658(status), cumulative_gas_used: 0,
1181 logs: vec![],
1182 },
1183 logs_bloom: Bloom::ZERO,
1184 },
1185 r#type: 0, },
1187 transaction_hash: tx_hash,
1188 transaction_index: Some(0),
1189 block_hash: block_number.map(|_| block_hash), block_number,
1191 gas_used: 21000,
1192 effective_gas_price: 1000,
1193 blob_gas_used: None,
1194 blob_gas_price: None,
1195 from: from_address,
1196 to: None,
1197 contract_address: None,
1198 },
1199 other: Default::default(),
1200 }
1201 }
1202
1203 mod check_transaction_status_tests {
1205 use super::*;
1206
1207 #[tokio::test]
1208 async fn test_not_mined() {
1209 let mut mocks = default_test_mocks();
1210 let relayer = create_test_relayer();
1211 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1212
1213 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1215 evm_data.hash = Some("0xFakeHash".to_string());
1216 }
1217
1218 mocks
1220 .provider
1221 .expect_get_transaction_receipt()
1222 .returning(|_| Box::pin(async { Ok(None) }));
1223
1224 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1225
1226 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
1227 assert_eq!(status, TransactionStatus::Submitted);
1228 }
1229
1230 #[tokio::test]
1231 async fn test_mined_but_not_confirmed() {
1232 let mut mocks = default_test_mocks();
1233 let relayer = create_test_relayer();
1234 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1235
1236 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1237 evm_data.hash = Some("0xFakeHash".to_string());
1238 }
1239
1240 mocks
1242 .provider
1243 .expect_get_transaction_receipt()
1244 .returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) }));
1245
1246 mocks
1248 .provider
1249 .expect_get_block_number()
1250 .return_once(|| Box::pin(async { Ok(100) }));
1251
1252 mocks
1254 .network_repo
1255 .expect_get_by_chain_id()
1256 .returning(|_, _| Ok(Some(create_test_network_model())));
1257
1258 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1259
1260 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
1261 assert_eq!(status, TransactionStatus::Mined);
1262 }
1263
1264 #[tokio::test]
1265 async fn test_confirmed() {
1266 let mut mocks = default_test_mocks();
1267 let relayer = create_test_relayer();
1268 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1269
1270 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1271 evm_data.hash = Some("0xFakeHash".to_string());
1272 }
1273
1274 mocks
1276 .provider
1277 .expect_get_transaction_receipt()
1278 .returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) }));
1279
1280 mocks
1282 .provider
1283 .expect_get_block_number()
1284 .return_once(|| Box::pin(async { Ok(113) }));
1285
1286 mocks
1288 .network_repo
1289 .expect_get_by_chain_id()
1290 .returning(|_, _| Ok(Some(create_test_network_model())));
1291
1292 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1293
1294 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
1295 assert_eq!(status, TransactionStatus::Confirmed);
1296 }
1297
1298 #[tokio::test]
1299 async fn test_failed() {
1300 let mut mocks = default_test_mocks();
1301 let relayer = create_test_relayer();
1302 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1303
1304 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1305 evm_data.hash = Some("0xFakeHash".to_string());
1306 }
1307
1308 mocks
1310 .provider
1311 .expect_get_transaction_receipt()
1312 .returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(false, Some(100)))) }));
1313
1314 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1315
1316 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
1317 assert_eq!(status, TransactionStatus::Failed);
1318 }
1319 }
1320
1321 mod should_resubmit_tests {
1323 use super::*;
1324 use crate::models::TransactionError;
1325
1326 #[tokio::test]
1327 async fn test_should_resubmit_true() {
1328 let mut mocks = default_test_mocks();
1329 let relayer = create_test_relayer();
1330
1331 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1333 tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
1334
1335 mocks
1337 .network_repo
1338 .expect_get_by_chain_id()
1339 .returning(|_, _| Ok(Some(create_test_network_model())));
1340
1341 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1342 let res = evm_transaction.should_resubmit(&tx).await.unwrap();
1343 assert!(res, "Transaction should be resubmitted after timeout.");
1344 }
1345
1346 #[tokio::test]
1347 async fn test_should_resubmit_false() {
1348 let mut mocks = default_test_mocks();
1349 let relayer = create_test_relayer();
1350
1351 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1353 tx.sent_at = Some(Utc::now().to_rfc3339());
1354
1355 mocks
1357 .network_repo
1358 .expect_get_by_chain_id()
1359 .returning(|_, _| Ok(Some(create_test_network_model())));
1360
1361 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1362 let res = evm_transaction.should_resubmit(&tx).await.unwrap();
1363 assert!(!res, "Transaction should not be resubmitted immediately.");
1364 }
1365
1366 #[tokio::test]
1367 async fn test_should_resubmit_true_for_no_mempool_network() {
1368 let mut mocks = default_test_mocks();
1369 let relayer = create_test_relayer();
1370
1371 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1373 tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
1374
1375 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1377 evm_data.chain_id = 42161; }
1379
1380 mocks
1382 .network_repo
1383 .expect_get_by_chain_id()
1384 .returning(|_, _| Ok(Some(create_test_no_mempool_network_model())));
1385
1386 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1387 let res = evm_transaction.should_resubmit(&tx).await.unwrap();
1388 assert!(
1389 res,
1390 "Transaction should be resubmitted for no-mempool networks."
1391 );
1392 }
1393
1394 #[tokio::test]
1395 async fn test_should_resubmit_network_not_found() {
1396 let mut mocks = default_test_mocks();
1397 let relayer = create_test_relayer();
1398
1399 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1400 tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
1401
1402 mocks
1404 .network_repo
1405 .expect_get_by_chain_id()
1406 .returning(|_, _| Ok(None));
1407
1408 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1409 let result = evm_transaction.should_resubmit(&tx).await;
1410
1411 assert!(
1412 result.is_err(),
1413 "should_resubmit should return error when network not found"
1414 );
1415 let error = result.unwrap_err();
1416 match error {
1417 TransactionError::UnexpectedError(msg) => {
1418 assert!(msg.contains("Network with chain id 1 not found"));
1419 }
1420 _ => panic!("Expected UnexpectedError for network not found"),
1421 }
1422 }
1423
1424 #[tokio::test]
1425 async fn test_should_resubmit_network_conversion_error() {
1426 let mut mocks = default_test_mocks();
1427 let relayer = create_test_relayer();
1428
1429 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1430 tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
1431
1432 let invalid_evm_config = EvmNetworkConfig {
1434 common: NetworkConfigCommon {
1435 network: "invalid-network".to_string(),
1436 from: None,
1437 rpc_urls: Some(vec![crate::models::RpcConfig::new(
1438 "https://rpc.example.com".to_string(),
1439 )]),
1440 explorer_urls: Some(vec!["https://explorer.example.com".to_string()]),
1441 average_blocktime_ms: Some(12000),
1442 is_testnet: Some(false),
1443 tags: Some(vec!["testnet".to_string()]),
1444 },
1445 chain_id: None, required_confirmations: Some(12),
1447 features: Some(vec!["eip1559".to_string()]),
1448 symbol: Some("ETH".to_string()),
1449 gas_price_cache: None,
1450 };
1451 let invalid_network = NetworkRepoModel {
1452 id: "evm:invalid".to_string(),
1453 name: "invalid-network".to_string(),
1454 network_type: NetworkType::Evm,
1455 config: NetworkConfigData::Evm(invalid_evm_config),
1456 };
1457
1458 mocks
1460 .network_repo
1461 .expect_get_by_chain_id()
1462 .returning(move |_, _| Ok(Some(invalid_network.clone())));
1463
1464 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1465 let result = evm_transaction.should_resubmit(&tx).await;
1466
1467 assert!(
1468 result.is_err(),
1469 "should_resubmit should return error when network conversion fails"
1470 );
1471 let error = result.unwrap_err();
1472 match error {
1473 TransactionError::UnexpectedError(msg) => {
1474 assert!(msg.contains("Error converting network model to EvmNetwork"));
1475 }
1476 _ => panic!("Expected UnexpectedError for network conversion failure"),
1477 }
1478 }
1479 }
1480
1481 mod should_noop_tests {
1483 use super::*;
1484
1485 #[tokio::test]
1486 async fn test_expired_transaction_triggers_noop() {
1487 let mut mocks = default_test_mocks();
1488 let relayer = create_test_relayer();
1489
1490 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1491 tx.valid_until = Some((Utc::now() - Duration::seconds(10)).to_rfc3339());
1493
1494 mocks
1496 .network_repo
1497 .expect_get_by_chain_id()
1498 .returning(|_, _| Ok(Some(create_test_network_model())));
1499
1500 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1501 let (res, reason) = evm_transaction.should_noop(&tx).await.unwrap();
1502 assert!(res, "Expired transaction should be replaced with a NOOP.");
1503 assert!(
1504 reason.is_some(),
1505 "Reason should be provided for expired transaction"
1506 );
1507 assert!(
1508 reason.unwrap().contains("expired"),
1509 "Reason should mention expiration"
1510 );
1511 }
1512
1513 #[tokio::test]
1514 async fn test_too_many_noop_attempts_returns_false() {
1515 let mocks = default_test_mocks();
1516 let relayer = create_test_relayer();
1517
1518 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1519 tx.noop_count = Some(51); let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1522 let (res, reason) = evm_transaction.should_noop(&tx).await.unwrap();
1523 assert!(
1524 !res,
1525 "Transaction with too many NOOP attempts should not be replaced."
1526 );
1527 assert!(
1528 reason.is_none(),
1529 "Reason should not be provided when should_noop is false"
1530 );
1531 }
1532
1533 #[tokio::test]
1534 async fn test_already_noop_returns_false() {
1535 let mut mocks = default_test_mocks();
1536 let relayer = create_test_relayer();
1537
1538 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1539 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1541 evm_data.to = None;
1542 evm_data.value = U256::from(0);
1543 }
1544
1545 mocks
1546 .network_repo
1547 .expect_get_by_chain_id()
1548 .returning(|_, _| Ok(Some(create_test_network_model())));
1549
1550 mocks.provider.expect_get_block_by_number().returning(|| {
1552 Box::pin(async {
1553 use alloy::{network::AnyRpcBlock, rpc::types::Block};
1554 let mut block: Block = Block::default();
1555 block.header.gas_limit = 30_000_000u64;
1556 Ok(AnyRpcBlock::from(block))
1557 })
1558 });
1559
1560 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1561 let (res, reason) = evm_transaction.should_noop(&tx).await.unwrap();
1562 assert!(
1563 !res,
1564 "Transaction that is already a NOOP should not be replaced."
1565 );
1566 assert!(
1567 reason.is_none(),
1568 "Reason should not be provided when should_noop is false"
1569 );
1570 }
1571
1572 #[tokio::test]
1573 async fn test_rollup_with_too_many_attempts_triggers_noop() {
1574 let mut mocks = default_test_mocks();
1575 let relayer = create_test_relayer();
1576
1577 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1578 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1580 evm_data.chain_id = 42161; }
1582 tx.hashes = vec!["0xHash1".to_string(); 51];
1584
1585 mocks
1587 .network_repo
1588 .expect_get_by_chain_id()
1589 .returning(|_, _| Ok(Some(create_test_no_mempool_network_model())));
1590
1591 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1592 let (res, reason) = evm_transaction.should_noop(&tx).await.unwrap();
1593 assert!(
1594 res,
1595 "Rollup transaction with too many attempts should be replaced with NOOP."
1596 );
1597 assert!(
1598 reason.is_some(),
1599 "Reason should be provided for rollup transaction"
1600 );
1601 assert!(
1602 reason.unwrap().contains("too many attempts"),
1603 "Reason should mention too many attempts"
1604 );
1605 }
1606
1607 #[tokio::test]
1608 async fn test_pending_state_timeout_triggers_noop() {
1609 let mut mocks = default_test_mocks();
1610 let relayer = create_test_relayer();
1611
1612 let mut tx = make_test_transaction(TransactionStatus::Pending);
1613 tx.created_at = (Utc::now() - Duration::minutes(3)).to_rfc3339();
1615
1616 mocks
1617 .network_repo
1618 .expect_get_by_chain_id()
1619 .returning(|_, _| Ok(Some(create_test_network_model())));
1620
1621 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1622 let (res, reason) = evm_transaction.should_noop(&tx).await.unwrap();
1623 assert!(
1624 res,
1625 "Pending transaction stuck for >2 minutes should be replaced with NOOP."
1626 );
1627 assert!(
1628 reason.is_some(),
1629 "Reason should be provided for pending timeout"
1630 );
1631 assert!(
1632 reason.unwrap().contains("Pending state"),
1633 "Reason should mention Pending state"
1634 );
1635 }
1636
1637 #[tokio::test]
1638 async fn test_valid_transaction_returns_false() {
1639 let mut mocks = default_test_mocks();
1640 let relayer = create_test_relayer();
1641
1642 let tx = make_test_transaction(TransactionStatus::Submitted);
1643 mocks
1646 .network_repo
1647 .expect_get_by_chain_id()
1648 .returning(|_, _| Ok(Some(create_test_network_model())));
1649
1650 mocks.provider.expect_get_block_by_number().returning(|| {
1652 Box::pin(async {
1653 use alloy::{network::AnyRpcBlock, rpc::types::Block};
1654 let mut block: Block = Block::default();
1655 block.header.gas_limit = 30_000_000u64;
1656 Ok(AnyRpcBlock::from(block))
1657 })
1658 });
1659
1660 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1661 let (res, reason) = evm_transaction.should_noop(&tx).await.unwrap();
1662 assert!(!res, "Valid transaction should not be replaced with NOOP.");
1663 assert!(
1664 reason.is_none(),
1665 "Reason should not be provided when should_noop is false"
1666 );
1667 }
1668 }
1669
1670 mod update_transaction_status_tests {
1672 use super::*;
1673
1674 #[tokio::test]
1675 async fn test_no_update_when_status_is_same() {
1676 let mocks = default_test_mocks();
1678 let relayer = create_test_relayer();
1679 let tx = make_test_transaction(TransactionStatus::Submitted);
1680 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1681
1682 let updated_tx = evm_transaction
1685 .update_transaction_status_if_needed(tx.clone(), TransactionStatus::Submitted, None)
1686 .await
1687 .unwrap();
1688 assert_eq!(updated_tx.status, TransactionStatus::Submitted);
1689 assert_eq!(updated_tx.id, tx.id);
1690 }
1691
1692 #[tokio::test]
1693 async fn test_updates_when_status_differs() {
1694 let mut mocks = default_test_mocks();
1695 let relayer = create_test_relayer();
1696 let tx = make_test_transaction(TransactionStatus::Submitted);
1697
1698 mocks
1700 .tx_repo
1701 .expect_partial_update()
1702 .returning(|_, update| {
1703 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1704 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1705 Ok(updated_tx)
1706 });
1707
1708 mocks
1710 .job_producer
1711 .expect_produce_send_notification_job()
1712 .returning(|_, _| Box::pin(async { Ok(()) }));
1713
1714 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1715 let updated_tx = evm_transaction
1716 .update_transaction_status_if_needed(tx.clone(), TransactionStatus::Mined, None)
1717 .await
1718 .unwrap();
1719
1720 assert_eq!(updated_tx.status, TransactionStatus::Mined);
1721 }
1722
1723 #[tokio::test]
1724 async fn test_updates_with_status_reason() {
1725 let mut mocks = default_test_mocks();
1726 let relayer = create_test_relayer();
1727 let tx = make_test_transaction(TransactionStatus::Submitted);
1728
1729 mocks
1730 .tx_repo
1731 .expect_partial_update()
1732 .withf(|_, update| {
1733 update.status == Some(TransactionStatus::Failed)
1734 && update.status_reason == Some("Transaction reverted on-chain".to_string())
1735 })
1736 .returning(|_, update| {
1737 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1738 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1739 updated_tx.status_reason = update.status_reason.clone();
1740 Ok(updated_tx)
1741 });
1742
1743 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1744 let updated_tx = evm_transaction
1745 .update_transaction_status_if_needed(
1746 tx.clone(),
1747 TransactionStatus::Failed,
1748 Some("Transaction reverted on-chain".to_string()),
1749 )
1750 .await
1751 .unwrap();
1752
1753 assert_eq!(updated_tx.status, TransactionStatus::Failed);
1754 assert_eq!(
1755 updated_tx.status_reason.as_deref(),
1756 Some("Transaction reverted on-chain")
1757 );
1758 }
1759 }
1760
1761 mod handle_sent_state_tests {
1763 use super::*;
1764
1765 #[tokio::test]
1766 async fn test_sent_state_recent_no_resend() {
1767 let mut mocks = default_test_mocks();
1768 let relayer = create_test_relayer();
1769
1770 let mut tx = make_test_transaction(TransactionStatus::Sent);
1771 tx.sent_at = Some((Utc::now() - Duration::seconds(10)).to_rfc3339());
1773
1774 mocks
1776 .network_repo
1777 .expect_get_by_chain_id()
1778 .returning(|_, _| Ok(Some(create_test_network_model())));
1779
1780 mocks.provider.expect_get_block_by_number().returning(|| {
1782 Box::pin(async {
1783 use alloy::{network::AnyRpcBlock, rpc::types::Block};
1784 let mut block: Block = Block::default();
1785 block.header.gas_limit = 30_000_000u64;
1786 Ok(AnyRpcBlock::from(block))
1787 })
1788 });
1789
1790 mocks
1792 .job_producer
1793 .expect_produce_check_transaction_status_job()
1794 .returning(|_, _| Box::pin(async { Ok(()) }));
1795
1796 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1797 let result = evm_transaction.handle_sent_state(tx.clone()).await.unwrap();
1798
1799 assert_eq!(result.status, TransactionStatus::Sent);
1800 }
1801
1802 #[tokio::test]
1803 async fn test_sent_state_stuck_schedules_resubmit() {
1804 let mut mocks = default_test_mocks();
1805 let relayer = create_test_relayer();
1806
1807 let mut tx = make_test_transaction(TransactionStatus::Sent);
1808 tx.sent_at = Some((Utc::now() - Duration::seconds(60)).to_rfc3339());
1810
1811 mocks
1813 .network_repo
1814 .expect_get_by_chain_id()
1815 .returning(|_, _| Ok(Some(create_test_network_model())));
1816
1817 mocks.provider.expect_get_block_by_number().returning(|| {
1819 Box::pin(async {
1820 use alloy::{network::AnyRpcBlock, rpc::types::Block};
1821 let mut block: Block = Block::default();
1822 block.header.gas_limit = 30_000_000u64;
1823 Ok(AnyRpcBlock::from(block))
1824 })
1825 });
1826
1827 mocks
1829 .job_producer
1830 .expect_produce_submit_transaction_job()
1831 .returning(|_, _| Box::pin(async { Ok(()) }));
1832
1833 mocks
1835 .job_producer
1836 .expect_produce_check_transaction_status_job()
1837 .returning(|_, _| Box::pin(async { Ok(()) }));
1838
1839 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1840 let result = evm_transaction.handle_sent_state(tx.clone()).await.unwrap();
1841
1842 assert_eq!(result.status, TransactionStatus::Sent);
1843 }
1844 }
1845
1846 mod prepare_noop_update_request_tests {
1848 use super::*;
1849
1850 #[tokio::test]
1851 async fn test_noop_request_without_cancellation() {
1852 let mocks = default_test_mocks_with_network();
1854 let relayer = create_test_relayer();
1855 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1856 tx.noop_count = Some(2);
1857 tx.is_canceled = Some(false);
1858
1859 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1860 let update_req = evm_transaction
1861 .prepare_noop_update_request(&tx, false, None)
1862 .await
1863 .unwrap();
1864
1865 assert_eq!(update_req.noop_count, Some(3));
1867 assert_eq!(update_req.is_canceled, Some(false));
1869 }
1870
1871 #[tokio::test]
1872 async fn test_noop_request_with_cancellation() {
1873 let mocks = default_test_mocks_with_network();
1875 let relayer = create_test_relayer();
1876 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1877 tx.noop_count = None;
1878 tx.is_canceled = Some(false);
1879
1880 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1881 let update_req = evm_transaction
1882 .prepare_noop_update_request(&tx, true, None)
1883 .await
1884 .unwrap();
1885
1886 assert_eq!(update_req.noop_count, Some(1));
1888 assert_eq!(update_req.is_canceled, Some(true));
1890 }
1891 }
1892
1893 mod handle_submitted_state_tests {
1895 use super::*;
1896
1897 #[tokio::test]
1898 async fn test_schedules_resubmit_job() {
1899 let mut mocks = default_test_mocks();
1900 let relayer = create_test_relayer();
1901
1902 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1904 tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
1905
1906 mocks
1908 .network_repo
1909 .expect_get_by_chain_id()
1910 .returning(|_, _| Ok(Some(create_test_network_model())));
1911
1912 mocks.provider.expect_get_block_by_number().returning(|| {
1914 Box::pin(async {
1915 use alloy::{network::AnyRpcBlock, rpc::types::Block};
1916 let mut block: Block = Block::default();
1917 block.header.gas_limit = 30_000_000u64;
1918 Ok(AnyRpcBlock::from(block))
1919 })
1920 });
1921
1922 mocks
1924 .job_producer
1925 .expect_produce_submit_transaction_job()
1926 .returning(|_, _| Box::pin(async { Ok(()) }));
1927
1928 mocks
1930 .job_producer
1931 .expect_produce_check_transaction_status_job()
1932 .returning(|_, _| Box::pin(async { Ok(()) }));
1933
1934 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1935 let updated_tx = evm_transaction.handle_submitted_state(tx).await.unwrap();
1936
1937 assert_eq!(updated_tx.status, TransactionStatus::Submitted);
1939 }
1940 }
1941
1942 mod handle_pending_state_tests {
1944 use super::*;
1945
1946 #[tokio::test]
1947 async fn test_pending_state_no_noop() {
1948 let mut mocks = default_test_mocks();
1950 let relayer = create_test_relayer();
1951 let mut tx = make_test_transaction(TransactionStatus::Pending);
1952 tx.created_at = Utc::now().to_rfc3339(); mocks
1956 .network_repo
1957 .expect_get_by_chain_id()
1958 .returning(|_, _| Ok(Some(create_test_network_model())));
1959
1960 mocks.provider.expect_get_block_by_number().returning(|| {
1962 Box::pin(async {
1963 use alloy::{network::AnyRpcBlock, rpc::types::Block};
1964 let mut block: Block = Block::default();
1965 block.header.gas_limit = 30_000_000u64;
1966 Ok(AnyRpcBlock::from(block))
1967 })
1968 });
1969
1970 mocks
1972 .job_producer
1973 .expect_produce_check_transaction_status_job()
1974 .returning(|_, _| Box::pin(async { Ok(()) }));
1975
1976 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1977 let result = evm_transaction
1978 .handle_pending_state(tx.clone())
1979 .await
1980 .unwrap();
1981
1982 assert_eq!(result.id, tx.id);
1984 assert_eq!(result.status, tx.status);
1985 assert_eq!(result.noop_count, tx.noop_count);
1986 }
1987
1988 #[tokio::test]
1989 async fn test_pending_state_with_noop() {
1990 let mut mocks = default_test_mocks();
1992 let relayer = create_test_relayer();
1993 let mut tx = make_test_transaction(TransactionStatus::Pending);
1994 tx.created_at = (Utc::now() - Duration::minutes(2)).to_rfc3339();
1995
1996 mocks
1998 .network_repo
1999 .expect_get_by_chain_id()
2000 .returning(|_, _| Ok(Some(create_test_network_model())));
2001
2002 mocks.provider.expect_get_block_by_number().returning(|| {
2004 Box::pin(async {
2005 use alloy::{network::AnyRpcBlock, rpc::types::Block};
2006 let mut block: Block = Block::default();
2007 block.header.gas_limit = 30_000_000u64;
2008 Ok(AnyRpcBlock::from(block))
2009 })
2010 });
2011
2012 let tx_clone = tx.clone();
2015 mocks
2016 .tx_repo
2017 .expect_partial_update()
2018 .withf(move |id, update| {
2019 id == "test-tx-id"
2020 && update.status == Some(TransactionStatus::Failed)
2021 && update.status_reason.is_some()
2022 })
2023 .returning(move |_, update| {
2024 let mut updated_tx = tx_clone.clone();
2025 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2026 updated_tx.status_reason = update.status_reason.clone();
2027 Ok(updated_tx)
2028 });
2029 mocks
2031 .job_producer
2032 .expect_produce_send_notification_job()
2033 .returning(|_, _| Box::pin(async { Ok(()) }));
2034
2035 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2036 let result = evm_transaction
2037 .handle_pending_state(tx.clone())
2038 .await
2039 .unwrap();
2040
2041 assert_eq!(result.status, TransactionStatus::Failed);
2043 assert!(result.status_reason.is_some());
2044 assert!(result.status_reason.unwrap().contains("Pending state"));
2045 }
2046 }
2047
2048 mod handle_mined_state_tests {
2050 use super::*;
2051
2052 #[tokio::test]
2053 async fn test_updates_status_and_schedules_check() {
2054 let mut mocks = default_test_mocks();
2055 let relayer = create_test_relayer();
2056 let tx = make_test_transaction(TransactionStatus::Submitted);
2058
2059 mocks
2061 .job_producer
2062 .expect_produce_check_transaction_status_job()
2063 .returning(|_, _| Box::pin(async { Ok(()) }));
2064 mocks
2066 .tx_repo
2067 .expect_partial_update()
2068 .returning(|_, update| {
2069 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2070 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2071 Ok(updated_tx)
2072 });
2073
2074 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2075 let result = evm_transaction
2076 .handle_mined_state(tx.clone())
2077 .await
2078 .unwrap();
2079 assert_eq!(result.status, TransactionStatus::Mined);
2080 }
2081 }
2082
2083 mod handle_final_state_tests {
2085 use super::*;
2086
2087 #[tokio::test]
2088 async fn test_final_state_confirmed() {
2089 let mut mocks = default_test_mocks();
2090 let relayer = create_test_relayer();
2091 let tx = make_test_transaction(TransactionStatus::Submitted);
2092
2093 mocks
2095 .tx_repo
2096 .expect_partial_update()
2097 .returning(|_, update| {
2098 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2099 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2100 Ok(updated_tx)
2101 });
2102
2103 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2104 let result = evm_transaction
2105 .handle_final_state(tx.clone(), TransactionStatus::Confirmed, None)
2106 .await
2107 .unwrap();
2108 assert_eq!(result.status, TransactionStatus::Confirmed);
2109 }
2110
2111 #[tokio::test]
2112 async fn test_final_state_failed() {
2113 let mut mocks = default_test_mocks();
2114 let relayer = create_test_relayer();
2115 let tx = make_test_transaction(TransactionStatus::Submitted);
2116
2117 mocks
2119 .tx_repo
2120 .expect_partial_update()
2121 .returning(|_, update| {
2122 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2123 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2124 updated_tx.status_reason = update.status_reason.clone();
2125 Ok(updated_tx)
2126 });
2127
2128 let reason = "Transaction reverted on-chain (receipt status: failed)".to_string();
2129 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2130 let result = evm_transaction
2131 .handle_final_state(tx.clone(), TransactionStatus::Failed, Some(reason.clone()))
2132 .await
2133 .unwrap();
2134 assert_eq!(result.status, TransactionStatus::Failed);
2135 assert_eq!(result.status_reason.as_deref(), Some(reason.as_str()));
2136 }
2137
2138 #[tokio::test]
2139 async fn test_final_state_expired() {
2140 let mut mocks = default_test_mocks();
2141 let relayer = create_test_relayer();
2142 let tx = make_test_transaction(TransactionStatus::Submitted);
2143
2144 mocks
2146 .tx_repo
2147 .expect_partial_update()
2148 .returning(|_, update| {
2149 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2150 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2151 Ok(updated_tx)
2152 });
2153
2154 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2155 let result = evm_transaction
2156 .handle_final_state(tx.clone(), TransactionStatus::Expired, None)
2157 .await
2158 .unwrap();
2159 assert_eq!(result.status, TransactionStatus::Expired);
2160 }
2161 }
2162
2163 mod handle_status_impl_tests {
2165 use super::*;
2166
2167 #[tokio::test]
2168 async fn test_impl_submitted_branch() {
2169 let mut mocks = default_test_mocks();
2170 let relayer = create_test_relayer();
2171 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2172 tx.sent_at = Some((Utc::now() - Duration::seconds(120)).to_rfc3339());
2173 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
2175 evm_data.hash = Some("0xFakeHash".to_string());
2176 }
2177 mocks
2179 .provider
2180 .expect_get_transaction_receipt()
2181 .returning(|_| Box::pin(async { Ok(None) }));
2182 mocks
2184 .network_repo
2185 .expect_get_by_chain_id()
2186 .returning(|_, _| Ok(Some(create_test_network_model())));
2187 mocks
2189 .job_producer
2190 .expect_produce_check_transaction_status_job()
2191 .returning(|_, _| Box::pin(async { Ok(()) }));
2192 mocks
2194 .tx_repo
2195 .expect_partial_update()
2196 .returning(|_, update| {
2197 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2198 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2199 Ok(updated_tx)
2200 });
2201
2202 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2203 let result = evm_transaction.handle_status_impl(tx, None).await.unwrap();
2204 assert_eq!(result.status, TransactionStatus::Submitted);
2205 }
2206
2207 #[tokio::test]
2208 async fn test_impl_mined_branch() {
2209 let mut mocks = default_test_mocks();
2210 let relayer = create_test_relayer();
2211 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2212 tx.created_at = (Utc::now() - Duration::minutes(1)).to_rfc3339();
2214 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
2216 evm_data.hash = Some("0xFakeHash".to_string());
2217 }
2218 mocks
2220 .provider
2221 .expect_get_transaction_receipt()
2222 .returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) }));
2223 mocks
2225 .provider
2226 .expect_get_block_number()
2227 .return_once(|| Box::pin(async { Ok(100) }));
2228 mocks
2230 .network_repo
2231 .expect_get_by_chain_id()
2232 .returning(|_, _| Ok(Some(create_test_network_model())));
2233 mocks
2235 .job_producer
2236 .expect_produce_send_notification_job()
2237 .returning(|_, _| Box::pin(async { Ok(()) }));
2238 mocks.tx_repo.expect_get_by_id().returning(|_| {
2240 let updated_tx = make_test_transaction(TransactionStatus::Mined);
2241 Ok(updated_tx)
2242 });
2243 mocks
2245 .tx_repo
2246 .expect_partial_update()
2247 .returning(|_, update| {
2248 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2249 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2250 Ok(updated_tx)
2251 });
2252
2253 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2254 let result = evm_transaction.handle_status_impl(tx, None).await.unwrap();
2255 assert_eq!(result.status, TransactionStatus::Mined);
2256 }
2257
2258 #[tokio::test]
2259 async fn test_impl_final_confirmed_branch() {
2260 let mut mocks = default_test_mocks();
2261 let relayer = create_test_relayer();
2262 let tx = make_test_transaction(TransactionStatus::Confirmed);
2264
2265 mocks
2268 .tx_repo
2269 .expect_partial_update()
2270 .returning(|_, update| {
2271 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2272 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2273 Ok(updated_tx)
2274 });
2275
2276 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2277 let result = evm_transaction.handle_status_impl(tx, None).await.unwrap();
2278 assert_eq!(result.status, TransactionStatus::Confirmed);
2279 }
2280
2281 #[tokio::test]
2282 async fn test_impl_final_failed_branch() {
2283 let mut mocks = default_test_mocks();
2284 let relayer = create_test_relayer();
2285 let tx = make_test_transaction(TransactionStatus::Failed);
2287
2288 mocks
2289 .tx_repo
2290 .expect_partial_update()
2291 .returning(|_, update| {
2292 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2293 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2294 Ok(updated_tx)
2295 });
2296
2297 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2298 let result = evm_transaction.handle_status_impl(tx, None).await.unwrap();
2299 assert_eq!(result.status, TransactionStatus::Failed);
2300 }
2301
2302 #[tokio::test]
2305 async fn test_impl_submitted_to_failed_sets_status_reason() {
2306 let mut mocks = default_test_mocks();
2307 let relayer = create_test_relayer();
2308 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2309 tx.created_at = (Utc::now() - Duration::minutes(1)).to_rfc3339();
2310 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
2311 evm_data.hash = Some("0xFakeHash".to_string());
2312 }
2313
2314 mocks
2316 .provider
2317 .expect_get_transaction_receipt()
2318 .returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(false, Some(100)))) }));
2319
2320 let tx_clone = tx.clone();
2322 mocks.tx_repo.expect_get_by_id().returning(move |_| {
2323 let mut reloaded = tx_clone.clone();
2324 reloaded.status = TransactionStatus::Submitted;
2325 Ok(reloaded)
2326 });
2327
2328 mocks
2330 .tx_repo
2331 .expect_partial_update()
2332 .withf(|_, update| {
2333 update.status == Some(TransactionStatus::Failed)
2334 && update.status_reason.is_some()
2335 })
2336 .returning(|_, update| {
2337 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2338 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2339 updated_tx.status_reason = update.status_reason.clone();
2340 Ok(updated_tx)
2341 });
2342
2343 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2344 let result = evm_transaction.handle_status_impl(tx, None).await.unwrap();
2345 assert_eq!(result.status, TransactionStatus::Failed);
2346 assert!(result.status_reason.is_some());
2347 assert!(
2348 result
2349 .status_reason
2350 .as_ref()
2351 .unwrap()
2352 .contains("reverted on-chain"),
2353 "Expected on-chain revert reason, got: {:?}",
2354 result.status_reason
2355 );
2356 }
2357
2358 #[tokio::test]
2359 async fn test_impl_final_expired_branch() {
2360 let mut mocks = default_test_mocks();
2361 let relayer = create_test_relayer();
2362 let tx = make_test_transaction(TransactionStatus::Expired);
2364
2365 mocks
2366 .tx_repo
2367 .expect_partial_update()
2368 .returning(|_, update| {
2369 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2370 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2371 Ok(updated_tx)
2372 });
2373
2374 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2375 let result = evm_transaction.handle_status_impl(tx, None).await.unwrap();
2376 assert_eq!(result.status, TransactionStatus::Expired);
2377 }
2378 }
2379
2380 mod circuit_breaker_tests {
2382 use super::*;
2383 use crate::jobs::StatusCheckContext;
2384
2385 fn create_triggered_context() -> StatusCheckContext {
2387 StatusCheckContext::new(
2388 30, 50, 60, 25, 75, NetworkType::Evm,
2394 )
2395 }
2396
2397 fn create_safe_context() -> StatusCheckContext {
2399 StatusCheckContext::new(
2400 5, 10, 15, 25, 75, NetworkType::Evm,
2406 )
2407 }
2408
2409 fn create_total_triggered_context() -> StatusCheckContext {
2411 StatusCheckContext::new(
2412 5, 80, 100, 25, 75, NetworkType::Evm,
2418 )
2419 }
2420
2421 #[tokio::test]
2422 async fn test_circuit_breaker_pending_marks_as_failed() {
2423 let mut mocks = default_test_mocks();
2424 let relayer = create_test_relayer();
2425 let tx = make_test_transaction(TransactionStatus::Pending);
2426
2427 mocks
2429 .tx_repo
2430 .expect_partial_update()
2431 .withf(|_, update| update.status == Some(TransactionStatus::Failed))
2432 .returning(|_, update| {
2433 let mut updated_tx = make_test_transaction(TransactionStatus::Pending);
2434 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2435 updated_tx.status_reason = update.status_reason.clone();
2436 Ok(updated_tx)
2437 });
2438
2439 mocks
2441 .job_producer
2442 .expect_produce_send_notification_job()
2443 .returning(|_, _| Box::pin(async { Ok(()) }));
2444
2445 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2446 let ctx = create_triggered_context();
2447
2448 let result = evm_transaction
2449 .handle_status_impl(tx, Some(ctx))
2450 .await
2451 .unwrap();
2452
2453 assert_eq!(result.status, TransactionStatus::Failed);
2454 assert!(result.status_reason.is_some());
2455 assert!(result.status_reason.unwrap().contains("consecutive errors"));
2456 }
2457
2458 #[tokio::test]
2459 async fn test_circuit_breaker_sent_marks_as_failed() {
2460 let mut mocks = default_test_mocks();
2461 let relayer = create_test_relayer();
2462 let tx = make_test_transaction(TransactionStatus::Sent);
2463
2464 mocks
2466 .tx_repo
2467 .expect_partial_update()
2468 .withf(|_, update| update.status == Some(TransactionStatus::Failed))
2469 .returning(|_, update| {
2470 let mut updated_tx = make_test_transaction(TransactionStatus::Sent);
2471 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2472 updated_tx.status_reason = update.status_reason.clone();
2473 Ok(updated_tx)
2474 });
2475
2476 mocks
2478 .job_producer
2479 .expect_produce_send_notification_job()
2480 .returning(|_, _| Box::pin(async { Ok(()) }));
2481
2482 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2483 let ctx = create_triggered_context();
2484
2485 let result = evm_transaction
2486 .handle_status_impl(tx, Some(ctx))
2487 .await
2488 .unwrap();
2489
2490 assert_eq!(result.status, TransactionStatus::Failed);
2491 }
2492
2493 #[tokio::test]
2494 async fn test_circuit_breaker_submitted_triggers_noop() {
2495 let mut mocks = default_test_mocks();
2496 let relayer = create_test_relayer();
2497 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2498 tx.sent_at = Some((Utc::now() - Duration::seconds(120)).to_rfc3339());
2499
2500 mocks
2502 .network_repo
2503 .expect_get_by_chain_id()
2504 .returning(|_, _| Ok(Some(create_test_network_model())));
2505
2506 mocks
2508 .tx_repo
2509 .expect_partial_update()
2510 .returning(|_, update| {
2511 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2512 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2513 updated_tx.status_reason = update.status_reason.clone();
2514 updated_tx.noop_count = update.noop_count;
2515 Ok(updated_tx)
2516 });
2517
2518 mocks
2520 .job_producer
2521 .expect_produce_submit_transaction_job()
2522 .returning(|_, _| Box::pin(async { Ok(()) }));
2523
2524 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2525 let ctx = create_triggered_context();
2526
2527 let result = evm_transaction
2528 .handle_status_impl(tx, Some(ctx))
2529 .await
2530 .unwrap();
2531
2532 assert!(result.noop_count.is_some());
2534 }
2535
2536 #[tokio::test]
2537 async fn test_circuit_breaker_noop_tx_excluded() {
2538 let mut mocks = default_test_mocks();
2539 let relayer = create_test_relayer();
2540
2541 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2543 tx.sent_at = Some((Utc::now() - Duration::seconds(120)).to_rfc3339());
2544 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
2545 evm_data.to = Some(evm_data.from.clone()); evm_data.value = U256::from(0);
2547 evm_data.data = Some("0x".to_string());
2548 evm_data.hash = Some("0xFakeHash".to_string());
2549 }
2550
2551 mocks
2554 .provider
2555 .expect_get_transaction_receipt()
2556 .returning(|_| Box::pin(async { Ok(None) }));
2557
2558 mocks
2559 .network_repo
2560 .expect_get_by_chain_id()
2561 .returning(|_, _| Ok(Some(create_test_network_model())));
2562
2563 mocks
2564 .job_producer
2565 .expect_produce_check_transaction_status_job()
2566 .returning(|_, _| Box::pin(async { Ok(()) }));
2567
2568 mocks
2570 .job_producer
2571 .expect_produce_submit_transaction_job()
2572 .returning(|_, _| Box::pin(async { Ok(()) }));
2573
2574 mocks
2575 .tx_repo
2576 .expect_partial_update()
2577 .returning(|_, update| {
2578 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2579 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2580 Ok(updated_tx)
2581 });
2582
2583 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2584 let ctx = create_triggered_context();
2585
2586 let result = evm_transaction
2587 .handle_status_impl(tx, Some(ctx))
2588 .await
2589 .unwrap();
2590
2591 assert_eq!(result.status, TransactionStatus::Submitted);
2593 }
2594
2595 #[tokio::test]
2596 async fn test_circuit_breaker_total_failures_triggers() {
2597 let mut mocks = default_test_mocks();
2598 let relayer = create_test_relayer();
2599 let tx = make_test_transaction(TransactionStatus::Pending);
2600
2601 mocks
2603 .tx_repo
2604 .expect_partial_update()
2605 .withf(|_, update| update.status == Some(TransactionStatus::Failed))
2606 .returning(|_, update| {
2607 let mut updated_tx = make_test_transaction(TransactionStatus::Pending);
2608 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2609 updated_tx.status_reason = update.status_reason.clone();
2610 Ok(updated_tx)
2611 });
2612
2613 mocks
2614 .job_producer
2615 .expect_produce_send_notification_job()
2616 .returning(|_, _| Box::pin(async { Ok(()) }));
2617
2618 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2619 let ctx = create_total_triggered_context();
2621
2622 let result = evm_transaction
2623 .handle_status_impl(tx, Some(ctx))
2624 .await
2625 .unwrap();
2626
2627 assert_eq!(result.status, TransactionStatus::Failed);
2628 assert!(result.status_reason.is_some());
2629 }
2630
2631 #[tokio::test]
2632 async fn test_circuit_breaker_below_threshold_continues_normally() {
2633 let mut mocks = default_test_mocks();
2634 let relayer = create_test_relayer();
2635 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2636 tx.sent_at = Some((Utc::now() - Duration::seconds(120)).to_rfc3339());
2637
2638 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
2639 evm_data.hash = Some("0xFakeHash".to_string());
2640 }
2641
2642 mocks
2644 .provider
2645 .expect_get_transaction_receipt()
2646 .returning(|_| Box::pin(async { Ok(None) }));
2647
2648 mocks
2649 .network_repo
2650 .expect_get_by_chain_id()
2651 .returning(|_, _| Ok(Some(create_test_network_model())));
2652
2653 mocks
2654 .job_producer
2655 .expect_produce_check_transaction_status_job()
2656 .returning(|_, _| Box::pin(async { Ok(()) }));
2657
2658 mocks
2659 .tx_repo
2660 .expect_partial_update()
2661 .returning(|_, update| {
2662 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2663 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2664 Ok(updated_tx)
2665 });
2666
2667 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2668 let ctx = create_safe_context();
2669
2670 let result = evm_transaction
2671 .handle_status_impl(tx, Some(ctx))
2672 .await
2673 .unwrap();
2674
2675 assert_eq!(result.status, TransactionStatus::Submitted);
2677 }
2678
2679 #[tokio::test]
2680 async fn test_circuit_breaker_no_context_continues_normally() {
2681 let mut mocks = default_test_mocks();
2682 let relayer = create_test_relayer();
2683 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2684 tx.sent_at = Some((Utc::now() - Duration::seconds(120)).to_rfc3339());
2685
2686 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
2687 evm_data.hash = Some("0xFakeHash".to_string());
2688 }
2689
2690 mocks
2692 .provider
2693 .expect_get_transaction_receipt()
2694 .returning(|_| Box::pin(async { Ok(None) }));
2695
2696 mocks
2697 .network_repo
2698 .expect_get_by_chain_id()
2699 .returning(|_, _| Ok(Some(create_test_network_model())));
2700
2701 mocks
2702 .job_producer
2703 .expect_produce_check_transaction_status_job()
2704 .returning(|_, _| Box::pin(async { Ok(()) }));
2705
2706 mocks
2707 .tx_repo
2708 .expect_partial_update()
2709 .returning(|_, update| {
2710 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2711 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2712 Ok(updated_tx)
2713 });
2714
2715 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2716
2717 let result = evm_transaction.handle_status_impl(tx, None).await.unwrap();
2719
2720 assert_eq!(result.status, TransactionStatus::Submitted);
2722 }
2723
2724 #[tokio::test]
2725 async fn test_circuit_breaker_final_state_early_return() {
2726 let mocks = default_test_mocks();
2727 let relayer = create_test_relayer();
2728 let tx = make_test_transaction(TransactionStatus::Confirmed);
2730
2731 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2732 let ctx = create_triggered_context();
2733
2734 let result = evm_transaction
2736 .handle_status_impl(tx, Some(ctx))
2737 .await
2738 .unwrap();
2739
2740 assert_eq!(result.status, TransactionStatus::Confirmed);
2741 }
2742 }
2743
2744 mod hash_recovery_tests {
2746 use super::*;
2747
2748 #[tokio::test]
2749 async fn test_should_try_hash_recovery_not_submitted() {
2750 let mocks = default_test_mocks();
2751 let relayer = create_test_relayer();
2752
2753 let mut tx = make_test_transaction(TransactionStatus::Sent);
2754 tx.hashes = vec![
2755 "0xHash1".to_string(),
2756 "0xHash2".to_string(),
2757 "0xHash3".to_string(),
2758 ];
2759
2760 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2761 let result = evm_transaction.should_try_hash_recovery(&tx).unwrap();
2762
2763 assert!(
2764 !result,
2765 "Should not attempt recovery for non-Submitted transactions"
2766 );
2767 }
2768
2769 #[tokio::test]
2770 async fn test_should_try_hash_recovery_not_enough_hashes() {
2771 let mocks = default_test_mocks();
2772 let relayer = create_test_relayer();
2773
2774 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2775 tx.hashes = vec!["0xHash1".to_string()]; tx.sent_at = Some((Utc::now() - Duration::minutes(3)).to_rfc3339());
2777
2778 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2779 let result = evm_transaction.should_try_hash_recovery(&tx).unwrap();
2780
2781 assert!(
2782 !result,
2783 "Should not attempt recovery with insufficient hashes"
2784 );
2785 }
2786
2787 #[tokio::test]
2788 async fn test_should_try_hash_recovery_too_recent() {
2789 let mocks = default_test_mocks();
2790 let relayer = create_test_relayer();
2791
2792 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2793 tx.hashes = vec![
2794 "0xHash1".to_string(),
2795 "0xHash2".to_string(),
2796 "0xHash3".to_string(),
2797 ];
2798 tx.sent_at = Some(Utc::now().to_rfc3339()); let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2801 let result = evm_transaction.should_try_hash_recovery(&tx).unwrap();
2802
2803 assert!(
2804 !result,
2805 "Should not attempt recovery for recently sent transactions"
2806 );
2807 }
2808
2809 #[tokio::test]
2810 async fn test_should_try_hash_recovery_success() {
2811 let mocks = default_test_mocks();
2812 let relayer = create_test_relayer();
2813
2814 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2815 tx.hashes = vec![
2816 "0xHash1".to_string(),
2817 "0xHash2".to_string(),
2818 "0xHash3".to_string(),
2819 ];
2820 tx.sent_at = Some((Utc::now() - Duration::minutes(3)).to_rfc3339());
2821
2822 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2823 let result = evm_transaction.should_try_hash_recovery(&tx).unwrap();
2824
2825 assert!(
2826 result,
2827 "Should attempt recovery for stuck transactions with multiple hashes"
2828 );
2829 }
2830
2831 #[tokio::test]
2832 async fn test_try_recover_no_historical_hash_found() {
2833 let mut mocks = default_test_mocks();
2834 let relayer = create_test_relayer();
2835
2836 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2837 tx.hashes = vec![
2838 "0xHash1".to_string(),
2839 "0xHash2".to_string(),
2840 "0xHash3".to_string(),
2841 ];
2842
2843 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
2844 evm_data.hash = Some("0xHash3".to_string());
2845 }
2846
2847 mocks
2849 .provider
2850 .expect_get_transaction_receipt()
2851 .returning(|_| Box::pin(async { Ok(None) }));
2852
2853 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2854 let evm_data = tx.network_data.get_evm_transaction_data().unwrap();
2855 let result = evm_transaction
2856 .try_recover_with_historical_hashes(&tx, &evm_data)
2857 .await
2858 .unwrap();
2859
2860 assert!(
2861 result.is_none(),
2862 "Should return None when no historical hash is found"
2863 );
2864 }
2865
2866 #[tokio::test]
2867 async fn test_try_recover_finds_mined_historical_hash() {
2868 let mut mocks = default_test_mocks();
2869 let relayer = create_test_relayer();
2870
2871 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2872 tx.hashes = vec![
2873 "0xHash1".to_string(),
2874 "0xHash2".to_string(), "0xHash3".to_string(),
2876 ];
2877
2878 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
2879 evm_data.hash = Some("0xHash3".to_string()); }
2881
2882 mocks
2884 .provider
2885 .expect_get_transaction_receipt()
2886 .returning(|hash| {
2887 if hash == "0xHash2" {
2888 Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) })
2889 } else {
2890 Box::pin(async { Ok(None) })
2891 }
2892 });
2893
2894 let tx_clone = tx.clone();
2896 mocks
2897 .tx_repo
2898 .expect_partial_update()
2899 .returning(move |_, update| {
2900 let mut updated_tx = tx_clone.clone();
2901 if let Some(status) = update.status {
2902 updated_tx.status = status;
2903 }
2904 if let Some(NetworkTransactionData::Evm(ref evm_data)) = update.network_data {
2905 if let NetworkTransactionData::Evm(ref mut updated_evm) =
2906 updated_tx.network_data
2907 {
2908 updated_evm.hash = evm_data.hash.clone();
2909 }
2910 }
2911 Ok(updated_tx)
2912 });
2913
2914 mocks
2916 .job_producer
2917 .expect_produce_send_notification_job()
2918 .returning(|_, _| Box::pin(async { Ok(()) }));
2919
2920 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2921 let evm_data = tx.network_data.get_evm_transaction_data().unwrap();
2922 let result = evm_transaction
2923 .try_recover_with_historical_hashes(&tx, &evm_data)
2924 .await
2925 .unwrap();
2926
2927 assert!(result.is_some(), "Should recover the transaction");
2928 let recovered_tx = result.unwrap();
2929 assert_eq!(recovered_tx.status, TransactionStatus::Mined);
2930 }
2931
2932 #[tokio::test]
2933 async fn test_try_recover_network_error_continues() {
2934 let mut mocks = default_test_mocks();
2935 let relayer = create_test_relayer();
2936
2937 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2938 tx.hashes = vec![
2939 "0xHash1".to_string(),
2940 "0xHash2".to_string(), "0xHash3".to_string(), ];
2943
2944 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
2945 evm_data.hash = Some("0xHash1".to_string());
2946 }
2947
2948 mocks
2950 .provider
2951 .expect_get_transaction_receipt()
2952 .returning(|hash| {
2953 if hash == "0xHash2" {
2954 Box::pin(async { Err(crate::services::provider::ProviderError::Timeout) })
2955 } else if hash == "0xHash3" {
2956 Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) })
2957 } else {
2958 Box::pin(async { Ok(None) })
2959 }
2960 });
2961
2962 let tx_clone = tx.clone();
2964 mocks
2965 .tx_repo
2966 .expect_partial_update()
2967 .returning(move |_, update| {
2968 let mut updated_tx = tx_clone.clone();
2969 if let Some(status) = update.status {
2970 updated_tx.status = status;
2971 }
2972 Ok(updated_tx)
2973 });
2974
2975 mocks
2977 .job_producer
2978 .expect_produce_send_notification_job()
2979 .returning(|_, _| Box::pin(async { Ok(()) }));
2980
2981 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2982 let evm_data = tx.network_data.get_evm_transaction_data().unwrap();
2983 let result = evm_transaction
2984 .try_recover_with_historical_hashes(&tx, &evm_data)
2985 .await
2986 .unwrap();
2987
2988 assert!(
2989 result.is_some(),
2990 "Should continue checking after network error and find mined hash"
2991 );
2992 }
2993
2994 #[tokio::test]
2995 async fn test_update_transaction_with_corrected_hash() {
2996 let mut mocks = default_test_mocks();
2997 let relayer = create_test_relayer();
2998
2999 let mut tx = make_test_transaction(TransactionStatus::Submitted);
3000 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
3001 evm_data.hash = Some("0xWrongHash".to_string());
3002 }
3003
3004 mocks
3006 .tx_repo
3007 .expect_partial_update()
3008 .returning(move |_, update| {
3009 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
3010 if let Some(status) = update.status {
3011 updated_tx.status = status;
3012 }
3013 if let Some(NetworkTransactionData::Evm(ref evm_data)) = update.network_data {
3014 if let NetworkTransactionData::Evm(ref mut updated_evm) =
3015 updated_tx.network_data
3016 {
3017 updated_evm.hash = evm_data.hash.clone();
3018 }
3019 }
3020 Ok(updated_tx)
3021 });
3022
3023 mocks
3025 .job_producer
3026 .expect_produce_send_notification_job()
3027 .returning(|_, _| Box::pin(async { Ok(()) }));
3028
3029 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
3030 let evm_data = tx.network_data.get_evm_transaction_data().unwrap();
3031 let result = evm_transaction
3032 .update_transaction_with_corrected_hash(
3033 &tx,
3034 &evm_data,
3035 "0xCorrectHash",
3036 TransactionStatus::Mined,
3037 )
3038 .await
3039 .unwrap();
3040
3041 assert_eq!(result.status, TransactionStatus::Mined);
3042 if let NetworkTransactionData::Evm(ref updated_evm) = result.network_data {
3043 assert_eq!(updated_evm.hash.as_ref().unwrap(), "0xCorrectHash");
3044 }
3045 }
3046 }
3047
3048 mod check_transaction_status_edge_cases {
3050 use super::*;
3051
3052 #[tokio::test]
3053 async fn test_missing_hash_returns_error() {
3054 let mocks = default_test_mocks();
3055 let relayer = create_test_relayer();
3056
3057 let tx = make_test_transaction(TransactionStatus::Submitted);
3058 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
3061 let result = evm_transaction.check_transaction_status(&tx).await;
3062
3063 assert!(result.is_err(), "Should return error when hash is missing");
3064 }
3065
3066 #[tokio::test]
3067 async fn test_pending_status_early_return() {
3068 let mocks = default_test_mocks();
3069 let relayer = create_test_relayer();
3070
3071 let tx = make_test_transaction(TransactionStatus::Pending);
3072
3073 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
3074 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
3075
3076 assert_eq!(
3077 status,
3078 TransactionStatus::Pending,
3079 "Should return Pending without querying blockchain"
3080 );
3081 }
3082
3083 #[tokio::test]
3084 async fn test_sent_status_early_return() {
3085 let mocks = default_test_mocks();
3086 let relayer = create_test_relayer();
3087
3088 let tx = make_test_transaction(TransactionStatus::Sent);
3089
3090 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
3091 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
3092
3093 assert_eq!(
3094 status,
3095 TransactionStatus::Sent,
3096 "Should return Sent without querying blockchain"
3097 );
3098 }
3099
3100 #[tokio::test]
3101 async fn test_final_state_early_return() {
3102 let mocks = default_test_mocks();
3103 let relayer = create_test_relayer();
3104
3105 let tx = make_test_transaction(TransactionStatus::Confirmed);
3106
3107 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
3108 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
3109
3110 assert_eq!(
3111 status,
3112 TransactionStatus::Confirmed,
3113 "Should return final state without querying blockchain"
3114 );
3115 }
3116 }
3117}