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        // Build the NOOP replacement. The transaction keeps its active status
1264        // (Sent/Submitted) and is marked is_canceled=true, so it is still tracked,
1265        // resubmitted, and recovered by the normal status machinery until the NOOP
1266        // actually mines — at which point it transitions to Canceled (see status.rs).
1267        // The is_canceled flag excludes it from active-status API views immediately.
1268        let update = self
1269            .prepare_noop_update_request(
1270                &tx,
1271                true,
1272                Some("Transaction canceled by user, replacing with NOOP".to_string()),
1273            )
1274            .await?;
1275
1276        let updated_tx = self
1277            .transaction_repository()
1278            .partial_update(tx.id.clone(), update)
1279            .await?;
1280
1281        // Submit the updated transaction to the network using the resubmit job
1282        self.send_transaction_resubmit_job(&updated_tx).await?;
1283
1284        // Send notification for the updated transaction
1285        if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
1286            error!(
1287                tx_id = %updated_tx.id,
1288                status = ?updated_tx.status,
1289                "sending transaction update notification failed after cancel: {:?}",
1290                e
1291            );
1292        }
1293
1294        debug!("original transaction updated with cancellation data");
1295        Ok(updated_tx)
1296    }
1297
1298    /// Replaces a transaction with a new one.
1299    ///
1300    /// # Arguments
1301    ///
1302    /// * `old_tx` - The transaction model to replace.
1303    /// * `new_tx_request` - The new transaction request data.
1304    ///
1305    /// # Returns
1306    ///
1307    /// A result containing the updated transaction model or a `TransactionError`.
1308    async fn replace_transaction(
1309        &self,
1310        old_tx: TransactionRepoModel,
1311        new_tx_request: NetworkTransactionRequest,
1312    ) -> Result<TransactionRepoModel, TransactionError> {
1313        debug!("replacing transaction");
1314
1315        // Validate state: can only replace transactions that are still pending
1316        ensure_status_one_of(
1317            &old_tx,
1318            &[
1319                TransactionStatus::Pending,
1320                TransactionStatus::Sent,
1321                TransactionStatus::Submitted,
1322            ],
1323            Some("replace_transaction"),
1324        )?;
1325
1326        // Extract EVM data from both old transaction and new request
1327        let old_evm_data = old_tx.network_data.get_evm_transaction_data()?;
1328        let new_evm_request = match new_tx_request {
1329            NetworkTransactionRequest::Evm(evm_req) => evm_req,
1330            _ => {
1331                return Err(TransactionError::InvalidType(
1332                    "New transaction request must be EVM type".to_string(),
1333                ))
1334            }
1335        };
1336
1337        let network_repo_model = self
1338            .network_repository()
1339            .get_by_chain_id(NetworkType::Evm, old_evm_data.chain_id)
1340            .await
1341            .map_err(|e| {
1342                TransactionError::NetworkConfiguration(format!(
1343                    "Failed to get network by chain_id {}: {}",
1344                    old_evm_data.chain_id, e
1345                ))
1346            })?
1347            .ok_or_else(|| {
1348                TransactionError::NetworkConfiguration(format!(
1349                    "Network with chain_id {} not found",
1350                    old_evm_data.chain_id
1351                ))
1352            })?;
1353
1354        let network = EvmNetwork::try_from(network_repo_model).map_err(|e| {
1355            TransactionError::NetworkConfiguration(format!("Failed to convert network model: {e}"))
1356        })?;
1357
1358        // First, create updated EVM data without price parameters
1359        let updated_evm_data = EvmTransactionData::for_replacement(&old_evm_data, &new_evm_request);
1360
1361        // Then determine pricing strategy and calculate price parameters using the updated data
1362        let price_params = super::replacement::determine_replacement_pricing(
1363            &old_evm_data,
1364            &updated_evm_data,
1365            self.relayer(),
1366            &self.price_calculator,
1367            network.lacks_mempool(),
1368        )
1369        .await?;
1370
1371        debug!(price_params = ?price_params, "replacement price params");
1372
1373        // Apply the calculated price parameters to the updated EVM data
1374        let evm_data_with_price_params = updated_evm_data.with_price_params(price_params.clone());
1375
1376        // Validate the relayer has sufficient balance
1377        self.ensure_sufficient_balance(price_params.total_cost)
1378            .await?;
1379
1380        let sig_result = self
1381            .signer
1382            .sign_transaction(NetworkTransactionData::Evm(
1383                evm_data_with_price_params.clone(),
1384            ))
1385            .await?;
1386
1387        let final_evm_data =
1388            evm_data_with_price_params.with_signed_transaction_data(sig_result.into_evm()?);
1389
1390        // Update the transaction in the repository
1391        let updated_tx = self
1392            .transaction_repository
1393            .update_network_data(
1394                old_tx.id.clone(),
1395                NetworkTransactionData::Evm(final_evm_data),
1396            )
1397            .await?;
1398
1399        self.send_transaction_resubmit_job(&updated_tx).await?;
1400
1401        // Send notification
1402        if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
1403            error!(
1404                tx_id = %updated_tx.id,
1405                status = ?updated_tx.status,
1406                "sending transaction update notification failed after replace: {:?}",
1407                e
1408            );
1409        }
1410
1411        Ok(updated_tx)
1412    }
1413
1414    /// Signs a transaction.
1415    ///
1416    /// # Arguments
1417    ///
1418    /// * `tx` - The transaction model to sign.
1419    ///
1420    /// # Returns
1421    ///
1422    /// A result containing the transaction model or a `TransactionError`.
1423    async fn sign_transaction(
1424        &self,
1425        tx: TransactionRepoModel,
1426    ) -> Result<TransactionRepoModel, TransactionError> {
1427        Ok(tx)
1428    }
1429
1430    /// Validates a transaction.
1431    ///
1432    /// # Arguments
1433    ///
1434    /// * `_tx` - The transaction model to validate.
1435    ///
1436    /// # Returns
1437    ///
1438    /// A result containing a boolean indicating validity or a `TransactionError`.
1439    async fn validate_transaction(
1440        &self,
1441        _tx: TransactionRepoModel,
1442    ) -> Result<bool, TransactionError> {
1443        Ok(true)
1444    }
1445}
1446// P: EvmProviderTrait,
1447// R: Repository<RelayerRepoModel, String>,
1448// T: TransactionRepository,
1449// J: JobProducerTrait,
1450// S: Signer,
1451// C: TransactionCounterTrait,
1452// PC: PriceCalculatorTrait,
1453// we define concrete type for the evm transaction
1454pub type DefaultEvmTransaction = EvmRelayerTransaction<
1455    EvmProvider,
1456    RelayerRepositoryStorage,
1457    NetworkRepositoryStorage,
1458    TransactionRepositoryStorage,
1459    JobProducer,
1460    EvmSigner,
1461    TransactionCounterRepositoryStorage,
1462    PriceCalculator<EvmGasPriceService<EvmProvider>>,
1463>;
1464#[cfg(test)]
1465mod tests {
1466
1467    use super::*;
1468    use crate::{
1469        domain::evm::price_calculator::PriceParams,
1470        jobs::MockJobProducerTrait,
1471        models::{
1472            evm::Speed, EvmTransactionData, EvmTransactionRequest, NetworkType,
1473            RelayerNetworkPolicy, U256,
1474        },
1475        repositories::{
1476            MockNetworkRepository, MockRelayerRepository, MockTransactionCounterTrait,
1477            MockTransactionRepository,
1478        },
1479        services::{provider::MockEvmProviderTrait, signer::MockSigner},
1480    };
1481    use chrono::Utc;
1482    use futures::future::ready;
1483    use mockall::{mock, predicate::*};
1484
1485    // Create a mock for PriceCalculatorTrait
1486    mock! {
1487        pub PriceCalculator {}
1488        #[async_trait]
1489        impl PriceCalculatorTrait for PriceCalculator {
1490            async fn get_transaction_price_params(
1491                &self,
1492                tx_data: &EvmTransactionData,
1493                relayer: &RelayerRepoModel
1494            ) -> Result<PriceParams, TransactionError>;
1495
1496            async fn calculate_bumped_gas_price(
1497                &self,
1498                tx: &EvmTransactionData,
1499                relayer: &RelayerRepoModel,
1500                force_bump: bool,
1501            ) -> Result<PriceParams, TransactionError>;
1502        }
1503    }
1504
1505    // Helper to create a relayer model with specific configuration for these tests
1506    fn create_test_relayer() -> RelayerRepoModel {
1507        create_test_relayer_with_policy(crate::models::RelayerEvmPolicy {
1508            min_balance: Some(100000000000000000u128), // 0.1 ETH
1509            gas_limit_estimation: Some(true),
1510            gas_price_cap: Some(100000000000), // 100 Gwei
1511            whitelist_receivers: Some(vec!["0xRecipient".to_string()]),
1512            eip1559_pricing: Some(false),
1513            private_transactions: Some(false),
1514        })
1515    }
1516
1517    fn create_test_relayer_with_policy(evm_policy: RelayerEvmPolicy) -> RelayerRepoModel {
1518        RelayerRepoModel {
1519            id: "test-relayer-id".to_string(),
1520            name: "Test Relayer".to_string(),
1521            network: "1".to_string(), // Ethereum Mainnet
1522            address: "0xSender".to_string(),
1523            paused: false,
1524            system_disabled: false,
1525            signer_id: "test-signer-id".to_string(),
1526            notification_id: Some("test-notification-id".to_string()),
1527            policies: RelayerNetworkPolicy::Evm(evm_policy),
1528            network_type: NetworkType::Evm,
1529            custom_rpc_urls: None,
1530            ..Default::default()
1531        }
1532    }
1533
1534    // Helper to create test transaction with specific configuration for these tests
1535    fn create_test_transaction() -> TransactionRepoModel {
1536        TransactionRepoModel {
1537            id: "test-tx-id".to_string(),
1538            relayer_id: "test-relayer-id".to_string(),
1539            status: TransactionStatus::Pending,
1540            status_reason: None,
1541            created_at: Utc::now().to_rfc3339(),
1542            sent_at: None,
1543            confirmed_at: None,
1544            valid_until: None,
1545            delete_at: None,
1546            network_type: NetworkType::Evm,
1547            network_data: NetworkTransactionData::Evm(EvmTransactionData {
1548                chain_id: 1,
1549                from: "0xSender".to_string(),
1550                to: Some("0xRecipient".to_string()),
1551                value: U256::from(1000000000000000000u64), // 1 ETH
1552                data: Some("0xData".to_string()),
1553                gas_limit: Some(21000),
1554                gas_price: Some(20000000000), // 20 Gwei
1555                max_fee_per_gas: None,
1556                max_priority_fee_per_gas: None,
1557                nonce: None,
1558                signature: None,
1559                hash: None,
1560                speed: Some(Speed::Fast),
1561                raw: None,
1562            }),
1563            priced_at: None,
1564            hashes: Vec::new(),
1565            noop_count: None,
1566            is_canceled: Some(false),
1567            metadata: None,
1568        }
1569    }
1570
1571    #[tokio::test]
1572    async fn test_prepare_transaction_with_sufficient_balance() {
1573        let mut mock_transaction = MockTransactionRepository::new();
1574        let mock_relayer = MockRelayerRepository::new();
1575        let mut mock_provider = MockEvmProviderTrait::new();
1576        let mut mock_signer = MockSigner::new();
1577        let mut mock_job_producer = MockJobProducerTrait::new();
1578        let mut mock_price_calculator = MockPriceCalculator::new();
1579        let mut counter_service = MockTransactionCounterTrait::new();
1580
1581        let relayer = create_test_relayer();
1582        let test_tx = create_test_transaction();
1583
1584        counter_service
1585            .expect_get_and_increment()
1586            .returning(|_, _| Box::pin(ready(Ok(42))));
1587
1588        let price_params = PriceParams {
1589            gas_price: Some(30000000000),
1590            max_fee_per_gas: None,
1591            max_priority_fee_per_gas: None,
1592            is_min_bumped: None,
1593            extra_fee: None,
1594            total_cost: U256::from(630000000000000u64),
1595        };
1596        mock_price_calculator
1597            .expect_get_transaction_price_params()
1598            .returning(move |_, _| Ok(price_params.clone()));
1599
1600        mock_signer.expect_sign_transaction().returning(|_| {
1601            Box::pin(ready(Ok(
1602                crate::domain::relayer::SignTransactionResponse::Evm(
1603                    crate::domain::relayer::SignTransactionResponseEvm {
1604                        hash: "0xtx_hash".to_string(),
1605                        signature: crate::models::EvmTransactionDataSignature {
1606                            r: "r".to_string(),
1607                            s: "s".to_string(),
1608                            v: 1,
1609                            sig: "0xsignature".to_string(),
1610                        },
1611                        raw: vec![1, 2, 3],
1612                    },
1613                ),
1614            )))
1615        });
1616
1617        mock_provider
1618            .expect_get_balance()
1619            .with(eq("0xSender"))
1620            .returning(|_| Box::pin(ready(Ok(U256::from(1000000000000000000u64)))));
1621
1622        // Mock get_block_by_number for gas limit validation (tx has gas_limit: Some(21000))
1623        mock_provider
1624            .expect_get_block_by_number()
1625            .times(1)
1626            .returning(|| {
1627                Box::pin(async {
1628                    use alloy::{network::AnyRpcBlock, rpc::types::Block};
1629                    let mut block: Block = Block::default();
1630                    // Set block gas limit to 30M (higher than tx gas limit of 21_000)
1631                    block.header.gas_limit = 30_000_000u64;
1632                    Ok(AnyRpcBlock::from(block))
1633                })
1634            });
1635
1636        let test_tx_clone = test_tx.clone();
1637        mock_transaction
1638            .expect_partial_update()
1639            .returning(move |_, update| {
1640                let mut updated_tx = test_tx_clone.clone();
1641                if let Some(status) = &update.status {
1642                    updated_tx.status = status.clone();
1643                }
1644                if let Some(network_data) = &update.network_data {
1645                    updated_tx.network_data = network_data.clone();
1646                }
1647                if let Some(hashes) = &update.hashes {
1648                    updated_tx.hashes = hashes.clone();
1649                }
1650                Ok(updated_tx)
1651            });
1652
1653        mock_job_producer
1654            .expect_produce_submit_transaction_job()
1655            .returning(|_, _| Box::pin(ready(Ok(()))));
1656        mock_job_producer
1657            .expect_produce_send_notification_job()
1658            .returning(|_, _| Box::pin(ready(Ok(()))));
1659
1660        let mock_network = MockNetworkRepository::new();
1661
1662        let evm_transaction = EvmRelayerTransaction {
1663            relayer: relayer.clone(),
1664            provider: mock_provider,
1665            relayer_repository: Arc::new(mock_relayer),
1666            network_repository: Arc::new(mock_network),
1667            transaction_repository: Arc::new(mock_transaction),
1668            transaction_counter_service: Arc::new(counter_service),
1669            job_producer: Arc::new(mock_job_producer),
1670            price_calculator: mock_price_calculator,
1671            signer: mock_signer,
1672        };
1673
1674        let result = evm_transaction.prepare_transaction(test_tx.clone()).await;
1675        assert!(result.is_ok());
1676        let prepared_tx = result.unwrap();
1677        assert_eq!(prepared_tx.status, TransactionStatus::Sent);
1678        assert!(!prepared_tx.hashes.is_empty());
1679    }
1680
1681    #[tokio::test]
1682    async fn test_prepare_transaction_with_insufficient_balance() {
1683        let mut mock_transaction = MockTransactionRepository::new();
1684        let mock_relayer = MockRelayerRepository::new();
1685        let mut mock_provider = MockEvmProviderTrait::new();
1686        let mut mock_signer = MockSigner::new();
1687        let mut mock_job_producer = MockJobProducerTrait::new();
1688        let mut mock_price_calculator = MockPriceCalculator::new();
1689        let mut counter_service = MockTransactionCounterTrait::new();
1690
1691        let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
1692            gas_limit_estimation: Some(false),
1693            min_balance: Some(100000000000000000u128),
1694            ..Default::default()
1695        });
1696        let test_tx = create_test_transaction();
1697
1698        counter_service
1699            .expect_get_and_increment()
1700            .returning(|_, _| Box::pin(ready(Ok(42))));
1701
1702        let price_params = PriceParams {
1703            gas_price: Some(30000000000),
1704            max_fee_per_gas: None,
1705            max_priority_fee_per_gas: None,
1706            is_min_bumped: None,
1707            extra_fee: None,
1708            total_cost: U256::from(630000000000000u64),
1709        };
1710        mock_price_calculator
1711            .expect_get_transaction_price_params()
1712            .returning(move |_, _| Ok(price_params.clone()));
1713
1714        mock_signer.expect_sign_transaction().returning(|_| {
1715            Box::pin(ready(Ok(
1716                crate::domain::relayer::SignTransactionResponse::Evm(
1717                    crate::domain::relayer::SignTransactionResponseEvm {
1718                        hash: "0xtx_hash".to_string(),
1719                        signature: crate::models::EvmTransactionDataSignature {
1720                            r: "r".to_string(),
1721                            s: "s".to_string(),
1722                            v: 1,
1723                            sig: "0xsignature".to_string(),
1724                        },
1725                        raw: vec![1, 2, 3],
1726                    },
1727                ),
1728            )))
1729        });
1730
1731        mock_provider
1732            .expect_get_balance()
1733            .with(eq("0xSender"))
1734            .returning(|_| Box::pin(ready(Ok(U256::from(90000000000000000u64)))));
1735
1736        // Mock get_block_by_number for gas limit validation (tx has gas_limit: Some(21000))
1737        mock_provider
1738            .expect_get_block_by_number()
1739            .times(1)
1740            .returning(|| {
1741                Box::pin(async {
1742                    use alloy::{network::AnyRpcBlock, rpc::types::Block};
1743                    let mut block: Block = Block::default();
1744                    // Set block gas limit to 30M (higher than tx gas limit of 21_000)
1745                    block.header.gas_limit = 30_000_000u64;
1746                    Ok(AnyRpcBlock::from(block))
1747                })
1748            });
1749
1750        let test_tx_clone = test_tx.clone();
1751        mock_transaction
1752            .expect_partial_update()
1753            .withf(move |id, update| {
1754                id == "test-tx-id" && update.status == Some(TransactionStatus::Failed)
1755            })
1756            .returning(move |_, update| {
1757                let mut updated_tx = test_tx_clone.clone();
1758                updated_tx.status = update.status.unwrap_or(updated_tx.status);
1759                updated_tx.status_reason = update.status_reason.clone();
1760                Ok(updated_tx)
1761            });
1762
1763        mock_job_producer
1764            .expect_produce_send_notification_job()
1765            .returning(|_, _| Box::pin(ready(Ok(()))));
1766
1767        let mock_network = MockNetworkRepository::new();
1768
1769        let evm_transaction = EvmRelayerTransaction {
1770            relayer: relayer.clone(),
1771            provider: mock_provider,
1772            relayer_repository: Arc::new(mock_relayer),
1773            network_repository: Arc::new(mock_network),
1774            transaction_repository: Arc::new(mock_transaction),
1775            transaction_counter_service: Arc::new(counter_service),
1776            job_producer: Arc::new(mock_job_producer),
1777            price_calculator: mock_price_calculator,
1778            signer: mock_signer,
1779        };
1780
1781        let result = evm_transaction.prepare_transaction(test_tx.clone()).await;
1782        assert!(result.is_ok(), "Expected Ok, got: {result:?}");
1783
1784        let updated_tx = result.unwrap();
1785        assert_eq!(
1786            updated_tx.status,
1787            TransactionStatus::Failed,
1788            "Transaction should be marked as Failed"
1789        );
1790        assert!(
1791            updated_tx.status_reason.is_some(),
1792            "Status reason should be set"
1793        );
1794        assert!(
1795            updated_tx
1796                .status_reason
1797                .as_ref()
1798                .unwrap()
1799                .to_lowercase()
1800                .contains("insufficient balance"),
1801            "Status reason should contain insufficient balance error, got: {:?}",
1802            updated_tx.status_reason
1803        );
1804    }
1805
1806    #[tokio::test]
1807    async fn test_prepare_transaction_with_gas_limit_exceeding_block_limit() {
1808        let mut mock_transaction = MockTransactionRepository::new();
1809        let mock_relayer = MockRelayerRepository::new();
1810        let mut mock_provider = MockEvmProviderTrait::new();
1811        let mock_signer = MockSigner::new();
1812        let mut mock_job_producer = MockJobProducerTrait::new();
1813        let mock_price_calculator = MockPriceCalculator::new();
1814        let mut counter_service = MockTransactionCounterTrait::new();
1815
1816        let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
1817            gas_limit_estimation: Some(false), // User provides gas limit
1818            min_balance: Some(100000000000000000u128),
1819            ..Default::default()
1820        });
1821
1822        // Create a transaction with a gas limit that exceeds block gas limit
1823        let mut test_tx = create_test_transaction();
1824        if let NetworkTransactionData::Evm(ref mut evm_data) = test_tx.network_data {
1825            evm_data.gas_limit = Some(30_000_001); // Exceeds typical block gas limit of 30M
1826        }
1827
1828        counter_service
1829            .expect_get_and_increment()
1830            .returning(|_, _| Box::pin(ready(Ok(42))));
1831
1832        // Mock get_block_by_number to return a block with gas_limit lower than tx gas_limit
1833        mock_provider
1834            .expect_get_block_by_number()
1835            .times(1)
1836            .returning(|| {
1837                Box::pin(async {
1838                    use alloy::{network::AnyRpcBlock, rpc::types::Block};
1839                    let mut block: Block = Block::default();
1840                    // Set block gas limit to 30M (lower than tx gas limit of 30_000_001)
1841                    block.header.gas_limit = 30_000_000u64;
1842                    Ok(AnyRpcBlock::from(block))
1843                })
1844            });
1845
1846        // Mock partial_update to be called when marking transaction as failed
1847        let test_tx_clone = test_tx.clone();
1848        mock_transaction
1849            .expect_partial_update()
1850            .withf(move |id, update| {
1851                id == "test-tx-id"
1852                    && update.status == Some(TransactionStatus::Failed)
1853                    && update.status_reason.is_some()
1854                    && update
1855                        .status_reason
1856                        .as_ref()
1857                        .unwrap()
1858                        .contains("exceeds block gas limit")
1859            })
1860            .returning(move |_, update| {
1861                let mut updated_tx = test_tx_clone.clone();
1862                updated_tx.status = update.status.unwrap_or(updated_tx.status);
1863                updated_tx.status_reason = update.status_reason.clone();
1864                Ok(updated_tx)
1865            });
1866
1867        mock_job_producer
1868            .expect_produce_send_notification_job()
1869            .returning(|_, _| Box::pin(ready(Ok(()))));
1870
1871        let mock_network = MockNetworkRepository::new();
1872
1873        let evm_transaction = EvmRelayerTransaction {
1874            relayer: relayer.clone(),
1875            provider: mock_provider,
1876            relayer_repository: Arc::new(mock_relayer),
1877            network_repository: Arc::new(mock_network),
1878            transaction_repository: Arc::new(mock_transaction),
1879            transaction_counter_service: Arc::new(counter_service),
1880            job_producer: Arc::new(mock_job_producer),
1881            price_calculator: mock_price_calculator,
1882            signer: mock_signer,
1883        };
1884
1885        let result = evm_transaction.prepare_transaction(test_tx.clone()).await;
1886        assert!(result.is_ok(), "Expected Ok, got: {result:?}");
1887
1888        let updated_tx = result.unwrap();
1889        assert_eq!(
1890            updated_tx.status,
1891            TransactionStatus::Failed,
1892            "Transaction should be marked as Failed"
1893        );
1894        assert!(
1895            updated_tx.status_reason.is_some(),
1896            "Status reason should be set"
1897        );
1898        assert!(
1899            updated_tx
1900                .status_reason
1901                .as_ref()
1902                .unwrap()
1903                .contains("exceeds block gas limit"),
1904            "Status reason should mention gas limit exceeds block gas limit, got: {:?}",
1905            updated_tx.status_reason
1906        );
1907        assert!(
1908            updated_tx
1909                .status_reason
1910                .as_ref()
1911                .unwrap()
1912                .contains("30000001"),
1913            "Status reason should contain transaction gas limit, got: {:?}",
1914            updated_tx.status_reason
1915        );
1916        assert!(
1917            updated_tx
1918                .status_reason
1919                .as_ref()
1920                .unwrap()
1921                .contains("30000000"),
1922            "Status reason should contain block gas limit, got: {:?}",
1923            updated_tx.status_reason
1924        );
1925    }
1926
1927    #[tokio::test]
1928    async fn test_prepare_transaction_with_gas_limit_within_block_limit() {
1929        let mut mock_transaction = MockTransactionRepository::new();
1930        let mock_relayer = MockRelayerRepository::new();
1931        let mut mock_provider = MockEvmProviderTrait::new();
1932        let mut mock_signer = MockSigner::new();
1933        let mut mock_job_producer = MockJobProducerTrait::new();
1934        let mut mock_price_calculator = MockPriceCalculator::new();
1935        let mut counter_service = MockTransactionCounterTrait::new();
1936
1937        let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
1938            gas_limit_estimation: Some(false), // User provides gas limit
1939            min_balance: Some(100000000000000000u128),
1940            ..Default::default()
1941        });
1942
1943        // Create a transaction with a gas limit within block gas limit
1944        let mut test_tx = create_test_transaction();
1945        if let NetworkTransactionData::Evm(ref mut evm_data) = test_tx.network_data {
1946            evm_data.gas_limit = Some(21_000); // Within typical block gas limit of 30M
1947        }
1948
1949        counter_service
1950            .expect_get_and_increment()
1951            .returning(|_, _| Box::pin(ready(Ok(42))));
1952
1953        let price_params = PriceParams {
1954            gas_price: Some(30000000000),
1955            max_fee_per_gas: None,
1956            max_priority_fee_per_gas: None,
1957            is_min_bumped: None,
1958            extra_fee: None,
1959            total_cost: U256::from(630000000000000u64),
1960        };
1961        mock_price_calculator
1962            .expect_get_transaction_price_params()
1963            .returning(move |_, _| Ok(price_params.clone()));
1964
1965        mock_signer.expect_sign_transaction().returning(|_| {
1966            Box::pin(ready(Ok(
1967                crate::domain::relayer::SignTransactionResponse::Evm(
1968                    crate::domain::relayer::SignTransactionResponseEvm {
1969                        hash: "0xtx_hash".to_string(),
1970                        signature: crate::models::EvmTransactionDataSignature {
1971                            r: "r".to_string(),
1972                            s: "s".to_string(),
1973                            v: 1,
1974                            sig: "0xsignature".to_string(),
1975                        },
1976                        raw: vec![1, 2, 3],
1977                    },
1978                ),
1979            )))
1980        });
1981
1982        mock_provider
1983            .expect_get_balance()
1984            .with(eq("0xSender"))
1985            .returning(|_| Box::pin(ready(Ok(U256::from(1000000000000000000u64)))));
1986
1987        // Mock get_block_by_number to return a block with gas_limit higher than tx gas_limit
1988        mock_provider
1989            .expect_get_block_by_number()
1990            .times(1)
1991            .returning(|| {
1992                Box::pin(async {
1993                    use alloy::{network::AnyRpcBlock, rpc::types::Block};
1994                    let mut block: Block = Block::default();
1995                    // Set block gas limit to 30M (higher than tx gas limit of 21_000)
1996                    block.header.gas_limit = 30_000_000u64;
1997                    Ok(AnyRpcBlock::from(block))
1998                })
1999            });
2000
2001        let test_tx_clone = test_tx.clone();
2002        mock_transaction
2003            .expect_partial_update()
2004            .returning(move |_, update| {
2005                let mut updated_tx = test_tx_clone.clone();
2006                if let Some(status) = &update.status {
2007                    updated_tx.status = status.clone();
2008                }
2009                if let Some(network_data) = &update.network_data {
2010                    updated_tx.network_data = network_data.clone();
2011                }
2012                if let Some(hashes) = &update.hashes {
2013                    updated_tx.hashes = hashes.clone();
2014                }
2015                Ok(updated_tx)
2016            });
2017
2018        mock_job_producer
2019            .expect_produce_submit_transaction_job()
2020            .returning(|_, _| Box::pin(ready(Ok(()))));
2021        mock_job_producer
2022            .expect_produce_send_notification_job()
2023            .returning(|_, _| Box::pin(ready(Ok(()))));
2024
2025        let mock_network = MockNetworkRepository::new();
2026
2027        let evm_transaction = EvmRelayerTransaction {
2028            relayer: relayer.clone(),
2029            provider: mock_provider,
2030            relayer_repository: Arc::new(mock_relayer),
2031            network_repository: Arc::new(mock_network),
2032            transaction_repository: Arc::new(mock_transaction),
2033            transaction_counter_service: Arc::new(counter_service),
2034            job_producer: Arc::new(mock_job_producer),
2035            price_calculator: mock_price_calculator,
2036            signer: mock_signer,
2037        };
2038
2039        let result = evm_transaction.prepare_transaction(test_tx.clone()).await;
2040        assert!(result.is_ok(), "Expected Ok, got: {result:?}");
2041
2042        let prepared_tx = result.unwrap();
2043        // Transaction should proceed normally (not be marked as Failed)
2044        assert_eq!(prepared_tx.status, TransactionStatus::Sent);
2045        assert!(!prepared_tx.hashes.is_empty());
2046    }
2047
2048    #[tokio::test]
2049    async fn test_cancel_transaction() {
2050        // Test Case 1: Canceling a pending transaction
2051        {
2052            // Create mocks for all dependencies
2053            let mut mock_transaction = MockTransactionRepository::new();
2054            let mock_relayer = MockRelayerRepository::new();
2055            let mock_provider = MockEvmProviderTrait::new();
2056            let mock_signer = MockSigner::new();
2057            let mut mock_job_producer = MockJobProducerTrait::new();
2058            let mock_price_calculator = MockPriceCalculator::new();
2059            let counter_service = MockTransactionCounterTrait::new();
2060
2061            // Create test relayer and pending transaction
2062            let relayer = create_test_relayer();
2063            let mut test_tx = create_test_transaction();
2064            test_tx.status = TransactionStatus::Pending;
2065
2066            // Transaction repository should update the transaction with Canceled status
2067            let test_tx_clone = test_tx.clone();
2068            mock_transaction
2069                .expect_partial_update()
2070                .withf(move |id, update| {
2071                    id == "test-tx-id" && update.status == Some(TransactionStatus::Canceled)
2072                })
2073                .returning(move |_, update| {
2074                    let mut updated_tx = test_tx_clone.clone();
2075                    updated_tx.status = update.status.unwrap_or(updated_tx.status);
2076                    Ok(updated_tx)
2077                });
2078
2079            // Job producer should send notification
2080            mock_job_producer
2081                .expect_produce_send_notification_job()
2082                .returning(|_, _| Box::pin(ready(Ok(()))));
2083
2084            let mock_network = MockNetworkRepository::new();
2085
2086            // Set up EVM transaction with the mocks
2087            let evm_transaction = EvmRelayerTransaction {
2088                relayer: relayer.clone(),
2089                provider: mock_provider,
2090                relayer_repository: Arc::new(mock_relayer),
2091                network_repository: Arc::new(mock_network),
2092                transaction_repository: Arc::new(mock_transaction),
2093                transaction_counter_service: Arc::new(counter_service),
2094                job_producer: Arc::new(mock_job_producer),
2095                price_calculator: mock_price_calculator,
2096                signer: mock_signer,
2097            };
2098
2099            // Call cancel_transaction and verify it succeeds
2100            let result = evm_transaction.cancel_transaction(test_tx.clone()).await;
2101            assert!(result.is_ok());
2102            let cancelled_tx = result.unwrap();
2103            assert_eq!(cancelled_tx.id, "test-tx-id");
2104            assert_eq!(cancelled_tx.status, TransactionStatus::Canceled);
2105        }
2106
2107        // Test Case 2: Canceling a submitted transaction
2108        {
2109            // Create mocks for all dependencies
2110            let mut mock_transaction = MockTransactionRepository::new();
2111            let mock_relayer = MockRelayerRepository::new();
2112            let mock_provider = MockEvmProviderTrait::new();
2113            let mut mock_signer = MockSigner::new();
2114            let mut mock_job_producer = MockJobProducerTrait::new();
2115            let mut mock_price_calculator = MockPriceCalculator::new();
2116            let counter_service = MockTransactionCounterTrait::new();
2117
2118            // Create test relayer and submitted transaction
2119            let relayer = create_test_relayer();
2120            let mut test_tx = create_test_transaction();
2121            test_tx.status = TransactionStatus::Submitted;
2122            test_tx.sent_at = Some(Utc::now().to_rfc3339());
2123            test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
2124                nonce: Some(42),
2125                hash: Some("0xoriginal_hash".to_string()),
2126                ..test_tx.network_data.get_evm_transaction_data().unwrap()
2127            });
2128
2129            // Set up price calculator expectations for cancellation tx
2130            mock_price_calculator
2131                .expect_get_transaction_price_params()
2132                .return_once(move |_, _| {
2133                    Ok(PriceParams {
2134                        gas_price: Some(40000000000), // 40 Gwei (higher than original)
2135                        max_fee_per_gas: None,
2136                        max_priority_fee_per_gas: None,
2137                        is_min_bumped: Some(true),
2138                        extra_fee: Some(U256::ZERO),
2139                        total_cost: U256::ZERO,
2140                    })
2141                });
2142
2143            // Signer should be called to sign the cancellation transaction
2144            mock_signer.expect_sign_transaction().returning(|_| {
2145                Box::pin(ready(Ok(
2146                    crate::domain::relayer::SignTransactionResponse::Evm(
2147                        crate::domain::relayer::SignTransactionResponseEvm {
2148                            hash: "0xcancellation_hash".to_string(),
2149                            signature: crate::models::EvmTransactionDataSignature {
2150                                r: "r".to_string(),
2151                                s: "s".to_string(),
2152                                v: 1,
2153                                sig: "0xsignature".to_string(),
2154                            },
2155                            raw: vec![1, 2, 3],
2156                        },
2157                    ),
2158                )))
2159            });
2160
2161            // Transaction repository should update the transaction
2162            let test_tx_clone = test_tx.clone();
2163            mock_transaction
2164                .expect_partial_update()
2165                .returning(move |tx_id, update| {
2166                    let mut updated_tx = test_tx_clone.clone();
2167                    updated_tx.id = tx_id;
2168                    updated_tx.status = update.status.unwrap_or(updated_tx.status);
2169                    updated_tx.network_data =
2170                        update.network_data.unwrap_or(updated_tx.network_data);
2171                    updated_tx.is_canceled = update.is_canceled.or(updated_tx.is_canceled);
2172                    if let Some(hashes) = update.hashes {
2173                        updated_tx.hashes = hashes;
2174                    }
2175                    Ok(updated_tx)
2176                });
2177
2178            // Job producer expectations
2179            mock_job_producer
2180                .expect_produce_submit_transaction_job()
2181                .returning(|_, _| Box::pin(ready(Ok(()))));
2182            mock_job_producer
2183                .expect_produce_send_notification_job()
2184                .returning(|_, _| Box::pin(ready(Ok(()))));
2185
2186            // Network repository expectations for cancellation NOOP transaction
2187            let mut mock_network = MockNetworkRepository::new();
2188            mock_network
2189                .expect_get_by_chain_id()
2190                .with(eq(NetworkType::Evm), eq(1))
2191                .returning(|_, _| {
2192                    use crate::config::{EvmNetworkConfig, NetworkConfigCommon};
2193                    use crate::models::{NetworkConfigData, NetworkRepoModel, RpcConfig};
2194
2195                    let config = EvmNetworkConfig {
2196                        common: NetworkConfigCommon {
2197                            network: "mainnet".to_string(),
2198                            from: None,
2199                            rpc_urls: Some(vec![RpcConfig::new(
2200                                "https://rpc.example.com".to_string(),
2201                            )]),
2202                            explorer_urls: None,
2203                            average_blocktime_ms: Some(12000),
2204                            is_testnet: Some(false),
2205                            tags: Some(vec!["mainnet".to_string()]),
2206                        },
2207                        chain_id: Some(1),
2208                        required_confirmations: Some(12),
2209                        features: Some(vec!["eip1559".to_string()]),
2210                        symbol: Some("ETH".to_string()),
2211                        gas_price_cache: None,
2212                    };
2213                    Ok(Some(NetworkRepoModel {
2214                        id: "evm:mainnet".to_string(),
2215                        name: "mainnet".to_string(),
2216                        network_type: NetworkType::Evm,
2217                        config: NetworkConfigData::Evm(config),
2218                    }))
2219                });
2220
2221            // Set up EVM transaction with the mocks
2222            let evm_transaction = EvmRelayerTransaction {
2223                relayer: relayer.clone(),
2224                provider: mock_provider,
2225                relayer_repository: Arc::new(mock_relayer),
2226                network_repository: Arc::new(mock_network),
2227                transaction_repository: Arc::new(mock_transaction),
2228                transaction_counter_service: Arc::new(counter_service),
2229                job_producer: Arc::new(mock_job_producer),
2230                price_calculator: mock_price_calculator,
2231                signer: mock_signer,
2232            };
2233
2234            // Call cancel_transaction and verify it succeeds
2235            let result = evm_transaction.cancel_transaction(test_tx.clone()).await;
2236            assert!(result.is_ok());
2237            let cancelled_tx = result.unwrap();
2238
2239            // Verify the cancellation transaction was properly created
2240            assert_eq!(cancelled_tx.id, "test-tx-id");
2241            // The tx keeps its active status until the NOOP mines; is_canceled excludes
2242            // it from active-status API views in the meantime.
2243            assert_eq!(cancelled_tx.status, TransactionStatus::Submitted);
2244            assert_eq!(cancelled_tx.is_canceled, Some(true));
2245
2246            // Verify the network data was properly updated
2247            if let NetworkTransactionData::Evm(evm_data) = &cancelled_tx.network_data {
2248                assert_eq!(evm_data.nonce, Some(42)); // Same nonce as original
2249            } else {
2250                panic!("Expected EVM transaction data");
2251            }
2252        }
2253
2254        // Test Case 3: Attempting to cancel a confirmed transaction (should fail)
2255        {
2256            // Create minimal mocks for failure case
2257            let mock_transaction = MockTransactionRepository::new();
2258            let mock_relayer = MockRelayerRepository::new();
2259            let mock_provider = MockEvmProviderTrait::new();
2260            let mock_signer = MockSigner::new();
2261            let mock_job_producer = MockJobProducerTrait::new();
2262            let mock_price_calculator = MockPriceCalculator::new();
2263            let counter_service = MockTransactionCounterTrait::new();
2264
2265            // Create test relayer and confirmed transaction
2266            let relayer = create_test_relayer();
2267            let mut test_tx = create_test_transaction();
2268            test_tx.status = TransactionStatus::Confirmed;
2269
2270            let mock_network = MockNetworkRepository::new();
2271
2272            // Set up EVM transaction with the mocks
2273            let evm_transaction = EvmRelayerTransaction {
2274                relayer: relayer.clone(),
2275                provider: mock_provider,
2276                relayer_repository: Arc::new(mock_relayer),
2277                network_repository: Arc::new(mock_network),
2278                transaction_repository: Arc::new(mock_transaction),
2279                transaction_counter_service: Arc::new(counter_service),
2280                job_producer: Arc::new(mock_job_producer),
2281                price_calculator: mock_price_calculator,
2282                signer: mock_signer,
2283            };
2284
2285            // Call cancel_transaction and verify it fails
2286            let result = evm_transaction.cancel_transaction(test_tx.clone()).await;
2287            assert!(result.is_err());
2288            if let Err(TransactionError::ValidationError(msg)) = result {
2289                assert!(msg.contains("Invalid transaction state for cancel_transaction"));
2290            } else {
2291                panic!("Expected ValidationError");
2292            }
2293        }
2294    }
2295
2296    #[tokio::test]
2297    async fn test_replace_transaction() {
2298        // Test Case: Replacing a submitted transaction with new gas price
2299        {
2300            // Create mocks for all dependencies
2301            let mut mock_transaction = MockTransactionRepository::new();
2302            let mock_relayer = MockRelayerRepository::new();
2303            let mut mock_provider = MockEvmProviderTrait::new();
2304            let mut mock_signer = MockSigner::new();
2305            let mut mock_job_producer = MockJobProducerTrait::new();
2306            let mut mock_price_calculator = MockPriceCalculator::new();
2307            let counter_service = MockTransactionCounterTrait::new();
2308
2309            // Create test relayer and submitted transaction
2310            let relayer = create_test_relayer();
2311            let mut test_tx = create_test_transaction();
2312            test_tx.status = TransactionStatus::Submitted;
2313            test_tx.sent_at = Some(Utc::now().to_rfc3339());
2314
2315            // Set up price calculator expectations for replacement
2316            mock_price_calculator
2317                .expect_get_transaction_price_params()
2318                .return_once(move |_, _| {
2319                    Ok(PriceParams {
2320                        gas_price: Some(40000000000), // 40 Gwei (higher than original)
2321                        max_fee_per_gas: None,
2322                        max_priority_fee_per_gas: None,
2323                        is_min_bumped: Some(true),
2324                        extra_fee: Some(U256::ZERO),
2325                        total_cost: U256::from(2001000000000000000u64), // 2 ETH + gas costs
2326                    })
2327                });
2328
2329            // Signer should be called to sign the replacement transaction
2330            mock_signer.expect_sign_transaction().returning(|_| {
2331                Box::pin(ready(Ok(
2332                    crate::domain::relayer::SignTransactionResponse::Evm(
2333                        crate::domain::relayer::SignTransactionResponseEvm {
2334                            hash: "0xreplacement_hash".to_string(),
2335                            signature: crate::models::EvmTransactionDataSignature {
2336                                r: "r".to_string(),
2337                                s: "s".to_string(),
2338                                v: 1,
2339                                sig: "0xsignature".to_string(),
2340                            },
2341                            raw: vec![1, 2, 3],
2342                        },
2343                    ),
2344                )))
2345            });
2346
2347            // Provider balance check should pass
2348            mock_provider
2349                .expect_get_balance()
2350                .with(eq("0xSender"))
2351                .returning(|_| Box::pin(ready(Ok(U256::from(3000000000000000000u64)))));
2352
2353            // Transaction repository should update using update_network_data
2354            let test_tx_clone = test_tx.clone();
2355            mock_transaction
2356                .expect_update_network_data()
2357                .returning(move |tx_id, network_data| {
2358                    let mut updated_tx = test_tx_clone.clone();
2359                    updated_tx.id = tx_id;
2360                    updated_tx.network_data = network_data;
2361                    Ok(updated_tx)
2362                });
2363
2364            // Job producer expectations
2365            mock_job_producer
2366                .expect_produce_submit_transaction_job()
2367                .returning(|_, _| Box::pin(ready(Ok(()))));
2368            mock_job_producer
2369                .expect_produce_send_notification_job()
2370                .returning(|_, _| Box::pin(ready(Ok(()))));
2371
2372            // Network repository expectations for mempool check
2373            let mut mock_network = MockNetworkRepository::new();
2374            mock_network
2375                .expect_get_by_chain_id()
2376                .with(eq(NetworkType::Evm), eq(1))
2377                .returning(|_, _| {
2378                    use crate::config::{EvmNetworkConfig, NetworkConfigCommon};
2379                    use crate::models::{NetworkConfigData, NetworkRepoModel};
2380
2381                    let config = EvmNetworkConfig {
2382                        common: NetworkConfigCommon {
2383                            network: "mainnet".to_string(),
2384                            from: None,
2385                            rpc_urls: Some(vec![crate::models::RpcConfig::new(
2386                                "https://rpc.example.com".to_string(),
2387                            )]),
2388                            explorer_urls: None,
2389                            average_blocktime_ms: Some(12000),
2390                            is_testnet: Some(false),
2391                            tags: Some(vec!["mainnet".to_string()]), // No "no-mempool" tag
2392                        },
2393                        chain_id: Some(1),
2394                        required_confirmations: Some(12),
2395                        features: Some(vec!["eip1559".to_string()]),
2396                        symbol: Some("ETH".to_string()),
2397                        gas_price_cache: None,
2398                    };
2399                    Ok(Some(NetworkRepoModel {
2400                        id: "evm:mainnet".to_string(),
2401                        name: "mainnet".to_string(),
2402                        network_type: NetworkType::Evm,
2403                        config: NetworkConfigData::Evm(config),
2404                    }))
2405                });
2406
2407            // Set up EVM transaction with the mocks
2408            let evm_transaction = EvmRelayerTransaction {
2409                relayer: relayer.clone(),
2410                provider: mock_provider,
2411                relayer_repository: Arc::new(mock_relayer),
2412                network_repository: Arc::new(mock_network),
2413                transaction_repository: Arc::new(mock_transaction),
2414                transaction_counter_service: Arc::new(counter_service),
2415                job_producer: Arc::new(mock_job_producer),
2416                price_calculator: mock_price_calculator,
2417                signer: mock_signer,
2418            };
2419
2420            // Create replacement request with speed-based pricing
2421            let replacement_request = NetworkTransactionRequest::Evm(EvmTransactionRequest {
2422                to: Some("0xNewRecipient".to_string()),
2423                value: U256::from(2000000000000000000u64), // 2 ETH
2424                data: Some("0xNewData".to_string()),
2425                gas_limit: Some(25000),
2426                gas_price: None, // Use speed-based pricing
2427                max_fee_per_gas: None,
2428                max_priority_fee_per_gas: None,
2429                speed: Some(Speed::Fast),
2430                valid_until: None,
2431            });
2432
2433            // Call replace_transaction and verify it succeeds
2434            let result = evm_transaction
2435                .replace_transaction(test_tx.clone(), replacement_request)
2436                .await;
2437            if let Err(ref e) = result {
2438                eprintln!("Replace transaction failed with error: {e:?}");
2439            }
2440            assert!(result.is_ok());
2441            let replaced_tx = result.unwrap();
2442
2443            // Verify the replacement was properly processed
2444            assert_eq!(replaced_tx.id, "test-tx-id");
2445
2446            // Verify the network data was properly updated
2447            if let NetworkTransactionData::Evm(evm_data) = &replaced_tx.network_data {
2448                assert_eq!(evm_data.to, Some("0xNewRecipient".to_string()));
2449                assert_eq!(evm_data.value, U256::from(2000000000000000000u64));
2450                assert_eq!(evm_data.gas_price, Some(40000000000));
2451                assert_eq!(evm_data.gas_limit, Some(25000));
2452                assert!(evm_data.hash.is_some());
2453                assert!(evm_data.raw.is_some());
2454            } else {
2455                panic!("Expected EVM transaction data");
2456            }
2457        }
2458
2459        // Test Case: Attempting to replace a confirmed transaction (should fail)
2460        {
2461            // Create minimal mocks for failure case
2462            let mock_transaction = MockTransactionRepository::new();
2463            let mock_relayer = MockRelayerRepository::new();
2464            let mock_provider = MockEvmProviderTrait::new();
2465            let mock_signer = MockSigner::new();
2466            let mock_job_producer = MockJobProducerTrait::new();
2467            let mock_price_calculator = MockPriceCalculator::new();
2468            let counter_service = MockTransactionCounterTrait::new();
2469
2470            // Create test relayer and confirmed transaction
2471            let relayer = create_test_relayer();
2472            let mut test_tx = create_test_transaction();
2473            test_tx.status = TransactionStatus::Confirmed;
2474
2475            let mock_network = MockNetworkRepository::new();
2476
2477            // Set up EVM transaction with the mocks
2478            let evm_transaction = EvmRelayerTransaction {
2479                relayer: relayer.clone(),
2480                provider: mock_provider,
2481                relayer_repository: Arc::new(mock_relayer),
2482                network_repository: Arc::new(mock_network),
2483                transaction_repository: Arc::new(mock_transaction),
2484                transaction_counter_service: Arc::new(counter_service),
2485                job_producer: Arc::new(mock_job_producer),
2486                price_calculator: mock_price_calculator,
2487                signer: mock_signer,
2488            };
2489
2490            // Create dummy replacement request
2491            let replacement_request = NetworkTransactionRequest::Evm(EvmTransactionRequest {
2492                to: Some("0xNewRecipient".to_string()),
2493                value: U256::from(1000000000000000000u64),
2494                data: Some("0xData".to_string()),
2495                gas_limit: Some(21000),
2496                gas_price: Some(30000000000),
2497                max_fee_per_gas: None,
2498                max_priority_fee_per_gas: None,
2499                speed: Some(Speed::Fast),
2500                valid_until: None,
2501            });
2502
2503            // Call replace_transaction and verify it fails
2504            let result = evm_transaction
2505                .replace_transaction(test_tx.clone(), replacement_request)
2506                .await;
2507            assert!(result.is_err());
2508            if let Err(TransactionError::ValidationError(msg)) = result {
2509                assert!(msg.contains("Invalid transaction state for replace_transaction"));
2510            } else {
2511                panic!("Expected ValidationError");
2512            }
2513        }
2514    }
2515
2516    #[tokio::test]
2517    async fn test_estimate_tx_gas_limit_success() {
2518        let mock_transaction = MockTransactionRepository::new();
2519        let mock_relayer = MockRelayerRepository::new();
2520        let mut mock_provider = MockEvmProviderTrait::new();
2521        let mock_signer = MockSigner::new();
2522        let mock_job_producer = MockJobProducerTrait::new();
2523        let mock_price_calculator = MockPriceCalculator::new();
2524        let counter_service = MockTransactionCounterTrait::new();
2525        let mock_network = MockNetworkRepository::new();
2526
2527        // Create test relayer and pending transaction
2528        let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2529            gas_limit_estimation: Some(true),
2530            ..Default::default()
2531        });
2532        let evm_data = EvmTransactionData {
2533            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
2534            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
2535            value: U256::from(1000000000000000000u128),
2536            data: Some("0x".to_string()),
2537            gas_limit: None,
2538            gas_price: Some(20_000_000_000),
2539            nonce: Some(1),
2540            chain_id: 1,
2541            hash: None,
2542            signature: None,
2543            speed: Some(Speed::Average),
2544            max_fee_per_gas: None,
2545            max_priority_fee_per_gas: None,
2546            raw: None,
2547        };
2548
2549        // Mock provider to return 21000 as estimated gas
2550        mock_provider
2551            .expect_estimate_gas()
2552            .times(1)
2553            .returning(|_| Box::pin(async { Ok(21000) }));
2554
2555        let transaction = EvmRelayerTransaction::new(
2556            relayer.clone(),
2557            mock_provider,
2558            Arc::new(mock_relayer),
2559            Arc::new(mock_network),
2560            Arc::new(mock_transaction),
2561            Arc::new(counter_service),
2562            Arc::new(mock_job_producer),
2563            mock_price_calculator,
2564            mock_signer,
2565        )
2566        .unwrap();
2567
2568        let result = transaction
2569            .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
2570            .await;
2571
2572        assert!(result.is_ok());
2573        // Expected: 21000 * 110 / 100 = 23100
2574        assert_eq!(result.unwrap(), 23100);
2575    }
2576
2577    #[tokio::test]
2578    async fn test_estimate_tx_gas_limit_disabled() {
2579        let mock_transaction = MockTransactionRepository::new();
2580        let mock_relayer = MockRelayerRepository::new();
2581        let mut mock_provider = MockEvmProviderTrait::new();
2582        let mock_signer = MockSigner::new();
2583        let mock_job_producer = MockJobProducerTrait::new();
2584        let mock_price_calculator = MockPriceCalculator::new();
2585        let counter_service = MockTransactionCounterTrait::new();
2586        let mock_network = MockNetworkRepository::new();
2587
2588        // Create test relayer and pending transaction
2589        let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2590            gas_limit_estimation: Some(false),
2591            ..Default::default()
2592        });
2593
2594        let evm_data = EvmTransactionData {
2595            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
2596            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
2597            value: U256::from(1000000000000000000u128),
2598            data: Some("0x".to_string()),
2599            gas_limit: None,
2600            gas_price: Some(20_000_000_000),
2601            nonce: Some(1),
2602            chain_id: 1,
2603            hash: None,
2604            signature: None,
2605            speed: Some(Speed::Average),
2606            max_fee_per_gas: None,
2607            max_priority_fee_per_gas: None,
2608            raw: None,
2609        };
2610
2611        // Provider should not be called when estimation is disabled
2612        mock_provider.expect_estimate_gas().times(0);
2613
2614        let transaction = EvmRelayerTransaction::new(
2615            relayer.clone(),
2616            mock_provider,
2617            Arc::new(mock_relayer),
2618            Arc::new(mock_network),
2619            Arc::new(mock_transaction),
2620            Arc::new(counter_service),
2621            Arc::new(mock_job_producer),
2622            mock_price_calculator,
2623            mock_signer,
2624        )
2625        .unwrap();
2626
2627        let result = transaction
2628            .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
2629            .await;
2630
2631        assert!(result.is_err());
2632        assert!(matches!(
2633            result.unwrap_err(),
2634            TransactionError::UnexpectedError(_)
2635        ));
2636    }
2637
2638    #[tokio::test]
2639    async fn test_estimate_tx_gas_limit_default_enabled() {
2640        let mock_transaction = MockTransactionRepository::new();
2641        let mock_relayer = MockRelayerRepository::new();
2642        let mut mock_provider = MockEvmProviderTrait::new();
2643        let mock_signer = MockSigner::new();
2644        let mock_job_producer = MockJobProducerTrait::new();
2645        let mock_price_calculator = MockPriceCalculator::new();
2646        let counter_service = MockTransactionCounterTrait::new();
2647        let mock_network = MockNetworkRepository::new();
2648
2649        let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2650            gas_limit_estimation: None, // Should default to true
2651            ..Default::default()
2652        });
2653
2654        let evm_data = EvmTransactionData {
2655            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
2656            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
2657            value: U256::from(1000000000000000000u128),
2658            data: Some("0x".to_string()),
2659            gas_limit: None,
2660            gas_price: Some(20_000_000_000),
2661            nonce: Some(1),
2662            chain_id: 1,
2663            hash: None,
2664            signature: None,
2665            speed: Some(Speed::Average),
2666            max_fee_per_gas: None,
2667            max_priority_fee_per_gas: None,
2668            raw: None,
2669        };
2670
2671        // Mock provider to return 50000 as estimated gas
2672        mock_provider
2673            .expect_estimate_gas()
2674            .times(1)
2675            .returning(|_| Box::pin(async { Ok(50000) }));
2676
2677        let transaction = EvmRelayerTransaction::new(
2678            relayer.clone(),
2679            mock_provider,
2680            Arc::new(mock_relayer),
2681            Arc::new(mock_network),
2682            Arc::new(mock_transaction),
2683            Arc::new(counter_service),
2684            Arc::new(mock_job_producer),
2685            mock_price_calculator,
2686            mock_signer,
2687        )
2688        .unwrap();
2689
2690        let result = transaction
2691            .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
2692            .await;
2693
2694        assert!(result.is_ok());
2695        // Expected: 50000 * 110 / 100 = 55000
2696        assert_eq!(result.unwrap(), 55000);
2697    }
2698
2699    #[tokio::test]
2700    async fn test_estimate_tx_gas_limit_provider_error() {
2701        let mock_transaction = MockTransactionRepository::new();
2702        let mock_relayer = MockRelayerRepository::new();
2703        let mut mock_provider = MockEvmProviderTrait::new();
2704        let mock_signer = MockSigner::new();
2705        let mock_job_producer = MockJobProducerTrait::new();
2706        let mock_price_calculator = MockPriceCalculator::new();
2707        let counter_service = MockTransactionCounterTrait::new();
2708        let mock_network = MockNetworkRepository::new();
2709
2710        let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2711            gas_limit_estimation: Some(true),
2712            ..Default::default()
2713        });
2714
2715        let evm_data = EvmTransactionData {
2716            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
2717            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
2718            value: U256::from(1000000000000000000u128),
2719            data: Some("0x".to_string()),
2720            gas_limit: None,
2721            gas_price: Some(20_000_000_000),
2722            nonce: Some(1),
2723            chain_id: 1,
2724            hash: None,
2725            signature: None,
2726            speed: Some(Speed::Average),
2727            max_fee_per_gas: None,
2728            max_priority_fee_per_gas: None,
2729            raw: None,
2730        };
2731
2732        // Mock provider to return an error
2733        mock_provider.expect_estimate_gas().times(1).returning(|_| {
2734            Box::pin(async {
2735                Err(crate::services::provider::ProviderError::Other(
2736                    "RPC error".to_string(),
2737                ))
2738            })
2739        });
2740
2741        let transaction = EvmRelayerTransaction::new(
2742            relayer.clone(),
2743            mock_provider,
2744            Arc::new(mock_relayer),
2745            Arc::new(mock_network),
2746            Arc::new(mock_transaction),
2747            Arc::new(counter_service),
2748            Arc::new(mock_job_producer),
2749            mock_price_calculator,
2750            mock_signer,
2751        )
2752        .unwrap();
2753
2754        let result = transaction
2755            .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
2756            .await;
2757
2758        assert!(result.is_err());
2759        assert!(matches!(
2760            result.unwrap_err(),
2761            TransactionError::UnexpectedError(_)
2762        ));
2763    }
2764
2765    #[tokio::test]
2766    async fn test_prepare_transaction_uses_gas_estimation_and_stores_result() {
2767        let mut mock_transaction = MockTransactionRepository::new();
2768        let mock_relayer = MockRelayerRepository::new();
2769        let mut mock_provider = MockEvmProviderTrait::new();
2770        let mut mock_signer = MockSigner::new();
2771        let mut mock_job_producer = MockJobProducerTrait::new();
2772        let mut mock_price_calculator = MockPriceCalculator::new();
2773        let mut counter_service = MockTransactionCounterTrait::new();
2774        let mock_network = MockNetworkRepository::new();
2775
2776        // Create test relayer with gas limit estimation enabled
2777        let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2778            gas_limit_estimation: Some(true),
2779            min_balance: Some(100000000000000000u128),
2780            ..Default::default()
2781        });
2782
2783        // Create test transaction WITHOUT gas_limit (so estimation will be triggered)
2784        let mut test_tx = create_test_transaction();
2785        if let NetworkTransactionData::Evm(ref mut evm_data) = test_tx.network_data {
2786            evm_data.gas_limit = None; // This should trigger gas estimation
2787            evm_data.nonce = None; // This will be set by the counter service
2788        }
2789
2790        // Expected estimated gas from provider
2791        const PROVIDER_GAS_ESTIMATE: u64 = 45000;
2792        const EXPECTED_GAS_WITH_BUFFER: u64 = 49500; // 45000 * 110 / 100
2793
2794        // Mock provider to return specific gas estimate
2795        mock_provider
2796            .expect_estimate_gas()
2797            .times(1)
2798            .returning(move |_| Box::pin(async move { Ok(PROVIDER_GAS_ESTIMATE) }));
2799
2800        // Mock provider for balance check
2801        mock_provider
2802            .expect_get_balance()
2803            .times(1)
2804            .returning(|_| Box::pin(async { Ok(U256::from(2000000000000000000u128)) })); // 2 ETH
2805
2806        let price_params = PriceParams {
2807            gas_price: Some(20_000_000_000), // 20 Gwei
2808            max_fee_per_gas: None,
2809            max_priority_fee_per_gas: None,
2810            is_min_bumped: None,
2811            extra_fee: None,
2812            total_cost: U256::from(1900000000000000000u128), // 1.9 ETH total cost
2813        };
2814
2815        // Mock price calculator
2816        mock_price_calculator
2817            .expect_get_transaction_price_params()
2818            .returning(move |_, _| Ok(price_params.clone()));
2819
2820        // Mock transaction counter to return a nonce
2821        counter_service
2822            .expect_get_and_increment()
2823            .times(1)
2824            .returning(|_, _| Box::pin(async { Ok(42) }));
2825
2826        // Mock signer to return a signed transaction
2827        mock_signer.expect_sign_transaction().returning(|_| {
2828            Box::pin(ready(Ok(
2829                crate::domain::relayer::SignTransactionResponse::Evm(
2830                    crate::domain::relayer::SignTransactionResponseEvm {
2831                        hash: "0xhash".to_string(),
2832                        signature: crate::models::EvmTransactionDataSignature {
2833                            r: "r".to_string(),
2834                            s: "s".to_string(),
2835                            v: 1,
2836                            sig: "0xsignature".to_string(),
2837                        },
2838                        raw: vec![1, 2, 3],
2839                    },
2840                ),
2841            )))
2842        });
2843
2844        // Mock job producer to capture the submission job
2845        mock_job_producer
2846            .expect_produce_submit_transaction_job()
2847            .returning(|_, _| Box::pin(async { Ok(()) }));
2848
2849        mock_job_producer
2850            .expect_produce_send_notification_job()
2851            .returning(|_, _| Box::pin(ready(Ok(()))));
2852
2853        // Mock transaction repository partial_update calls
2854        // Note: prepare_transaction calls partial_update twice:
2855        // 1. Presign update (saves nonce before signing)
2856        // 2. Postsign update (saves signed data and marks as Sent)
2857        let expected_gas_limit = EXPECTED_GAS_WITH_BUFFER;
2858
2859        let test_tx_clone = test_tx.clone();
2860        mock_transaction
2861            .expect_partial_update()
2862            .times(2)
2863            .returning(move |_, update| {
2864                let mut updated_tx = test_tx_clone.clone();
2865
2866                // Apply the updates from the request
2867                if let Some(status) = &update.status {
2868                    updated_tx.status = status.clone();
2869                }
2870                if let Some(network_data) = &update.network_data {
2871                    updated_tx.network_data = network_data.clone();
2872                } else {
2873                    // If network_data is not being updated, ensure gas_limit is set
2874                    if let NetworkTransactionData::Evm(ref mut evm_data) = updated_tx.network_data {
2875                        if evm_data.gas_limit.is_none() {
2876                            evm_data.gas_limit = Some(expected_gas_limit);
2877                        }
2878                    }
2879                }
2880                if let Some(hashes) = &update.hashes {
2881                    updated_tx.hashes = hashes.clone();
2882                }
2883
2884                Ok(updated_tx)
2885            });
2886
2887        let transaction = EvmRelayerTransaction::new(
2888            relayer.clone(),
2889            mock_provider,
2890            Arc::new(mock_relayer),
2891            Arc::new(mock_network),
2892            Arc::new(mock_transaction),
2893            Arc::new(counter_service),
2894            Arc::new(mock_job_producer),
2895            mock_price_calculator,
2896            mock_signer,
2897        )
2898        .unwrap();
2899
2900        // Call prepare_transaction
2901        let result = transaction.prepare_transaction(test_tx).await;
2902
2903        // Verify the transaction was prepared successfully
2904        assert!(result.is_ok(), "prepare_transaction should succeed");
2905        let prepared_tx = result.unwrap();
2906
2907        // Verify the final transaction has the estimated gas limit
2908        if let NetworkTransactionData::Evm(evm_data) = prepared_tx.network_data {
2909            assert_eq!(evm_data.gas_limit, Some(EXPECTED_GAS_WITH_BUFFER));
2910        } else {
2911            panic!("Expected EVM network data");
2912        }
2913    }
2914
2915    #[tokio::test]
2916    async fn test_prepare_transaction_estimates_gas_for_contract_creation() {
2917        let mut mock_transaction = MockTransactionRepository::new();
2918        let mock_relayer = MockRelayerRepository::new();
2919        let mut mock_provider = MockEvmProviderTrait::new();
2920        let mut mock_signer = MockSigner::new();
2921        let mut mock_job_producer = MockJobProducerTrait::new();
2922        let mut mock_price_calculator = MockPriceCalculator::new();
2923        let mut counter_service = MockTransactionCounterTrait::new();
2924        let mock_network = MockNetworkRepository::new();
2925
2926        let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2927            gas_limit_estimation: Some(true),
2928            min_balance: Some(100000000000000000u128),
2929            ..Default::default()
2930        });
2931
2932        let mut test_tx = create_test_transaction();
2933        if let NetworkTransactionData::Evm(ref mut evm_data) = test_tx.network_data {
2934            evm_data.to = None;
2935            evm_data.data = Some("0x6080604052348015600f57600080fd5b".to_string());
2936            evm_data.gas_limit = None;
2937            evm_data.nonce = None;
2938        }
2939
2940        const PROVIDER_GAS_ESTIMATE: u64 = 1500000;
2941        const EXPECTED_GAS_WITH_BUFFER: u64 = 1650000;
2942
2943        mock_provider
2944            .expect_estimate_gas()
2945            .withf(|tx| tx.to.is_none())
2946            .times(1)
2947            .returning(move |_| Box::pin(async move { Ok(PROVIDER_GAS_ESTIMATE) }));
2948
2949        mock_provider
2950            .expect_get_balance()
2951            .times(1)
2952            .returning(|_| Box::pin(async { Ok(U256::from(2000000000000000000u128)) }));
2953
2954        let price_params = PriceParams {
2955            gas_price: Some(20_000_000_000),
2956            max_fee_per_gas: None,
2957            max_priority_fee_per_gas: None,
2958            is_min_bumped: None,
2959            extra_fee: None,
2960            total_cost: U256::from(1900000000000000000u128),
2961        };
2962
2963        mock_price_calculator
2964            .expect_get_transaction_price_params()
2965            .returning(move |_, _| Ok(price_params.clone()));
2966
2967        counter_service
2968            .expect_get_and_increment()
2969            .times(1)
2970            .returning(|_, _| Box::pin(async { Ok(42) }));
2971
2972        mock_signer.expect_sign_transaction().returning(|_| {
2973            Box::pin(ready(Ok(
2974                crate::domain::relayer::SignTransactionResponse::Evm(
2975                    crate::domain::relayer::SignTransactionResponseEvm {
2976                        hash: "0xhash".to_string(),
2977                        signature: crate::models::EvmTransactionDataSignature {
2978                            r: "r".to_string(),
2979                            s: "s".to_string(),
2980                            v: 1,
2981                            sig: "0xsignature".to_string(),
2982                        },
2983                        raw: vec![1, 2, 3],
2984                    },
2985                ),
2986            )))
2987        });
2988
2989        mock_job_producer
2990            .expect_produce_submit_transaction_job()
2991            .returning(|_, _| Box::pin(async { Ok(()) }));
2992
2993        mock_job_producer
2994            .expect_produce_send_notification_job()
2995            .returning(|_, _| Box::pin(ready(Ok(()))));
2996
2997        let expected_gas_limit = EXPECTED_GAS_WITH_BUFFER;
2998        let test_tx_clone = test_tx.clone();
2999        mock_transaction
3000            .expect_partial_update()
3001            .times(2)
3002            .returning(move |_, update| {
3003                let mut updated_tx = test_tx_clone.clone();
3004
3005                if let Some(status) = &update.status {
3006                    updated_tx.status = status.clone();
3007                }
3008                if let Some(network_data) = &update.network_data {
3009                    updated_tx.network_data = network_data.clone();
3010                } else if let NetworkTransactionData::Evm(ref mut evm_data) =
3011                    updated_tx.network_data
3012                {
3013                    if evm_data.gas_limit.is_none() {
3014                        evm_data.gas_limit = Some(expected_gas_limit);
3015                    }
3016                }
3017                if let Some(hashes) = &update.hashes {
3018                    updated_tx.hashes = hashes.clone();
3019                }
3020
3021                Ok(updated_tx)
3022            });
3023
3024        let transaction = EvmRelayerTransaction::new(
3025            relayer,
3026            mock_provider,
3027            Arc::new(mock_relayer),
3028            Arc::new(mock_network),
3029            Arc::new(mock_transaction),
3030            Arc::new(counter_service),
3031            Arc::new(mock_job_producer),
3032            mock_price_calculator,
3033            mock_signer,
3034        )
3035        .unwrap();
3036
3037        let result = transaction.prepare_transaction(test_tx).await;
3038
3039        assert!(result.is_ok(), "prepare_transaction should succeed");
3040        let prepared_tx = result.unwrap();
3041
3042        if let NetworkTransactionData::Evm(evm_data) = prepared_tx.network_data {
3043            assert_eq!(evm_data.to, None);
3044            assert_eq!(evm_data.gas_limit, Some(EXPECTED_GAS_WITH_BUFFER));
3045        } else {
3046            panic!("Expected EVM network data");
3047        }
3048    }
3049
3050    #[test]
3051    fn test_is_already_submitted_error_detection() {
3052        // Test "already known" variants
3053        assert!(DefaultEvmTransaction::is_already_submitted_error(
3054            &"already known"
3055        ));
3056        assert!(DefaultEvmTransaction::is_already_submitted_error(
3057            &"Transaction already known"
3058        ));
3059        assert!(DefaultEvmTransaction::is_already_submitted_error(
3060            &"Error: already known"
3061        ));
3062
3063        // Test "nonce too low" variants
3064        assert!(DefaultEvmTransaction::is_already_submitted_error(
3065            &"nonce too low"
3066        ));
3067        assert!(DefaultEvmTransaction::is_already_submitted_error(
3068            &"Nonce Too Low"
3069        ));
3070        assert!(DefaultEvmTransaction::is_already_submitted_error(
3071            &"Error: nonce too low"
3072        ));
3073
3074        // Test "nonce is too low" variants (some providers use this wording)
3075        assert!(DefaultEvmTransaction::is_already_submitted_error(
3076            &"nonce is too low"
3077        ));
3078        assert!(DefaultEvmTransaction::is_already_submitted_error(
3079            &"Error: nonce is too low"
3080        ));
3081
3082        // Test "known transaction" variants (Besu)
3083        assert!(DefaultEvmTransaction::is_already_submitted_error(
3084            &"known transaction"
3085        ));
3086        assert!(DefaultEvmTransaction::is_already_submitted_error(
3087            &"Known Transaction"
3088        ));
3089
3090        // Test "replacement transaction underpriced" variants
3091        assert!(DefaultEvmTransaction::is_already_submitted_error(
3092            &"replacement transaction underpriced"
3093        ));
3094        assert!(DefaultEvmTransaction::is_already_submitted_error(
3095            &"Replacement Transaction Underpriced"
3096        ));
3097
3098        // Test "same hash was already imported" (OpenEthereum)
3099        assert!(DefaultEvmTransaction::is_already_submitted_error(
3100            &"same hash was already imported"
3101        ));
3102
3103        // Test non-matching errors
3104        assert!(!DefaultEvmTransaction::is_already_submitted_error(
3105            &"insufficient funds"
3106        ));
3107        assert!(!DefaultEvmTransaction::is_already_submitted_error(
3108            &"execution reverted"
3109        ));
3110        assert!(!DefaultEvmTransaction::is_already_submitted_error(
3111            &"gas too low"
3112        ));
3113        assert!(!DefaultEvmTransaction::is_already_submitted_error(
3114            &"timeout"
3115        ));
3116        // "unknown transaction" must NOT match "known transaction"
3117        assert!(!DefaultEvmTransaction::is_already_submitted_error(
3118            &"Unknown transaction status"
3119        ));
3120    }
3121
3122    /// Test submit_transaction with "already known" error in Sent status
3123    /// This should treat the error as success and update to Submitted
3124    #[tokio::test]
3125    async fn test_submit_transaction_already_known_error_from_sent() {
3126        let mut mock_transaction = MockTransactionRepository::new();
3127        let mock_relayer = MockRelayerRepository::new();
3128        let mut mock_provider = MockEvmProviderTrait::new();
3129        let mock_signer = MockSigner::new();
3130        let mut mock_job_producer = MockJobProducerTrait::new();
3131        let mock_price_calculator = MockPriceCalculator::new();
3132        let counter_service = MockTransactionCounterTrait::new();
3133        let mock_network = MockNetworkRepository::new();
3134
3135        let relayer = create_test_relayer();
3136        let mut test_tx = create_test_transaction();
3137        test_tx.status = TransactionStatus::Sent;
3138        test_tx.sent_at = Some(Utc::now().to_rfc3339());
3139        test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3140            nonce: Some(42),
3141            hash: Some("0xhash".to_string()),
3142            raw: Some(vec![1, 2, 3]),
3143            ..test_tx.network_data.get_evm_transaction_data().unwrap()
3144        });
3145
3146        // Provider returns "already known" error
3147        mock_provider
3148            .expect_send_raw_transaction()
3149            .times(1)
3150            .returning(|_| {
3151                Box::pin(async {
3152                    Err(crate::services::provider::ProviderError::Other(
3153                        "already known: transaction already in mempool".to_string(),
3154                    ))
3155                })
3156            });
3157
3158        // Should still update to Submitted status
3159        let test_tx_clone = test_tx.clone();
3160        mock_transaction
3161            .expect_partial_update()
3162            .times(1)
3163            .withf(|_, update| update.status == Some(TransactionStatus::Submitted))
3164            .returning(move |_, update| {
3165                let mut updated_tx = test_tx_clone.clone();
3166                updated_tx.status = update.status.unwrap();
3167                updated_tx.sent_at = update.sent_at.clone();
3168                Ok(updated_tx)
3169            });
3170
3171        mock_job_producer
3172            .expect_produce_send_notification_job()
3173            .times(1)
3174            .returning(|_, _| Box::pin(ready(Ok(()))));
3175
3176        let evm_transaction = EvmRelayerTransaction {
3177            relayer: relayer.clone(),
3178            provider: mock_provider,
3179            relayer_repository: Arc::new(mock_relayer),
3180            network_repository: Arc::new(mock_network),
3181            transaction_repository: Arc::new(mock_transaction),
3182            transaction_counter_service: Arc::new(counter_service),
3183            job_producer: Arc::new(mock_job_producer),
3184            price_calculator: mock_price_calculator,
3185            signer: mock_signer,
3186        };
3187
3188        let result = evm_transaction.submit_transaction(test_tx).await;
3189        assert!(result.is_ok());
3190        let updated_tx = result.unwrap();
3191        assert_eq!(updated_tx.status, TransactionStatus::Submitted);
3192    }
3193
3194    /// Test submit_transaction with real error (not "already known") should fail
3195    #[tokio::test]
3196    async fn test_submit_transaction_real_error_fails() {
3197        let mock_transaction = MockTransactionRepository::new();
3198        let mock_relayer = MockRelayerRepository::new();
3199        let mut mock_provider = MockEvmProviderTrait::new();
3200        let mock_signer = MockSigner::new();
3201        let mock_job_producer = MockJobProducerTrait::new();
3202        let mock_price_calculator = MockPriceCalculator::new();
3203        let counter_service = MockTransactionCounterTrait::new();
3204        let mock_network = MockNetworkRepository::new();
3205
3206        let relayer = create_test_relayer();
3207        let mut test_tx = create_test_transaction();
3208        test_tx.status = TransactionStatus::Sent;
3209        test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3210            raw: Some(vec![1, 2, 3]),
3211            ..test_tx.network_data.get_evm_transaction_data().unwrap()
3212        });
3213
3214        // Provider returns a real error
3215        mock_provider
3216            .expect_send_raw_transaction()
3217            .times(1)
3218            .returning(|_| {
3219                Box::pin(async {
3220                    Err(crate::services::provider::ProviderError::Other(
3221                        "insufficient funds for gas * price + value".to_string(),
3222                    ))
3223                })
3224            });
3225
3226        let evm_transaction = EvmRelayerTransaction {
3227            relayer: relayer.clone(),
3228            provider: mock_provider,
3229            relayer_repository: Arc::new(mock_relayer),
3230            network_repository: Arc::new(mock_network),
3231            transaction_repository: Arc::new(mock_transaction),
3232            transaction_counter_service: Arc::new(counter_service),
3233            job_producer: Arc::new(mock_job_producer),
3234            price_calculator: mock_price_calculator,
3235            signer: mock_signer,
3236        };
3237
3238        let result = evm_transaction.submit_transaction(test_tx).await;
3239        assert!(result.is_err());
3240    }
3241
3242    /// Test resubmit_transaction when transaction is already submitted
3243    /// Should NOT update hash, only status
3244    #[tokio::test]
3245    async fn test_resubmit_transaction_already_submitted_preserves_hash() {
3246        let mut mock_transaction = MockTransactionRepository::new();
3247        let mock_relayer = MockRelayerRepository::new();
3248        let mut mock_provider = MockEvmProviderTrait::new();
3249        let mut mock_signer = MockSigner::new();
3250        let mock_job_producer = MockJobProducerTrait::new();
3251        let mut mock_price_calculator = MockPriceCalculator::new();
3252        let counter_service = MockTransactionCounterTrait::new();
3253        let mock_network = MockNetworkRepository::new();
3254
3255        let relayer = create_test_relayer();
3256        let mut test_tx = create_test_transaction();
3257        test_tx.status = TransactionStatus::Submitted;
3258        test_tx.sent_at = Some(Utc::now().to_rfc3339());
3259        let original_hash = "0xoriginal_hash".to_string();
3260        test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3261            nonce: Some(42),
3262            hash: Some(original_hash.clone()),
3263            raw: Some(vec![1, 2, 3]),
3264            ..test_tx.network_data.get_evm_transaction_data().unwrap()
3265        });
3266        test_tx.hashes = vec![original_hash.clone()];
3267
3268        // Price calculator returns bumped price
3269        mock_price_calculator
3270            .expect_calculate_bumped_gas_price()
3271            .times(1)
3272            .returning(|_, _, _| {
3273                Ok(PriceParams {
3274                    gas_price: Some(25000000000), // 25% bump
3275                    max_fee_per_gas: None,
3276                    max_priority_fee_per_gas: None,
3277                    is_min_bumped: Some(true),
3278                    extra_fee: None,
3279                    total_cost: U256::from(525000000000000u64),
3280                })
3281            });
3282
3283        // Balance check passes
3284        mock_provider
3285            .expect_get_balance()
3286            .times(1)
3287            .returning(|_| Box::pin(async { Ok(U256::from(1000000000000000000u64)) }));
3288
3289        // Signer creates new transaction with new hash
3290        mock_signer
3291            .expect_sign_transaction()
3292            .times(1)
3293            .returning(|_| {
3294                Box::pin(ready(Ok(
3295                    crate::domain::relayer::SignTransactionResponse::Evm(
3296                        crate::domain::relayer::SignTransactionResponseEvm {
3297                            hash: "0xnew_hash_that_should_not_be_saved".to_string(),
3298                            signature: crate::models::EvmTransactionDataSignature {
3299                                r: "r".to_string(),
3300                                s: "s".to_string(),
3301                                v: 1,
3302                                sig: "0xsignature".to_string(),
3303                            },
3304                            raw: vec![4, 5, 6],
3305                        },
3306                    ),
3307                )))
3308            });
3309
3310        // Provider returns "already known" - transaction is already in mempool
3311        mock_provider
3312            .expect_send_raw_transaction()
3313            .times(1)
3314            .returning(|_| {
3315                Box::pin(async {
3316                    Err(crate::services::provider::ProviderError::Other(
3317                        "already known: transaction with same nonce already in mempool".to_string(),
3318                    ))
3319                })
3320            });
3321
3322        // Verify that partial_update is called with NO network_data (preserving original hash)
3323        let test_tx_clone = test_tx.clone();
3324        mock_transaction
3325            .expect_partial_update()
3326            .times(1)
3327            .withf(|_, update| {
3328                // Should only update status, NOT network_data or hashes
3329                update.status == Some(TransactionStatus::Submitted)
3330                    && update.network_data.is_none()
3331                    && update.hashes.is_none()
3332            })
3333            .returning(move |_, _| {
3334                let mut updated_tx = test_tx_clone.clone();
3335                updated_tx.status = TransactionStatus::Submitted;
3336                // Hash should remain unchanged!
3337                Ok(updated_tx)
3338            });
3339
3340        let evm_transaction = EvmRelayerTransaction {
3341            relayer: relayer.clone(),
3342            provider: mock_provider,
3343            relayer_repository: Arc::new(mock_relayer),
3344            network_repository: Arc::new(mock_network),
3345            transaction_repository: Arc::new(mock_transaction),
3346            transaction_counter_service: Arc::new(counter_service),
3347            job_producer: Arc::new(mock_job_producer),
3348            price_calculator: mock_price_calculator,
3349            signer: mock_signer,
3350        };
3351
3352        let result = evm_transaction.resubmit_transaction(test_tx.clone()).await;
3353        assert!(result.is_ok());
3354        let updated_tx = result.unwrap();
3355
3356        // Verify hash was NOT changed
3357        if let NetworkTransactionData::Evm(evm_data) = &updated_tx.network_data {
3358            assert_eq!(evm_data.hash, Some(original_hash));
3359        } else {
3360            panic!("Expected EVM network data");
3361        }
3362    }
3363
3364    /// Test submit_transaction with database update failure
3365    /// Transaction is on-chain, but DB update fails - should return Ok with original tx
3366    #[tokio::test]
3367    async fn test_submit_transaction_db_failure_after_blockchain_success() {
3368        let mut mock_transaction = MockTransactionRepository::new();
3369        let mock_relayer = MockRelayerRepository::new();
3370        let mut mock_provider = MockEvmProviderTrait::new();
3371        let mock_signer = MockSigner::new();
3372        let mut mock_job_producer = MockJobProducerTrait::new();
3373        let mock_price_calculator = MockPriceCalculator::new();
3374        let counter_service = MockTransactionCounterTrait::new();
3375        let mock_network = MockNetworkRepository::new();
3376
3377        let relayer = create_test_relayer();
3378        let mut test_tx = create_test_transaction();
3379        test_tx.status = TransactionStatus::Sent;
3380        test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3381            raw: Some(vec![1, 2, 3]),
3382            ..test_tx.network_data.get_evm_transaction_data().unwrap()
3383        });
3384
3385        // Provider succeeds
3386        mock_provider
3387            .expect_send_raw_transaction()
3388            .times(1)
3389            .returning(|_| Box::pin(async { Ok("0xsubmitted_hash".to_string()) }));
3390
3391        // But database update fails
3392        mock_transaction
3393            .expect_partial_update()
3394            .times(1)
3395            .returning(|_, _| {
3396                Err(crate::models::RepositoryError::UnexpectedError(
3397                    "Redis timeout".to_string(),
3398                ))
3399            });
3400
3401        // Notification will still be sent (with original tx data)
3402        mock_job_producer
3403            .expect_produce_send_notification_job()
3404            .times(1)
3405            .returning(|_, _| Box::pin(ready(Ok(()))));
3406
3407        let evm_transaction = EvmRelayerTransaction {
3408            relayer: relayer.clone(),
3409            provider: mock_provider,
3410            relayer_repository: Arc::new(mock_relayer),
3411            network_repository: Arc::new(mock_network),
3412            transaction_repository: Arc::new(mock_transaction),
3413            transaction_counter_service: Arc::new(counter_service),
3414            job_producer: Arc::new(mock_job_producer),
3415            price_calculator: mock_price_calculator,
3416            signer: mock_signer,
3417        };
3418
3419        let result = evm_transaction.submit_transaction(test_tx.clone()).await;
3420        // Should return Ok (transaction is on-chain, don't retry)
3421        assert!(result.is_ok());
3422        let returned_tx = result.unwrap();
3423        // Should return original tx since DB update failed
3424        assert_eq!(returned_tx.id, test_tx.id);
3425        assert_eq!(returned_tx.status, TransactionStatus::Sent); // Original status
3426    }
3427
3428    /// Test send_transaction_resend_job success
3429    #[tokio::test]
3430    async fn test_send_transaction_resend_job_success() {
3431        let mock_transaction = MockTransactionRepository::new();
3432        let mock_relayer = MockRelayerRepository::new();
3433        let mock_provider = MockEvmProviderTrait::new();
3434        let mock_signer = MockSigner::new();
3435        let mut mock_job_producer = MockJobProducerTrait::new();
3436        let mock_price_calculator = MockPriceCalculator::new();
3437        let counter_service = MockTransactionCounterTrait::new();
3438        let mock_network = MockNetworkRepository::new();
3439
3440        let relayer = create_test_relayer();
3441        let test_tx = create_test_transaction();
3442
3443        // Expect produce_submit_transaction_job to be called with resend job
3444        mock_job_producer
3445            .expect_produce_submit_transaction_job()
3446            .times(1)
3447            .withf(|job, delay| {
3448                // Verify it's a resend job with correct IDs
3449                job.transaction_id == "test-tx-id"
3450                    && job.relayer_id == "test-relayer-id"
3451                    && matches!(job.command, crate::jobs::TransactionCommand::Resend)
3452                    && delay.is_none()
3453            })
3454            .returning(|_, _| Box::pin(ready(Ok(()))));
3455
3456        let evm_transaction = EvmRelayerTransaction {
3457            relayer: relayer.clone(),
3458            provider: mock_provider,
3459            relayer_repository: Arc::new(mock_relayer),
3460            network_repository: Arc::new(mock_network),
3461            transaction_repository: Arc::new(mock_transaction),
3462            transaction_counter_service: Arc::new(counter_service),
3463            job_producer: Arc::new(mock_job_producer),
3464            price_calculator: mock_price_calculator,
3465            signer: mock_signer,
3466        };
3467
3468        let result = evm_transaction.send_transaction_resend_job(&test_tx).await;
3469        assert!(result.is_ok());
3470    }
3471
3472    /// Test send_transaction_resend_job failure
3473    #[tokio::test]
3474    async fn test_send_transaction_resend_job_failure() {
3475        let mock_transaction = MockTransactionRepository::new();
3476        let mock_relayer = MockRelayerRepository::new();
3477        let mock_provider = MockEvmProviderTrait::new();
3478        let mock_signer = MockSigner::new();
3479        let mut mock_job_producer = MockJobProducerTrait::new();
3480        let mock_price_calculator = MockPriceCalculator::new();
3481        let counter_service = MockTransactionCounterTrait::new();
3482        let mock_network = MockNetworkRepository::new();
3483
3484        let relayer = create_test_relayer();
3485        let test_tx = create_test_transaction();
3486
3487        // Job producer returns an error
3488        mock_job_producer
3489            .expect_produce_submit_transaction_job()
3490            .times(1)
3491            .returning(|_, _| {
3492                Box::pin(ready(Err(crate::jobs::JobProducerError::QueueError(
3493                    "Job queue is full".to_string(),
3494                ))))
3495            });
3496
3497        let evm_transaction = EvmRelayerTransaction {
3498            relayer: relayer.clone(),
3499            provider: mock_provider,
3500            relayer_repository: Arc::new(mock_relayer),
3501            network_repository: Arc::new(mock_network),
3502            transaction_repository: Arc::new(mock_transaction),
3503            transaction_counter_service: Arc::new(counter_service),
3504            job_producer: Arc::new(mock_job_producer),
3505            price_calculator: mock_price_calculator,
3506            signer: mock_signer,
3507        };
3508
3509        let result = evm_transaction.send_transaction_resend_job(&test_tx).await;
3510        assert!(result.is_err());
3511        let err = result.unwrap_err();
3512        match err {
3513            TransactionError::UnexpectedError(msg) => {
3514                assert!(msg.contains("Failed to produce resend job"));
3515            }
3516            _ => panic!("Expected UnexpectedError"),
3517        }
3518    }
3519
3520    /// Test send_transaction_request_job success
3521    #[tokio::test]
3522    async fn test_send_transaction_request_job_success() {
3523        let mock_transaction = MockTransactionRepository::new();
3524        let mock_relayer = MockRelayerRepository::new();
3525        let mock_provider = MockEvmProviderTrait::new();
3526        let mock_signer = MockSigner::new();
3527        let mut mock_job_producer = MockJobProducerTrait::new();
3528        let mock_price_calculator = MockPriceCalculator::new();
3529        let counter_service = MockTransactionCounterTrait::new();
3530        let mock_network = MockNetworkRepository::new();
3531
3532        let relayer = create_test_relayer();
3533        let test_tx = create_test_transaction();
3534
3535        // Expect produce_transaction_request_job to be called
3536        mock_job_producer
3537            .expect_produce_transaction_request_job()
3538            .times(1)
3539            .withf(|job, delay| {
3540                // Verify correct transaction ID and relayer ID
3541                job.transaction_id == "test-tx-id"
3542                    && job.relayer_id == "test-relayer-id"
3543                    && delay.is_none()
3544            })
3545            .returning(|_, _| Box::pin(ready(Ok(()))));
3546
3547        let evm_transaction = EvmRelayerTransaction {
3548            relayer: relayer.clone(),
3549            provider: mock_provider,
3550            relayer_repository: Arc::new(mock_relayer),
3551            network_repository: Arc::new(mock_network),
3552            transaction_repository: Arc::new(mock_transaction),
3553            transaction_counter_service: Arc::new(counter_service),
3554            job_producer: Arc::new(mock_job_producer),
3555            price_calculator: mock_price_calculator,
3556            signer: mock_signer,
3557        };
3558
3559        let result = evm_transaction.send_transaction_request_job(&test_tx).await;
3560        assert!(result.is_ok());
3561    }
3562
3563    /// Test send_transaction_request_job failure
3564    #[tokio::test]
3565    async fn test_send_transaction_request_job_failure() {
3566        let mock_transaction = MockTransactionRepository::new();
3567        let mock_relayer = MockRelayerRepository::new();
3568        let mock_provider = MockEvmProviderTrait::new();
3569        let mock_signer = MockSigner::new();
3570        let mut mock_job_producer = MockJobProducerTrait::new();
3571        let mock_price_calculator = MockPriceCalculator::new();
3572        let counter_service = MockTransactionCounterTrait::new();
3573        let mock_network = MockNetworkRepository::new();
3574
3575        let relayer = create_test_relayer();
3576        let test_tx = create_test_transaction();
3577
3578        // Job producer returns an error
3579        mock_job_producer
3580            .expect_produce_transaction_request_job()
3581            .times(1)
3582            .returning(|_, _| {
3583                Box::pin(ready(Err(crate::jobs::JobProducerError::QueueError(
3584                    "Redis connection failed".to_string(),
3585                ))))
3586            });
3587
3588        let evm_transaction = EvmRelayerTransaction {
3589            relayer: relayer.clone(),
3590            provider: mock_provider,
3591            relayer_repository: Arc::new(mock_relayer),
3592            network_repository: Arc::new(mock_network),
3593            transaction_repository: Arc::new(mock_transaction),
3594            transaction_counter_service: Arc::new(counter_service),
3595            job_producer: Arc::new(mock_job_producer),
3596            price_calculator: mock_price_calculator,
3597            signer: mock_signer,
3598        };
3599
3600        let result = evm_transaction.send_transaction_request_job(&test_tx).await;
3601        assert!(result.is_err());
3602        let err = result.unwrap_err();
3603        match err {
3604            TransactionError::UnexpectedError(msg) => {
3605                assert!(msg.contains("Failed to produce request job"));
3606            }
3607            _ => panic!("Expected UnexpectedError"),
3608        }
3609    }
3610
3611    /// Test resubmit_transaction successfully transitions from Sent to Submitted status
3612    #[tokio::test]
3613    async fn test_resubmit_transaction_sent_to_submitted() {
3614        let mut mock_transaction = MockTransactionRepository::new();
3615        let mock_relayer = MockRelayerRepository::new();
3616        let mut mock_provider = MockEvmProviderTrait::new();
3617        let mut mock_signer = MockSigner::new();
3618        let mock_job_producer = MockJobProducerTrait::new();
3619        let mut mock_price_calculator = MockPriceCalculator::new();
3620        let counter_service = MockTransactionCounterTrait::new();
3621        let mock_network = MockNetworkRepository::new();
3622
3623        let relayer = create_test_relayer();
3624        let mut test_tx = create_test_transaction();
3625        test_tx.status = TransactionStatus::Sent;
3626        test_tx.sent_at = Some(Utc::now().to_rfc3339());
3627        let original_hash = "0xoriginal_hash".to_string();
3628        test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3629            nonce: Some(42),
3630            hash: Some(original_hash.clone()),
3631            raw: Some(vec![1, 2, 3]),
3632            gas_price: Some(20000000000), // 20 Gwei
3633            ..test_tx.network_data.get_evm_transaction_data().unwrap()
3634        });
3635        test_tx.hashes = vec![original_hash.clone()];
3636
3637        // Price calculator returns bumped price
3638        mock_price_calculator
3639            .expect_calculate_bumped_gas_price()
3640            .times(1)
3641            .returning(|_, _, _| {
3642                Ok(PriceParams {
3643                    gas_price: Some(25000000000), // 25 Gwei (25% bump)
3644                    max_fee_per_gas: None,
3645                    max_priority_fee_per_gas: None,
3646                    is_min_bumped: Some(true),
3647                    extra_fee: None,
3648                    total_cost: U256::from(525000000000000u64),
3649                })
3650            });
3651
3652        // Mock balance check
3653        mock_provider
3654            .expect_get_balance()
3655            .returning(|_| Box::pin(ready(Ok(U256::from(1000000000000000000u64)))));
3656
3657        // Mock signer to return new signed transaction
3658        mock_signer.expect_sign_transaction().returning(|_| {
3659            Box::pin(ready(Ok(
3660                crate::domain::relayer::SignTransactionResponse::Evm(
3661                    crate::domain::relayer::SignTransactionResponseEvm {
3662                        hash: "0xnew_hash".to_string(),
3663                        signature: crate::models::EvmTransactionDataSignature {
3664                            r: "r".to_string(),
3665                            s: "s".to_string(),
3666                            v: 1,
3667                            sig: "0xsignature".to_string(),
3668                        },
3669                        raw: vec![4, 5, 6],
3670                    },
3671                ),
3672            )))
3673        });
3674
3675        // Provider successfully sends the resubmitted transaction
3676        mock_provider
3677            .expect_send_raw_transaction()
3678            .times(1)
3679            .returning(|_| Box::pin(async { Ok("0xnew_hash".to_string()) }));
3680
3681        // Should update to Submitted status with new hash
3682        let test_tx_clone = test_tx.clone();
3683        mock_transaction
3684            .expect_partial_update()
3685            .times(1)
3686            .withf(|_, update| {
3687                update.status == Some(TransactionStatus::Submitted)
3688                    && update.sent_at.is_some()
3689                    && update.priced_at.is_some()
3690                    && update.hashes.is_some()
3691            })
3692            .returning(move |_, update| {
3693                let mut updated_tx = test_tx_clone.clone();
3694                updated_tx.status = update.status.unwrap();
3695                updated_tx.sent_at = update.sent_at.clone();
3696                updated_tx.priced_at = update.priced_at.clone();
3697                if let Some(hashes) = update.hashes.clone() {
3698                    updated_tx.hashes = hashes;
3699                }
3700                if let Some(network_data) = update.network_data.clone() {
3701                    updated_tx.network_data = network_data;
3702                }
3703                Ok(updated_tx)
3704            });
3705
3706        let evm_transaction = EvmRelayerTransaction {
3707            relayer: relayer.clone(),
3708            provider: mock_provider,
3709            relayer_repository: Arc::new(mock_relayer),
3710            network_repository: Arc::new(mock_network),
3711            transaction_repository: Arc::new(mock_transaction),
3712            transaction_counter_service: Arc::new(counter_service),
3713            job_producer: Arc::new(mock_job_producer),
3714            price_calculator: mock_price_calculator,
3715            signer: mock_signer,
3716        };
3717
3718        let result = evm_transaction.resubmit_transaction(test_tx.clone()).await;
3719        assert!(result.is_ok(), "Expected Ok, got: {result:?}");
3720        let updated_tx = result.unwrap();
3721        assert_eq!(
3722            updated_tx.status,
3723            TransactionStatus::Submitted,
3724            "Transaction status should transition from Sent to Submitted"
3725        );
3726    }
3727
3728    #[test]
3729    fn test_classify_submission_error_nonce_too_low() {
3730        assert_eq!(
3731            DefaultEvmTransaction::classify_submission_error(&"nonce too low"),
3732            SubmissionErrorKind::NonceTooLow
3733        );
3734        assert_eq!(
3735            DefaultEvmTransaction::classify_submission_error(&"Nonce Too Low"),
3736            SubmissionErrorKind::NonceTooLow
3737        );
3738        assert_eq!(
3739            DefaultEvmTransaction::classify_submission_error(&"nonce is too low"),
3740            SubmissionErrorKind::NonceTooLow
3741        );
3742    }
3743
3744    #[test]
3745    fn test_classify_submission_error_already_known() {
3746        assert_eq!(
3747            DefaultEvmTransaction::classify_submission_error(&"already known"),
3748            SubmissionErrorKind::AlreadyKnown
3749        );
3750        assert_eq!(
3751            DefaultEvmTransaction::classify_submission_error(&"known transaction"),
3752            SubmissionErrorKind::AlreadyKnown
3753        );
3754        assert_eq!(
3755            DefaultEvmTransaction::classify_submission_error(&"same hash was already imported"),
3756            SubmissionErrorKind::AlreadyKnown
3757        );
3758        // "unknown transaction" must NOT match
3759        assert!(matches!(
3760            DefaultEvmTransaction::classify_submission_error(&"unknown transaction"),
3761            SubmissionErrorKind::Other(_)
3762        ));
3763    }
3764
3765    #[test]
3766    fn test_classify_submission_error_replacement_underpriced() {
3767        assert_eq!(
3768            DefaultEvmTransaction::classify_submission_error(
3769                &"replacement transaction underpriced"
3770            ),
3771            SubmissionErrorKind::ReplacementUnderpriced
3772        );
3773    }
3774
3775    #[test]
3776    fn test_classify_submission_error_other() {
3777        assert!(matches!(
3778            DefaultEvmTransaction::classify_submission_error(&"execution reverted"),
3779            SubmissionErrorKind::Other(_)
3780        ));
3781        assert!(matches!(
3782            DefaultEvmTransaction::classify_submission_error(&"gas too low"),
3783            SubmissionErrorKind::Other(_)
3784        ));
3785        // insufficient funds is not a nonce-related error — maps to Other
3786        assert!(matches!(
3787            DefaultEvmTransaction::classify_submission_error(
3788                &"insufficient funds for gas * price + value"
3789            ),
3790            SubmissionErrorKind::Other(_)
3791        ));
3792    }
3793
3794    #[test]
3795    fn test_classify_submission_error_nonce_too_high() {
3796        assert_eq!(
3797            DefaultEvmTransaction::classify_submission_error(&"nonce too high"),
3798            SubmissionErrorKind::NonceTooHigh
3799        );
3800        assert_eq!(
3801            DefaultEvmTransaction::classify_submission_error(&"exceeds next nonce"),
3802            SubmissionErrorKind::NonceTooHigh
3803        );
3804        assert_eq!(
3805            DefaultEvmTransaction::classify_submission_error(&"nonce too far in the future"),
3806            SubmissionErrorKind::NonceTooHigh
3807        );
3808        assert_eq!(
3809            DefaultEvmTransaction::classify_submission_error(&"nonce out of range"),
3810            SubmissionErrorKind::NonceTooHigh
3811        );
3812    }
3813
3814    #[test]
3815    fn test_classify_submission_error_nonce_too_high_case_insensitive() {
3816        assert_eq!(
3817            DefaultEvmTransaction::classify_submission_error(&"Nonce Too High"),
3818            SubmissionErrorKind::NonceTooHigh
3819        );
3820        assert_eq!(
3821            DefaultEvmTransaction::classify_submission_error(&"NONCE OUT OF RANGE"),
3822            SubmissionErrorKind::NonceTooHigh
3823        );
3824        assert_eq!(
3825            DefaultEvmTransaction::classify_submission_error(&"Exceeds Next Nonce"),
3826            SubmissionErrorKind::NonceTooHigh
3827        );
3828    }
3829
3830    /// Test submit_transaction with NonceTooLow on non-Sent (Submitted) tx
3831    /// Should return Ok and schedule nonce recovery status check
3832    #[tokio::test]
3833    async fn test_submit_transaction_nonce_too_low_on_submitted_schedules_recovery() {
3834        let mut mock_transaction = MockTransactionRepository::new();
3835        let mock_relayer = MockRelayerRepository::new();
3836        let mut mock_provider = MockEvmProviderTrait::new();
3837        let mock_signer = MockSigner::new();
3838        let mut mock_job_producer = MockJobProducerTrait::new();
3839        let mock_price_calculator = MockPriceCalculator::new();
3840        let counter_service = MockTransactionCounterTrait::new();
3841        let mock_network = MockNetworkRepository::new();
3842
3843        let relayer = create_test_relayer();
3844        let mut test_tx = create_test_transaction();
3845        test_tx.status = TransactionStatus::Submitted;
3846        test_tx.sent_at = Some(Utc::now().to_rfc3339());
3847        test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3848            nonce: Some(42),
3849            hash: Some("0xhash".to_string()),
3850            raw: Some(vec![1, 2, 3]),
3851            ..test_tx.network_data.get_evm_transaction_data().unwrap()
3852        });
3853
3854        // Provider returns "nonce too low" error
3855        mock_provider
3856            .expect_send_raw_transaction()
3857            .times(1)
3858            .returning(|_| {
3859                Box::pin(async {
3860                    Err(crate::services::provider::ProviderError::Other(
3861                        "nonce too low".to_string(),
3862                    ))
3863                })
3864            });
3865
3866        // Should persist status_reason
3867        let test_tx_clone = test_tx.clone();
3868        mock_transaction
3869            .expect_partial_update()
3870            .times(1)
3871            .withf(|_, update| update.status_reason.is_some())
3872            .returning(move |_, _| Ok(test_tx_clone.clone()));
3873
3874        // Should schedule nonce recovery status check
3875        mock_job_producer
3876            .expect_produce_check_transaction_status_job()
3877            .times(1)
3878            .withf(|job, _| {
3879                job.metadata
3880                    .as_ref()
3881                    .map(|m| m.contains_key(TX_NONCE_RECONCILE_TRIGGER))
3882                    .unwrap_or(false)
3883            })
3884            .returning(|_, _| Box::pin(ready(Ok(()))));
3885
3886        let evm_transaction = EvmRelayerTransaction {
3887            relayer: relayer.clone(),
3888            provider: mock_provider,
3889            relayer_repository: Arc::new(mock_relayer),
3890            network_repository: Arc::new(mock_network),
3891            transaction_repository: Arc::new(mock_transaction),
3892            transaction_counter_service: Arc::new(counter_service),
3893            job_producer: Arc::new(mock_job_producer),
3894            price_calculator: mock_price_calculator,
3895            signer: mock_signer,
3896        };
3897
3898        // Should return Ok (not error → Dead Queue)
3899        let result = evm_transaction.submit_transaction(test_tx).await;
3900        assert!(
3901            result.is_ok(),
3902            "Expected Ok on nonce error for non-Sent tx, got: {result:?}"
3903        );
3904    }
3905
3906    /// Test submit_transaction with NonceTooLow on Sent tx schedules nonce recovery.
3907    /// NonceTooLow means the nonce was consumed, but we don't know by whom — could be
3908    /// our tx (retry after crash) or a different tx (multi-instance / external wallet).
3909    /// Must NOT blindly advance to Submitted; instead schedule reconciliation.
3910    #[tokio::test]
3911    async fn test_submit_transaction_nonce_too_low_on_sent_schedules_recovery() {
3912        let mut mock_transaction = MockTransactionRepository::new();
3913        let mock_relayer = MockRelayerRepository::new();
3914        let mut mock_provider = MockEvmProviderTrait::new();
3915        let mock_signer = MockSigner::new();
3916        let mut mock_job_producer = MockJobProducerTrait::new();
3917        let mock_price_calculator = MockPriceCalculator::new();
3918        let counter_service = MockTransactionCounterTrait::new();
3919        let mock_network = MockNetworkRepository::new();
3920
3921        let relayer = create_test_relayer();
3922        let mut test_tx = create_test_transaction();
3923        test_tx.status = TransactionStatus::Sent;
3924        test_tx.sent_at = Some(Utc::now().to_rfc3339());
3925        test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3926            nonce: Some(42),
3927            hash: Some("0xhash".to_string()),
3928            raw: Some(vec![1, 2, 3]),
3929            ..test_tx.network_data.get_evm_transaction_data().unwrap()
3930        });
3931
3932        // Provider returns "nonce too low" error
3933        mock_provider
3934            .expect_send_raw_transaction()
3935            .times(1)
3936            .returning(|_| {
3937                Box::pin(async {
3938                    Err(crate::services::provider::ProviderError::Other(
3939                        "nonce too low".to_string(),
3940                    ))
3941                })
3942            });
3943
3944        // Should persist status_reason (nonce recovery path)
3945        let test_tx_clone = test_tx.clone();
3946        mock_transaction
3947            .expect_partial_update()
3948            .times(1)
3949            .withf(|_, update| update.status_reason.is_some())
3950            .returning(move |_, _| Ok(test_tx_clone.clone()));
3951
3952        // Should schedule nonce recovery status check
3953        mock_job_producer
3954            .expect_produce_check_transaction_status_job()
3955            .times(1)
3956            .withf(|job, _| {
3957                job.metadata
3958                    .as_ref()
3959                    .map(|m| m.contains_key(TX_NONCE_RECONCILE_TRIGGER))
3960                    .unwrap_or(false)
3961            })
3962            .returning(|_, _| Box::pin(ready(Ok(()))));
3963
3964        let evm_transaction = EvmRelayerTransaction {
3965            relayer: relayer.clone(),
3966            provider: mock_provider,
3967            relayer_repository: Arc::new(mock_relayer),
3968            network_repository: Arc::new(mock_network),
3969            transaction_repository: Arc::new(mock_transaction),
3970            transaction_counter_service: Arc::new(counter_service),
3971            job_producer: Arc::new(mock_job_producer),
3972            price_calculator: mock_price_calculator,
3973            signer: mock_signer,
3974        };
3975
3976        // Should return Ok (not error → Dead Queue) but NOT advance to Submitted
3977        let result = evm_transaction.submit_transaction(test_tx.clone()).await;
3978        assert!(
3979            result.is_ok(),
3980            "Expected Ok on nonce too low for Sent tx, got: {result:?}"
3981        );
3982        // Status should remain Sent — reconciliation happens via status checker
3983        let returned_tx = result.unwrap();
3984        assert_eq!(returned_tx.status, TransactionStatus::Sent);
3985    }
3986
3987    /// Test resubmit_transaction with NonceTooLow schedules recovery and treats as already submitted
3988    #[tokio::test]
3989    async fn test_resubmit_transaction_nonce_too_low_schedules_recovery() {
3990        let mut mock_transaction = MockTransactionRepository::new();
3991        let mock_relayer = MockRelayerRepository::new();
3992        let mut mock_provider = MockEvmProviderTrait::new();
3993        let mut mock_signer = MockSigner::new();
3994        let mut mock_job_producer = MockJobProducerTrait::new();
3995        let mut mock_price_calculator = MockPriceCalculator::new();
3996        let counter_service = MockTransactionCounterTrait::new();
3997        let mock_network = MockNetworkRepository::new();
3998
3999        let relayer = create_test_relayer();
4000        let mut test_tx = create_test_transaction();
4001        test_tx.status = TransactionStatus::Submitted;
4002        test_tx.sent_at = Some(Utc::now().to_rfc3339());
4003        let original_hash = "0xoriginal_hash".to_string();
4004        test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
4005            nonce: Some(42),
4006            hash: Some(original_hash.clone()),
4007            raw: Some(vec![1, 2, 3]),
4008            ..test_tx.network_data.get_evm_transaction_data().unwrap()
4009        });
4010        test_tx.hashes = vec![original_hash.clone()];
4011
4012        // Price calculator returns bumped price
4013        mock_price_calculator
4014            .expect_calculate_bumped_gas_price()
4015            .times(1)
4016            .returning(|_, _, _| {
4017                Ok(PriceParams {
4018                    gas_price: Some(25000000000),
4019                    max_fee_per_gas: None,
4020                    max_priority_fee_per_gas: None,
4021                    is_min_bumped: Some(true),
4022                    extra_fee: None,
4023                    total_cost: U256::from(525000000000000u64),
4024                })
4025            });
4026
4027        // Balance check passes
4028        mock_provider
4029            .expect_get_balance()
4030            .times(1)
4031            .returning(|_| Box::pin(async { Ok(U256::from(1000000000000000000u64)) }));
4032
4033        // Signer creates new transaction
4034        mock_signer
4035            .expect_sign_transaction()
4036            .times(1)
4037            .returning(|_| {
4038                Box::pin(ready(Ok(
4039                    crate::domain::relayer::SignTransactionResponse::Evm(
4040                        crate::domain::relayer::SignTransactionResponseEvm {
4041                            hash: "0xnew_hash".to_string(),
4042                            signature: crate::models::EvmTransactionDataSignature {
4043                                r: "r".to_string(),
4044                                s: "s".to_string(),
4045                                v: 1,
4046                                sig: "0xsignature".to_string(),
4047                            },
4048                            raw: vec![4, 5, 6],
4049                        },
4050                    ),
4051                )))
4052            });
4053
4054        // Provider returns "nonce too low"
4055        mock_provider
4056            .expect_send_raw_transaction()
4057            .times(1)
4058            .returning(|_| {
4059                Box::pin(async {
4060                    Err(crate::services::provider::ProviderError::Other(
4061                        "nonce too low".to_string(),
4062                    ))
4063                })
4064            });
4065
4066        // Should schedule nonce recovery status check
4067        mock_job_producer
4068            .expect_produce_check_transaction_status_job()
4069            .times(1)
4070            .withf(|job, _| {
4071                job.metadata
4072                    .as_ref()
4073                    .map(|m| m.contains_key(TX_NONCE_RECONCILE_TRIGGER))
4074                    .unwrap_or(false)
4075            })
4076            .returning(|_, _| Box::pin(ready(Ok(()))));
4077
4078        // Should update status without changing hash (was_already_submitted = true)
4079        let test_tx_clone = test_tx.clone();
4080        mock_transaction
4081            .expect_partial_update()
4082            .times(1)
4083            .withf(|_, update| {
4084                update.status == Some(TransactionStatus::Submitted)
4085                    && update.network_data.is_none()
4086                    && update.hashes.is_none()
4087            })
4088            .returning(move |_, _| {
4089                let mut updated_tx = test_tx_clone.clone();
4090                updated_tx.status = TransactionStatus::Submitted;
4091                Ok(updated_tx)
4092            });
4093
4094        let evm_transaction = EvmRelayerTransaction {
4095            relayer: relayer.clone(),
4096            provider: mock_provider,
4097            relayer_repository: Arc::new(mock_relayer),
4098            network_repository: Arc::new(mock_network),
4099            transaction_repository: Arc::new(mock_transaction),
4100            transaction_counter_service: Arc::new(counter_service),
4101            job_producer: Arc::new(mock_job_producer),
4102            price_calculator: mock_price_calculator,
4103            signer: mock_signer,
4104        };
4105
4106        let result = evm_transaction.resubmit_transaction(test_tx.clone()).await;
4107        assert!(result.is_ok());
4108        let updated_tx = result.unwrap();
4109        // Hash should remain unchanged
4110        if let NetworkTransactionData::Evm(evm_data) = &updated_tx.network_data {
4111            assert_eq!(evm_data.hash, Some(original_hash));
4112        } else {
4113            panic!("Expected EVM network data");
4114        }
4115    }
4116
4117    /// Test submit_transaction with NonceTooHigh increments the retry counter in metadata
4118    #[tokio::test]
4119    async fn test_submit_transaction_nonce_too_high_increments_retry_counter() {
4120        let mut mock_transaction = MockTransactionRepository::new();
4121        let mock_relayer = MockRelayerRepository::new();
4122        let mut mock_provider = MockEvmProviderTrait::new();
4123        let mock_signer = MockSigner::new();
4124        let mock_job_producer = MockJobProducerTrait::new();
4125        let mock_price_calculator = MockPriceCalculator::new();
4126        let counter_service = MockTransactionCounterTrait::new();
4127        let mock_network = MockNetworkRepository::new();
4128
4129        let relayer = create_test_relayer();
4130        let mut test_tx = create_test_transaction();
4131        test_tx.status = TransactionStatus::Sent;
4132        test_tx.sent_at = Some(Utc::now().to_rfc3339());
4133        test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
4134            nonce: Some(10),
4135            hash: Some("0xhash".to_string()),
4136            raw: Some(vec![1, 2, 3]),
4137            ..test_tx.network_data.get_evm_transaction_data().unwrap()
4138        });
4139        // Start with nonce_too_high_retries = 0 (below threshold of 3)
4140        test_tx.metadata = Some(crate::models::TransactionMetadata {
4141            nonce_too_high_retries: 0,
4142            ..Default::default()
4143        });
4144
4145        // Provider returns "nonce too high" error
4146        mock_provider
4147            .expect_send_raw_transaction()
4148            .times(1)
4149            .returning(|_| {
4150                Box::pin(async {
4151                    Err(crate::services::provider::ProviderError::Other(
4152                        "nonce too high".to_string(),
4153                    ))
4154                })
4155            });
4156
4157        // Should persist incremented counter (nonce_too_high_retries = 1) in metadata
4158        let test_tx_clone = test_tx.clone();
4159        mock_transaction
4160            .expect_partial_update()
4161            .times(1)
4162            .withf(|_, update| {
4163                update
4164                    .metadata
4165                    .as_ref()
4166                    .map(|m| m.nonce_too_high_retries == 1)
4167                    .unwrap_or(false)
4168            })
4169            .returning(move |_, _| Ok(test_tx_clone.clone()));
4170
4171        let evm_transaction = EvmRelayerTransaction {
4172            relayer: relayer.clone(),
4173            provider: mock_provider,
4174            relayer_repository: Arc::new(mock_relayer),
4175            network_repository: Arc::new(mock_network),
4176            transaction_repository: Arc::new(mock_transaction),
4177            transaction_counter_service: Arc::new(counter_service),
4178            job_producer: Arc::new(mock_job_producer),
4179            price_calculator: mock_price_calculator,
4180            signer: mock_signer,
4181        };
4182
4183        // Should return Ok (not error → Dead Queue)
4184        let result = evm_transaction.submit_transaction(test_tx).await;
4185        assert!(
4186            result.is_ok(),
4187            "Expected Ok on nonce too high, got: {result:?}"
4188        );
4189    }
4190
4191    /// Test submit_transaction with NonceTooHigh schedules health check job at threshold
4192    /// When nonce_too_high_retries reaches MAX_NONCE_TOO_HIGH_RETRIES (3), a health job is produced
4193    #[tokio::test]
4194    async fn test_submit_transaction_nonce_too_high_schedules_health_job_at_threshold() {
4195        let mut mock_transaction = MockTransactionRepository::new();
4196        let mock_relayer = MockRelayerRepository::new();
4197        let mut mock_provider = MockEvmProviderTrait::new();
4198        let mock_signer = MockSigner::new();
4199        let mut mock_job_producer = MockJobProducerTrait::new();
4200        let mock_price_calculator = MockPriceCalculator::new();
4201        let counter_service = MockTransactionCounterTrait::new();
4202        let mock_network = MockNetworkRepository::new();
4203
4204        let relayer = create_test_relayer();
4205        let mut test_tx = create_test_transaction();
4206        test_tx.status = TransactionStatus::Sent;
4207        test_tx.sent_at = Some(Utc::now().to_rfc3339());
4208        test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
4209            nonce: Some(10),
4210            hash: Some("0xhash".to_string()),
4211            raw: Some(vec![1, 2, 3]),
4212            ..test_tx.network_data.get_evm_transaction_data().unwrap()
4213        });
4214        // Set retries to 2 so that after increment it becomes 3 = MAX_NONCE_TOO_HIGH_RETRIES
4215        test_tx.metadata = Some(crate::models::TransactionMetadata {
4216            nonce_too_high_retries: 2,
4217            ..Default::default()
4218        });
4219
4220        // Provider returns "nonce too high" error
4221        mock_provider
4222            .expect_send_raw_transaction()
4223            .times(1)
4224            .returning(|_| {
4225                Box::pin(async {
4226                    Err(crate::services::provider::ProviderError::Other(
4227                        "nonce too high".to_string(),
4228                    ))
4229                })
4230            });
4231
4232        // Should persist incremented counter (nonce_too_high_retries = 3) in metadata
4233        let test_tx_clone = test_tx.clone();
4234        mock_transaction
4235            .expect_partial_update()
4236            .times(1)
4237            .withf(|_, update| {
4238                update
4239                    .metadata
4240                    .as_ref()
4241                    .map(|m| m.nonce_too_high_retries == 3)
4242                    .unwrap_or(false)
4243            })
4244            .returning(move |_, _| Ok(test_tx_clone.clone()));
4245
4246        // Should schedule a relayer health check job at the threshold
4247        mock_job_producer
4248            .expect_produce_relayer_health_check_job()
4249            .times(1)
4250            .returning(|_, _| Box::pin(ready(Ok(()))));
4251
4252        let evm_transaction = EvmRelayerTransaction {
4253            relayer: relayer.clone(),
4254            provider: mock_provider,
4255            relayer_repository: Arc::new(mock_relayer),
4256            network_repository: Arc::new(mock_network),
4257            transaction_repository: Arc::new(mock_transaction),
4258            transaction_counter_service: Arc::new(counter_service),
4259            job_producer: Arc::new(mock_job_producer),
4260            price_calculator: mock_price_calculator,
4261            signer: mock_signer,
4262        };
4263
4264        // Should return Ok (not error → Dead Queue)
4265        let result = evm_transaction.submit_transaction(test_tx).await;
4266        assert!(
4267            result.is_ok(),
4268            "Expected Ok on nonce too high at threshold, got: {result:?}"
4269        );
4270    }
4271
4272    /// Test resubmit_transaction with NonceTooHigh returns Ok without changing status
4273    #[tokio::test]
4274    async fn test_resubmit_transaction_nonce_too_high_returns_ok() {
4275        let mut mock_transaction = MockTransactionRepository::new();
4276        let mock_relayer = MockRelayerRepository::new();
4277        let mut mock_provider = MockEvmProviderTrait::new();
4278        let mut mock_signer = MockSigner::new();
4279        let mock_job_producer = MockJobProducerTrait::new();
4280        let mut mock_price_calculator = MockPriceCalculator::new();
4281        let counter_service = MockTransactionCounterTrait::new();
4282        let mock_network = MockNetworkRepository::new();
4283
4284        let relayer = create_test_relayer();
4285        let mut test_tx = create_test_transaction();
4286        test_tx.status = TransactionStatus::Submitted;
4287        test_tx.sent_at = Some(Utc::now().to_rfc3339());
4288        let original_hash = "0xoriginal_hash".to_string();
4289        test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
4290            nonce: Some(10),
4291            hash: Some(original_hash.clone()),
4292            raw: Some(vec![1, 2, 3]),
4293            ..test_tx.network_data.get_evm_transaction_data().unwrap()
4294        });
4295        test_tx.hashes = vec![original_hash.clone()];
4296        // Start with nonce_too_high_retries = 0 (below threshold)
4297        test_tx.metadata = Some(crate::models::TransactionMetadata {
4298            nonce_too_high_retries: 0,
4299            ..Default::default()
4300        });
4301
4302        // Price calculator returns bumped price
4303        mock_price_calculator
4304            .expect_calculate_bumped_gas_price()
4305            .times(1)
4306            .returning(|_, _, _| {
4307                Ok(PriceParams {
4308                    gas_price: Some(25000000000),
4309                    max_fee_per_gas: None,
4310                    max_priority_fee_per_gas: None,
4311                    is_min_bumped: Some(true),
4312                    extra_fee: None,
4313                    total_cost: U256::from(525000000000000u64),
4314                })
4315            });
4316
4317        // Balance check passes
4318        mock_provider
4319            .expect_get_balance()
4320            .times(1)
4321            .returning(|_| Box::pin(async { Ok(U256::from(1000000000000000000u64)) }));
4322
4323        // Signer creates new signed transaction
4324        mock_signer
4325            .expect_sign_transaction()
4326            .times(1)
4327            .returning(|_| {
4328                Box::pin(ready(Ok(
4329                    crate::domain::relayer::SignTransactionResponse::Evm(
4330                        crate::domain::relayer::SignTransactionResponseEvm {
4331                            hash: "0xnew_hash".to_string(),
4332                            signature: crate::models::EvmTransactionDataSignature {
4333                                r: "r".to_string(),
4334                                s: "s".to_string(),
4335                                v: 1,
4336                                sig: "0xsignature".to_string(),
4337                            },
4338                            raw: vec![4, 5, 6],
4339                        },
4340                    ),
4341                )))
4342            });
4343
4344        // Provider returns "nonce too high" error on send
4345        mock_provider
4346            .expect_send_raw_transaction()
4347            .times(1)
4348            .returning(|_| {
4349                Box::pin(async {
4350                    Err(crate::services::provider::ProviderError::Other(
4351                        "nonce too high".to_string(),
4352                    ))
4353                })
4354            });
4355
4356        // Should persist incremented counter (nonce_too_high_retries = 1) in metadata
4357        let test_tx_clone = test_tx.clone();
4358        mock_transaction
4359            .expect_partial_update()
4360            .times(1)
4361            .withf(|_, update| {
4362                update
4363                    .metadata
4364                    .as_ref()
4365                    .map(|m| m.nonce_too_high_retries == 1)
4366                    .unwrap_or(false)
4367            })
4368            .returning(move |_, _| Ok(test_tx_clone.clone()));
4369
4370        let evm_transaction = EvmRelayerTransaction {
4371            relayer: relayer.clone(),
4372            provider: mock_provider,
4373            relayer_repository: Arc::new(mock_relayer),
4374            network_repository: Arc::new(mock_network),
4375            transaction_repository: Arc::new(mock_transaction),
4376            transaction_counter_service: Arc::new(counter_service),
4377            job_producer: Arc::new(mock_job_producer),
4378            price_calculator: mock_price_calculator,
4379            signer: mock_signer,
4380        };
4381
4382        // Should return Ok without changing tx status
4383        let result = evm_transaction.resubmit_transaction(test_tx.clone()).await;
4384        assert!(
4385            result.is_ok(),
4386            "Expected Ok on nonce too high during resubmit, got: {result:?}"
4387        );
4388        let returned_tx = result.unwrap();
4389        // Status should remain Submitted (unchanged)
4390        assert_eq!(returned_tx.status, TransactionStatus::Submitted);
4391    }
4392}