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