openzeppelin_relayer/domain/transaction/evm/
evm_transaction.rs

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