openzeppelin_relayer/domain/transaction/evm/
status.rs

1//! This module contains the status-related functionality for EVM transactions.
2//! It includes methods for checking transaction status, determining when to resubmit
3//! or replace transactions with NOOPs, and updating transaction status in the repository.
4
5use alloy::network::ReceiptResponse;
6use chrono::{DateTime, Duration, Utc};
7use eyre::Result;
8use tracing::{debug, error, info, 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, EVM_PREPARE_TIMEOUT_MINUTES,
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,
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        // Early return if transaction is already in a final state
55        if is_final_state(&tx.status) {
56            return Ok(tx.status.clone());
57        }
58
59        // Early return for Pending/Sent states - these are DB-only states
60        // that don't require on-chain queries and may not have a hash yet
61        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!(tx_hash = %tx_hash, "transaction mined but not confirmed");
110                return Ok(TransactionStatus::Mined);
111            }
112            Ok(TransactionStatus::Confirmed)
113        } else {
114            debug!(tx_hash = %tx_hash, "transaction not yet mined");
115
116            // FALLBACK: Try to find transaction by checking all historical hashes
117            // Only do this for transactions that have multiple resubmission attempts
118            // and have been stuck in Submitted for a while
119            if tx.hashes.len() > 1 && self.should_try_hash_recovery(tx)? {
120                if let Some(recovered_tx) = self
121                    .try_recover_with_historical_hashes(tx, &evm_data)
122                    .await?
123                {
124                    // Return the status from the recovered (updated) transaction
125                    return Ok(recovered_tx.status);
126                }
127            }
128
129            Ok(TransactionStatus::Submitted)
130        }
131    }
132
133    /// Determines if a transaction should be resubmitted.
134    pub(super) async fn should_resubmit(
135        &self,
136        tx: &TransactionRepoModel,
137    ) -> Result<bool, TransactionError> {
138        // Validate transaction is in correct state for resubmission
139        ensure_status(tx, TransactionStatus::Submitted, Some("should_resubmit"))?;
140
141        let evm_data = tx.network_data.get_evm_transaction_data()?;
142        let age = get_age_of_sent_at(tx)?;
143
144        // Check if network lacks mempool and determine appropriate timeout
145        let network_model = self
146            .network_repository()
147            .get_by_chain_id(NetworkType::Evm, evm_data.chain_id)
148            .await?
149            .ok_or(TransactionError::UnexpectedError(format!(
150                "Network with chain id {} not found",
151                evm_data.chain_id
152            )))?;
153
154        let network = EvmNetwork::try_from(network_model).map_err(|e| {
155            TransactionError::UnexpectedError(format!(
156                "Error converting network model to EvmNetwork: {e}"
157            ))
158        })?;
159
160        let timeout = match network.is_arbitrum() {
161            true => ARBITRUM_TIME_TO_RESUBMIT,
162            false => get_resubmit_timeout_for_speed(&evm_data.speed),
163        };
164
165        let timeout_with_backoff = match network.is_arbitrum() {
166            true => timeout, // Use base timeout without backoff for Arbitrum
167            false => get_resubmit_timeout_with_backoff(timeout, tx.hashes.len()),
168        };
169
170        if age > Duration::milliseconds(timeout_with_backoff) {
171            info!("Transaction has been pending for too long, resubmitting");
172            return Ok(true);
173        }
174        Ok(false)
175    }
176
177    /// Determines if a transaction should be replaced with a NOOP transaction.
178    pub(super) async fn should_noop(
179        &self,
180        tx: &TransactionRepoModel,
181    ) -> Result<bool, TransactionError> {
182        if too_many_noop_attempts(tx) {
183            info!("Transaction has too many NOOP attempts already");
184            return Ok(false);
185        }
186
187        let evm_data = tx.network_data.get_evm_transaction_data()?;
188        if is_noop(&evm_data) {
189            return Ok(false);
190        }
191
192        let network_model = self
193            .network_repository()
194            .get_by_chain_id(NetworkType::Evm, evm_data.chain_id)
195            .await?
196            .ok_or(TransactionError::UnexpectedError(format!(
197                "Network with chain id {} not found",
198                evm_data.chain_id
199            )))?;
200
201        let network = EvmNetwork::try_from(network_model).map_err(|e| {
202            TransactionError::UnexpectedError(format!(
203                "Error converting network model to EvmNetwork: {e}"
204            ))
205        })?;
206
207        if network.is_rollup() && too_many_attempts(tx) {
208            info!("Rollup transaction has too many attempts, will replace with NOOP");
209            return Ok(true);
210        }
211
212        if !is_transaction_valid(&tx.created_at, &tx.valid_until) {
213            info!("Transaction is expired, will replace with NOOP");
214            return Ok(true);
215        }
216
217        if tx.status == TransactionStatus::Pending {
218            let created_at = &tx.created_at;
219            let created_time = DateTime::parse_from_rfc3339(created_at)
220                .map_err(|e| {
221                    TransactionError::UnexpectedError(format!("Invalid created_at timestamp: {e}"))
222                })?
223                .with_timezone(&Utc);
224            let age = Utc::now().signed_duration_since(created_time);
225            if age > get_evm_prepare_timeout() {
226                info!("Transaction in Pending state for over {EVM_PREPARE_TIMEOUT_MINUTES} minutes, will replace with NOOP");
227                return Ok(true);
228            }
229        }
230        Ok(false)
231    }
232
233    /// Helper method that updates transaction status only if it's different from the current status.
234    pub(super) async fn update_transaction_status_if_needed(
235        &self,
236        tx: TransactionRepoModel,
237        new_status: TransactionStatus,
238    ) -> Result<TransactionRepoModel, TransactionError> {
239        if tx.status != new_status {
240            return self.update_transaction_status(tx, new_status).await;
241        }
242        Ok(tx)
243    }
244
245    /// Prepares a NOOP transaction update request.
246    pub(super) async fn prepare_noop_update_request(
247        &self,
248        tx: &TransactionRepoModel,
249        is_cancellation: bool,
250    ) -> Result<TransactionUpdateRequest, TransactionError> {
251        let mut evm_data = tx.network_data.get_evm_transaction_data()?;
252        let network_model = self
253            .network_repository()
254            .get_by_chain_id(NetworkType::Evm, evm_data.chain_id)
255            .await?
256            .ok_or(TransactionError::UnexpectedError(format!(
257                "Network with chain id {} not found",
258                evm_data.chain_id
259            )))?;
260
261        let network = EvmNetwork::try_from(network_model).map_err(|e| {
262            TransactionError::UnexpectedError(format!(
263                "Error converting network model to EvmNetwork: {e}"
264            ))
265        })?;
266
267        make_noop(&mut evm_data, &network, Some(self.provider())).await?;
268
269        let noop_count = tx.noop_count.unwrap_or(0) + 1;
270        let update_request = TransactionUpdateRequest {
271            network_data: Some(NetworkTransactionData::Evm(evm_data)),
272            noop_count: Some(noop_count),
273            is_canceled: if is_cancellation {
274                Some(true)
275            } else {
276                tx.is_canceled
277            },
278            ..Default::default()
279        };
280        Ok(update_request)
281    }
282
283    /// Handles transactions in the Submitted state.
284    async fn handle_submitted_state(
285        &self,
286        tx: TransactionRepoModel,
287    ) -> Result<TransactionRepoModel, TransactionError> {
288        if self.should_resubmit(&tx).await? {
289            let resubmitted_tx = self.handle_resubmission(tx).await?;
290            return Ok(resubmitted_tx);
291        }
292
293        self.update_transaction_status_if_needed(tx, TransactionStatus::Submitted)
294            .await
295    }
296
297    /// Processes transaction resubmission logic
298    async fn handle_resubmission(
299        &self,
300        tx: TransactionRepoModel,
301    ) -> Result<TransactionRepoModel, TransactionError> {
302        debug!("scheduling resubmit job for transaction");
303
304        let tx_to_process = if self.should_noop(&tx).await? {
305            self.process_noop_transaction(&tx).await?
306        } else {
307            tx
308        };
309
310        self.send_transaction_resubmit_job(&tx_to_process).await?;
311        Ok(tx_to_process)
312    }
313
314    /// Handles NOOP transaction processing before resubmission
315    async fn process_noop_transaction(
316        &self,
317        tx: &TransactionRepoModel,
318    ) -> Result<TransactionRepoModel, TransactionError> {
319        debug!("preparing transaction NOOP before resubmission");
320        let update = self.prepare_noop_update_request(tx, false).await?;
321        let updated_tx = self
322            .transaction_repository()
323            .partial_update(tx.id.clone(), update)
324            .await?;
325
326        let res = self.send_transaction_update_notification(&updated_tx).await;
327        if let Err(e) = res {
328            error!(
329                tx_id = %updated_tx.id,
330                status = ?updated_tx.status,
331                "sending transaction update notification failed for NOOP transaction: {:?}",
332                e
333            );
334        }
335        Ok(updated_tx)
336    }
337
338    /// Handles transactions in the Pending state.
339    async fn handle_pending_state(
340        &self,
341        tx: TransactionRepoModel,
342    ) -> Result<TransactionRepoModel, TransactionError> {
343        if self.should_noop(&tx).await? {
344            debug!("preparing NOOP for pending transaction {}", tx.id);
345            let update = self.prepare_noop_update_request(&tx, false).await?;
346            let updated_tx = self
347                .transaction_repository()
348                .partial_update(tx.id.clone(), update)
349                .await?;
350
351            self.send_transaction_submit_job(&updated_tx).await?;
352            let res = self.send_transaction_update_notification(&updated_tx).await;
353            if let Err(e) = res {
354                error!(
355                    tx_id = %updated_tx.id,
356                    status = ?updated_tx.status,
357                    "sending transaction update notification failed for Pending state NOOP: {:?}",
358                    e
359                );
360            }
361            return Ok(updated_tx);
362        }
363
364        // Check if transaction is stuck in Pending (prepare job may have failed)
365        let age = get_age_since_created(&tx)?;
366        if age > get_evm_pending_recovery_trigger_timeout() {
367            warn!(
368                tx_id = %tx.id,
369                age_seconds = age.num_seconds(),
370                "transaction stuck in Pending, queuing prepare job"
371            );
372
373            // Re-queue prepare job
374            self.send_transaction_request_job(&tx).await?;
375        }
376
377        Ok(tx)
378    }
379
380    /// Handles transactions in the Mined state.
381    async fn handle_mined_state(
382        &self,
383        tx: TransactionRepoModel,
384    ) -> Result<TransactionRepoModel, TransactionError> {
385        self.update_transaction_status_if_needed(tx, TransactionStatus::Mined)
386            .await
387    }
388
389    /// Handles transactions in final states (Confirmed, Failed, Expired).
390    async fn handle_final_state(
391        &self,
392        tx: TransactionRepoModel,
393        status: TransactionStatus,
394    ) -> Result<TransactionRepoModel, TransactionError> {
395        self.update_transaction_status_if_needed(tx, status).await
396    }
397
398    /// Inherent status-handling method.
399    ///
400    /// This method encapsulates the full logic for handling transaction status,
401    /// including resubmission, NOOP replacement, timeout detection, and updating status.
402    pub async fn handle_status_impl(
403        &self,
404        tx: TransactionRepoModel,
405    ) -> Result<TransactionRepoModel, TransactionError> {
406        debug!("checking transaction status {}", tx.id);
407
408        // 1. Early return if final state
409        if is_final_state(&tx.status) {
410            debug!(status = ?tx.status, "transaction already in final state");
411            return Ok(tx);
412        }
413
414        // 2. Check transaction status first
415        // This allows fast transactions to update their status immediately,
416        // even if they're young (<20s). For Pending/Sent states, this returns
417        // early without querying the blockchain.
418        let status = self.check_transaction_status(&tx).await?;
419
420        debug!(
421            tx_id = %tx.id,
422            previous_status = ?tx.status,
423            new_status = ?status,
424            "transaction status check completed"
425        );
426
427        // 2.1. Reload transaction from DB if status changed
428        // This ensures we have fresh data if check_transaction_status triggered a recovery
429        // or any other update that modified the transaction in the database.
430        let tx = if status != tx.status {
431            debug!(
432                tx_id = %tx.id,
433                old_status = ?tx.status,
434                new_status = ?status,
435                "status changed during check, reloading transaction from DB to ensure fresh data"
436            );
437            self.transaction_repository()
438                .get_by_id(tx.id.clone())
439                .await?
440        } else {
441            tx
442        };
443
444        // 3. Check if too early for resubmission on in-progress transactions
445        // For Pending/Sent/Submitted states, defer resubmission logic and timeout checks
446        // if the transaction is too young. Just update status and return.
447        // For other states (Mined/Confirmed/Failed/etc), process immediately regardless of age.
448        if is_too_early_to_resubmit(&tx)? && is_pending_transaction(&status) {
449            // Update status if it changed, then return
450            return self.update_transaction_status_if_needed(tx, status).await;
451        }
452
453        // 4. Handle based on status (including complex operations like resubmission)
454        match status {
455            TransactionStatus::Pending => self.handle_pending_state(tx).await,
456            TransactionStatus::Sent => self.handle_sent_state(tx).await,
457            TransactionStatus::Submitted => self.handle_submitted_state(tx).await,
458            TransactionStatus::Mined => self.handle_mined_state(tx).await,
459            TransactionStatus::Confirmed
460            | TransactionStatus::Failed
461            | TransactionStatus::Expired
462            | TransactionStatus::Canceled => self.handle_final_state(tx, status).await,
463        }
464    }
465
466    /// Handle transactions stuck in Sent (prepared but not submitted)
467    async fn handle_sent_state(
468        &self,
469        tx: TransactionRepoModel,
470    ) -> Result<TransactionRepoModel, TransactionError> {
471        debug!(tx_id = %tx.id, "handling Sent state");
472
473        // Transaction was prepared but submission job may have failed
474        // Re-queue a resend job if it's been stuck for a while
475        let age_since_sent = get_age_since_status_change(&tx)?;
476
477        if age_since_sent > get_evm_resend_timeout() {
478            warn!(
479                tx_id = %tx.id,
480                age_seconds = age_since_sent.num_seconds(),
481                "transaction stuck in Sent, queuing resubmit job with repricing"
482            );
483
484            // Queue resubmit job to reprice the transaction for better acceptance
485            self.send_transaction_resubmit_job(&tx).await?;
486        }
487
488        self.update_transaction_status_if_needed(tx, TransactionStatus::Sent)
489            .await
490    }
491
492    /// Determines if we should attempt hash recovery for a stuck transaction.
493    ///
494    /// This is an expensive operation, so we only do it when:
495    /// - Transaction has been in Submitted status for a while (> 2 minutes)
496    /// - Transaction has had at least 2 resubmission attempts (hashes.len() > 1)
497    /// - Haven't tried recovery too recently (to avoid repeated attempts)
498    fn should_try_hash_recovery(
499        &self,
500        tx: &TransactionRepoModel,
501    ) -> Result<bool, TransactionError> {
502        // Only try recovery for transactions stuck in Submitted
503        if tx.status != TransactionStatus::Submitted {
504            return Ok(false);
505        }
506
507        // Must have multiple hashes (indicating resubmissions happened)
508        if tx.hashes.len() <= 1 {
509            return Ok(false);
510        }
511
512        // Only try if transaction has been stuck for a while
513        let age = get_age_of_sent_at(tx)?;
514        let min_age_for_recovery = get_evm_min_age_for_hash_recovery();
515
516        if age < min_age_for_recovery {
517            return Ok(false);
518        }
519
520        // Check if we've had enough resubmission attempts (more attempts = more likely to have wrong hash)
521        // Only try recovery if we have at least 3 hashes (2 resubmissions)
522        if tx.hashes.len() < EVM_MIN_HASHES_FOR_RECOVERY {
523            return Ok(false);
524        }
525
526        Ok(true)
527    }
528
529    /// Attempts to recover transaction status by checking all historical hashes.
530    ///
531    /// When a transaction is resubmitted multiple times due to timeouts, the database
532    /// may contain multiple hashes. The "current" hash (network_data.hash) might not
533    /// be the one that actually got mined. This method checks all historical hashes
534    /// to find if any were mined, and updates the database with the correct one.
535    ///
536    /// Returns the updated transaction model if recovery was successful, None otherwise.
537    async fn try_recover_with_historical_hashes(
538        &self,
539        tx: &TransactionRepoModel,
540        evm_data: &crate::models::EvmTransactionData,
541    ) -> Result<Option<TransactionRepoModel>, TransactionError> {
542        warn!(
543            tx_id = %tx.id,
544            current_hash = ?evm_data.hash,
545            total_hashes = %tx.hashes.len(),
546            "attempting hash recovery - checking historical hashes"
547        );
548
549        // Check each historical hash (most recent first, since it's more likely)
550        for (idx, historical_hash) in tx.hashes.iter().rev().enumerate() {
551            // Skip if this is the current hash (already checked)
552            if Some(historical_hash) == evm_data.hash.as_ref() {
553                continue;
554            }
555
556            debug!(
557                tx_id = %tx.id,
558                hash = %historical_hash,
559                index = %idx,
560                "checking historical hash"
561            );
562
563            // Try to get receipt for this hash
564            match self
565                .provider()
566                .get_transaction_receipt(historical_hash)
567                .await
568            {
569                Ok(Some(receipt)) => {
570                    warn!(
571                        tx_id = %tx.id,
572                        mined_hash = %historical_hash,
573                        wrong_hash = ?evm_data.hash,
574                        block_number = ?receipt.block_number,
575                        "RECOVERED: found mined transaction with historical hash - correcting database"
576                    );
577
578                    // Update with correct hash and Mined status
579                    // Let the normal status check flow handle confirmation checking
580                    let updated_tx = self
581                        .update_transaction_with_corrected_hash(
582                            tx,
583                            evm_data,
584                            historical_hash,
585                            TransactionStatus::Mined,
586                        )
587                        .await?;
588
589                    return Ok(Some(updated_tx));
590                }
591                Ok(None) => {
592                    // This hash not found either, continue to next
593                    continue;
594                }
595                Err(e) => {
596                    // Network error, log but continue checking other hashes
597                    warn!(
598                        tx_id = %tx.id,
599                        hash = %historical_hash,
600                        error = %e,
601                        "error checking historical hash, continuing to next"
602                    );
603                    continue;
604                }
605            }
606        }
607
608        // None of the historical hashes found on-chain
609        debug!(
610            tx_id = %tx.id,
611            "hash recovery completed - no historical hashes found on-chain"
612        );
613        Ok(None)
614    }
615
616    /// Updates transaction with the corrected hash and status
617    ///
618    /// Returns the updated transaction model and sends a notification about the status change.
619    async fn update_transaction_with_corrected_hash(
620        &self,
621        tx: &TransactionRepoModel,
622        evm_data: &crate::models::EvmTransactionData,
623        correct_hash: &str,
624        status: TransactionStatus,
625    ) -> Result<TransactionRepoModel, TransactionError> {
626        let mut corrected_data = evm_data.clone();
627        corrected_data.hash = Some(correct_hash.to_string());
628
629        let updated_tx = self
630            .transaction_repository()
631            .partial_update(
632                tx.id.clone(),
633                TransactionUpdateRequest {
634                    network_data: Some(NetworkTransactionData::Evm(corrected_data)),
635                    status: Some(status),
636                    ..Default::default()
637                },
638            )
639            .await?;
640
641        // Send notification about the recovered transaction
642        if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
643            error!(
644                tx_id = %updated_tx.id,
645                error = %e,
646                "failed to send notification after hash recovery"
647            );
648        }
649
650        Ok(updated_tx)
651    }
652}
653
654#[cfg(test)]
655mod tests {
656    use crate::{
657        config::{EvmNetworkConfig, NetworkConfigCommon},
658        domain::transaction::evm::{EvmRelayerTransaction, MockPriceCalculatorTrait},
659        jobs::MockJobProducerTrait,
660        models::{
661            evm::Speed, EvmTransactionData, NetworkConfigData, NetworkRepoModel,
662            NetworkTransactionData, NetworkType, RelayerEvmPolicy, RelayerNetworkPolicy,
663            RelayerRepoModel, TransactionReceipt, TransactionRepoModel, TransactionStatus, U256,
664        },
665        repositories::{
666            MockNetworkRepository, MockRelayerRepository, MockTransactionCounterTrait,
667            MockTransactionRepository,
668        },
669        services::{provider::MockEvmProviderTrait, signer::MockSigner},
670    };
671    use alloy::{
672        consensus::{Eip658Value, Receipt, ReceiptWithBloom},
673        network::AnyReceiptEnvelope,
674        primitives::{b256, Address, BlockHash, Bloom, TxHash},
675    };
676    use chrono::{Duration, Utc};
677    use std::sync::Arc;
678
679    /// Helper struct holding all the mocks we often need
680    pub struct TestMocks {
681        pub provider: MockEvmProviderTrait,
682        pub relayer_repo: MockRelayerRepository,
683        pub network_repo: MockNetworkRepository,
684        pub tx_repo: MockTransactionRepository,
685        pub job_producer: MockJobProducerTrait,
686        pub signer: MockSigner,
687        pub counter: MockTransactionCounterTrait,
688        pub price_calc: MockPriceCalculatorTrait,
689    }
690
691    /// Returns a default `TestMocks` with zero-configuration stubs.
692    /// You can override expectations in each test as needed.
693    pub fn default_test_mocks() -> TestMocks {
694        TestMocks {
695            provider: MockEvmProviderTrait::new(),
696            relayer_repo: MockRelayerRepository::new(),
697            network_repo: MockNetworkRepository::new(),
698            tx_repo: MockTransactionRepository::new(),
699            job_producer: MockJobProducerTrait::new(),
700            signer: MockSigner::new(),
701            counter: MockTransactionCounterTrait::new(),
702            price_calc: MockPriceCalculatorTrait::new(),
703        }
704    }
705
706    /// Returns a `TestMocks` with network repository configured for prepare_noop_update_request tests.
707    pub fn default_test_mocks_with_network() -> TestMocks {
708        let mut mocks = default_test_mocks();
709        // Set up default expectation for get_by_chain_id that prepare_noop_update_request tests need
710        mocks
711            .network_repo
712            .expect_get_by_chain_id()
713            .returning(|network_type, chain_id| {
714                if network_type == NetworkType::Evm && chain_id == 1 {
715                    Ok(Some(create_test_network_model()))
716                } else {
717                    Ok(None)
718                }
719            });
720        mocks
721    }
722
723    /// Creates a test NetworkRepoModel for chain_id 1 (mainnet)
724    pub fn create_test_network_model() -> NetworkRepoModel {
725        let evm_config = EvmNetworkConfig {
726            common: NetworkConfigCommon {
727                network: "mainnet".to_string(),
728                from: None,
729                rpc_urls: Some(vec!["https://rpc.example.com".to_string()]),
730                explorer_urls: Some(vec!["https://explorer.example.com".to_string()]),
731                average_blocktime_ms: Some(12000),
732                is_testnet: Some(false),
733                tags: Some(vec!["mainnet".to_string()]),
734            },
735            chain_id: Some(1),
736            required_confirmations: Some(12),
737            features: Some(vec!["eip1559".to_string()]),
738            symbol: Some("ETH".to_string()),
739            gas_price_cache: None,
740        };
741        NetworkRepoModel {
742            id: "evm:mainnet".to_string(),
743            name: "mainnet".to_string(),
744            network_type: NetworkType::Evm,
745            config: NetworkConfigData::Evm(evm_config),
746        }
747    }
748
749    /// Creates a test NetworkRepoModel for chain_id 42161 (Arbitrum-like) with no-mempool tag
750    pub fn create_test_no_mempool_network_model() -> NetworkRepoModel {
751        let evm_config = EvmNetworkConfig {
752            common: NetworkConfigCommon {
753                network: "arbitrum".to_string(),
754                from: None,
755                rpc_urls: Some(vec!["https://arb-rpc.example.com".to_string()]),
756                explorer_urls: Some(vec!["https://arb-explorer.example.com".to_string()]),
757                average_blocktime_ms: Some(1000),
758                is_testnet: Some(false),
759                tags: Some(vec![
760                    "arbitrum".to_string(),
761                    "rollup".to_string(),
762                    "no-mempool".to_string(),
763                ]),
764            },
765            chain_id: Some(42161),
766            required_confirmations: Some(12),
767            features: Some(vec!["eip1559".to_string()]),
768            symbol: Some("ETH".to_string()),
769            gas_price_cache: None,
770        };
771        NetworkRepoModel {
772            id: "evm:arbitrum".to_string(),
773            name: "arbitrum".to_string(),
774            network_type: NetworkType::Evm,
775            config: NetworkConfigData::Evm(evm_config),
776        }
777    }
778
779    /// Minimal "builder" for TransactionRepoModel.
780    /// Allows quick creation of a test transaction with default fields,
781    /// then updates them based on the provided status or overrides.
782    pub fn make_test_transaction(status: TransactionStatus) -> TransactionRepoModel {
783        TransactionRepoModel {
784            id: "test-tx-id".to_string(),
785            relayer_id: "test-relayer-id".to_string(),
786            status,
787            status_reason: None,
788            created_at: Utc::now().to_rfc3339(),
789            sent_at: None,
790            confirmed_at: None,
791            valid_until: None,
792            delete_at: None,
793            network_type: NetworkType::Evm,
794            network_data: NetworkTransactionData::Evm(EvmTransactionData {
795                chain_id: 1,
796                from: "0xSender".to_string(),
797                to: Some("0xRecipient".to_string()),
798                value: U256::from(0),
799                data: Some("0xData".to_string()),
800                gas_limit: Some(21000),
801                gas_price: Some(20000000000),
802                max_fee_per_gas: None,
803                max_priority_fee_per_gas: None,
804                nonce: None,
805                signature: None,
806                hash: None,
807                speed: Some(Speed::Fast),
808                raw: None,
809            }),
810            priced_at: None,
811            hashes: Vec::new(),
812            noop_count: None,
813            is_canceled: Some(false),
814        }
815    }
816
817    /// Minimal "builder" for EvmRelayerTransaction.
818    /// Takes mock dependencies as arguments.
819    pub fn make_test_evm_relayer_transaction(
820        relayer: RelayerRepoModel,
821        mocks: TestMocks,
822    ) -> EvmRelayerTransaction<
823        MockEvmProviderTrait,
824        MockRelayerRepository,
825        MockNetworkRepository,
826        MockTransactionRepository,
827        MockJobProducerTrait,
828        MockSigner,
829        MockTransactionCounterTrait,
830        MockPriceCalculatorTrait,
831    > {
832        EvmRelayerTransaction::new(
833            relayer,
834            mocks.provider,
835            Arc::new(mocks.relayer_repo),
836            Arc::new(mocks.network_repo),
837            Arc::new(mocks.tx_repo),
838            Arc::new(mocks.counter),
839            Arc::new(mocks.job_producer),
840            mocks.price_calc,
841            mocks.signer,
842        )
843        .unwrap()
844    }
845
846    fn create_test_relayer() -> RelayerRepoModel {
847        RelayerRepoModel {
848            id: "test-relayer-id".to_string(),
849            name: "Test Relayer".to_string(),
850            paused: false,
851            system_disabled: false,
852            network: "test_network".to_string(),
853            network_type: NetworkType::Evm,
854            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
855            signer_id: "test_signer".to_string(),
856            address: "0x".to_string(),
857            notification_id: None,
858            custom_rpc_urls: None,
859            ..Default::default()
860        }
861    }
862
863    fn make_mock_receipt(status: bool, block_number: Option<u64>) -> TransactionReceipt {
864        // Use some placeholder values for minimal completeness
865        let tx_hash = TxHash::from(b256!(
866            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
867        ));
868        let block_hash = BlockHash::from(b256!(
869            "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
870        ));
871        let from_address = Address::from([0x11; 20]);
872
873        TransactionReceipt {
874            inner: alloy::rpc::types::TransactionReceipt {
875                inner: AnyReceiptEnvelope {
876                    inner: ReceiptWithBloom {
877                        receipt: Receipt {
878                            status: Eip658Value::Eip658(status), // determines success/fail
879                            cumulative_gas_used: 0,
880                            logs: vec![],
881                        },
882                        logs_bloom: Bloom::ZERO,
883                    },
884                    r#type: 0, // Legacy transaction type
885                },
886                transaction_hash: tx_hash,
887                transaction_index: Some(0),
888                block_hash: block_number.map(|_| block_hash), // only set if mined
889                block_number,
890                gas_used: 21000,
891                effective_gas_price: 1000,
892                blob_gas_used: None,
893                blob_gas_price: None,
894                from: from_address,
895                to: None,
896                contract_address: None,
897            },
898            other: Default::default(),
899        }
900    }
901
902    // Tests for `check_transaction_status`
903    mod check_transaction_status_tests {
904        use super::*;
905
906        #[tokio::test]
907        async fn test_not_mined() {
908            let mut mocks = default_test_mocks();
909            let relayer = create_test_relayer();
910            let mut tx = make_test_transaction(TransactionStatus::Submitted);
911
912            // Provide a hash so we can check for receipt
913            if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
914                evm_data.hash = Some("0xFakeHash".to_string());
915            }
916
917            // Mock that get_transaction_receipt returns None (not mined)
918            mocks
919                .provider
920                .expect_get_transaction_receipt()
921                .returning(|_| Box::pin(async { Ok(None) }));
922
923            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
924
925            let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
926            assert_eq!(status, TransactionStatus::Submitted);
927        }
928
929        #[tokio::test]
930        async fn test_mined_but_not_confirmed() {
931            let mut mocks = default_test_mocks();
932            let relayer = create_test_relayer();
933            let mut tx = make_test_transaction(TransactionStatus::Submitted);
934
935            if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
936                evm_data.hash = Some("0xFakeHash".to_string());
937            }
938
939            // Mock a mined receipt with block_number = 100
940            mocks
941                .provider
942                .expect_get_transaction_receipt()
943                .returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) }));
944
945            // Mock block_number that hasn't reached the confirmation threshold
946            mocks
947                .provider
948                .expect_get_block_number()
949                .return_once(|| Box::pin(async { Ok(100) }));
950
951            // Mock network repository to return a test network model
952            mocks
953                .network_repo
954                .expect_get_by_chain_id()
955                .returning(|_, _| Ok(Some(create_test_network_model())));
956
957            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
958
959            let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
960            assert_eq!(status, TransactionStatus::Mined);
961        }
962
963        #[tokio::test]
964        async fn test_confirmed() {
965            let mut mocks = default_test_mocks();
966            let relayer = create_test_relayer();
967            let mut tx = make_test_transaction(TransactionStatus::Submitted);
968
969            if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
970                evm_data.hash = Some("0xFakeHash".to_string());
971            }
972
973            // Mock a mined receipt with block_number = 100
974            mocks
975                .provider
976                .expect_get_transaction_receipt()
977                .returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) }));
978
979            // Mock block_number that meets the confirmation threshold
980            mocks
981                .provider
982                .expect_get_block_number()
983                .return_once(|| Box::pin(async { Ok(113) }));
984
985            // Mock network repository to return a test network model
986            mocks
987                .network_repo
988                .expect_get_by_chain_id()
989                .returning(|_, _| Ok(Some(create_test_network_model())));
990
991            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
992
993            let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
994            assert_eq!(status, TransactionStatus::Confirmed);
995        }
996
997        #[tokio::test]
998        async fn test_failed() {
999            let mut mocks = default_test_mocks();
1000            let relayer = create_test_relayer();
1001            let mut tx = make_test_transaction(TransactionStatus::Submitted);
1002
1003            if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1004                evm_data.hash = Some("0xFakeHash".to_string());
1005            }
1006
1007            // Mock a mined receipt with failure
1008            mocks
1009                .provider
1010                .expect_get_transaction_receipt()
1011                .returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(false, Some(100)))) }));
1012
1013            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1014
1015            let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
1016            assert_eq!(status, TransactionStatus::Failed);
1017        }
1018    }
1019
1020    // Tests for `should_resubmit`
1021    mod should_resubmit_tests {
1022        use super::*;
1023        use crate::models::TransactionError;
1024
1025        #[tokio::test]
1026        async fn test_should_resubmit_true() {
1027            let mut mocks = default_test_mocks();
1028            let relayer = create_test_relayer();
1029
1030            // Set sent_at to 600 seconds ago to force resubmission
1031            let mut tx = make_test_transaction(TransactionStatus::Submitted);
1032            tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
1033
1034            // Mock network repository to return a regular network model
1035            mocks
1036                .network_repo
1037                .expect_get_by_chain_id()
1038                .returning(|_, _| Ok(Some(create_test_network_model())));
1039
1040            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1041            let res = evm_transaction.should_resubmit(&tx).await.unwrap();
1042            assert!(res, "Transaction should be resubmitted after timeout.");
1043        }
1044
1045        #[tokio::test]
1046        async fn test_should_resubmit_false() {
1047            let mut mocks = default_test_mocks();
1048            let relayer = create_test_relayer();
1049
1050            // Make a transaction with status Submitted but recently sent
1051            let mut tx = make_test_transaction(TransactionStatus::Submitted);
1052            tx.sent_at = Some(Utc::now().to_rfc3339());
1053
1054            // Mock network repository to return a regular network model
1055            mocks
1056                .network_repo
1057                .expect_get_by_chain_id()
1058                .returning(|_, _| Ok(Some(create_test_network_model())));
1059
1060            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1061            let res = evm_transaction.should_resubmit(&tx).await.unwrap();
1062            assert!(!res, "Transaction should not be resubmitted immediately.");
1063        }
1064
1065        #[tokio::test]
1066        async fn test_should_resubmit_true_for_no_mempool_network() {
1067            let mut mocks = default_test_mocks();
1068            let relayer = create_test_relayer();
1069
1070            // Set up a transaction that would normally be resubmitted (sent_at long ago)
1071            let mut tx = make_test_transaction(TransactionStatus::Submitted);
1072            tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
1073
1074            // Set chain_id to match the no-mempool network
1075            if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1076                evm_data.chain_id = 42161; // Arbitrum chain ID
1077            }
1078
1079            // Mock network repository to return a no-mempool network model
1080            mocks
1081                .network_repo
1082                .expect_get_by_chain_id()
1083                .returning(|_, _| Ok(Some(create_test_no_mempool_network_model())));
1084
1085            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1086            let res = evm_transaction.should_resubmit(&tx).await.unwrap();
1087            assert!(
1088                res,
1089                "Transaction should be resubmitted for no-mempool networks."
1090            );
1091        }
1092
1093        #[tokio::test]
1094        async fn test_should_resubmit_network_not_found() {
1095            let mut mocks = default_test_mocks();
1096            let relayer = create_test_relayer();
1097
1098            let mut tx = make_test_transaction(TransactionStatus::Submitted);
1099            tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
1100
1101            // Mock network repository to return None (network not found)
1102            mocks
1103                .network_repo
1104                .expect_get_by_chain_id()
1105                .returning(|_, _| Ok(None));
1106
1107            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1108            let result = evm_transaction.should_resubmit(&tx).await;
1109
1110            assert!(
1111                result.is_err(),
1112                "should_resubmit should return error when network not found"
1113            );
1114            let error = result.unwrap_err();
1115            match error {
1116                TransactionError::UnexpectedError(msg) => {
1117                    assert!(msg.contains("Network with chain id 1 not found"));
1118                }
1119                _ => panic!("Expected UnexpectedError for network not found"),
1120            }
1121        }
1122
1123        #[tokio::test]
1124        async fn test_should_resubmit_network_conversion_error() {
1125            let mut mocks = default_test_mocks();
1126            let relayer = create_test_relayer();
1127
1128            let mut tx = make_test_transaction(TransactionStatus::Submitted);
1129            tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
1130
1131            // Create a network model with invalid EVM config (missing chain_id)
1132            let invalid_evm_config = EvmNetworkConfig {
1133                common: NetworkConfigCommon {
1134                    network: "invalid-network".to_string(),
1135                    from: None,
1136                    rpc_urls: Some(vec!["https://rpc.example.com".to_string()]),
1137                    explorer_urls: Some(vec!["https://explorer.example.com".to_string()]),
1138                    average_blocktime_ms: Some(12000),
1139                    is_testnet: Some(false),
1140                    tags: Some(vec!["testnet".to_string()]),
1141                },
1142                chain_id: None, // This will cause the conversion to fail
1143                required_confirmations: Some(12),
1144                features: Some(vec!["eip1559".to_string()]),
1145                symbol: Some("ETH".to_string()),
1146                gas_price_cache: None,
1147            };
1148            let invalid_network = NetworkRepoModel {
1149                id: "evm:invalid".to_string(),
1150                name: "invalid-network".to_string(),
1151                network_type: NetworkType::Evm,
1152                config: NetworkConfigData::Evm(invalid_evm_config),
1153            };
1154
1155            // Mock network repository to return the invalid network model
1156            mocks
1157                .network_repo
1158                .expect_get_by_chain_id()
1159                .returning(move |_, _| Ok(Some(invalid_network.clone())));
1160
1161            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1162            let result = evm_transaction.should_resubmit(&tx).await;
1163
1164            assert!(
1165                result.is_err(),
1166                "should_resubmit should return error when network conversion fails"
1167            );
1168            let error = result.unwrap_err();
1169            match error {
1170                TransactionError::UnexpectedError(msg) => {
1171                    assert!(msg.contains("Error converting network model to EvmNetwork"));
1172                }
1173                _ => panic!("Expected UnexpectedError for network conversion failure"),
1174            }
1175        }
1176    }
1177
1178    // Tests for `should_noop`
1179    mod should_noop_tests {
1180        use super::*;
1181
1182        #[tokio::test]
1183        async fn test_expired_transaction_triggers_noop() {
1184            let mut mocks = default_test_mocks();
1185            let relayer = create_test_relayer();
1186
1187            let mut tx = make_test_transaction(TransactionStatus::Submitted);
1188            // Force the transaction to be "expired" by setting valid_until in the past
1189            tx.valid_until = Some((Utc::now() - Duration::seconds(10)).to_rfc3339());
1190
1191            // Mock network repository to return a test network model
1192            mocks
1193                .network_repo
1194                .expect_get_by_chain_id()
1195                .returning(|_, _| Ok(Some(create_test_network_model())));
1196
1197            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1198            let res = evm_transaction.should_noop(&tx).await.unwrap();
1199            assert!(res, "Expired transaction should be replaced with a NOOP.");
1200        }
1201
1202        #[tokio::test]
1203        async fn test_too_many_noop_attempts_returns_false() {
1204            let mocks = default_test_mocks();
1205            let relayer = create_test_relayer();
1206
1207            let mut tx = make_test_transaction(TransactionStatus::Submitted);
1208            tx.noop_count = Some(51); // Max is 50, so this should return false
1209
1210            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1211            let res = evm_transaction.should_noop(&tx).await.unwrap();
1212            assert!(
1213                !res,
1214                "Transaction with too many NOOP attempts should not be replaced."
1215            );
1216        }
1217
1218        #[tokio::test]
1219        async fn test_already_noop_returns_false() {
1220            let mut mocks = default_test_mocks();
1221            let relayer = create_test_relayer();
1222
1223            let mut tx = make_test_transaction(TransactionStatus::Submitted);
1224            // Make it a NOOP by setting to=None and value=0
1225            if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1226                evm_data.to = None;
1227                evm_data.value = U256::from(0);
1228            }
1229
1230            mocks
1231                .network_repo
1232                .expect_get_by_chain_id()
1233                .returning(|_, _| Ok(Some(create_test_network_model())));
1234
1235            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1236            let res = evm_transaction.should_noop(&tx).await.unwrap();
1237            assert!(
1238                !res,
1239                "Transaction that is already a NOOP should not be replaced."
1240            );
1241        }
1242
1243        #[tokio::test]
1244        async fn test_rollup_with_too_many_attempts_triggers_noop() {
1245            let mut mocks = default_test_mocks();
1246            let relayer = create_test_relayer();
1247
1248            let mut tx = make_test_transaction(TransactionStatus::Submitted);
1249            // Set chain_id to Arbitrum (rollup network)
1250            if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1251                evm_data.chain_id = 42161; // Arbitrum
1252            }
1253            // Set enough hashes to trigger too_many_attempts (> 50)
1254            tx.hashes = vec!["0xHash1".to_string(); 51];
1255
1256            // Mock network repository to return Arbitrum network
1257            mocks
1258                .network_repo
1259                .expect_get_by_chain_id()
1260                .returning(|_, _| Ok(Some(create_test_no_mempool_network_model())));
1261
1262            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1263            let res = evm_transaction.should_noop(&tx).await.unwrap();
1264            assert!(
1265                res,
1266                "Rollup transaction with too many attempts should be replaced with NOOP."
1267            );
1268        }
1269
1270        #[tokio::test]
1271        async fn test_pending_state_timeout_triggers_noop() {
1272            let mut mocks = default_test_mocks();
1273            let relayer = create_test_relayer();
1274
1275            let mut tx = make_test_transaction(TransactionStatus::Pending);
1276            // Set created_at to 3 minutes ago (> 2 minute timeout)
1277            tx.created_at = (Utc::now() - Duration::minutes(3)).to_rfc3339();
1278
1279            mocks
1280                .network_repo
1281                .expect_get_by_chain_id()
1282                .returning(|_, _| Ok(Some(create_test_network_model())));
1283
1284            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1285            let res = evm_transaction.should_noop(&tx).await.unwrap();
1286            assert!(
1287                res,
1288                "Pending transaction stuck for >2 minutes should be replaced with NOOP."
1289            );
1290        }
1291
1292        #[tokio::test]
1293        async fn test_valid_transaction_returns_false() {
1294            let mut mocks = default_test_mocks();
1295            let relayer = create_test_relayer();
1296
1297            let tx = make_test_transaction(TransactionStatus::Submitted);
1298            // Transaction is recent, not expired, not on rollup, no issues
1299
1300            mocks
1301                .network_repo
1302                .expect_get_by_chain_id()
1303                .returning(|_, _| Ok(Some(create_test_network_model())));
1304
1305            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1306            let res = evm_transaction.should_noop(&tx).await.unwrap();
1307            assert!(!res, "Valid transaction should not be replaced with NOOP.");
1308        }
1309    }
1310
1311    // Tests for `update_transaction_status_if_needed`
1312    mod update_transaction_status_tests {
1313        use super::*;
1314
1315        #[tokio::test]
1316        async fn test_no_update_when_status_is_same() {
1317            // Create mocks, relayer, and a transaction with status Submitted.
1318            let mocks = default_test_mocks();
1319            let relayer = create_test_relayer();
1320            let tx = make_test_transaction(TransactionStatus::Submitted);
1321            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1322
1323            // When new status is the same as current, update_transaction_status_if_needed
1324            // should simply return the original transaction.
1325            let updated_tx = evm_transaction
1326                .update_transaction_status_if_needed(tx.clone(), TransactionStatus::Submitted)
1327                .await
1328                .unwrap();
1329            assert_eq!(updated_tx.status, TransactionStatus::Submitted);
1330            assert_eq!(updated_tx.id, tx.id);
1331        }
1332
1333        #[tokio::test]
1334        async fn test_updates_when_status_differs() {
1335            let mut mocks = default_test_mocks();
1336            let relayer = create_test_relayer();
1337            let tx = make_test_transaction(TransactionStatus::Submitted);
1338
1339            // Mock partial_update to return a transaction with new status
1340            mocks
1341                .tx_repo
1342                .expect_partial_update()
1343                .returning(|_, update| {
1344                    let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1345                    updated_tx.status = update.status.unwrap_or(updated_tx.status);
1346                    Ok(updated_tx)
1347                });
1348
1349            // Mock notification job
1350            mocks
1351                .job_producer
1352                .expect_produce_send_notification_job()
1353                .returning(|_, _| Box::pin(async { Ok(()) }));
1354
1355            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1356            let updated_tx = evm_transaction
1357                .update_transaction_status_if_needed(tx.clone(), TransactionStatus::Mined)
1358                .await
1359                .unwrap();
1360
1361            assert_eq!(updated_tx.status, TransactionStatus::Mined);
1362        }
1363    }
1364
1365    // Tests for `handle_sent_state`
1366    mod handle_sent_state_tests {
1367        use super::*;
1368
1369        #[tokio::test]
1370        async fn test_sent_state_recent_no_resend() {
1371            let mut mocks = default_test_mocks();
1372            let relayer = create_test_relayer();
1373
1374            let mut tx = make_test_transaction(TransactionStatus::Sent);
1375            // Set sent_at to recent (e.g., 10 seconds ago)
1376            tx.sent_at = Some((Utc::now() - Duration::seconds(10)).to_rfc3339());
1377
1378            // Mock status check job scheduling
1379            mocks
1380                .job_producer
1381                .expect_produce_check_transaction_status_job()
1382                .returning(|_, _| Box::pin(async { Ok(()) }));
1383
1384            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1385            let result = evm_transaction.handle_sent_state(tx.clone()).await.unwrap();
1386
1387            assert_eq!(result.status, TransactionStatus::Sent);
1388        }
1389
1390        #[tokio::test]
1391        async fn test_sent_state_stuck_schedules_resubmit() {
1392            let mut mocks = default_test_mocks();
1393            let relayer = create_test_relayer();
1394
1395            let mut tx = make_test_transaction(TransactionStatus::Sent);
1396            // Set sent_at to long ago (> 30 seconds for resend timeout)
1397            tx.sent_at = Some((Utc::now() - Duration::seconds(60)).to_rfc3339());
1398
1399            // Mock resubmit job scheduling
1400            mocks
1401                .job_producer
1402                .expect_produce_submit_transaction_job()
1403                .returning(|_, _| Box::pin(async { Ok(()) }));
1404
1405            // Mock status check job scheduling
1406            mocks
1407                .job_producer
1408                .expect_produce_check_transaction_status_job()
1409                .returning(|_, _| Box::pin(async { Ok(()) }));
1410
1411            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1412            let result = evm_transaction.handle_sent_state(tx.clone()).await.unwrap();
1413
1414            assert_eq!(result.status, TransactionStatus::Sent);
1415        }
1416    }
1417
1418    // Tests for `prepare_noop_update_request`
1419    mod prepare_noop_update_request_tests {
1420        use super::*;
1421
1422        #[tokio::test]
1423        async fn test_noop_request_without_cancellation() {
1424            // Create a transaction with an initial noop_count of 2 and is_canceled set to false.
1425            let mocks = default_test_mocks_with_network();
1426            let relayer = create_test_relayer();
1427            let mut tx = make_test_transaction(TransactionStatus::Submitted);
1428            tx.noop_count = Some(2);
1429            tx.is_canceled = Some(false);
1430
1431            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1432            let update_req = evm_transaction
1433                .prepare_noop_update_request(&tx, false)
1434                .await
1435                .unwrap();
1436
1437            // NOOP count should be incremented: 2 becomes 3.
1438            assert_eq!(update_req.noop_count, Some(3));
1439            // When not cancelling, the is_canceled flag should remain as in the original transaction.
1440            assert_eq!(update_req.is_canceled, Some(false));
1441        }
1442
1443        #[tokio::test]
1444        async fn test_noop_request_with_cancellation() {
1445            // Create a transaction with no initial noop_count (None) and is_canceled false.
1446            let mocks = default_test_mocks_with_network();
1447            let relayer = create_test_relayer();
1448            let mut tx = make_test_transaction(TransactionStatus::Submitted);
1449            tx.noop_count = None;
1450            tx.is_canceled = Some(false);
1451
1452            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1453            let update_req = evm_transaction
1454                .prepare_noop_update_request(&tx, true)
1455                .await
1456                .unwrap();
1457
1458            // NOOP count should default to 1.
1459            assert_eq!(update_req.noop_count, Some(1));
1460            // When cancelling, the is_canceled flag should be forced to true.
1461            assert_eq!(update_req.is_canceled, Some(true));
1462        }
1463    }
1464
1465    // Tests for `handle_submitted_state`
1466    mod handle_submitted_state_tests {
1467        use super::*;
1468
1469        #[tokio::test]
1470        async fn test_schedules_resubmit_job() {
1471            let mut mocks = default_test_mocks();
1472            let relayer = create_test_relayer();
1473
1474            // Set sent_at far in the past to force resubmission
1475            let mut tx = make_test_transaction(TransactionStatus::Submitted);
1476            tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
1477
1478            // Mock network repository to return a test network model for should_noop check
1479            mocks
1480                .network_repo
1481                .expect_get_by_chain_id()
1482                .returning(|_, _| Ok(Some(create_test_network_model())));
1483
1484            // Expect the resubmit job to be produced
1485            mocks
1486                .job_producer
1487                .expect_produce_submit_transaction_job()
1488                .returning(|_, _| Box::pin(async { Ok(()) }));
1489
1490            // Expect status check to be scheduled
1491            mocks
1492                .job_producer
1493                .expect_produce_check_transaction_status_job()
1494                .returning(|_, _| Box::pin(async { Ok(()) }));
1495
1496            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1497            let updated_tx = evm_transaction.handle_submitted_state(tx).await.unwrap();
1498
1499            // We remain in "Submitted" after scheduling the resubmit
1500            assert_eq!(updated_tx.status, TransactionStatus::Submitted);
1501        }
1502    }
1503
1504    // Tests for `handle_pending_state`
1505    mod handle_pending_state_tests {
1506        use super::*;
1507
1508        #[tokio::test]
1509        async fn test_pending_state_no_noop() {
1510            // Create a pending transaction that is fresh (created now).
1511            let mut mocks = default_test_mocks();
1512            let relayer = create_test_relayer();
1513            let mut tx = make_test_transaction(TransactionStatus::Pending);
1514            tx.created_at = Utc::now().to_rfc3339(); // less than one minute old
1515
1516            // Mock network repository to return a test network model
1517            mocks
1518                .network_repo
1519                .expect_get_by_chain_id()
1520                .returning(|_, _| Ok(Some(create_test_network_model())));
1521
1522            // Expect status check to be scheduled when not doing NOOP
1523            mocks
1524                .job_producer
1525                .expect_produce_check_transaction_status_job()
1526                .returning(|_, _| Box::pin(async { Ok(()) }));
1527
1528            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1529            let result = evm_transaction
1530                .handle_pending_state(tx.clone())
1531                .await
1532                .unwrap();
1533
1534            // When should_noop returns false the original transaction is returned unchanged.
1535            assert_eq!(result.id, tx.id);
1536            assert_eq!(result.status, tx.status);
1537            assert_eq!(result.noop_count, tx.noop_count);
1538        }
1539
1540        #[tokio::test]
1541        async fn test_pending_state_with_noop() {
1542            // Create a pending transaction that is old (created 2 minutes ago)
1543            let mut mocks = default_test_mocks();
1544            let relayer = create_test_relayer();
1545            let mut tx = make_test_transaction(TransactionStatus::Pending);
1546            tx.created_at = (Utc::now() - Duration::minutes(2)).to_rfc3339();
1547
1548            // Mock network repository to return a test network model
1549            mocks
1550                .network_repo
1551                .expect_get_by_chain_id()
1552                .returning(|_, _| Ok(Some(create_test_network_model())));
1553
1554            // Expect partial_update to be called and simulate a NOOP update by setting noop_count.
1555            let tx_clone = tx.clone();
1556            mocks
1557                .tx_repo
1558                .expect_partial_update()
1559                .returning(move |_, update| {
1560                    let mut updated_tx = tx_clone.clone();
1561                    updated_tx.noop_count = update.noop_count;
1562                    Ok(updated_tx)
1563                });
1564            // Expect that a submit job and notification are produced.
1565            mocks
1566                .job_producer
1567                .expect_produce_submit_transaction_job()
1568                .returning(|_, _| Box::pin(async { Ok(()) }));
1569            mocks
1570                .job_producer
1571                .expect_produce_send_notification_job()
1572                .returning(|_, _| Box::pin(async { Ok(()) }));
1573
1574            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1575            let result = evm_transaction
1576                .handle_pending_state(tx.clone())
1577                .await
1578                .unwrap();
1579
1580            // Since should_noop returns true, the returned transaction should have a nonzero noop_count.
1581            assert!(result.noop_count.unwrap_or(0) > 0);
1582        }
1583    }
1584
1585    // Tests for `handle_mined_state`
1586    mod handle_mined_state_tests {
1587        use super::*;
1588
1589        #[tokio::test]
1590        async fn test_updates_status_and_schedules_check() {
1591            let mut mocks = default_test_mocks();
1592            let relayer = create_test_relayer();
1593            // Create a transaction in Submitted state (the mined branch is reached via status check).
1594            let tx = make_test_transaction(TransactionStatus::Submitted);
1595
1596            // Expect schedule_status_check to be called with delay 5.
1597            mocks
1598                .job_producer
1599                .expect_produce_check_transaction_status_job()
1600                .returning(|_, _| Box::pin(async { Ok(()) }));
1601            // Expect partial_update to update the transaction status to Mined.
1602            mocks
1603                .tx_repo
1604                .expect_partial_update()
1605                .returning(|_, update| {
1606                    let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1607                    updated_tx.status = update.status.unwrap_or(updated_tx.status);
1608                    Ok(updated_tx)
1609                });
1610
1611            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1612            let result = evm_transaction
1613                .handle_mined_state(tx.clone())
1614                .await
1615                .unwrap();
1616            assert_eq!(result.status, TransactionStatus::Mined);
1617        }
1618    }
1619
1620    // Tests for `handle_final_state`
1621    mod handle_final_state_tests {
1622        use super::*;
1623
1624        #[tokio::test]
1625        async fn test_final_state_confirmed() {
1626            let mut mocks = default_test_mocks();
1627            let relayer = create_test_relayer();
1628            let tx = make_test_transaction(TransactionStatus::Submitted);
1629
1630            // Expect partial_update to update status to Confirmed.
1631            mocks
1632                .tx_repo
1633                .expect_partial_update()
1634                .returning(|_, update| {
1635                    let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1636                    updated_tx.status = update.status.unwrap_or(updated_tx.status);
1637                    Ok(updated_tx)
1638                });
1639
1640            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1641            let result = evm_transaction
1642                .handle_final_state(tx.clone(), TransactionStatus::Confirmed)
1643                .await
1644                .unwrap();
1645            assert_eq!(result.status, TransactionStatus::Confirmed);
1646        }
1647
1648        #[tokio::test]
1649        async fn test_final_state_failed() {
1650            let mut mocks = default_test_mocks();
1651            let relayer = create_test_relayer();
1652            let tx = make_test_transaction(TransactionStatus::Submitted);
1653
1654            // Expect partial_update to update status to Failed.
1655            mocks
1656                .tx_repo
1657                .expect_partial_update()
1658                .returning(|_, update| {
1659                    let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1660                    updated_tx.status = update.status.unwrap_or(updated_tx.status);
1661                    Ok(updated_tx)
1662                });
1663
1664            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1665            let result = evm_transaction
1666                .handle_final_state(tx.clone(), TransactionStatus::Failed)
1667                .await
1668                .unwrap();
1669            assert_eq!(result.status, TransactionStatus::Failed);
1670        }
1671
1672        #[tokio::test]
1673        async fn test_final_state_expired() {
1674            let mut mocks = default_test_mocks();
1675            let relayer = create_test_relayer();
1676            let tx = make_test_transaction(TransactionStatus::Submitted);
1677
1678            // Expect partial_update to update status to Expired.
1679            mocks
1680                .tx_repo
1681                .expect_partial_update()
1682                .returning(|_, update| {
1683                    let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1684                    updated_tx.status = update.status.unwrap_or(updated_tx.status);
1685                    Ok(updated_tx)
1686                });
1687
1688            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1689            let result = evm_transaction
1690                .handle_final_state(tx.clone(), TransactionStatus::Expired)
1691                .await
1692                .unwrap();
1693            assert_eq!(result.status, TransactionStatus::Expired);
1694        }
1695    }
1696
1697    // Integration tests for `handle_status_impl`
1698    mod handle_status_impl_tests {
1699        use super::*;
1700
1701        #[tokio::test]
1702        async fn test_impl_submitted_branch() {
1703            let mut mocks = default_test_mocks();
1704            let relayer = create_test_relayer();
1705            let mut tx = make_test_transaction(TransactionStatus::Submitted);
1706            tx.sent_at = Some((Utc::now() - Duration::seconds(120)).to_rfc3339());
1707            // Set a dummy hash so check_transaction_status can proceed.
1708            if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1709                evm_data.hash = Some("0xFakeHash".to_string());
1710            }
1711            // Simulate no receipt found.
1712            mocks
1713                .provider
1714                .expect_get_transaction_receipt()
1715                .returning(|_| Box::pin(async { Ok(None) }));
1716            // Mock network repository for should_resubmit check
1717            mocks
1718                .network_repo
1719                .expect_get_by_chain_id()
1720                .returning(|_, _| Ok(Some(create_test_network_model())));
1721            // Expect that a status check job is scheduled.
1722            mocks
1723                .job_producer
1724                .expect_produce_check_transaction_status_job()
1725                .returning(|_, _| Box::pin(async { Ok(()) }));
1726            // Expect update_transaction_status_if_needed to update status to Submitted.
1727            mocks
1728                .tx_repo
1729                .expect_partial_update()
1730                .returning(|_, update| {
1731                    let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1732                    updated_tx.status = update.status.unwrap_or(updated_tx.status);
1733                    Ok(updated_tx)
1734                });
1735
1736            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1737            let result = evm_transaction.handle_status_impl(tx).await.unwrap();
1738            assert_eq!(result.status, TransactionStatus::Submitted);
1739        }
1740
1741        #[tokio::test]
1742        async fn test_impl_mined_branch() {
1743            let mut mocks = default_test_mocks();
1744            let relayer = create_test_relayer();
1745            let mut tx = make_test_transaction(TransactionStatus::Submitted);
1746            // Set created_at to be old enough to pass is_too_early_to_resubmit
1747            tx.created_at = (Utc::now() - Duration::minutes(1)).to_rfc3339();
1748            // Set a dummy hash.
1749            if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1750                evm_data.hash = Some("0xFakeHash".to_string());
1751            }
1752            // Simulate a receipt with a block number of 100 and a successful receipt.
1753            mocks
1754                .provider
1755                .expect_get_transaction_receipt()
1756                .returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) }));
1757            // Simulate that the current block number is 100 (so confirmations are insufficient).
1758            mocks
1759                .provider
1760                .expect_get_block_number()
1761                .return_once(|| Box::pin(async { Ok(100) }));
1762            // Mock network repository to return a test network model
1763            mocks
1764                .network_repo
1765                .expect_get_by_chain_id()
1766                .returning(|_, _| Ok(Some(create_test_network_model())));
1767            // Mock the notification job that gets sent after status update
1768            mocks
1769                .job_producer
1770                .expect_produce_send_notification_job()
1771                .returning(|_, _| Box::pin(async { Ok(()) }));
1772            // Expect get_by_id to reload the transaction after status change
1773            mocks.tx_repo.expect_get_by_id().returning(|_| {
1774                let updated_tx = make_test_transaction(TransactionStatus::Mined);
1775                Ok(updated_tx)
1776            });
1777            // Expect update_transaction_status_if_needed to update status to Mined.
1778            mocks
1779                .tx_repo
1780                .expect_partial_update()
1781                .returning(|_, update| {
1782                    let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1783                    updated_tx.status = update.status.unwrap_or(updated_tx.status);
1784                    Ok(updated_tx)
1785                });
1786
1787            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1788            let result = evm_transaction.handle_status_impl(tx).await.unwrap();
1789            assert_eq!(result.status, TransactionStatus::Mined);
1790        }
1791
1792        #[tokio::test]
1793        async fn test_impl_final_confirmed_branch() {
1794            let mut mocks = default_test_mocks();
1795            let relayer = create_test_relayer();
1796            // Create a transaction with status Confirmed.
1797            let tx = make_test_transaction(TransactionStatus::Confirmed);
1798
1799            // In this branch, check_transaction_status returns the final status immediately,
1800            // so we expect partial_update to update the transaction status to Confirmed.
1801            mocks
1802                .tx_repo
1803                .expect_partial_update()
1804                .returning(|_, update| {
1805                    let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1806                    updated_tx.status = update.status.unwrap_or(updated_tx.status);
1807                    Ok(updated_tx)
1808                });
1809
1810            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1811            let result = evm_transaction.handle_status_impl(tx).await.unwrap();
1812            assert_eq!(result.status, TransactionStatus::Confirmed);
1813        }
1814
1815        #[tokio::test]
1816        async fn test_impl_final_failed_branch() {
1817            let mut mocks = default_test_mocks();
1818            let relayer = create_test_relayer();
1819            // Create a transaction with status Failed.
1820            let tx = make_test_transaction(TransactionStatus::Failed);
1821
1822            mocks
1823                .tx_repo
1824                .expect_partial_update()
1825                .returning(|_, update| {
1826                    let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1827                    updated_tx.status = update.status.unwrap_or(updated_tx.status);
1828                    Ok(updated_tx)
1829                });
1830
1831            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1832            let result = evm_transaction.handle_status_impl(tx).await.unwrap();
1833            assert_eq!(result.status, TransactionStatus::Failed);
1834        }
1835
1836        #[tokio::test]
1837        async fn test_impl_final_expired_branch() {
1838            let mut mocks = default_test_mocks();
1839            let relayer = create_test_relayer();
1840            // Create a transaction with status Expired.
1841            let tx = make_test_transaction(TransactionStatus::Expired);
1842
1843            mocks
1844                .tx_repo
1845                .expect_partial_update()
1846                .returning(|_, update| {
1847                    let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1848                    updated_tx.status = update.status.unwrap_or(updated_tx.status);
1849                    Ok(updated_tx)
1850                });
1851
1852            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1853            let result = evm_transaction.handle_status_impl(tx).await.unwrap();
1854            assert_eq!(result.status, TransactionStatus::Expired);
1855        }
1856    }
1857
1858    // Tests for hash recovery functions
1859    mod hash_recovery_tests {
1860        use super::*;
1861
1862        #[tokio::test]
1863        async fn test_should_try_hash_recovery_not_submitted() {
1864            let mocks = default_test_mocks();
1865            let relayer = create_test_relayer();
1866
1867            let mut tx = make_test_transaction(TransactionStatus::Sent);
1868            tx.hashes = vec![
1869                "0xHash1".to_string(),
1870                "0xHash2".to_string(),
1871                "0xHash3".to_string(),
1872            ];
1873
1874            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1875            let result = evm_transaction.should_try_hash_recovery(&tx).unwrap();
1876
1877            assert!(
1878                !result,
1879                "Should not attempt recovery for non-Submitted transactions"
1880            );
1881        }
1882
1883        #[tokio::test]
1884        async fn test_should_try_hash_recovery_not_enough_hashes() {
1885            let mocks = default_test_mocks();
1886            let relayer = create_test_relayer();
1887
1888            let mut tx = make_test_transaction(TransactionStatus::Submitted);
1889            tx.hashes = vec!["0xHash1".to_string()]; // Only 1 hash
1890            tx.sent_at = Some((Utc::now() - Duration::minutes(3)).to_rfc3339());
1891
1892            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1893            let result = evm_transaction.should_try_hash_recovery(&tx).unwrap();
1894
1895            assert!(
1896                !result,
1897                "Should not attempt recovery with insufficient hashes"
1898            );
1899        }
1900
1901        #[tokio::test]
1902        async fn test_should_try_hash_recovery_too_recent() {
1903            let mocks = default_test_mocks();
1904            let relayer = create_test_relayer();
1905
1906            let mut tx = make_test_transaction(TransactionStatus::Submitted);
1907            tx.hashes = vec![
1908                "0xHash1".to_string(),
1909                "0xHash2".to_string(),
1910                "0xHash3".to_string(),
1911            ];
1912            tx.sent_at = Some(Utc::now().to_rfc3339()); // Recent
1913
1914            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1915            let result = evm_transaction.should_try_hash_recovery(&tx).unwrap();
1916
1917            assert!(
1918                !result,
1919                "Should not attempt recovery for recently sent transactions"
1920            );
1921        }
1922
1923        #[tokio::test]
1924        async fn test_should_try_hash_recovery_success() {
1925            let mocks = default_test_mocks();
1926            let relayer = create_test_relayer();
1927
1928            let mut tx = make_test_transaction(TransactionStatus::Submitted);
1929            tx.hashes = vec![
1930                "0xHash1".to_string(),
1931                "0xHash2".to_string(),
1932                "0xHash3".to_string(),
1933            ];
1934            tx.sent_at = Some((Utc::now() - Duration::minutes(3)).to_rfc3339());
1935
1936            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1937            let result = evm_transaction.should_try_hash_recovery(&tx).unwrap();
1938
1939            assert!(
1940                result,
1941                "Should attempt recovery for stuck transactions with multiple hashes"
1942            );
1943        }
1944
1945        #[tokio::test]
1946        async fn test_try_recover_no_historical_hash_found() {
1947            let mut mocks = default_test_mocks();
1948            let relayer = create_test_relayer();
1949
1950            let mut tx = make_test_transaction(TransactionStatus::Submitted);
1951            tx.hashes = vec![
1952                "0xHash1".to_string(),
1953                "0xHash2".to_string(),
1954                "0xHash3".to_string(),
1955            ];
1956
1957            if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1958                evm_data.hash = Some("0xHash3".to_string());
1959            }
1960
1961            // Mock provider to return None for all hash lookups
1962            mocks
1963                .provider
1964                .expect_get_transaction_receipt()
1965                .returning(|_| Box::pin(async { Ok(None) }));
1966
1967            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1968            let evm_data = tx.network_data.get_evm_transaction_data().unwrap();
1969            let result = evm_transaction
1970                .try_recover_with_historical_hashes(&tx, &evm_data)
1971                .await
1972                .unwrap();
1973
1974            assert!(
1975                result.is_none(),
1976                "Should return None when no historical hash is found"
1977            );
1978        }
1979
1980        #[tokio::test]
1981        async fn test_try_recover_finds_mined_historical_hash() {
1982            let mut mocks = default_test_mocks();
1983            let relayer = create_test_relayer();
1984
1985            let mut tx = make_test_transaction(TransactionStatus::Submitted);
1986            tx.hashes = vec![
1987                "0xHash1".to_string(),
1988                "0xHash2".to_string(), // This one is mined
1989                "0xHash3".to_string(),
1990            ];
1991
1992            if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1993                evm_data.hash = Some("0xHash3".to_string()); // Current hash (wrong one)
1994            }
1995
1996            // Mock provider to return None for Hash1 and Hash3, but receipt for Hash2
1997            mocks
1998                .provider
1999                .expect_get_transaction_receipt()
2000                .returning(|hash| {
2001                    if hash == "0xHash2" {
2002                        Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) })
2003                    } else {
2004                        Box::pin(async { Ok(None) })
2005                    }
2006                });
2007
2008            // Mock partial_update for correcting the hash
2009            let tx_clone = tx.clone();
2010            mocks
2011                .tx_repo
2012                .expect_partial_update()
2013                .returning(move |_, update| {
2014                    let mut updated_tx = tx_clone.clone();
2015                    if let Some(status) = update.status {
2016                        updated_tx.status = status;
2017                    }
2018                    if let Some(NetworkTransactionData::Evm(ref evm_data)) = update.network_data {
2019                        if let NetworkTransactionData::Evm(ref mut updated_evm) =
2020                            updated_tx.network_data
2021                        {
2022                            updated_evm.hash = evm_data.hash.clone();
2023                        }
2024                    }
2025                    Ok(updated_tx)
2026                });
2027
2028            // Mock notification job
2029            mocks
2030                .job_producer
2031                .expect_produce_send_notification_job()
2032                .returning(|_, _| Box::pin(async { Ok(()) }));
2033
2034            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2035            let evm_data = tx.network_data.get_evm_transaction_data().unwrap();
2036            let result = evm_transaction
2037                .try_recover_with_historical_hashes(&tx, &evm_data)
2038                .await
2039                .unwrap();
2040
2041            assert!(result.is_some(), "Should recover the transaction");
2042            let recovered_tx = result.unwrap();
2043            assert_eq!(recovered_tx.status, TransactionStatus::Mined);
2044        }
2045
2046        #[tokio::test]
2047        async fn test_try_recover_network_error_continues() {
2048            let mut mocks = default_test_mocks();
2049            let relayer = create_test_relayer();
2050
2051            let mut tx = make_test_transaction(TransactionStatus::Submitted);
2052            tx.hashes = vec![
2053                "0xHash1".to_string(),
2054                "0xHash2".to_string(), // Network error
2055                "0xHash3".to_string(), // This one is mined
2056            ];
2057
2058            if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
2059                evm_data.hash = Some("0xHash1".to_string());
2060            }
2061
2062            // Mock provider to return error for Hash2, receipt for Hash3
2063            mocks
2064                .provider
2065                .expect_get_transaction_receipt()
2066                .returning(|hash| {
2067                    if hash == "0xHash2" {
2068                        Box::pin(async { Err(crate::services::provider::ProviderError::Timeout) })
2069                    } else if hash == "0xHash3" {
2070                        Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) })
2071                    } else {
2072                        Box::pin(async { Ok(None) })
2073                    }
2074                });
2075
2076            // Mock partial_update for correcting the hash
2077            let tx_clone = tx.clone();
2078            mocks
2079                .tx_repo
2080                .expect_partial_update()
2081                .returning(move |_, update| {
2082                    let mut updated_tx = tx_clone.clone();
2083                    if let Some(status) = update.status {
2084                        updated_tx.status = status;
2085                    }
2086                    Ok(updated_tx)
2087                });
2088
2089            // Mock notification job
2090            mocks
2091                .job_producer
2092                .expect_produce_send_notification_job()
2093                .returning(|_, _| Box::pin(async { Ok(()) }));
2094
2095            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2096            let evm_data = tx.network_data.get_evm_transaction_data().unwrap();
2097            let result = evm_transaction
2098                .try_recover_with_historical_hashes(&tx, &evm_data)
2099                .await
2100                .unwrap();
2101
2102            assert!(
2103                result.is_some(),
2104                "Should continue checking after network error and find mined hash"
2105            );
2106        }
2107
2108        #[tokio::test]
2109        async fn test_update_transaction_with_corrected_hash() {
2110            let mut mocks = default_test_mocks();
2111            let relayer = create_test_relayer();
2112
2113            let mut tx = make_test_transaction(TransactionStatus::Submitted);
2114            if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
2115                evm_data.hash = Some("0xWrongHash".to_string());
2116            }
2117
2118            // Mock partial_update
2119            mocks
2120                .tx_repo
2121                .expect_partial_update()
2122                .returning(move |_, update| {
2123                    let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2124                    if let Some(status) = update.status {
2125                        updated_tx.status = status;
2126                    }
2127                    if let Some(NetworkTransactionData::Evm(ref evm_data)) = update.network_data {
2128                        if let NetworkTransactionData::Evm(ref mut updated_evm) =
2129                            updated_tx.network_data
2130                        {
2131                            updated_evm.hash = evm_data.hash.clone();
2132                        }
2133                    }
2134                    Ok(updated_tx)
2135                });
2136
2137            // Mock notification job
2138            mocks
2139                .job_producer
2140                .expect_produce_send_notification_job()
2141                .returning(|_, _| Box::pin(async { Ok(()) }));
2142
2143            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2144            let evm_data = tx.network_data.get_evm_transaction_data().unwrap();
2145            let result = evm_transaction
2146                .update_transaction_with_corrected_hash(
2147                    &tx,
2148                    &evm_data,
2149                    "0xCorrectHash",
2150                    TransactionStatus::Mined,
2151                )
2152                .await
2153                .unwrap();
2154
2155            assert_eq!(result.status, TransactionStatus::Mined);
2156            if let NetworkTransactionData::Evm(ref updated_evm) = result.network_data {
2157                assert_eq!(updated_evm.hash.as_ref().unwrap(), "0xCorrectHash");
2158            }
2159        }
2160    }
2161
2162    // Tests for check_transaction_status edge cases
2163    mod check_transaction_status_edge_cases {
2164        use super::*;
2165
2166        #[tokio::test]
2167        async fn test_missing_hash_returns_error() {
2168            let mocks = default_test_mocks();
2169            let relayer = create_test_relayer();
2170
2171            let tx = make_test_transaction(TransactionStatus::Submitted);
2172            // Hash is None by default
2173
2174            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2175            let result = evm_transaction.check_transaction_status(&tx).await;
2176
2177            assert!(result.is_err(), "Should return error when hash is missing");
2178        }
2179
2180        #[tokio::test]
2181        async fn test_pending_status_early_return() {
2182            let mocks = default_test_mocks();
2183            let relayer = create_test_relayer();
2184
2185            let tx = make_test_transaction(TransactionStatus::Pending);
2186
2187            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2188            let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
2189
2190            assert_eq!(
2191                status,
2192                TransactionStatus::Pending,
2193                "Should return Pending without querying blockchain"
2194            );
2195        }
2196
2197        #[tokio::test]
2198        async fn test_sent_status_early_return() {
2199            let mocks = default_test_mocks();
2200            let relayer = create_test_relayer();
2201
2202            let tx = make_test_transaction(TransactionStatus::Sent);
2203
2204            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2205            let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
2206
2207            assert_eq!(
2208                status,
2209                TransactionStatus::Sent,
2210                "Should return Sent without querying blockchain"
2211            );
2212        }
2213
2214        #[tokio::test]
2215        async fn test_final_state_early_return() {
2216            let mocks = default_test_mocks();
2217            let relayer = create_test_relayer();
2218
2219            let tx = make_test_transaction(TransactionStatus::Confirmed);
2220
2221            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2222            let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
2223
2224            assert_eq!(
2225                status,
2226                TransactionStatus::Confirmed,
2227                "Should return final state without querying blockchain"
2228            );
2229        }
2230    }
2231}