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