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 ) -> Result<TransactionRepoModel, TransactionError> {
304 if tx.status != new_status {
305 return self.update_transaction_status(tx, new_status).await;
306 }
307 Ok(tx)
308 }
309
310 pub(super) async fn prepare_noop_update_request(
312 &self,
313 tx: &TransactionRepoModel,
314 is_cancellation: bool,
315 reason: Option<String>,
316 ) -> Result<TransactionUpdateRequest, TransactionError> {
317 let mut evm_data = tx.network_data.get_evm_transaction_data()?;
318 let network_model = self
319 .network_repository()
320 .get_by_chain_id(NetworkType::Evm, evm_data.chain_id)
321 .await?
322 .ok_or(TransactionError::UnexpectedError(format!(
323 "Network with chain id {} not found",
324 evm_data.chain_id
325 )))?;
326
327 let network = EvmNetwork::try_from(network_model).map_err(|e| {
328 TransactionError::UnexpectedError(format!(
329 "Error converting network model to EvmNetwork: {e}"
330 ))
331 })?;
332
333 make_noop(&mut evm_data, &network, Some(self.provider())).await?;
334
335 let noop_count = tx.noop_count.unwrap_or(0) + 1;
336 let update_request = TransactionUpdateRequest {
337 network_data: Some(NetworkTransactionData::Evm(evm_data)),
338 noop_count: Some(noop_count),
339 status_reason: reason,
340 is_canceled: if is_cancellation {
341 Some(true)
342 } else {
343 tx.is_canceled
344 },
345 ..Default::default()
346 };
347 Ok(update_request)
348 }
349
350 async fn handle_submitted_state(
352 &self,
353 tx: TransactionRepoModel,
354 ) -> Result<TransactionRepoModel, TransactionError> {
355 if self.should_resubmit(&tx).await? {
356 let resubmitted_tx = self.handle_resubmission(tx).await?;
357 return Ok(resubmitted_tx);
358 }
359
360 self.update_transaction_status_if_needed(tx, TransactionStatus::Submitted)
361 .await
362 }
363
364 async fn handle_resubmission(
366 &self,
367 tx: TransactionRepoModel,
368 ) -> Result<TransactionRepoModel, TransactionError> {
369 debug!(
370 tx_id = %tx.id,
371 relayer_id = %tx.relayer_id,
372 status = ?tx.status,
373 "scheduling resubmit job for transaction"
374 );
375
376 let (should_noop, reason) = self.should_noop(&tx).await?;
378 let tx_to_process = if should_noop {
379 self.process_noop_transaction(&tx, reason).await?
380 } else {
381 tx
382 };
383
384 self.send_transaction_resubmit_job(&tx_to_process).await?;
385 Ok(tx_to_process)
386 }
387
388 async fn process_noop_transaction(
390 &self,
391 tx: &TransactionRepoModel,
392 reason: Option<String>,
393 ) -> Result<TransactionRepoModel, TransactionError> {
394 debug!(
395 tx_id = %tx.id,
396 relayer_id = %tx.relayer_id,
397 status = ?tx.status,
398 "preparing transaction NOOP before resubmission"
399 );
400 let update = self.prepare_noop_update_request(tx, false, reason).await?;
401 let updated_tx = self
402 .transaction_repository()
403 .partial_update(tx.id.clone(), update)
404 .await?;
405
406 let res = self.send_transaction_update_notification(&updated_tx).await;
407 if let Err(e) = res {
408 error!(
409 tx_id = %updated_tx.id,
410 relayer_id = %updated_tx.relayer_id,
411 status = ?updated_tx.status,
412 error = %e,
413 "sending transaction update notification failed for NOOP transaction"
414 );
415 }
416 Ok(updated_tx)
417 }
418
419 async fn handle_pending_state(
421 &self,
422 tx: TransactionRepoModel,
423 ) -> Result<TransactionRepoModel, TransactionError> {
424 let (should_noop, reason) = self.should_noop(&tx).await?;
425 if should_noop {
426 debug!(
429 tx_id = %tx.id,
430 relayer_id = %tx.relayer_id,
431 reason = %reason.as_ref().unwrap_or(&"unknown".to_string()),
432 "marking pending transaction as Failed (nonce not assigned, no NOOP needed)"
433 );
434 let update = TransactionUpdateRequest {
435 status: Some(TransactionStatus::Failed),
436 status_reason: reason,
437 ..Default::default()
438 };
439 let updated_tx = self
440 .transaction_repository()
441 .partial_update(tx.id.clone(), update)
442 .await?;
443
444 let res = self.send_transaction_update_notification(&updated_tx).await;
445 if let Err(e) = res {
446 error!(
447 tx_id = %updated_tx.id,
448 relayer_id = %updated_tx.relayer_id,
449 status = ?updated_tx.status,
450 error = %e,
451 "sending transaction update notification failed for Pending state NOOP"
452 );
453 }
454 return Ok(updated_tx);
455 }
456
457 let age = get_age_since_created(&tx)?;
459 if age > get_evm_pending_recovery_trigger_timeout() {
460 warn!(
461 tx_id = %tx.id,
462 relayer_id = %tx.relayer_id,
463 age_seconds = age.num_seconds(),
464 "transaction stuck in Pending, queuing prepare job"
465 );
466
467 self.send_transaction_request_job(&tx).await?;
469 }
470
471 Ok(tx)
472 }
473
474 async fn handle_mined_state(
476 &self,
477 tx: TransactionRepoModel,
478 ) -> Result<TransactionRepoModel, TransactionError> {
479 self.update_transaction_status_if_needed(tx, TransactionStatus::Mined)
480 .await
481 }
482
483 async fn handle_final_state(
485 &self,
486 tx: TransactionRepoModel,
487 status: TransactionStatus,
488 ) -> Result<TransactionRepoModel, TransactionError> {
489 self.update_transaction_status_if_needed(tx, status).await
490 }
491
492 async fn mark_as_failed(
494 &self,
495 tx: TransactionRepoModel,
496 reason: String,
497 ) -> Result<TransactionRepoModel, TransactionError> {
498 warn!(
499 tx_id = %tx.id,
500 relayer_id = %tx.relayer_id,
501 reason = %reason,
502 "force-failing transaction due to circuit breaker"
503 );
504
505 let update = TransactionUpdateRequest {
506 status: Some(TransactionStatus::Failed),
507 status_reason: Some(reason),
508 ..Default::default()
509 };
510
511 let updated_tx = self
512 .transaction_repository()
513 .partial_update(tx.id.clone(), update)
514 .await?;
515
516 if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
518 error!(
519 tx_id = %updated_tx.id,
520 relayer_id = %updated_tx.relayer_id,
521 error = %e,
522 "failed to send notification for force-failed transaction"
523 );
524 }
525
526 Ok(updated_tx)
527 }
528
529 async fn handle_circuit_breaker_safely(
541 &self,
542 tx: TransactionRepoModel,
543 ctx: &StatusCheckContext,
544 ) -> Result<TransactionRepoModel, TransactionError> {
545 let reason = format!(
546 "Transaction status monitoring failed after {} consecutive errors (total: {}). \
547 Last status: {:?}.",
548 ctx.consecutive_failures, ctx.total_failures, tx.status
549 );
550
551 match tx.status {
552 TransactionStatus::Pending | TransactionStatus::Sent => {
553 debug!(
557 tx_id = %tx.id,
558 relayer_id = %tx.relayer_id,
559 status = ?tx.status,
560 "circuit breaker: transaction never broadcast - safe to mark as Failed"
561 );
562 self.mark_as_failed(tx, reason).await
563 }
564 TransactionStatus::Submitted => {
565 warn!(
571 tx_id = %tx.id,
572 relayer_id = %tx.relayer_id,
573 "circuit breaker: Submitted transaction - triggering NOOP to safely clear nonce"
574 );
575 let noop_reason = Some(format!(
576 "{reason}. Replacing with NOOP to clear nonce slot."
577 ));
578 let updated_tx = self.process_noop_transaction(&tx, noop_reason).await?;
579 self.send_transaction_resubmit_job(&updated_tx).await?;
580 Ok(updated_tx)
581 }
582 _ => {
583 debug!(
585 tx_id = %tx.id,
586 relayer_id = %tx.relayer_id,
587 status = ?tx.status,
588 "circuit breaker: unexpected status, returning transaction unchanged"
589 );
590 Ok(tx)
591 }
592 }
593 }
594
595 pub async fn handle_status_impl(
600 &self,
601 tx: TransactionRepoModel,
602 context: Option<StatusCheckContext>,
603 ) -> Result<TransactionRepoModel, TransactionError> {
604 debug!(
605 tx_id = %tx.id,
606 relayer_id = %tx.relayer_id,
607 status = ?tx.status,
608 "checking transaction status"
609 );
610
611 if is_final_state(&tx.status) {
613 debug!(
614 tx_id = %tx.id,
615 relayer_id = %tx.relayer_id,
616 status = ?tx.status,
617 "transaction already in final state"
618 );
619 return Ok(tx);
620 }
621
622 if let Some(ref ctx) = context {
626 let is_noop_tx = tx
627 .network_data
628 .get_evm_transaction_data()
629 .map(|data| is_noop(&data))
630 .unwrap_or(false);
631
632 if ctx.should_force_finalize() && !is_noop_tx {
633 warn!(
634 tx_id = %tx.id,
635 consecutive_failures = ctx.consecutive_failures,
636 total_failures = ctx.total_failures,
637 max_consecutive = ctx.max_consecutive_failures,
638 status = ?tx.status,
639 "circuit breaker triggered - handling safely based on transaction state"
640 );
641 return self.handle_circuit_breaker_safely(tx, ctx).await;
642 }
643
644 if ctx.should_force_finalize() && is_noop_tx {
645 debug!(
646 tx_id = %tx.id,
647 consecutive_failures = ctx.consecutive_failures,
648 relayer_id = %tx.relayer_id,
649 "circuit breaker would trigger but transaction is NOOP - continuing with normal status logic"
650 );
651 }
652 }
653
654 let status = self.check_transaction_status(&tx).await?;
659
660 debug!(
661 tx_id = %tx.id,
662 previous_status = ?tx.status,
663 new_status = ?status,
664 relayer_id = %tx.relayer_id,
665 "transaction status check completed"
666 );
667
668 let tx = if status != tx.status {
672 debug!(
673 tx_id = %tx.id,
674 old_status = ?tx.status,
675 new_status = ?status,
676 relayer_id = %tx.relayer_id,
677 "status changed during check, reloading transaction from DB to ensure fresh data"
678 );
679 self.transaction_repository()
680 .get_by_id(tx.id.clone())
681 .await?
682 } else {
683 tx
684 };
685
686 if is_too_early_to_resubmit(&tx)? && is_pending_transaction(&status) {
691 return self.update_transaction_status_if_needed(tx, status).await;
693 }
694
695 match status {
697 TransactionStatus::Pending => self.handle_pending_state(tx).await,
698 TransactionStatus::Sent => self.handle_sent_state(tx).await,
699 TransactionStatus::Submitted => self.handle_submitted_state(tx).await,
700 TransactionStatus::Mined => self.handle_mined_state(tx).await,
701 TransactionStatus::Confirmed
702 | TransactionStatus::Failed
703 | TransactionStatus::Expired
704 | TransactionStatus::Canceled => self.handle_final_state(tx, status).await,
705 }
706 }
707
708 async fn handle_sent_state(
710 &self,
711 tx: TransactionRepoModel,
712 ) -> Result<TransactionRepoModel, TransactionError> {
713 debug!(
714 tx_id = %tx.id,
715 relayer_id = %tx.relayer_id,
716 "handling Sent state"
717 );
718
719 let (should_noop, reason) = self.should_noop(&tx).await?;
721 if should_noop {
722 debug!(
723 tx_id = %tx.id,
724 relayer_id = %tx.relayer_id,
725 "preparing NOOP for sent transaction"
726 );
727 let update = self.prepare_noop_update_request(&tx, false, reason).await?;
728 let updated_tx = self
729 .transaction_repository()
730 .partial_update(tx.id.clone(), update)
731 .await?;
732
733 self.send_transaction_submit_job(&updated_tx).await?;
734 let res = self.send_transaction_update_notification(&updated_tx).await;
735 if let Err(e) = res {
736 error!(
737 tx_id = %updated_tx.id,
738 relayer_id = %updated_tx.relayer_id,
739 status = ?updated_tx.status,
740 error = %e,
741 "sending transaction update notification failed for Sent state NOOP"
742 );
743 }
744 return Ok(updated_tx);
745 }
746
747 let age_since_sent = get_age_since_status_change(&tx)?;
750
751 if age_since_sent > get_evm_resend_timeout() {
752 warn!(
753 tx_id = %tx.id,
754 relayer_id = %tx.relayer_id,
755 age_seconds = age_since_sent.num_seconds(),
756 "transaction stuck in Sent, queuing resubmit job with repricing"
757 );
758
759 self.send_transaction_resubmit_job(&tx).await?;
761 }
762
763 self.update_transaction_status_if_needed(tx, TransactionStatus::Sent)
764 .await
765 }
766
767 fn should_try_hash_recovery(
774 &self,
775 tx: &TransactionRepoModel,
776 ) -> Result<bool, TransactionError> {
777 if tx.status != TransactionStatus::Submitted {
779 return Ok(false);
780 }
781
782 if tx.hashes.len() <= 1 {
784 return Ok(false);
785 }
786
787 let age = get_age_of_sent_at(tx)?;
789 let min_age_for_recovery = get_evm_min_age_for_hash_recovery();
790
791 if age < min_age_for_recovery {
792 return Ok(false);
793 }
794
795 if tx.hashes.len() < EVM_MIN_HASHES_FOR_RECOVERY {
798 return Ok(false);
799 }
800
801 Ok(true)
802 }
803
804 async fn try_recover_with_historical_hashes(
813 &self,
814 tx: &TransactionRepoModel,
815 evm_data: &crate::models::EvmTransactionData,
816 ) -> Result<Option<TransactionRepoModel>, TransactionError> {
817 warn!(
818 tx_id = %tx.id,
819 relayer_id = %tx.relayer_id,
820 current_hash = ?evm_data.hash,
821 total_hashes = %tx.hashes.len(),
822 "attempting hash recovery - checking historical hashes"
823 );
824
825 for (idx, historical_hash) in tx.hashes.iter().rev().enumerate() {
827 if Some(historical_hash) == evm_data.hash.as_ref() {
829 continue;
830 }
831
832 debug!(
833 tx_id = %tx.id,
834 relayer_id = %tx.relayer_id,
835 hash = %historical_hash,
836 index = %idx,
837 "checking historical hash"
838 );
839
840 match self
842 .provider()
843 .get_transaction_receipt(historical_hash)
844 .await
845 {
846 Ok(Some(receipt)) => {
847 warn!(
848 tx_id = %tx.id,
849 relayer_id = %tx.relayer_id,
850 mined_hash = %historical_hash,
851 wrong_hash = ?evm_data.hash,
852 block_number = ?receipt.block_number,
853 "RECOVERED: found mined transaction with historical hash - correcting database"
854 );
855
856 let updated_tx = self
859 .update_transaction_with_corrected_hash(
860 tx,
861 evm_data,
862 historical_hash,
863 TransactionStatus::Mined,
864 )
865 .await?;
866
867 return Ok(Some(updated_tx));
868 }
869 Ok(None) => {
870 continue;
872 }
873 Err(e) => {
874 warn!(
876 tx_id = %tx.id,
877 relayer_id = %tx.relayer_id,
878 hash = %historical_hash,
879 error = %e,
880 "error checking historical hash, continuing to next"
881 );
882 continue;
883 }
884 }
885 }
886
887 debug!(
889 tx_id = %tx.id,
890 relayer_id = %tx.relayer_id,
891 "hash recovery completed - no historical hashes found on-chain"
892 );
893 Ok(None)
894 }
895
896 async fn update_transaction_with_corrected_hash(
900 &self,
901 tx: &TransactionRepoModel,
902 evm_data: &crate::models::EvmTransactionData,
903 correct_hash: &str,
904 status: TransactionStatus,
905 ) -> Result<TransactionRepoModel, TransactionError> {
906 let mut corrected_data = evm_data.clone();
907 corrected_data.hash = Some(correct_hash.to_string());
908
909 let updated_tx = self
910 .transaction_repository()
911 .partial_update(
912 tx.id.clone(),
913 TransactionUpdateRequest {
914 network_data: Some(NetworkTransactionData::Evm(corrected_data)),
915 status: Some(status),
916 ..Default::default()
917 },
918 )
919 .await?;
920
921 if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
923 error!(
924 tx_id = %updated_tx.id,
925 relayer_id = %updated_tx.relayer_id,
926 error = %e,
927 "failed to send notification for hash recovery"
928 );
929 }
930
931 Ok(updated_tx)
932 }
933}
934
935#[cfg(test)]
936mod tests {
937 use crate::{
938 config::{EvmNetworkConfig, NetworkConfigCommon},
939 domain::transaction::evm::{EvmRelayerTransaction, MockPriceCalculatorTrait},
940 jobs::MockJobProducerTrait,
941 models::{
942 evm::Speed, EvmTransactionData, NetworkConfigData, NetworkRepoModel,
943 NetworkTransactionData, NetworkType, RelayerEvmPolicy, RelayerNetworkPolicy,
944 RelayerRepoModel, RpcConfig, TransactionReceipt, TransactionRepoModel,
945 TransactionStatus, U256,
946 },
947 repositories::{
948 MockNetworkRepository, MockRelayerRepository, MockTransactionCounterTrait,
949 MockTransactionRepository,
950 },
951 services::{provider::MockEvmProviderTrait, signer::MockSigner},
952 };
953 use alloy::{
954 consensus::{Eip658Value, Receipt, ReceiptWithBloom},
955 network::AnyReceiptEnvelope,
956 primitives::{b256, Address, BlockHash, Bloom, TxHash},
957 };
958 use chrono::{Duration, Utc};
959 use std::sync::Arc;
960
961 pub struct TestMocks {
963 pub provider: MockEvmProviderTrait,
964 pub relayer_repo: MockRelayerRepository,
965 pub network_repo: MockNetworkRepository,
966 pub tx_repo: MockTransactionRepository,
967 pub job_producer: MockJobProducerTrait,
968 pub signer: MockSigner,
969 pub counter: MockTransactionCounterTrait,
970 pub price_calc: MockPriceCalculatorTrait,
971 }
972
973 pub fn default_test_mocks() -> TestMocks {
976 TestMocks {
977 provider: MockEvmProviderTrait::new(),
978 relayer_repo: MockRelayerRepository::new(),
979 network_repo: MockNetworkRepository::new(),
980 tx_repo: MockTransactionRepository::new(),
981 job_producer: MockJobProducerTrait::new(),
982 signer: MockSigner::new(),
983 counter: MockTransactionCounterTrait::new(),
984 price_calc: MockPriceCalculatorTrait::new(),
985 }
986 }
987
988 pub fn default_test_mocks_with_network() -> TestMocks {
990 let mut mocks = default_test_mocks();
991 mocks
993 .network_repo
994 .expect_get_by_chain_id()
995 .returning(|network_type, chain_id| {
996 if network_type == NetworkType::Evm && chain_id == 1 {
997 Ok(Some(create_test_network_model()))
998 } else {
999 Ok(None)
1000 }
1001 });
1002 mocks
1003 }
1004
1005 pub fn create_test_network_model() -> NetworkRepoModel {
1007 let evm_config = EvmNetworkConfig {
1008 common: NetworkConfigCommon {
1009 network: "mainnet".to_string(),
1010 from: None,
1011 rpc_urls: Some(vec![RpcConfig::new("https://rpc.example.com".to_string())]),
1012 explorer_urls: Some(vec!["https://explorer.example.com".to_string()]),
1013 average_blocktime_ms: Some(12000),
1014 is_testnet: Some(false),
1015 tags: Some(vec!["mainnet".to_string()]),
1016 },
1017 chain_id: Some(1),
1018 required_confirmations: Some(12),
1019 features: Some(vec!["eip1559".to_string()]),
1020 symbol: Some("ETH".to_string()),
1021 gas_price_cache: None,
1022 };
1023 NetworkRepoModel {
1024 id: "evm:mainnet".to_string(),
1025 name: "mainnet".to_string(),
1026 network_type: NetworkType::Evm,
1027 config: NetworkConfigData::Evm(evm_config),
1028 }
1029 }
1030
1031 pub fn create_test_no_mempool_network_model() -> NetworkRepoModel {
1033 let evm_config = EvmNetworkConfig {
1034 common: NetworkConfigCommon {
1035 network: "arbitrum".to_string(),
1036 from: None,
1037 rpc_urls: Some(vec![crate::models::RpcConfig::new(
1038 "https://arb-rpc.example.com".to_string(),
1039 )]),
1040 explorer_urls: Some(vec!["https://arb-explorer.example.com".to_string()]),
1041 average_blocktime_ms: Some(1000),
1042 is_testnet: Some(false),
1043 tags: Some(vec![
1044 "arbitrum".to_string(),
1045 "rollup".to_string(),
1046 "no-mempool".to_string(),
1047 ]),
1048 },
1049 chain_id: Some(42161),
1050 required_confirmations: Some(12),
1051 features: Some(vec!["eip1559".to_string()]),
1052 symbol: Some("ETH".to_string()),
1053 gas_price_cache: None,
1054 };
1055 NetworkRepoModel {
1056 id: "evm:arbitrum".to_string(),
1057 name: "arbitrum".to_string(),
1058 network_type: NetworkType::Evm,
1059 config: NetworkConfigData::Evm(evm_config),
1060 }
1061 }
1062
1063 pub fn make_test_transaction(status: TransactionStatus) -> TransactionRepoModel {
1067 TransactionRepoModel {
1068 id: "test-tx-id".to_string(),
1069 relayer_id: "test-relayer-id".to_string(),
1070 status,
1071 status_reason: None,
1072 created_at: Utc::now().to_rfc3339(),
1073 sent_at: None,
1074 confirmed_at: None,
1075 valid_until: None,
1076 delete_at: None,
1077 network_type: NetworkType::Evm,
1078 network_data: NetworkTransactionData::Evm(EvmTransactionData {
1079 chain_id: 1,
1080 from: "0xSender".to_string(),
1081 to: Some("0xRecipient".to_string()),
1082 value: U256::from(0),
1083 data: Some("0xData".to_string()),
1084 gas_limit: Some(21000),
1085 gas_price: Some(20000000000),
1086 max_fee_per_gas: None,
1087 max_priority_fee_per_gas: None,
1088 nonce: None,
1089 signature: None,
1090 hash: None,
1091 speed: Some(Speed::Fast),
1092 raw: None,
1093 }),
1094 priced_at: None,
1095 hashes: Vec::new(),
1096 noop_count: None,
1097 is_canceled: Some(false),
1098 }
1099 }
1100
1101 pub fn make_test_evm_relayer_transaction(
1104 relayer: RelayerRepoModel,
1105 mocks: TestMocks,
1106 ) -> EvmRelayerTransaction<
1107 MockEvmProviderTrait,
1108 MockRelayerRepository,
1109 MockNetworkRepository,
1110 MockTransactionRepository,
1111 MockJobProducerTrait,
1112 MockSigner,
1113 MockTransactionCounterTrait,
1114 MockPriceCalculatorTrait,
1115 > {
1116 EvmRelayerTransaction::new(
1117 relayer,
1118 mocks.provider,
1119 Arc::new(mocks.relayer_repo),
1120 Arc::new(mocks.network_repo),
1121 Arc::new(mocks.tx_repo),
1122 Arc::new(mocks.counter),
1123 Arc::new(mocks.job_producer),
1124 mocks.price_calc,
1125 mocks.signer,
1126 )
1127 .unwrap()
1128 }
1129
1130 fn create_test_relayer() -> RelayerRepoModel {
1131 RelayerRepoModel {
1132 id: "test-relayer-id".to_string(),
1133 name: "Test Relayer".to_string(),
1134 paused: false,
1135 system_disabled: false,
1136 network: "test_network".to_string(),
1137 network_type: NetworkType::Evm,
1138 policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
1139 signer_id: "test_signer".to_string(),
1140 address: "0x".to_string(),
1141 notification_id: None,
1142 custom_rpc_urls: None,
1143 ..Default::default()
1144 }
1145 }
1146
1147 fn make_mock_receipt(status: bool, block_number: Option<u64>) -> TransactionReceipt {
1148 let tx_hash = TxHash::from(b256!(
1150 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
1151 ));
1152 let block_hash = BlockHash::from(b256!(
1153 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
1154 ));
1155 let from_address = Address::from([0x11; 20]);
1156
1157 TransactionReceipt {
1158 inner: alloy::rpc::types::TransactionReceipt {
1159 inner: AnyReceiptEnvelope {
1160 inner: ReceiptWithBloom {
1161 receipt: Receipt {
1162 status: Eip658Value::Eip658(status), cumulative_gas_used: 0,
1164 logs: vec![],
1165 },
1166 logs_bloom: Bloom::ZERO,
1167 },
1168 r#type: 0, },
1170 transaction_hash: tx_hash,
1171 transaction_index: Some(0),
1172 block_hash: block_number.map(|_| block_hash), block_number,
1174 gas_used: 21000,
1175 effective_gas_price: 1000,
1176 blob_gas_used: None,
1177 blob_gas_price: None,
1178 from: from_address,
1179 to: None,
1180 contract_address: None,
1181 },
1182 other: Default::default(),
1183 }
1184 }
1185
1186 mod check_transaction_status_tests {
1188 use super::*;
1189
1190 #[tokio::test]
1191 async fn test_not_mined() {
1192 let mut mocks = default_test_mocks();
1193 let relayer = create_test_relayer();
1194 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1195
1196 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1198 evm_data.hash = Some("0xFakeHash".to_string());
1199 }
1200
1201 mocks
1203 .provider
1204 .expect_get_transaction_receipt()
1205 .returning(|_| Box::pin(async { Ok(None) }));
1206
1207 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1208
1209 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
1210 assert_eq!(status, TransactionStatus::Submitted);
1211 }
1212
1213 #[tokio::test]
1214 async fn test_mined_but_not_confirmed() {
1215 let mut mocks = default_test_mocks();
1216 let relayer = create_test_relayer();
1217 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1218
1219 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1220 evm_data.hash = Some("0xFakeHash".to_string());
1221 }
1222
1223 mocks
1225 .provider
1226 .expect_get_transaction_receipt()
1227 .returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) }));
1228
1229 mocks
1231 .provider
1232 .expect_get_block_number()
1233 .return_once(|| Box::pin(async { Ok(100) }));
1234
1235 mocks
1237 .network_repo
1238 .expect_get_by_chain_id()
1239 .returning(|_, _| Ok(Some(create_test_network_model())));
1240
1241 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1242
1243 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
1244 assert_eq!(status, TransactionStatus::Mined);
1245 }
1246
1247 #[tokio::test]
1248 async fn test_confirmed() {
1249 let mut mocks = default_test_mocks();
1250 let relayer = create_test_relayer();
1251 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1252
1253 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1254 evm_data.hash = Some("0xFakeHash".to_string());
1255 }
1256
1257 mocks
1259 .provider
1260 .expect_get_transaction_receipt()
1261 .returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) }));
1262
1263 mocks
1265 .provider
1266 .expect_get_block_number()
1267 .return_once(|| Box::pin(async { Ok(113) }));
1268
1269 mocks
1271 .network_repo
1272 .expect_get_by_chain_id()
1273 .returning(|_, _| Ok(Some(create_test_network_model())));
1274
1275 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1276
1277 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
1278 assert_eq!(status, TransactionStatus::Confirmed);
1279 }
1280
1281 #[tokio::test]
1282 async fn test_failed() {
1283 let mut mocks = default_test_mocks();
1284 let relayer = create_test_relayer();
1285 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1286
1287 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1288 evm_data.hash = Some("0xFakeHash".to_string());
1289 }
1290
1291 mocks
1293 .provider
1294 .expect_get_transaction_receipt()
1295 .returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(false, Some(100)))) }));
1296
1297 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1298
1299 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
1300 assert_eq!(status, TransactionStatus::Failed);
1301 }
1302 }
1303
1304 mod should_resubmit_tests {
1306 use super::*;
1307 use crate::models::TransactionError;
1308
1309 #[tokio::test]
1310 async fn test_should_resubmit_true() {
1311 let mut mocks = default_test_mocks();
1312 let relayer = create_test_relayer();
1313
1314 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1316 tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
1317
1318 mocks
1320 .network_repo
1321 .expect_get_by_chain_id()
1322 .returning(|_, _| Ok(Some(create_test_network_model())));
1323
1324 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1325 let res = evm_transaction.should_resubmit(&tx).await.unwrap();
1326 assert!(res, "Transaction should be resubmitted after timeout.");
1327 }
1328
1329 #[tokio::test]
1330 async fn test_should_resubmit_false() {
1331 let mut mocks = default_test_mocks();
1332 let relayer = create_test_relayer();
1333
1334 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1336 tx.sent_at = Some(Utc::now().to_rfc3339());
1337
1338 mocks
1340 .network_repo
1341 .expect_get_by_chain_id()
1342 .returning(|_, _| Ok(Some(create_test_network_model())));
1343
1344 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1345 let res = evm_transaction.should_resubmit(&tx).await.unwrap();
1346 assert!(!res, "Transaction should not be resubmitted immediately.");
1347 }
1348
1349 #[tokio::test]
1350 async fn test_should_resubmit_true_for_no_mempool_network() {
1351 let mut mocks = default_test_mocks();
1352 let relayer = create_test_relayer();
1353
1354 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1356 tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
1357
1358 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1360 evm_data.chain_id = 42161; }
1362
1363 mocks
1365 .network_repo
1366 .expect_get_by_chain_id()
1367 .returning(|_, _| Ok(Some(create_test_no_mempool_network_model())));
1368
1369 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1370 let res = evm_transaction.should_resubmit(&tx).await.unwrap();
1371 assert!(
1372 res,
1373 "Transaction should be resubmitted for no-mempool networks."
1374 );
1375 }
1376
1377 #[tokio::test]
1378 async fn test_should_resubmit_network_not_found() {
1379 let mut mocks = default_test_mocks();
1380 let relayer = create_test_relayer();
1381
1382 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1383 tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
1384
1385 mocks
1387 .network_repo
1388 .expect_get_by_chain_id()
1389 .returning(|_, _| Ok(None));
1390
1391 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1392 let result = evm_transaction.should_resubmit(&tx).await;
1393
1394 assert!(
1395 result.is_err(),
1396 "should_resubmit should return error when network not found"
1397 );
1398 let error = result.unwrap_err();
1399 match error {
1400 TransactionError::UnexpectedError(msg) => {
1401 assert!(msg.contains("Network with chain id 1 not found"));
1402 }
1403 _ => panic!("Expected UnexpectedError for network not found"),
1404 }
1405 }
1406
1407 #[tokio::test]
1408 async fn test_should_resubmit_network_conversion_error() {
1409 let mut mocks = default_test_mocks();
1410 let relayer = create_test_relayer();
1411
1412 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1413 tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
1414
1415 let invalid_evm_config = EvmNetworkConfig {
1417 common: NetworkConfigCommon {
1418 network: "invalid-network".to_string(),
1419 from: None,
1420 rpc_urls: Some(vec![crate::models::RpcConfig::new(
1421 "https://rpc.example.com".to_string(),
1422 )]),
1423 explorer_urls: Some(vec!["https://explorer.example.com".to_string()]),
1424 average_blocktime_ms: Some(12000),
1425 is_testnet: Some(false),
1426 tags: Some(vec!["testnet".to_string()]),
1427 },
1428 chain_id: None, required_confirmations: Some(12),
1430 features: Some(vec!["eip1559".to_string()]),
1431 symbol: Some("ETH".to_string()),
1432 gas_price_cache: None,
1433 };
1434 let invalid_network = NetworkRepoModel {
1435 id: "evm:invalid".to_string(),
1436 name: "invalid-network".to_string(),
1437 network_type: NetworkType::Evm,
1438 config: NetworkConfigData::Evm(invalid_evm_config),
1439 };
1440
1441 mocks
1443 .network_repo
1444 .expect_get_by_chain_id()
1445 .returning(move |_, _| Ok(Some(invalid_network.clone())));
1446
1447 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1448 let result = evm_transaction.should_resubmit(&tx).await;
1449
1450 assert!(
1451 result.is_err(),
1452 "should_resubmit should return error when network conversion fails"
1453 );
1454 let error = result.unwrap_err();
1455 match error {
1456 TransactionError::UnexpectedError(msg) => {
1457 assert!(msg.contains("Error converting network model to EvmNetwork"));
1458 }
1459 _ => panic!("Expected UnexpectedError for network conversion failure"),
1460 }
1461 }
1462 }
1463
1464 mod should_noop_tests {
1466 use super::*;
1467
1468 #[tokio::test]
1469 async fn test_expired_transaction_triggers_noop() {
1470 let mut mocks = default_test_mocks();
1471 let relayer = create_test_relayer();
1472
1473 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1474 tx.valid_until = Some((Utc::now() - Duration::seconds(10)).to_rfc3339());
1476
1477 mocks
1479 .network_repo
1480 .expect_get_by_chain_id()
1481 .returning(|_, _| Ok(Some(create_test_network_model())));
1482
1483 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1484 let (res, reason) = evm_transaction.should_noop(&tx).await.unwrap();
1485 assert!(res, "Expired transaction should be replaced with a NOOP.");
1486 assert!(
1487 reason.is_some(),
1488 "Reason should be provided for expired transaction"
1489 );
1490 assert!(
1491 reason.unwrap().contains("expired"),
1492 "Reason should mention expiration"
1493 );
1494 }
1495
1496 #[tokio::test]
1497 async fn test_too_many_noop_attempts_returns_false() {
1498 let mocks = default_test_mocks();
1499 let relayer = create_test_relayer();
1500
1501 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1502 tx.noop_count = Some(51); let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1505 let (res, reason) = evm_transaction.should_noop(&tx).await.unwrap();
1506 assert!(
1507 !res,
1508 "Transaction with too many NOOP attempts should not be replaced."
1509 );
1510 assert!(
1511 reason.is_none(),
1512 "Reason should not be provided when should_noop is false"
1513 );
1514 }
1515
1516 #[tokio::test]
1517 async fn test_already_noop_returns_false() {
1518 let mut mocks = default_test_mocks();
1519 let relayer = create_test_relayer();
1520
1521 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1522 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1524 evm_data.to = None;
1525 evm_data.value = U256::from(0);
1526 }
1527
1528 mocks
1529 .network_repo
1530 .expect_get_by_chain_id()
1531 .returning(|_, _| Ok(Some(create_test_network_model())));
1532
1533 mocks.provider.expect_get_block_by_number().returning(|| {
1535 Box::pin(async {
1536 use alloy::{network::AnyRpcBlock, rpc::types::Block};
1537 let mut block: Block = Block::default();
1538 block.header.gas_limit = 30_000_000u64;
1539 Ok(AnyRpcBlock::from(block))
1540 })
1541 });
1542
1543 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1544 let (res, reason) = evm_transaction.should_noop(&tx).await.unwrap();
1545 assert!(
1546 !res,
1547 "Transaction that is already a NOOP should not be replaced."
1548 );
1549 assert!(
1550 reason.is_none(),
1551 "Reason should not be provided when should_noop is false"
1552 );
1553 }
1554
1555 #[tokio::test]
1556 async fn test_rollup_with_too_many_attempts_triggers_noop() {
1557 let mut mocks = default_test_mocks();
1558 let relayer = create_test_relayer();
1559
1560 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1561 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1563 evm_data.chain_id = 42161; }
1565 tx.hashes = vec!["0xHash1".to_string(); 51];
1567
1568 mocks
1570 .network_repo
1571 .expect_get_by_chain_id()
1572 .returning(|_, _| Ok(Some(create_test_no_mempool_network_model())));
1573
1574 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1575 let (res, reason) = evm_transaction.should_noop(&tx).await.unwrap();
1576 assert!(
1577 res,
1578 "Rollup transaction with too many attempts should be replaced with NOOP."
1579 );
1580 assert!(
1581 reason.is_some(),
1582 "Reason should be provided for rollup transaction"
1583 );
1584 assert!(
1585 reason.unwrap().contains("too many attempts"),
1586 "Reason should mention too many attempts"
1587 );
1588 }
1589
1590 #[tokio::test]
1591 async fn test_pending_state_timeout_triggers_noop() {
1592 let mut mocks = default_test_mocks();
1593 let relayer = create_test_relayer();
1594
1595 let mut tx = make_test_transaction(TransactionStatus::Pending);
1596 tx.created_at = (Utc::now() - Duration::minutes(3)).to_rfc3339();
1598
1599 mocks
1600 .network_repo
1601 .expect_get_by_chain_id()
1602 .returning(|_, _| Ok(Some(create_test_network_model())));
1603
1604 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1605 let (res, reason) = evm_transaction.should_noop(&tx).await.unwrap();
1606 assert!(
1607 res,
1608 "Pending transaction stuck for >2 minutes should be replaced with NOOP."
1609 );
1610 assert!(
1611 reason.is_some(),
1612 "Reason should be provided for pending timeout"
1613 );
1614 assert!(
1615 reason.unwrap().contains("Pending state"),
1616 "Reason should mention Pending state"
1617 );
1618 }
1619
1620 #[tokio::test]
1621 async fn test_valid_transaction_returns_false() {
1622 let mut mocks = default_test_mocks();
1623 let relayer = create_test_relayer();
1624
1625 let tx = make_test_transaction(TransactionStatus::Submitted);
1626 mocks
1629 .network_repo
1630 .expect_get_by_chain_id()
1631 .returning(|_, _| Ok(Some(create_test_network_model())));
1632
1633 mocks.provider.expect_get_block_by_number().returning(|| {
1635 Box::pin(async {
1636 use alloy::{network::AnyRpcBlock, rpc::types::Block};
1637 let mut block: Block = Block::default();
1638 block.header.gas_limit = 30_000_000u64;
1639 Ok(AnyRpcBlock::from(block))
1640 })
1641 });
1642
1643 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1644 let (res, reason) = evm_transaction.should_noop(&tx).await.unwrap();
1645 assert!(!res, "Valid transaction should not be replaced with NOOP.");
1646 assert!(
1647 reason.is_none(),
1648 "Reason should not be provided when should_noop is false"
1649 );
1650 }
1651 }
1652
1653 mod update_transaction_status_tests {
1655 use super::*;
1656
1657 #[tokio::test]
1658 async fn test_no_update_when_status_is_same() {
1659 let mocks = default_test_mocks();
1661 let relayer = create_test_relayer();
1662 let tx = make_test_transaction(TransactionStatus::Submitted);
1663 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1664
1665 let updated_tx = evm_transaction
1668 .update_transaction_status_if_needed(tx.clone(), TransactionStatus::Submitted)
1669 .await
1670 .unwrap();
1671 assert_eq!(updated_tx.status, TransactionStatus::Submitted);
1672 assert_eq!(updated_tx.id, tx.id);
1673 }
1674
1675 #[tokio::test]
1676 async fn test_updates_when_status_differs() {
1677 let mut mocks = default_test_mocks();
1678 let relayer = create_test_relayer();
1679 let tx = make_test_transaction(TransactionStatus::Submitted);
1680
1681 mocks
1683 .tx_repo
1684 .expect_partial_update()
1685 .returning(|_, update| {
1686 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1687 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1688 Ok(updated_tx)
1689 });
1690
1691 mocks
1693 .job_producer
1694 .expect_produce_send_notification_job()
1695 .returning(|_, _| Box::pin(async { Ok(()) }));
1696
1697 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1698 let updated_tx = evm_transaction
1699 .update_transaction_status_if_needed(tx.clone(), TransactionStatus::Mined)
1700 .await
1701 .unwrap();
1702
1703 assert_eq!(updated_tx.status, TransactionStatus::Mined);
1704 }
1705 }
1706
1707 mod handle_sent_state_tests {
1709 use super::*;
1710
1711 #[tokio::test]
1712 async fn test_sent_state_recent_no_resend() {
1713 let mut mocks = default_test_mocks();
1714 let relayer = create_test_relayer();
1715
1716 let mut tx = make_test_transaction(TransactionStatus::Sent);
1717 tx.sent_at = Some((Utc::now() - Duration::seconds(10)).to_rfc3339());
1719
1720 mocks
1722 .network_repo
1723 .expect_get_by_chain_id()
1724 .returning(|_, _| Ok(Some(create_test_network_model())));
1725
1726 mocks.provider.expect_get_block_by_number().returning(|| {
1728 Box::pin(async {
1729 use alloy::{network::AnyRpcBlock, rpc::types::Block};
1730 let mut block: Block = Block::default();
1731 block.header.gas_limit = 30_000_000u64;
1732 Ok(AnyRpcBlock::from(block))
1733 })
1734 });
1735
1736 mocks
1738 .job_producer
1739 .expect_produce_check_transaction_status_job()
1740 .returning(|_, _| Box::pin(async { Ok(()) }));
1741
1742 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1743 let result = evm_transaction.handle_sent_state(tx.clone()).await.unwrap();
1744
1745 assert_eq!(result.status, TransactionStatus::Sent);
1746 }
1747
1748 #[tokio::test]
1749 async fn test_sent_state_stuck_schedules_resubmit() {
1750 let mut mocks = default_test_mocks();
1751 let relayer = create_test_relayer();
1752
1753 let mut tx = make_test_transaction(TransactionStatus::Sent);
1754 tx.sent_at = Some((Utc::now() - Duration::seconds(60)).to_rfc3339());
1756
1757 mocks
1759 .network_repo
1760 .expect_get_by_chain_id()
1761 .returning(|_, _| Ok(Some(create_test_network_model())));
1762
1763 mocks.provider.expect_get_block_by_number().returning(|| {
1765 Box::pin(async {
1766 use alloy::{network::AnyRpcBlock, rpc::types::Block};
1767 let mut block: Block = Block::default();
1768 block.header.gas_limit = 30_000_000u64;
1769 Ok(AnyRpcBlock::from(block))
1770 })
1771 });
1772
1773 mocks
1775 .job_producer
1776 .expect_produce_submit_transaction_job()
1777 .returning(|_, _| Box::pin(async { Ok(()) }));
1778
1779 mocks
1781 .job_producer
1782 .expect_produce_check_transaction_status_job()
1783 .returning(|_, _| Box::pin(async { Ok(()) }));
1784
1785 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1786 let result = evm_transaction.handle_sent_state(tx.clone()).await.unwrap();
1787
1788 assert_eq!(result.status, TransactionStatus::Sent);
1789 }
1790 }
1791
1792 mod prepare_noop_update_request_tests {
1794 use super::*;
1795
1796 #[tokio::test]
1797 async fn test_noop_request_without_cancellation() {
1798 let mocks = default_test_mocks_with_network();
1800 let relayer = create_test_relayer();
1801 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1802 tx.noop_count = Some(2);
1803 tx.is_canceled = Some(false);
1804
1805 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1806 let update_req = evm_transaction
1807 .prepare_noop_update_request(&tx, false, None)
1808 .await
1809 .unwrap();
1810
1811 assert_eq!(update_req.noop_count, Some(3));
1813 assert_eq!(update_req.is_canceled, Some(false));
1815 }
1816
1817 #[tokio::test]
1818 async fn test_noop_request_with_cancellation() {
1819 let mocks = default_test_mocks_with_network();
1821 let relayer = create_test_relayer();
1822 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1823 tx.noop_count = None;
1824 tx.is_canceled = Some(false);
1825
1826 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1827 let update_req = evm_transaction
1828 .prepare_noop_update_request(&tx, true, None)
1829 .await
1830 .unwrap();
1831
1832 assert_eq!(update_req.noop_count, Some(1));
1834 assert_eq!(update_req.is_canceled, Some(true));
1836 }
1837 }
1838
1839 mod handle_submitted_state_tests {
1841 use super::*;
1842
1843 #[tokio::test]
1844 async fn test_schedules_resubmit_job() {
1845 let mut mocks = default_test_mocks();
1846 let relayer = create_test_relayer();
1847
1848 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1850 tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
1851
1852 mocks
1854 .network_repo
1855 .expect_get_by_chain_id()
1856 .returning(|_, _| Ok(Some(create_test_network_model())));
1857
1858 mocks.provider.expect_get_block_by_number().returning(|| {
1860 Box::pin(async {
1861 use alloy::{network::AnyRpcBlock, rpc::types::Block};
1862 let mut block: Block = Block::default();
1863 block.header.gas_limit = 30_000_000u64;
1864 Ok(AnyRpcBlock::from(block))
1865 })
1866 });
1867
1868 mocks
1870 .job_producer
1871 .expect_produce_submit_transaction_job()
1872 .returning(|_, _| Box::pin(async { Ok(()) }));
1873
1874 mocks
1876 .job_producer
1877 .expect_produce_check_transaction_status_job()
1878 .returning(|_, _| Box::pin(async { Ok(()) }));
1879
1880 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1881 let updated_tx = evm_transaction.handle_submitted_state(tx).await.unwrap();
1882
1883 assert_eq!(updated_tx.status, TransactionStatus::Submitted);
1885 }
1886 }
1887
1888 mod handle_pending_state_tests {
1890 use super::*;
1891
1892 #[tokio::test]
1893 async fn test_pending_state_no_noop() {
1894 let mut mocks = default_test_mocks();
1896 let relayer = create_test_relayer();
1897 let mut tx = make_test_transaction(TransactionStatus::Pending);
1898 tx.created_at = Utc::now().to_rfc3339(); mocks
1902 .network_repo
1903 .expect_get_by_chain_id()
1904 .returning(|_, _| Ok(Some(create_test_network_model())));
1905
1906 mocks.provider.expect_get_block_by_number().returning(|| {
1908 Box::pin(async {
1909 use alloy::{network::AnyRpcBlock, rpc::types::Block};
1910 let mut block: Block = Block::default();
1911 block.header.gas_limit = 30_000_000u64;
1912 Ok(AnyRpcBlock::from(block))
1913 })
1914 });
1915
1916 mocks
1918 .job_producer
1919 .expect_produce_check_transaction_status_job()
1920 .returning(|_, _| Box::pin(async { Ok(()) }));
1921
1922 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1923 let result = evm_transaction
1924 .handle_pending_state(tx.clone())
1925 .await
1926 .unwrap();
1927
1928 assert_eq!(result.id, tx.id);
1930 assert_eq!(result.status, tx.status);
1931 assert_eq!(result.noop_count, tx.noop_count);
1932 }
1933
1934 #[tokio::test]
1935 async fn test_pending_state_with_noop() {
1936 let mut mocks = default_test_mocks();
1938 let relayer = create_test_relayer();
1939 let mut tx = make_test_transaction(TransactionStatus::Pending);
1940 tx.created_at = (Utc::now() - Duration::minutes(2)).to_rfc3339();
1941
1942 mocks
1944 .network_repo
1945 .expect_get_by_chain_id()
1946 .returning(|_, _| Ok(Some(create_test_network_model())));
1947
1948 mocks.provider.expect_get_block_by_number().returning(|| {
1950 Box::pin(async {
1951 use alloy::{network::AnyRpcBlock, rpc::types::Block};
1952 let mut block: Block = Block::default();
1953 block.header.gas_limit = 30_000_000u64;
1954 Ok(AnyRpcBlock::from(block))
1955 })
1956 });
1957
1958 let tx_clone = tx.clone();
1961 mocks
1962 .tx_repo
1963 .expect_partial_update()
1964 .withf(move |id, update| {
1965 id == "test-tx-id"
1966 && update.status == Some(TransactionStatus::Failed)
1967 && update.status_reason.is_some()
1968 })
1969 .returning(move |_, update| {
1970 let mut updated_tx = tx_clone.clone();
1971 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1972 updated_tx.status_reason = update.status_reason.clone();
1973 Ok(updated_tx)
1974 });
1975 mocks
1977 .job_producer
1978 .expect_produce_send_notification_job()
1979 .returning(|_, _| Box::pin(async { Ok(()) }));
1980
1981 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1982 let result = evm_transaction
1983 .handle_pending_state(tx.clone())
1984 .await
1985 .unwrap();
1986
1987 assert_eq!(result.status, TransactionStatus::Failed);
1989 assert!(result.status_reason.is_some());
1990 assert!(result.status_reason.unwrap().contains("Pending state"));
1991 }
1992 }
1993
1994 mod handle_mined_state_tests {
1996 use super::*;
1997
1998 #[tokio::test]
1999 async fn test_updates_status_and_schedules_check() {
2000 let mut mocks = default_test_mocks();
2001 let relayer = create_test_relayer();
2002 let tx = make_test_transaction(TransactionStatus::Submitted);
2004
2005 mocks
2007 .job_producer
2008 .expect_produce_check_transaction_status_job()
2009 .returning(|_, _| Box::pin(async { Ok(()) }));
2010 mocks
2012 .tx_repo
2013 .expect_partial_update()
2014 .returning(|_, update| {
2015 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2016 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2017 Ok(updated_tx)
2018 });
2019
2020 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2021 let result = evm_transaction
2022 .handle_mined_state(tx.clone())
2023 .await
2024 .unwrap();
2025 assert_eq!(result.status, TransactionStatus::Mined);
2026 }
2027 }
2028
2029 mod handle_final_state_tests {
2031 use super::*;
2032
2033 #[tokio::test]
2034 async fn test_final_state_confirmed() {
2035 let mut mocks = default_test_mocks();
2036 let relayer = create_test_relayer();
2037 let tx = make_test_transaction(TransactionStatus::Submitted);
2038
2039 mocks
2041 .tx_repo
2042 .expect_partial_update()
2043 .returning(|_, update| {
2044 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2045 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2046 Ok(updated_tx)
2047 });
2048
2049 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2050 let result = evm_transaction
2051 .handle_final_state(tx.clone(), TransactionStatus::Confirmed)
2052 .await
2053 .unwrap();
2054 assert_eq!(result.status, TransactionStatus::Confirmed);
2055 }
2056
2057 #[tokio::test]
2058 async fn test_final_state_failed() {
2059 let mut mocks = default_test_mocks();
2060 let relayer = create_test_relayer();
2061 let tx = make_test_transaction(TransactionStatus::Submitted);
2062
2063 mocks
2065 .tx_repo
2066 .expect_partial_update()
2067 .returning(|_, update| {
2068 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2069 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2070 Ok(updated_tx)
2071 });
2072
2073 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2074 let result = evm_transaction
2075 .handle_final_state(tx.clone(), TransactionStatus::Failed)
2076 .await
2077 .unwrap();
2078 assert_eq!(result.status, TransactionStatus::Failed);
2079 }
2080
2081 #[tokio::test]
2082 async fn test_final_state_expired() {
2083 let mut mocks = default_test_mocks();
2084 let relayer = create_test_relayer();
2085 let tx = make_test_transaction(TransactionStatus::Submitted);
2086
2087 mocks
2089 .tx_repo
2090 .expect_partial_update()
2091 .returning(|_, update| {
2092 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2093 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2094 Ok(updated_tx)
2095 });
2096
2097 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2098 let result = evm_transaction
2099 .handle_final_state(tx.clone(), TransactionStatus::Expired)
2100 .await
2101 .unwrap();
2102 assert_eq!(result.status, TransactionStatus::Expired);
2103 }
2104 }
2105
2106 mod handle_status_impl_tests {
2108 use super::*;
2109
2110 #[tokio::test]
2111 async fn test_impl_submitted_branch() {
2112 let mut mocks = default_test_mocks();
2113 let relayer = create_test_relayer();
2114 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2115 tx.sent_at = Some((Utc::now() - Duration::seconds(120)).to_rfc3339());
2116 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
2118 evm_data.hash = Some("0xFakeHash".to_string());
2119 }
2120 mocks
2122 .provider
2123 .expect_get_transaction_receipt()
2124 .returning(|_| Box::pin(async { Ok(None) }));
2125 mocks
2127 .network_repo
2128 .expect_get_by_chain_id()
2129 .returning(|_, _| Ok(Some(create_test_network_model())));
2130 mocks
2132 .job_producer
2133 .expect_produce_check_transaction_status_job()
2134 .returning(|_, _| Box::pin(async { Ok(()) }));
2135 mocks
2137 .tx_repo
2138 .expect_partial_update()
2139 .returning(|_, update| {
2140 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2141 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2142 Ok(updated_tx)
2143 });
2144
2145 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2146 let result = evm_transaction.handle_status_impl(tx, None).await.unwrap();
2147 assert_eq!(result.status, TransactionStatus::Submitted);
2148 }
2149
2150 #[tokio::test]
2151 async fn test_impl_mined_branch() {
2152 let mut mocks = default_test_mocks();
2153 let relayer = create_test_relayer();
2154 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2155 tx.created_at = (Utc::now() - Duration::minutes(1)).to_rfc3339();
2157 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
2159 evm_data.hash = Some("0xFakeHash".to_string());
2160 }
2161 mocks
2163 .provider
2164 .expect_get_transaction_receipt()
2165 .returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) }));
2166 mocks
2168 .provider
2169 .expect_get_block_number()
2170 .return_once(|| Box::pin(async { Ok(100) }));
2171 mocks
2173 .network_repo
2174 .expect_get_by_chain_id()
2175 .returning(|_, _| Ok(Some(create_test_network_model())));
2176 mocks
2178 .job_producer
2179 .expect_produce_send_notification_job()
2180 .returning(|_, _| Box::pin(async { Ok(()) }));
2181 mocks.tx_repo.expect_get_by_id().returning(|_| {
2183 let updated_tx = make_test_transaction(TransactionStatus::Mined);
2184 Ok(updated_tx)
2185 });
2186 mocks
2188 .tx_repo
2189 .expect_partial_update()
2190 .returning(|_, update| {
2191 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2192 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2193 Ok(updated_tx)
2194 });
2195
2196 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2197 let result = evm_transaction.handle_status_impl(tx, None).await.unwrap();
2198 assert_eq!(result.status, TransactionStatus::Mined);
2199 }
2200
2201 #[tokio::test]
2202 async fn test_impl_final_confirmed_branch() {
2203 let mut mocks = default_test_mocks();
2204 let relayer = create_test_relayer();
2205 let tx = make_test_transaction(TransactionStatus::Confirmed);
2207
2208 mocks
2211 .tx_repo
2212 .expect_partial_update()
2213 .returning(|_, update| {
2214 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2215 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2216 Ok(updated_tx)
2217 });
2218
2219 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2220 let result = evm_transaction.handle_status_impl(tx, None).await.unwrap();
2221 assert_eq!(result.status, TransactionStatus::Confirmed);
2222 }
2223
2224 #[tokio::test]
2225 async fn test_impl_final_failed_branch() {
2226 let mut mocks = default_test_mocks();
2227 let relayer = create_test_relayer();
2228 let tx = make_test_transaction(TransactionStatus::Failed);
2230
2231 mocks
2232 .tx_repo
2233 .expect_partial_update()
2234 .returning(|_, update| {
2235 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2236 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2237 Ok(updated_tx)
2238 });
2239
2240 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2241 let result = evm_transaction.handle_status_impl(tx, None).await.unwrap();
2242 assert_eq!(result.status, TransactionStatus::Failed);
2243 }
2244
2245 #[tokio::test]
2246 async fn test_impl_final_expired_branch() {
2247 let mut mocks = default_test_mocks();
2248 let relayer = create_test_relayer();
2249 let tx = make_test_transaction(TransactionStatus::Expired);
2251
2252 mocks
2253 .tx_repo
2254 .expect_partial_update()
2255 .returning(|_, update| {
2256 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2257 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2258 Ok(updated_tx)
2259 });
2260
2261 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2262 let result = evm_transaction.handle_status_impl(tx, None).await.unwrap();
2263 assert_eq!(result.status, TransactionStatus::Expired);
2264 }
2265 }
2266
2267 mod circuit_breaker_tests {
2269 use super::*;
2270 use crate::jobs::StatusCheckContext;
2271
2272 fn create_triggered_context() -> StatusCheckContext {
2274 StatusCheckContext::new(
2275 30, 50, 60, 25, 75, NetworkType::Evm,
2281 )
2282 }
2283
2284 fn create_safe_context() -> StatusCheckContext {
2286 StatusCheckContext::new(
2287 5, 10, 15, 25, 75, NetworkType::Evm,
2293 )
2294 }
2295
2296 fn create_total_triggered_context() -> StatusCheckContext {
2298 StatusCheckContext::new(
2299 5, 80, 100, 25, 75, NetworkType::Evm,
2305 )
2306 }
2307
2308 #[tokio::test]
2309 async fn test_circuit_breaker_pending_marks_as_failed() {
2310 let mut mocks = default_test_mocks();
2311 let relayer = create_test_relayer();
2312 let tx = make_test_transaction(TransactionStatus::Pending);
2313
2314 mocks
2316 .tx_repo
2317 .expect_partial_update()
2318 .withf(|_, update| update.status == Some(TransactionStatus::Failed))
2319 .returning(|_, update| {
2320 let mut updated_tx = make_test_transaction(TransactionStatus::Pending);
2321 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2322 updated_tx.status_reason = update.status_reason.clone();
2323 Ok(updated_tx)
2324 });
2325
2326 mocks
2328 .job_producer
2329 .expect_produce_send_notification_job()
2330 .returning(|_, _| Box::pin(async { Ok(()) }));
2331
2332 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2333 let ctx = create_triggered_context();
2334
2335 let result = evm_transaction
2336 .handle_status_impl(tx, Some(ctx))
2337 .await
2338 .unwrap();
2339
2340 assert_eq!(result.status, TransactionStatus::Failed);
2341 assert!(result.status_reason.is_some());
2342 assert!(result.status_reason.unwrap().contains("consecutive errors"));
2343 }
2344
2345 #[tokio::test]
2346 async fn test_circuit_breaker_sent_marks_as_failed() {
2347 let mut mocks = default_test_mocks();
2348 let relayer = create_test_relayer();
2349 let tx = make_test_transaction(TransactionStatus::Sent);
2350
2351 mocks
2353 .tx_repo
2354 .expect_partial_update()
2355 .withf(|_, update| update.status == Some(TransactionStatus::Failed))
2356 .returning(|_, update| {
2357 let mut updated_tx = make_test_transaction(TransactionStatus::Sent);
2358 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2359 updated_tx.status_reason = update.status_reason.clone();
2360 Ok(updated_tx)
2361 });
2362
2363 mocks
2365 .job_producer
2366 .expect_produce_send_notification_job()
2367 .returning(|_, _| Box::pin(async { Ok(()) }));
2368
2369 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2370 let ctx = create_triggered_context();
2371
2372 let result = evm_transaction
2373 .handle_status_impl(tx, Some(ctx))
2374 .await
2375 .unwrap();
2376
2377 assert_eq!(result.status, TransactionStatus::Failed);
2378 }
2379
2380 #[tokio::test]
2381 async fn test_circuit_breaker_submitted_triggers_noop() {
2382 let mut mocks = default_test_mocks();
2383 let relayer = create_test_relayer();
2384 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2385 tx.sent_at = Some((Utc::now() - Duration::seconds(120)).to_rfc3339());
2386
2387 mocks
2389 .network_repo
2390 .expect_get_by_chain_id()
2391 .returning(|_, _| Ok(Some(create_test_network_model())));
2392
2393 mocks
2395 .tx_repo
2396 .expect_partial_update()
2397 .returning(|_, update| {
2398 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2399 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2400 updated_tx.status_reason = update.status_reason.clone();
2401 updated_tx.noop_count = update.noop_count;
2402 Ok(updated_tx)
2403 });
2404
2405 mocks
2407 .job_producer
2408 .expect_produce_submit_transaction_job()
2409 .returning(|_, _| Box::pin(async { Ok(()) }));
2410
2411 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2412 let ctx = create_triggered_context();
2413
2414 let result = evm_transaction
2415 .handle_status_impl(tx, Some(ctx))
2416 .await
2417 .unwrap();
2418
2419 assert!(result.noop_count.is_some());
2421 }
2422
2423 #[tokio::test]
2424 async fn test_circuit_breaker_noop_tx_excluded() {
2425 let mut mocks = default_test_mocks();
2426 let relayer = create_test_relayer();
2427
2428 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2430 tx.sent_at = Some((Utc::now() - Duration::seconds(120)).to_rfc3339());
2431 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
2432 evm_data.to = Some(evm_data.from.clone()); evm_data.value = U256::from(0);
2434 evm_data.data = Some("0x".to_string());
2435 evm_data.hash = Some("0xFakeHash".to_string());
2436 }
2437
2438 mocks
2441 .provider
2442 .expect_get_transaction_receipt()
2443 .returning(|_| Box::pin(async { Ok(None) }));
2444
2445 mocks
2446 .network_repo
2447 .expect_get_by_chain_id()
2448 .returning(|_, _| Ok(Some(create_test_network_model())));
2449
2450 mocks
2451 .job_producer
2452 .expect_produce_check_transaction_status_job()
2453 .returning(|_, _| Box::pin(async { Ok(()) }));
2454
2455 mocks
2457 .job_producer
2458 .expect_produce_submit_transaction_job()
2459 .returning(|_, _| Box::pin(async { Ok(()) }));
2460
2461 mocks
2462 .tx_repo
2463 .expect_partial_update()
2464 .returning(|_, update| {
2465 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2466 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2467 Ok(updated_tx)
2468 });
2469
2470 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2471 let ctx = create_triggered_context();
2472
2473 let result = evm_transaction
2474 .handle_status_impl(tx, Some(ctx))
2475 .await
2476 .unwrap();
2477
2478 assert_eq!(result.status, TransactionStatus::Submitted);
2480 }
2481
2482 #[tokio::test]
2483 async fn test_circuit_breaker_total_failures_triggers() {
2484 let mut mocks = default_test_mocks();
2485 let relayer = create_test_relayer();
2486 let tx = make_test_transaction(TransactionStatus::Pending);
2487
2488 mocks
2490 .tx_repo
2491 .expect_partial_update()
2492 .withf(|_, update| update.status == Some(TransactionStatus::Failed))
2493 .returning(|_, update| {
2494 let mut updated_tx = make_test_transaction(TransactionStatus::Pending);
2495 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2496 updated_tx.status_reason = update.status_reason.clone();
2497 Ok(updated_tx)
2498 });
2499
2500 mocks
2501 .job_producer
2502 .expect_produce_send_notification_job()
2503 .returning(|_, _| Box::pin(async { Ok(()) }));
2504
2505 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2506 let ctx = create_total_triggered_context();
2508
2509 let result = evm_transaction
2510 .handle_status_impl(tx, Some(ctx))
2511 .await
2512 .unwrap();
2513
2514 assert_eq!(result.status, TransactionStatus::Failed);
2515 assert!(result.status_reason.is_some());
2516 }
2517
2518 #[tokio::test]
2519 async fn test_circuit_breaker_below_threshold_continues_normally() {
2520 let mut mocks = default_test_mocks();
2521 let relayer = create_test_relayer();
2522 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2523 tx.sent_at = Some((Utc::now() - Duration::seconds(120)).to_rfc3339());
2524
2525 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
2526 evm_data.hash = Some("0xFakeHash".to_string());
2527 }
2528
2529 mocks
2531 .provider
2532 .expect_get_transaction_receipt()
2533 .returning(|_| Box::pin(async { Ok(None) }));
2534
2535 mocks
2536 .network_repo
2537 .expect_get_by_chain_id()
2538 .returning(|_, _| Ok(Some(create_test_network_model())));
2539
2540 mocks
2541 .job_producer
2542 .expect_produce_check_transaction_status_job()
2543 .returning(|_, _| Box::pin(async { Ok(()) }));
2544
2545 mocks
2546 .tx_repo
2547 .expect_partial_update()
2548 .returning(|_, update| {
2549 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2550 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2551 Ok(updated_tx)
2552 });
2553
2554 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2555 let ctx = create_safe_context();
2556
2557 let result = evm_transaction
2558 .handle_status_impl(tx, Some(ctx))
2559 .await
2560 .unwrap();
2561
2562 assert_eq!(result.status, TransactionStatus::Submitted);
2564 }
2565
2566 #[tokio::test]
2567 async fn test_circuit_breaker_no_context_continues_normally() {
2568 let mut mocks = default_test_mocks();
2569 let relayer = create_test_relayer();
2570 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2571 tx.sent_at = Some((Utc::now() - Duration::seconds(120)).to_rfc3339());
2572
2573 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
2574 evm_data.hash = Some("0xFakeHash".to_string());
2575 }
2576
2577 mocks
2579 .provider
2580 .expect_get_transaction_receipt()
2581 .returning(|_| Box::pin(async { Ok(None) }));
2582
2583 mocks
2584 .network_repo
2585 .expect_get_by_chain_id()
2586 .returning(|_, _| Ok(Some(create_test_network_model())));
2587
2588 mocks
2589 .job_producer
2590 .expect_produce_check_transaction_status_job()
2591 .returning(|_, _| Box::pin(async { Ok(()) }));
2592
2593 mocks
2594 .tx_repo
2595 .expect_partial_update()
2596 .returning(|_, update| {
2597 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2598 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2599 Ok(updated_tx)
2600 });
2601
2602 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2603
2604 let result = evm_transaction.handle_status_impl(tx, None).await.unwrap();
2606
2607 assert_eq!(result.status, TransactionStatus::Submitted);
2609 }
2610
2611 #[tokio::test]
2612 async fn test_circuit_breaker_final_state_early_return() {
2613 let mocks = default_test_mocks();
2614 let relayer = create_test_relayer();
2615 let tx = make_test_transaction(TransactionStatus::Confirmed);
2617
2618 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2619 let ctx = create_triggered_context();
2620
2621 let result = evm_transaction
2623 .handle_status_impl(tx, Some(ctx))
2624 .await
2625 .unwrap();
2626
2627 assert_eq!(result.status, TransactionStatus::Confirmed);
2628 }
2629 }
2630
2631 mod hash_recovery_tests {
2633 use super::*;
2634
2635 #[tokio::test]
2636 async fn test_should_try_hash_recovery_not_submitted() {
2637 let mocks = default_test_mocks();
2638 let relayer = create_test_relayer();
2639
2640 let mut tx = make_test_transaction(TransactionStatus::Sent);
2641 tx.hashes = vec![
2642 "0xHash1".to_string(),
2643 "0xHash2".to_string(),
2644 "0xHash3".to_string(),
2645 ];
2646
2647 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2648 let result = evm_transaction.should_try_hash_recovery(&tx).unwrap();
2649
2650 assert!(
2651 !result,
2652 "Should not attempt recovery for non-Submitted transactions"
2653 );
2654 }
2655
2656 #[tokio::test]
2657 async fn test_should_try_hash_recovery_not_enough_hashes() {
2658 let mocks = default_test_mocks();
2659 let relayer = create_test_relayer();
2660
2661 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2662 tx.hashes = vec!["0xHash1".to_string()]; tx.sent_at = Some((Utc::now() - Duration::minutes(3)).to_rfc3339());
2664
2665 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2666 let result = evm_transaction.should_try_hash_recovery(&tx).unwrap();
2667
2668 assert!(
2669 !result,
2670 "Should not attempt recovery with insufficient hashes"
2671 );
2672 }
2673
2674 #[tokio::test]
2675 async fn test_should_try_hash_recovery_too_recent() {
2676 let mocks = default_test_mocks();
2677 let relayer = create_test_relayer();
2678
2679 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2680 tx.hashes = vec![
2681 "0xHash1".to_string(),
2682 "0xHash2".to_string(),
2683 "0xHash3".to_string(),
2684 ];
2685 tx.sent_at = Some(Utc::now().to_rfc3339()); let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2688 let result = evm_transaction.should_try_hash_recovery(&tx).unwrap();
2689
2690 assert!(
2691 !result,
2692 "Should not attempt recovery for recently sent transactions"
2693 );
2694 }
2695
2696 #[tokio::test]
2697 async fn test_should_try_hash_recovery_success() {
2698 let mocks = default_test_mocks();
2699 let relayer = create_test_relayer();
2700
2701 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2702 tx.hashes = vec![
2703 "0xHash1".to_string(),
2704 "0xHash2".to_string(),
2705 "0xHash3".to_string(),
2706 ];
2707 tx.sent_at = Some((Utc::now() - Duration::minutes(3)).to_rfc3339());
2708
2709 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2710 let result = evm_transaction.should_try_hash_recovery(&tx).unwrap();
2711
2712 assert!(
2713 result,
2714 "Should attempt recovery for stuck transactions with multiple hashes"
2715 );
2716 }
2717
2718 #[tokio::test]
2719 async fn test_try_recover_no_historical_hash_found() {
2720 let mut mocks = default_test_mocks();
2721 let relayer = create_test_relayer();
2722
2723 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2724 tx.hashes = vec![
2725 "0xHash1".to_string(),
2726 "0xHash2".to_string(),
2727 "0xHash3".to_string(),
2728 ];
2729
2730 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
2731 evm_data.hash = Some("0xHash3".to_string());
2732 }
2733
2734 mocks
2736 .provider
2737 .expect_get_transaction_receipt()
2738 .returning(|_| Box::pin(async { Ok(None) }));
2739
2740 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2741 let evm_data = tx.network_data.get_evm_transaction_data().unwrap();
2742 let result = evm_transaction
2743 .try_recover_with_historical_hashes(&tx, &evm_data)
2744 .await
2745 .unwrap();
2746
2747 assert!(
2748 result.is_none(),
2749 "Should return None when no historical hash is found"
2750 );
2751 }
2752
2753 #[tokio::test]
2754 async fn test_try_recover_finds_mined_historical_hash() {
2755 let mut mocks = default_test_mocks();
2756 let relayer = create_test_relayer();
2757
2758 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2759 tx.hashes = vec![
2760 "0xHash1".to_string(),
2761 "0xHash2".to_string(), "0xHash3".to_string(),
2763 ];
2764
2765 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
2766 evm_data.hash = Some("0xHash3".to_string()); }
2768
2769 mocks
2771 .provider
2772 .expect_get_transaction_receipt()
2773 .returning(|hash| {
2774 if hash == "0xHash2" {
2775 Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) })
2776 } else {
2777 Box::pin(async { Ok(None) })
2778 }
2779 });
2780
2781 let tx_clone = tx.clone();
2783 mocks
2784 .tx_repo
2785 .expect_partial_update()
2786 .returning(move |_, update| {
2787 let mut updated_tx = tx_clone.clone();
2788 if let Some(status) = update.status {
2789 updated_tx.status = status;
2790 }
2791 if let Some(NetworkTransactionData::Evm(ref evm_data)) = update.network_data {
2792 if let NetworkTransactionData::Evm(ref mut updated_evm) =
2793 updated_tx.network_data
2794 {
2795 updated_evm.hash = evm_data.hash.clone();
2796 }
2797 }
2798 Ok(updated_tx)
2799 });
2800
2801 mocks
2803 .job_producer
2804 .expect_produce_send_notification_job()
2805 .returning(|_, _| Box::pin(async { Ok(()) }));
2806
2807 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2808 let evm_data = tx.network_data.get_evm_transaction_data().unwrap();
2809 let result = evm_transaction
2810 .try_recover_with_historical_hashes(&tx, &evm_data)
2811 .await
2812 .unwrap();
2813
2814 assert!(result.is_some(), "Should recover the transaction");
2815 let recovered_tx = result.unwrap();
2816 assert_eq!(recovered_tx.status, TransactionStatus::Mined);
2817 }
2818
2819 #[tokio::test]
2820 async fn test_try_recover_network_error_continues() {
2821 let mut mocks = default_test_mocks();
2822 let relayer = create_test_relayer();
2823
2824 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2825 tx.hashes = vec![
2826 "0xHash1".to_string(),
2827 "0xHash2".to_string(), "0xHash3".to_string(), ];
2830
2831 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
2832 evm_data.hash = Some("0xHash1".to_string());
2833 }
2834
2835 mocks
2837 .provider
2838 .expect_get_transaction_receipt()
2839 .returning(|hash| {
2840 if hash == "0xHash2" {
2841 Box::pin(async { Err(crate::services::provider::ProviderError::Timeout) })
2842 } else if hash == "0xHash3" {
2843 Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) })
2844 } else {
2845 Box::pin(async { Ok(None) })
2846 }
2847 });
2848
2849 let tx_clone = tx.clone();
2851 mocks
2852 .tx_repo
2853 .expect_partial_update()
2854 .returning(move |_, update| {
2855 let mut updated_tx = tx_clone.clone();
2856 if let Some(status) = update.status {
2857 updated_tx.status = status;
2858 }
2859 Ok(updated_tx)
2860 });
2861
2862 mocks
2864 .job_producer
2865 .expect_produce_send_notification_job()
2866 .returning(|_, _| Box::pin(async { Ok(()) }));
2867
2868 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2869 let evm_data = tx.network_data.get_evm_transaction_data().unwrap();
2870 let result = evm_transaction
2871 .try_recover_with_historical_hashes(&tx, &evm_data)
2872 .await
2873 .unwrap();
2874
2875 assert!(
2876 result.is_some(),
2877 "Should continue checking after network error and find mined hash"
2878 );
2879 }
2880
2881 #[tokio::test]
2882 async fn test_update_transaction_with_corrected_hash() {
2883 let mut mocks = default_test_mocks();
2884 let relayer = create_test_relayer();
2885
2886 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2887 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
2888 evm_data.hash = Some("0xWrongHash".to_string());
2889 }
2890
2891 mocks
2893 .tx_repo
2894 .expect_partial_update()
2895 .returning(move |_, update| {
2896 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2897 if let Some(status) = update.status {
2898 updated_tx.status = status;
2899 }
2900 if let Some(NetworkTransactionData::Evm(ref evm_data)) = update.network_data {
2901 if let NetworkTransactionData::Evm(ref mut updated_evm) =
2902 updated_tx.network_data
2903 {
2904 updated_evm.hash = evm_data.hash.clone();
2905 }
2906 }
2907 Ok(updated_tx)
2908 });
2909
2910 mocks
2912 .job_producer
2913 .expect_produce_send_notification_job()
2914 .returning(|_, _| Box::pin(async { Ok(()) }));
2915
2916 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2917 let evm_data = tx.network_data.get_evm_transaction_data().unwrap();
2918 let result = evm_transaction
2919 .update_transaction_with_corrected_hash(
2920 &tx,
2921 &evm_data,
2922 "0xCorrectHash",
2923 TransactionStatus::Mined,
2924 )
2925 .await
2926 .unwrap();
2927
2928 assert_eq!(result.status, TransactionStatus::Mined);
2929 if let NetworkTransactionData::Evm(ref updated_evm) = result.network_data {
2930 assert_eq!(updated_evm.hash.as_ref().unwrap(), "0xCorrectHash");
2931 }
2932 }
2933 }
2934
2935 mod check_transaction_status_edge_cases {
2937 use super::*;
2938
2939 #[tokio::test]
2940 async fn test_missing_hash_returns_error() {
2941 let mocks = default_test_mocks();
2942 let relayer = create_test_relayer();
2943
2944 let tx = make_test_transaction(TransactionStatus::Submitted);
2945 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2948 let result = evm_transaction.check_transaction_status(&tx).await;
2949
2950 assert!(result.is_err(), "Should return error when hash is missing");
2951 }
2952
2953 #[tokio::test]
2954 async fn test_pending_status_early_return() {
2955 let mocks = default_test_mocks();
2956 let relayer = create_test_relayer();
2957
2958 let tx = make_test_transaction(TransactionStatus::Pending);
2959
2960 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2961 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
2962
2963 assert_eq!(
2964 status,
2965 TransactionStatus::Pending,
2966 "Should return Pending without querying blockchain"
2967 );
2968 }
2969
2970 #[tokio::test]
2971 async fn test_sent_status_early_return() {
2972 let mocks = default_test_mocks();
2973 let relayer = create_test_relayer();
2974
2975 let tx = make_test_transaction(TransactionStatus::Sent);
2976
2977 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2978 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
2979
2980 assert_eq!(
2981 status,
2982 TransactionStatus::Sent,
2983 "Should return Sent without querying blockchain"
2984 );
2985 }
2986
2987 #[tokio::test]
2988 async fn test_final_state_early_return() {
2989 let mocks = default_test_mocks();
2990 let relayer = create_test_relayer();
2991
2992 let tx = make_test_transaction(TransactionStatus::Confirmed);
2993
2994 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2995 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
2996
2997 assert_eq!(
2998 status,
2999 TransactionStatus::Confirmed,
3000 "Should return final state without querying blockchain"
3001 );
3002 }
3003 }
3004}