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,
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, StatusCheckContext, TransactionSend, TransactionStatusCheck,
28    },
29    models::{
30        produce_transaction_update_notification_payload, EvmNetwork, EvmTransactionData,
31        NetworkRepoModel, NetworkTransactionData, NetworkTransactionRequest, NetworkType,
32        RelayerEvmPolicy, RelayerRepoModel, TransactionError, TransactionRepoModel,
33        TransactionStatus, TransactionUpdateRequest,
34    },
35    repositories::{
36        NetworkRepository, NetworkRepositoryStorage, RelayerRepository, RelayerRepositoryStorage,
37        Repository, TransactionCounterRepositoryStorage, TransactionCounterTrait,
38        TransactionRepository, TransactionRepositoryStorage,
39    },
40    services::{
41        gas::evm_gas_price::EvmGasPriceService,
42        provider::{EvmProvider, EvmProviderTrait},
43        signer::{EvmSigner, Signer},
44    },
45    utils::{calculate_scheduled_timestamp, get_evm_default_gas_limit_for_tx},
46};
47
48use super::PriceParams;
49
50#[allow(dead_code)]
51pub struct EvmRelayerTransaction<P, RR, NR, TR, J, S, TCR, PC>
52where
53    P: EvmProviderTrait,
54    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
55    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
56    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
57    J: JobProducerTrait + Send + Sync + 'static,
58    S: Signer + Send + Sync + 'static,
59    TCR: TransactionCounterTrait + Send + Sync + 'static,
60    PC: PriceCalculatorTrait,
61{
62    provider: P,
63    relayer_repository: Arc<RR>,
64    network_repository: Arc<NR>,
65    transaction_repository: Arc<TR>,
66    job_producer: Arc<J>,
67    signer: S,
68    relayer: RelayerRepoModel,
69    transaction_counter_service: Arc<TCR>,
70    price_calculator: PC,
71}
72
73#[allow(dead_code, clippy::too_many_arguments)]
74impl<P, RR, NR, TR, J, S, TCR, PC> EvmRelayerTransaction<P, RR, NR, TR, J, S, TCR, PC>
75where
76    P: EvmProviderTrait,
77    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
78    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
79    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
80    J: JobProducerTrait + Send + Sync + 'static,
81    S: Signer + Send + Sync + 'static,
82    TCR: TransactionCounterTrait + Send + Sync + 'static,
83    PC: PriceCalculatorTrait,
84{
85    /// Creates a new `EvmRelayerTransaction`.
86    ///
87    /// # Arguments
88    ///
89    /// * `relayer` - The relayer model.
90    /// * `provider` - The EVM provider.
91    /// * `relayer_repository` - Storage for relayer repository.
92    /// * `transaction_repository` - Storage for transaction repository.
93    /// * `transaction_counter_service` - Service for managing transaction counters.
94    /// * `job_producer` - Producer for job queue.
95    /// * `price_calculator` - Price calculator for gas price management.
96    /// * `signer` - The EVM signer.
97    ///
98    /// # Returns
99    ///
100    /// A result containing the new `EvmRelayerTransaction` or a `TransactionError`.
101    pub fn new(
102        relayer: RelayerRepoModel,
103        provider: P,
104        relayer_repository: Arc<RR>,
105        network_repository: Arc<NR>,
106        transaction_repository: Arc<TR>,
107        transaction_counter_service: Arc<TCR>,
108        job_producer: Arc<J>,
109        price_calculator: PC,
110        signer: S,
111    ) -> Result<Self, TransactionError> {
112        Ok(Self {
113            relayer,
114            provider,
115            relayer_repository,
116            network_repository,
117            transaction_repository,
118            transaction_counter_service,
119            job_producer,
120            price_calculator,
121            signer,
122        })
123    }
124
125    /// Returns a reference to the provider.
126    pub fn provider(&self) -> &P {
127        &self.provider
128    }
129
130    /// Returns a reference to the relayer model.
131    pub fn relayer(&self) -> &RelayerRepoModel {
132        &self.relayer
133    }
134
135    /// Returns a reference to the network repository.
136    pub fn network_repository(&self) -> &NR {
137        &self.network_repository
138    }
139
140    /// Returns a reference to the job producer.
141    pub fn job_producer(&self) -> &J {
142        &self.job_producer
143    }
144
145    pub fn transaction_repository(&self) -> &TR {
146        &self.transaction_repository
147    }
148
149    /// Checks if a provider error indicates the transaction was already submitted to the blockchain.
150    /// This handles cases where the transaction was submitted by another instance or in a previous retry.
151    ///
152    /// Uses the shared `ALREADY_SUBMITTED_PATTERNS` from constants, consistent with
153    /// `is_non_retriable_transaction_rpc_message` in `services::provider`.
154    fn is_already_submitted_error(error: &impl std::fmt::Display) -> bool {
155        let error_msg = error.to_string().to_lowercase();
156        ALREADY_SUBMITTED_PATTERNS
157            .iter()
158            .any(|p| error_msg.contains(p))
159            || matches_known_transaction(&error_msg)
160    }
161
162    /// Helper method to schedule a transaction status check job.
163    pub(super) async fn schedule_status_check(
164        &self,
165        tx: &TransactionRepoModel,
166        delay_seconds: Option<i64>,
167    ) -> Result<(), TransactionError> {
168        let delay = delay_seconds.map(calculate_scheduled_timestamp);
169        self.job_producer()
170            .produce_check_transaction_status_job(
171                TransactionStatusCheck::new(
172                    tx.id.clone(),
173                    tx.relayer_id.clone(),
174                    crate::models::NetworkType::Evm,
175                ),
176                delay,
177            )
178            .await
179            .map_err(|e| {
180                TransactionError::UnexpectedError(format!("Failed to schedule status check: {e}"))
181            })
182    }
183
184    /// Helper method to produce a submit transaction job.
185    pub(super) async fn send_transaction_submit_job(
186        &self,
187        tx: &TransactionRepoModel,
188    ) -> Result<(), TransactionError> {
189        debug!(
190            tx_id = %tx.id,
191            relayer_id = %tx.relayer_id,
192            "enqueueing submit transaction job"
193        );
194        let job = TransactionSend::submit(tx.id.clone(), tx.relayer_id.clone());
195
196        self.job_producer()
197            .produce_submit_transaction_job(job, None)
198            .await
199            .map_err(|e| {
200                TransactionError::UnexpectedError(format!("Failed to produce submit job: {e}"))
201            })
202    }
203
204    /// Helper method to produce a resubmit transaction job.
205    pub(super) async fn send_transaction_resubmit_job(
206        &self,
207        tx: &TransactionRepoModel,
208    ) -> Result<(), TransactionError> {
209        debug!(
210            tx_id = %tx.id,
211            relayer_id = %tx.relayer_id,
212            "enqueueing resubmit transaction job"
213        );
214        let job = TransactionSend::resubmit(tx.id.clone(), tx.relayer_id.clone());
215
216        self.job_producer()
217            .produce_submit_transaction_job(job, None)
218            .await
219            .map_err(|e| {
220                TransactionError::UnexpectedError(format!("Failed to produce resubmit job: {e}"))
221            })
222    }
223
224    /// Helper method to produce a resend transaction job.
225    pub(super) async fn send_transaction_resend_job(
226        &self,
227        tx: &TransactionRepoModel,
228    ) -> Result<(), TransactionError> {
229        debug!(
230            tx_id = %tx.id,
231            relayer_id = %tx.relayer_id,
232            "enqueueing resend transaction job"
233        );
234        let job = TransactionSend::resend(tx.id.clone(), tx.relayer_id.clone());
235
236        self.job_producer()
237            .produce_submit_transaction_job(job, None)
238            .await
239            .map_err(|e| {
240                TransactionError::UnexpectedError(format!("Failed to produce resend job: {e}"))
241            })
242    }
243
244    /// Helper method to produce a transaction request (prepare) job.
245    pub(super) async fn send_transaction_request_job(
246        &self,
247        tx: &TransactionRepoModel,
248    ) -> Result<(), TransactionError> {
249        use crate::jobs::TransactionRequest;
250
251        let job = TransactionRequest::new(tx.id.clone(), tx.relayer_id.clone());
252
253        self.job_producer()
254            .produce_transaction_request_job(job, None)
255            .await
256            .map_err(|e| {
257                TransactionError::UnexpectedError(format!("Failed to produce request job: {e}"))
258            })
259    }
260
261    /// Updates a transaction's status, optionally including a status reason.
262    pub(super) async fn update_transaction_status(
263        &self,
264        tx: TransactionRepoModel,
265        new_status: TransactionStatus,
266        status_reason: Option<String>,
267    ) -> Result<TransactionRepoModel, TransactionError> {
268        let confirmed_at = if new_status == TransactionStatus::Confirmed {
269            Some(Utc::now().to_rfc3339())
270        } else {
271            None
272        };
273
274        let update_request = TransactionUpdateRequest {
275            status: Some(new_status),
276            confirmed_at,
277            status_reason,
278            ..Default::default()
279        };
280
281        let updated_tx = self
282            .transaction_repository()
283            .partial_update(tx.id.clone(), update_request)
284            .await?;
285
286        if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
287            error!(
288                tx_id = %updated_tx.id,
289                status = ?updated_tx.status,
290                "sending transaction update notification failed: {:?}",
291                e
292            );
293        }
294        Ok(updated_tx)
295    }
296
297    /// Sends a transaction update notification if a notification ID is configured.
298    ///
299    /// This is a best-effort operation that logs errors but does not propagate them,
300    /// as notification failures should not affect the transaction lifecycle.
301    pub(super) async fn send_transaction_update_notification(
302        &self,
303        tx: &TransactionRepoModel,
304    ) -> Result<(), eyre::Report> {
305        if let Some(notification_id) = &self.relayer().notification_id {
306            self.job_producer()
307                .produce_send_notification_job(
308                    produce_transaction_update_notification_payload(notification_id, tx),
309                    None,
310                )
311                .await?;
312        }
313        Ok(())
314    }
315
316    /// Marks a transaction as failed with a reason, updates it, sends notification, and returns the updated transaction.
317    ///
318    /// This is a common pattern used when a transaction should be marked as failed.
319    ///
320    /// # Arguments
321    ///
322    /// * `tx` - The transaction to mark as failed
323    /// * `reason` - The reason for the failure
324    /// * `error_context` - Context string for error logging (e.g., "gas limit exceeds block gas limit")
325    ///
326    /// # Returns
327    ///
328    /// The updated transaction with Failed status
329    async fn mark_transaction_as_failed(
330        &self,
331        tx: &TransactionRepoModel,
332        reason: String,
333        error_context: &str,
334    ) -> Result<TransactionRepoModel, TransactionError> {
335        let update = TransactionUpdateRequest {
336            status: Some(TransactionStatus::Failed),
337            status_reason: Some(reason.clone()),
338            ..Default::default()
339        };
340
341        let updated_tx = self
342            .transaction_repository
343            .partial_update(tx.id.clone(), update)
344            .await?;
345
346        if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
347            error!(
348                tx_id = %updated_tx.id,
349                status = ?TransactionStatus::Failed,
350                "sending transaction update notification failed for {}: {:?}",
351                error_context,
352                e
353            );
354        }
355
356        Ok(updated_tx)
357    }
358
359    /// Validates that the relayer has sufficient balance for the transaction.
360    ///
361    /// # Arguments
362    ///
363    /// * `total_cost` - The total cost of the transaction (gas + value)
364    ///
365    /// # Returns
366    ///
367    /// A `Result` indicating success or a `TransactionError`.
368    /// - Returns `InsufficientBalance` only when balance is truly insufficient (permanent failure)
369    /// - Returns `UnexpectedError` for RPC/network issues (retryable)
370    async fn ensure_sufficient_balance(
371        &self,
372        total_cost: crate::models::U256,
373    ) -> Result<(), TransactionError> {
374        EvmTransactionValidator::validate_sufficient_relayer_balance(
375            total_cost,
376            &self.relayer().address,
377            &self.relayer().policies.get_evm_policy(),
378            &self.provider,
379        )
380        .await
381        .map_err(|validation_error| match validation_error {
382            // Only convert actual insufficient balance to permanent failure
383            EvmTransactionValidationError::InsufficientBalance(msg) => {
384                TransactionError::InsufficientBalance(msg)
385            }
386            // Provider errors are retryable (RPC down, timeout, etc.)
387            EvmTransactionValidationError::ProviderError(msg) => {
388                TransactionError::UnexpectedError(format!("Failed to check balance: {msg}"))
389            }
390            // Validation errors are also retryable
391            EvmTransactionValidationError::ValidationError(msg) => {
392                TransactionError::UnexpectedError(format!("Balance validation error: {msg}"))
393            }
394        })
395    }
396
397    /// Estimates the gas limit for a transaction.
398    ///
399    /// # Arguments
400    ///
401    /// * `evm_data` - The EVM transaction data.
402    /// * `relayer_policy` - The relayer policy.
403    ///
404    async fn estimate_tx_gas_limit(
405        &self,
406        evm_data: &EvmTransactionData,
407        relayer_policy: &RelayerEvmPolicy,
408    ) -> Result<u64, TransactionError> {
409        if !relayer_policy
410            .gas_limit_estimation
411            .unwrap_or(DEFAULT_EVM_GAS_LIMIT_ESTIMATION)
412        {
413            warn!("gas limit estimation is disabled for relayer");
414            return Err(TransactionError::UnexpectedError(
415                "Gas limit estimation is disabled".to_string(),
416            ));
417        }
418
419        let estimated_gas = self.provider.estimate_gas(evm_data).await.map_err(|e| {
420            warn!(error = ?e, tx_data = ?evm_data, "failed to estimate gas");
421            TransactionError::UnexpectedError(format!("Failed to estimate gas: {e}"))
422        })?;
423
424        Ok(estimated_gas * GAS_LIMIT_BUFFER_MULTIPLIER / 100)
425    }
426}
427
428#[async_trait]
429impl<P, RR, NR, TR, J, S, TCR, PC> Transaction
430    for EvmRelayerTransaction<P, RR, NR, TR, J, S, TCR, PC>
431where
432    P: EvmProviderTrait + Send + Sync + 'static,
433    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
434    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
435    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
436    J: JobProducerTrait + Send + Sync + 'static,
437    S: Signer + Send + Sync + 'static,
438    TCR: TransactionCounterTrait + Send + Sync + 'static,
439    PC: PriceCalculatorTrait + Send + Sync + 'static,
440{
441    /// Prepares a transaction for submission.
442    ///
443    /// # Arguments
444    ///
445    /// * `tx` - The transaction model to prepare.
446    ///
447    /// # Returns
448    ///
449    /// A result containing the updated transaction model or a `TransactionError`.
450    async fn prepare_transaction(
451        &self,
452        tx: TransactionRepoModel,
453    ) -> Result<TransactionRepoModel, TransactionError> {
454        debug!(
455            tx_id = %tx.id,
456            relayer_id = %tx.relayer_id,
457            status = ?tx.status,
458            "preparing transaction"
459        );
460
461        // If transaction is not in Pending status, return Ok to avoid wasteful retries
462        // (e.g., if it's already Sent, Failed, or in another state)
463        if let Err(e) = ensure_status(&tx, TransactionStatus::Pending, Some("prepare_transaction"))
464        {
465            warn!(
466                tx_id = %tx.id,
467                status = ?tx.status,
468                error = %e,
469                "transaction not in Pending status, skipping preparation"
470            );
471            return Ok(tx);
472        }
473
474        let mut evm_data = tx.network_data.get_evm_transaction_data()?;
475        let relayer = self.relayer();
476
477        if evm_data.gas_limit.is_none() {
478            match self
479                .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
480                .await
481            {
482                Ok(estimated_gas_limit) => {
483                    evm_data.gas_limit = Some(estimated_gas_limit);
484                }
485                Err(estimation_error) => {
486                    error!(
487                        tx_id = %tx.id,
488                        relayer_id = %tx.relayer_id,
489                        error = ?estimation_error,
490                        "failed to estimate gas limit"
491                    );
492
493                    let default_gas_limit = get_evm_default_gas_limit_for_tx(&evm_data);
494                    debug!(
495                        tx_id = %tx.id,
496                        gas_limit = %default_gas_limit,
497                        "fallback to default gas limit"
498                    );
499                    evm_data.gas_limit = Some(default_gas_limit);
500                }
501            }
502        } else {
503            // do user gas limit validation against block gas limit
504            let block = self.provider.get_block_by_number().await;
505            if let Ok(block) = block {
506                let block_gas_limit = block.header.gas_limit;
507                if let Some(gas_limit) = evm_data.gas_limit {
508                    if gas_limit > block_gas_limit {
509                        let reason = format!(
510                            "Transaction gas limit ({gas_limit}) exceeds block gas limit ({block_gas_limit})",
511                        );
512                        warn!(
513                            tx_id = %tx.id,
514                            tx_gas_limit = %gas_limit,
515                            block_gas_limit = %block_gas_limit,
516                            "transaction gas limit exceeds block gas limit"
517                        );
518
519                        let updated_tx = self
520                            .mark_transaction_as_failed(
521                                &tx,
522                                reason,
523                                "gas limit exceeds block gas limit",
524                            )
525                            .await?;
526                        return Ok(updated_tx);
527                    }
528                }
529            }
530        }
531
532        // set the gas price
533        let price_params: PriceParams = self
534            .price_calculator
535            .get_transaction_price_params(&evm_data, relayer)
536            .await?;
537
538        debug!(
539            tx_id = %tx.id,
540            relayer_id = %tx.relayer_id,
541            gas_price = ?price_params.gas_price,
542            "gas price"
543        );
544
545        // Validate the relayer has sufficient balance before consuming nonce and signing
546        if let Err(balance_error) = self
547            .ensure_sufficient_balance(price_params.total_cost)
548            .await
549        {
550            // Only mark as Failed for actual insufficient balance, not RPC errors
551            match &balance_error {
552                TransactionError::InsufficientBalance(_) => {
553                    warn!(
554                        tx_id = %tx.id,
555                        relayer_id = %tx.relayer_id,
556                        error = %balance_error,
557                        "insufficient balance for transaction"
558                    );
559
560                    let updated_tx = self
561                        .mark_transaction_as_failed(
562                            &tx,
563                            balance_error.to_string(),
564                            "insufficient balance",
565                        )
566                        .await?;
567
568                    // Return Ok since transaction is in final Failed state - no retry needed
569                    return Ok(updated_tx);
570                }
571                // For RPC/provider errors, propagate without marking as Failed
572                // This allows the handler to retry
573                _ => {
574                    debug!(error = %balance_error, "failed to check balance, will retry");
575                    return Err(balance_error);
576                }
577            }
578        }
579
580        // Check if transaction already has a nonce (recovery from failed signing attempt)
581        let tx_with_nonce = if let Some(existing_nonce) = evm_data.nonce {
582            debug!(
583                nonce = existing_nonce,
584                "transaction already has nonce assigned, reusing for retry"
585            );
586            // Retry flow: When reusing an existing nonce from a failed attempt, we intentionally
587            // do NOT persist the fresh price_params (computed earlier) to the DB here. The DB may
588            // temporarily hold stale price_params from the failed attempt. However, fresh price_params
589            // are applied just before signing, ensuring the transaction uses
590            // current gas prices.
591            tx
592        } else {
593            // Balance validation passed, proceed to increment nonce
594            let new_nonce = self
595                .transaction_counter_service
596                .get_and_increment(&self.relayer.id, &self.relayer.address)
597                .await
598                .map_err(|e| TransactionError::UnexpectedError(e.to_string()))?;
599
600            debug!(nonce = new_nonce, "assigned new nonce to transaction");
601
602            let updated_evm_data = evm_data
603                .with_price_params(price_params.clone())
604                .with_nonce(new_nonce);
605
606            // Save transaction with nonce BEFORE signing
607            // This ensures we can recover if signing fails (timeout, KMS error, etc.)
608            let presign_update = TransactionUpdateRequest {
609                network_data: Some(NetworkTransactionData::Evm(updated_evm_data.clone())),
610                priced_at: Some(Utc::now().to_rfc3339()),
611                ..Default::default()
612            };
613
614            self.transaction_repository
615                .partial_update(tx.id.clone(), presign_update)
616                .await?
617        };
618
619        // Apply price params for signing (recalculated on every attempt)
620        let updated_evm_data = tx_with_nonce
621            .network_data
622            .get_evm_transaction_data()?
623            .with_price_params(price_params.clone());
624
625        // Now sign the transaction - if this fails, we still have the tx with nonce saved
626        let sig_result = self
627            .signer
628            .sign_transaction(NetworkTransactionData::Evm(updated_evm_data.clone()))
629            .await?;
630
631        let updated_evm_data =
632            updated_evm_data.with_signed_transaction_data(sig_result.into_evm()?);
633
634        // Track the transaction hash
635        let mut hashes = tx_with_nonce.hashes.clone();
636        if let Some(hash) = updated_evm_data.hash.clone() {
637            hashes.push(hash);
638        }
639
640        // Update with signed data and mark as Sent
641        let postsign_update = TransactionUpdateRequest {
642            status: Some(TransactionStatus::Sent),
643            network_data: Some(NetworkTransactionData::Evm(updated_evm_data)),
644            hashes: Some(hashes),
645            ..Default::default()
646        };
647
648        let updated_tx = self
649            .transaction_repository
650            .partial_update(tx_with_nonce.id.clone(), postsign_update)
651            .await?;
652
653        debug!(
654            tx_id = %updated_tx.id,
655            relayer_id = %updated_tx.relayer_id,
656            status = ?updated_tx.status,
657            "transaction status updated to Sent"
658        );
659
660        // after preparing the transaction, we need to submit it to the job queue
661        self.job_producer
662            .produce_submit_transaction_job(
663                TransactionSend::submit(updated_tx.id.clone(), updated_tx.relayer_id.clone()),
664                None,
665            )
666            .await?;
667
668        if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
669            error!(
670                tx_id = %updated_tx.id,
671                relayer_id = %updated_tx.relayer_id,
672                status = ?TransactionStatus::Sent,
673                error = %e,
674                "sending transaction update notification failed after prepare"
675            );
676        }
677
678        Ok(updated_tx)
679    }
680
681    /// Submits a transaction for processing.
682    ///
683    /// # Arguments
684    ///
685    /// * `tx` - The transaction model to submit.
686    ///
687    /// # Returns
688    ///
689    /// A result containing the updated transaction model or a `TransactionError`.
690    async fn submit_transaction(
691        &self,
692        tx: TransactionRepoModel,
693    ) -> Result<TransactionRepoModel, TransactionError> {
694        debug!(
695            tx_id = %tx.id,
696            relayer_id = %tx.relayer_id,
697            status = ?tx.status,
698            "submitting transaction"
699        );
700
701        // If transaction is not in correct status, return Ok to avoid wasteful retries
702        // (e.g., if it's already in a final state like Failed, Confirmed, etc.)
703        if let Err(e) = ensure_status_one_of(
704            &tx,
705            &[TransactionStatus::Sent, TransactionStatus::Submitted],
706            Some("submit_transaction"),
707        ) {
708            warn!(
709                tx_id = %tx.id,
710                status = ?tx.status,
711                error = %e,
712                "transaction not in expected status for submission, skipping"
713            );
714            return Ok(tx);
715        }
716
717        let evm_tx_data = tx.network_data.get_evm_transaction_data()?;
718        let raw_tx = evm_tx_data.raw.as_ref().ok_or_else(|| {
719            TransactionError::InvalidType("Raw transaction data is missing".to_string())
720        })?;
721
722        // Send transaction to blockchain - this is the critical operation
723        // If this fails, retry is safe due to nonce idempotency
724        match self.provider.send_raw_transaction(raw_tx).await {
725            Ok(_) => {
726                // Transaction submitted successfully
727            }
728            Err(e) => {
729                // SAFETY CHECK: If transaction is in Sent status and we get "already known" or
730                // "nonce too low" errors, it means the transaction was already submitted
731                // (possibly by another instance or in a previous retry)
732                if tx.status == TransactionStatus::Sent && Self::is_already_submitted_error(&e) {
733                    warn!(
734                        tx_id = %tx.id,
735                        error = %e,
736                        "transaction appears to be already submitted based on RPC error - treating as success"
737                    );
738                    // Continue to update status to Submitted
739                } else {
740                    // Real error - propagate it
741                    return Err(e.into());
742                }
743            }
744        }
745
746        // Transaction is now on-chain - update database
747        // If this fails, transaction is still valid, just not tracked correctly
748        let update = TransactionUpdateRequest {
749            status: Some(TransactionStatus::Submitted),
750            sent_at: Some(Utc::now().to_rfc3339()),
751            ..Default::default()
752        };
753
754        let updated_tx = match self
755            .transaction_repository
756            .partial_update(tx.id.clone(), update)
757            .await
758        {
759            Ok(tx) => tx,
760            Err(e) => {
761                error!(
762                    tx_id = %tx.id,
763                    relayer_id = %tx.relayer_id,
764                    error = %e,
765                    "CRITICAL: transaction sent to blockchain but failed to update database - transaction may not be tracked correctly"
766                );
767                // Transaction is on-chain - don't propagate error to avoid wasteful retries
768                // Return the original transaction data
769                tx
770            }
771        };
772
773        if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
774            error!(
775                tx_id = %updated_tx.id,
776                relayer_id = %updated_tx.relayer_id,
777                status = ?TransactionStatus::Submitted,
778                error = %e,
779                "sending transaction update notification failed after submit",
780            );
781        }
782
783        Ok(updated_tx)
784    }
785
786    /// Handles the status of a transaction.
787    ///
788    /// # Arguments
789    ///
790    /// * `tx` - The transaction model to handle.
791    ///
792    /// # Returns
793    ///
794    /// A result containing the updated transaction model or a `TransactionError`.
795    async fn handle_transaction_status(
796        &self,
797        tx: TransactionRepoModel,
798        context: Option<StatusCheckContext>,
799    ) -> Result<TransactionRepoModel, TransactionError> {
800        self.handle_status_impl(tx, context).await
801    }
802    /// Resubmits a transaction with updated parameters.
803    ///
804    /// # Arguments
805    ///
806    /// * `tx` - The transaction model to resubmit.
807    ///
808    /// # Returns
809    ///
810    /// A result containing the resubmitted transaction model or a `TransactionError`.
811    async fn resubmit_transaction(
812        &self,
813        tx: TransactionRepoModel,
814    ) -> Result<TransactionRepoModel, TransactionError> {
815        debug!(
816            tx_id = %tx.id,
817            relayer_id = %tx.relayer_id,
818            status = ?tx.status,
819            "resubmitting transaction"
820        );
821
822        // If transaction is not in correct status, return Ok to avoid wasteful retries
823        if let Err(e) = ensure_status_one_of(
824            &tx,
825            &[TransactionStatus::Sent, TransactionStatus::Submitted],
826            Some("resubmit_transaction"),
827        ) {
828            warn!(
829                tx_id = %tx.id,
830                status = ?tx.status,
831                error = %e,
832                "transaction not in expected status for resubmission, skipping"
833            );
834            return Ok(tx);
835        }
836
837        let evm_data = tx.network_data.get_evm_transaction_data()?;
838
839        // Calculate bumped gas price
840        // For noop transactions, force_bump=true to skip gas price cap and ensure bump succeeds
841        let bumped_price_params = self
842            .price_calculator
843            .calculate_bumped_gas_price(&evm_data, self.relayer(), is_noop(&evm_data))
844            .await?;
845
846        if !bumped_price_params.is_min_bumped.is_some_and(|b| b) {
847            warn!(
848                tx_id = %tx.id,
849                relayer_id = %tx.relayer_id,
850                price_params = ?bumped_price_params,
851                "bumped gas price does not meet minimum requirement, skipping resubmission"
852            );
853            return Ok(tx);
854        }
855
856        // Validate the relayer has sufficient balance
857        self.ensure_sufficient_balance(bumped_price_params.total_cost)
858            .await?;
859
860        // Create new transaction data with bumped gas price
861        let updated_evm_data = evm_data.with_price_params(bumped_price_params.clone());
862
863        // Sign the transaction
864        let sig_result = self
865            .signer
866            .sign_transaction(NetworkTransactionData::Evm(updated_evm_data.clone()))
867            .await?;
868
869        let final_evm_data = updated_evm_data.with_signed_transaction_data(sig_result.into_evm()?);
870
871        let raw_tx = final_evm_data.raw.as_ref().ok_or_else(|| {
872            TransactionError::InvalidType("Raw transaction data is missing".to_string())
873        })?;
874
875        // Send resubmitted transaction to blockchain - this is the critical operation
876        let was_already_submitted = match self.provider.send_raw_transaction(raw_tx).await {
877            Ok(_) => {
878                // Transaction resubmitted successfully with new pricing
879                false
880            }
881            Err(e) => {
882                // SAFETY CHECK: If we get "already known" or "nonce too low" errors,
883                // it means a transaction with this nonce was already submitted
884                let is_already_submitted = Self::is_already_submitted_error(&e);
885
886                if is_already_submitted {
887                    warn!(
888                        tx_id = %tx.id,
889                        error = %e,
890                        "resubmission indicates transaction already in mempool/mined - keeping original hash"
891                    );
892                    // Don't update with new hash - the original transaction is what's on-chain
893                    true
894                } else {
895                    // Real error - propagate it
896                    return Err(e.into());
897                }
898            }
899        };
900
901        // If transaction was already submitted, just update status without changing hash
902        let update = if was_already_submitted {
903            // Keep original hash and data - just ensure status is Submitted
904            TransactionUpdateRequest {
905                status: Some(TransactionStatus::Submitted),
906                ..Default::default()
907            }
908        } else {
909            // Transaction resubmitted successfully - update with new hash and pricing
910            let mut hashes = tx.hashes.clone();
911            if let Some(hash) = final_evm_data.hash.clone() {
912                hashes.push(hash);
913            }
914
915            TransactionUpdateRequest {
916                network_data: Some(NetworkTransactionData::Evm(final_evm_data)),
917                hashes: Some(hashes),
918                status: Some(TransactionStatus::Submitted),
919                priced_at: Some(Utc::now().to_rfc3339()),
920                sent_at: Some(Utc::now().to_rfc3339()),
921                ..Default::default()
922            }
923        };
924
925        let updated_tx = match self
926            .transaction_repository
927            .partial_update(tx.id.clone(), update)
928            .await
929        {
930            Ok(tx) => tx,
931            Err(e) => {
932                error!(
933                    error = %e,
934                    tx_id = %tx.id,
935                    "CRITICAL: resubmitted transaction sent to blockchain but failed to update database"
936                );
937                // Transaction is on-chain - return original tx data to avoid wasteful retries
938                tx
939            }
940        };
941
942        Ok(updated_tx)
943    }
944
945    /// Cancels a transaction.
946    ///
947    /// # Arguments
948    ///
949    /// * `tx` - The transaction model to cancel.
950    ///
951    /// # Returns
952    ///
953    /// A result containing the transaction model or a `TransactionError`.
954    async fn cancel_transaction(
955        &self,
956        tx: TransactionRepoModel,
957    ) -> Result<TransactionRepoModel, TransactionError> {
958        info!(tx_id = %tx.id, status = ?tx.status, "cancelling transaction");
959
960        // Validate state: can only cancel transactions that are still pending
961        ensure_status_one_of(
962            &tx,
963            &[
964                TransactionStatus::Pending,
965                TransactionStatus::Sent,
966                TransactionStatus::Submitted,
967            ],
968            Some("cancel_transaction"),
969        )?;
970
971        // If the transaction is in Pending state, we can just update its status
972        if tx.status == TransactionStatus::Pending {
973            debug!("transaction is in pending state, updating status to canceled");
974            return self
975                .update_transaction_status(
976                    tx,
977                    TransactionStatus::Canceled,
978                    Some("Transaction canceled by user".to_string()),
979                )
980                .await;
981        }
982
983        let update = self
984            .prepare_noop_update_request(
985                &tx,
986                true,
987                Some("Transaction canceled by user, replacing with NOOP".to_string()),
988            )
989            .await?;
990        let updated_tx = self
991            .transaction_repository()
992            .partial_update(tx.id.clone(), update)
993            .await?;
994
995        // Submit the updated transaction to the network using the resubmit job
996        self.send_transaction_resubmit_job(&updated_tx).await?;
997
998        // Send notification for the updated transaction
999        if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
1000            error!(
1001                tx_id = %updated_tx.id,
1002                status = ?updated_tx.status,
1003                "sending transaction update notification failed after cancel: {:?}",
1004                e
1005            );
1006        }
1007
1008        debug!("original transaction updated with cancellation data");
1009        Ok(updated_tx)
1010    }
1011
1012    /// Replaces a transaction with a new one.
1013    ///
1014    /// # Arguments
1015    ///
1016    /// * `old_tx` - The transaction model to replace.
1017    /// * `new_tx_request` - The new transaction request data.
1018    ///
1019    /// # Returns
1020    ///
1021    /// A result containing the updated transaction model or a `TransactionError`.
1022    async fn replace_transaction(
1023        &self,
1024        old_tx: TransactionRepoModel,
1025        new_tx_request: NetworkTransactionRequest,
1026    ) -> Result<TransactionRepoModel, TransactionError> {
1027        debug!("replacing transaction");
1028
1029        // Validate state: can only replace transactions that are still pending
1030        ensure_status_one_of(
1031            &old_tx,
1032            &[
1033                TransactionStatus::Pending,
1034                TransactionStatus::Sent,
1035                TransactionStatus::Submitted,
1036            ],
1037            Some("replace_transaction"),
1038        )?;
1039
1040        // Extract EVM data from both old transaction and new request
1041        let old_evm_data = old_tx.network_data.get_evm_transaction_data()?;
1042        let new_evm_request = match new_tx_request {
1043            NetworkTransactionRequest::Evm(evm_req) => evm_req,
1044            _ => {
1045                return Err(TransactionError::InvalidType(
1046                    "New transaction request must be EVM type".to_string(),
1047                ))
1048            }
1049        };
1050
1051        let network_repo_model = self
1052            .network_repository()
1053            .get_by_chain_id(NetworkType::Evm, old_evm_data.chain_id)
1054            .await
1055            .map_err(|e| {
1056                TransactionError::NetworkConfiguration(format!(
1057                    "Failed to get network by chain_id {}: {}",
1058                    old_evm_data.chain_id, e
1059                ))
1060            })?
1061            .ok_or_else(|| {
1062                TransactionError::NetworkConfiguration(format!(
1063                    "Network with chain_id {} not found",
1064                    old_evm_data.chain_id
1065                ))
1066            })?;
1067
1068        let network = EvmNetwork::try_from(network_repo_model).map_err(|e| {
1069            TransactionError::NetworkConfiguration(format!("Failed to convert network model: {e}"))
1070        })?;
1071
1072        // First, create updated EVM data without price parameters
1073        let updated_evm_data = EvmTransactionData::for_replacement(&old_evm_data, &new_evm_request);
1074
1075        // Then determine pricing strategy and calculate price parameters using the updated data
1076        let price_params = super::replacement::determine_replacement_pricing(
1077            &old_evm_data,
1078            &updated_evm_data,
1079            self.relayer(),
1080            &self.price_calculator,
1081            network.lacks_mempool(),
1082        )
1083        .await?;
1084
1085        debug!(price_params = ?price_params, "replacement price params");
1086
1087        // Apply the calculated price parameters to the updated EVM data
1088        let evm_data_with_price_params = updated_evm_data.with_price_params(price_params.clone());
1089
1090        // Validate the relayer has sufficient balance
1091        self.ensure_sufficient_balance(price_params.total_cost)
1092            .await?;
1093
1094        let sig_result = self
1095            .signer
1096            .sign_transaction(NetworkTransactionData::Evm(
1097                evm_data_with_price_params.clone(),
1098            ))
1099            .await?;
1100
1101        let final_evm_data =
1102            evm_data_with_price_params.with_signed_transaction_data(sig_result.into_evm()?);
1103
1104        // Update the transaction in the repository
1105        let updated_tx = self
1106            .transaction_repository
1107            .update_network_data(
1108                old_tx.id.clone(),
1109                NetworkTransactionData::Evm(final_evm_data),
1110            )
1111            .await?;
1112
1113        self.send_transaction_resubmit_job(&updated_tx).await?;
1114
1115        // Send notification
1116        if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
1117            error!(
1118                tx_id = %updated_tx.id,
1119                status = ?updated_tx.status,
1120                "sending transaction update notification failed after replace: {:?}",
1121                e
1122            );
1123        }
1124
1125        Ok(updated_tx)
1126    }
1127
1128    /// Signs a transaction.
1129    ///
1130    /// # Arguments
1131    ///
1132    /// * `tx` - The transaction model to sign.
1133    ///
1134    /// # Returns
1135    ///
1136    /// A result containing the transaction model or a `TransactionError`.
1137    async fn sign_transaction(
1138        &self,
1139        tx: TransactionRepoModel,
1140    ) -> Result<TransactionRepoModel, TransactionError> {
1141        Ok(tx)
1142    }
1143
1144    /// Validates a transaction.
1145    ///
1146    /// # Arguments
1147    ///
1148    /// * `_tx` - The transaction model to validate.
1149    ///
1150    /// # Returns
1151    ///
1152    /// A result containing a boolean indicating validity or a `TransactionError`.
1153    async fn validate_transaction(
1154        &self,
1155        _tx: TransactionRepoModel,
1156    ) -> Result<bool, TransactionError> {
1157        Ok(true)
1158    }
1159}
1160// P: EvmProviderTrait,
1161// R: Repository<RelayerRepoModel, String>,
1162// T: TransactionRepository,
1163// J: JobProducerTrait,
1164// S: Signer,
1165// C: TransactionCounterTrait,
1166// PC: PriceCalculatorTrait,
1167// we define concrete type for the evm transaction
1168pub type DefaultEvmTransaction = EvmRelayerTransaction<
1169    EvmProvider,
1170    RelayerRepositoryStorage,
1171    NetworkRepositoryStorage,
1172    TransactionRepositoryStorage,
1173    JobProducer,
1174    EvmSigner,
1175    TransactionCounterRepositoryStorage,
1176    PriceCalculator<EvmGasPriceService<EvmProvider>>,
1177>;
1178#[cfg(test)]
1179mod tests {
1180
1181    use super::*;
1182    use crate::{
1183        domain::evm::price_calculator::PriceParams,
1184        jobs::MockJobProducerTrait,
1185        models::{
1186            evm::Speed, EvmTransactionData, EvmTransactionRequest, NetworkType,
1187            RelayerNetworkPolicy, U256,
1188        },
1189        repositories::{
1190            MockNetworkRepository, MockRelayerRepository, MockTransactionCounterTrait,
1191            MockTransactionRepository,
1192        },
1193        services::{provider::MockEvmProviderTrait, signer::MockSigner},
1194    };
1195    use chrono::Utc;
1196    use futures::future::ready;
1197    use mockall::{mock, predicate::*};
1198
1199    // Create a mock for PriceCalculatorTrait
1200    mock! {
1201        pub PriceCalculator {}
1202        #[async_trait]
1203        impl PriceCalculatorTrait for PriceCalculator {
1204            async fn get_transaction_price_params(
1205                &self,
1206                tx_data: &EvmTransactionData,
1207                relayer: &RelayerRepoModel
1208            ) -> Result<PriceParams, TransactionError>;
1209
1210            async fn calculate_bumped_gas_price(
1211                &self,
1212                tx: &EvmTransactionData,
1213                relayer: &RelayerRepoModel,
1214                force_bump: bool,
1215            ) -> Result<PriceParams, TransactionError>;
1216        }
1217    }
1218
1219    // Helper to create a relayer model with specific configuration for these tests
1220    fn create_test_relayer() -> RelayerRepoModel {
1221        create_test_relayer_with_policy(crate::models::RelayerEvmPolicy {
1222            min_balance: Some(100000000000000000u128), // 0.1 ETH
1223            gas_limit_estimation: Some(true),
1224            gas_price_cap: Some(100000000000), // 100 Gwei
1225            whitelist_receivers: Some(vec!["0xRecipient".to_string()]),
1226            eip1559_pricing: Some(false),
1227            private_transactions: Some(false),
1228        })
1229    }
1230
1231    fn create_test_relayer_with_policy(evm_policy: RelayerEvmPolicy) -> RelayerRepoModel {
1232        RelayerRepoModel {
1233            id: "test-relayer-id".to_string(),
1234            name: "Test Relayer".to_string(),
1235            network: "1".to_string(), // Ethereum Mainnet
1236            address: "0xSender".to_string(),
1237            paused: false,
1238            system_disabled: false,
1239            signer_id: "test-signer-id".to_string(),
1240            notification_id: Some("test-notification-id".to_string()),
1241            policies: RelayerNetworkPolicy::Evm(evm_policy),
1242            network_type: NetworkType::Evm,
1243            custom_rpc_urls: None,
1244            ..Default::default()
1245        }
1246    }
1247
1248    // Helper to create test transaction with specific configuration for these tests
1249    fn create_test_transaction() -> TransactionRepoModel {
1250        TransactionRepoModel {
1251            id: "test-tx-id".to_string(),
1252            relayer_id: "test-relayer-id".to_string(),
1253            status: TransactionStatus::Pending,
1254            status_reason: None,
1255            created_at: Utc::now().to_rfc3339(),
1256            sent_at: None,
1257            confirmed_at: None,
1258            valid_until: None,
1259            delete_at: None,
1260            network_type: NetworkType::Evm,
1261            network_data: NetworkTransactionData::Evm(EvmTransactionData {
1262                chain_id: 1,
1263                from: "0xSender".to_string(),
1264                to: Some("0xRecipient".to_string()),
1265                value: U256::from(1000000000000000000u64), // 1 ETH
1266                data: Some("0xData".to_string()),
1267                gas_limit: Some(21000),
1268                gas_price: Some(20000000000), // 20 Gwei
1269                max_fee_per_gas: None,
1270                max_priority_fee_per_gas: None,
1271                nonce: None,
1272                signature: None,
1273                hash: None,
1274                speed: Some(Speed::Fast),
1275                raw: None,
1276            }),
1277            priced_at: None,
1278            hashes: Vec::new(),
1279            noop_count: None,
1280            is_canceled: Some(false),
1281            metadata: None,
1282        }
1283    }
1284
1285    #[tokio::test]
1286    async fn test_prepare_transaction_with_sufficient_balance() {
1287        let mut mock_transaction = MockTransactionRepository::new();
1288        let mock_relayer = MockRelayerRepository::new();
1289        let mut mock_provider = MockEvmProviderTrait::new();
1290        let mut mock_signer = MockSigner::new();
1291        let mut mock_job_producer = MockJobProducerTrait::new();
1292        let mut mock_price_calculator = MockPriceCalculator::new();
1293        let mut counter_service = MockTransactionCounterTrait::new();
1294
1295        let relayer = create_test_relayer();
1296        let test_tx = create_test_transaction();
1297
1298        counter_service
1299            .expect_get_and_increment()
1300            .returning(|_, _| Box::pin(ready(Ok(42))));
1301
1302        let price_params = PriceParams {
1303            gas_price: Some(30000000000),
1304            max_fee_per_gas: None,
1305            max_priority_fee_per_gas: None,
1306            is_min_bumped: None,
1307            extra_fee: None,
1308            total_cost: U256::from(630000000000000u64),
1309        };
1310        mock_price_calculator
1311            .expect_get_transaction_price_params()
1312            .returning(move |_, _| Ok(price_params.clone()));
1313
1314        mock_signer.expect_sign_transaction().returning(|_| {
1315            Box::pin(ready(Ok(
1316                crate::domain::relayer::SignTransactionResponse::Evm(
1317                    crate::domain::relayer::SignTransactionResponseEvm {
1318                        hash: "0xtx_hash".to_string(),
1319                        signature: crate::models::EvmTransactionDataSignature {
1320                            r: "r".to_string(),
1321                            s: "s".to_string(),
1322                            v: 1,
1323                            sig: "0xsignature".to_string(),
1324                        },
1325                        raw: vec![1, 2, 3],
1326                    },
1327                ),
1328            )))
1329        });
1330
1331        mock_provider
1332            .expect_get_balance()
1333            .with(eq("0xSender"))
1334            .returning(|_| Box::pin(ready(Ok(U256::from(1000000000000000000u64)))));
1335
1336        // Mock get_block_by_number for gas limit validation (tx has gas_limit: Some(21000))
1337        mock_provider
1338            .expect_get_block_by_number()
1339            .times(1)
1340            .returning(|| {
1341                Box::pin(async {
1342                    use alloy::{network::AnyRpcBlock, rpc::types::Block};
1343                    let mut block: Block = Block::default();
1344                    // Set block gas limit to 30M (higher than tx gas limit of 21_000)
1345                    block.header.gas_limit = 30_000_000u64;
1346                    Ok(AnyRpcBlock::from(block))
1347                })
1348            });
1349
1350        let test_tx_clone = test_tx.clone();
1351        mock_transaction
1352            .expect_partial_update()
1353            .returning(move |_, update| {
1354                let mut updated_tx = test_tx_clone.clone();
1355                if let Some(status) = &update.status {
1356                    updated_tx.status = status.clone();
1357                }
1358                if let Some(network_data) = &update.network_data {
1359                    updated_tx.network_data = network_data.clone();
1360                }
1361                if let Some(hashes) = &update.hashes {
1362                    updated_tx.hashes = hashes.clone();
1363                }
1364                Ok(updated_tx)
1365            });
1366
1367        mock_job_producer
1368            .expect_produce_submit_transaction_job()
1369            .returning(|_, _| Box::pin(ready(Ok(()))));
1370        mock_job_producer
1371            .expect_produce_send_notification_job()
1372            .returning(|_, _| Box::pin(ready(Ok(()))));
1373
1374        let mock_network = MockNetworkRepository::new();
1375
1376        let evm_transaction = EvmRelayerTransaction {
1377            relayer: relayer.clone(),
1378            provider: mock_provider,
1379            relayer_repository: Arc::new(mock_relayer),
1380            network_repository: Arc::new(mock_network),
1381            transaction_repository: Arc::new(mock_transaction),
1382            transaction_counter_service: Arc::new(counter_service),
1383            job_producer: Arc::new(mock_job_producer),
1384            price_calculator: mock_price_calculator,
1385            signer: mock_signer,
1386        };
1387
1388        let result = evm_transaction.prepare_transaction(test_tx.clone()).await;
1389        assert!(result.is_ok());
1390        let prepared_tx = result.unwrap();
1391        assert_eq!(prepared_tx.status, TransactionStatus::Sent);
1392        assert!(!prepared_tx.hashes.is_empty());
1393    }
1394
1395    #[tokio::test]
1396    async fn test_prepare_transaction_with_insufficient_balance() {
1397        let mut mock_transaction = MockTransactionRepository::new();
1398        let mock_relayer = MockRelayerRepository::new();
1399        let mut mock_provider = MockEvmProviderTrait::new();
1400        let mut mock_signer = MockSigner::new();
1401        let mut mock_job_producer = MockJobProducerTrait::new();
1402        let mut mock_price_calculator = MockPriceCalculator::new();
1403        let mut counter_service = MockTransactionCounterTrait::new();
1404
1405        let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
1406            gas_limit_estimation: Some(false),
1407            min_balance: Some(100000000000000000u128),
1408            ..Default::default()
1409        });
1410        let test_tx = create_test_transaction();
1411
1412        counter_service
1413            .expect_get_and_increment()
1414            .returning(|_, _| Box::pin(ready(Ok(42))));
1415
1416        let price_params = PriceParams {
1417            gas_price: Some(30000000000),
1418            max_fee_per_gas: None,
1419            max_priority_fee_per_gas: None,
1420            is_min_bumped: None,
1421            extra_fee: None,
1422            total_cost: U256::from(630000000000000u64),
1423        };
1424        mock_price_calculator
1425            .expect_get_transaction_price_params()
1426            .returning(move |_, _| Ok(price_params.clone()));
1427
1428        mock_signer.expect_sign_transaction().returning(|_| {
1429            Box::pin(ready(Ok(
1430                crate::domain::relayer::SignTransactionResponse::Evm(
1431                    crate::domain::relayer::SignTransactionResponseEvm {
1432                        hash: "0xtx_hash".to_string(),
1433                        signature: crate::models::EvmTransactionDataSignature {
1434                            r: "r".to_string(),
1435                            s: "s".to_string(),
1436                            v: 1,
1437                            sig: "0xsignature".to_string(),
1438                        },
1439                        raw: vec![1, 2, 3],
1440                    },
1441                ),
1442            )))
1443        });
1444
1445        mock_provider
1446            .expect_get_balance()
1447            .with(eq("0xSender"))
1448            .returning(|_| Box::pin(ready(Ok(U256::from(90000000000000000u64)))));
1449
1450        // Mock get_block_by_number for gas limit validation (tx has gas_limit: Some(21000))
1451        mock_provider
1452            .expect_get_block_by_number()
1453            .times(1)
1454            .returning(|| {
1455                Box::pin(async {
1456                    use alloy::{network::AnyRpcBlock, rpc::types::Block};
1457                    let mut block: Block = Block::default();
1458                    // Set block gas limit to 30M (higher than tx gas limit of 21_000)
1459                    block.header.gas_limit = 30_000_000u64;
1460                    Ok(AnyRpcBlock::from(block))
1461                })
1462            });
1463
1464        let test_tx_clone = test_tx.clone();
1465        mock_transaction
1466            .expect_partial_update()
1467            .withf(move |id, update| {
1468                id == "test-tx-id" && update.status == Some(TransactionStatus::Failed)
1469            })
1470            .returning(move |_, update| {
1471                let mut updated_tx = test_tx_clone.clone();
1472                updated_tx.status = update.status.unwrap_or(updated_tx.status);
1473                updated_tx.status_reason = update.status_reason.clone();
1474                Ok(updated_tx)
1475            });
1476
1477        mock_job_producer
1478            .expect_produce_send_notification_job()
1479            .returning(|_, _| Box::pin(ready(Ok(()))));
1480
1481        let mock_network = MockNetworkRepository::new();
1482
1483        let evm_transaction = EvmRelayerTransaction {
1484            relayer: relayer.clone(),
1485            provider: mock_provider,
1486            relayer_repository: Arc::new(mock_relayer),
1487            network_repository: Arc::new(mock_network),
1488            transaction_repository: Arc::new(mock_transaction),
1489            transaction_counter_service: Arc::new(counter_service),
1490            job_producer: Arc::new(mock_job_producer),
1491            price_calculator: mock_price_calculator,
1492            signer: mock_signer,
1493        };
1494
1495        let result = evm_transaction.prepare_transaction(test_tx.clone()).await;
1496        assert!(result.is_ok(), "Expected Ok, got: {result:?}");
1497
1498        let updated_tx = result.unwrap();
1499        assert_eq!(
1500            updated_tx.status,
1501            TransactionStatus::Failed,
1502            "Transaction should be marked as Failed"
1503        );
1504        assert!(
1505            updated_tx.status_reason.is_some(),
1506            "Status reason should be set"
1507        );
1508        assert!(
1509            updated_tx
1510                .status_reason
1511                .as_ref()
1512                .unwrap()
1513                .to_lowercase()
1514                .contains("insufficient balance"),
1515            "Status reason should contain insufficient balance error, got: {:?}",
1516            updated_tx.status_reason
1517        );
1518    }
1519
1520    #[tokio::test]
1521    async fn test_prepare_transaction_with_gas_limit_exceeding_block_limit() {
1522        let mut mock_transaction = MockTransactionRepository::new();
1523        let mock_relayer = MockRelayerRepository::new();
1524        let mut mock_provider = MockEvmProviderTrait::new();
1525        let mock_signer = MockSigner::new();
1526        let mut mock_job_producer = MockJobProducerTrait::new();
1527        let mock_price_calculator = MockPriceCalculator::new();
1528        let mut counter_service = MockTransactionCounterTrait::new();
1529
1530        let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
1531            gas_limit_estimation: Some(false), // User provides gas limit
1532            min_balance: Some(100000000000000000u128),
1533            ..Default::default()
1534        });
1535
1536        // Create a transaction with a gas limit that exceeds block gas limit
1537        let mut test_tx = create_test_transaction();
1538        if let NetworkTransactionData::Evm(ref mut evm_data) = test_tx.network_data {
1539            evm_data.gas_limit = Some(30_000_001); // Exceeds typical block gas limit of 30M
1540        }
1541
1542        counter_service
1543            .expect_get_and_increment()
1544            .returning(|_, _| Box::pin(ready(Ok(42))));
1545
1546        // Mock get_block_by_number to return a block with gas_limit lower than tx gas_limit
1547        mock_provider
1548            .expect_get_block_by_number()
1549            .times(1)
1550            .returning(|| {
1551                Box::pin(async {
1552                    use alloy::{network::AnyRpcBlock, rpc::types::Block};
1553                    let mut block: Block = Block::default();
1554                    // Set block gas limit to 30M (lower than tx gas limit of 30_000_001)
1555                    block.header.gas_limit = 30_000_000u64;
1556                    Ok(AnyRpcBlock::from(block))
1557                })
1558            });
1559
1560        // Mock partial_update to be called when marking transaction as failed
1561        let test_tx_clone = test_tx.clone();
1562        mock_transaction
1563            .expect_partial_update()
1564            .withf(move |id, update| {
1565                id == "test-tx-id"
1566                    && update.status == Some(TransactionStatus::Failed)
1567                    && update.status_reason.is_some()
1568                    && update
1569                        .status_reason
1570                        .as_ref()
1571                        .unwrap()
1572                        .contains("exceeds block gas limit")
1573            })
1574            .returning(move |_, update| {
1575                let mut updated_tx = test_tx_clone.clone();
1576                updated_tx.status = update.status.unwrap_or(updated_tx.status);
1577                updated_tx.status_reason = update.status_reason.clone();
1578                Ok(updated_tx)
1579            });
1580
1581        mock_job_producer
1582            .expect_produce_send_notification_job()
1583            .returning(|_, _| Box::pin(ready(Ok(()))));
1584
1585        let mock_network = MockNetworkRepository::new();
1586
1587        let evm_transaction = EvmRelayerTransaction {
1588            relayer: relayer.clone(),
1589            provider: mock_provider,
1590            relayer_repository: Arc::new(mock_relayer),
1591            network_repository: Arc::new(mock_network),
1592            transaction_repository: Arc::new(mock_transaction),
1593            transaction_counter_service: Arc::new(counter_service),
1594            job_producer: Arc::new(mock_job_producer),
1595            price_calculator: mock_price_calculator,
1596            signer: mock_signer,
1597        };
1598
1599        let result = evm_transaction.prepare_transaction(test_tx.clone()).await;
1600        assert!(result.is_ok(), "Expected Ok, got: {result:?}");
1601
1602        let updated_tx = result.unwrap();
1603        assert_eq!(
1604            updated_tx.status,
1605            TransactionStatus::Failed,
1606            "Transaction should be marked as Failed"
1607        );
1608        assert!(
1609            updated_tx.status_reason.is_some(),
1610            "Status reason should be set"
1611        );
1612        assert!(
1613            updated_tx
1614                .status_reason
1615                .as_ref()
1616                .unwrap()
1617                .contains("exceeds block gas limit"),
1618            "Status reason should mention gas limit exceeds block gas limit, got: {:?}",
1619            updated_tx.status_reason
1620        );
1621        assert!(
1622            updated_tx
1623                .status_reason
1624                .as_ref()
1625                .unwrap()
1626                .contains("30000001"),
1627            "Status reason should contain transaction gas limit, got: {:?}",
1628            updated_tx.status_reason
1629        );
1630        assert!(
1631            updated_tx
1632                .status_reason
1633                .as_ref()
1634                .unwrap()
1635                .contains("30000000"),
1636            "Status reason should contain block gas limit, got: {:?}",
1637            updated_tx.status_reason
1638        );
1639    }
1640
1641    #[tokio::test]
1642    async fn test_prepare_transaction_with_gas_limit_within_block_limit() {
1643        let mut mock_transaction = MockTransactionRepository::new();
1644        let mock_relayer = MockRelayerRepository::new();
1645        let mut mock_provider = MockEvmProviderTrait::new();
1646        let mut mock_signer = MockSigner::new();
1647        let mut mock_job_producer = MockJobProducerTrait::new();
1648        let mut mock_price_calculator = MockPriceCalculator::new();
1649        let mut counter_service = MockTransactionCounterTrait::new();
1650
1651        let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
1652            gas_limit_estimation: Some(false), // User provides gas limit
1653            min_balance: Some(100000000000000000u128),
1654            ..Default::default()
1655        });
1656
1657        // Create a transaction with a gas limit within block gas limit
1658        let mut test_tx = create_test_transaction();
1659        if let NetworkTransactionData::Evm(ref mut evm_data) = test_tx.network_data {
1660            evm_data.gas_limit = Some(21_000); // Within typical block gas limit of 30M
1661        }
1662
1663        counter_service
1664            .expect_get_and_increment()
1665            .returning(|_, _| Box::pin(ready(Ok(42))));
1666
1667        let price_params = PriceParams {
1668            gas_price: Some(30000000000),
1669            max_fee_per_gas: None,
1670            max_priority_fee_per_gas: None,
1671            is_min_bumped: None,
1672            extra_fee: None,
1673            total_cost: U256::from(630000000000000u64),
1674        };
1675        mock_price_calculator
1676            .expect_get_transaction_price_params()
1677            .returning(move |_, _| Ok(price_params.clone()));
1678
1679        mock_signer.expect_sign_transaction().returning(|_| {
1680            Box::pin(ready(Ok(
1681                crate::domain::relayer::SignTransactionResponse::Evm(
1682                    crate::domain::relayer::SignTransactionResponseEvm {
1683                        hash: "0xtx_hash".to_string(),
1684                        signature: crate::models::EvmTransactionDataSignature {
1685                            r: "r".to_string(),
1686                            s: "s".to_string(),
1687                            v: 1,
1688                            sig: "0xsignature".to_string(),
1689                        },
1690                        raw: vec![1, 2, 3],
1691                    },
1692                ),
1693            )))
1694        });
1695
1696        mock_provider
1697            .expect_get_balance()
1698            .with(eq("0xSender"))
1699            .returning(|_| Box::pin(ready(Ok(U256::from(1000000000000000000u64)))));
1700
1701        // Mock get_block_by_number to return a block with gas_limit higher than tx gas_limit
1702        mock_provider
1703            .expect_get_block_by_number()
1704            .times(1)
1705            .returning(|| {
1706                Box::pin(async {
1707                    use alloy::{network::AnyRpcBlock, rpc::types::Block};
1708                    let mut block: Block = Block::default();
1709                    // Set block gas limit to 30M (higher than tx gas limit of 21_000)
1710                    block.header.gas_limit = 30_000_000u64;
1711                    Ok(AnyRpcBlock::from(block))
1712                })
1713            });
1714
1715        let test_tx_clone = test_tx.clone();
1716        mock_transaction
1717            .expect_partial_update()
1718            .returning(move |_, update| {
1719                let mut updated_tx = test_tx_clone.clone();
1720                if let Some(status) = &update.status {
1721                    updated_tx.status = status.clone();
1722                }
1723                if let Some(network_data) = &update.network_data {
1724                    updated_tx.network_data = network_data.clone();
1725                }
1726                if let Some(hashes) = &update.hashes {
1727                    updated_tx.hashes = hashes.clone();
1728                }
1729                Ok(updated_tx)
1730            });
1731
1732        mock_job_producer
1733            .expect_produce_submit_transaction_job()
1734            .returning(|_, _| Box::pin(ready(Ok(()))));
1735        mock_job_producer
1736            .expect_produce_send_notification_job()
1737            .returning(|_, _| Box::pin(ready(Ok(()))));
1738
1739        let mock_network = MockNetworkRepository::new();
1740
1741        let evm_transaction = EvmRelayerTransaction {
1742            relayer: relayer.clone(),
1743            provider: mock_provider,
1744            relayer_repository: Arc::new(mock_relayer),
1745            network_repository: Arc::new(mock_network),
1746            transaction_repository: Arc::new(mock_transaction),
1747            transaction_counter_service: Arc::new(counter_service),
1748            job_producer: Arc::new(mock_job_producer),
1749            price_calculator: mock_price_calculator,
1750            signer: mock_signer,
1751        };
1752
1753        let result = evm_transaction.prepare_transaction(test_tx.clone()).await;
1754        assert!(result.is_ok(), "Expected Ok, got: {result:?}");
1755
1756        let prepared_tx = result.unwrap();
1757        // Transaction should proceed normally (not be marked as Failed)
1758        assert_eq!(prepared_tx.status, TransactionStatus::Sent);
1759        assert!(!prepared_tx.hashes.is_empty());
1760    }
1761
1762    #[tokio::test]
1763    async fn test_cancel_transaction() {
1764        // Test Case 1: Canceling a pending transaction
1765        {
1766            // Create mocks for all dependencies
1767            let mut mock_transaction = MockTransactionRepository::new();
1768            let mock_relayer = MockRelayerRepository::new();
1769            let mock_provider = MockEvmProviderTrait::new();
1770            let mock_signer = MockSigner::new();
1771            let mut mock_job_producer = MockJobProducerTrait::new();
1772            let mock_price_calculator = MockPriceCalculator::new();
1773            let counter_service = MockTransactionCounterTrait::new();
1774
1775            // Create test relayer and pending transaction
1776            let relayer = create_test_relayer();
1777            let mut test_tx = create_test_transaction();
1778            test_tx.status = TransactionStatus::Pending;
1779
1780            // Transaction repository should update the transaction with Canceled status
1781            let test_tx_clone = test_tx.clone();
1782            mock_transaction
1783                .expect_partial_update()
1784                .withf(move |id, update| {
1785                    id == "test-tx-id" && update.status == Some(TransactionStatus::Canceled)
1786                })
1787                .returning(move |_, update| {
1788                    let mut updated_tx = test_tx_clone.clone();
1789                    updated_tx.status = update.status.unwrap_or(updated_tx.status);
1790                    Ok(updated_tx)
1791                });
1792
1793            // Job producer should send notification
1794            mock_job_producer
1795                .expect_produce_send_notification_job()
1796                .returning(|_, _| Box::pin(ready(Ok(()))));
1797
1798            let mock_network = MockNetworkRepository::new();
1799
1800            // Set up EVM transaction with the mocks
1801            let evm_transaction = EvmRelayerTransaction {
1802                relayer: relayer.clone(),
1803                provider: mock_provider,
1804                relayer_repository: Arc::new(mock_relayer),
1805                network_repository: Arc::new(mock_network),
1806                transaction_repository: Arc::new(mock_transaction),
1807                transaction_counter_service: Arc::new(counter_service),
1808                job_producer: Arc::new(mock_job_producer),
1809                price_calculator: mock_price_calculator,
1810                signer: mock_signer,
1811            };
1812
1813            // Call cancel_transaction and verify it succeeds
1814            let result = evm_transaction.cancel_transaction(test_tx.clone()).await;
1815            assert!(result.is_ok());
1816            let cancelled_tx = result.unwrap();
1817            assert_eq!(cancelled_tx.id, "test-tx-id");
1818            assert_eq!(cancelled_tx.status, TransactionStatus::Canceled);
1819        }
1820
1821        // Test Case 2: Canceling a submitted transaction
1822        {
1823            // Create mocks for all dependencies
1824            let mut mock_transaction = MockTransactionRepository::new();
1825            let mock_relayer = MockRelayerRepository::new();
1826            let mock_provider = MockEvmProviderTrait::new();
1827            let mut mock_signer = MockSigner::new();
1828            let mut mock_job_producer = MockJobProducerTrait::new();
1829            let mut mock_price_calculator = MockPriceCalculator::new();
1830            let counter_service = MockTransactionCounterTrait::new();
1831
1832            // Create test relayer and submitted transaction
1833            let relayer = create_test_relayer();
1834            let mut test_tx = create_test_transaction();
1835            test_tx.status = TransactionStatus::Submitted;
1836            test_tx.sent_at = Some(Utc::now().to_rfc3339());
1837            test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
1838                nonce: Some(42),
1839                hash: Some("0xoriginal_hash".to_string()),
1840                ..test_tx.network_data.get_evm_transaction_data().unwrap()
1841            });
1842
1843            // Set up price calculator expectations for cancellation tx
1844            mock_price_calculator
1845                .expect_get_transaction_price_params()
1846                .return_once(move |_, _| {
1847                    Ok(PriceParams {
1848                        gas_price: Some(40000000000), // 40 Gwei (higher than original)
1849                        max_fee_per_gas: None,
1850                        max_priority_fee_per_gas: None,
1851                        is_min_bumped: Some(true),
1852                        extra_fee: Some(U256::ZERO),
1853                        total_cost: U256::ZERO,
1854                    })
1855                });
1856
1857            // Signer should be called to sign the cancellation transaction
1858            mock_signer.expect_sign_transaction().returning(|_| {
1859                Box::pin(ready(Ok(
1860                    crate::domain::relayer::SignTransactionResponse::Evm(
1861                        crate::domain::relayer::SignTransactionResponseEvm {
1862                            hash: "0xcancellation_hash".to_string(),
1863                            signature: crate::models::EvmTransactionDataSignature {
1864                                r: "r".to_string(),
1865                                s: "s".to_string(),
1866                                v: 1,
1867                                sig: "0xsignature".to_string(),
1868                            },
1869                            raw: vec![1, 2, 3],
1870                        },
1871                    ),
1872                )))
1873            });
1874
1875            // Transaction repository should update the transaction
1876            let test_tx_clone = test_tx.clone();
1877            mock_transaction
1878                .expect_partial_update()
1879                .returning(move |tx_id, update| {
1880                    let mut updated_tx = test_tx_clone.clone();
1881                    updated_tx.id = tx_id;
1882                    updated_tx.status = update.status.unwrap_or(updated_tx.status);
1883                    updated_tx.network_data =
1884                        update.network_data.unwrap_or(updated_tx.network_data);
1885                    if let Some(hashes) = update.hashes {
1886                        updated_tx.hashes = hashes;
1887                    }
1888                    Ok(updated_tx)
1889                });
1890
1891            // Job producer expectations
1892            mock_job_producer
1893                .expect_produce_submit_transaction_job()
1894                .returning(|_, _| Box::pin(ready(Ok(()))));
1895            mock_job_producer
1896                .expect_produce_send_notification_job()
1897                .returning(|_, _| Box::pin(ready(Ok(()))));
1898
1899            // Network repository expectations for cancellation NOOP transaction
1900            let mut mock_network = MockNetworkRepository::new();
1901            mock_network
1902                .expect_get_by_chain_id()
1903                .with(eq(NetworkType::Evm), eq(1))
1904                .returning(|_, _| {
1905                    use crate::config::{EvmNetworkConfig, NetworkConfigCommon};
1906                    use crate::models::{NetworkConfigData, NetworkRepoModel, RpcConfig};
1907
1908                    let config = EvmNetworkConfig {
1909                        common: NetworkConfigCommon {
1910                            network: "mainnet".to_string(),
1911                            from: None,
1912                            rpc_urls: Some(vec![RpcConfig::new(
1913                                "https://rpc.example.com".to_string(),
1914                            )]),
1915                            explorer_urls: None,
1916                            average_blocktime_ms: Some(12000),
1917                            is_testnet: Some(false),
1918                            tags: Some(vec!["mainnet".to_string()]),
1919                        },
1920                        chain_id: Some(1),
1921                        required_confirmations: Some(12),
1922                        features: Some(vec!["eip1559".to_string()]),
1923                        symbol: Some("ETH".to_string()),
1924                        gas_price_cache: None,
1925                    };
1926                    Ok(Some(NetworkRepoModel {
1927                        id: "evm:mainnet".to_string(),
1928                        name: "mainnet".to_string(),
1929                        network_type: NetworkType::Evm,
1930                        config: NetworkConfigData::Evm(config),
1931                    }))
1932                });
1933
1934            // Set up EVM transaction with the mocks
1935            let evm_transaction = EvmRelayerTransaction {
1936                relayer: relayer.clone(),
1937                provider: mock_provider,
1938                relayer_repository: Arc::new(mock_relayer),
1939                network_repository: Arc::new(mock_network),
1940                transaction_repository: Arc::new(mock_transaction),
1941                transaction_counter_service: Arc::new(counter_service),
1942                job_producer: Arc::new(mock_job_producer),
1943                price_calculator: mock_price_calculator,
1944                signer: mock_signer,
1945            };
1946
1947            // Call cancel_transaction and verify it succeeds
1948            let result = evm_transaction.cancel_transaction(test_tx.clone()).await;
1949            assert!(result.is_ok());
1950            let cancelled_tx = result.unwrap();
1951
1952            // Verify the cancellation transaction was properly created
1953            assert_eq!(cancelled_tx.id, "test-tx-id");
1954            assert_eq!(cancelled_tx.status, TransactionStatus::Submitted);
1955
1956            // Verify the network data was properly updated
1957            if let NetworkTransactionData::Evm(evm_data) = &cancelled_tx.network_data {
1958                assert_eq!(evm_data.nonce, Some(42)); // Same nonce as original
1959            } else {
1960                panic!("Expected EVM transaction data");
1961            }
1962        }
1963
1964        // Test Case 3: Attempting to cancel a confirmed transaction (should fail)
1965        {
1966            // Create minimal mocks for failure case
1967            let mock_transaction = MockTransactionRepository::new();
1968            let mock_relayer = MockRelayerRepository::new();
1969            let mock_provider = MockEvmProviderTrait::new();
1970            let mock_signer = MockSigner::new();
1971            let mock_job_producer = MockJobProducerTrait::new();
1972            let mock_price_calculator = MockPriceCalculator::new();
1973            let counter_service = MockTransactionCounterTrait::new();
1974
1975            // Create test relayer and confirmed transaction
1976            let relayer = create_test_relayer();
1977            let mut test_tx = create_test_transaction();
1978            test_tx.status = TransactionStatus::Confirmed;
1979
1980            let mock_network = MockNetworkRepository::new();
1981
1982            // Set up EVM transaction with the mocks
1983            let evm_transaction = EvmRelayerTransaction {
1984                relayer: relayer.clone(),
1985                provider: mock_provider,
1986                relayer_repository: Arc::new(mock_relayer),
1987                network_repository: Arc::new(mock_network),
1988                transaction_repository: Arc::new(mock_transaction),
1989                transaction_counter_service: Arc::new(counter_service),
1990                job_producer: Arc::new(mock_job_producer),
1991                price_calculator: mock_price_calculator,
1992                signer: mock_signer,
1993            };
1994
1995            // Call cancel_transaction and verify it fails
1996            let result = evm_transaction.cancel_transaction(test_tx.clone()).await;
1997            assert!(result.is_err());
1998            if let Err(TransactionError::ValidationError(msg)) = result {
1999                assert!(msg.contains("Invalid transaction state for cancel_transaction"));
2000            } else {
2001                panic!("Expected ValidationError");
2002            }
2003        }
2004    }
2005
2006    #[tokio::test]
2007    async fn test_replace_transaction() {
2008        // Test Case: Replacing a submitted transaction with new gas price
2009        {
2010            // Create mocks for all dependencies
2011            let mut mock_transaction = MockTransactionRepository::new();
2012            let mock_relayer = MockRelayerRepository::new();
2013            let mut mock_provider = MockEvmProviderTrait::new();
2014            let mut mock_signer = MockSigner::new();
2015            let mut mock_job_producer = MockJobProducerTrait::new();
2016            let mut mock_price_calculator = MockPriceCalculator::new();
2017            let counter_service = MockTransactionCounterTrait::new();
2018
2019            // Create test relayer and submitted transaction
2020            let relayer = create_test_relayer();
2021            let mut test_tx = create_test_transaction();
2022            test_tx.status = TransactionStatus::Submitted;
2023            test_tx.sent_at = Some(Utc::now().to_rfc3339());
2024
2025            // Set up price calculator expectations for replacement
2026            mock_price_calculator
2027                .expect_get_transaction_price_params()
2028                .return_once(move |_, _| {
2029                    Ok(PriceParams {
2030                        gas_price: Some(40000000000), // 40 Gwei (higher than original)
2031                        max_fee_per_gas: None,
2032                        max_priority_fee_per_gas: None,
2033                        is_min_bumped: Some(true),
2034                        extra_fee: Some(U256::ZERO),
2035                        total_cost: U256::from(2001000000000000000u64), // 2 ETH + gas costs
2036                    })
2037                });
2038
2039            // Signer should be called to sign the replacement transaction
2040            mock_signer.expect_sign_transaction().returning(|_| {
2041                Box::pin(ready(Ok(
2042                    crate::domain::relayer::SignTransactionResponse::Evm(
2043                        crate::domain::relayer::SignTransactionResponseEvm {
2044                            hash: "0xreplacement_hash".to_string(),
2045                            signature: crate::models::EvmTransactionDataSignature {
2046                                r: "r".to_string(),
2047                                s: "s".to_string(),
2048                                v: 1,
2049                                sig: "0xsignature".to_string(),
2050                            },
2051                            raw: vec![1, 2, 3],
2052                        },
2053                    ),
2054                )))
2055            });
2056
2057            // Provider balance check should pass
2058            mock_provider
2059                .expect_get_balance()
2060                .with(eq("0xSender"))
2061                .returning(|_| Box::pin(ready(Ok(U256::from(3000000000000000000u64)))));
2062
2063            // Transaction repository should update using update_network_data
2064            let test_tx_clone = test_tx.clone();
2065            mock_transaction
2066                .expect_update_network_data()
2067                .returning(move |tx_id, network_data| {
2068                    let mut updated_tx = test_tx_clone.clone();
2069                    updated_tx.id = tx_id;
2070                    updated_tx.network_data = network_data;
2071                    Ok(updated_tx)
2072                });
2073
2074            // Job producer expectations
2075            mock_job_producer
2076                .expect_produce_submit_transaction_job()
2077                .returning(|_, _| Box::pin(ready(Ok(()))));
2078            mock_job_producer
2079                .expect_produce_send_notification_job()
2080                .returning(|_, _| Box::pin(ready(Ok(()))));
2081
2082            // Network repository expectations for mempool check
2083            let mut mock_network = MockNetworkRepository::new();
2084            mock_network
2085                .expect_get_by_chain_id()
2086                .with(eq(NetworkType::Evm), eq(1))
2087                .returning(|_, _| {
2088                    use crate::config::{EvmNetworkConfig, NetworkConfigCommon};
2089                    use crate::models::{NetworkConfigData, NetworkRepoModel};
2090
2091                    let config = EvmNetworkConfig {
2092                        common: NetworkConfigCommon {
2093                            network: "mainnet".to_string(),
2094                            from: None,
2095                            rpc_urls: Some(vec![crate::models::RpcConfig::new(
2096                                "https://rpc.example.com".to_string(),
2097                            )]),
2098                            explorer_urls: None,
2099                            average_blocktime_ms: Some(12000),
2100                            is_testnet: Some(false),
2101                            tags: Some(vec!["mainnet".to_string()]), // No "no-mempool" tag
2102                        },
2103                        chain_id: Some(1),
2104                        required_confirmations: Some(12),
2105                        features: Some(vec!["eip1559".to_string()]),
2106                        symbol: Some("ETH".to_string()),
2107                        gas_price_cache: None,
2108                    };
2109                    Ok(Some(NetworkRepoModel {
2110                        id: "evm:mainnet".to_string(),
2111                        name: "mainnet".to_string(),
2112                        network_type: NetworkType::Evm,
2113                        config: NetworkConfigData::Evm(config),
2114                    }))
2115                });
2116
2117            // Set up EVM transaction with the mocks
2118            let evm_transaction = EvmRelayerTransaction {
2119                relayer: relayer.clone(),
2120                provider: mock_provider,
2121                relayer_repository: Arc::new(mock_relayer),
2122                network_repository: Arc::new(mock_network),
2123                transaction_repository: Arc::new(mock_transaction),
2124                transaction_counter_service: Arc::new(counter_service),
2125                job_producer: Arc::new(mock_job_producer),
2126                price_calculator: mock_price_calculator,
2127                signer: mock_signer,
2128            };
2129
2130            // Create replacement request with speed-based pricing
2131            let replacement_request = NetworkTransactionRequest::Evm(EvmTransactionRequest {
2132                to: Some("0xNewRecipient".to_string()),
2133                value: U256::from(2000000000000000000u64), // 2 ETH
2134                data: Some("0xNewData".to_string()),
2135                gas_limit: Some(25000),
2136                gas_price: None, // Use speed-based pricing
2137                max_fee_per_gas: None,
2138                max_priority_fee_per_gas: None,
2139                speed: Some(Speed::Fast),
2140                valid_until: None,
2141            });
2142
2143            // Call replace_transaction and verify it succeeds
2144            let result = evm_transaction
2145                .replace_transaction(test_tx.clone(), replacement_request)
2146                .await;
2147            if let Err(ref e) = result {
2148                eprintln!("Replace transaction failed with error: {e:?}");
2149            }
2150            assert!(result.is_ok());
2151            let replaced_tx = result.unwrap();
2152
2153            // Verify the replacement was properly processed
2154            assert_eq!(replaced_tx.id, "test-tx-id");
2155
2156            // Verify the network data was properly updated
2157            if let NetworkTransactionData::Evm(evm_data) = &replaced_tx.network_data {
2158                assert_eq!(evm_data.to, Some("0xNewRecipient".to_string()));
2159                assert_eq!(evm_data.value, U256::from(2000000000000000000u64));
2160                assert_eq!(evm_data.gas_price, Some(40000000000));
2161                assert_eq!(evm_data.gas_limit, Some(25000));
2162                assert!(evm_data.hash.is_some());
2163                assert!(evm_data.raw.is_some());
2164            } else {
2165                panic!("Expected EVM transaction data");
2166            }
2167        }
2168
2169        // Test Case: Attempting to replace a confirmed transaction (should fail)
2170        {
2171            // Create minimal mocks for failure case
2172            let mock_transaction = MockTransactionRepository::new();
2173            let mock_relayer = MockRelayerRepository::new();
2174            let mock_provider = MockEvmProviderTrait::new();
2175            let mock_signer = MockSigner::new();
2176            let mock_job_producer = MockJobProducerTrait::new();
2177            let mock_price_calculator = MockPriceCalculator::new();
2178            let counter_service = MockTransactionCounterTrait::new();
2179
2180            // Create test relayer and confirmed transaction
2181            let relayer = create_test_relayer();
2182            let mut test_tx = create_test_transaction();
2183            test_tx.status = TransactionStatus::Confirmed;
2184
2185            let mock_network = MockNetworkRepository::new();
2186
2187            // Set up EVM transaction with the mocks
2188            let evm_transaction = EvmRelayerTransaction {
2189                relayer: relayer.clone(),
2190                provider: mock_provider,
2191                relayer_repository: Arc::new(mock_relayer),
2192                network_repository: Arc::new(mock_network),
2193                transaction_repository: Arc::new(mock_transaction),
2194                transaction_counter_service: Arc::new(counter_service),
2195                job_producer: Arc::new(mock_job_producer),
2196                price_calculator: mock_price_calculator,
2197                signer: mock_signer,
2198            };
2199
2200            // Create dummy replacement request
2201            let replacement_request = NetworkTransactionRequest::Evm(EvmTransactionRequest {
2202                to: Some("0xNewRecipient".to_string()),
2203                value: U256::from(1000000000000000000u64),
2204                data: Some("0xData".to_string()),
2205                gas_limit: Some(21000),
2206                gas_price: Some(30000000000),
2207                max_fee_per_gas: None,
2208                max_priority_fee_per_gas: None,
2209                speed: Some(Speed::Fast),
2210                valid_until: None,
2211            });
2212
2213            // Call replace_transaction and verify it fails
2214            let result = evm_transaction
2215                .replace_transaction(test_tx.clone(), replacement_request)
2216                .await;
2217            assert!(result.is_err());
2218            if let Err(TransactionError::ValidationError(msg)) = result {
2219                assert!(msg.contains("Invalid transaction state for replace_transaction"));
2220            } else {
2221                panic!("Expected ValidationError");
2222            }
2223        }
2224    }
2225
2226    #[tokio::test]
2227    async fn test_estimate_tx_gas_limit_success() {
2228        let mock_transaction = MockTransactionRepository::new();
2229        let mock_relayer = MockRelayerRepository::new();
2230        let mut mock_provider = MockEvmProviderTrait::new();
2231        let mock_signer = MockSigner::new();
2232        let mock_job_producer = MockJobProducerTrait::new();
2233        let mock_price_calculator = MockPriceCalculator::new();
2234        let counter_service = MockTransactionCounterTrait::new();
2235        let mock_network = MockNetworkRepository::new();
2236
2237        // Create test relayer and pending transaction
2238        let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2239            gas_limit_estimation: Some(true),
2240            ..Default::default()
2241        });
2242        let evm_data = EvmTransactionData {
2243            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
2244            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
2245            value: U256::from(1000000000000000000u128),
2246            data: Some("0x".to_string()),
2247            gas_limit: None,
2248            gas_price: Some(20_000_000_000),
2249            nonce: Some(1),
2250            chain_id: 1,
2251            hash: None,
2252            signature: None,
2253            speed: Some(Speed::Average),
2254            max_fee_per_gas: None,
2255            max_priority_fee_per_gas: None,
2256            raw: None,
2257        };
2258
2259        // Mock provider to return 21000 as estimated gas
2260        mock_provider
2261            .expect_estimate_gas()
2262            .times(1)
2263            .returning(|_| Box::pin(async { Ok(21000) }));
2264
2265        let transaction = EvmRelayerTransaction::new(
2266            relayer.clone(),
2267            mock_provider,
2268            Arc::new(mock_relayer),
2269            Arc::new(mock_network),
2270            Arc::new(mock_transaction),
2271            Arc::new(counter_service),
2272            Arc::new(mock_job_producer),
2273            mock_price_calculator,
2274            mock_signer,
2275        )
2276        .unwrap();
2277
2278        let result = transaction
2279            .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
2280            .await;
2281
2282        assert!(result.is_ok());
2283        // Expected: 21000 * 110 / 100 = 23100
2284        assert_eq!(result.unwrap(), 23100);
2285    }
2286
2287    #[tokio::test]
2288    async fn test_estimate_tx_gas_limit_disabled() {
2289        let mock_transaction = MockTransactionRepository::new();
2290        let mock_relayer = MockRelayerRepository::new();
2291        let mut mock_provider = MockEvmProviderTrait::new();
2292        let mock_signer = MockSigner::new();
2293        let mock_job_producer = MockJobProducerTrait::new();
2294        let mock_price_calculator = MockPriceCalculator::new();
2295        let counter_service = MockTransactionCounterTrait::new();
2296        let mock_network = MockNetworkRepository::new();
2297
2298        // Create test relayer and pending transaction
2299        let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2300            gas_limit_estimation: Some(false),
2301            ..Default::default()
2302        });
2303
2304        let evm_data = EvmTransactionData {
2305            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
2306            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
2307            value: U256::from(1000000000000000000u128),
2308            data: Some("0x".to_string()),
2309            gas_limit: None,
2310            gas_price: Some(20_000_000_000),
2311            nonce: Some(1),
2312            chain_id: 1,
2313            hash: None,
2314            signature: None,
2315            speed: Some(Speed::Average),
2316            max_fee_per_gas: None,
2317            max_priority_fee_per_gas: None,
2318            raw: None,
2319        };
2320
2321        // Provider should not be called when estimation is disabled
2322        mock_provider.expect_estimate_gas().times(0);
2323
2324        let transaction = EvmRelayerTransaction::new(
2325            relayer.clone(),
2326            mock_provider,
2327            Arc::new(mock_relayer),
2328            Arc::new(mock_network),
2329            Arc::new(mock_transaction),
2330            Arc::new(counter_service),
2331            Arc::new(mock_job_producer),
2332            mock_price_calculator,
2333            mock_signer,
2334        )
2335        .unwrap();
2336
2337        let result = transaction
2338            .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
2339            .await;
2340
2341        assert!(result.is_err());
2342        assert!(matches!(
2343            result.unwrap_err(),
2344            TransactionError::UnexpectedError(_)
2345        ));
2346    }
2347
2348    #[tokio::test]
2349    async fn test_estimate_tx_gas_limit_default_enabled() {
2350        let mock_transaction = MockTransactionRepository::new();
2351        let mock_relayer = MockRelayerRepository::new();
2352        let mut mock_provider = MockEvmProviderTrait::new();
2353        let mock_signer = MockSigner::new();
2354        let mock_job_producer = MockJobProducerTrait::new();
2355        let mock_price_calculator = MockPriceCalculator::new();
2356        let counter_service = MockTransactionCounterTrait::new();
2357        let mock_network = MockNetworkRepository::new();
2358
2359        let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2360            gas_limit_estimation: None, // Should default to true
2361            ..Default::default()
2362        });
2363
2364        let evm_data = EvmTransactionData {
2365            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
2366            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
2367            value: U256::from(1000000000000000000u128),
2368            data: Some("0x".to_string()),
2369            gas_limit: None,
2370            gas_price: Some(20_000_000_000),
2371            nonce: Some(1),
2372            chain_id: 1,
2373            hash: None,
2374            signature: None,
2375            speed: Some(Speed::Average),
2376            max_fee_per_gas: None,
2377            max_priority_fee_per_gas: None,
2378            raw: None,
2379        };
2380
2381        // Mock provider to return 50000 as estimated gas
2382        mock_provider
2383            .expect_estimate_gas()
2384            .times(1)
2385            .returning(|_| Box::pin(async { Ok(50000) }));
2386
2387        let transaction = EvmRelayerTransaction::new(
2388            relayer.clone(),
2389            mock_provider,
2390            Arc::new(mock_relayer),
2391            Arc::new(mock_network),
2392            Arc::new(mock_transaction),
2393            Arc::new(counter_service),
2394            Arc::new(mock_job_producer),
2395            mock_price_calculator,
2396            mock_signer,
2397        )
2398        .unwrap();
2399
2400        let result = transaction
2401            .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
2402            .await;
2403
2404        assert!(result.is_ok());
2405        // Expected: 50000 * 110 / 100 = 55000
2406        assert_eq!(result.unwrap(), 55000);
2407    }
2408
2409    #[tokio::test]
2410    async fn test_estimate_tx_gas_limit_provider_error() {
2411        let mock_transaction = MockTransactionRepository::new();
2412        let mock_relayer = MockRelayerRepository::new();
2413        let mut mock_provider = MockEvmProviderTrait::new();
2414        let mock_signer = MockSigner::new();
2415        let mock_job_producer = MockJobProducerTrait::new();
2416        let mock_price_calculator = MockPriceCalculator::new();
2417        let counter_service = MockTransactionCounterTrait::new();
2418        let mock_network = MockNetworkRepository::new();
2419
2420        let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2421            gas_limit_estimation: Some(true),
2422            ..Default::default()
2423        });
2424
2425        let evm_data = EvmTransactionData {
2426            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
2427            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
2428            value: U256::from(1000000000000000000u128),
2429            data: Some("0x".to_string()),
2430            gas_limit: None,
2431            gas_price: Some(20_000_000_000),
2432            nonce: Some(1),
2433            chain_id: 1,
2434            hash: None,
2435            signature: None,
2436            speed: Some(Speed::Average),
2437            max_fee_per_gas: None,
2438            max_priority_fee_per_gas: None,
2439            raw: None,
2440        };
2441
2442        // Mock provider to return an error
2443        mock_provider.expect_estimate_gas().times(1).returning(|_| {
2444            Box::pin(async {
2445                Err(crate::services::provider::ProviderError::Other(
2446                    "RPC error".to_string(),
2447                ))
2448            })
2449        });
2450
2451        let transaction = EvmRelayerTransaction::new(
2452            relayer.clone(),
2453            mock_provider,
2454            Arc::new(mock_relayer),
2455            Arc::new(mock_network),
2456            Arc::new(mock_transaction),
2457            Arc::new(counter_service),
2458            Arc::new(mock_job_producer),
2459            mock_price_calculator,
2460            mock_signer,
2461        )
2462        .unwrap();
2463
2464        let result = transaction
2465            .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
2466            .await;
2467
2468        assert!(result.is_err());
2469        assert!(matches!(
2470            result.unwrap_err(),
2471            TransactionError::UnexpectedError(_)
2472        ));
2473    }
2474
2475    #[tokio::test]
2476    async fn test_prepare_transaction_uses_gas_estimation_and_stores_result() {
2477        let mut mock_transaction = MockTransactionRepository::new();
2478        let mock_relayer = MockRelayerRepository::new();
2479        let mut mock_provider = MockEvmProviderTrait::new();
2480        let mut mock_signer = MockSigner::new();
2481        let mut mock_job_producer = MockJobProducerTrait::new();
2482        let mut mock_price_calculator = MockPriceCalculator::new();
2483        let mut counter_service = MockTransactionCounterTrait::new();
2484        let mock_network = MockNetworkRepository::new();
2485
2486        // Create test relayer with gas limit estimation enabled
2487        let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2488            gas_limit_estimation: Some(true),
2489            min_balance: Some(100000000000000000u128),
2490            ..Default::default()
2491        });
2492
2493        // Create test transaction WITHOUT gas_limit (so estimation will be triggered)
2494        let mut test_tx = create_test_transaction();
2495        if let NetworkTransactionData::Evm(ref mut evm_data) = test_tx.network_data {
2496            evm_data.gas_limit = None; // This should trigger gas estimation
2497            evm_data.nonce = None; // This will be set by the counter service
2498        }
2499
2500        // Expected estimated gas from provider
2501        const PROVIDER_GAS_ESTIMATE: u64 = 45000;
2502        const EXPECTED_GAS_WITH_BUFFER: u64 = 49500; // 45000 * 110 / 100
2503
2504        // Mock provider to return specific gas estimate
2505        mock_provider
2506            .expect_estimate_gas()
2507            .times(1)
2508            .returning(move |_| Box::pin(async move { Ok(PROVIDER_GAS_ESTIMATE) }));
2509
2510        // Mock provider for balance check
2511        mock_provider
2512            .expect_get_balance()
2513            .times(1)
2514            .returning(|_| Box::pin(async { Ok(U256::from(2000000000000000000u128)) })); // 2 ETH
2515
2516        let price_params = PriceParams {
2517            gas_price: Some(20_000_000_000), // 20 Gwei
2518            max_fee_per_gas: None,
2519            max_priority_fee_per_gas: None,
2520            is_min_bumped: None,
2521            extra_fee: None,
2522            total_cost: U256::from(1900000000000000000u128), // 1.9 ETH total cost
2523        };
2524
2525        // Mock price calculator
2526        mock_price_calculator
2527            .expect_get_transaction_price_params()
2528            .returning(move |_, _| Ok(price_params.clone()));
2529
2530        // Mock transaction counter to return a nonce
2531        counter_service
2532            .expect_get_and_increment()
2533            .times(1)
2534            .returning(|_, _| Box::pin(async { Ok(42) }));
2535
2536        // Mock signer to return a signed transaction
2537        mock_signer.expect_sign_transaction().returning(|_| {
2538            Box::pin(ready(Ok(
2539                crate::domain::relayer::SignTransactionResponse::Evm(
2540                    crate::domain::relayer::SignTransactionResponseEvm {
2541                        hash: "0xhash".to_string(),
2542                        signature: crate::models::EvmTransactionDataSignature {
2543                            r: "r".to_string(),
2544                            s: "s".to_string(),
2545                            v: 1,
2546                            sig: "0xsignature".to_string(),
2547                        },
2548                        raw: vec![1, 2, 3],
2549                    },
2550                ),
2551            )))
2552        });
2553
2554        // Mock job producer to capture the submission job
2555        mock_job_producer
2556            .expect_produce_submit_transaction_job()
2557            .returning(|_, _| Box::pin(async { Ok(()) }));
2558
2559        mock_job_producer
2560            .expect_produce_send_notification_job()
2561            .returning(|_, _| Box::pin(ready(Ok(()))));
2562
2563        // Mock transaction repository partial_update calls
2564        // Note: prepare_transaction calls partial_update twice:
2565        // 1. Presign update (saves nonce before signing)
2566        // 2. Postsign update (saves signed data and marks as Sent)
2567        let expected_gas_limit = EXPECTED_GAS_WITH_BUFFER;
2568
2569        let test_tx_clone = test_tx.clone();
2570        mock_transaction
2571            .expect_partial_update()
2572            .times(2)
2573            .returning(move |_, update| {
2574                let mut updated_tx = test_tx_clone.clone();
2575
2576                // Apply the updates from the request
2577                if let Some(status) = &update.status {
2578                    updated_tx.status = status.clone();
2579                }
2580                if let Some(network_data) = &update.network_data {
2581                    updated_tx.network_data = network_data.clone();
2582                } else {
2583                    // If network_data is not being updated, ensure gas_limit is set
2584                    if let NetworkTransactionData::Evm(ref mut evm_data) = updated_tx.network_data {
2585                        if evm_data.gas_limit.is_none() {
2586                            evm_data.gas_limit = Some(expected_gas_limit);
2587                        }
2588                    }
2589                }
2590                if let Some(hashes) = &update.hashes {
2591                    updated_tx.hashes = hashes.clone();
2592                }
2593
2594                Ok(updated_tx)
2595            });
2596
2597        let transaction = EvmRelayerTransaction::new(
2598            relayer.clone(),
2599            mock_provider,
2600            Arc::new(mock_relayer),
2601            Arc::new(mock_network),
2602            Arc::new(mock_transaction),
2603            Arc::new(counter_service),
2604            Arc::new(mock_job_producer),
2605            mock_price_calculator,
2606            mock_signer,
2607        )
2608        .unwrap();
2609
2610        // Call prepare_transaction
2611        let result = transaction.prepare_transaction(test_tx).await;
2612
2613        // Verify the transaction was prepared successfully
2614        assert!(result.is_ok(), "prepare_transaction should succeed");
2615        let prepared_tx = result.unwrap();
2616
2617        // Verify the final transaction has the estimated gas limit
2618        if let NetworkTransactionData::Evm(evm_data) = prepared_tx.network_data {
2619            assert_eq!(evm_data.gas_limit, Some(EXPECTED_GAS_WITH_BUFFER));
2620        } else {
2621            panic!("Expected EVM network data");
2622        }
2623    }
2624
2625    #[tokio::test]
2626    async fn test_prepare_transaction_estimates_gas_for_contract_creation() {
2627        let mut mock_transaction = MockTransactionRepository::new();
2628        let mock_relayer = MockRelayerRepository::new();
2629        let mut mock_provider = MockEvmProviderTrait::new();
2630        let mut mock_signer = MockSigner::new();
2631        let mut mock_job_producer = MockJobProducerTrait::new();
2632        let mut mock_price_calculator = MockPriceCalculator::new();
2633        let mut counter_service = MockTransactionCounterTrait::new();
2634        let mock_network = MockNetworkRepository::new();
2635
2636        let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2637            gas_limit_estimation: Some(true),
2638            min_balance: Some(100000000000000000u128),
2639            ..Default::default()
2640        });
2641
2642        let mut test_tx = create_test_transaction();
2643        if let NetworkTransactionData::Evm(ref mut evm_data) = test_tx.network_data {
2644            evm_data.to = None;
2645            evm_data.data = Some("0x6080604052348015600f57600080fd5b".to_string());
2646            evm_data.gas_limit = None;
2647            evm_data.nonce = None;
2648        }
2649
2650        const PROVIDER_GAS_ESTIMATE: u64 = 1500000;
2651        const EXPECTED_GAS_WITH_BUFFER: u64 = 1650000;
2652
2653        mock_provider
2654            .expect_estimate_gas()
2655            .withf(|tx| tx.to.is_none())
2656            .times(1)
2657            .returning(move |_| Box::pin(async move { Ok(PROVIDER_GAS_ESTIMATE) }));
2658
2659        mock_provider
2660            .expect_get_balance()
2661            .times(1)
2662            .returning(|_| Box::pin(async { Ok(U256::from(2000000000000000000u128)) }));
2663
2664        let price_params = PriceParams {
2665            gas_price: Some(20_000_000_000),
2666            max_fee_per_gas: None,
2667            max_priority_fee_per_gas: None,
2668            is_min_bumped: None,
2669            extra_fee: None,
2670            total_cost: U256::from(1900000000000000000u128),
2671        };
2672
2673        mock_price_calculator
2674            .expect_get_transaction_price_params()
2675            .returning(move |_, _| Ok(price_params.clone()));
2676
2677        counter_service
2678            .expect_get_and_increment()
2679            .times(1)
2680            .returning(|_, _| Box::pin(async { Ok(42) }));
2681
2682        mock_signer.expect_sign_transaction().returning(|_| {
2683            Box::pin(ready(Ok(
2684                crate::domain::relayer::SignTransactionResponse::Evm(
2685                    crate::domain::relayer::SignTransactionResponseEvm {
2686                        hash: "0xhash".to_string(),
2687                        signature: crate::models::EvmTransactionDataSignature {
2688                            r: "r".to_string(),
2689                            s: "s".to_string(),
2690                            v: 1,
2691                            sig: "0xsignature".to_string(),
2692                        },
2693                        raw: vec![1, 2, 3],
2694                    },
2695                ),
2696            )))
2697        });
2698
2699        mock_job_producer
2700            .expect_produce_submit_transaction_job()
2701            .returning(|_, _| Box::pin(async { Ok(()) }));
2702
2703        mock_job_producer
2704            .expect_produce_send_notification_job()
2705            .returning(|_, _| Box::pin(ready(Ok(()))));
2706
2707        let expected_gas_limit = EXPECTED_GAS_WITH_BUFFER;
2708        let test_tx_clone = test_tx.clone();
2709        mock_transaction
2710            .expect_partial_update()
2711            .times(2)
2712            .returning(move |_, update| {
2713                let mut updated_tx = test_tx_clone.clone();
2714
2715                if let Some(status) = &update.status {
2716                    updated_tx.status = status.clone();
2717                }
2718                if let Some(network_data) = &update.network_data {
2719                    updated_tx.network_data = network_data.clone();
2720                } else if let NetworkTransactionData::Evm(ref mut evm_data) =
2721                    updated_tx.network_data
2722                {
2723                    if evm_data.gas_limit.is_none() {
2724                        evm_data.gas_limit = Some(expected_gas_limit);
2725                    }
2726                }
2727                if let Some(hashes) = &update.hashes {
2728                    updated_tx.hashes = hashes.clone();
2729                }
2730
2731                Ok(updated_tx)
2732            });
2733
2734        let transaction = EvmRelayerTransaction::new(
2735            relayer,
2736            mock_provider,
2737            Arc::new(mock_relayer),
2738            Arc::new(mock_network),
2739            Arc::new(mock_transaction),
2740            Arc::new(counter_service),
2741            Arc::new(mock_job_producer),
2742            mock_price_calculator,
2743            mock_signer,
2744        )
2745        .unwrap();
2746
2747        let result = transaction.prepare_transaction(test_tx).await;
2748
2749        assert!(result.is_ok(), "prepare_transaction should succeed");
2750        let prepared_tx = result.unwrap();
2751
2752        if let NetworkTransactionData::Evm(evm_data) = prepared_tx.network_data {
2753            assert_eq!(evm_data.to, None);
2754            assert_eq!(evm_data.gas_limit, Some(EXPECTED_GAS_WITH_BUFFER));
2755        } else {
2756            panic!("Expected EVM network data");
2757        }
2758    }
2759
2760    #[test]
2761    fn test_is_already_submitted_error_detection() {
2762        // Test "already known" variants
2763        assert!(DefaultEvmTransaction::is_already_submitted_error(
2764            &"already known"
2765        ));
2766        assert!(DefaultEvmTransaction::is_already_submitted_error(
2767            &"Transaction already known"
2768        ));
2769        assert!(DefaultEvmTransaction::is_already_submitted_error(
2770            &"Error: already known"
2771        ));
2772
2773        // Test "nonce too low" variants
2774        assert!(DefaultEvmTransaction::is_already_submitted_error(
2775            &"nonce too low"
2776        ));
2777        assert!(DefaultEvmTransaction::is_already_submitted_error(
2778            &"Nonce Too Low"
2779        ));
2780        assert!(DefaultEvmTransaction::is_already_submitted_error(
2781            &"Error: nonce too low"
2782        ));
2783
2784        // Test "nonce is too low" variants (some providers use this wording)
2785        assert!(DefaultEvmTransaction::is_already_submitted_error(
2786            &"nonce is too low"
2787        ));
2788        assert!(DefaultEvmTransaction::is_already_submitted_error(
2789            &"Error: nonce is too low"
2790        ));
2791
2792        // Test "known transaction" variants (Besu)
2793        assert!(DefaultEvmTransaction::is_already_submitted_error(
2794            &"known transaction"
2795        ));
2796        assert!(DefaultEvmTransaction::is_already_submitted_error(
2797            &"Known Transaction"
2798        ));
2799
2800        // Test "replacement transaction underpriced" variants
2801        assert!(DefaultEvmTransaction::is_already_submitted_error(
2802            &"replacement transaction underpriced"
2803        ));
2804        assert!(DefaultEvmTransaction::is_already_submitted_error(
2805            &"Replacement Transaction Underpriced"
2806        ));
2807
2808        // Test "same hash was already imported" (OpenEthereum)
2809        assert!(DefaultEvmTransaction::is_already_submitted_error(
2810            &"same hash was already imported"
2811        ));
2812
2813        // Test non-matching errors
2814        assert!(!DefaultEvmTransaction::is_already_submitted_error(
2815            &"insufficient funds"
2816        ));
2817        assert!(!DefaultEvmTransaction::is_already_submitted_error(
2818            &"execution reverted"
2819        ));
2820        assert!(!DefaultEvmTransaction::is_already_submitted_error(
2821            &"gas too low"
2822        ));
2823        assert!(!DefaultEvmTransaction::is_already_submitted_error(
2824            &"timeout"
2825        ));
2826        // "unknown transaction" must NOT match "known transaction"
2827        assert!(!DefaultEvmTransaction::is_already_submitted_error(
2828            &"Unknown transaction status"
2829        ));
2830    }
2831
2832    /// Test submit_transaction with "already known" error in Sent status
2833    /// This should treat the error as success and update to Submitted
2834    #[tokio::test]
2835    async fn test_submit_transaction_already_known_error_from_sent() {
2836        let mut mock_transaction = MockTransactionRepository::new();
2837        let mock_relayer = MockRelayerRepository::new();
2838        let mut mock_provider = MockEvmProviderTrait::new();
2839        let mock_signer = MockSigner::new();
2840        let mut mock_job_producer = MockJobProducerTrait::new();
2841        let mock_price_calculator = MockPriceCalculator::new();
2842        let counter_service = MockTransactionCounterTrait::new();
2843        let mock_network = MockNetworkRepository::new();
2844
2845        let relayer = create_test_relayer();
2846        let mut test_tx = create_test_transaction();
2847        test_tx.status = TransactionStatus::Sent;
2848        test_tx.sent_at = Some(Utc::now().to_rfc3339());
2849        test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
2850            nonce: Some(42),
2851            hash: Some("0xhash".to_string()),
2852            raw: Some(vec![1, 2, 3]),
2853            ..test_tx.network_data.get_evm_transaction_data().unwrap()
2854        });
2855
2856        // Provider returns "already known" error
2857        mock_provider
2858            .expect_send_raw_transaction()
2859            .times(1)
2860            .returning(|_| {
2861                Box::pin(async {
2862                    Err(crate::services::provider::ProviderError::Other(
2863                        "already known: transaction already in mempool".to_string(),
2864                    ))
2865                })
2866            });
2867
2868        // Should still update to Submitted status
2869        let test_tx_clone = test_tx.clone();
2870        mock_transaction
2871            .expect_partial_update()
2872            .times(1)
2873            .withf(|_, update| update.status == Some(TransactionStatus::Submitted))
2874            .returning(move |_, update| {
2875                let mut updated_tx = test_tx_clone.clone();
2876                updated_tx.status = update.status.unwrap();
2877                updated_tx.sent_at = update.sent_at.clone();
2878                Ok(updated_tx)
2879            });
2880
2881        mock_job_producer
2882            .expect_produce_send_notification_job()
2883            .times(1)
2884            .returning(|_, _| Box::pin(ready(Ok(()))));
2885
2886        let evm_transaction = EvmRelayerTransaction {
2887            relayer: relayer.clone(),
2888            provider: mock_provider,
2889            relayer_repository: Arc::new(mock_relayer),
2890            network_repository: Arc::new(mock_network),
2891            transaction_repository: Arc::new(mock_transaction),
2892            transaction_counter_service: Arc::new(counter_service),
2893            job_producer: Arc::new(mock_job_producer),
2894            price_calculator: mock_price_calculator,
2895            signer: mock_signer,
2896        };
2897
2898        let result = evm_transaction.submit_transaction(test_tx).await;
2899        assert!(result.is_ok());
2900        let updated_tx = result.unwrap();
2901        assert_eq!(updated_tx.status, TransactionStatus::Submitted);
2902    }
2903
2904    /// Test submit_transaction with real error (not "already known") should fail
2905    #[tokio::test]
2906    async fn test_submit_transaction_real_error_fails() {
2907        let mock_transaction = MockTransactionRepository::new();
2908        let mock_relayer = MockRelayerRepository::new();
2909        let mut mock_provider = MockEvmProviderTrait::new();
2910        let mock_signer = MockSigner::new();
2911        let mock_job_producer = MockJobProducerTrait::new();
2912        let mock_price_calculator = MockPriceCalculator::new();
2913        let counter_service = MockTransactionCounterTrait::new();
2914        let mock_network = MockNetworkRepository::new();
2915
2916        let relayer = create_test_relayer();
2917        let mut test_tx = create_test_transaction();
2918        test_tx.status = TransactionStatus::Sent;
2919        test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
2920            raw: Some(vec![1, 2, 3]),
2921            ..test_tx.network_data.get_evm_transaction_data().unwrap()
2922        });
2923
2924        // Provider returns a real error
2925        mock_provider
2926            .expect_send_raw_transaction()
2927            .times(1)
2928            .returning(|_| {
2929                Box::pin(async {
2930                    Err(crate::services::provider::ProviderError::Other(
2931                        "insufficient funds for gas * price + value".to_string(),
2932                    ))
2933                })
2934            });
2935
2936        let evm_transaction = EvmRelayerTransaction {
2937            relayer: relayer.clone(),
2938            provider: mock_provider,
2939            relayer_repository: Arc::new(mock_relayer),
2940            network_repository: Arc::new(mock_network),
2941            transaction_repository: Arc::new(mock_transaction),
2942            transaction_counter_service: Arc::new(counter_service),
2943            job_producer: Arc::new(mock_job_producer),
2944            price_calculator: mock_price_calculator,
2945            signer: mock_signer,
2946        };
2947
2948        let result = evm_transaction.submit_transaction(test_tx).await;
2949        assert!(result.is_err());
2950    }
2951
2952    /// Test resubmit_transaction when transaction is already submitted
2953    /// Should NOT update hash, only status
2954    #[tokio::test]
2955    async fn test_resubmit_transaction_already_submitted_preserves_hash() {
2956        let mut mock_transaction = MockTransactionRepository::new();
2957        let mock_relayer = MockRelayerRepository::new();
2958        let mut mock_provider = MockEvmProviderTrait::new();
2959        let mut mock_signer = MockSigner::new();
2960        let mock_job_producer = MockJobProducerTrait::new();
2961        let mut mock_price_calculator = MockPriceCalculator::new();
2962        let counter_service = MockTransactionCounterTrait::new();
2963        let mock_network = MockNetworkRepository::new();
2964
2965        let relayer = create_test_relayer();
2966        let mut test_tx = create_test_transaction();
2967        test_tx.status = TransactionStatus::Submitted;
2968        test_tx.sent_at = Some(Utc::now().to_rfc3339());
2969        let original_hash = "0xoriginal_hash".to_string();
2970        test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
2971            nonce: Some(42),
2972            hash: Some(original_hash.clone()),
2973            raw: Some(vec![1, 2, 3]),
2974            ..test_tx.network_data.get_evm_transaction_data().unwrap()
2975        });
2976        test_tx.hashes = vec![original_hash.clone()];
2977
2978        // Price calculator returns bumped price
2979        mock_price_calculator
2980            .expect_calculate_bumped_gas_price()
2981            .times(1)
2982            .returning(|_, _, _| {
2983                Ok(PriceParams {
2984                    gas_price: Some(25000000000), // 25% bump
2985                    max_fee_per_gas: None,
2986                    max_priority_fee_per_gas: None,
2987                    is_min_bumped: Some(true),
2988                    extra_fee: None,
2989                    total_cost: U256::from(525000000000000u64),
2990                })
2991            });
2992
2993        // Balance check passes
2994        mock_provider
2995            .expect_get_balance()
2996            .times(1)
2997            .returning(|_| Box::pin(async { Ok(U256::from(1000000000000000000u64)) }));
2998
2999        // Signer creates new transaction with new hash
3000        mock_signer
3001            .expect_sign_transaction()
3002            .times(1)
3003            .returning(|_| {
3004                Box::pin(ready(Ok(
3005                    crate::domain::relayer::SignTransactionResponse::Evm(
3006                        crate::domain::relayer::SignTransactionResponseEvm {
3007                            hash: "0xnew_hash_that_should_not_be_saved".to_string(),
3008                            signature: crate::models::EvmTransactionDataSignature {
3009                                r: "r".to_string(),
3010                                s: "s".to_string(),
3011                                v: 1,
3012                                sig: "0xsignature".to_string(),
3013                            },
3014                            raw: vec![4, 5, 6],
3015                        },
3016                    ),
3017                )))
3018            });
3019
3020        // Provider returns "already known" - transaction is already in mempool
3021        mock_provider
3022            .expect_send_raw_transaction()
3023            .times(1)
3024            .returning(|_| {
3025                Box::pin(async {
3026                    Err(crate::services::provider::ProviderError::Other(
3027                        "already known: transaction with same nonce already in mempool".to_string(),
3028                    ))
3029                })
3030            });
3031
3032        // Verify that partial_update is called with NO network_data (preserving original hash)
3033        let test_tx_clone = test_tx.clone();
3034        mock_transaction
3035            .expect_partial_update()
3036            .times(1)
3037            .withf(|_, update| {
3038                // Should only update status, NOT network_data or hashes
3039                update.status == Some(TransactionStatus::Submitted)
3040                    && update.network_data.is_none()
3041                    && update.hashes.is_none()
3042            })
3043            .returning(move |_, _| {
3044                let mut updated_tx = test_tx_clone.clone();
3045                updated_tx.status = TransactionStatus::Submitted;
3046                // Hash should remain unchanged!
3047                Ok(updated_tx)
3048            });
3049
3050        let evm_transaction = EvmRelayerTransaction {
3051            relayer: relayer.clone(),
3052            provider: mock_provider,
3053            relayer_repository: Arc::new(mock_relayer),
3054            network_repository: Arc::new(mock_network),
3055            transaction_repository: Arc::new(mock_transaction),
3056            transaction_counter_service: Arc::new(counter_service),
3057            job_producer: Arc::new(mock_job_producer),
3058            price_calculator: mock_price_calculator,
3059            signer: mock_signer,
3060        };
3061
3062        let result = evm_transaction.resubmit_transaction(test_tx.clone()).await;
3063        assert!(result.is_ok());
3064        let updated_tx = result.unwrap();
3065
3066        // Verify hash was NOT changed
3067        if let NetworkTransactionData::Evm(evm_data) = &updated_tx.network_data {
3068            assert_eq!(evm_data.hash, Some(original_hash));
3069        } else {
3070            panic!("Expected EVM network data");
3071        }
3072    }
3073
3074    /// Test submit_transaction with database update failure
3075    /// Transaction is on-chain, but DB update fails - should return Ok with original tx
3076    #[tokio::test]
3077    async fn test_submit_transaction_db_failure_after_blockchain_success() {
3078        let mut mock_transaction = MockTransactionRepository::new();
3079        let mock_relayer = MockRelayerRepository::new();
3080        let mut mock_provider = MockEvmProviderTrait::new();
3081        let mock_signer = MockSigner::new();
3082        let mut mock_job_producer = MockJobProducerTrait::new();
3083        let mock_price_calculator = MockPriceCalculator::new();
3084        let counter_service = MockTransactionCounterTrait::new();
3085        let mock_network = MockNetworkRepository::new();
3086
3087        let relayer = create_test_relayer();
3088        let mut test_tx = create_test_transaction();
3089        test_tx.status = TransactionStatus::Sent;
3090        test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3091            raw: Some(vec![1, 2, 3]),
3092            ..test_tx.network_data.get_evm_transaction_data().unwrap()
3093        });
3094
3095        // Provider succeeds
3096        mock_provider
3097            .expect_send_raw_transaction()
3098            .times(1)
3099            .returning(|_| Box::pin(async { Ok("0xsubmitted_hash".to_string()) }));
3100
3101        // But database update fails
3102        mock_transaction
3103            .expect_partial_update()
3104            .times(1)
3105            .returning(|_, _| {
3106                Err(crate::models::RepositoryError::UnexpectedError(
3107                    "Redis timeout".to_string(),
3108                ))
3109            });
3110
3111        // Notification will still be sent (with original tx data)
3112        mock_job_producer
3113            .expect_produce_send_notification_job()
3114            .times(1)
3115            .returning(|_, _| Box::pin(ready(Ok(()))));
3116
3117        let evm_transaction = EvmRelayerTransaction {
3118            relayer: relayer.clone(),
3119            provider: mock_provider,
3120            relayer_repository: Arc::new(mock_relayer),
3121            network_repository: Arc::new(mock_network),
3122            transaction_repository: Arc::new(mock_transaction),
3123            transaction_counter_service: Arc::new(counter_service),
3124            job_producer: Arc::new(mock_job_producer),
3125            price_calculator: mock_price_calculator,
3126            signer: mock_signer,
3127        };
3128
3129        let result = evm_transaction.submit_transaction(test_tx.clone()).await;
3130        // Should return Ok (transaction is on-chain, don't retry)
3131        assert!(result.is_ok());
3132        let returned_tx = result.unwrap();
3133        // Should return original tx since DB update failed
3134        assert_eq!(returned_tx.id, test_tx.id);
3135        assert_eq!(returned_tx.status, TransactionStatus::Sent); // Original status
3136    }
3137
3138    /// Test send_transaction_resend_job success
3139    #[tokio::test]
3140    async fn test_send_transaction_resend_job_success() {
3141        let mock_transaction = MockTransactionRepository::new();
3142        let mock_relayer = MockRelayerRepository::new();
3143        let mock_provider = MockEvmProviderTrait::new();
3144        let mock_signer = MockSigner::new();
3145        let mut mock_job_producer = MockJobProducerTrait::new();
3146        let mock_price_calculator = MockPriceCalculator::new();
3147        let counter_service = MockTransactionCounterTrait::new();
3148        let mock_network = MockNetworkRepository::new();
3149
3150        let relayer = create_test_relayer();
3151        let test_tx = create_test_transaction();
3152
3153        // Expect produce_submit_transaction_job to be called with resend job
3154        mock_job_producer
3155            .expect_produce_submit_transaction_job()
3156            .times(1)
3157            .withf(|job, delay| {
3158                // Verify it's a resend job with correct IDs
3159                job.transaction_id == "test-tx-id"
3160                    && job.relayer_id == "test-relayer-id"
3161                    && matches!(job.command, crate::jobs::TransactionCommand::Resend)
3162                    && delay.is_none()
3163            })
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.send_transaction_resend_job(&test_tx).await;
3179        assert!(result.is_ok());
3180    }
3181
3182    /// Test send_transaction_resend_job failure
3183    #[tokio::test]
3184    async fn test_send_transaction_resend_job_failure() {
3185        let mock_transaction = MockTransactionRepository::new();
3186        let mock_relayer = MockRelayerRepository::new();
3187        let mock_provider = MockEvmProviderTrait::new();
3188        let mock_signer = MockSigner::new();
3189        let mut mock_job_producer = MockJobProducerTrait::new();
3190        let mock_price_calculator = MockPriceCalculator::new();
3191        let counter_service = MockTransactionCounterTrait::new();
3192        let mock_network = MockNetworkRepository::new();
3193
3194        let relayer = create_test_relayer();
3195        let test_tx = create_test_transaction();
3196
3197        // Job producer returns an error
3198        mock_job_producer
3199            .expect_produce_submit_transaction_job()
3200            .times(1)
3201            .returning(|_, _| {
3202                Box::pin(ready(Err(crate::jobs::JobProducerError::QueueError(
3203                    "Job queue is full".to_string(),
3204                ))))
3205            });
3206
3207        let evm_transaction = EvmRelayerTransaction {
3208            relayer: relayer.clone(),
3209            provider: mock_provider,
3210            relayer_repository: Arc::new(mock_relayer),
3211            network_repository: Arc::new(mock_network),
3212            transaction_repository: Arc::new(mock_transaction),
3213            transaction_counter_service: Arc::new(counter_service),
3214            job_producer: Arc::new(mock_job_producer),
3215            price_calculator: mock_price_calculator,
3216            signer: mock_signer,
3217        };
3218
3219        let result = evm_transaction.send_transaction_resend_job(&test_tx).await;
3220        assert!(result.is_err());
3221        let err = result.unwrap_err();
3222        match err {
3223            TransactionError::UnexpectedError(msg) => {
3224                assert!(msg.contains("Failed to produce resend job"));
3225            }
3226            _ => panic!("Expected UnexpectedError"),
3227        }
3228    }
3229
3230    /// Test send_transaction_request_job success
3231    #[tokio::test]
3232    async fn test_send_transaction_request_job_success() {
3233        let mock_transaction = MockTransactionRepository::new();
3234        let mock_relayer = MockRelayerRepository::new();
3235        let mock_provider = MockEvmProviderTrait::new();
3236        let mock_signer = MockSigner::new();
3237        let mut mock_job_producer = MockJobProducerTrait::new();
3238        let mock_price_calculator = MockPriceCalculator::new();
3239        let counter_service = MockTransactionCounterTrait::new();
3240        let mock_network = MockNetworkRepository::new();
3241
3242        let relayer = create_test_relayer();
3243        let test_tx = create_test_transaction();
3244
3245        // Expect produce_transaction_request_job to be called
3246        mock_job_producer
3247            .expect_produce_transaction_request_job()
3248            .times(1)
3249            .withf(|job, delay| {
3250                // Verify correct transaction ID and relayer ID
3251                job.transaction_id == "test-tx-id"
3252                    && job.relayer_id == "test-relayer-id"
3253                    && delay.is_none()
3254            })
3255            .returning(|_, _| Box::pin(ready(Ok(()))));
3256
3257        let evm_transaction = EvmRelayerTransaction {
3258            relayer: relayer.clone(),
3259            provider: mock_provider,
3260            relayer_repository: Arc::new(mock_relayer),
3261            network_repository: Arc::new(mock_network),
3262            transaction_repository: Arc::new(mock_transaction),
3263            transaction_counter_service: Arc::new(counter_service),
3264            job_producer: Arc::new(mock_job_producer),
3265            price_calculator: mock_price_calculator,
3266            signer: mock_signer,
3267        };
3268
3269        let result = evm_transaction.send_transaction_request_job(&test_tx).await;
3270        assert!(result.is_ok());
3271    }
3272
3273    /// Test send_transaction_request_job failure
3274    #[tokio::test]
3275    async fn test_send_transaction_request_job_failure() {
3276        let mock_transaction = MockTransactionRepository::new();
3277        let mock_relayer = MockRelayerRepository::new();
3278        let mock_provider = MockEvmProviderTrait::new();
3279        let mock_signer = MockSigner::new();
3280        let mut mock_job_producer = MockJobProducerTrait::new();
3281        let mock_price_calculator = MockPriceCalculator::new();
3282        let counter_service = MockTransactionCounterTrait::new();
3283        let mock_network = MockNetworkRepository::new();
3284
3285        let relayer = create_test_relayer();
3286        let test_tx = create_test_transaction();
3287
3288        // Job producer returns an error
3289        mock_job_producer
3290            .expect_produce_transaction_request_job()
3291            .times(1)
3292            .returning(|_, _| {
3293                Box::pin(ready(Err(crate::jobs::JobProducerError::QueueError(
3294                    "Redis connection failed".to_string(),
3295                ))))
3296            });
3297
3298        let evm_transaction = EvmRelayerTransaction {
3299            relayer: relayer.clone(),
3300            provider: mock_provider,
3301            relayer_repository: Arc::new(mock_relayer),
3302            network_repository: Arc::new(mock_network),
3303            transaction_repository: Arc::new(mock_transaction),
3304            transaction_counter_service: Arc::new(counter_service),
3305            job_producer: Arc::new(mock_job_producer),
3306            price_calculator: mock_price_calculator,
3307            signer: mock_signer,
3308        };
3309
3310        let result = evm_transaction.send_transaction_request_job(&test_tx).await;
3311        assert!(result.is_err());
3312        let err = result.unwrap_err();
3313        match err {
3314            TransactionError::UnexpectedError(msg) => {
3315                assert!(msg.contains("Failed to produce request job"));
3316            }
3317            _ => panic!("Expected UnexpectedError"),
3318        }
3319    }
3320
3321    /// Test resubmit_transaction successfully transitions from Sent to Submitted status
3322    #[tokio::test]
3323    async fn test_resubmit_transaction_sent_to_submitted() {
3324        let mut mock_transaction = MockTransactionRepository::new();
3325        let mock_relayer = MockRelayerRepository::new();
3326        let mut mock_provider = MockEvmProviderTrait::new();
3327        let mut mock_signer = MockSigner::new();
3328        let mock_job_producer = MockJobProducerTrait::new();
3329        let mut mock_price_calculator = MockPriceCalculator::new();
3330        let counter_service = MockTransactionCounterTrait::new();
3331        let mock_network = MockNetworkRepository::new();
3332
3333        let relayer = create_test_relayer();
3334        let mut test_tx = create_test_transaction();
3335        test_tx.status = TransactionStatus::Sent;
3336        test_tx.sent_at = Some(Utc::now().to_rfc3339());
3337        let original_hash = "0xoriginal_hash".to_string();
3338        test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3339            nonce: Some(42),
3340            hash: Some(original_hash.clone()),
3341            raw: Some(vec![1, 2, 3]),
3342            gas_price: Some(20000000000), // 20 Gwei
3343            ..test_tx.network_data.get_evm_transaction_data().unwrap()
3344        });
3345        test_tx.hashes = vec![original_hash.clone()];
3346
3347        // Price calculator returns bumped price
3348        mock_price_calculator
3349            .expect_calculate_bumped_gas_price()
3350            .times(1)
3351            .returning(|_, _, _| {
3352                Ok(PriceParams {
3353                    gas_price: Some(25000000000), // 25 Gwei (25% bump)
3354                    max_fee_per_gas: None,
3355                    max_priority_fee_per_gas: None,
3356                    is_min_bumped: Some(true),
3357                    extra_fee: None,
3358                    total_cost: U256::from(525000000000000u64),
3359                })
3360            });
3361
3362        // Mock balance check
3363        mock_provider
3364            .expect_get_balance()
3365            .returning(|_| Box::pin(ready(Ok(U256::from(1000000000000000000u64)))));
3366
3367        // Mock signer to return new signed transaction
3368        mock_signer.expect_sign_transaction().returning(|_| {
3369            Box::pin(ready(Ok(
3370                crate::domain::relayer::SignTransactionResponse::Evm(
3371                    crate::domain::relayer::SignTransactionResponseEvm {
3372                        hash: "0xnew_hash".to_string(),
3373                        signature: crate::models::EvmTransactionDataSignature {
3374                            r: "r".to_string(),
3375                            s: "s".to_string(),
3376                            v: 1,
3377                            sig: "0xsignature".to_string(),
3378                        },
3379                        raw: vec![4, 5, 6],
3380                    },
3381                ),
3382            )))
3383        });
3384
3385        // Provider successfully sends the resubmitted transaction
3386        mock_provider
3387            .expect_send_raw_transaction()
3388            .times(1)
3389            .returning(|_| Box::pin(async { Ok("0xnew_hash".to_string()) }));
3390
3391        // Should update to Submitted status with new hash
3392        let test_tx_clone = test_tx.clone();
3393        mock_transaction
3394            .expect_partial_update()
3395            .times(1)
3396            .withf(|_, update| {
3397                update.status == Some(TransactionStatus::Submitted)
3398                    && update.sent_at.is_some()
3399                    && update.priced_at.is_some()
3400                    && update.hashes.is_some()
3401            })
3402            .returning(move |_, update| {
3403                let mut updated_tx = test_tx_clone.clone();
3404                updated_tx.status = update.status.unwrap();
3405                updated_tx.sent_at = update.sent_at.clone();
3406                updated_tx.priced_at = update.priced_at.clone();
3407                if let Some(hashes) = update.hashes.clone() {
3408                    updated_tx.hashes = hashes;
3409                }
3410                if let Some(network_data) = update.network_data.clone() {
3411                    updated_tx.network_data = network_data;
3412                }
3413                Ok(updated_tx)
3414            });
3415
3416        let evm_transaction = EvmRelayerTransaction {
3417            relayer: relayer.clone(),
3418            provider: mock_provider,
3419            relayer_repository: Arc::new(mock_relayer),
3420            network_repository: Arc::new(mock_network),
3421            transaction_repository: Arc::new(mock_transaction),
3422            transaction_counter_service: Arc::new(counter_service),
3423            job_producer: Arc::new(mock_job_producer),
3424            price_calculator: mock_price_calculator,
3425            signer: mock_signer,
3426        };
3427
3428        let result = evm_transaction.resubmit_transaction(test_tx.clone()).await;
3429        assert!(result.is_ok(), "Expected Ok, got: {result:?}");
3430        let updated_tx = result.unwrap();
3431        assert_eq!(
3432            updated_tx.status,
3433            TransactionStatus::Submitted,
3434            "Transaction status should transition from Sent to Submitted"
3435        );
3436    }
3437}