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