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