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