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 chrono::{DateTime, Duration, Utc};
6use eyre::Result;
7use log::info;
8
9use super::EvmRelayerTransaction;
10use super::{
11    get_age_of_sent_at, has_enough_confirmations, is_noop, is_transaction_valid, make_noop,
12    too_many_attempts, too_many_noop_attempts,
13};
14use crate::constants::ARBITRUM_TIME_TO_RESUBMIT;
15use crate::models::{EvmNetwork, NetworkRepoModel, NetworkType};
16use crate::repositories::{NetworkRepository, RelayerRepository};
17use crate::{
18    domain::transaction::evm::price_calculator::PriceCalculatorTrait,
19    jobs::JobProducerTrait,
20    models::{
21        NetworkTransactionData, RelayerRepoModel, TransactionError, TransactionRepoModel,
22        TransactionStatus, TransactionUpdateRequest,
23    },
24    repositories::{Repository, TransactionCounterTrait, TransactionRepository},
25    services::{EvmProviderTrait, Signer},
26    utils::{get_resubmit_timeout_for_speed, get_resubmit_timeout_with_backoff},
27};
28
29impl<P, RR, NR, TR, J, S, TCR, PC> EvmRelayerTransaction<P, RR, NR, TR, J, S, TCR, PC>
30where
31    P: EvmProviderTrait + Send + Sync,
32    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
33    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
34    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
35    J: JobProducerTrait + Send + Sync + 'static,
36    S: Signer + Send + Sync + 'static,
37    TCR: TransactionCounterTrait + Send + Sync + 'static,
38    PC: PriceCalculatorTrait + Send + Sync,
39{
40    pub(super) async fn check_transaction_status(
41        &self,
42        tx: &TransactionRepoModel,
43    ) -> Result<TransactionStatus, TransactionError> {
44        if tx.status == TransactionStatus::Expired
45            || tx.status == TransactionStatus::Failed
46            || tx.status == TransactionStatus::Confirmed
47        {
48            return Ok(tx.status.clone());
49        }
50
51        let evm_data = tx.network_data.get_evm_transaction_data()?;
52        let tx_hash = evm_data
53            .hash
54            .as_ref()
55            .ok_or(TransactionError::UnexpectedError(
56                "Transaction hash is missing".to_string(),
57            ))?;
58
59        let receipt_result = self.provider().get_transaction_receipt(tx_hash).await?;
60
61        if let Some(receipt) = receipt_result {
62            if !receipt.status() {
63                return Ok(TransactionStatus::Failed);
64            }
65            let last_block_number = self.provider().get_block_number().await?;
66            let tx_block_number = receipt
67                .block_number
68                .ok_or(TransactionError::UnexpectedError(
69                    "Transaction receipt missing block number".to_string(),
70                ))?;
71
72            let network_model = self
73                .network_repository()
74                .get_by_chain_id(NetworkType::Evm, evm_data.chain_id)
75                .await?
76                .ok_or(TransactionError::UnexpectedError(format!(
77                    "Network with chain id {} not found",
78                    evm_data.chain_id
79                )))?;
80
81            let network = EvmNetwork::try_from(network_model).map_err(|e| {
82                TransactionError::UnexpectedError(format!(
83                    "Error converting network model to EvmNetwork: {}",
84                    e
85                ))
86            })?;
87
88            if !has_enough_confirmations(
89                tx_block_number,
90                last_block_number,
91                network.required_confirmations,
92            ) {
93                info!("Transaction mined but not confirmed: {}", tx_hash);
94                return Ok(TransactionStatus::Mined);
95            }
96            Ok(TransactionStatus::Confirmed)
97        } else {
98            info!("Transaction not yet mined: {}", tx_hash);
99            Ok(TransactionStatus::Submitted)
100        }
101    }
102
103    /// Determines if a transaction should be resubmitted.
104    pub(super) async fn should_resubmit(
105        &self,
106        tx: &TransactionRepoModel,
107    ) -> Result<bool, TransactionError> {
108        if tx.status != TransactionStatus::Submitted {
109            return Err(TransactionError::UnexpectedError(format!(
110                "Transaction must be in Submitted status to resubmit, found: {:?}",
111                tx.status
112            )));
113        }
114
115        let evm_data = tx.network_data.get_evm_transaction_data()?;
116        let age = get_age_of_sent_at(tx)?;
117
118        // Check if network lacks mempool and determine appropriate timeout
119        let network_model = self
120            .network_repository()
121            .get_by_chain_id(NetworkType::Evm, evm_data.chain_id)
122            .await?
123            .ok_or(TransactionError::UnexpectedError(format!(
124                "Network with chain id {} not found",
125                evm_data.chain_id
126            )))?;
127
128        let network = EvmNetwork::try_from(network_model).map_err(|e| {
129            TransactionError::UnexpectedError(format!(
130                "Error converting network model to EvmNetwork: {}",
131                e
132            ))
133        })?;
134
135        let timeout = match network.is_arbitrum() {
136            true => ARBITRUM_TIME_TO_RESUBMIT,
137            false => get_resubmit_timeout_for_speed(&evm_data.speed),
138        };
139
140        let timeout_with_backoff = match network.is_arbitrum() {
141            true => timeout, // Use base timeout without backoff for Arbitrum
142            false => get_resubmit_timeout_with_backoff(timeout, tx.hashes.len()),
143        };
144
145        if age > Duration::milliseconds(timeout_with_backoff) {
146            info!("Transaction has been pending for too long, resubmitting");
147            return Ok(true);
148        }
149        Ok(false)
150    }
151
152    /// Determines if a transaction should be replaced with a NOOP transaction.
153    pub(super) async fn should_noop(
154        &self,
155        tx: &TransactionRepoModel,
156    ) -> Result<bool, TransactionError> {
157        if too_many_noop_attempts(tx) {
158            info!("Transaction has too many NOOP attempts already");
159            return Ok(false);
160        }
161
162        let evm_data = tx.network_data.get_evm_transaction_data()?;
163        if is_noop(&evm_data) {
164            return Ok(false);
165        }
166
167        let network_model = self
168            .network_repository()
169            .get_by_chain_id(NetworkType::Evm, evm_data.chain_id)
170            .await?
171            .ok_or(TransactionError::UnexpectedError(format!(
172                "Network with chain id {} not found",
173                evm_data.chain_id
174            )))?;
175
176        let network = EvmNetwork::try_from(network_model).map_err(|e| {
177            TransactionError::UnexpectedError(format!(
178                "Error converting network model to EvmNetwork: {}",
179                e
180            ))
181        })?;
182
183        if network.is_rollup() && too_many_attempts(tx) {
184            info!("Rollup transaction has too many attempts, will replace with NOOP");
185            return Ok(true);
186        }
187
188        if !is_transaction_valid(&tx.created_at, &tx.valid_until) {
189            info!("Transaction is expired, will replace with NOOP");
190            return Ok(true);
191        }
192
193        if tx.status == TransactionStatus::Pending {
194            let created_at = &tx.created_at;
195            let created_time = DateTime::parse_from_rfc3339(created_at)
196                .map_err(|_| {
197                    TransactionError::UnexpectedError("Error parsing created_at time".to_string())
198                })?
199                .with_timezone(&Utc);
200            let age = Utc::now().signed_duration_since(created_time);
201            if age > Duration::minutes(1) {
202                info!("Transaction in Pending state for over 1 minute, will replace with NOOP");
203                return Ok(true);
204            }
205        }
206        Ok(false)
207    }
208
209    /// Helper method that updates transaction status only if it's different from the current status.
210    pub(super) async fn update_transaction_status_if_needed(
211        &self,
212        tx: TransactionRepoModel,
213        new_status: TransactionStatus,
214    ) -> Result<TransactionRepoModel, TransactionError> {
215        if tx.status != new_status {
216            return self.update_transaction_status(tx, new_status).await;
217        }
218        Ok(tx)
219    }
220
221    /// Prepares a NOOP transaction update request.
222    pub(super) async fn prepare_noop_update_request(
223        &self,
224        tx: &TransactionRepoModel,
225        is_cancellation: bool,
226    ) -> Result<TransactionUpdateRequest, TransactionError> {
227        let mut evm_data = tx.network_data.get_evm_transaction_data()?;
228        let network_model = self
229            .network_repository()
230            .get_by_chain_id(NetworkType::Evm, evm_data.chain_id)
231            .await?
232            .ok_or(TransactionError::UnexpectedError(format!(
233                "Network with chain id {} not found",
234                evm_data.chain_id
235            )))?;
236
237        let network = EvmNetwork::try_from(network_model).map_err(|e| {
238            TransactionError::UnexpectedError(format!(
239                "Error converting network model to EvmNetwork: {}",
240                e
241            ))
242        })?;
243
244        make_noop(&mut evm_data, &network, Some(self.provider())).await?;
245
246        let noop_count = tx.noop_count.unwrap_or(0) + 1;
247        let update_request = TransactionUpdateRequest {
248            network_data: Some(NetworkTransactionData::Evm(evm_data)),
249            noop_count: Some(noop_count),
250            is_canceled: if is_cancellation {
251                Some(true)
252            } else {
253                tx.is_canceled
254            },
255            ..Default::default()
256        };
257        Ok(update_request)
258    }
259
260    /// Handles transactions in the Submitted state.
261    async fn handle_submitted_state(
262        &self,
263        tx: TransactionRepoModel,
264    ) -> Result<TransactionRepoModel, TransactionError> {
265        if self.should_resubmit(&tx).await? {
266            let resubmitted_tx = self.handle_resubmission(tx).await?;
267            self.schedule_status_check(&resubmitted_tx, None).await?;
268            return Ok(resubmitted_tx);
269        }
270
271        self.schedule_status_check(&tx, Some(5)).await?;
272        self.update_transaction_status_if_needed(tx, TransactionStatus::Submitted)
273            .await
274    }
275
276    /// Processes transaction resubmission logic
277    async fn handle_resubmission(
278        &self,
279        tx: TransactionRepoModel,
280    ) -> Result<TransactionRepoModel, TransactionError> {
281        info!("Scheduling resubmit job for transaction: {}", tx.id);
282
283        let tx_to_process = if self.should_noop(&tx).await? {
284            self.process_noop_transaction(&tx).await?
285        } else {
286            tx
287        };
288
289        self.send_transaction_resubmit_job(&tx_to_process).await?;
290        Ok(tx_to_process)
291    }
292
293    /// Handles NOOP transaction processing before resubmission
294    async fn process_noop_transaction(
295        &self,
296        tx: &TransactionRepoModel,
297    ) -> Result<TransactionRepoModel, TransactionError> {
298        info!("Preparing transaction NOOP before resubmission: {}", tx.id);
299        let update = self.prepare_noop_update_request(tx, false).await?;
300        let updated_tx = self
301            .transaction_repository()
302            .partial_update(tx.id.clone(), update)
303            .await?;
304
305        self.send_transaction_update_notification(&updated_tx)
306            .await?;
307        Ok(updated_tx)
308    }
309
310    /// Handles transactions in the Pending state.
311    async fn handle_pending_state(
312        &self,
313        tx: TransactionRepoModel,
314    ) -> Result<TransactionRepoModel, TransactionError> {
315        if self.should_noop(&tx).await? {
316            info!("Preparing NOOP for pending transaction: {}", tx.id);
317            let update = self.prepare_noop_update_request(&tx, false).await?;
318            let updated_tx = self
319                .transaction_repository()
320                .partial_update(tx.id.clone(), update)
321                .await?;
322
323            self.send_transaction_submit_job(&updated_tx).await?;
324            self.send_transaction_update_notification(&updated_tx)
325                .await?;
326            return Ok(updated_tx);
327        } else {
328            self.schedule_status_check(&tx, Some(5)).await?;
329        }
330        Ok(tx)
331    }
332
333    /// Handles transactions in the Mined state.
334    async fn handle_mined_state(
335        &self,
336        tx: TransactionRepoModel,
337    ) -> Result<TransactionRepoModel, TransactionError> {
338        self.schedule_status_check(&tx, Some(5)).await?;
339        self.update_transaction_status_if_needed(tx, TransactionStatus::Mined)
340            .await
341    }
342
343    /// Handles transactions in final states (Confirmed, Failed, Expired).
344    async fn handle_final_state(
345        &self,
346        tx: TransactionRepoModel,
347        status: TransactionStatus,
348    ) -> Result<TransactionRepoModel, TransactionError> {
349        self.update_transaction_status_if_needed(tx, status).await
350    }
351
352    /// Inherent status-handling method.
353    ///
354    /// This method encapsulates the full logic for handling transaction status,
355    /// including resubmission, NOOP replacement, and updating status.
356    pub async fn handle_status_impl(
357        &self,
358        tx: TransactionRepoModel,
359    ) -> Result<TransactionRepoModel, TransactionError> {
360        info!("Checking transaction status for tx: {:?}", tx.id);
361
362        let status = self.check_transaction_status(&tx).await?;
363        info!("Transaction status: {:?}", status);
364
365        match status {
366            TransactionStatus::Submitted => self.handle_submitted_state(tx).await,
367            TransactionStatus::Pending => self.handle_pending_state(tx).await,
368            TransactionStatus::Mined => self.handle_mined_state(tx).await,
369            TransactionStatus::Confirmed
370            | TransactionStatus::Failed
371            | TransactionStatus::Expired => self.handle_final_state(tx, status).await,
372            _ => Err(TransactionError::UnexpectedError(format!(
373                "Unexpected transaction status: {:?}",
374                status
375            ))),
376        }
377    }
378}
379
380#[cfg(test)]
381mod tests {
382    use crate::{
383        config::{EvmNetworkConfig, NetworkConfigCommon},
384        domain::transaction::evm::{EvmRelayerTransaction, MockPriceCalculatorTrait},
385        jobs::MockJobProducerTrait,
386        models::{
387            evm::Speed, EvmTransactionData, NetworkConfigData, NetworkRepoModel,
388            NetworkTransactionData, NetworkType, RelayerEvmPolicy, RelayerNetworkPolicy,
389            RelayerRepoModel, TransactionRepoModel, TransactionStatus, U256,
390        },
391        repositories::{
392            MockNetworkRepository, MockRelayerRepository, MockTransactionCounterTrait,
393            MockTransactionRepository,
394        },
395        services::{MockEvmProviderTrait, MockSigner},
396    };
397    use alloy::{
398        consensus::{Eip658Value, Receipt, ReceiptEnvelope, ReceiptWithBloom},
399        primitives::{b256, Address, BlockHash, Bloom, TxHash},
400        rpc::types::TransactionReceipt,
401    };
402    use chrono::{Duration, Utc};
403    use std::sync::Arc;
404
405    /// Helper struct holding all the mocks we often need
406    pub struct TestMocks {
407        pub provider: MockEvmProviderTrait,
408        pub relayer_repo: MockRelayerRepository,
409        pub network_repo: MockNetworkRepository,
410        pub tx_repo: MockTransactionRepository,
411        pub job_producer: MockJobProducerTrait,
412        pub signer: MockSigner,
413        pub counter: MockTransactionCounterTrait,
414        pub price_calc: MockPriceCalculatorTrait,
415    }
416
417    /// Returns a default `TestMocks` with zero-configuration stubs.
418    /// You can override expectations in each test as needed.
419    pub fn default_test_mocks() -> TestMocks {
420        TestMocks {
421            provider: MockEvmProviderTrait::new(),
422            relayer_repo: MockRelayerRepository::new(),
423            network_repo: MockNetworkRepository::new(),
424            tx_repo: MockTransactionRepository::new(),
425            job_producer: MockJobProducerTrait::new(),
426            signer: MockSigner::new(),
427            counter: MockTransactionCounterTrait::new(),
428            price_calc: MockPriceCalculatorTrait::new(),
429        }
430    }
431
432    /// Returns a `TestMocks` with network repository configured for prepare_noop_update_request tests.
433    pub fn default_test_mocks_with_network() -> TestMocks {
434        let mut mocks = default_test_mocks();
435        // Set up default expectation for get_by_chain_id that prepare_noop_update_request tests need
436        mocks
437            .network_repo
438            .expect_get_by_chain_id()
439            .returning(|network_type, chain_id| {
440                if network_type == NetworkType::Evm && chain_id == 1 {
441                    Ok(Some(create_test_network_model()))
442                } else {
443                    Ok(None)
444                }
445            });
446        mocks
447    }
448
449    /// Creates a test NetworkRepoModel for chain_id 1 (mainnet)
450    pub fn create_test_network_model() -> NetworkRepoModel {
451        let evm_config = EvmNetworkConfig {
452            common: NetworkConfigCommon {
453                network: "mainnet".to_string(),
454                from: None,
455                rpc_urls: Some(vec!["https://rpc.example.com".to_string()]),
456                explorer_urls: Some(vec!["https://explorer.example.com".to_string()]),
457                average_blocktime_ms: Some(12000),
458                is_testnet: Some(false),
459                tags: Some(vec!["mainnet".to_string()]),
460            },
461            chain_id: Some(1),
462            required_confirmations: Some(12),
463            features: Some(vec!["eip1559".to_string()]),
464            symbol: Some("ETH".to_string()),
465        };
466        NetworkRepoModel {
467            id: "evm:mainnet".to_string(),
468            name: "mainnet".to_string(),
469            network_type: NetworkType::Evm,
470            config: NetworkConfigData::Evm(evm_config),
471        }
472    }
473
474    /// Creates a test NetworkRepoModel for chain_id 42161 (Arbitrum-like) with no-mempool tag
475    pub fn create_test_no_mempool_network_model() -> NetworkRepoModel {
476        let evm_config = EvmNetworkConfig {
477            common: NetworkConfigCommon {
478                network: "arbitrum".to_string(),
479                from: None,
480                rpc_urls: Some(vec!["https://arb-rpc.example.com".to_string()]),
481                explorer_urls: Some(vec!["https://arb-explorer.example.com".to_string()]),
482                average_blocktime_ms: Some(1000),
483                is_testnet: Some(false),
484                tags: Some(vec!["arbitrum".to_string(), "no-mempool".to_string()]),
485            },
486            chain_id: Some(42161),
487            required_confirmations: Some(12),
488            features: Some(vec!["eip1559".to_string()]),
489            symbol: Some("ETH".to_string()),
490        };
491        NetworkRepoModel {
492            id: "evm:arbitrum".to_string(),
493            name: "arbitrum".to_string(),
494            network_type: NetworkType::Evm,
495            config: NetworkConfigData::Evm(evm_config),
496        }
497    }
498
499    /// Minimal "builder" for TransactionRepoModel.
500    /// Allows quick creation of a test transaction with default fields,
501    /// then updates them based on the provided status or overrides.
502    pub fn make_test_transaction(status: TransactionStatus) -> TransactionRepoModel {
503        TransactionRepoModel {
504            id: "test-tx-id".to_string(),
505            relayer_id: "test-relayer-id".to_string(),
506            status,
507            status_reason: None,
508            created_at: Utc::now().to_rfc3339(),
509            sent_at: None,
510            confirmed_at: None,
511            valid_until: None,
512            delete_at: None,
513            network_type: NetworkType::Evm,
514            network_data: NetworkTransactionData::Evm(EvmTransactionData {
515                chain_id: 1,
516                from: "0xSender".to_string(),
517                to: Some("0xRecipient".to_string()),
518                value: U256::from(0),
519                data: Some("0xData".to_string()),
520                gas_limit: Some(21000),
521                gas_price: Some(20000000000),
522                max_fee_per_gas: None,
523                max_priority_fee_per_gas: None,
524                nonce: None,
525                signature: None,
526                hash: None,
527                speed: Some(Speed::Fast),
528                raw: None,
529            }),
530            priced_at: None,
531            hashes: Vec::new(),
532            noop_count: None,
533            is_canceled: Some(false),
534        }
535    }
536
537    /// Minimal "builder" for EvmRelayerTransaction.
538    /// Takes mock dependencies as arguments.
539    pub fn make_test_evm_relayer_transaction(
540        relayer: RelayerRepoModel,
541        mocks: TestMocks,
542    ) -> EvmRelayerTransaction<
543        MockEvmProviderTrait,
544        MockRelayerRepository,
545        MockNetworkRepository,
546        MockTransactionRepository,
547        MockJobProducerTrait,
548        MockSigner,
549        MockTransactionCounterTrait,
550        MockPriceCalculatorTrait,
551    > {
552        EvmRelayerTransaction::new(
553            relayer,
554            mocks.provider,
555            Arc::new(mocks.relayer_repo),
556            Arc::new(mocks.network_repo),
557            Arc::new(mocks.tx_repo),
558            Arc::new(mocks.counter),
559            Arc::new(mocks.job_producer),
560            mocks.price_calc,
561            mocks.signer,
562        )
563        .unwrap()
564    }
565
566    fn create_test_relayer() -> RelayerRepoModel {
567        RelayerRepoModel {
568            id: "test-relayer-id".to_string(),
569            name: "Test Relayer".to_string(),
570            paused: false,
571            system_disabled: false,
572            network: "test_network".to_string(),
573            network_type: NetworkType::Evm,
574            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
575            signer_id: "test_signer".to_string(),
576            address: "0x".to_string(),
577            notification_id: None,
578            custom_rpc_urls: None,
579        }
580    }
581
582    fn make_mock_receipt(status: bool, block_number: Option<u64>) -> TransactionReceipt {
583        // Use some placeholder values for minimal completeness
584        let tx_hash = TxHash::from(b256!(
585            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
586        ));
587        let block_hash = BlockHash::from(b256!(
588            "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
589        ));
590        let from_address = Address::from([0x11; 20]);
591
592        TransactionReceipt {
593            // A default, minimal "Legacy" receipt envelope
594            inner: ReceiptEnvelope::Legacy(ReceiptWithBloom {
595                receipt: Receipt {
596                    status: Eip658Value::Eip658(status), // determines success/fail
597                    cumulative_gas_used: 0,
598                    logs: vec![],
599                },
600                logs_bloom: Bloom::ZERO,
601            }),
602            transaction_hash: tx_hash,
603            transaction_index: Some(0),
604            block_hash: block_number.map(|_| block_hash), // only set if mined
605            block_number,
606            gas_used: 21000,
607            effective_gas_price: 1000,
608            blob_gas_used: None,
609            blob_gas_price: None,
610            from: from_address,
611            to: None,
612            contract_address: None,
613            authorization_list: None,
614        }
615    }
616
617    // Tests for `check_transaction_status`
618    mod check_transaction_status_tests {
619        use super::*;
620
621        #[tokio::test]
622        async fn test_not_mined() {
623            let mut mocks = default_test_mocks();
624            let relayer = create_test_relayer();
625            let mut tx = make_test_transaction(TransactionStatus::Submitted);
626
627            // Provide a hash so we can check for receipt
628            if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
629                evm_data.hash = Some("0xFakeHash".to_string());
630            }
631
632            // Mock that get_transaction_receipt returns None (not mined)
633            mocks
634                .provider
635                .expect_get_transaction_receipt()
636                .returning(|_| Box::pin(async { Ok(None) }));
637
638            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
639
640            let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
641            assert_eq!(status, TransactionStatus::Submitted);
642        }
643
644        #[tokio::test]
645        async fn test_mined_but_not_confirmed() {
646            let mut mocks = default_test_mocks();
647            let relayer = create_test_relayer();
648            let mut tx = make_test_transaction(TransactionStatus::Submitted);
649
650            if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
651                evm_data.hash = Some("0xFakeHash".to_string());
652            }
653
654            // Mock a mined receipt with block_number = 100
655            mocks
656                .provider
657                .expect_get_transaction_receipt()
658                .returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) }));
659
660            // Mock block_number that hasn't reached the confirmation threshold
661            mocks
662                .provider
663                .expect_get_block_number()
664                .return_once(|| Box::pin(async { Ok(100) }));
665
666            // Mock network repository to return a test network model
667            mocks
668                .network_repo
669                .expect_get_by_chain_id()
670                .returning(|_, _| Ok(Some(create_test_network_model())));
671
672            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
673
674            let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
675            assert_eq!(status, TransactionStatus::Mined);
676        }
677
678        #[tokio::test]
679        async fn test_confirmed() {
680            let mut mocks = default_test_mocks();
681            let relayer = create_test_relayer();
682            let mut tx = make_test_transaction(TransactionStatus::Submitted);
683
684            if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
685                evm_data.hash = Some("0xFakeHash".to_string());
686            }
687
688            // Mock a mined receipt with block_number = 100
689            mocks
690                .provider
691                .expect_get_transaction_receipt()
692                .returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) }));
693
694            // Mock block_number that meets the confirmation threshold
695            mocks
696                .provider
697                .expect_get_block_number()
698                .return_once(|| Box::pin(async { Ok(113) }));
699
700            // Mock network repository to return a test network model
701            mocks
702                .network_repo
703                .expect_get_by_chain_id()
704                .returning(|_, _| Ok(Some(create_test_network_model())));
705
706            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
707
708            let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
709            assert_eq!(status, TransactionStatus::Confirmed);
710        }
711
712        #[tokio::test]
713        async fn test_failed() {
714            let mut mocks = default_test_mocks();
715            let relayer = create_test_relayer();
716            let mut tx = make_test_transaction(TransactionStatus::Submitted);
717
718            if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
719                evm_data.hash = Some("0xFakeHash".to_string());
720            }
721
722            // Mock a mined receipt with failure
723            mocks
724                .provider
725                .expect_get_transaction_receipt()
726                .returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(false, Some(100)))) }));
727
728            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
729
730            let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
731            assert_eq!(status, TransactionStatus::Failed);
732        }
733    }
734
735    // Tests for `should_resubmit`
736    mod should_resubmit_tests {
737        use super::*;
738        use crate::models::TransactionError;
739
740        #[tokio::test]
741        async fn test_should_resubmit_true() {
742            let mut mocks = default_test_mocks();
743            let relayer = create_test_relayer();
744
745            // Set sent_at to 600 seconds ago to force resubmission
746            let mut tx = make_test_transaction(TransactionStatus::Submitted);
747            tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
748
749            // Mock network repository to return a regular network model
750            mocks
751                .network_repo
752                .expect_get_by_chain_id()
753                .returning(|_, _| Ok(Some(create_test_network_model())));
754
755            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
756            let res = evm_transaction.should_resubmit(&tx).await.unwrap();
757            assert!(res, "Transaction should be resubmitted after timeout.");
758        }
759
760        #[tokio::test]
761        async fn test_should_resubmit_false() {
762            let mut mocks = default_test_mocks();
763            let relayer = create_test_relayer();
764
765            // Make a transaction with status Submitted but recently sent
766            let mut tx = make_test_transaction(TransactionStatus::Submitted);
767            tx.sent_at = Some(Utc::now().to_rfc3339());
768
769            // Mock network repository to return a regular network model
770            mocks
771                .network_repo
772                .expect_get_by_chain_id()
773                .returning(|_, _| Ok(Some(create_test_network_model())));
774
775            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
776            let res = evm_transaction.should_resubmit(&tx).await.unwrap();
777            assert!(!res, "Transaction should not be resubmitted immediately.");
778        }
779
780        #[tokio::test]
781        async fn test_should_resubmit_true_for_no_mempool_network() {
782            let mut mocks = default_test_mocks();
783            let relayer = create_test_relayer();
784
785            // Set up a transaction that would normally be resubmitted (sent_at long ago)
786            let mut tx = make_test_transaction(TransactionStatus::Submitted);
787            tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
788
789            // Set chain_id to match the no-mempool network
790            if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
791                evm_data.chain_id = 42161; // Arbitrum chain ID
792            }
793
794            // Mock network repository to return a no-mempool network model
795            mocks
796                .network_repo
797                .expect_get_by_chain_id()
798                .returning(|_, _| Ok(Some(create_test_no_mempool_network_model())));
799
800            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
801            let res = evm_transaction.should_resubmit(&tx).await.unwrap();
802            assert!(
803                res,
804                "Transaction should be resubmitted for no-mempool networks."
805            );
806        }
807
808        #[tokio::test]
809        async fn test_should_resubmit_network_not_found() {
810            let mut mocks = default_test_mocks();
811            let relayer = create_test_relayer();
812
813            let mut tx = make_test_transaction(TransactionStatus::Submitted);
814            tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
815
816            // Mock network repository to return None (network not found)
817            mocks
818                .network_repo
819                .expect_get_by_chain_id()
820                .returning(|_, _| Ok(None));
821
822            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
823            let result = evm_transaction.should_resubmit(&tx).await;
824
825            assert!(
826                result.is_err(),
827                "should_resubmit should return error when network not found"
828            );
829            let error = result.unwrap_err();
830            match error {
831                TransactionError::UnexpectedError(msg) => {
832                    assert!(msg.contains("Network with chain id 1 not found"));
833                }
834                _ => panic!("Expected UnexpectedError for network not found"),
835            }
836        }
837
838        #[tokio::test]
839        async fn test_should_resubmit_network_conversion_error() {
840            let mut mocks = default_test_mocks();
841            let relayer = create_test_relayer();
842
843            let mut tx = make_test_transaction(TransactionStatus::Submitted);
844            tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
845
846            // Create a network model with invalid EVM config (missing chain_id)
847            let invalid_evm_config = EvmNetworkConfig {
848                common: NetworkConfigCommon {
849                    network: "invalid-network".to_string(),
850                    from: None,
851                    rpc_urls: Some(vec!["https://rpc.example.com".to_string()]),
852                    explorer_urls: Some(vec!["https://explorer.example.com".to_string()]),
853                    average_blocktime_ms: Some(12000),
854                    is_testnet: Some(false),
855                    tags: Some(vec!["testnet".to_string()]),
856                },
857                chain_id: None, // This will cause the conversion to fail
858                required_confirmations: Some(12),
859                features: Some(vec!["eip1559".to_string()]),
860                symbol: Some("ETH".to_string()),
861            };
862            let invalid_network = NetworkRepoModel {
863                id: "evm:invalid".to_string(),
864                name: "invalid-network".to_string(),
865                network_type: NetworkType::Evm,
866                config: NetworkConfigData::Evm(invalid_evm_config),
867            };
868
869            // Mock network repository to return the invalid network model
870            mocks
871                .network_repo
872                .expect_get_by_chain_id()
873                .returning(move |_, _| Ok(Some(invalid_network.clone())));
874
875            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
876            let result = evm_transaction.should_resubmit(&tx).await;
877
878            assert!(
879                result.is_err(),
880                "should_resubmit should return error when network conversion fails"
881            );
882            let error = result.unwrap_err();
883            match error {
884                TransactionError::UnexpectedError(msg) => {
885                    assert!(msg.contains("Error converting network model to EvmNetwork"));
886                }
887                _ => panic!("Expected UnexpectedError for network conversion failure"),
888            }
889        }
890    }
891
892    // Tests for `should_noop`
893    mod should_noop_tests {
894        use super::*;
895
896        #[tokio::test]
897        async fn test_expired_transaction_triggers_noop() {
898            let mut mocks = default_test_mocks();
899            let relayer = create_test_relayer();
900
901            let mut tx = make_test_transaction(TransactionStatus::Submitted);
902            // Force the transaction to be "expired" by setting valid_until in the past
903            tx.valid_until = Some((Utc::now() - Duration::seconds(10)).to_rfc3339());
904
905            // Mock network repository to return a test network model
906            mocks
907                .network_repo
908                .expect_get_by_chain_id()
909                .returning(|_, _| Ok(Some(create_test_network_model())));
910
911            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
912            let res = evm_transaction.should_noop(&tx).await.unwrap();
913            assert!(res, "Expired transaction should be replaced with a NOOP.");
914        }
915    }
916
917    // Tests for `update_transaction_status_if_needed`
918    mod update_transaction_status_tests {
919        use super::*;
920
921        #[tokio::test]
922        async fn test_no_update_when_status_is_same() {
923            // Create mocks, relayer, and a transaction with status Submitted.
924            let mocks = default_test_mocks();
925            let relayer = create_test_relayer();
926            let tx = make_test_transaction(TransactionStatus::Submitted);
927            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
928
929            // When new status is the same as current, update_transaction_status_if_needed
930            // should simply return the original transaction.
931            let updated_tx = evm_transaction
932                .update_transaction_status_if_needed(tx.clone(), TransactionStatus::Submitted)
933                .await
934                .unwrap();
935            assert_eq!(updated_tx.status, TransactionStatus::Submitted);
936            assert_eq!(updated_tx.id, tx.id);
937        }
938    }
939
940    // Tests for `prepare_noop_update_request`
941    mod prepare_noop_update_request_tests {
942        use super::*;
943
944        #[tokio::test]
945        async fn test_noop_request_without_cancellation() {
946            // Create a transaction with an initial noop_count of 2 and is_canceled set to false.
947            let mocks = default_test_mocks_with_network();
948            let relayer = create_test_relayer();
949            let mut tx = make_test_transaction(TransactionStatus::Submitted);
950            tx.noop_count = Some(2);
951            tx.is_canceled = Some(false);
952
953            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
954            let update_req = evm_transaction
955                .prepare_noop_update_request(&tx, false)
956                .await
957                .unwrap();
958
959            // NOOP count should be incremented: 2 becomes 3.
960            assert_eq!(update_req.noop_count, Some(3));
961            // When not cancelling, the is_canceled flag should remain as in the original transaction.
962            assert_eq!(update_req.is_canceled, Some(false));
963        }
964
965        #[tokio::test]
966        async fn test_noop_request_with_cancellation() {
967            // Create a transaction with no initial noop_count (None) and is_canceled false.
968            let mocks = default_test_mocks_with_network();
969            let relayer = create_test_relayer();
970            let mut tx = make_test_transaction(TransactionStatus::Submitted);
971            tx.noop_count = None;
972            tx.is_canceled = Some(false);
973
974            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
975            let update_req = evm_transaction
976                .prepare_noop_update_request(&tx, true)
977                .await
978                .unwrap();
979
980            // NOOP count should default to 1.
981            assert_eq!(update_req.noop_count, Some(1));
982            // When cancelling, the is_canceled flag should be forced to true.
983            assert_eq!(update_req.is_canceled, Some(true));
984        }
985    }
986
987    // Tests for `handle_submitted_state`
988    mod handle_submitted_state_tests {
989        use super::*;
990
991        #[tokio::test]
992        async fn test_schedules_resubmit_job() {
993            let mut mocks = default_test_mocks();
994            let relayer = create_test_relayer();
995
996            // Set sent_at far in the past to force resubmission
997            let mut tx = make_test_transaction(TransactionStatus::Submitted);
998            tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
999
1000            // Mock network repository to return a test network model for should_noop check
1001            mocks
1002                .network_repo
1003                .expect_get_by_chain_id()
1004                .returning(|_, _| Ok(Some(create_test_network_model())));
1005
1006            // Expect the resubmit job to be produced
1007            mocks
1008                .job_producer
1009                .expect_produce_submit_transaction_job()
1010                .returning(|_, _| Box::pin(async { Ok(()) }));
1011
1012            // Expect status check to be scheduled
1013            mocks
1014                .job_producer
1015                .expect_produce_check_transaction_status_job()
1016                .returning(|_, _| Box::pin(async { Ok(()) }));
1017
1018            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1019            let updated_tx = evm_transaction.handle_submitted_state(tx).await.unwrap();
1020
1021            // We remain in "Submitted" after scheduling the resubmit
1022            assert_eq!(updated_tx.status, TransactionStatus::Submitted);
1023        }
1024    }
1025
1026    // Tests for `handle_pending_state`
1027    mod handle_pending_state_tests {
1028        use super::*;
1029
1030        #[tokio::test]
1031        async fn test_pending_state_no_noop() {
1032            // Create a pending transaction that is fresh (created now).
1033            let mut mocks = default_test_mocks();
1034            let relayer = create_test_relayer();
1035            let mut tx = make_test_transaction(TransactionStatus::Pending);
1036            tx.created_at = Utc::now().to_rfc3339(); // less than one minute old
1037
1038            // Mock network repository to return a test network model
1039            mocks
1040                .network_repo
1041                .expect_get_by_chain_id()
1042                .returning(|_, _| Ok(Some(create_test_network_model())));
1043
1044            // Expect status check to be scheduled when not doing NOOP
1045            mocks
1046                .job_producer
1047                .expect_produce_check_transaction_status_job()
1048                .returning(|_, _| Box::pin(async { Ok(()) }));
1049
1050            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1051            let result = evm_transaction
1052                .handle_pending_state(tx.clone())
1053                .await
1054                .unwrap();
1055
1056            // When should_noop returns false the original transaction is returned unchanged.
1057            assert_eq!(result.id, tx.id);
1058            assert_eq!(result.status, tx.status);
1059            assert_eq!(result.noop_count, tx.noop_count);
1060        }
1061
1062        #[tokio::test]
1063        async fn test_pending_state_with_noop() {
1064            // Create a pending transaction that is old (created 2 minutes ago)
1065            let mut mocks = default_test_mocks();
1066            let relayer = create_test_relayer();
1067            let mut tx = make_test_transaction(TransactionStatus::Pending);
1068            tx.created_at = (Utc::now() - Duration::minutes(2)).to_rfc3339();
1069
1070            // Mock network repository to return a test network model
1071            mocks
1072                .network_repo
1073                .expect_get_by_chain_id()
1074                .returning(|_, _| Ok(Some(create_test_network_model())));
1075
1076            // Expect partial_update to be called and simulate a NOOP update by setting noop_count.
1077            let tx_clone = tx.clone();
1078            mocks
1079                .tx_repo
1080                .expect_partial_update()
1081                .returning(move |_, update| {
1082                    let mut updated_tx = tx_clone.clone();
1083                    updated_tx.noop_count = update.noop_count;
1084                    Ok(updated_tx)
1085                });
1086            // Expect that a submit job and notification are produced.
1087            mocks
1088                .job_producer
1089                .expect_produce_submit_transaction_job()
1090                .returning(|_, _| Box::pin(async { Ok(()) }));
1091            mocks
1092                .job_producer
1093                .expect_produce_send_notification_job()
1094                .returning(|_, _| Box::pin(async { Ok(()) }));
1095
1096            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1097            let result = evm_transaction
1098                .handle_pending_state(tx.clone())
1099                .await
1100                .unwrap();
1101
1102            // Since should_noop returns true, the returned transaction should have a nonzero noop_count.
1103            assert!(result.noop_count.unwrap_or(0) > 0);
1104        }
1105    }
1106
1107    // Tests for `handle_mined_state`
1108    mod handle_mined_state_tests {
1109        use super::*;
1110
1111        #[tokio::test]
1112        async fn test_updates_status_and_schedules_check() {
1113            let mut mocks = default_test_mocks();
1114            let relayer = create_test_relayer();
1115            // Create a transaction in Submitted state (the mined branch is reached via status check).
1116            let tx = make_test_transaction(TransactionStatus::Submitted);
1117
1118            // Expect schedule_status_check to be called with delay 5.
1119            mocks
1120                .job_producer
1121                .expect_produce_check_transaction_status_job()
1122                .returning(|_, _| Box::pin(async { Ok(()) }));
1123            // Expect partial_update to update the transaction status to Mined.
1124            mocks
1125                .tx_repo
1126                .expect_partial_update()
1127                .returning(|_, update| {
1128                    let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1129                    updated_tx.status = update.status.unwrap_or(updated_tx.status);
1130                    Ok(updated_tx)
1131                });
1132
1133            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1134            let result = evm_transaction
1135                .handle_mined_state(tx.clone())
1136                .await
1137                .unwrap();
1138            assert_eq!(result.status, TransactionStatus::Mined);
1139        }
1140    }
1141
1142    // Tests for `handle_final_state`
1143    mod handle_final_state_tests {
1144        use super::*;
1145
1146        #[tokio::test]
1147        async fn test_final_state_confirmed() {
1148            let mut mocks = default_test_mocks();
1149            let relayer = create_test_relayer();
1150            let tx = make_test_transaction(TransactionStatus::Submitted);
1151
1152            // Expect partial_update to update status to Confirmed.
1153            mocks
1154                .tx_repo
1155                .expect_partial_update()
1156                .returning(|_, update| {
1157                    let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1158                    updated_tx.status = update.status.unwrap_or(updated_tx.status);
1159                    Ok(updated_tx)
1160                });
1161
1162            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1163            let result = evm_transaction
1164                .handle_final_state(tx.clone(), TransactionStatus::Confirmed)
1165                .await
1166                .unwrap();
1167            assert_eq!(result.status, TransactionStatus::Confirmed);
1168        }
1169
1170        #[tokio::test]
1171        async fn test_final_state_failed() {
1172            let mut mocks = default_test_mocks();
1173            let relayer = create_test_relayer();
1174            let tx = make_test_transaction(TransactionStatus::Submitted);
1175
1176            // Expect partial_update to update status to Failed.
1177            mocks
1178                .tx_repo
1179                .expect_partial_update()
1180                .returning(|_, update| {
1181                    let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1182                    updated_tx.status = update.status.unwrap_or(updated_tx.status);
1183                    Ok(updated_tx)
1184                });
1185
1186            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1187            let result = evm_transaction
1188                .handle_final_state(tx.clone(), TransactionStatus::Failed)
1189                .await
1190                .unwrap();
1191            assert_eq!(result.status, TransactionStatus::Failed);
1192        }
1193
1194        #[tokio::test]
1195        async fn test_final_state_expired() {
1196            let mut mocks = default_test_mocks();
1197            let relayer = create_test_relayer();
1198            let tx = make_test_transaction(TransactionStatus::Submitted);
1199
1200            // Expect partial_update to update status to Expired.
1201            mocks
1202                .tx_repo
1203                .expect_partial_update()
1204                .returning(|_, update| {
1205                    let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1206                    updated_tx.status = update.status.unwrap_or(updated_tx.status);
1207                    Ok(updated_tx)
1208                });
1209
1210            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1211            let result = evm_transaction
1212                .handle_final_state(tx.clone(), TransactionStatus::Expired)
1213                .await
1214                .unwrap();
1215            assert_eq!(result.status, TransactionStatus::Expired);
1216        }
1217    }
1218
1219    // Integration tests for `handle_status_impl`
1220    mod handle_status_impl_tests {
1221        use super::*;
1222
1223        #[tokio::test]
1224        async fn test_impl_submitted_branch() {
1225            let mut mocks = default_test_mocks();
1226            let relayer = create_test_relayer();
1227            let mut tx = make_test_transaction(TransactionStatus::Submitted);
1228            tx.sent_at = Some((Utc::now() - Duration::seconds(120)).to_rfc3339());
1229            // Set a dummy hash so check_transaction_status can proceed.
1230            if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1231                evm_data.hash = Some("0xFakeHash".to_string());
1232            }
1233            // Simulate no receipt found.
1234            mocks
1235                .provider
1236                .expect_get_transaction_receipt()
1237                .returning(|_| Box::pin(async { Ok(None) }));
1238            // Mock network repository for should_resubmit check
1239            mocks
1240                .network_repo
1241                .expect_get_by_chain_id()
1242                .returning(|_, _| Ok(Some(create_test_network_model())));
1243            // Expect that a status check job is scheduled.
1244            mocks
1245                .job_producer
1246                .expect_produce_check_transaction_status_job()
1247                .returning(|_, _| Box::pin(async { Ok(()) }));
1248            // Expect update_transaction_status_if_needed to update status to Submitted.
1249            mocks
1250                .tx_repo
1251                .expect_partial_update()
1252                .returning(|_, update| {
1253                    let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1254                    updated_tx.status = update.status.unwrap_or(updated_tx.status);
1255                    Ok(updated_tx)
1256                });
1257
1258            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1259            let result = evm_transaction.handle_status_impl(tx).await.unwrap();
1260            assert_eq!(result.status, TransactionStatus::Submitted);
1261        }
1262
1263        #[tokio::test]
1264        async fn test_impl_mined_branch() {
1265            let mut mocks = default_test_mocks();
1266            let relayer = create_test_relayer();
1267            let mut tx = make_test_transaction(TransactionStatus::Submitted);
1268            // Set a dummy hash.
1269            if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1270                evm_data.hash = Some("0xFakeHash".to_string());
1271            }
1272            // Simulate a receipt with a block number of 100 and a successful receipt.
1273            mocks
1274                .provider
1275                .expect_get_transaction_receipt()
1276                .returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) }));
1277            // Simulate that the current block number is 100 (so confirmations are insufficient).
1278            mocks
1279                .provider
1280                .expect_get_block_number()
1281                .return_once(|| Box::pin(async { Ok(100) }));
1282            // Mock network repository to return a test network model
1283            mocks
1284                .network_repo
1285                .expect_get_by_chain_id()
1286                .returning(|_, _| Ok(Some(create_test_network_model())));
1287            // Expect a status check job to be scheduled.
1288            mocks
1289                .job_producer
1290                .expect_produce_check_transaction_status_job()
1291                .returning(|_, _| Box::pin(async { Ok(()) }));
1292            // Expect update_transaction_status_if_needed to update status to Mined.
1293            mocks
1294                .tx_repo
1295                .expect_partial_update()
1296                .returning(|_, update| {
1297                    let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1298                    updated_tx.status = update.status.unwrap_or(updated_tx.status);
1299                    Ok(updated_tx)
1300                });
1301
1302            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1303            let result = evm_transaction.handle_status_impl(tx).await.unwrap();
1304            assert_eq!(result.status, TransactionStatus::Mined);
1305        }
1306
1307        #[tokio::test]
1308        async fn test_impl_final_confirmed_branch() {
1309            let mut mocks = default_test_mocks();
1310            let relayer = create_test_relayer();
1311            // Create a transaction with status Confirmed.
1312            let tx = make_test_transaction(TransactionStatus::Confirmed);
1313
1314            // In this branch, check_transaction_status returns the final status immediately,
1315            // so we expect partial_update to update the transaction status to Confirmed.
1316            mocks
1317                .tx_repo
1318                .expect_partial_update()
1319                .returning(|_, update| {
1320                    let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1321                    updated_tx.status = update.status.unwrap_or(updated_tx.status);
1322                    Ok(updated_tx)
1323                });
1324
1325            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1326            let result = evm_transaction.handle_status_impl(tx).await.unwrap();
1327            assert_eq!(result.status, TransactionStatus::Confirmed);
1328        }
1329
1330        #[tokio::test]
1331        async fn test_impl_final_failed_branch() {
1332            let mut mocks = default_test_mocks();
1333            let relayer = create_test_relayer();
1334            // Create a transaction with status Failed.
1335            let tx = make_test_transaction(TransactionStatus::Failed);
1336
1337            mocks
1338                .tx_repo
1339                .expect_partial_update()
1340                .returning(|_, update| {
1341                    let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1342                    updated_tx.status = update.status.unwrap_or(updated_tx.status);
1343                    Ok(updated_tx)
1344                });
1345
1346            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1347            let result = evm_transaction.handle_status_impl(tx).await.unwrap();
1348            assert_eq!(result.status, TransactionStatus::Failed);
1349        }
1350
1351        #[tokio::test]
1352        async fn test_impl_final_expired_branch() {
1353            let mut mocks = default_test_mocks();
1354            let relayer = create_test_relayer();
1355            // Create a transaction with status Expired.
1356            let tx = make_test_transaction(TransactionStatus::Expired);
1357
1358            mocks
1359                .tx_repo
1360                .expect_partial_update()
1361                .returning(|_, update| {
1362                    let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1363                    updated_tx.status = update.status.unwrap_or(updated_tx.status);
1364                    Ok(updated_tx)
1365                });
1366
1367            let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1368            let result = evm_transaction.handle_status_impl(tx).await.unwrap();
1369            assert_eq!(result.status, TransactionStatus::Expired);
1370        }
1371    }
1372}