openzeppelin_relayer/domain/transaction/evm/
evm_transaction.rs

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