openzeppelin_relayer/domain/transaction/evm/
evm_transaction.rs

1//! This module defines the `EvmRelayerTransaction` struct and its associated
2//! functionality for handling Ethereum Virtual Machine (EVM) transactions.
3//! It includes methods for preparing, submitting, handling status, and
4//! managing notifications for transactions. The module leverages various
5//! services and repositories to perform these operations asynchronously.
6
7use async_trait::async_trait;
8use chrono::Utc;
9use eyre::Result;
10use std::sync::Arc;
11use tracing::{debug, error, info, warn};
12
13use crate::{
14    constants::{
15        matches_known_transaction, ALREADY_SUBMITTED_PATTERNS, DEFAULT_EVM_GAS_LIMIT_ESTIMATION,
16        GAS_LIMIT_BUFFER_MULTIPLIER, MAX_NONCE_TOO_HIGH_RETRIES, NONCE_TOO_HIGH_PATTERNS,
17    },
18    domain::{
19        evm::is_noop,
20        transaction::{
21            evm::{ensure_status, ensure_status_one_of, PriceCalculator, PriceCalculatorTrait},
22            Transaction,
23        },
24        EvmTransactionValidationError, EvmTransactionValidator,
25    },
26    jobs::{
27        JobProducer, JobProducerTrait, RelayerHealthCheck, StatusCheckContext, TransactionSend,
28        TransactionStatusCheck,
29    },
30    models::{
31        produce_transaction_update_notification_payload, EvmNetwork, EvmTransactionData,
32        NetworkRepoModel, NetworkTransactionData, NetworkTransactionRequest, NetworkType,
33        RelayerEvmPolicy, RelayerRepoModel, TransactionError, TransactionMetadata,
34        TransactionRepoModel, TransactionStatus, TransactionUpdateRequest,
35    },
36    repositories::{
37        NetworkRepository, NetworkRepositoryStorage, RelayerRepository, RelayerRepositoryStorage,
38        Repository, TransactionCounterRepositoryStorage, TransactionCounterTrait,
39        TransactionRepository, TransactionRepositoryStorage,
40    },
41    services::{
42        gas::evm_gas_price::EvmGasPriceService,
43        provider::{EvmProvider, EvmProviderTrait},
44        signer::{EvmSigner, Signer},
45    },
46    utils::{calculate_scheduled_timestamp, get_evm_default_gas_limit_for_tx},
47};
48
49use super::PriceParams;
50
51/// Metadata key that triggers nonce reconciliation in the status checker.
52/// Written by `schedule_nonce_recovery_status_check`, read by `handle_status_impl`.
53/// The value carries the `SubmissionErrorKind` that caused the trigger.
54pub(super) const TX_NONCE_RECONCILE_TRIGGER: &str = "tx_nonce_reconcile_trigger";
55
56/// Classifies submission/resubmission RPC errors for targeted handling.
57///
58/// Built on top of `ALREADY_SUBMITTED_PATTERNS` to stay aligned with the
59/// provider-level retry classification in `is_non_retriable_transaction_rpc_message`.
60///
61/// Different nonce-related errors require different recovery strategies:
62/// - `NonceTooLow`: The nonce was consumed (by us or externally) — needs reconciliation
63/// - `AlreadyKnown`: The exact transaction is already in the mempool — safe to treat as submitted
64/// - `ReplacementUnderpriced`: A tx with this nonce exists but our gas price is too low
65/// - `Other`: Unrecognized error — propagate as-is
66#[derive(Debug, Clone, PartialEq)]
67pub(super) enum SubmissionErrorKind {
68    NonceTooLow,
69    AlreadyKnown,
70    ReplacementUnderpriced,
71    NonceTooHigh,
72    Other(String),
73}
74
75#[allow(dead_code)]
76pub struct EvmRelayerTransaction<P, RR, NR, TR, J, S, TCR, PC>
77where
78    P: EvmProviderTrait,
79    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
80    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
81    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
82    J: JobProducerTrait + Send + Sync + 'static,
83    S: Signer + Send + Sync + 'static,
84    TCR: TransactionCounterTrait + Send + Sync + 'static,
85    PC: PriceCalculatorTrait,
86{
87    provider: P,
88    relayer_repository: Arc<RR>,
89    network_repository: Arc<NR>,
90    transaction_repository: Arc<TR>,
91    job_producer: Arc<J>,
92    signer: S,
93    relayer: RelayerRepoModel,
94    transaction_counter_service: Arc<TCR>,
95    price_calculator: PC,
96}
97
98#[allow(dead_code, clippy::too_many_arguments)]
99impl<P, RR, NR, TR, J, S, TCR, PC> EvmRelayerTransaction<P, RR, NR, TR, J, S, TCR, PC>
100where
101    P: EvmProviderTrait,
102    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
103    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
104    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
105    J: JobProducerTrait + Send + Sync + 'static,
106    S: Signer + Send + Sync + 'static,
107    TCR: TransactionCounterTrait + Send + Sync + 'static,
108    PC: PriceCalculatorTrait,
109{
110    /// Creates a new `EvmRelayerTransaction`.
111    ///
112    /// # Arguments
113    ///
114    /// * `relayer` - The relayer model.
115    /// * `provider` - The EVM provider.
116    /// * `relayer_repository` - Storage for relayer repository.
117    /// * `transaction_repository` - Storage for transaction repository.
118    /// * `transaction_counter_service` - Service for managing transaction counters.
119    /// * `job_producer` - Producer for job queue.
120    /// * `price_calculator` - Price calculator for gas price management.
121    /// * `signer` - The EVM signer.
122    ///
123    /// # Returns
124    ///
125    /// A result containing the new `EvmRelayerTransaction` or a `TransactionError`.
126    pub fn new(
127        relayer: RelayerRepoModel,
128        provider: P,
129        relayer_repository: Arc<RR>,
130        network_repository: Arc<NR>,
131        transaction_repository: Arc<TR>,
132        transaction_counter_service: Arc<TCR>,
133        job_producer: Arc<J>,
134        price_calculator: PC,
135        signer: S,
136    ) -> Result<Self, TransactionError> {
137        Ok(Self {
138            relayer,
139            provider,
140            relayer_repository,
141            network_repository,
142            transaction_repository,
143            transaction_counter_service,
144            job_producer,
145            price_calculator,
146            signer,
147        })
148    }
149
150    /// Returns a reference to the provider.
151    pub fn provider(&self) -> &P {
152        &self.provider
153    }
154
155    /// Returns a reference to the relayer model.
156    pub fn relayer(&self) -> &RelayerRepoModel {
157        &self.relayer
158    }
159
160    /// Returns a reference to the network repository.
161    pub fn network_repository(&self) -> &NR {
162        &self.network_repository
163    }
164
165    /// Returns a reference to the job producer.
166    pub fn job_producer(&self) -> &J {
167        &self.job_producer
168    }
169
170    pub fn transaction_repository(&self) -> &TR {
171        &self.transaction_repository
172    }
173
174    /// Classifies a submission/resubmission error into a specific kind for targeted handling.
175    ///
176    /// Uses `ALREADY_SUBMITTED_PATTERNS` and `matches_known_transaction` from constants
177    /// to stay aligned with `is_non_retriable_transaction_rpc_message` in `services::provider`.
178    /// The patterns are grouped into finer-grained categories to enable different recovery
179    /// strategies (e.g., NonceTooLow triggers reconciliation, AlreadyKnown is safe to ignore).
180    fn classify_submission_error(error: &impl std::fmt::Display) -> SubmissionErrorKind {
181        let original = error.to_string();
182        let error_msg = original.to_lowercase();
183
184        // Check against ALREADY_SUBMITTED_PATTERNS first — this is the canonical pattern list.
185        // We classify each match into the appropriate SubmissionErrorKind.
186        for pattern in ALREADY_SUBMITTED_PATTERNS {
187            if error_msg.contains(pattern) {
188                return match *pattern {
189                    "nonce too low" | "nonce is too low" => SubmissionErrorKind::NonceTooLow,
190                    "replacement transaction underpriced" => {
191                        SubmissionErrorKind::ReplacementUnderpriced
192                    }
193                    // "already known", "same hash was already imported"
194                    _ => SubmissionErrorKind::AlreadyKnown,
195                };
196            }
197        }
198
199        // Also check the special "known transaction" pattern (Besu) which isn't a simple
200        // substring match — it needs to avoid matching "unknown transaction".
201        if matches_known_transaction(&error_msg) {
202            return SubmissionErrorKind::AlreadyKnown;
203        }
204
205        // Check for "nonce too high" patterns — kept separate from ALREADY_SUBMITTED_PATTERNS
206        // because they require a different recovery strategy (retry then escalate vs reconcile).
207        for pattern in NONCE_TOO_HIGH_PATTERNS {
208            if error_msg.contains(pattern) {
209                return SubmissionErrorKind::NonceTooHigh;
210            }
211        }
212
213        SubmissionErrorKind::Other(original)
214    }
215
216    /// Checks if a provider error indicates the transaction was already submitted to the blockchain.
217    /// Delegates to `classify_submission_error` which uses `ALREADY_SUBMITTED_PATTERNS`.
218    fn is_already_submitted_error(error: &impl std::fmt::Display) -> bool {
219        matches!(
220            Self::classify_submission_error(error),
221            SubmissionErrorKind::NonceTooLow
222                | SubmissionErrorKind::AlreadyKnown
223                | SubmissionErrorKind::ReplacementUnderpriced
224        )
225    }
226
227    /// Helper method to schedule a transaction status check job.
228    ///
229    /// Optionally attaches metadata (e.g., nonce recovery hints) to the job.
230    pub(super) async fn schedule_status_check(
231        &self,
232        tx: &TransactionRepoModel,
233        delay_seconds: Option<i64>,
234        metadata: Option<std::collections::HashMap<String, String>>,
235    ) -> Result<(), TransactionError> {
236        let delay = delay_seconds.map(calculate_scheduled_timestamp);
237        let mut job = TransactionStatusCheck::new(
238            tx.id.clone(),
239            tx.relayer_id.clone(),
240            crate::models::NetworkType::Evm,
241        );
242        if let Some(meta) = metadata {
243            job = job.with_metadata(meta);
244        }
245        self.job_producer()
246            .produce_check_transaction_status_job(job, delay)
247            .await
248            .map_err(|e| {
249                TransactionError::UnexpectedError(format!("Failed to schedule status check: {e}"))
250            })
251    }
252
253    /// Schedules a status check with nonce recovery metadata for immediate execution.
254    ///
255    /// This is used when a nonce-related error occurs during submission. The metadata
256    /// signals the status checker to perform nonce reconciliation on first check.
257    /// Subsequent retries (re-queued via `Err(Retry)`) won't carry the metadata,
258    /// so they follow normal status check flow — this is intentional one-shot behavior.
259    pub(super) async fn schedule_nonce_recovery_status_check(
260        &self,
261        tx: &TransactionRepoModel,
262        error_kind: &SubmissionErrorKind,
263    ) -> Result<(), TransactionError> {
264        let mut metadata = std::collections::HashMap::new();
265        metadata.insert(
266            TX_NONCE_RECONCILE_TRIGGER.to_string(),
267            format!("{error_kind:?}"),
268        );
269        self.schedule_status_check(tx, None, Some(metadata)).await
270    }
271
272    /// Schedules a targeted nonce health job for this transaction's relayer.
273    ///
274    /// Called when "nonce too high" retries are exhausted, indicating a persistent
275    /// counter drift rather than transient burst ordering. The health job will
276    /// detect and fill nonce gaps with NOOPs.
277    pub(super) async fn schedule_relayer_nonce_health_job(
278        &self,
279        tx: &TransactionRepoModel,
280    ) -> Result<(), TransactionError> {
281        // Include the tx nonce as a hint so resolve_nonce_gaps can extend
282        // its scan range even if the counter was reset below this nonce.
283        let nonce_hint = tx
284            .network_data
285            .get_evm_transaction_data()
286            .ok()
287            .and_then(|d| d.nonce);
288        let job = match nonce_hint {
289            Some(nonce) => RelayerHealthCheck::nonce_health_with_hint(tx.relayer_id.clone(), nonce),
290            None => RelayerHealthCheck::nonce_health(tx.relayer_id.clone()),
291        };
292
293        self.job_producer()
294            .produce_relayer_health_check_job(job, None)
295            .await
296            .map_err(|e| {
297                TransactionError::UnexpectedError(format!(
298                    "Failed to schedule nonce health check: {e}"
299                ))
300            })
301    }
302
303    /// Handles a "nonce too high" error by incrementing the retry counter and
304    /// escalating to a nonce health job after the threshold.
305    pub(super) async fn handle_nonce_too_high(&self, tx: &TransactionRepoModel, context: &str) {
306        let retry_count = tx
307            .metadata
308            .as_ref()
309            .map(|m| m.nonce_too_high_retries)
310            .unwrap_or(0);
311
312        let new_count = retry_count + 1;
313
314        // Persist incremented counter + status_reason on tx metadata
315        let update = TransactionUpdateRequest {
316            metadata: Some(TransactionMetadata {
317                nonce_too_high_retries: new_count,
318                ..tx.metadata.clone().unwrap_or_default()
319            }),
320            status_reason: Some(format!("Nonce too high (attempt {new_count})")),
321            ..Default::default()
322        };
323        if let Err(update_err) = self
324            .transaction_repository
325            .partial_update(tx.id.clone(), update)
326            .await
327        {
328            warn!(
329                tx_id = %tx.id,
330                error = %update_err,
331                "failed to persist nonce_too_high_retries metadata {context}"
332            );
333        }
334
335        if new_count >= MAX_NONCE_TOO_HIGH_RETRIES {
336            warn!(
337                tx_id = %tx.id,
338                "nonce too high after {} attempts {context}, scheduling nonce health check",
339                new_count
340            );
341            if let Err(schedule_err) = self.schedule_relayer_nonce_health_job(tx).await {
342                error!(
343                    tx_id = %tx.id,
344                    error = %schedule_err,
345                    "failed to schedule nonce health check {context}"
346                );
347            }
348        } else {
349            warn!(
350                tx_id = %tx.id,
351                "nonce too high {context} (attempt {}/{}), status checker will retry",
352                new_count,
353                MAX_NONCE_TOO_HIGH_RETRIES
354            );
355        }
356    }
357
358    /// Helper method to produce a submit transaction job.
359    pub(super) async fn send_transaction_submit_job(
360        &self,
361        tx: &TransactionRepoModel,
362    ) -> Result<(), TransactionError> {
363        debug!(
364            tx_id = %tx.id,
365            relayer_id = %tx.relayer_id,
366            "enqueueing submit transaction job"
367        );
368        let job = TransactionSend::submit(tx.id.clone(), tx.relayer_id.clone());
369
370        self.job_producer()
371            .produce_submit_transaction_job(job, None)
372            .await
373            .map_err(|e| {
374                TransactionError::UnexpectedError(format!("Failed to produce submit job: {e}"))
375            })
376    }
377
378    /// Helper method to produce a resubmit transaction job.
379    pub(super) async fn send_transaction_resubmit_job(
380        &self,
381        tx: &TransactionRepoModel,
382    ) -> Result<(), TransactionError> {
383        debug!(
384            tx_id = %tx.id,
385            relayer_id = %tx.relayer_id,
386            "enqueueing resubmit transaction job"
387        );
388        let job = TransactionSend::resubmit(tx.id.clone(), tx.relayer_id.clone());
389
390        self.job_producer()
391            .produce_submit_transaction_job(job, None)
392            .await
393            .map_err(|e| {
394                TransactionError::UnexpectedError(format!("Failed to produce resubmit job: {e}"))
395            })
396    }
397
398    /// Helper method to produce a resend transaction job.
399    pub(super) async fn send_transaction_resend_job(
400        &self,
401        tx: &TransactionRepoModel,
402    ) -> Result<(), TransactionError> {
403        debug!(
404            tx_id = %tx.id,
405            relayer_id = %tx.relayer_id,
406            "enqueueing resend transaction job"
407        );
408        let job = TransactionSend::resend(tx.id.clone(), tx.relayer_id.clone());
409
410        self.job_producer()
411            .produce_submit_transaction_job(job, None)
412            .await
413            .map_err(|e| {
414                TransactionError::UnexpectedError(format!("Failed to produce resend job: {e}"))
415            })
416    }
417
418    /// Helper method to produce a transaction request (prepare) job.
419    pub(super) async fn send_transaction_request_job(
420        &self,
421        tx: &TransactionRepoModel,
422    ) -> Result<(), TransactionError> {
423        use crate::jobs::TransactionRequest;
424
425        let job = TransactionRequest::new(tx.id.clone(), tx.relayer_id.clone());
426
427        self.job_producer()
428            .produce_transaction_request_job(job, None)
429            .await
430            .map_err(|e| {
431                TransactionError::UnexpectedError(format!("Failed to produce request job: {e}"))
432            })
433    }
434
435    /// Updates a transaction's status, optionally including a status reason.
436    pub(super) async fn update_transaction_status(
437        &self,
438        tx: TransactionRepoModel,
439        new_status: TransactionStatus,
440        status_reason: Option<String>,
441    ) -> Result<TransactionRepoModel, TransactionError> {
442        let confirmed_at = if new_status == TransactionStatus::Confirmed {
443            Some(Utc::now().to_rfc3339())
444        } else {
445            None
446        };
447
448        let update_request = TransactionUpdateRequest {
449            status: Some(new_status),
450            confirmed_at,
451            status_reason,
452            ..Default::default()
453        };
454
455        let updated_tx = self
456            .transaction_repository()
457            .partial_update(tx.id.clone(), update_request)
458            .await?;
459
460        if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
461            error!(
462                tx_id = %updated_tx.id,
463                status = ?updated_tx.status,
464                "sending transaction update notification failed: {:?}",
465                e
466            );
467        }
468        Ok(updated_tx)
469    }
470
471    /// Sends a transaction update notification if a notification ID is configured.
472    ///
473    /// This is a best-effort operation that logs errors but does not propagate them,
474    /// as notification failures should not affect the transaction lifecycle.
475    pub(super) async fn send_transaction_update_notification(
476        &self,
477        tx: &TransactionRepoModel,
478    ) -> Result<(), eyre::Report> {
479        if let Some(notification_id) = &self.relayer().notification_id {
480            self.job_producer()
481                .produce_send_notification_job(
482                    produce_transaction_update_notification_payload(notification_id, tx),
483                    None,
484                )
485                .await?;
486        }
487        Ok(())
488    }
489
490    /// Marks a transaction as failed with a reason, updates it, sends notification, and returns the updated transaction.
491    ///
492    /// This is a common pattern used when a transaction should be marked as failed.
493    ///
494    /// # Arguments
495    ///
496    /// * `tx` - The transaction to mark as failed
497    /// * `reason` - The reason for the failure
498    /// * `error_context` - Context string for error logging (e.g., "gas limit exceeds block gas limit")
499    ///
500    /// # Returns
501    ///
502    /// The updated transaction with Failed status
503    async fn mark_transaction_as_failed(
504        &self,
505        tx: &TransactionRepoModel,
506        reason: String,
507        error_context: &str,
508    ) -> Result<TransactionRepoModel, TransactionError> {
509        let update = TransactionUpdateRequest {
510            status: Some(TransactionStatus::Failed),
511            status_reason: Some(reason.clone()),
512            ..Default::default()
513        };
514
515        let updated_tx = self
516            .transaction_repository
517            .partial_update(tx.id.clone(), update)
518            .await?;
519
520        if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
521            error!(
522                tx_id = %updated_tx.id,
523                status = ?TransactionStatus::Failed,
524                "sending transaction update notification failed for {}: {:?}",
525                error_context,
526                e
527            );
528        }
529
530        Ok(updated_tx)
531    }
532
533    /// Validates that the relayer has sufficient balance for the transaction.
534    ///
535    /// # Arguments
536    ///
537    /// * `total_cost` - The total cost of the transaction (gas + value)
538    ///
539    /// # Returns
540    ///
541    /// A `Result` indicating success or a `TransactionError`.
542    /// - Returns `InsufficientBalance` only when balance is truly insufficient (permanent failure)
543    /// - Returns `UnexpectedError` for RPC/network issues (retryable)
544    async fn ensure_sufficient_balance(
545        &self,
546        total_cost: crate::models::U256,
547    ) -> Result<(), TransactionError> {
548        EvmTransactionValidator::validate_sufficient_relayer_balance(
549            total_cost,
550            &self.relayer().address,
551            &self.relayer().policies.get_evm_policy(),
552            &self.provider,
553        )
554        .await
555        .map_err(|validation_error| match validation_error {
556            // Only convert actual insufficient balance to permanent failure
557            EvmTransactionValidationError::InsufficientBalance(msg) => {
558                TransactionError::InsufficientBalance(msg)
559            }
560            // Provider errors are retryable (RPC down, timeout, etc.)
561            EvmTransactionValidationError::ProviderError(msg) => {
562                TransactionError::UnexpectedError(format!("Failed to check balance: {msg}"))
563            }
564            // Validation errors are also retryable
565            EvmTransactionValidationError::ValidationError(msg) => {
566                TransactionError::UnexpectedError(format!("Balance validation error: {msg}"))
567            }
568        })
569    }
570
571    /// Estimates the gas limit for a transaction.
572    ///
573    /// # Arguments
574    ///
575    /// * `evm_data` - The EVM transaction data.
576    /// * `relayer_policy` - The relayer policy.
577    ///
578    async fn estimate_tx_gas_limit(
579        &self,
580        evm_data: &EvmTransactionData,
581        relayer_policy: &RelayerEvmPolicy,
582    ) -> Result<u64, TransactionError> {
583        if !relayer_policy
584            .gas_limit_estimation
585            .unwrap_or(DEFAULT_EVM_GAS_LIMIT_ESTIMATION)
586        {
587            warn!("gas limit estimation is disabled for relayer");
588            return Err(TransactionError::UnexpectedError(
589                "Gas limit estimation is disabled".to_string(),
590            ));
591        }
592
593        let estimated_gas = self.provider.estimate_gas(evm_data).await.map_err(|e| {
594            warn!(error = ?e, tx_data = ?evm_data, "failed to estimate gas");
595            TransactionError::UnexpectedError(format!("Failed to estimate gas: {e}"))
596        })?;
597
598        Ok(estimated_gas * GAS_LIMIT_BUFFER_MULTIPLIER / 100)
599    }
600}
601
602#[async_trait]
603impl<P, RR, NR, TR, J, S, TCR, PC> Transaction
604    for EvmRelayerTransaction<P, RR, NR, TR, J, S, TCR, PC>
605where
606    P: EvmProviderTrait + Send + Sync + 'static,
607    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
608    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
609    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
610    J: JobProducerTrait + Send + Sync + 'static,
611    S: Signer + Send + Sync + 'static,
612    TCR: TransactionCounterTrait + Send + Sync + 'static,
613    PC: PriceCalculatorTrait + Send + Sync + 'static,
614{
615    /// Prepares a transaction for submission.
616    ///
617    /// # Arguments
618    ///
619    /// * `tx` - The transaction model to prepare.
620    ///
621    /// # Returns
622    ///
623    /// A result containing the updated transaction model or a `TransactionError`.
624    async fn prepare_transaction(
625        &self,
626        tx: TransactionRepoModel,
627    ) -> Result<TransactionRepoModel, TransactionError> {
628        debug!(
629            tx_id = %tx.id,
630            relayer_id = %tx.relayer_id,
631            status = ?tx.status,
632            "preparing transaction"
633        );
634
635        // If transaction is not in Pending status, return Ok to avoid wasteful retries
636        // (e.g., if it's already Sent, Failed, or in another state)
637        if let Err(e) = ensure_status(&tx, TransactionStatus::Pending, Some("prepare_transaction"))
638        {
639            warn!(
640                tx_id = %tx.id,
641                status = ?tx.status,
642                error = %e,
643                "transaction not in Pending status, skipping preparation"
644            );
645            return Ok(tx);
646        }
647
648        let mut evm_data = tx.network_data.get_evm_transaction_data()?;
649        let relayer = self.relayer();
650
651        if evm_data.gas_limit.is_none() {
652            match self
653                .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
654                .await
655            {
656                Ok(estimated_gas_limit) => {
657                    evm_data.gas_limit = Some(estimated_gas_limit);
658                }
659                Err(estimation_error) => {
660                    error!(
661                        tx_id = %tx.id,
662                        relayer_id = %tx.relayer_id,
663                        error = ?estimation_error,
664                        "failed to estimate gas limit"
665                    );
666
667                    let default_gas_limit = get_evm_default_gas_limit_for_tx(&evm_data);
668                    debug!(
669                        tx_id = %tx.id,
670                        gas_limit = %default_gas_limit,
671                        "fallback to default gas limit"
672                    );
673                    evm_data.gas_limit = Some(default_gas_limit);
674                }
675            }
676        } else {
677            // do user gas limit validation against block gas limit
678            let block = self.provider.get_block_by_number().await;
679            if let Ok(block) = block {
680                let block_gas_limit = block.header.gas_limit;
681                if let Some(gas_limit) = evm_data.gas_limit {
682                    if gas_limit > block_gas_limit {
683                        let reason = format!(
684                            "Transaction gas limit ({gas_limit}) exceeds block gas limit ({block_gas_limit})",
685                        );
686                        warn!(
687                            tx_id = %tx.id,
688                            tx_gas_limit = %gas_limit,
689                            block_gas_limit = %block_gas_limit,
690                            "transaction gas limit exceeds block gas limit"
691                        );
692
693                        let updated_tx = self
694                            .mark_transaction_as_failed(
695                                &tx,
696                                reason,
697                                "gas limit exceeds block gas limit",
698                            )
699                            .await?;
700                        return Ok(updated_tx);
701                    }
702                }
703            }
704        }
705
706        // set the gas price
707        let price_params: PriceParams = self
708            .price_calculator
709            .get_transaction_price_params(&evm_data, relayer)
710            .await?;
711
712        debug!(
713            tx_id = %tx.id,
714            relayer_id = %tx.relayer_id,
715            gas_price = ?price_params.gas_price,
716            "gas price"
717        );
718
719        // Validate the relayer has sufficient balance before consuming nonce and signing
720        if let Err(balance_error) = self
721            .ensure_sufficient_balance(price_params.total_cost)
722            .await
723        {
724            // Only mark as Failed for actual insufficient balance, not RPC errors
725            match &balance_error {
726                TransactionError::InsufficientBalance(_) => {
727                    warn!(
728                        tx_id = %tx.id,
729                        relayer_id = %tx.relayer_id,
730                        error = %balance_error,
731                        "insufficient balance for transaction"
732                    );
733
734                    let updated_tx = self
735                        .mark_transaction_as_failed(
736                            &tx,
737                            balance_error.to_string(),
738                            "insufficient balance",
739                        )
740                        .await?;
741
742                    // Return Ok since transaction is in final Failed state - no retry needed
743                    return Ok(updated_tx);
744                }
745                // For RPC/provider errors, propagate without marking as Failed
746                // This allows the handler to retry
747                _ => {
748                    debug!(error = %balance_error, "failed to check balance, will retry");
749                    return Err(balance_error);
750                }
751            }
752        }
753
754        // Check if transaction already has a nonce (recovery from failed signing attempt)
755        let tx_with_nonce = if let Some(existing_nonce) = evm_data.nonce {
756            debug!(
757                nonce = existing_nonce,
758                "transaction already has nonce assigned, reusing for retry"
759            );
760            // Retry flow: When reusing an existing nonce from a failed attempt, we intentionally
761            // do NOT persist the fresh price_params (computed earlier) to the DB here. The DB may
762            // temporarily hold stale price_params from the failed attempt. However, fresh price_params
763            // are applied just before signing, ensuring the transaction uses
764            // current gas prices.
765            tx
766        } else {
767            // Balance validation passed, proceed to increment nonce
768            let new_nonce = self
769                .transaction_counter_service
770                .get_and_increment(&self.relayer.id, &self.relayer.address)
771                .await
772                .map_err(|e| TransactionError::UnexpectedError(e.to_string()))?;
773
774            debug!(nonce = new_nonce, "assigned new nonce to transaction");
775
776            let updated_evm_data = evm_data
777                .with_price_params(price_params.clone())
778                .with_nonce(new_nonce);
779
780            // Save transaction with nonce BEFORE signing
781            // This ensures we can recover if signing fails (timeout, KMS error, etc.)
782            let presign_update = TransactionUpdateRequest {
783                network_data: Some(NetworkTransactionData::Evm(updated_evm_data.clone())),
784                priced_at: Some(Utc::now().to_rfc3339()),
785                ..Default::default()
786            };
787
788            self.transaction_repository
789                .partial_update(tx.id.clone(), presign_update)
790                .await?
791        };
792
793        // Apply price params for signing (recalculated on every attempt)
794        let updated_evm_data = tx_with_nonce
795            .network_data
796            .get_evm_transaction_data()?
797            .with_price_params(price_params.clone());
798
799        // Now sign the transaction - if this fails, we still have the tx with nonce saved
800        let sig_result = self
801            .signer
802            .sign_transaction(NetworkTransactionData::Evm(updated_evm_data.clone()))
803            .await?;
804
805        let updated_evm_data =
806            updated_evm_data.with_signed_transaction_data(sig_result.into_evm()?);
807
808        // Track the transaction hash
809        let mut hashes = tx_with_nonce.hashes.clone();
810        if let Some(hash) = updated_evm_data.hash.clone() {
811            hashes.push(hash);
812        }
813
814        // Update with signed data and mark as Sent
815        let postsign_update = TransactionUpdateRequest {
816            status: Some(TransactionStatus::Sent),
817            network_data: Some(NetworkTransactionData::Evm(updated_evm_data)),
818            hashes: Some(hashes),
819            ..Default::default()
820        };
821
822        let updated_tx = self
823            .transaction_repository
824            .partial_update(tx_with_nonce.id.clone(), postsign_update)
825            .await?;
826
827        debug!(
828            tx_id = %updated_tx.id,
829            relayer_id = %updated_tx.relayer_id,
830            status = ?updated_tx.status,
831            "transaction status updated to Sent"
832        );
833
834        // after preparing the transaction, we need to submit it to the job queue
835        self.job_producer
836            .produce_submit_transaction_job(
837                TransactionSend::submit(updated_tx.id.clone(), updated_tx.relayer_id.clone()),
838                None,
839            )
840            .await?;
841
842        if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
843            error!(
844                tx_id = %updated_tx.id,
845                relayer_id = %updated_tx.relayer_id,
846                status = ?TransactionStatus::Sent,
847                error = %e,
848                "sending transaction update notification failed after prepare"
849            );
850        }
851
852        Ok(updated_tx)
853    }
854
855    /// Submits a transaction for processing.
856    ///
857    /// # Arguments
858    ///
859    /// * `tx` - The transaction model to submit.
860    ///
861    /// # Returns
862    ///
863    /// A result containing the updated transaction model or a `TransactionError`.
864    async fn submit_transaction(
865        &self,
866        tx: TransactionRepoModel,
867    ) -> Result<TransactionRepoModel, TransactionError> {
868        debug!(
869            tx_id = %tx.id,
870            relayer_id = %tx.relayer_id,
871            status = ?tx.status,
872            "submitting transaction"
873        );
874
875        // If transaction is not in correct status, return Ok to avoid wasteful retries
876        // (e.g., if it's already in a final state like Failed, Confirmed, etc.)
877        if let Err(e) = ensure_status_one_of(
878            &tx,
879            &[TransactionStatus::Sent, TransactionStatus::Submitted],
880            Some("submit_transaction"),
881        ) {
882            warn!(
883                tx_id = %tx.id,
884                status = ?tx.status,
885                error = %e,
886                "transaction not in expected status for submission, skipping"
887            );
888            return Ok(tx);
889        }
890
891        let evm_tx_data = tx.network_data.get_evm_transaction_data()?;
892        let raw_tx = evm_tx_data.raw.as_ref().ok_or_else(|| {
893            TransactionError::InvalidType("Raw transaction data is missing".to_string())
894        })?;
895
896        // Send transaction to blockchain - this is the critical operation
897        // If this fails, retry is safe due to nonce idempotency
898        match self.provider.send_raw_transaction(raw_tx).await {
899            Ok(_) => {
900                // Transaction submitted successfully
901            }
902            Err(e) => {
903                let error_kind = Self::classify_submission_error(&e);
904
905                match (&tx.status, &error_kind) {
906                    // AlreadyKnown / ReplacementUnderpriced (any status):
907                    // The node recognizes the exact same transaction bytes (same hash)
908                    // in its mempool — this confirms it's our tx. Safe to treat as submitted.
909                    (_, SubmissionErrorKind::AlreadyKnown)
910                    | (_, SubmissionErrorKind::ReplacementUnderpriced) => {
911                        warn!(
912                            tx_id = %tx.id,
913                            error = %e,
914                            error_kind = ?error_kind,
915                            "transaction appears to be already submitted based on RPC error - treating as success"
916                        );
917                        // Continue to update status to Submitted
918                    }
919                    // NonceTooLow (any status): the nonce was consumed, but we don't know
920                    // by whom — could be our tx (retry after crash) or a different tx
921                    // (multi-instance / external wallet). Schedule nonce recovery via the
922                    // status checker, which will check receipts and on-chain nonce to
923                    // determine the actual outcome.
924                    (_, SubmissionErrorKind::NonceTooLow) => {
925                        warn!(
926                            tx_id = %tx.id,
927                            status = ?tx.status,
928                            error = %e,
929                            error_kind = ?error_kind,
930                            "nonce error during submission - scheduling nonce recovery"
931                        );
932
933                        // Persist status_reason so the error is visible
934                        let reason = format!("Nonce error during submission: {error_kind:?}");
935                        let update = TransactionUpdateRequest {
936                            status_reason: Some(reason),
937                            ..Default::default()
938                        };
939                        if let Err(update_err) = self
940                            .transaction_repository
941                            .partial_update(tx.id.clone(), update)
942                            .await
943                        {
944                            warn!(
945                                tx_id = %tx.id,
946                                error = %update_err,
947                                "failed to persist status_reason for nonce error"
948                            );
949                        }
950
951                        // Schedule nonce recovery status check (best effort)
952                        if let Err(schedule_err) = self
953                            .schedule_nonce_recovery_status_check(&tx, &error_kind)
954                            .await
955                        {
956                            error!(
957                                tx_id = %tx.id,
958                                error = %schedule_err,
959                                "failed to schedule nonce recovery status check"
960                            );
961                        }
962
963                        // Return Ok to prevent Dead Queue — status checker handles reconciliation
964                        return Ok(tx);
965                    }
966                    // NonceTooHigh: transaction nonce is ahead of on-chain nonce.
967                    // Could be transient (burst ordering) or persistent (counter drift).
968                    // Track retries and escalate to nonce health job after threshold.
969                    (_, SubmissionErrorKind::NonceTooHigh) => {
970                        self.handle_nonce_too_high(&tx, "during submission").await;
971                        // Return Ok to prevent Dead Queue — status checker handles resubmission
972                        return Ok(tx);
973                    }
974                    // All other errors: propagate as before
975                    _ => {
976                        return Err(e.into());
977                    }
978                }
979            }
980        }
981
982        // Transaction is now on-chain - update database
983        // If this fails, transaction is still valid, just not tracked correctly
984        // Reset nonce_too_high_retries on success so resubmission gets a fresh retry budget.
985        let metadata_reset = tx
986            .metadata
987            .as_ref()
988            .and_then(|m| m.with_nonce_retries_reset());
989        let update = TransactionUpdateRequest {
990            status: Some(TransactionStatus::Submitted),
991            sent_at: Some(Utc::now().to_rfc3339()),
992            metadata: metadata_reset,
993            ..Default::default()
994        };
995
996        let updated_tx = match self
997            .transaction_repository
998            .partial_update(tx.id.clone(), update)
999            .await
1000        {
1001            Ok(tx) => tx,
1002            Err(e) => {
1003                error!(
1004                    tx_id = %tx.id,
1005                    relayer_id = %tx.relayer_id,
1006                    error = %e,
1007                    "CRITICAL: transaction sent to blockchain but failed to update database - transaction may not be tracked correctly"
1008                );
1009                // Transaction is on-chain - don't propagate error to avoid wasteful retries
1010                // Return the original transaction data
1011                tx
1012            }
1013        };
1014
1015        if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
1016            error!(
1017                tx_id = %updated_tx.id,
1018                relayer_id = %updated_tx.relayer_id,
1019                status = ?TransactionStatus::Submitted,
1020                error = %e,
1021                "sending transaction update notification failed after submit",
1022            );
1023        }
1024
1025        Ok(updated_tx)
1026    }
1027
1028    /// Handles the status of a transaction.
1029    ///
1030    /// # Arguments
1031    ///
1032    /// * `tx` - The transaction model to handle.
1033    ///
1034    /// # Returns
1035    ///
1036    /// A result containing the updated transaction model or a `TransactionError`.
1037    async fn handle_transaction_status(
1038        &self,
1039        tx: TransactionRepoModel,
1040        context: Option<StatusCheckContext>,
1041    ) -> Result<TransactionRepoModel, TransactionError> {
1042        self.handle_status_impl(tx, context).await
1043    }
1044    /// Resubmits a transaction with updated parameters.
1045    ///
1046    /// # Arguments
1047    ///
1048    /// * `tx` - The transaction model to resubmit.
1049    ///
1050    /// # Returns
1051    ///
1052    /// A result containing the resubmitted transaction model or a `TransactionError`.
1053    async fn resubmit_transaction(
1054        &self,
1055        tx: TransactionRepoModel,
1056    ) -> Result<TransactionRepoModel, TransactionError> {
1057        debug!(
1058            tx_id = %tx.id,
1059            relayer_id = %tx.relayer_id,
1060            status = ?tx.status,
1061            "resubmitting transaction"
1062        );
1063
1064        // If transaction is not in correct status, return Ok to avoid wasteful retries
1065        if let Err(e) = ensure_status_one_of(
1066            &tx,
1067            &[TransactionStatus::Sent, TransactionStatus::Submitted],
1068            Some("resubmit_transaction"),
1069        ) {
1070            warn!(
1071                tx_id = %tx.id,
1072                status = ?tx.status,
1073                error = %e,
1074                "transaction not in expected status for resubmission, skipping"
1075            );
1076            return Ok(tx);
1077        }
1078
1079        let evm_data = tx.network_data.get_evm_transaction_data()?;
1080
1081        // Calculate bumped gas price
1082        // For noop transactions, force_bump=true to skip gas price cap and ensure bump succeeds
1083        let bumped_price_params = self
1084            .price_calculator
1085            .calculate_bumped_gas_price(&evm_data, self.relayer(), is_noop(&evm_data))
1086            .await?;
1087
1088        if !bumped_price_params.is_min_bumped.is_some_and(|b| b) {
1089            warn!(
1090                tx_id = %tx.id,
1091                relayer_id = %tx.relayer_id,
1092                price_params = ?bumped_price_params,
1093                "bumped gas price does not meet minimum requirement, skipping resubmission"
1094            );
1095            return Ok(tx);
1096        }
1097
1098        // Validate the relayer has sufficient balance
1099        self.ensure_sufficient_balance(bumped_price_params.total_cost)
1100            .await?;
1101
1102        // Create new transaction data with bumped gas price
1103        let updated_evm_data = evm_data.with_price_params(bumped_price_params.clone());
1104
1105        // Sign the transaction
1106        let sig_result = self
1107            .signer
1108            .sign_transaction(NetworkTransactionData::Evm(updated_evm_data.clone()))
1109            .await?;
1110
1111        let final_evm_data = updated_evm_data.with_signed_transaction_data(sig_result.into_evm()?);
1112
1113        let raw_tx = final_evm_data.raw.as_ref().ok_or_else(|| {
1114            TransactionError::InvalidType("Raw transaction data is missing".to_string())
1115        })?;
1116
1117        // Send resubmitted transaction to blockchain - this is the critical operation
1118        let was_already_submitted = match self.provider.send_raw_transaction(raw_tx).await {
1119            Ok(_) => {
1120                // Transaction resubmitted successfully with new pricing
1121                false
1122            }
1123            Err(e) => {
1124                let error_kind = Self::classify_submission_error(&e);
1125
1126                match &error_kind {
1127                    // AlreadyKnown / ReplacementUnderpriced: existing behavior — keep original hash
1128                    SubmissionErrorKind::AlreadyKnown
1129                    | SubmissionErrorKind::ReplacementUnderpriced => {
1130                        warn!(
1131                            tx_id = %tx.id,
1132                            error = %e,
1133                            error_kind = ?error_kind,
1134                            "resubmission indicates transaction already in mempool/mined - keeping original hash"
1135                        );
1136                        true
1137                    }
1138                    // NonceTooLow: nonce was consumed (possibly externally).
1139                    // Schedule nonce recovery and treat as already submitted — the
1140                    // status checker will determine the actual outcome.
1141                    SubmissionErrorKind::NonceTooLow => {
1142                        warn!(
1143                            tx_id = %tx.id,
1144                            error = %e,
1145                            "resubmission got nonce too low - scheduling nonce recovery"
1146                        );
1147                        if let Err(schedule_err) = self
1148                            .schedule_nonce_recovery_status_check(&tx, &error_kind)
1149                            .await
1150                        {
1151                            error!(
1152                                tx_id = %tx.id,
1153                                error = %schedule_err,
1154                                "failed to schedule nonce recovery status check during resubmission"
1155                            );
1156                        }
1157                        true
1158                    }
1159                    // NonceTooHigh: same pattern as submit_transaction — track retries, escalate
1160                    SubmissionErrorKind::NonceTooHigh => {
1161                        self.handle_nonce_too_high(&tx, "during resubmission").await;
1162                        // Return Ok — status checker handles resubmission
1163                        return Ok(tx);
1164                    }
1165                    // All other errors: propagate as before
1166                    _ => {
1167                        return Err(e.into());
1168                    }
1169                }
1170            }
1171        };
1172
1173        // Reset nonce_too_high_retries on success so subsequent resubmissions get a fresh budget.
1174        let metadata_reset = tx
1175            .metadata
1176            .as_ref()
1177            .and_then(|m| m.with_nonce_retries_reset());
1178
1179        // If transaction was already submitted, just update status without changing hash
1180        let update = if was_already_submitted {
1181            // Keep original hash and data - just ensure status is Submitted
1182            TransactionUpdateRequest {
1183                status: Some(TransactionStatus::Submitted),
1184                metadata: metadata_reset,
1185                ..Default::default()
1186            }
1187        } else {
1188            // Transaction resubmitted successfully - update with new hash and pricing
1189            let mut hashes = tx.hashes.clone();
1190            if let Some(hash) = final_evm_data.hash.clone() {
1191                hashes.push(hash);
1192            }
1193
1194            TransactionUpdateRequest {
1195                network_data: Some(NetworkTransactionData::Evm(final_evm_data)),
1196                hashes: Some(hashes),
1197                status: Some(TransactionStatus::Submitted),
1198                priced_at: Some(Utc::now().to_rfc3339()),
1199                sent_at: Some(Utc::now().to_rfc3339()),
1200                metadata: metadata_reset,
1201                ..Default::default()
1202            }
1203        };
1204
1205        let updated_tx = match self
1206            .transaction_repository
1207            .partial_update(tx.id.clone(), update)
1208            .await
1209        {
1210            Ok(tx) => tx,
1211            Err(e) => {
1212                error!(
1213                    error = %e,
1214                    tx_id = %tx.id,
1215                    "CRITICAL: resubmitted transaction sent to blockchain but failed to update database"
1216                );
1217                // Transaction is on-chain - return original tx data to avoid wasteful retries
1218                tx
1219            }
1220        };
1221
1222        Ok(updated_tx)
1223    }
1224
1225    /// Cancels a transaction.
1226    ///
1227    /// # Arguments
1228    ///
1229    /// * `tx` - The transaction model to cancel.
1230    ///
1231    /// # Returns
1232    ///
1233    /// A result containing the transaction model or a `TransactionError`.
1234    async fn cancel_transaction(
1235        &self,
1236        tx: TransactionRepoModel,
1237    ) -> Result<TransactionRepoModel, TransactionError> {
1238        info!(tx_id = %tx.id, status = ?tx.status, "cancelling transaction");
1239
1240        // Validate state: can only cancel transactions that are still pending
1241        ensure_status_one_of(
1242            &tx,
1243            &[
1244                TransactionStatus::Pending,
1245                TransactionStatus::Sent,
1246                TransactionStatus::Submitted,
1247            ],
1248            Some("cancel_transaction"),
1249        )?;
1250
1251        // If the transaction is in Pending state, we can just update its status
1252        if tx.status == TransactionStatus::Pending {
1253            debug!("transaction is in pending state, updating status to canceled");
1254            return self
1255                .update_transaction_status(
1256                    tx,
1257                    TransactionStatus::Canceled,
1258                    Some("Transaction canceled by user".to_string()),
1259                )
1260                .await;
1261        }
1262
1263        let update = self
1264            .prepare_noop_update_request(
1265                &tx,
1266                true,
1267                Some("Transaction canceled by user, replacing with NOOP".to_string()),
1268            )
1269            .await?;
1270        let updated_tx = self
1271            .transaction_repository()
1272            .partial_update(tx.id.clone(), update)
1273            .await?;
1274
1275        // Submit the updated transaction to the network using the resubmit job
1276        self.send_transaction_resubmit_job(&updated_tx).await?;
1277
1278        // Send notification for the updated transaction
1279        if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
1280            error!(
1281                tx_id = %updated_tx.id,
1282                status = ?updated_tx.status,
1283                "sending transaction update notification failed after cancel: {:?}",
1284                e
1285            );
1286        }
1287
1288        debug!("original transaction updated with cancellation data");
1289        Ok(updated_tx)
1290    }
1291
1292    /// Replaces a transaction with a new one.
1293    ///
1294    /// # Arguments
1295    ///
1296    /// * `old_tx` - The transaction model to replace.
1297    /// * `new_tx_request` - The new transaction request data.
1298    ///
1299    /// # Returns
1300    ///
1301    /// A result containing the updated transaction model or a `TransactionError`.
1302    async fn replace_transaction(
1303        &self,
1304        old_tx: TransactionRepoModel,
1305        new_tx_request: NetworkTransactionRequest,
1306    ) -> Result<TransactionRepoModel, TransactionError> {
1307        debug!("replacing transaction");
1308
1309        // Validate state: can only replace transactions that are still pending
1310        ensure_status_one_of(
1311            &old_tx,
1312            &[
1313                TransactionStatus::Pending,
1314                TransactionStatus::Sent,
1315                TransactionStatus::Submitted,
1316            ],
1317            Some("replace_transaction"),
1318        )?;
1319
1320        // Extract EVM data from both old transaction and new request
1321        let old_evm_data = old_tx.network_data.get_evm_transaction_data()?;
1322        let new_evm_request = match new_tx_request {
1323            NetworkTransactionRequest::Evm(evm_req) => evm_req,
1324            _ => {
1325                return Err(TransactionError::InvalidType(
1326                    "New transaction request must be EVM type".to_string(),
1327                ))
1328            }
1329        };
1330
1331        let network_repo_model = self
1332            .network_repository()
1333            .get_by_chain_id(NetworkType::Evm, old_evm_data.chain_id)
1334            .await
1335            .map_err(|e| {
1336                TransactionError::NetworkConfiguration(format!(
1337                    "Failed to get network by chain_id {}: {}",
1338                    old_evm_data.chain_id, e
1339                ))
1340            })?
1341            .ok_or_else(|| {
1342                TransactionError::NetworkConfiguration(format!(
1343                    "Network with chain_id {} not found",
1344                    old_evm_data.chain_id
1345                ))
1346            })?;
1347
1348        let network = EvmNetwork::try_from(network_repo_model).map_err(|e| {
1349            TransactionError::NetworkConfiguration(format!("Failed to convert network model: {e}"))
1350        })?;
1351
1352        // First, create updated EVM data without price parameters
1353        let updated_evm_data = EvmTransactionData::for_replacement(&old_evm_data, &new_evm_request);
1354
1355        // Then determine pricing strategy and calculate price parameters using the updated data
1356        let price_params = super::replacement::determine_replacement_pricing(
1357            &old_evm_data,
1358            &updated_evm_data,
1359            self.relayer(),
1360            &self.price_calculator,
1361            network.lacks_mempool(),
1362        )
1363        .await?;
1364
1365        debug!(price_params = ?price_params, "replacement price params");
1366
1367        // Apply the calculated price parameters to the updated EVM data
1368        let evm_data_with_price_params = updated_evm_data.with_price_params(price_params.clone());
1369
1370        // Validate the relayer has sufficient balance
1371        self.ensure_sufficient_balance(price_params.total_cost)
1372            .await?;
1373
1374        let sig_result = self
1375            .signer
1376            .sign_transaction(NetworkTransactionData::Evm(
1377                evm_data_with_price_params.clone(),
1378            ))
1379            .await?;
1380
1381        let final_evm_data =
1382            evm_data_with_price_params.with_signed_transaction_data(sig_result.into_evm()?);
1383
1384        // Update the transaction in the repository
1385        let updated_tx = self
1386            .transaction_repository
1387            .update_network_data(
1388                old_tx.id.clone(),
1389                NetworkTransactionData::Evm(final_evm_data),
1390            )
1391            .await?;
1392
1393        self.send_transaction_resubmit_job(&updated_tx).await?;
1394
1395        // Send notification
1396        if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
1397            error!(
1398                tx_id = %updated_tx.id,
1399                status = ?updated_tx.status,
1400                "sending transaction update notification failed after replace: {:?}",
1401                e
1402            );
1403        }
1404
1405        Ok(updated_tx)
1406    }
1407
1408    /// Signs a transaction.
1409    ///
1410    /// # Arguments
1411    ///
1412    /// * `tx` - The transaction model to sign.
1413    ///
1414    /// # Returns
1415    ///
1416    /// A result containing the transaction model or a `TransactionError`.
1417    async fn sign_transaction(
1418        &self,
1419        tx: TransactionRepoModel,
1420    ) -> Result<TransactionRepoModel, TransactionError> {
1421        Ok(tx)
1422    }
1423
1424    /// Validates a transaction.
1425    ///
1426    /// # Arguments
1427    ///
1428    /// * `_tx` - The transaction model to validate.
1429    ///
1430    /// # Returns
1431    ///
1432    /// A result containing a boolean indicating validity or a `TransactionError`.
1433    async fn validate_transaction(
1434        &self,
1435        _tx: TransactionRepoModel,
1436    ) -> Result<bool, TransactionError> {
1437        Ok(true)
1438    }
1439}
1440// P: EvmProviderTrait,
1441// R: Repository<RelayerRepoModel, String>,
1442// T: TransactionRepository,
1443// J: JobProducerTrait,
1444// S: Signer,
1445// C: TransactionCounterTrait,
1446// PC: PriceCalculatorTrait,
1447// we define concrete type for the evm transaction
1448pub type DefaultEvmTransaction = EvmRelayerTransaction<
1449    EvmProvider,
1450    RelayerRepositoryStorage,
1451    NetworkRepositoryStorage,
1452    TransactionRepositoryStorage,
1453    JobProducer,
1454    EvmSigner,
1455    TransactionCounterRepositoryStorage,
1456    PriceCalculator<EvmGasPriceService<EvmProvider>>,
1457>;
1458#[cfg(test)]
1459mod tests {
1460
1461    use super::*;
1462    use crate::{
1463        domain::evm::price_calculator::PriceParams,
1464        jobs::MockJobProducerTrait,
1465        models::{
1466            evm::Speed, EvmTransactionData, EvmTransactionRequest, NetworkType,
1467            RelayerNetworkPolicy, U256,
1468        },
1469        repositories::{
1470            MockNetworkRepository, MockRelayerRepository, MockTransactionCounterTrait,
1471            MockTransactionRepository,
1472        },
1473        services::{provider::MockEvmProviderTrait, signer::MockSigner},
1474    };
1475    use chrono::Utc;
1476    use futures::future::ready;
1477    use mockall::{mock, predicate::*};
1478
1479    // Create a mock for PriceCalculatorTrait
1480    mock! {
1481        pub PriceCalculator {}
1482        #[async_trait]
1483        impl PriceCalculatorTrait for PriceCalculator {
1484            async fn get_transaction_price_params(
1485                &self,
1486                tx_data: &EvmTransactionData,
1487                relayer: &RelayerRepoModel
1488            ) -> Result<PriceParams, TransactionError>;
1489
1490            async fn calculate_bumped_gas_price(
1491                &self,
1492                tx: &EvmTransactionData,
1493                relayer: &RelayerRepoModel,
1494                force_bump: bool,
1495            ) -> Result<PriceParams, TransactionError>;
1496        }
1497    }
1498
1499    // Helper to create a relayer model with specific configuration for these tests
1500    fn create_test_relayer() -> RelayerRepoModel {
1501        create_test_relayer_with_policy(crate::models::RelayerEvmPolicy {
1502            include_revert_data: None,
1503            min_balance: Some(100000000000000000u128), // 0.1 ETH
1504            gas_limit_estimation: Some(true),
1505            gas_price_cap: Some(100000000000), // 100 Gwei
1506            whitelist_receivers: Some(vec!["0xRecipient".to_string()]),
1507            eip1559_pricing: Some(false),
1508            private_transactions: Some(false),
1509        })
1510    }
1511
1512    fn create_test_relayer_with_policy(evm_policy: RelayerEvmPolicy) -> RelayerRepoModel {
1513        RelayerRepoModel {
1514            id: "test-relayer-id".to_string(),
1515            name: "Test Relayer".to_string(),
1516            network: "1".to_string(), // Ethereum Mainnet
1517            address: "0xSender".to_string(),
1518            paused: false,
1519            system_disabled: false,
1520            signer_id: "test-signer-id".to_string(),
1521            notification_id: Some("test-notification-id".to_string()),
1522            policies: RelayerNetworkPolicy::Evm(evm_policy),
1523            network_type: NetworkType::Evm,
1524            custom_rpc_urls: None,
1525            ..Default::default()
1526        }
1527    }
1528
1529    // Helper to create test transaction with specific configuration for these tests
1530    fn create_test_transaction() -> TransactionRepoModel {
1531        TransactionRepoModel {
1532            id: "test-tx-id".to_string(),
1533            relayer_id: "test-relayer-id".to_string(),
1534            status: TransactionStatus::Pending,
1535            status_reason: None,
1536            created_at: Utc::now().to_rfc3339(),
1537            sent_at: None,
1538            confirmed_at: None,
1539            valid_until: None,
1540            delete_at: None,
1541            network_type: NetworkType::Evm,
1542            network_data: NetworkTransactionData::Evm(EvmTransactionData {
1543                chain_id: 1,
1544                from: "0xSender".to_string(),
1545                to: Some("0xRecipient".to_string()),
1546                value: U256::from(1000000000000000000u64), // 1 ETH
1547                data: Some("0xData".to_string()),
1548                gas_limit: Some(21000),
1549                gas_price: Some(20000000000), // 20 Gwei
1550                max_fee_per_gas: None,
1551                max_priority_fee_per_gas: None,
1552                nonce: None,
1553                signature: None,
1554                hash: None,
1555                speed: Some(Speed::Fast),
1556                raw: None,
1557            }),
1558            priced_at: None,
1559            hashes: Vec::new(),
1560            noop_count: None,
1561            is_canceled: Some(false),
1562            metadata: None,
1563        }
1564    }
1565
1566    #[tokio::test]
1567    async fn test_prepare_transaction_with_sufficient_balance() {
1568        let mut mock_transaction = MockTransactionRepository::new();
1569        let mock_relayer = MockRelayerRepository::new();
1570        let mut mock_provider = MockEvmProviderTrait::new();
1571        let mut mock_signer = MockSigner::new();
1572        let mut mock_job_producer = MockJobProducerTrait::new();
1573        let mut mock_price_calculator = MockPriceCalculator::new();
1574        let mut counter_service = MockTransactionCounterTrait::new();
1575
1576        let relayer = create_test_relayer();
1577        let test_tx = create_test_transaction();
1578
1579        counter_service
1580            .expect_get_and_increment()
1581            .returning(|_, _| Box::pin(ready(Ok(42))));
1582
1583        let price_params = PriceParams {
1584            gas_price: Some(30000000000),
1585            max_fee_per_gas: None,
1586            max_priority_fee_per_gas: None,
1587            is_min_bumped: None,
1588            extra_fee: None,
1589            total_cost: U256::from(630000000000000u64),
1590        };
1591        mock_price_calculator
1592            .expect_get_transaction_price_params()
1593            .returning(move |_, _| Ok(price_params.clone()));
1594
1595        mock_signer.expect_sign_transaction().returning(|_| {
1596            Box::pin(ready(Ok(
1597                crate::domain::relayer::SignTransactionResponse::Evm(
1598                    crate::domain::relayer::SignTransactionResponseEvm {
1599                        hash: "0xtx_hash".to_string(),
1600                        signature: crate::models::EvmTransactionDataSignature {
1601                            r: "r".to_string(),
1602                            s: "s".to_string(),
1603                            v: 1,
1604                            sig: "0xsignature".to_string(),
1605                        },
1606                        raw: vec![1, 2, 3],
1607                    },
1608                ),
1609            )))
1610        });
1611
1612        mock_provider
1613            .expect_get_balance()
1614            .with(eq("0xSender"))
1615            .returning(|_| Box::pin(ready(Ok(U256::from(1000000000000000000u64)))));
1616
1617        // Mock get_block_by_number for gas limit validation (tx has gas_limit: Some(21000))
1618        mock_provider
1619            .expect_get_block_by_number()
1620            .times(1)
1621            .returning(|| {
1622                Box::pin(async {
1623                    use alloy::{network::AnyRpcBlock, rpc::types::Block};
1624                    let mut block: Block = Block::default();
1625                    // Set block gas limit to 30M (higher than tx gas limit of 21_000)
1626                    block.header.gas_limit = 30_000_000u64;
1627                    Ok(AnyRpcBlock::from(block))
1628                })
1629            });
1630
1631        let test_tx_clone = test_tx.clone();
1632        mock_transaction
1633            .expect_partial_update()
1634            .returning(move |_, update| {
1635                let mut updated_tx = test_tx_clone.clone();
1636                if let Some(status) = &update.status {
1637                    updated_tx.status = status.clone();
1638                }
1639                if let Some(network_data) = &update.network_data {
1640                    updated_tx.network_data = network_data.clone();
1641                }
1642                if let Some(hashes) = &update.hashes {
1643                    updated_tx.hashes = hashes.clone();
1644                }
1645                Ok(updated_tx)
1646            });
1647
1648        mock_job_producer
1649            .expect_produce_submit_transaction_job()
1650            .returning(|_, _| Box::pin(ready(Ok(()))));
1651        mock_job_producer
1652            .expect_produce_send_notification_job()
1653            .returning(|_, _| Box::pin(ready(Ok(()))));
1654
1655        let mock_network = MockNetworkRepository::new();
1656
1657        let evm_transaction = EvmRelayerTransaction {
1658            relayer: relayer.clone(),
1659            provider: mock_provider,
1660            relayer_repository: Arc::new(mock_relayer),
1661            network_repository: Arc::new(mock_network),
1662            transaction_repository: Arc::new(mock_transaction),
1663            transaction_counter_service: Arc::new(counter_service),
1664            job_producer: Arc::new(mock_job_producer),
1665            price_calculator: mock_price_calculator,
1666            signer: mock_signer,
1667        };
1668
1669        let result = evm_transaction.prepare_transaction(test_tx.clone()).await;
1670        assert!(result.is_ok());
1671        let prepared_tx = result.unwrap();
1672        assert_eq!(prepared_tx.status, TransactionStatus::Sent);
1673        assert!(!prepared_tx.hashes.is_empty());
1674    }
1675
1676    #[tokio::test]
1677    async fn test_prepare_transaction_with_insufficient_balance() {
1678        let mut mock_transaction = MockTransactionRepository::new();
1679        let mock_relayer = MockRelayerRepository::new();
1680        let mut mock_provider = MockEvmProviderTrait::new();
1681        let mut mock_signer = MockSigner::new();
1682        let mut mock_job_producer = MockJobProducerTrait::new();
1683        let mut mock_price_calculator = MockPriceCalculator::new();
1684        let mut counter_service = MockTransactionCounterTrait::new();
1685
1686        let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
1687            gas_limit_estimation: Some(false),
1688            min_balance: Some(100000000000000000u128),
1689            ..Default::default()
1690        });
1691        let test_tx = create_test_transaction();
1692
1693        counter_service
1694            .expect_get_and_increment()
1695            .returning(|_, _| Box::pin(ready(Ok(42))));
1696
1697        let price_params = PriceParams {
1698            gas_price: Some(30000000000),
1699            max_fee_per_gas: None,
1700            max_priority_fee_per_gas: None,
1701            is_min_bumped: None,
1702            extra_fee: None,
1703            total_cost: U256::from(630000000000000u64),
1704        };
1705        mock_price_calculator
1706            .expect_get_transaction_price_params()
1707            .returning(move |_, _| Ok(price_params.clone()));
1708
1709        mock_signer.expect_sign_transaction().returning(|_| {
1710            Box::pin(ready(Ok(
1711                crate::domain::relayer::SignTransactionResponse::Evm(
1712                    crate::domain::relayer::SignTransactionResponseEvm {
1713                        hash: "0xtx_hash".to_string(),
1714                        signature: crate::models::EvmTransactionDataSignature {
1715                            r: "r".to_string(),
1716                            s: "s".to_string(),
1717                            v: 1,
1718                            sig: "0xsignature".to_string(),
1719                        },
1720                        raw: vec![1, 2, 3],
1721                    },
1722                ),
1723            )))
1724        });
1725
1726        mock_provider
1727            .expect_get_balance()
1728            .with(eq("0xSender"))
1729            .returning(|_| Box::pin(ready(Ok(U256::from(90000000000000000u64)))));
1730
1731        // Mock get_block_by_number for gas limit validation (tx has gas_limit: Some(21000))
1732        mock_provider
1733            .expect_get_block_by_number()
1734            .times(1)
1735            .returning(|| {
1736                Box::pin(async {
1737                    use alloy::{network::AnyRpcBlock, rpc::types::Block};
1738                    let mut block: Block = Block::default();
1739                    // Set block gas limit to 30M (higher than tx gas limit of 21_000)
1740                    block.header.gas_limit = 30_000_000u64;
1741                    Ok(AnyRpcBlock::from(block))
1742                })
1743            });
1744
1745        let test_tx_clone = test_tx.clone();
1746        mock_transaction
1747            .expect_partial_update()
1748            .withf(move |id, update| {
1749                id == "test-tx-id" && update.status == Some(TransactionStatus::Failed)
1750            })
1751            .returning(move |_, update| {
1752                let mut updated_tx = test_tx_clone.clone();
1753                updated_tx.status = update.status.unwrap_or(updated_tx.status);
1754                updated_tx.status_reason = update.status_reason.clone();
1755                Ok(updated_tx)
1756            });
1757
1758        mock_job_producer
1759            .expect_produce_send_notification_job()
1760            .returning(|_, _| Box::pin(ready(Ok(()))));
1761
1762        let mock_network = MockNetworkRepository::new();
1763
1764        let evm_transaction = EvmRelayerTransaction {
1765            relayer: relayer.clone(),
1766            provider: mock_provider,
1767            relayer_repository: Arc::new(mock_relayer),
1768            network_repository: Arc::new(mock_network),
1769            transaction_repository: Arc::new(mock_transaction),
1770            transaction_counter_service: Arc::new(counter_service),
1771            job_producer: Arc::new(mock_job_producer),
1772            price_calculator: mock_price_calculator,
1773            signer: mock_signer,
1774        };
1775
1776        let result = evm_transaction.prepare_transaction(test_tx.clone()).await;
1777        assert!(result.is_ok(), "Expected Ok, got: {result:?}");
1778
1779        let updated_tx = result.unwrap();
1780        assert_eq!(
1781            updated_tx.status,
1782            TransactionStatus::Failed,
1783            "Transaction should be marked as Failed"
1784        );
1785        assert!(
1786            updated_tx.status_reason.is_some(),
1787            "Status reason should be set"
1788        );
1789        assert!(
1790            updated_tx
1791                .status_reason
1792                .as_ref()
1793                .unwrap()
1794                .to_lowercase()
1795                .contains("insufficient balance"),
1796            "Status reason should contain insufficient balance error, got: {:?}",
1797            updated_tx.status_reason
1798        );
1799    }
1800
1801    #[tokio::test]
1802    async fn test_prepare_transaction_with_gas_limit_exceeding_block_limit() {
1803        let mut mock_transaction = MockTransactionRepository::new();
1804        let mock_relayer = MockRelayerRepository::new();
1805        let mut mock_provider = MockEvmProviderTrait::new();
1806        let mock_signer = MockSigner::new();
1807        let mut mock_job_producer = MockJobProducerTrait::new();
1808        let mock_price_calculator = MockPriceCalculator::new();
1809        let mut counter_service = MockTransactionCounterTrait::new();
1810
1811        let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
1812            gas_limit_estimation: Some(false), // User provides gas limit
1813            min_balance: Some(100000000000000000u128),
1814            ..Default::default()
1815        });
1816
1817        // Create a transaction with a gas limit that exceeds block gas limit
1818        let mut test_tx = create_test_transaction();
1819        if let NetworkTransactionData::Evm(ref mut evm_data) = test_tx.network_data {
1820            evm_data.gas_limit = Some(30_000_001); // Exceeds typical block gas limit of 30M
1821        }
1822
1823        counter_service
1824            .expect_get_and_increment()
1825            .returning(|_, _| Box::pin(ready(Ok(42))));
1826
1827        // Mock get_block_by_number to return a block with gas_limit lower than tx gas_limit
1828        mock_provider
1829            .expect_get_block_by_number()
1830            .times(1)
1831            .returning(|| {
1832                Box::pin(async {
1833                    use alloy::{network::AnyRpcBlock, rpc::types::Block};
1834                    let mut block: Block = Block::default();
1835                    // Set block gas limit to 30M (lower than tx gas limit of 30_000_001)
1836                    block.header.gas_limit = 30_000_000u64;
1837                    Ok(AnyRpcBlock::from(block))
1838                })
1839            });
1840
1841        // Mock partial_update to be called when marking transaction as failed
1842        let test_tx_clone = test_tx.clone();
1843        mock_transaction
1844            .expect_partial_update()
1845            .withf(move |id, update| {
1846                id == "test-tx-id"
1847                    && update.status == Some(TransactionStatus::Failed)
1848                    && update.status_reason.is_some()
1849                    && update
1850                        .status_reason
1851                        .as_ref()
1852                        .unwrap()
1853                        .contains("exceeds block gas limit")
1854            })
1855            .returning(move |_, update| {
1856                let mut updated_tx = test_tx_clone.clone();
1857                updated_tx.status = update.status.unwrap_or(updated_tx.status);
1858                updated_tx.status_reason = update.status_reason.clone();
1859                Ok(updated_tx)
1860            });
1861
1862        mock_job_producer
1863            .expect_produce_send_notification_job()
1864            .returning(|_, _| Box::pin(ready(Ok(()))));
1865
1866        let mock_network = MockNetworkRepository::new();
1867
1868        let evm_transaction = EvmRelayerTransaction {
1869            relayer: relayer.clone(),
1870            provider: mock_provider,
1871            relayer_repository: Arc::new(mock_relayer),
1872            network_repository: Arc::new(mock_network),
1873            transaction_repository: Arc::new(mock_transaction),
1874            transaction_counter_service: Arc::new(counter_service),
1875            job_producer: Arc::new(mock_job_producer),
1876            price_calculator: mock_price_calculator,
1877            signer: mock_signer,
1878        };
1879
1880        let result = evm_transaction.prepare_transaction(test_tx.clone()).await;
1881        assert!(result.is_ok(), "Expected Ok, got: {result:?}");
1882
1883        let updated_tx = result.unwrap();
1884        assert_eq!(
1885            updated_tx.status,
1886            TransactionStatus::Failed,
1887            "Transaction should be marked as Failed"
1888        );
1889        assert!(
1890            updated_tx.status_reason.is_some(),
1891            "Status reason should be set"
1892        );
1893        assert!(
1894            updated_tx
1895                .status_reason
1896                .as_ref()
1897                .unwrap()
1898                .contains("exceeds block gas limit"),
1899            "Status reason should mention gas limit exceeds block gas limit, got: {:?}",
1900            updated_tx.status_reason
1901        );
1902        assert!(
1903            updated_tx
1904                .status_reason
1905                .as_ref()
1906                .unwrap()
1907                .contains("30000001"),
1908            "Status reason should contain transaction gas limit, got: {:?}",
1909            updated_tx.status_reason
1910        );
1911        assert!(
1912            updated_tx
1913                .status_reason
1914                .as_ref()
1915                .unwrap()
1916                .contains("30000000"),
1917            "Status reason should contain block gas limit, got: {:?}",
1918            updated_tx.status_reason
1919        );
1920    }
1921
1922    #[tokio::test]
1923    async fn test_prepare_transaction_with_gas_limit_within_block_limit() {
1924        let mut mock_transaction = MockTransactionRepository::new();
1925        let mock_relayer = MockRelayerRepository::new();
1926        let mut mock_provider = MockEvmProviderTrait::new();
1927        let mut mock_signer = MockSigner::new();
1928        let mut mock_job_producer = MockJobProducerTrait::new();
1929        let mut mock_price_calculator = MockPriceCalculator::new();
1930        let mut counter_service = MockTransactionCounterTrait::new();
1931
1932        let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
1933            gas_limit_estimation: Some(false), // User provides gas limit
1934            min_balance: Some(100000000000000000u128),
1935            ..Default::default()
1936        });
1937
1938        // Create a transaction with a gas limit within block gas limit
1939        let mut test_tx = create_test_transaction();
1940        if let NetworkTransactionData::Evm(ref mut evm_data) = test_tx.network_data {
1941            evm_data.gas_limit = Some(21_000); // Within typical block gas limit of 30M
1942        }
1943
1944        counter_service
1945            .expect_get_and_increment()
1946            .returning(|_, _| Box::pin(ready(Ok(42))));
1947
1948        let price_params = PriceParams {
1949            gas_price: Some(30000000000),
1950            max_fee_per_gas: None,
1951            max_priority_fee_per_gas: None,
1952            is_min_bumped: None,
1953            extra_fee: None,
1954            total_cost: U256::from(630000000000000u64),
1955        };
1956        mock_price_calculator
1957            .expect_get_transaction_price_params()
1958            .returning(move |_, _| Ok(price_params.clone()));
1959
1960        mock_signer.expect_sign_transaction().returning(|_| {
1961            Box::pin(ready(Ok(
1962                crate::domain::relayer::SignTransactionResponse::Evm(
1963                    crate::domain::relayer::SignTransactionResponseEvm {
1964                        hash: "0xtx_hash".to_string(),
1965                        signature: crate::models::EvmTransactionDataSignature {
1966                            r: "r".to_string(),
1967                            s: "s".to_string(),
1968                            v: 1,
1969                            sig: "0xsignature".to_string(),
1970                        },
1971                        raw: vec![1, 2, 3],
1972                    },
1973                ),
1974            )))
1975        });
1976
1977        mock_provider
1978            .expect_get_balance()
1979            .with(eq("0xSender"))
1980            .returning(|_| Box::pin(ready(Ok(U256::from(1000000000000000000u64)))));
1981
1982        // Mock get_block_by_number to return a block with gas_limit higher than tx gas_limit
1983        mock_provider
1984            .expect_get_block_by_number()
1985            .times(1)
1986            .returning(|| {
1987                Box::pin(async {
1988                    use alloy::{network::AnyRpcBlock, rpc::types::Block};
1989                    let mut block: Block = Block::default();
1990                    // Set block gas limit to 30M (higher than tx gas limit of 21_000)
1991                    block.header.gas_limit = 30_000_000u64;
1992                    Ok(AnyRpcBlock::from(block))
1993                })
1994            });
1995
1996        let test_tx_clone = test_tx.clone();
1997        mock_transaction
1998            .expect_partial_update()
1999            .returning(move |_, update| {
2000                let mut updated_tx = test_tx_clone.clone();
2001                if let Some(status) = &update.status {
2002                    updated_tx.status = status.clone();
2003                }
2004                if let Some(network_data) = &update.network_data {
2005                    updated_tx.network_data = network_data.clone();
2006                }
2007                if let Some(hashes) = &update.hashes {
2008                    updated_tx.hashes = hashes.clone();
2009                }
2010                Ok(updated_tx)
2011            });
2012
2013        mock_job_producer
2014            .expect_produce_submit_transaction_job()
2015            .returning(|_, _| Box::pin(ready(Ok(()))));
2016        mock_job_producer
2017            .expect_produce_send_notification_job()
2018            .returning(|_, _| Box::pin(ready(Ok(()))));
2019
2020        let mock_network = MockNetworkRepository::new();
2021
2022        let evm_transaction = EvmRelayerTransaction {
2023            relayer: relayer.clone(),
2024            provider: mock_provider,
2025            relayer_repository: Arc::new(mock_relayer),
2026            network_repository: Arc::new(mock_network),
2027            transaction_repository: Arc::new(mock_transaction),
2028            transaction_counter_service: Arc::new(counter_service),
2029            job_producer: Arc::new(mock_job_producer),
2030            price_calculator: mock_price_calculator,
2031            signer: mock_signer,
2032        };
2033
2034        let result = evm_transaction.prepare_transaction(test_tx.clone()).await;
2035        assert!(result.is_ok(), "Expected Ok, got: {result:?}");
2036
2037        let prepared_tx = result.unwrap();
2038        // Transaction should proceed normally (not be marked as Failed)
2039        assert_eq!(prepared_tx.status, TransactionStatus::Sent);
2040        assert!(!prepared_tx.hashes.is_empty());
2041    }
2042
2043    #[tokio::test]
2044    async fn test_cancel_transaction() {
2045        // Test Case 1: Canceling a pending transaction
2046        {
2047            // Create mocks for all dependencies
2048            let mut mock_transaction = MockTransactionRepository::new();
2049            let mock_relayer = MockRelayerRepository::new();
2050            let mock_provider = MockEvmProviderTrait::new();
2051            let mock_signer = MockSigner::new();
2052            let mut mock_job_producer = MockJobProducerTrait::new();
2053            let mock_price_calculator = MockPriceCalculator::new();
2054            let counter_service = MockTransactionCounterTrait::new();
2055
2056            // Create test relayer and pending transaction
2057            let relayer = create_test_relayer();
2058            let mut test_tx = create_test_transaction();
2059            test_tx.status = TransactionStatus::Pending;
2060
2061            // Transaction repository should update the transaction with Canceled status
2062            let test_tx_clone = test_tx.clone();
2063            mock_transaction
2064                .expect_partial_update()
2065                .withf(move |id, update| {
2066                    id == "test-tx-id" && update.status == Some(TransactionStatus::Canceled)
2067                })
2068                .returning(move |_, update| {
2069                    let mut updated_tx = test_tx_clone.clone();
2070                    updated_tx.status = update.status.unwrap_or(updated_tx.status);
2071                    Ok(updated_tx)
2072                });
2073
2074            // Job producer should send notification
2075            mock_job_producer
2076                .expect_produce_send_notification_job()
2077                .returning(|_, _| Box::pin(ready(Ok(()))));
2078
2079            let mock_network = MockNetworkRepository::new();
2080
2081            // Set up EVM transaction with the mocks
2082            let evm_transaction = EvmRelayerTransaction {
2083                relayer: relayer.clone(),
2084                provider: mock_provider,
2085                relayer_repository: Arc::new(mock_relayer),
2086                network_repository: Arc::new(mock_network),
2087                transaction_repository: Arc::new(mock_transaction),
2088                transaction_counter_service: Arc::new(counter_service),
2089                job_producer: Arc::new(mock_job_producer),
2090                price_calculator: mock_price_calculator,
2091                signer: mock_signer,
2092            };
2093
2094            // Call cancel_transaction and verify it succeeds
2095            let result = evm_transaction.cancel_transaction(test_tx.clone()).await;
2096            assert!(result.is_ok());
2097            let cancelled_tx = result.unwrap();
2098            assert_eq!(cancelled_tx.id, "test-tx-id");
2099            assert_eq!(cancelled_tx.status, TransactionStatus::Canceled);
2100        }
2101
2102        // Test Case 2: Canceling a submitted transaction
2103        {
2104            // Create mocks for all dependencies
2105            let mut mock_transaction = MockTransactionRepository::new();
2106            let mock_relayer = MockRelayerRepository::new();
2107            let mock_provider = MockEvmProviderTrait::new();
2108            let mut mock_signer = MockSigner::new();
2109            let mut mock_job_producer = MockJobProducerTrait::new();
2110            let mut mock_price_calculator = MockPriceCalculator::new();
2111            let counter_service = MockTransactionCounterTrait::new();
2112
2113            // Create test relayer and submitted transaction
2114            let relayer = create_test_relayer();
2115            let mut test_tx = create_test_transaction();
2116            test_tx.status = TransactionStatus::Submitted;
2117            test_tx.sent_at = Some(Utc::now().to_rfc3339());
2118            test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
2119                nonce: Some(42),
2120                hash: Some("0xoriginal_hash".to_string()),
2121                ..test_tx.network_data.get_evm_transaction_data().unwrap()
2122            });
2123
2124            // Set up price calculator expectations for cancellation tx
2125            mock_price_calculator
2126                .expect_get_transaction_price_params()
2127                .return_once(move |_, _| {
2128                    Ok(PriceParams {
2129                        gas_price: Some(40000000000), // 40 Gwei (higher than original)
2130                        max_fee_per_gas: None,
2131                        max_priority_fee_per_gas: None,
2132                        is_min_bumped: Some(true),
2133                        extra_fee: Some(U256::ZERO),
2134                        total_cost: U256::ZERO,
2135                    })
2136                });
2137
2138            // Signer should be called to sign the cancellation transaction
2139            mock_signer.expect_sign_transaction().returning(|_| {
2140                Box::pin(ready(Ok(
2141                    crate::domain::relayer::SignTransactionResponse::Evm(
2142                        crate::domain::relayer::SignTransactionResponseEvm {
2143                            hash: "0xcancellation_hash".to_string(),
2144                            signature: crate::models::EvmTransactionDataSignature {
2145                                r: "r".to_string(),
2146                                s: "s".to_string(),
2147                                v: 1,
2148                                sig: "0xsignature".to_string(),
2149                            },
2150                            raw: vec![1, 2, 3],
2151                        },
2152                    ),
2153                )))
2154            });
2155
2156            // Transaction repository should update the transaction
2157            let test_tx_clone = test_tx.clone();
2158            mock_transaction
2159                .expect_partial_update()
2160                .returning(move |tx_id, update| {
2161                    let mut updated_tx = test_tx_clone.clone();
2162                    updated_tx.id = tx_id;
2163                    updated_tx.status = update.status.unwrap_or(updated_tx.status);
2164                    updated_tx.network_data =
2165                        update.network_data.unwrap_or(updated_tx.network_data);
2166                    if let Some(hashes) = update.hashes {
2167                        updated_tx.hashes = hashes;
2168                    }
2169                    Ok(updated_tx)
2170                });
2171
2172            // Job producer expectations
2173            mock_job_producer
2174                .expect_produce_submit_transaction_job()
2175                .returning(|_, _| Box::pin(ready(Ok(()))));
2176            mock_job_producer
2177                .expect_produce_send_notification_job()
2178                .returning(|_, _| Box::pin(ready(Ok(()))));
2179
2180            // Network repository expectations for cancellation NOOP transaction
2181            let mut mock_network = MockNetworkRepository::new();
2182            mock_network
2183                .expect_get_by_chain_id()
2184                .with(eq(NetworkType::Evm), eq(1))
2185                .returning(|_, _| {
2186                    use crate::config::{EvmNetworkConfig, NetworkConfigCommon};
2187                    use crate::models::{NetworkConfigData, NetworkRepoModel, RpcConfig};
2188
2189                    let config = EvmNetworkConfig {
2190                        common: NetworkConfigCommon {
2191                            network: "mainnet".to_string(),
2192                            from: None,
2193                            rpc_urls: Some(vec![RpcConfig::new(
2194                                "https://rpc.example.com".to_string(),
2195                            )]),
2196                            explorer_urls: None,
2197                            average_blocktime_ms: Some(12000),
2198                            is_testnet: Some(false),
2199                            tags: Some(vec!["mainnet".to_string()]),
2200                        },
2201                        chain_id: Some(1),
2202                        required_confirmations: Some(12),
2203                        features: Some(vec!["eip1559".to_string()]),
2204                        symbol: Some("ETH".to_string()),
2205                        gas_price_cache: None,
2206                    };
2207                    Ok(Some(NetworkRepoModel {
2208                        id: "evm:mainnet".to_string(),
2209                        name: "mainnet".to_string(),
2210                        network_type: NetworkType::Evm,
2211                        config: NetworkConfigData::Evm(config),
2212                    }))
2213                });
2214
2215            // Set up EVM transaction with the mocks
2216            let evm_transaction = EvmRelayerTransaction {
2217                relayer: relayer.clone(),
2218                provider: mock_provider,
2219                relayer_repository: Arc::new(mock_relayer),
2220                network_repository: Arc::new(mock_network),
2221                transaction_repository: Arc::new(mock_transaction),
2222                transaction_counter_service: Arc::new(counter_service),
2223                job_producer: Arc::new(mock_job_producer),
2224                price_calculator: mock_price_calculator,
2225                signer: mock_signer,
2226            };
2227
2228            // Call cancel_transaction and verify it succeeds
2229            let result = evm_transaction.cancel_transaction(test_tx.clone()).await;
2230            assert!(result.is_ok());
2231            let cancelled_tx = result.unwrap();
2232
2233            // Verify the cancellation transaction was properly created
2234            assert_eq!(cancelled_tx.id, "test-tx-id");
2235            assert_eq!(cancelled_tx.status, TransactionStatus::Submitted);
2236
2237            // Verify the network data was properly updated
2238            if let NetworkTransactionData::Evm(evm_data) = &cancelled_tx.network_data {
2239                assert_eq!(evm_data.nonce, Some(42)); // Same nonce as original
2240            } else {
2241                panic!("Expected EVM transaction data");
2242            }
2243        }
2244
2245        // Test Case 3: Attempting to cancel a confirmed transaction (should fail)
2246        {
2247            // Create minimal mocks for failure case
2248            let mock_transaction = MockTransactionRepository::new();
2249            let mock_relayer = MockRelayerRepository::new();
2250            let mock_provider = MockEvmProviderTrait::new();
2251            let mock_signer = MockSigner::new();
2252            let mock_job_producer = MockJobProducerTrait::new();
2253            let mock_price_calculator = MockPriceCalculator::new();
2254            let counter_service = MockTransactionCounterTrait::new();
2255
2256            // Create test relayer and confirmed transaction
2257            let relayer = create_test_relayer();
2258            let mut test_tx = create_test_transaction();
2259            test_tx.status = TransactionStatus::Confirmed;
2260
2261            let mock_network = MockNetworkRepository::new();
2262
2263            // Set up EVM transaction with the mocks
2264            let evm_transaction = EvmRelayerTransaction {
2265                relayer: relayer.clone(),
2266                provider: mock_provider,
2267                relayer_repository: Arc::new(mock_relayer),
2268                network_repository: Arc::new(mock_network),
2269                transaction_repository: Arc::new(mock_transaction),
2270                transaction_counter_service: Arc::new(counter_service),
2271                job_producer: Arc::new(mock_job_producer),
2272                price_calculator: mock_price_calculator,
2273                signer: mock_signer,
2274            };
2275
2276            // Call cancel_transaction and verify it fails
2277            let result = evm_transaction.cancel_transaction(test_tx.clone()).await;
2278            assert!(result.is_err());
2279            if let Err(TransactionError::ValidationError(msg)) = result {
2280                assert!(msg.contains("Invalid transaction state for cancel_transaction"));
2281            } else {
2282                panic!("Expected ValidationError");
2283            }
2284        }
2285    }
2286
2287    #[tokio::test]
2288    async fn test_replace_transaction() {
2289        // Test Case: Replacing a submitted transaction with new gas price
2290        {
2291            // Create mocks for all dependencies
2292            let mut mock_transaction = MockTransactionRepository::new();
2293            let mock_relayer = MockRelayerRepository::new();
2294            let mut mock_provider = MockEvmProviderTrait::new();
2295            let mut mock_signer = MockSigner::new();
2296            let mut mock_job_producer = MockJobProducerTrait::new();
2297            let mut mock_price_calculator = MockPriceCalculator::new();
2298            let counter_service = MockTransactionCounterTrait::new();
2299
2300            // Create test relayer and submitted transaction
2301            let relayer = create_test_relayer();
2302            let mut test_tx = create_test_transaction();
2303            test_tx.status = TransactionStatus::Submitted;
2304            test_tx.sent_at = Some(Utc::now().to_rfc3339());
2305
2306            // Set up price calculator expectations for replacement
2307            mock_price_calculator
2308                .expect_get_transaction_price_params()
2309                .return_once(move |_, _| {
2310                    Ok(PriceParams {
2311                        gas_price: Some(40000000000), // 40 Gwei (higher than original)
2312                        max_fee_per_gas: None,
2313                        max_priority_fee_per_gas: None,
2314                        is_min_bumped: Some(true),
2315                        extra_fee: Some(U256::ZERO),
2316                        total_cost: U256::from(2001000000000000000u64), // 2 ETH + gas costs
2317                    })
2318                });
2319
2320            // Signer should be called to sign the replacement transaction
2321            mock_signer.expect_sign_transaction().returning(|_| {
2322                Box::pin(ready(Ok(
2323                    crate::domain::relayer::SignTransactionResponse::Evm(
2324                        crate::domain::relayer::SignTransactionResponseEvm {
2325                            hash: "0xreplacement_hash".to_string(),
2326                            signature: crate::models::EvmTransactionDataSignature {
2327                                r: "r".to_string(),
2328                                s: "s".to_string(),
2329                                v: 1,
2330                                sig: "0xsignature".to_string(),
2331                            },
2332                            raw: vec![1, 2, 3],
2333                        },
2334                    ),
2335                )))
2336            });
2337
2338            // Provider balance check should pass
2339            mock_provider
2340                .expect_get_balance()
2341                .with(eq("0xSender"))
2342                .returning(|_| Box::pin(ready(Ok(U256::from(3000000000000000000u64)))));
2343
2344            // Transaction repository should update using update_network_data
2345            let test_tx_clone = test_tx.clone();
2346            mock_transaction
2347                .expect_update_network_data()
2348                .returning(move |tx_id, network_data| {
2349                    let mut updated_tx = test_tx_clone.clone();
2350                    updated_tx.id = tx_id;
2351                    updated_tx.network_data = network_data;
2352                    Ok(updated_tx)
2353                });
2354
2355            // Job producer expectations
2356            mock_job_producer
2357                .expect_produce_submit_transaction_job()
2358                .returning(|_, _| Box::pin(ready(Ok(()))));
2359            mock_job_producer
2360                .expect_produce_send_notification_job()
2361                .returning(|_, _| Box::pin(ready(Ok(()))));
2362
2363            // Network repository expectations for mempool check
2364            let mut mock_network = MockNetworkRepository::new();
2365            mock_network
2366                .expect_get_by_chain_id()
2367                .with(eq(NetworkType::Evm), eq(1))
2368                .returning(|_, _| {
2369                    use crate::config::{EvmNetworkConfig, NetworkConfigCommon};
2370                    use crate::models::{NetworkConfigData, NetworkRepoModel};
2371
2372                    let config = EvmNetworkConfig {
2373                        common: NetworkConfigCommon {
2374                            network: "mainnet".to_string(),
2375                            from: None,
2376                            rpc_urls: Some(vec![crate::models::RpcConfig::new(
2377                                "https://rpc.example.com".to_string(),
2378                            )]),
2379                            explorer_urls: None,
2380                            average_blocktime_ms: Some(12000),
2381                            is_testnet: Some(false),
2382                            tags: Some(vec!["mainnet".to_string()]), // No "no-mempool" tag
2383                        },
2384                        chain_id: Some(1),
2385                        required_confirmations: Some(12),
2386                        features: Some(vec!["eip1559".to_string()]),
2387                        symbol: Some("ETH".to_string()),
2388                        gas_price_cache: None,
2389                    };
2390                    Ok(Some(NetworkRepoModel {
2391                        id: "evm:mainnet".to_string(),
2392                        name: "mainnet".to_string(),
2393                        network_type: NetworkType::Evm,
2394                        config: NetworkConfigData::Evm(config),
2395                    }))
2396                });
2397
2398            // Set up EVM transaction with the mocks
2399            let evm_transaction = EvmRelayerTransaction {
2400                relayer: relayer.clone(),
2401                provider: mock_provider,
2402                relayer_repository: Arc::new(mock_relayer),
2403                network_repository: Arc::new(mock_network),
2404                transaction_repository: Arc::new(mock_transaction),
2405                transaction_counter_service: Arc::new(counter_service),
2406                job_producer: Arc::new(mock_job_producer),
2407                price_calculator: mock_price_calculator,
2408                signer: mock_signer,
2409            };
2410
2411            // Create replacement request with speed-based pricing
2412            let replacement_request = NetworkTransactionRequest::Evm(EvmTransactionRequest {
2413                to: Some("0xNewRecipient".to_string()),
2414                value: U256::from(2000000000000000000u64), // 2 ETH
2415                data: Some("0xNewData".to_string()),
2416                gas_limit: Some(25000),
2417                gas_price: None, // Use speed-based pricing
2418                max_fee_per_gas: None,
2419                max_priority_fee_per_gas: None,
2420                speed: Some(Speed::Fast),
2421                valid_until: None,
2422            });
2423
2424            // Call replace_transaction and verify it succeeds
2425            let result = evm_transaction
2426                .replace_transaction(test_tx.clone(), replacement_request)
2427                .await;
2428            if let Err(ref e) = result {
2429                eprintln!("Replace transaction failed with error: {e:?}");
2430            }
2431            assert!(result.is_ok());
2432            let replaced_tx = result.unwrap();
2433
2434            // Verify the replacement was properly processed
2435            assert_eq!(replaced_tx.id, "test-tx-id");
2436
2437            // Verify the network data was properly updated
2438            if let NetworkTransactionData::Evm(evm_data) = &replaced_tx.network_data {
2439                assert_eq!(evm_data.to, Some("0xNewRecipient".to_string()));
2440                assert_eq!(evm_data.value, U256::from(2000000000000000000u64));
2441                assert_eq!(evm_data.gas_price, Some(40000000000));
2442                assert_eq!(evm_data.gas_limit, Some(25000));
2443                assert!(evm_data.hash.is_some());
2444                assert!(evm_data.raw.is_some());
2445            } else {
2446                panic!("Expected EVM transaction data");
2447            }
2448        }
2449
2450        // Test Case: Attempting to replace a confirmed transaction (should fail)
2451        {
2452            // Create minimal mocks for failure case
2453            let mock_transaction = MockTransactionRepository::new();
2454            let mock_relayer = MockRelayerRepository::new();
2455            let mock_provider = MockEvmProviderTrait::new();
2456            let mock_signer = MockSigner::new();
2457            let mock_job_producer = MockJobProducerTrait::new();
2458            let mock_price_calculator = MockPriceCalculator::new();
2459            let counter_service = MockTransactionCounterTrait::new();
2460
2461            // Create test relayer and confirmed transaction
2462            let relayer = create_test_relayer();
2463            let mut test_tx = create_test_transaction();
2464            test_tx.status = TransactionStatus::Confirmed;
2465
2466            let mock_network = MockNetworkRepository::new();
2467
2468            // Set up EVM transaction with the mocks
2469            let evm_transaction = EvmRelayerTransaction {
2470                relayer: relayer.clone(),
2471                provider: mock_provider,
2472                relayer_repository: Arc::new(mock_relayer),
2473                network_repository: Arc::new(mock_network),
2474                transaction_repository: Arc::new(mock_transaction),
2475                transaction_counter_service: Arc::new(counter_service),
2476                job_producer: Arc::new(mock_job_producer),
2477                price_calculator: mock_price_calculator,
2478                signer: mock_signer,
2479            };
2480
2481            // Create dummy replacement request
2482            let replacement_request = NetworkTransactionRequest::Evm(EvmTransactionRequest {
2483                to: Some("0xNewRecipient".to_string()),
2484                value: U256::from(1000000000000000000u64),
2485                data: Some("0xData".to_string()),
2486                gas_limit: Some(21000),
2487                gas_price: Some(30000000000),
2488                max_fee_per_gas: None,
2489                max_priority_fee_per_gas: None,
2490                speed: Some(Speed::Fast),
2491                valid_until: None,
2492            });
2493
2494            // Call replace_transaction and verify it fails
2495            let result = evm_transaction
2496                .replace_transaction(test_tx.clone(), replacement_request)
2497                .await;
2498            assert!(result.is_err());
2499            if let Err(TransactionError::ValidationError(msg)) = result {
2500                assert!(msg.contains("Invalid transaction state for replace_transaction"));
2501            } else {
2502                panic!("Expected ValidationError");
2503            }
2504        }
2505    }
2506
2507    #[tokio::test]
2508    async fn test_estimate_tx_gas_limit_success() {
2509        let mock_transaction = MockTransactionRepository::new();
2510        let mock_relayer = MockRelayerRepository::new();
2511        let mut mock_provider = MockEvmProviderTrait::new();
2512        let mock_signer = MockSigner::new();
2513        let mock_job_producer = MockJobProducerTrait::new();
2514        let mock_price_calculator = MockPriceCalculator::new();
2515        let counter_service = MockTransactionCounterTrait::new();
2516        let mock_network = MockNetworkRepository::new();
2517
2518        // Create test relayer and pending transaction
2519        let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2520            gas_limit_estimation: Some(true),
2521            ..Default::default()
2522        });
2523        let evm_data = EvmTransactionData {
2524            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
2525            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
2526            value: U256::from(1000000000000000000u128),
2527            data: Some("0x".to_string()),
2528            gas_limit: None,
2529            gas_price: Some(20_000_000_000),
2530            nonce: Some(1),
2531            chain_id: 1,
2532            hash: None,
2533            signature: None,
2534            speed: Some(Speed::Average),
2535            max_fee_per_gas: None,
2536            max_priority_fee_per_gas: None,
2537            raw: None,
2538        };
2539
2540        // Mock provider to return 21000 as estimated gas
2541        mock_provider
2542            .expect_estimate_gas()
2543            .times(1)
2544            .returning(|_| Box::pin(async { Ok(21000) }));
2545
2546        let transaction = EvmRelayerTransaction::new(
2547            relayer.clone(),
2548            mock_provider,
2549            Arc::new(mock_relayer),
2550            Arc::new(mock_network),
2551            Arc::new(mock_transaction),
2552            Arc::new(counter_service),
2553            Arc::new(mock_job_producer),
2554            mock_price_calculator,
2555            mock_signer,
2556        )
2557        .unwrap();
2558
2559        let result = transaction
2560            .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
2561            .await;
2562
2563        assert!(result.is_ok());
2564        // Expected: 21000 * 110 / 100 = 23100
2565        assert_eq!(result.unwrap(), 23100);
2566    }
2567
2568    #[tokio::test]
2569    async fn test_estimate_tx_gas_limit_disabled() {
2570        let mock_transaction = MockTransactionRepository::new();
2571        let mock_relayer = MockRelayerRepository::new();
2572        let mut mock_provider = MockEvmProviderTrait::new();
2573        let mock_signer = MockSigner::new();
2574        let mock_job_producer = MockJobProducerTrait::new();
2575        let mock_price_calculator = MockPriceCalculator::new();
2576        let counter_service = MockTransactionCounterTrait::new();
2577        let mock_network = MockNetworkRepository::new();
2578
2579        // Create test relayer and pending transaction
2580        let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2581            gas_limit_estimation: Some(false),
2582            ..Default::default()
2583        });
2584
2585        let evm_data = EvmTransactionData {
2586            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
2587            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
2588            value: U256::from(1000000000000000000u128),
2589            data: Some("0x".to_string()),
2590            gas_limit: None,
2591            gas_price: Some(20_000_000_000),
2592            nonce: Some(1),
2593            chain_id: 1,
2594            hash: None,
2595            signature: None,
2596            speed: Some(Speed::Average),
2597            max_fee_per_gas: None,
2598            max_priority_fee_per_gas: None,
2599            raw: None,
2600        };
2601
2602        // Provider should not be called when estimation is disabled
2603        mock_provider.expect_estimate_gas().times(0);
2604
2605        let transaction = EvmRelayerTransaction::new(
2606            relayer.clone(),
2607            mock_provider,
2608            Arc::new(mock_relayer),
2609            Arc::new(mock_network),
2610            Arc::new(mock_transaction),
2611            Arc::new(counter_service),
2612            Arc::new(mock_job_producer),
2613            mock_price_calculator,
2614            mock_signer,
2615        )
2616        .unwrap();
2617
2618        let result = transaction
2619            .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
2620            .await;
2621
2622        assert!(result.is_err());
2623        assert!(matches!(
2624            result.unwrap_err(),
2625            TransactionError::UnexpectedError(_)
2626        ));
2627    }
2628
2629    #[tokio::test]
2630    async fn test_estimate_tx_gas_limit_default_enabled() {
2631        let mock_transaction = MockTransactionRepository::new();
2632        let mock_relayer = MockRelayerRepository::new();
2633        let mut mock_provider = MockEvmProviderTrait::new();
2634        let mock_signer = MockSigner::new();
2635        let mock_job_producer = MockJobProducerTrait::new();
2636        let mock_price_calculator = MockPriceCalculator::new();
2637        let counter_service = MockTransactionCounterTrait::new();
2638        let mock_network = MockNetworkRepository::new();
2639
2640        let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2641            gas_limit_estimation: None, // Should default to true
2642            ..Default::default()
2643        });
2644
2645        let evm_data = EvmTransactionData {
2646            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
2647            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
2648            value: U256::from(1000000000000000000u128),
2649            data: Some("0x".to_string()),
2650            gas_limit: None,
2651            gas_price: Some(20_000_000_000),
2652            nonce: Some(1),
2653            chain_id: 1,
2654            hash: None,
2655            signature: None,
2656            speed: Some(Speed::Average),
2657            max_fee_per_gas: None,
2658            max_priority_fee_per_gas: None,
2659            raw: None,
2660        };
2661
2662        // Mock provider to return 50000 as estimated gas
2663        mock_provider
2664            .expect_estimate_gas()
2665            .times(1)
2666            .returning(|_| Box::pin(async { Ok(50000) }));
2667
2668        let transaction = EvmRelayerTransaction::new(
2669            relayer.clone(),
2670            mock_provider,
2671            Arc::new(mock_relayer),
2672            Arc::new(mock_network),
2673            Arc::new(mock_transaction),
2674            Arc::new(counter_service),
2675            Arc::new(mock_job_producer),
2676            mock_price_calculator,
2677            mock_signer,
2678        )
2679        .unwrap();
2680
2681        let result = transaction
2682            .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
2683            .await;
2684
2685        assert!(result.is_ok());
2686        // Expected: 50000 * 110 / 100 = 55000
2687        assert_eq!(result.unwrap(), 55000);
2688    }
2689
2690    #[tokio::test]
2691    async fn test_estimate_tx_gas_limit_provider_error() {
2692        let mock_transaction = MockTransactionRepository::new();
2693        let mock_relayer = MockRelayerRepository::new();
2694        let mut mock_provider = MockEvmProviderTrait::new();
2695        let mock_signer = MockSigner::new();
2696        let mock_job_producer = MockJobProducerTrait::new();
2697        let mock_price_calculator = MockPriceCalculator::new();
2698        let counter_service = MockTransactionCounterTrait::new();
2699        let mock_network = MockNetworkRepository::new();
2700
2701        let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2702            gas_limit_estimation: Some(true),
2703            ..Default::default()
2704        });
2705
2706        let evm_data = EvmTransactionData {
2707            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
2708            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
2709            value: U256::from(1000000000000000000u128),
2710            data: Some("0x".to_string()),
2711            gas_limit: None,
2712            gas_price: Some(20_000_000_000),
2713            nonce: Some(1),
2714            chain_id: 1,
2715            hash: None,
2716            signature: None,
2717            speed: Some(Speed::Average),
2718            max_fee_per_gas: None,
2719            max_priority_fee_per_gas: None,
2720            raw: None,
2721        };
2722
2723        // Mock provider to return an error
2724        mock_provider.expect_estimate_gas().times(1).returning(|_| {
2725            Box::pin(async {
2726                Err(crate::services::provider::ProviderError::Other(
2727                    "RPC error".to_string(),
2728                ))
2729            })
2730        });
2731
2732        let transaction = EvmRelayerTransaction::new(
2733            relayer.clone(),
2734            mock_provider,
2735            Arc::new(mock_relayer),
2736            Arc::new(mock_network),
2737            Arc::new(mock_transaction),
2738            Arc::new(counter_service),
2739            Arc::new(mock_job_producer),
2740            mock_price_calculator,
2741            mock_signer,
2742        )
2743        .unwrap();
2744
2745        let result = transaction
2746            .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
2747            .await;
2748
2749        assert!(result.is_err());
2750        assert!(matches!(
2751            result.unwrap_err(),
2752            TransactionError::UnexpectedError(_)
2753        ));
2754    }
2755
2756    #[tokio::test]
2757    async fn test_prepare_transaction_uses_gas_estimation_and_stores_result() {
2758        let mut mock_transaction = MockTransactionRepository::new();
2759        let mock_relayer = MockRelayerRepository::new();
2760        let mut mock_provider = MockEvmProviderTrait::new();
2761        let mut mock_signer = MockSigner::new();
2762        let mut mock_job_producer = MockJobProducerTrait::new();
2763        let mut mock_price_calculator = MockPriceCalculator::new();
2764        let mut counter_service = MockTransactionCounterTrait::new();
2765        let mock_network = MockNetworkRepository::new();
2766
2767        // Create test relayer with gas limit estimation enabled
2768        let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2769            gas_limit_estimation: Some(true),
2770            min_balance: Some(100000000000000000u128),
2771            ..Default::default()
2772        });
2773
2774        // Create test transaction WITHOUT gas_limit (so estimation will be triggered)
2775        let mut test_tx = create_test_transaction();
2776        if let NetworkTransactionData::Evm(ref mut evm_data) = test_tx.network_data {
2777            evm_data.gas_limit = None; // This should trigger gas estimation
2778            evm_data.nonce = None; // This will be set by the counter service
2779        }
2780
2781        // Expected estimated gas from provider
2782        const PROVIDER_GAS_ESTIMATE: u64 = 45000;
2783        const EXPECTED_GAS_WITH_BUFFER: u64 = 49500; // 45000 * 110 / 100
2784
2785        // Mock provider to return specific gas estimate
2786        mock_provider
2787            .expect_estimate_gas()
2788            .times(1)
2789            .returning(move |_| Box::pin(async move { Ok(PROVIDER_GAS_ESTIMATE) }));
2790
2791        // Mock provider for balance check
2792        mock_provider
2793            .expect_get_balance()
2794            .times(1)
2795            .returning(|_| Box::pin(async { Ok(U256::from(2000000000000000000u128)) })); // 2 ETH
2796
2797        let price_params = PriceParams {
2798            gas_price: Some(20_000_000_000), // 20 Gwei
2799            max_fee_per_gas: None,
2800            max_priority_fee_per_gas: None,
2801            is_min_bumped: None,
2802            extra_fee: None,
2803            total_cost: U256::from(1900000000000000000u128), // 1.9 ETH total cost
2804        };
2805
2806        // Mock price calculator
2807        mock_price_calculator
2808            .expect_get_transaction_price_params()
2809            .returning(move |_, _| Ok(price_params.clone()));
2810
2811        // Mock transaction counter to return a nonce
2812        counter_service
2813            .expect_get_and_increment()
2814            .times(1)
2815            .returning(|_, _| Box::pin(async { Ok(42) }));
2816
2817        // Mock signer to return a signed transaction
2818        mock_signer.expect_sign_transaction().returning(|_| {
2819            Box::pin(ready(Ok(
2820                crate::domain::relayer::SignTransactionResponse::Evm(
2821                    crate::domain::relayer::SignTransactionResponseEvm {
2822                        hash: "0xhash".to_string(),
2823                        signature: crate::models::EvmTransactionDataSignature {
2824                            r: "r".to_string(),
2825                            s: "s".to_string(),
2826                            v: 1,
2827                            sig: "0xsignature".to_string(),
2828                        },
2829                        raw: vec![1, 2, 3],
2830                    },
2831                ),
2832            )))
2833        });
2834
2835        // Mock job producer to capture the submission job
2836        mock_job_producer
2837            .expect_produce_submit_transaction_job()
2838            .returning(|_, _| Box::pin(async { Ok(()) }));
2839
2840        mock_job_producer
2841            .expect_produce_send_notification_job()
2842            .returning(|_, _| Box::pin(ready(Ok(()))));
2843
2844        // Mock transaction repository partial_update calls
2845        // Note: prepare_transaction calls partial_update twice:
2846        // 1. Presign update (saves nonce before signing)
2847        // 2. Postsign update (saves signed data and marks as Sent)
2848        let expected_gas_limit = EXPECTED_GAS_WITH_BUFFER;
2849
2850        let test_tx_clone = test_tx.clone();
2851        mock_transaction
2852            .expect_partial_update()
2853            .times(2)
2854            .returning(move |_, update| {
2855                let mut updated_tx = test_tx_clone.clone();
2856
2857                // Apply the updates from the request
2858                if let Some(status) = &update.status {
2859                    updated_tx.status = status.clone();
2860                }
2861                if let Some(network_data) = &update.network_data {
2862                    updated_tx.network_data = network_data.clone();
2863                } else {
2864                    // If network_data is not being updated, ensure gas_limit is set
2865                    if let NetworkTransactionData::Evm(ref mut evm_data) = updated_tx.network_data {
2866                        if evm_data.gas_limit.is_none() {
2867                            evm_data.gas_limit = Some(expected_gas_limit);
2868                        }
2869                    }
2870                }
2871                if let Some(hashes) = &update.hashes {
2872                    updated_tx.hashes = hashes.clone();
2873                }
2874
2875                Ok(updated_tx)
2876            });
2877
2878        let transaction = EvmRelayerTransaction::new(
2879            relayer.clone(),
2880            mock_provider,
2881            Arc::new(mock_relayer),
2882            Arc::new(mock_network),
2883            Arc::new(mock_transaction),
2884            Arc::new(counter_service),
2885            Arc::new(mock_job_producer),
2886            mock_price_calculator,
2887            mock_signer,
2888        )
2889        .unwrap();
2890
2891        // Call prepare_transaction
2892        let result = transaction.prepare_transaction(test_tx).await;
2893
2894        // Verify the transaction was prepared successfully
2895        assert!(result.is_ok(), "prepare_transaction should succeed");
2896        let prepared_tx = result.unwrap();
2897
2898        // Verify the final transaction has the estimated gas limit
2899        if let NetworkTransactionData::Evm(evm_data) = prepared_tx.network_data {
2900            assert_eq!(evm_data.gas_limit, Some(EXPECTED_GAS_WITH_BUFFER));
2901        } else {
2902            panic!("Expected EVM network data");
2903        }
2904    }
2905
2906    #[tokio::test]
2907    async fn test_prepare_transaction_estimates_gas_for_contract_creation() {
2908        let mut mock_transaction = MockTransactionRepository::new();
2909        let mock_relayer = MockRelayerRepository::new();
2910        let mut mock_provider = MockEvmProviderTrait::new();
2911        let mut mock_signer = MockSigner::new();
2912        let mut mock_job_producer = MockJobProducerTrait::new();
2913        let mut mock_price_calculator = MockPriceCalculator::new();
2914        let mut counter_service = MockTransactionCounterTrait::new();
2915        let mock_network = MockNetworkRepository::new();
2916
2917        let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2918            gas_limit_estimation: Some(true),
2919            min_balance: Some(100000000000000000u128),
2920            ..Default::default()
2921        });
2922
2923        let mut test_tx = create_test_transaction();
2924        if let NetworkTransactionData::Evm(ref mut evm_data) = test_tx.network_data {
2925            evm_data.to = None;
2926            evm_data.data = Some("0x6080604052348015600f57600080fd5b".to_string());
2927            evm_data.gas_limit = None;
2928            evm_data.nonce = None;
2929        }
2930
2931        const PROVIDER_GAS_ESTIMATE: u64 = 1500000;
2932        const EXPECTED_GAS_WITH_BUFFER: u64 = 1650000;
2933
2934        mock_provider
2935            .expect_estimate_gas()
2936            .withf(|tx| tx.to.is_none())
2937            .times(1)
2938            .returning(move |_| Box::pin(async move { Ok(PROVIDER_GAS_ESTIMATE) }));
2939
2940        mock_provider
2941            .expect_get_balance()
2942            .times(1)
2943            .returning(|_| Box::pin(async { Ok(U256::from(2000000000000000000u128)) }));
2944
2945        let price_params = PriceParams {
2946            gas_price: Some(20_000_000_000),
2947            max_fee_per_gas: None,
2948            max_priority_fee_per_gas: None,
2949            is_min_bumped: None,
2950            extra_fee: None,
2951            total_cost: U256::from(1900000000000000000u128),
2952        };
2953
2954        mock_price_calculator
2955            .expect_get_transaction_price_params()
2956            .returning(move |_, _| Ok(price_params.clone()));
2957
2958        counter_service
2959            .expect_get_and_increment()
2960            .times(1)
2961            .returning(|_, _| Box::pin(async { Ok(42) }));
2962
2963        mock_signer.expect_sign_transaction().returning(|_| {
2964            Box::pin(ready(Ok(
2965                crate::domain::relayer::SignTransactionResponse::Evm(
2966                    crate::domain::relayer::SignTransactionResponseEvm {
2967                        hash: "0xhash".to_string(),
2968                        signature: crate::models::EvmTransactionDataSignature {
2969                            r: "r".to_string(),
2970                            s: "s".to_string(),
2971                            v: 1,
2972                            sig: "0xsignature".to_string(),
2973                        },
2974                        raw: vec![1, 2, 3],
2975                    },
2976                ),
2977            )))
2978        });
2979
2980        mock_job_producer
2981            .expect_produce_submit_transaction_job()
2982            .returning(|_, _| Box::pin(async { Ok(()) }));
2983
2984        mock_job_producer
2985            .expect_produce_send_notification_job()
2986            .returning(|_, _| Box::pin(ready(Ok(()))));
2987
2988        let expected_gas_limit = EXPECTED_GAS_WITH_BUFFER;
2989        let test_tx_clone = test_tx.clone();
2990        mock_transaction
2991            .expect_partial_update()
2992            .times(2)
2993            .returning(move |_, update| {
2994                let mut updated_tx = test_tx_clone.clone();
2995
2996                if let Some(status) = &update.status {
2997                    updated_tx.status = status.clone();
2998                }
2999                if let Some(network_data) = &update.network_data {
3000                    updated_tx.network_data = network_data.clone();
3001                } else if let NetworkTransactionData::Evm(ref mut evm_data) =
3002                    updated_tx.network_data
3003                {
3004                    if evm_data.gas_limit.is_none() {
3005                        evm_data.gas_limit = Some(expected_gas_limit);
3006                    }
3007                }
3008                if let Some(hashes) = &update.hashes {
3009                    updated_tx.hashes = hashes.clone();
3010                }
3011
3012                Ok(updated_tx)
3013            });
3014
3015        let transaction = EvmRelayerTransaction::new(
3016            relayer,
3017            mock_provider,
3018            Arc::new(mock_relayer),
3019            Arc::new(mock_network),
3020            Arc::new(mock_transaction),
3021            Arc::new(counter_service),
3022            Arc::new(mock_job_producer),
3023            mock_price_calculator,
3024            mock_signer,
3025        )
3026        .unwrap();
3027
3028        let result = transaction.prepare_transaction(test_tx).await;
3029
3030        assert!(result.is_ok(), "prepare_transaction should succeed");
3031        let prepared_tx = result.unwrap();
3032
3033        if let NetworkTransactionData::Evm(evm_data) = prepared_tx.network_data {
3034            assert_eq!(evm_data.to, None);
3035            assert_eq!(evm_data.gas_limit, Some(EXPECTED_GAS_WITH_BUFFER));
3036        } else {
3037            panic!("Expected EVM network data");
3038        }
3039    }
3040
3041    #[test]
3042    fn test_is_already_submitted_error_detection() {
3043        // Test "already known" variants
3044        assert!(DefaultEvmTransaction::is_already_submitted_error(
3045            &"already known"
3046        ));
3047        assert!(DefaultEvmTransaction::is_already_submitted_error(
3048            &"Transaction already known"
3049        ));
3050        assert!(DefaultEvmTransaction::is_already_submitted_error(
3051            &"Error: already known"
3052        ));
3053
3054        // Test "nonce too low" variants
3055        assert!(DefaultEvmTransaction::is_already_submitted_error(
3056            &"nonce too low"
3057        ));
3058        assert!(DefaultEvmTransaction::is_already_submitted_error(
3059            &"Nonce Too Low"
3060        ));
3061        assert!(DefaultEvmTransaction::is_already_submitted_error(
3062            &"Error: nonce too low"
3063        ));
3064
3065        // Test "nonce is too low" variants (some providers use this wording)
3066        assert!(DefaultEvmTransaction::is_already_submitted_error(
3067            &"nonce is too low"
3068        ));
3069        assert!(DefaultEvmTransaction::is_already_submitted_error(
3070            &"Error: nonce is too low"
3071        ));
3072
3073        // Test "known transaction" variants (Besu)
3074        assert!(DefaultEvmTransaction::is_already_submitted_error(
3075            &"known transaction"
3076        ));
3077        assert!(DefaultEvmTransaction::is_already_submitted_error(
3078            &"Known Transaction"
3079        ));
3080
3081        // Test "replacement transaction underpriced" variants
3082        assert!(DefaultEvmTransaction::is_already_submitted_error(
3083            &"replacement transaction underpriced"
3084        ));
3085        assert!(DefaultEvmTransaction::is_already_submitted_error(
3086            &"Replacement Transaction Underpriced"
3087        ));
3088
3089        // Test "same hash was already imported" (OpenEthereum)
3090        assert!(DefaultEvmTransaction::is_already_submitted_error(
3091            &"same hash was already imported"
3092        ));
3093
3094        // Test non-matching errors
3095        assert!(!DefaultEvmTransaction::is_already_submitted_error(
3096            &"insufficient funds"
3097        ));
3098        assert!(!DefaultEvmTransaction::is_already_submitted_error(
3099            &"execution reverted"
3100        ));
3101        assert!(!DefaultEvmTransaction::is_already_submitted_error(
3102            &"gas too low"
3103        ));
3104        assert!(!DefaultEvmTransaction::is_already_submitted_error(
3105            &"timeout"
3106        ));
3107        // "unknown transaction" must NOT match "known transaction"
3108        assert!(!DefaultEvmTransaction::is_already_submitted_error(
3109            &"Unknown transaction status"
3110        ));
3111    }
3112
3113    /// Test submit_transaction with "already known" error in Sent status
3114    /// This should treat the error as success and update to Submitted
3115    #[tokio::test]
3116    async fn test_submit_transaction_already_known_error_from_sent() {
3117        let mut mock_transaction = MockTransactionRepository::new();
3118        let mock_relayer = MockRelayerRepository::new();
3119        let mut mock_provider = MockEvmProviderTrait::new();
3120        let mock_signer = MockSigner::new();
3121        let mut mock_job_producer = MockJobProducerTrait::new();
3122        let mock_price_calculator = MockPriceCalculator::new();
3123        let counter_service = MockTransactionCounterTrait::new();
3124        let mock_network = MockNetworkRepository::new();
3125
3126        let relayer = create_test_relayer();
3127        let mut test_tx = create_test_transaction();
3128        test_tx.status = TransactionStatus::Sent;
3129        test_tx.sent_at = Some(Utc::now().to_rfc3339());
3130        test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3131            nonce: Some(42),
3132            hash: Some("0xhash".to_string()),
3133            raw: Some(vec![1, 2, 3]),
3134            ..test_tx.network_data.get_evm_transaction_data().unwrap()
3135        });
3136
3137        // Provider returns "already known" error
3138        mock_provider
3139            .expect_send_raw_transaction()
3140            .times(1)
3141            .returning(|_| {
3142                Box::pin(async {
3143                    Err(crate::services::provider::ProviderError::Other(
3144                        "already known: transaction already in mempool".to_string(),
3145                    ))
3146                })
3147            });
3148
3149        // Should still update to Submitted status
3150        let test_tx_clone = test_tx.clone();
3151        mock_transaction
3152            .expect_partial_update()
3153            .times(1)
3154            .withf(|_, update| update.status == Some(TransactionStatus::Submitted))
3155            .returning(move |_, update| {
3156                let mut updated_tx = test_tx_clone.clone();
3157                updated_tx.status = update.status.unwrap();
3158                updated_tx.sent_at = update.sent_at.clone();
3159                Ok(updated_tx)
3160            });
3161
3162        mock_job_producer
3163            .expect_produce_send_notification_job()
3164            .times(1)
3165            .returning(|_, _| Box::pin(ready(Ok(()))));
3166
3167        let evm_transaction = EvmRelayerTransaction {
3168            relayer: relayer.clone(),
3169            provider: mock_provider,
3170            relayer_repository: Arc::new(mock_relayer),
3171            network_repository: Arc::new(mock_network),
3172            transaction_repository: Arc::new(mock_transaction),
3173            transaction_counter_service: Arc::new(counter_service),
3174            job_producer: Arc::new(mock_job_producer),
3175            price_calculator: mock_price_calculator,
3176            signer: mock_signer,
3177        };
3178
3179        let result = evm_transaction.submit_transaction(test_tx).await;
3180        assert!(result.is_ok());
3181        let updated_tx = result.unwrap();
3182        assert_eq!(updated_tx.status, TransactionStatus::Submitted);
3183    }
3184
3185    /// Test submit_transaction with real error (not "already known") should fail
3186    #[tokio::test]
3187    async fn test_submit_transaction_real_error_fails() {
3188        let mock_transaction = MockTransactionRepository::new();
3189        let mock_relayer = MockRelayerRepository::new();
3190        let mut mock_provider = MockEvmProviderTrait::new();
3191        let mock_signer = MockSigner::new();
3192        let mock_job_producer = MockJobProducerTrait::new();
3193        let mock_price_calculator = MockPriceCalculator::new();
3194        let counter_service = MockTransactionCounterTrait::new();
3195        let mock_network = MockNetworkRepository::new();
3196
3197        let relayer = create_test_relayer();
3198        let mut test_tx = create_test_transaction();
3199        test_tx.status = TransactionStatus::Sent;
3200        test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3201            raw: Some(vec![1, 2, 3]),
3202            ..test_tx.network_data.get_evm_transaction_data().unwrap()
3203        });
3204
3205        // Provider returns a real error
3206        mock_provider
3207            .expect_send_raw_transaction()
3208            .times(1)
3209            .returning(|_| {
3210                Box::pin(async {
3211                    Err(crate::services::provider::ProviderError::Other(
3212                        "insufficient funds for gas * price + value".to_string(),
3213                    ))
3214                })
3215            });
3216
3217        let evm_transaction = EvmRelayerTransaction {
3218            relayer: relayer.clone(),
3219            provider: mock_provider,
3220            relayer_repository: Arc::new(mock_relayer),
3221            network_repository: Arc::new(mock_network),
3222            transaction_repository: Arc::new(mock_transaction),
3223            transaction_counter_service: Arc::new(counter_service),
3224            job_producer: Arc::new(mock_job_producer),
3225            price_calculator: mock_price_calculator,
3226            signer: mock_signer,
3227        };
3228
3229        let result = evm_transaction.submit_transaction(test_tx).await;
3230        assert!(result.is_err());
3231    }
3232
3233    /// Test resubmit_transaction when transaction is already submitted
3234    /// Should NOT update hash, only status
3235    #[tokio::test]
3236    async fn test_resubmit_transaction_already_submitted_preserves_hash() {
3237        let mut mock_transaction = MockTransactionRepository::new();
3238        let mock_relayer = MockRelayerRepository::new();
3239        let mut mock_provider = MockEvmProviderTrait::new();
3240        let mut mock_signer = MockSigner::new();
3241        let mock_job_producer = MockJobProducerTrait::new();
3242        let mut mock_price_calculator = MockPriceCalculator::new();
3243        let counter_service = MockTransactionCounterTrait::new();
3244        let mock_network = MockNetworkRepository::new();
3245
3246        let relayer = create_test_relayer();
3247        let mut test_tx = create_test_transaction();
3248        test_tx.status = TransactionStatus::Submitted;
3249        test_tx.sent_at = Some(Utc::now().to_rfc3339());
3250        let original_hash = "0xoriginal_hash".to_string();
3251        test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3252            nonce: Some(42),
3253            hash: Some(original_hash.clone()),
3254            raw: Some(vec![1, 2, 3]),
3255            ..test_tx.network_data.get_evm_transaction_data().unwrap()
3256        });
3257        test_tx.hashes = vec![original_hash.clone()];
3258
3259        // Price calculator returns bumped price
3260        mock_price_calculator
3261            .expect_calculate_bumped_gas_price()
3262            .times(1)
3263            .returning(|_, _, _| {
3264                Ok(PriceParams {
3265                    gas_price: Some(25000000000), // 25% bump
3266                    max_fee_per_gas: None,
3267                    max_priority_fee_per_gas: None,
3268                    is_min_bumped: Some(true),
3269                    extra_fee: None,
3270                    total_cost: U256::from(525000000000000u64),
3271                })
3272            });
3273
3274        // Balance check passes
3275        mock_provider
3276            .expect_get_balance()
3277            .times(1)
3278            .returning(|_| Box::pin(async { Ok(U256::from(1000000000000000000u64)) }));
3279
3280        // Signer creates new transaction with new hash
3281        mock_signer
3282            .expect_sign_transaction()
3283            .times(1)
3284            .returning(|_| {
3285                Box::pin(ready(Ok(
3286                    crate::domain::relayer::SignTransactionResponse::Evm(
3287                        crate::domain::relayer::SignTransactionResponseEvm {
3288                            hash: "0xnew_hash_that_should_not_be_saved".to_string(),
3289                            signature: crate::models::EvmTransactionDataSignature {
3290                                r: "r".to_string(),
3291                                s: "s".to_string(),
3292                                v: 1,
3293                                sig: "0xsignature".to_string(),
3294                            },
3295                            raw: vec![4, 5, 6],
3296                        },
3297                    ),
3298                )))
3299            });
3300
3301        // Provider returns "already known" - transaction is already in mempool
3302        mock_provider
3303            .expect_send_raw_transaction()
3304            .times(1)
3305            .returning(|_| {
3306                Box::pin(async {
3307                    Err(crate::services::provider::ProviderError::Other(
3308                        "already known: transaction with same nonce already in mempool".to_string(),
3309                    ))
3310                })
3311            });
3312
3313        // Verify that partial_update is called with NO network_data (preserving original hash)
3314        let test_tx_clone = test_tx.clone();
3315        mock_transaction
3316            .expect_partial_update()
3317            .times(1)
3318            .withf(|_, update| {
3319                // Should only update status, NOT network_data or hashes
3320                update.status == Some(TransactionStatus::Submitted)
3321                    && update.network_data.is_none()
3322                    && update.hashes.is_none()
3323            })
3324            .returning(move |_, _| {
3325                let mut updated_tx = test_tx_clone.clone();
3326                updated_tx.status = TransactionStatus::Submitted;
3327                // Hash should remain unchanged!
3328                Ok(updated_tx)
3329            });
3330
3331        let evm_transaction = EvmRelayerTransaction {
3332            relayer: relayer.clone(),
3333            provider: mock_provider,
3334            relayer_repository: Arc::new(mock_relayer),
3335            network_repository: Arc::new(mock_network),
3336            transaction_repository: Arc::new(mock_transaction),
3337            transaction_counter_service: Arc::new(counter_service),
3338            job_producer: Arc::new(mock_job_producer),
3339            price_calculator: mock_price_calculator,
3340            signer: mock_signer,
3341        };
3342
3343        let result = evm_transaction.resubmit_transaction(test_tx.clone()).await;
3344        assert!(result.is_ok());
3345        let updated_tx = result.unwrap();
3346
3347        // Verify hash was NOT changed
3348        if let NetworkTransactionData::Evm(evm_data) = &updated_tx.network_data {
3349            assert_eq!(evm_data.hash, Some(original_hash));
3350        } else {
3351            panic!("Expected EVM network data");
3352        }
3353    }
3354
3355    /// Test submit_transaction with database update failure
3356    /// Transaction is on-chain, but DB update fails - should return Ok with original tx
3357    #[tokio::test]
3358    async fn test_submit_transaction_db_failure_after_blockchain_success() {
3359        let mut mock_transaction = MockTransactionRepository::new();
3360        let mock_relayer = MockRelayerRepository::new();
3361        let mut mock_provider = MockEvmProviderTrait::new();
3362        let mock_signer = MockSigner::new();
3363        let mut mock_job_producer = MockJobProducerTrait::new();
3364        let mock_price_calculator = MockPriceCalculator::new();
3365        let counter_service = MockTransactionCounterTrait::new();
3366        let mock_network = MockNetworkRepository::new();
3367
3368        let relayer = create_test_relayer();
3369        let mut test_tx = create_test_transaction();
3370        test_tx.status = TransactionStatus::Sent;
3371        test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3372            raw: Some(vec![1, 2, 3]),
3373            ..test_tx.network_data.get_evm_transaction_data().unwrap()
3374        });
3375
3376        // Provider succeeds
3377        mock_provider
3378            .expect_send_raw_transaction()
3379            .times(1)
3380            .returning(|_| Box::pin(async { Ok("0xsubmitted_hash".to_string()) }));
3381
3382        // But database update fails
3383        mock_transaction
3384            .expect_partial_update()
3385            .times(1)
3386            .returning(|_, _| {
3387                Err(crate::models::RepositoryError::UnexpectedError(
3388                    "Redis timeout".to_string(),
3389                ))
3390            });
3391
3392        // Notification will still be sent (with original tx data)
3393        mock_job_producer
3394            .expect_produce_send_notification_job()
3395            .times(1)
3396            .returning(|_, _| Box::pin(ready(Ok(()))));
3397
3398        let evm_transaction = EvmRelayerTransaction {
3399            relayer: relayer.clone(),
3400            provider: mock_provider,
3401            relayer_repository: Arc::new(mock_relayer),
3402            network_repository: Arc::new(mock_network),
3403            transaction_repository: Arc::new(mock_transaction),
3404            transaction_counter_service: Arc::new(counter_service),
3405            job_producer: Arc::new(mock_job_producer),
3406            price_calculator: mock_price_calculator,
3407            signer: mock_signer,
3408        };
3409
3410        let result = evm_transaction.submit_transaction(test_tx.clone()).await;
3411        // Should return Ok (transaction is on-chain, don't retry)
3412        assert!(result.is_ok());
3413        let returned_tx = result.unwrap();
3414        // Should return original tx since DB update failed
3415        assert_eq!(returned_tx.id, test_tx.id);
3416        assert_eq!(returned_tx.status, TransactionStatus::Sent); // Original status
3417    }
3418
3419    /// Test send_transaction_resend_job success
3420    #[tokio::test]
3421    async fn test_send_transaction_resend_job_success() {
3422        let mock_transaction = MockTransactionRepository::new();
3423        let mock_relayer = MockRelayerRepository::new();
3424        let mock_provider = MockEvmProviderTrait::new();
3425        let mock_signer = MockSigner::new();
3426        let mut mock_job_producer = MockJobProducerTrait::new();
3427        let mock_price_calculator = MockPriceCalculator::new();
3428        let counter_service = MockTransactionCounterTrait::new();
3429        let mock_network = MockNetworkRepository::new();
3430
3431        let relayer = create_test_relayer();
3432        let test_tx = create_test_transaction();
3433
3434        // Expect produce_submit_transaction_job to be called with resend job
3435        mock_job_producer
3436            .expect_produce_submit_transaction_job()
3437            .times(1)
3438            .withf(|job, delay| {
3439                // Verify it's a resend job with correct IDs
3440                job.transaction_id == "test-tx-id"
3441                    && job.relayer_id == "test-relayer-id"
3442                    && matches!(job.command, crate::jobs::TransactionCommand::Resend)
3443                    && delay.is_none()
3444            })
3445            .returning(|_, _| Box::pin(ready(Ok(()))));
3446
3447        let evm_transaction = EvmRelayerTransaction {
3448            relayer: relayer.clone(),
3449            provider: mock_provider,
3450            relayer_repository: Arc::new(mock_relayer),
3451            network_repository: Arc::new(mock_network),
3452            transaction_repository: Arc::new(mock_transaction),
3453            transaction_counter_service: Arc::new(counter_service),
3454            job_producer: Arc::new(mock_job_producer),
3455            price_calculator: mock_price_calculator,
3456            signer: mock_signer,
3457        };
3458
3459        let result = evm_transaction.send_transaction_resend_job(&test_tx).await;
3460        assert!(result.is_ok());
3461    }
3462
3463    /// Test send_transaction_resend_job failure
3464    #[tokio::test]
3465    async fn test_send_transaction_resend_job_failure() {
3466        let mock_transaction = MockTransactionRepository::new();
3467        let mock_relayer = MockRelayerRepository::new();
3468        let mock_provider = MockEvmProviderTrait::new();
3469        let mock_signer = MockSigner::new();
3470        let mut mock_job_producer = MockJobProducerTrait::new();
3471        let mock_price_calculator = MockPriceCalculator::new();
3472        let counter_service = MockTransactionCounterTrait::new();
3473        let mock_network = MockNetworkRepository::new();
3474
3475        let relayer = create_test_relayer();
3476        let test_tx = create_test_transaction();
3477
3478        // Job producer returns an error
3479        mock_job_producer
3480            .expect_produce_submit_transaction_job()
3481            .times(1)
3482            .returning(|_, _| {
3483                Box::pin(ready(Err(crate::jobs::JobProducerError::QueueError(
3484                    "Job queue is full".to_string(),
3485                ))))
3486            });
3487
3488        let evm_transaction = EvmRelayerTransaction {
3489            relayer: relayer.clone(),
3490            provider: mock_provider,
3491            relayer_repository: Arc::new(mock_relayer),
3492            network_repository: Arc::new(mock_network),
3493            transaction_repository: Arc::new(mock_transaction),
3494            transaction_counter_service: Arc::new(counter_service),
3495            job_producer: Arc::new(mock_job_producer),
3496            price_calculator: mock_price_calculator,
3497            signer: mock_signer,
3498        };
3499
3500        let result = evm_transaction.send_transaction_resend_job(&test_tx).await;
3501        assert!(result.is_err());
3502        let err = result.unwrap_err();
3503        match err {
3504            TransactionError::UnexpectedError(msg) => {
3505                assert!(msg.contains("Failed to produce resend job"));
3506            }
3507            _ => panic!("Expected UnexpectedError"),
3508        }
3509    }
3510
3511    /// Test send_transaction_request_job success
3512    #[tokio::test]
3513    async fn test_send_transaction_request_job_success() {
3514        let mock_transaction = MockTransactionRepository::new();
3515        let mock_relayer = MockRelayerRepository::new();
3516        let mock_provider = MockEvmProviderTrait::new();
3517        let mock_signer = MockSigner::new();
3518        let mut mock_job_producer = MockJobProducerTrait::new();
3519        let mock_price_calculator = MockPriceCalculator::new();
3520        let counter_service = MockTransactionCounterTrait::new();
3521        let mock_network = MockNetworkRepository::new();
3522
3523        let relayer = create_test_relayer();
3524        let test_tx = create_test_transaction();
3525
3526        // Expect produce_transaction_request_job to be called
3527        mock_job_producer
3528            .expect_produce_transaction_request_job()
3529            .times(1)
3530            .withf(|job, delay| {
3531                // Verify correct transaction ID and relayer ID
3532                job.transaction_id == "test-tx-id"
3533                    && job.relayer_id == "test-relayer-id"
3534                    && delay.is_none()
3535            })
3536            .returning(|_, _| Box::pin(ready(Ok(()))));
3537
3538        let evm_transaction = EvmRelayerTransaction {
3539            relayer: relayer.clone(),
3540            provider: mock_provider,
3541            relayer_repository: Arc::new(mock_relayer),
3542            network_repository: Arc::new(mock_network),
3543            transaction_repository: Arc::new(mock_transaction),
3544            transaction_counter_service: Arc::new(counter_service),
3545            job_producer: Arc::new(mock_job_producer),
3546            price_calculator: mock_price_calculator,
3547            signer: mock_signer,
3548        };
3549
3550        let result = evm_transaction.send_transaction_request_job(&test_tx).await;
3551        assert!(result.is_ok());
3552    }
3553
3554    /// Test send_transaction_request_job failure
3555    #[tokio::test]
3556    async fn test_send_transaction_request_job_failure() {
3557        let mock_transaction = MockTransactionRepository::new();
3558        let mock_relayer = MockRelayerRepository::new();
3559        let mock_provider = MockEvmProviderTrait::new();
3560        let mock_signer = MockSigner::new();
3561        let mut mock_job_producer = MockJobProducerTrait::new();
3562        let mock_price_calculator = MockPriceCalculator::new();
3563        let counter_service = MockTransactionCounterTrait::new();
3564        let mock_network = MockNetworkRepository::new();
3565
3566        let relayer = create_test_relayer();
3567        let test_tx = create_test_transaction();
3568
3569        // Job producer returns an error
3570        mock_job_producer
3571            .expect_produce_transaction_request_job()
3572            .times(1)
3573            .returning(|_, _| {
3574                Box::pin(ready(Err(crate::jobs::JobProducerError::QueueError(
3575                    "Redis connection failed".to_string(),
3576                ))))
3577            });
3578
3579        let evm_transaction = EvmRelayerTransaction {
3580            relayer: relayer.clone(),
3581            provider: mock_provider,
3582            relayer_repository: Arc::new(mock_relayer),
3583            network_repository: Arc::new(mock_network),
3584            transaction_repository: Arc::new(mock_transaction),
3585            transaction_counter_service: Arc::new(counter_service),
3586            job_producer: Arc::new(mock_job_producer),
3587            price_calculator: mock_price_calculator,
3588            signer: mock_signer,
3589        };
3590
3591        let result = evm_transaction.send_transaction_request_job(&test_tx).await;
3592        assert!(result.is_err());
3593        let err = result.unwrap_err();
3594        match err {
3595            TransactionError::UnexpectedError(msg) => {
3596                assert!(msg.contains("Failed to produce request job"));
3597            }
3598            _ => panic!("Expected UnexpectedError"),
3599        }
3600    }
3601
3602    /// Test resubmit_transaction successfully transitions from Sent to Submitted status
3603    #[tokio::test]
3604    async fn test_resubmit_transaction_sent_to_submitted() {
3605        let mut mock_transaction = MockTransactionRepository::new();
3606        let mock_relayer = MockRelayerRepository::new();
3607        let mut mock_provider = MockEvmProviderTrait::new();
3608        let mut mock_signer = MockSigner::new();
3609        let mock_job_producer = MockJobProducerTrait::new();
3610        let mut mock_price_calculator = MockPriceCalculator::new();
3611        let counter_service = MockTransactionCounterTrait::new();
3612        let mock_network = MockNetworkRepository::new();
3613
3614        let relayer = create_test_relayer();
3615        let mut test_tx = create_test_transaction();
3616        test_tx.status = TransactionStatus::Sent;
3617        test_tx.sent_at = Some(Utc::now().to_rfc3339());
3618        let original_hash = "0xoriginal_hash".to_string();
3619        test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3620            nonce: Some(42),
3621            hash: Some(original_hash.clone()),
3622            raw: Some(vec![1, 2, 3]),
3623            gas_price: Some(20000000000), // 20 Gwei
3624            ..test_tx.network_data.get_evm_transaction_data().unwrap()
3625        });
3626        test_tx.hashes = vec![original_hash.clone()];
3627
3628        // Price calculator returns bumped price
3629        mock_price_calculator
3630            .expect_calculate_bumped_gas_price()
3631            .times(1)
3632            .returning(|_, _, _| {
3633                Ok(PriceParams {
3634                    gas_price: Some(25000000000), // 25 Gwei (25% bump)
3635                    max_fee_per_gas: None,
3636                    max_priority_fee_per_gas: None,
3637                    is_min_bumped: Some(true),
3638                    extra_fee: None,
3639                    total_cost: U256::from(525000000000000u64),
3640                })
3641            });
3642
3643        // Mock balance check
3644        mock_provider
3645            .expect_get_balance()
3646            .returning(|_| Box::pin(ready(Ok(U256::from(1000000000000000000u64)))));
3647
3648        // Mock signer to return new signed transaction
3649        mock_signer.expect_sign_transaction().returning(|_| {
3650            Box::pin(ready(Ok(
3651                crate::domain::relayer::SignTransactionResponse::Evm(
3652                    crate::domain::relayer::SignTransactionResponseEvm {
3653                        hash: "0xnew_hash".to_string(),
3654                        signature: crate::models::EvmTransactionDataSignature {
3655                            r: "r".to_string(),
3656                            s: "s".to_string(),
3657                            v: 1,
3658                            sig: "0xsignature".to_string(),
3659                        },
3660                        raw: vec![4, 5, 6],
3661                    },
3662                ),
3663            )))
3664        });
3665
3666        // Provider successfully sends the resubmitted transaction
3667        mock_provider
3668            .expect_send_raw_transaction()
3669            .times(1)
3670            .returning(|_| Box::pin(async { Ok("0xnew_hash".to_string()) }));
3671
3672        // Should update to Submitted status with new hash
3673        let test_tx_clone = test_tx.clone();
3674        mock_transaction
3675            .expect_partial_update()
3676            .times(1)
3677            .withf(|_, update| {
3678                update.status == Some(TransactionStatus::Submitted)
3679                    && update.sent_at.is_some()
3680                    && update.priced_at.is_some()
3681                    && update.hashes.is_some()
3682            })
3683            .returning(move |_, update| {
3684                let mut updated_tx = test_tx_clone.clone();
3685                updated_tx.status = update.status.unwrap();
3686                updated_tx.sent_at = update.sent_at.clone();
3687                updated_tx.priced_at = update.priced_at.clone();
3688                if let Some(hashes) = update.hashes.clone() {
3689                    updated_tx.hashes = hashes;
3690                }
3691                if let Some(network_data) = update.network_data.clone() {
3692                    updated_tx.network_data = network_data;
3693                }
3694                Ok(updated_tx)
3695            });
3696
3697        let evm_transaction = EvmRelayerTransaction {
3698            relayer: relayer.clone(),
3699            provider: mock_provider,
3700            relayer_repository: Arc::new(mock_relayer),
3701            network_repository: Arc::new(mock_network),
3702            transaction_repository: Arc::new(mock_transaction),
3703            transaction_counter_service: Arc::new(counter_service),
3704            job_producer: Arc::new(mock_job_producer),
3705            price_calculator: mock_price_calculator,
3706            signer: mock_signer,
3707        };
3708
3709        let result = evm_transaction.resubmit_transaction(test_tx.clone()).await;
3710        assert!(result.is_ok(), "Expected Ok, got: {result:?}");
3711        let updated_tx = result.unwrap();
3712        assert_eq!(
3713            updated_tx.status,
3714            TransactionStatus::Submitted,
3715            "Transaction status should transition from Sent to Submitted"
3716        );
3717    }
3718
3719    #[test]
3720    fn test_classify_submission_error_nonce_too_low() {
3721        assert_eq!(
3722            DefaultEvmTransaction::classify_submission_error(&"nonce too low"),
3723            SubmissionErrorKind::NonceTooLow
3724        );
3725        assert_eq!(
3726            DefaultEvmTransaction::classify_submission_error(&"Nonce Too Low"),
3727            SubmissionErrorKind::NonceTooLow
3728        );
3729        assert_eq!(
3730            DefaultEvmTransaction::classify_submission_error(&"nonce is too low"),
3731            SubmissionErrorKind::NonceTooLow
3732        );
3733    }
3734
3735    #[test]
3736    fn test_classify_submission_error_already_known() {
3737        assert_eq!(
3738            DefaultEvmTransaction::classify_submission_error(&"already known"),
3739            SubmissionErrorKind::AlreadyKnown
3740        );
3741        assert_eq!(
3742            DefaultEvmTransaction::classify_submission_error(&"known transaction"),
3743            SubmissionErrorKind::AlreadyKnown
3744        );
3745        assert_eq!(
3746            DefaultEvmTransaction::classify_submission_error(&"same hash was already imported"),
3747            SubmissionErrorKind::AlreadyKnown
3748        );
3749        // "unknown transaction" must NOT match
3750        assert!(matches!(
3751            DefaultEvmTransaction::classify_submission_error(&"unknown transaction"),
3752            SubmissionErrorKind::Other(_)
3753        ));
3754    }
3755
3756    #[test]
3757    fn test_classify_submission_error_replacement_underpriced() {
3758        assert_eq!(
3759            DefaultEvmTransaction::classify_submission_error(
3760                &"replacement transaction underpriced"
3761            ),
3762            SubmissionErrorKind::ReplacementUnderpriced
3763        );
3764    }
3765
3766    #[test]
3767    fn test_classify_submission_error_other() {
3768        assert!(matches!(
3769            DefaultEvmTransaction::classify_submission_error(&"execution reverted"),
3770            SubmissionErrorKind::Other(_)
3771        ));
3772        assert!(matches!(
3773            DefaultEvmTransaction::classify_submission_error(&"gas too low"),
3774            SubmissionErrorKind::Other(_)
3775        ));
3776        // insufficient funds is not a nonce-related error — maps to Other
3777        assert!(matches!(
3778            DefaultEvmTransaction::classify_submission_error(
3779                &"insufficient funds for gas * price + value"
3780            ),
3781            SubmissionErrorKind::Other(_)
3782        ));
3783    }
3784
3785    #[test]
3786    fn test_classify_submission_error_nonce_too_high() {
3787        assert_eq!(
3788            DefaultEvmTransaction::classify_submission_error(&"nonce too high"),
3789            SubmissionErrorKind::NonceTooHigh
3790        );
3791        assert_eq!(
3792            DefaultEvmTransaction::classify_submission_error(&"exceeds next nonce"),
3793            SubmissionErrorKind::NonceTooHigh
3794        );
3795        assert_eq!(
3796            DefaultEvmTransaction::classify_submission_error(&"nonce too far in the future"),
3797            SubmissionErrorKind::NonceTooHigh
3798        );
3799        assert_eq!(
3800            DefaultEvmTransaction::classify_submission_error(&"nonce out of range"),
3801            SubmissionErrorKind::NonceTooHigh
3802        );
3803    }
3804
3805    #[test]
3806    fn test_classify_submission_error_nonce_too_high_case_insensitive() {
3807        assert_eq!(
3808            DefaultEvmTransaction::classify_submission_error(&"Nonce Too High"),
3809            SubmissionErrorKind::NonceTooHigh
3810        );
3811        assert_eq!(
3812            DefaultEvmTransaction::classify_submission_error(&"NONCE OUT OF RANGE"),
3813            SubmissionErrorKind::NonceTooHigh
3814        );
3815        assert_eq!(
3816            DefaultEvmTransaction::classify_submission_error(&"Exceeds Next Nonce"),
3817            SubmissionErrorKind::NonceTooHigh
3818        );
3819    }
3820
3821    /// Test submit_transaction with NonceTooLow on non-Sent (Submitted) tx
3822    /// Should return Ok and schedule nonce recovery status check
3823    #[tokio::test]
3824    async fn test_submit_transaction_nonce_too_low_on_submitted_schedules_recovery() {
3825        let mut mock_transaction = MockTransactionRepository::new();
3826        let mock_relayer = MockRelayerRepository::new();
3827        let mut mock_provider = MockEvmProviderTrait::new();
3828        let mock_signer = MockSigner::new();
3829        let mut mock_job_producer = MockJobProducerTrait::new();
3830        let mock_price_calculator = MockPriceCalculator::new();
3831        let counter_service = MockTransactionCounterTrait::new();
3832        let mock_network = MockNetworkRepository::new();
3833
3834        let relayer = create_test_relayer();
3835        let mut test_tx = create_test_transaction();
3836        test_tx.status = TransactionStatus::Submitted;
3837        test_tx.sent_at = Some(Utc::now().to_rfc3339());
3838        test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3839            nonce: Some(42),
3840            hash: Some("0xhash".to_string()),
3841            raw: Some(vec![1, 2, 3]),
3842            ..test_tx.network_data.get_evm_transaction_data().unwrap()
3843        });
3844
3845        // Provider returns "nonce too low" error
3846        mock_provider
3847            .expect_send_raw_transaction()
3848            .times(1)
3849            .returning(|_| {
3850                Box::pin(async {
3851                    Err(crate::services::provider::ProviderError::Other(
3852                        "nonce too low".to_string(),
3853                    ))
3854                })
3855            });
3856
3857        // Should persist status_reason
3858        let test_tx_clone = test_tx.clone();
3859        mock_transaction
3860            .expect_partial_update()
3861            .times(1)
3862            .withf(|_, update| update.status_reason.is_some())
3863            .returning(move |_, _| Ok(test_tx_clone.clone()));
3864
3865        // Should schedule nonce recovery status check
3866        mock_job_producer
3867            .expect_produce_check_transaction_status_job()
3868            .times(1)
3869            .withf(|job, _| {
3870                job.metadata
3871                    .as_ref()
3872                    .map(|m| m.contains_key(TX_NONCE_RECONCILE_TRIGGER))
3873                    .unwrap_or(false)
3874            })
3875            .returning(|_, _| Box::pin(ready(Ok(()))));
3876
3877        let evm_transaction = EvmRelayerTransaction {
3878            relayer: relayer.clone(),
3879            provider: mock_provider,
3880            relayer_repository: Arc::new(mock_relayer),
3881            network_repository: Arc::new(mock_network),
3882            transaction_repository: Arc::new(mock_transaction),
3883            transaction_counter_service: Arc::new(counter_service),
3884            job_producer: Arc::new(mock_job_producer),
3885            price_calculator: mock_price_calculator,
3886            signer: mock_signer,
3887        };
3888
3889        // Should return Ok (not error → Dead Queue)
3890        let result = evm_transaction.submit_transaction(test_tx).await;
3891        assert!(
3892            result.is_ok(),
3893            "Expected Ok on nonce error for non-Sent tx, got: {result:?}"
3894        );
3895    }
3896
3897    /// Test submit_transaction with NonceTooLow on Sent tx schedules nonce recovery.
3898    /// NonceTooLow means the nonce was consumed, but we don't know by whom — could be
3899    /// our tx (retry after crash) or a different tx (multi-instance / external wallet).
3900    /// Must NOT blindly advance to Submitted; instead schedule reconciliation.
3901    #[tokio::test]
3902    async fn test_submit_transaction_nonce_too_low_on_sent_schedules_recovery() {
3903        let mut mock_transaction = MockTransactionRepository::new();
3904        let mock_relayer = MockRelayerRepository::new();
3905        let mut mock_provider = MockEvmProviderTrait::new();
3906        let mock_signer = MockSigner::new();
3907        let mut mock_job_producer = MockJobProducerTrait::new();
3908        let mock_price_calculator = MockPriceCalculator::new();
3909        let counter_service = MockTransactionCounterTrait::new();
3910        let mock_network = MockNetworkRepository::new();
3911
3912        let relayer = create_test_relayer();
3913        let mut test_tx = create_test_transaction();
3914        test_tx.status = TransactionStatus::Sent;
3915        test_tx.sent_at = Some(Utc::now().to_rfc3339());
3916        test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3917            nonce: Some(42),
3918            hash: Some("0xhash".to_string()),
3919            raw: Some(vec![1, 2, 3]),
3920            ..test_tx.network_data.get_evm_transaction_data().unwrap()
3921        });
3922
3923        // Provider returns "nonce too low" error
3924        mock_provider
3925            .expect_send_raw_transaction()
3926            .times(1)
3927            .returning(|_| {
3928                Box::pin(async {
3929                    Err(crate::services::provider::ProviderError::Other(
3930                        "nonce too low".to_string(),
3931                    ))
3932                })
3933            });
3934
3935        // Should persist status_reason (nonce recovery path)
3936        let test_tx_clone = test_tx.clone();
3937        mock_transaction
3938            .expect_partial_update()
3939            .times(1)
3940            .withf(|_, update| update.status_reason.is_some())
3941            .returning(move |_, _| Ok(test_tx_clone.clone()));
3942
3943        // Should schedule nonce recovery status check
3944        mock_job_producer
3945            .expect_produce_check_transaction_status_job()
3946            .times(1)
3947            .withf(|job, _| {
3948                job.metadata
3949                    .as_ref()
3950                    .map(|m| m.contains_key(TX_NONCE_RECONCILE_TRIGGER))
3951                    .unwrap_or(false)
3952            })
3953            .returning(|_, _| Box::pin(ready(Ok(()))));
3954
3955        let evm_transaction = EvmRelayerTransaction {
3956            relayer: relayer.clone(),
3957            provider: mock_provider,
3958            relayer_repository: Arc::new(mock_relayer),
3959            network_repository: Arc::new(mock_network),
3960            transaction_repository: Arc::new(mock_transaction),
3961            transaction_counter_service: Arc::new(counter_service),
3962            job_producer: Arc::new(mock_job_producer),
3963            price_calculator: mock_price_calculator,
3964            signer: mock_signer,
3965        };
3966
3967        // Should return Ok (not error → Dead Queue) but NOT advance to Submitted
3968        let result = evm_transaction.submit_transaction(test_tx.clone()).await;
3969        assert!(
3970            result.is_ok(),
3971            "Expected Ok on nonce too low for Sent tx, got: {result:?}"
3972        );
3973        // Status should remain Sent — reconciliation happens via status checker
3974        let returned_tx = result.unwrap();
3975        assert_eq!(returned_tx.status, TransactionStatus::Sent);
3976    }
3977
3978    /// Test resubmit_transaction with NonceTooLow schedules recovery and treats as already submitted
3979    #[tokio::test]
3980    async fn test_resubmit_transaction_nonce_too_low_schedules_recovery() {
3981        let mut mock_transaction = MockTransactionRepository::new();
3982        let mock_relayer = MockRelayerRepository::new();
3983        let mut mock_provider = MockEvmProviderTrait::new();
3984        let mut mock_signer = MockSigner::new();
3985        let mut mock_job_producer = MockJobProducerTrait::new();
3986        let mut mock_price_calculator = MockPriceCalculator::new();
3987        let counter_service = MockTransactionCounterTrait::new();
3988        let mock_network = MockNetworkRepository::new();
3989
3990        let relayer = create_test_relayer();
3991        let mut test_tx = create_test_transaction();
3992        test_tx.status = TransactionStatus::Submitted;
3993        test_tx.sent_at = Some(Utc::now().to_rfc3339());
3994        let original_hash = "0xoriginal_hash".to_string();
3995        test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3996            nonce: Some(42),
3997            hash: Some(original_hash.clone()),
3998            raw: Some(vec![1, 2, 3]),
3999            ..test_tx.network_data.get_evm_transaction_data().unwrap()
4000        });
4001        test_tx.hashes = vec![original_hash.clone()];
4002
4003        // Price calculator returns bumped price
4004        mock_price_calculator
4005            .expect_calculate_bumped_gas_price()
4006            .times(1)
4007            .returning(|_, _, _| {
4008                Ok(PriceParams {
4009                    gas_price: Some(25000000000),
4010                    max_fee_per_gas: None,
4011                    max_priority_fee_per_gas: None,
4012                    is_min_bumped: Some(true),
4013                    extra_fee: None,
4014                    total_cost: U256::from(525000000000000u64),
4015                })
4016            });
4017
4018        // Balance check passes
4019        mock_provider
4020            .expect_get_balance()
4021            .times(1)
4022            .returning(|_| Box::pin(async { Ok(U256::from(1000000000000000000u64)) }));
4023
4024        // Signer creates new transaction
4025        mock_signer
4026            .expect_sign_transaction()
4027            .times(1)
4028            .returning(|_| {
4029                Box::pin(ready(Ok(
4030                    crate::domain::relayer::SignTransactionResponse::Evm(
4031                        crate::domain::relayer::SignTransactionResponseEvm {
4032                            hash: "0xnew_hash".to_string(),
4033                            signature: crate::models::EvmTransactionDataSignature {
4034                                r: "r".to_string(),
4035                                s: "s".to_string(),
4036                                v: 1,
4037                                sig: "0xsignature".to_string(),
4038                            },
4039                            raw: vec![4, 5, 6],
4040                        },
4041                    ),
4042                )))
4043            });
4044
4045        // Provider returns "nonce too low"
4046        mock_provider
4047            .expect_send_raw_transaction()
4048            .times(1)
4049            .returning(|_| {
4050                Box::pin(async {
4051                    Err(crate::services::provider::ProviderError::Other(
4052                        "nonce too low".to_string(),
4053                    ))
4054                })
4055            });
4056
4057        // Should schedule nonce recovery status check
4058        mock_job_producer
4059            .expect_produce_check_transaction_status_job()
4060            .times(1)
4061            .withf(|job, _| {
4062                job.metadata
4063                    .as_ref()
4064                    .map(|m| m.contains_key(TX_NONCE_RECONCILE_TRIGGER))
4065                    .unwrap_or(false)
4066            })
4067            .returning(|_, _| Box::pin(ready(Ok(()))));
4068
4069        // Should update status without changing hash (was_already_submitted = true)
4070        let test_tx_clone = test_tx.clone();
4071        mock_transaction
4072            .expect_partial_update()
4073            .times(1)
4074            .withf(|_, update| {
4075                update.status == Some(TransactionStatus::Submitted)
4076                    && update.network_data.is_none()
4077                    && update.hashes.is_none()
4078            })
4079            .returning(move |_, _| {
4080                let mut updated_tx = test_tx_clone.clone();
4081                updated_tx.status = TransactionStatus::Submitted;
4082                Ok(updated_tx)
4083            });
4084
4085        let evm_transaction = EvmRelayerTransaction {
4086            relayer: relayer.clone(),
4087            provider: mock_provider,
4088            relayer_repository: Arc::new(mock_relayer),
4089            network_repository: Arc::new(mock_network),
4090            transaction_repository: Arc::new(mock_transaction),
4091            transaction_counter_service: Arc::new(counter_service),
4092            job_producer: Arc::new(mock_job_producer),
4093            price_calculator: mock_price_calculator,
4094            signer: mock_signer,
4095        };
4096
4097        let result = evm_transaction.resubmit_transaction(test_tx.clone()).await;
4098        assert!(result.is_ok());
4099        let updated_tx = result.unwrap();
4100        // Hash should remain unchanged
4101        if let NetworkTransactionData::Evm(evm_data) = &updated_tx.network_data {
4102            assert_eq!(evm_data.hash, Some(original_hash));
4103        } else {
4104            panic!("Expected EVM network data");
4105        }
4106    }
4107
4108    /// Test submit_transaction with NonceTooHigh increments the retry counter in metadata
4109    #[tokio::test]
4110    async fn test_submit_transaction_nonce_too_high_increments_retry_counter() {
4111        let mut mock_transaction = MockTransactionRepository::new();
4112        let mock_relayer = MockRelayerRepository::new();
4113        let mut mock_provider = MockEvmProviderTrait::new();
4114        let mock_signer = MockSigner::new();
4115        let mock_job_producer = MockJobProducerTrait::new();
4116        let mock_price_calculator = MockPriceCalculator::new();
4117        let counter_service = MockTransactionCounterTrait::new();
4118        let mock_network = MockNetworkRepository::new();
4119
4120        let relayer = create_test_relayer();
4121        let mut test_tx = create_test_transaction();
4122        test_tx.status = TransactionStatus::Sent;
4123        test_tx.sent_at = Some(Utc::now().to_rfc3339());
4124        test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
4125            nonce: Some(10),
4126            hash: Some("0xhash".to_string()),
4127            raw: Some(vec![1, 2, 3]),
4128            ..test_tx.network_data.get_evm_transaction_data().unwrap()
4129        });
4130        // Start with nonce_too_high_retries = 0 (below threshold of 3)
4131        test_tx.metadata = Some(crate::models::TransactionMetadata {
4132            nonce_too_high_retries: 0,
4133            ..Default::default()
4134        });
4135
4136        // Provider returns "nonce too high" error
4137        mock_provider
4138            .expect_send_raw_transaction()
4139            .times(1)
4140            .returning(|_| {
4141                Box::pin(async {
4142                    Err(crate::services::provider::ProviderError::Other(
4143                        "nonce too high".to_string(),
4144                    ))
4145                })
4146            });
4147
4148        // Should persist incremented counter (nonce_too_high_retries = 1) in metadata
4149        let test_tx_clone = test_tx.clone();
4150        mock_transaction
4151            .expect_partial_update()
4152            .times(1)
4153            .withf(|_, update| {
4154                update
4155                    .metadata
4156                    .as_ref()
4157                    .map(|m| m.nonce_too_high_retries == 1)
4158                    .unwrap_or(false)
4159            })
4160            .returning(move |_, _| Ok(test_tx_clone.clone()));
4161
4162        let evm_transaction = EvmRelayerTransaction {
4163            relayer: relayer.clone(),
4164            provider: mock_provider,
4165            relayer_repository: Arc::new(mock_relayer),
4166            network_repository: Arc::new(mock_network),
4167            transaction_repository: Arc::new(mock_transaction),
4168            transaction_counter_service: Arc::new(counter_service),
4169            job_producer: Arc::new(mock_job_producer),
4170            price_calculator: mock_price_calculator,
4171            signer: mock_signer,
4172        };
4173
4174        // Should return Ok (not error → Dead Queue)
4175        let result = evm_transaction.submit_transaction(test_tx).await;
4176        assert!(
4177            result.is_ok(),
4178            "Expected Ok on nonce too high, got: {result:?}"
4179        );
4180    }
4181
4182    /// Test submit_transaction with NonceTooHigh schedules health check job at threshold
4183    /// When nonce_too_high_retries reaches MAX_NONCE_TOO_HIGH_RETRIES (3), a health job is produced
4184    #[tokio::test]
4185    async fn test_submit_transaction_nonce_too_high_schedules_health_job_at_threshold() {
4186        let mut mock_transaction = MockTransactionRepository::new();
4187        let mock_relayer = MockRelayerRepository::new();
4188        let mut mock_provider = MockEvmProviderTrait::new();
4189        let mock_signer = MockSigner::new();
4190        let mut mock_job_producer = MockJobProducerTrait::new();
4191        let mock_price_calculator = MockPriceCalculator::new();
4192        let counter_service = MockTransactionCounterTrait::new();
4193        let mock_network = MockNetworkRepository::new();
4194
4195        let relayer = create_test_relayer();
4196        let mut test_tx = create_test_transaction();
4197        test_tx.status = TransactionStatus::Sent;
4198        test_tx.sent_at = Some(Utc::now().to_rfc3339());
4199        test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
4200            nonce: Some(10),
4201            hash: Some("0xhash".to_string()),
4202            raw: Some(vec![1, 2, 3]),
4203            ..test_tx.network_data.get_evm_transaction_data().unwrap()
4204        });
4205        // Set retries to 2 so that after increment it becomes 3 = MAX_NONCE_TOO_HIGH_RETRIES
4206        test_tx.metadata = Some(crate::models::TransactionMetadata {
4207            nonce_too_high_retries: 2,
4208            ..Default::default()
4209        });
4210
4211        // Provider returns "nonce too high" error
4212        mock_provider
4213            .expect_send_raw_transaction()
4214            .times(1)
4215            .returning(|_| {
4216                Box::pin(async {
4217                    Err(crate::services::provider::ProviderError::Other(
4218                        "nonce too high".to_string(),
4219                    ))
4220                })
4221            });
4222
4223        // Should persist incremented counter (nonce_too_high_retries = 3) in metadata
4224        let test_tx_clone = test_tx.clone();
4225        mock_transaction
4226            .expect_partial_update()
4227            .times(1)
4228            .withf(|_, update| {
4229                update
4230                    .metadata
4231                    .as_ref()
4232                    .map(|m| m.nonce_too_high_retries == 3)
4233                    .unwrap_or(false)
4234            })
4235            .returning(move |_, _| Ok(test_tx_clone.clone()));
4236
4237        // Should schedule a relayer health check job at the threshold
4238        mock_job_producer
4239            .expect_produce_relayer_health_check_job()
4240            .times(1)
4241            .returning(|_, _| Box::pin(ready(Ok(()))));
4242
4243        let evm_transaction = EvmRelayerTransaction {
4244            relayer: relayer.clone(),
4245            provider: mock_provider,
4246            relayer_repository: Arc::new(mock_relayer),
4247            network_repository: Arc::new(mock_network),
4248            transaction_repository: Arc::new(mock_transaction),
4249            transaction_counter_service: Arc::new(counter_service),
4250            job_producer: Arc::new(mock_job_producer),
4251            price_calculator: mock_price_calculator,
4252            signer: mock_signer,
4253        };
4254
4255        // Should return Ok (not error → Dead Queue)
4256        let result = evm_transaction.submit_transaction(test_tx).await;
4257        assert!(
4258            result.is_ok(),
4259            "Expected Ok on nonce too high at threshold, got: {result:?}"
4260        );
4261    }
4262
4263    /// Test resubmit_transaction with NonceTooHigh returns Ok without changing status
4264    #[tokio::test]
4265    async fn test_resubmit_transaction_nonce_too_high_returns_ok() {
4266        let mut mock_transaction = MockTransactionRepository::new();
4267        let mock_relayer = MockRelayerRepository::new();
4268        let mut mock_provider = MockEvmProviderTrait::new();
4269        let mut mock_signer = MockSigner::new();
4270        let mock_job_producer = MockJobProducerTrait::new();
4271        let mut mock_price_calculator = MockPriceCalculator::new();
4272        let counter_service = MockTransactionCounterTrait::new();
4273        let mock_network = MockNetworkRepository::new();
4274
4275        let relayer = create_test_relayer();
4276        let mut test_tx = create_test_transaction();
4277        test_tx.status = TransactionStatus::Submitted;
4278        test_tx.sent_at = Some(Utc::now().to_rfc3339());
4279        let original_hash = "0xoriginal_hash".to_string();
4280        test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
4281            nonce: Some(10),
4282            hash: Some(original_hash.clone()),
4283            raw: Some(vec![1, 2, 3]),
4284            ..test_tx.network_data.get_evm_transaction_data().unwrap()
4285        });
4286        test_tx.hashes = vec![original_hash.clone()];
4287        // Start with nonce_too_high_retries = 0 (below threshold)
4288        test_tx.metadata = Some(crate::models::TransactionMetadata {
4289            nonce_too_high_retries: 0,
4290            ..Default::default()
4291        });
4292
4293        // Price calculator returns bumped price
4294        mock_price_calculator
4295            .expect_calculate_bumped_gas_price()
4296            .times(1)
4297            .returning(|_, _, _| {
4298                Ok(PriceParams {
4299                    gas_price: Some(25000000000),
4300                    max_fee_per_gas: None,
4301                    max_priority_fee_per_gas: None,
4302                    is_min_bumped: Some(true),
4303                    extra_fee: None,
4304                    total_cost: U256::from(525000000000000u64),
4305                })
4306            });
4307
4308        // Balance check passes
4309        mock_provider
4310            .expect_get_balance()
4311            .times(1)
4312            .returning(|_| Box::pin(async { Ok(U256::from(1000000000000000000u64)) }));
4313
4314        // Signer creates new signed transaction
4315        mock_signer
4316            .expect_sign_transaction()
4317            .times(1)
4318            .returning(|_| {
4319                Box::pin(ready(Ok(
4320                    crate::domain::relayer::SignTransactionResponse::Evm(
4321                        crate::domain::relayer::SignTransactionResponseEvm {
4322                            hash: "0xnew_hash".to_string(),
4323                            signature: crate::models::EvmTransactionDataSignature {
4324                                r: "r".to_string(),
4325                                s: "s".to_string(),
4326                                v: 1,
4327                                sig: "0xsignature".to_string(),
4328                            },
4329                            raw: vec![4, 5, 6],
4330                        },
4331                    ),
4332                )))
4333            });
4334
4335        // Provider returns "nonce too high" error on send
4336        mock_provider
4337            .expect_send_raw_transaction()
4338            .times(1)
4339            .returning(|_| {
4340                Box::pin(async {
4341                    Err(crate::services::provider::ProviderError::Other(
4342                        "nonce too high".to_string(),
4343                    ))
4344                })
4345            });
4346
4347        // Should persist incremented counter (nonce_too_high_retries = 1) in metadata
4348        let test_tx_clone = test_tx.clone();
4349        mock_transaction
4350            .expect_partial_update()
4351            .times(1)
4352            .withf(|_, update| {
4353                update
4354                    .metadata
4355                    .as_ref()
4356                    .map(|m| m.nonce_too_high_retries == 1)
4357                    .unwrap_or(false)
4358            })
4359            .returning(move |_, _| Ok(test_tx_clone.clone()));
4360
4361        let evm_transaction = EvmRelayerTransaction {
4362            relayer: relayer.clone(),
4363            provider: mock_provider,
4364            relayer_repository: Arc::new(mock_relayer),
4365            network_repository: Arc::new(mock_network),
4366            transaction_repository: Arc::new(mock_transaction),
4367            transaction_counter_service: Arc::new(counter_service),
4368            job_producer: Arc::new(mock_job_producer),
4369            price_calculator: mock_price_calculator,
4370            signer: mock_signer,
4371        };
4372
4373        // Should return Ok without changing tx status
4374        let result = evm_transaction.resubmit_transaction(test_tx.clone()).await;
4375        assert!(
4376            result.is_ok(),
4377            "Expected Ok on nonce too high during resubmit, got: {result:?}"
4378        );
4379        let returned_tx = result.unwrap();
4380        // Status should remain Submitted (unchanged)
4381        assert_eq!(returned_tx.status, TransactionStatus::Submitted);
4382    }
4383}