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    #[test]
2619    fn test_is_already_submitted_error_detection() {
2620        // Test "already known" variants
2621        assert!(DefaultEvmTransaction::is_already_submitted_error(
2622            &"already known"
2623        ));
2624        assert!(DefaultEvmTransaction::is_already_submitted_error(
2625            &"Transaction already known"
2626        ));
2627        assert!(DefaultEvmTransaction::is_already_submitted_error(
2628            &"Error: already known"
2629        ));
2630
2631        // Test "nonce too low" variants
2632        assert!(DefaultEvmTransaction::is_already_submitted_error(
2633            &"nonce too low"
2634        ));
2635        assert!(DefaultEvmTransaction::is_already_submitted_error(
2636            &"Nonce Too Low"
2637        ));
2638        assert!(DefaultEvmTransaction::is_already_submitted_error(
2639            &"Error: nonce too low"
2640        ));
2641
2642        // Test "replacement transaction underpriced" variants
2643        assert!(DefaultEvmTransaction::is_already_submitted_error(
2644            &"replacement transaction underpriced"
2645        ));
2646        assert!(DefaultEvmTransaction::is_already_submitted_error(
2647            &"Replacement Transaction Underpriced"
2648        ));
2649
2650        // Test non-matching errors
2651        assert!(!DefaultEvmTransaction::is_already_submitted_error(
2652            &"insufficient funds"
2653        ));
2654        assert!(!DefaultEvmTransaction::is_already_submitted_error(
2655            &"execution reverted"
2656        ));
2657        assert!(!DefaultEvmTransaction::is_already_submitted_error(
2658            &"gas too low"
2659        ));
2660        assert!(!DefaultEvmTransaction::is_already_submitted_error(
2661            &"timeout"
2662        ));
2663    }
2664
2665    /// Test submit_transaction with "already known" error in Sent status
2666    /// This should treat the error as success and update to Submitted
2667    #[tokio::test]
2668    async fn test_submit_transaction_already_known_error_from_sent() {
2669        let mut mock_transaction = MockTransactionRepository::new();
2670        let mock_relayer = MockRelayerRepository::new();
2671        let mut mock_provider = MockEvmProviderTrait::new();
2672        let mock_signer = MockSigner::new();
2673        let mut mock_job_producer = MockJobProducerTrait::new();
2674        let mock_price_calculator = MockPriceCalculator::new();
2675        let counter_service = MockTransactionCounterTrait::new();
2676        let mock_network = MockNetworkRepository::new();
2677
2678        let relayer = create_test_relayer();
2679        let mut test_tx = create_test_transaction();
2680        test_tx.status = TransactionStatus::Sent;
2681        test_tx.sent_at = Some(Utc::now().to_rfc3339());
2682        test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
2683            nonce: Some(42),
2684            hash: Some("0xhash".to_string()),
2685            raw: Some(vec![1, 2, 3]),
2686            ..test_tx.network_data.get_evm_transaction_data().unwrap()
2687        });
2688
2689        // Provider returns "already known" error
2690        mock_provider
2691            .expect_send_raw_transaction()
2692            .times(1)
2693            .returning(|_| {
2694                Box::pin(async {
2695                    Err(crate::services::provider::ProviderError::Other(
2696                        "already known: transaction already in mempool".to_string(),
2697                    ))
2698                })
2699            });
2700
2701        // Should still update to Submitted status
2702        let test_tx_clone = test_tx.clone();
2703        mock_transaction
2704            .expect_partial_update()
2705            .times(1)
2706            .withf(|_, update| update.status == Some(TransactionStatus::Submitted))
2707            .returning(move |_, update| {
2708                let mut updated_tx = test_tx_clone.clone();
2709                updated_tx.status = update.status.unwrap();
2710                updated_tx.sent_at = update.sent_at.clone();
2711                Ok(updated_tx)
2712            });
2713
2714        mock_job_producer
2715            .expect_produce_send_notification_job()
2716            .times(1)
2717            .returning(|_, _| Box::pin(ready(Ok(()))));
2718
2719        let evm_transaction = EvmRelayerTransaction {
2720            relayer: relayer.clone(),
2721            provider: mock_provider,
2722            relayer_repository: Arc::new(mock_relayer),
2723            network_repository: Arc::new(mock_network),
2724            transaction_repository: Arc::new(mock_transaction),
2725            transaction_counter_service: Arc::new(counter_service),
2726            job_producer: Arc::new(mock_job_producer),
2727            price_calculator: mock_price_calculator,
2728            signer: mock_signer,
2729        };
2730
2731        let result = evm_transaction.submit_transaction(test_tx).await;
2732        assert!(result.is_ok());
2733        let updated_tx = result.unwrap();
2734        assert_eq!(updated_tx.status, TransactionStatus::Submitted);
2735    }
2736
2737    /// Test submit_transaction with real error (not "already known") should fail
2738    #[tokio::test]
2739    async fn test_submit_transaction_real_error_fails() {
2740        let mock_transaction = MockTransactionRepository::new();
2741        let mock_relayer = MockRelayerRepository::new();
2742        let mut mock_provider = MockEvmProviderTrait::new();
2743        let mock_signer = MockSigner::new();
2744        let mock_job_producer = MockJobProducerTrait::new();
2745        let mock_price_calculator = MockPriceCalculator::new();
2746        let counter_service = MockTransactionCounterTrait::new();
2747        let mock_network = MockNetworkRepository::new();
2748
2749        let relayer = create_test_relayer();
2750        let mut test_tx = create_test_transaction();
2751        test_tx.status = TransactionStatus::Sent;
2752        test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
2753            raw: Some(vec![1, 2, 3]),
2754            ..test_tx.network_data.get_evm_transaction_data().unwrap()
2755        });
2756
2757        // Provider returns a real error
2758        mock_provider
2759            .expect_send_raw_transaction()
2760            .times(1)
2761            .returning(|_| {
2762                Box::pin(async {
2763                    Err(crate::services::provider::ProviderError::Other(
2764                        "insufficient funds for gas * price + value".to_string(),
2765                    ))
2766                })
2767            });
2768
2769        let evm_transaction = EvmRelayerTransaction {
2770            relayer: relayer.clone(),
2771            provider: mock_provider,
2772            relayer_repository: Arc::new(mock_relayer),
2773            network_repository: Arc::new(mock_network),
2774            transaction_repository: Arc::new(mock_transaction),
2775            transaction_counter_service: Arc::new(counter_service),
2776            job_producer: Arc::new(mock_job_producer),
2777            price_calculator: mock_price_calculator,
2778            signer: mock_signer,
2779        };
2780
2781        let result = evm_transaction.submit_transaction(test_tx).await;
2782        assert!(result.is_err());
2783    }
2784
2785    /// Test resubmit_transaction when transaction is already submitted
2786    /// Should NOT update hash, only status
2787    #[tokio::test]
2788    async fn test_resubmit_transaction_already_submitted_preserves_hash() {
2789        let mut mock_transaction = MockTransactionRepository::new();
2790        let mock_relayer = MockRelayerRepository::new();
2791        let mut mock_provider = MockEvmProviderTrait::new();
2792        let mut mock_signer = MockSigner::new();
2793        let mock_job_producer = MockJobProducerTrait::new();
2794        let mut mock_price_calculator = MockPriceCalculator::new();
2795        let counter_service = MockTransactionCounterTrait::new();
2796        let mock_network = MockNetworkRepository::new();
2797
2798        let relayer = create_test_relayer();
2799        let mut test_tx = create_test_transaction();
2800        test_tx.status = TransactionStatus::Submitted;
2801        test_tx.sent_at = Some(Utc::now().to_rfc3339());
2802        let original_hash = "0xoriginal_hash".to_string();
2803        test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
2804            nonce: Some(42),
2805            hash: Some(original_hash.clone()),
2806            raw: Some(vec![1, 2, 3]),
2807            ..test_tx.network_data.get_evm_transaction_data().unwrap()
2808        });
2809        test_tx.hashes = vec![original_hash.clone()];
2810
2811        // Price calculator returns bumped price
2812        mock_price_calculator
2813            .expect_calculate_bumped_gas_price()
2814            .times(1)
2815            .returning(|_, _, _| {
2816                Ok(PriceParams {
2817                    gas_price: Some(25000000000), // 25% bump
2818                    max_fee_per_gas: None,
2819                    max_priority_fee_per_gas: None,
2820                    is_min_bumped: Some(true),
2821                    extra_fee: None,
2822                    total_cost: U256::from(525000000000000u64),
2823                })
2824            });
2825
2826        // Balance check passes
2827        mock_provider
2828            .expect_get_balance()
2829            .times(1)
2830            .returning(|_| Box::pin(async { Ok(U256::from(1000000000000000000u64)) }));
2831
2832        // Signer creates new transaction with new hash
2833        mock_signer
2834            .expect_sign_transaction()
2835            .times(1)
2836            .returning(|_| {
2837                Box::pin(ready(Ok(
2838                    crate::domain::relayer::SignTransactionResponse::Evm(
2839                        crate::domain::relayer::SignTransactionResponseEvm {
2840                            hash: "0xnew_hash_that_should_not_be_saved".to_string(),
2841                            signature: crate::models::EvmTransactionDataSignature {
2842                                r: "r".to_string(),
2843                                s: "s".to_string(),
2844                                v: 1,
2845                                sig: "0xsignature".to_string(),
2846                            },
2847                            raw: vec![4, 5, 6],
2848                        },
2849                    ),
2850                )))
2851            });
2852
2853        // Provider returns "already known" - transaction is already in mempool
2854        mock_provider
2855            .expect_send_raw_transaction()
2856            .times(1)
2857            .returning(|_| {
2858                Box::pin(async {
2859                    Err(crate::services::provider::ProviderError::Other(
2860                        "already known: transaction with same nonce already in mempool".to_string(),
2861                    ))
2862                })
2863            });
2864
2865        // Verify that partial_update is called with NO network_data (preserving original hash)
2866        let test_tx_clone = test_tx.clone();
2867        mock_transaction
2868            .expect_partial_update()
2869            .times(1)
2870            .withf(|_, update| {
2871                // Should only update status, NOT network_data or hashes
2872                update.status == Some(TransactionStatus::Submitted)
2873                    && update.network_data.is_none()
2874                    && update.hashes.is_none()
2875            })
2876            .returning(move |_, _| {
2877                let mut updated_tx = test_tx_clone.clone();
2878                updated_tx.status = TransactionStatus::Submitted;
2879                // Hash should remain unchanged!
2880                Ok(updated_tx)
2881            });
2882
2883        let evm_transaction = EvmRelayerTransaction {
2884            relayer: relayer.clone(),
2885            provider: mock_provider,
2886            relayer_repository: Arc::new(mock_relayer),
2887            network_repository: Arc::new(mock_network),
2888            transaction_repository: Arc::new(mock_transaction),
2889            transaction_counter_service: Arc::new(counter_service),
2890            job_producer: Arc::new(mock_job_producer),
2891            price_calculator: mock_price_calculator,
2892            signer: mock_signer,
2893        };
2894
2895        let result = evm_transaction.resubmit_transaction(test_tx.clone()).await;
2896        assert!(result.is_ok());
2897        let updated_tx = result.unwrap();
2898
2899        // Verify hash was NOT changed
2900        if let NetworkTransactionData::Evm(evm_data) = &updated_tx.network_data {
2901            assert_eq!(evm_data.hash, Some(original_hash));
2902        } else {
2903            panic!("Expected EVM network data");
2904        }
2905    }
2906
2907    /// Test submit_transaction with database update failure
2908    /// Transaction is on-chain, but DB update fails - should return Ok with original tx
2909    #[tokio::test]
2910    async fn test_submit_transaction_db_failure_after_blockchain_success() {
2911        let mut mock_transaction = MockTransactionRepository::new();
2912        let mock_relayer = MockRelayerRepository::new();
2913        let mut mock_provider = MockEvmProviderTrait::new();
2914        let mock_signer = MockSigner::new();
2915        let mut mock_job_producer = MockJobProducerTrait::new();
2916        let mock_price_calculator = MockPriceCalculator::new();
2917        let counter_service = MockTransactionCounterTrait::new();
2918        let mock_network = MockNetworkRepository::new();
2919
2920        let relayer = create_test_relayer();
2921        let mut test_tx = create_test_transaction();
2922        test_tx.status = TransactionStatus::Sent;
2923        test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
2924            raw: Some(vec![1, 2, 3]),
2925            ..test_tx.network_data.get_evm_transaction_data().unwrap()
2926        });
2927
2928        // Provider succeeds
2929        mock_provider
2930            .expect_send_raw_transaction()
2931            .times(1)
2932            .returning(|_| Box::pin(async { Ok("0xsubmitted_hash".to_string()) }));
2933
2934        // But database update fails
2935        mock_transaction
2936            .expect_partial_update()
2937            .times(1)
2938            .returning(|_, _| {
2939                Err(crate::models::RepositoryError::UnexpectedError(
2940                    "Redis timeout".to_string(),
2941                ))
2942            });
2943
2944        // Notification will still be sent (with original tx data)
2945        mock_job_producer
2946            .expect_produce_send_notification_job()
2947            .times(1)
2948            .returning(|_, _| Box::pin(ready(Ok(()))));
2949
2950        let evm_transaction = EvmRelayerTransaction {
2951            relayer: relayer.clone(),
2952            provider: mock_provider,
2953            relayer_repository: Arc::new(mock_relayer),
2954            network_repository: Arc::new(mock_network),
2955            transaction_repository: Arc::new(mock_transaction),
2956            transaction_counter_service: Arc::new(counter_service),
2957            job_producer: Arc::new(mock_job_producer),
2958            price_calculator: mock_price_calculator,
2959            signer: mock_signer,
2960        };
2961
2962        let result = evm_transaction.submit_transaction(test_tx.clone()).await;
2963        // Should return Ok (transaction is on-chain, don't retry)
2964        assert!(result.is_ok());
2965        let returned_tx = result.unwrap();
2966        // Should return original tx since DB update failed
2967        assert_eq!(returned_tx.id, test_tx.id);
2968        assert_eq!(returned_tx.status, TransactionStatus::Sent); // Original status
2969    }
2970
2971    /// Test send_transaction_resend_job success
2972    #[tokio::test]
2973    async fn test_send_transaction_resend_job_success() {
2974        let mock_transaction = MockTransactionRepository::new();
2975        let mock_relayer = MockRelayerRepository::new();
2976        let mock_provider = MockEvmProviderTrait::new();
2977        let mock_signer = MockSigner::new();
2978        let mut mock_job_producer = MockJobProducerTrait::new();
2979        let mock_price_calculator = MockPriceCalculator::new();
2980        let counter_service = MockTransactionCounterTrait::new();
2981        let mock_network = MockNetworkRepository::new();
2982
2983        let relayer = create_test_relayer();
2984        let test_tx = create_test_transaction();
2985
2986        // Expect produce_submit_transaction_job to be called with resend job
2987        mock_job_producer
2988            .expect_produce_submit_transaction_job()
2989            .times(1)
2990            .withf(|job, delay| {
2991                // Verify it's a resend job with correct IDs
2992                job.transaction_id == "test-tx-id"
2993                    && job.relayer_id == "test-relayer-id"
2994                    && matches!(job.command, crate::jobs::TransactionCommand::Resend)
2995                    && delay.is_none()
2996            })
2997            .returning(|_, _| Box::pin(ready(Ok(()))));
2998
2999        let evm_transaction = EvmRelayerTransaction {
3000            relayer: relayer.clone(),
3001            provider: mock_provider,
3002            relayer_repository: Arc::new(mock_relayer),
3003            network_repository: Arc::new(mock_network),
3004            transaction_repository: Arc::new(mock_transaction),
3005            transaction_counter_service: Arc::new(counter_service),
3006            job_producer: Arc::new(mock_job_producer),
3007            price_calculator: mock_price_calculator,
3008            signer: mock_signer,
3009        };
3010
3011        let result = evm_transaction.send_transaction_resend_job(&test_tx).await;
3012        assert!(result.is_ok());
3013    }
3014
3015    /// Test send_transaction_resend_job failure
3016    #[tokio::test]
3017    async fn test_send_transaction_resend_job_failure() {
3018        let mock_transaction = MockTransactionRepository::new();
3019        let mock_relayer = MockRelayerRepository::new();
3020        let mock_provider = MockEvmProviderTrait::new();
3021        let mock_signer = MockSigner::new();
3022        let mut mock_job_producer = MockJobProducerTrait::new();
3023        let mock_price_calculator = MockPriceCalculator::new();
3024        let counter_service = MockTransactionCounterTrait::new();
3025        let mock_network = MockNetworkRepository::new();
3026
3027        let relayer = create_test_relayer();
3028        let test_tx = create_test_transaction();
3029
3030        // Job producer returns an error
3031        mock_job_producer
3032            .expect_produce_submit_transaction_job()
3033            .times(1)
3034            .returning(|_, _| {
3035                Box::pin(ready(Err(crate::jobs::JobProducerError::QueueError(
3036                    "Job queue is full".to_string(),
3037                ))))
3038            });
3039
3040        let evm_transaction = EvmRelayerTransaction {
3041            relayer: relayer.clone(),
3042            provider: mock_provider,
3043            relayer_repository: Arc::new(mock_relayer),
3044            network_repository: Arc::new(mock_network),
3045            transaction_repository: Arc::new(mock_transaction),
3046            transaction_counter_service: Arc::new(counter_service),
3047            job_producer: Arc::new(mock_job_producer),
3048            price_calculator: mock_price_calculator,
3049            signer: mock_signer,
3050        };
3051
3052        let result = evm_transaction.send_transaction_resend_job(&test_tx).await;
3053        assert!(result.is_err());
3054        let err = result.unwrap_err();
3055        match err {
3056            TransactionError::UnexpectedError(msg) => {
3057                assert!(msg.contains("Failed to produce resend job"));
3058            }
3059            _ => panic!("Expected UnexpectedError"),
3060        }
3061    }
3062
3063    /// Test send_transaction_request_job success
3064    #[tokio::test]
3065    async fn test_send_transaction_request_job_success() {
3066        let mock_transaction = MockTransactionRepository::new();
3067        let mock_relayer = MockRelayerRepository::new();
3068        let mock_provider = MockEvmProviderTrait::new();
3069        let mock_signer = MockSigner::new();
3070        let mut mock_job_producer = MockJobProducerTrait::new();
3071        let mock_price_calculator = MockPriceCalculator::new();
3072        let counter_service = MockTransactionCounterTrait::new();
3073        let mock_network = MockNetworkRepository::new();
3074
3075        let relayer = create_test_relayer();
3076        let test_tx = create_test_transaction();
3077
3078        // Expect produce_transaction_request_job to be called
3079        mock_job_producer
3080            .expect_produce_transaction_request_job()
3081            .times(1)
3082            .withf(|job, delay| {
3083                // Verify correct transaction ID and relayer ID
3084                job.transaction_id == "test-tx-id"
3085                    && job.relayer_id == "test-relayer-id"
3086                    && delay.is_none()
3087            })
3088            .returning(|_, _| Box::pin(ready(Ok(()))));
3089
3090        let evm_transaction = EvmRelayerTransaction {
3091            relayer: relayer.clone(),
3092            provider: mock_provider,
3093            relayer_repository: Arc::new(mock_relayer),
3094            network_repository: Arc::new(mock_network),
3095            transaction_repository: Arc::new(mock_transaction),
3096            transaction_counter_service: Arc::new(counter_service),
3097            job_producer: Arc::new(mock_job_producer),
3098            price_calculator: mock_price_calculator,
3099            signer: mock_signer,
3100        };
3101
3102        let result = evm_transaction.send_transaction_request_job(&test_tx).await;
3103        assert!(result.is_ok());
3104    }
3105
3106    /// Test send_transaction_request_job failure
3107    #[tokio::test]
3108    async fn test_send_transaction_request_job_failure() {
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        // Job producer returns an error
3122        mock_job_producer
3123            .expect_produce_transaction_request_job()
3124            .times(1)
3125            .returning(|_, _| {
3126                Box::pin(ready(Err(crate::jobs::JobProducerError::QueueError(
3127                    "Redis connection failed".to_string(),
3128                ))))
3129            });
3130
3131        let evm_transaction = EvmRelayerTransaction {
3132            relayer: relayer.clone(),
3133            provider: mock_provider,
3134            relayer_repository: Arc::new(mock_relayer),
3135            network_repository: Arc::new(mock_network),
3136            transaction_repository: Arc::new(mock_transaction),
3137            transaction_counter_service: Arc::new(counter_service),
3138            job_producer: Arc::new(mock_job_producer),
3139            price_calculator: mock_price_calculator,
3140            signer: mock_signer,
3141        };
3142
3143        let result = evm_transaction.send_transaction_request_job(&test_tx).await;
3144        assert!(result.is_err());
3145        let err = result.unwrap_err();
3146        match err {
3147            TransactionError::UnexpectedError(msg) => {
3148                assert!(msg.contains("Failed to produce request job"));
3149            }
3150            _ => panic!("Expected UnexpectedError"),
3151        }
3152    }
3153
3154    /// Test resubmit_transaction successfully transitions from Sent to Submitted status
3155    #[tokio::test]
3156    async fn test_resubmit_transaction_sent_to_submitted() {
3157        let mut mock_transaction = MockTransactionRepository::new();
3158        let mock_relayer = MockRelayerRepository::new();
3159        let mut mock_provider = MockEvmProviderTrait::new();
3160        let mut mock_signer = MockSigner::new();
3161        let mock_job_producer = MockJobProducerTrait::new();
3162        let mut mock_price_calculator = MockPriceCalculator::new();
3163        let counter_service = MockTransactionCounterTrait::new();
3164        let mock_network = MockNetworkRepository::new();
3165
3166        let relayer = create_test_relayer();
3167        let mut test_tx = create_test_transaction();
3168        test_tx.status = TransactionStatus::Sent;
3169        test_tx.sent_at = Some(Utc::now().to_rfc3339());
3170        let original_hash = "0xoriginal_hash".to_string();
3171        test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3172            nonce: Some(42),
3173            hash: Some(original_hash.clone()),
3174            raw: Some(vec![1, 2, 3]),
3175            gas_price: Some(20000000000), // 20 Gwei
3176            ..test_tx.network_data.get_evm_transaction_data().unwrap()
3177        });
3178        test_tx.hashes = vec![original_hash.clone()];
3179
3180        // Price calculator returns bumped price
3181        mock_price_calculator
3182            .expect_calculate_bumped_gas_price()
3183            .times(1)
3184            .returning(|_, _, _| {
3185                Ok(PriceParams {
3186                    gas_price: Some(25000000000), // 25 Gwei (25% bump)
3187                    max_fee_per_gas: None,
3188                    max_priority_fee_per_gas: None,
3189                    is_min_bumped: Some(true),
3190                    extra_fee: None,
3191                    total_cost: U256::from(525000000000000u64),
3192                })
3193            });
3194
3195        // Mock balance check
3196        mock_provider
3197            .expect_get_balance()
3198            .returning(|_| Box::pin(ready(Ok(U256::from(1000000000000000000u64)))));
3199
3200        // Mock signer to return new signed transaction
3201        mock_signer.expect_sign_transaction().returning(|_| {
3202            Box::pin(ready(Ok(
3203                crate::domain::relayer::SignTransactionResponse::Evm(
3204                    crate::domain::relayer::SignTransactionResponseEvm {
3205                        hash: "0xnew_hash".to_string(),
3206                        signature: crate::models::EvmTransactionDataSignature {
3207                            r: "r".to_string(),
3208                            s: "s".to_string(),
3209                            v: 1,
3210                            sig: "0xsignature".to_string(),
3211                        },
3212                        raw: vec![4, 5, 6],
3213                    },
3214                ),
3215            )))
3216        });
3217
3218        // Provider successfully sends the resubmitted transaction
3219        mock_provider
3220            .expect_send_raw_transaction()
3221            .times(1)
3222            .returning(|_| Box::pin(async { Ok("0xnew_hash".to_string()) }));
3223
3224        // Should update to Submitted status with new hash
3225        let test_tx_clone = test_tx.clone();
3226        mock_transaction
3227            .expect_partial_update()
3228            .times(1)
3229            .withf(|_, update| {
3230                update.status == Some(TransactionStatus::Submitted)
3231                    && update.sent_at.is_some()
3232                    && update.priced_at.is_some()
3233                    && update.hashes.is_some()
3234            })
3235            .returning(move |_, update| {
3236                let mut updated_tx = test_tx_clone.clone();
3237                updated_tx.status = update.status.unwrap();
3238                updated_tx.sent_at = update.sent_at.clone();
3239                updated_tx.priced_at = update.priced_at.clone();
3240                if let Some(hashes) = update.hashes.clone() {
3241                    updated_tx.hashes = hashes;
3242                }
3243                if let Some(network_data) = update.network_data.clone() {
3244                    updated_tx.network_data = network_data;
3245                }
3246                Ok(updated_tx)
3247            });
3248
3249        let evm_transaction = EvmRelayerTransaction {
3250            relayer: relayer.clone(),
3251            provider: mock_provider,
3252            relayer_repository: Arc::new(mock_relayer),
3253            network_repository: Arc::new(mock_network),
3254            transaction_repository: Arc::new(mock_transaction),
3255            transaction_counter_service: Arc::new(counter_service),
3256            job_producer: Arc::new(mock_job_producer),
3257            price_calculator: mock_price_calculator,
3258            signer: mock_signer,
3259        };
3260
3261        let result = evm_transaction.resubmit_transaction(test_tx.clone()).await;
3262        assert!(result.is_ok(), "Expected Ok, got: {result:?}");
3263        let updated_tx = result.unwrap();
3264        assert_eq!(
3265            updated_tx.status,
3266            TransactionStatus::Submitted,
3267            "Transaction status should transition from Sent to Submitted"
3268        );
3269    }
3270}