openzeppelin_relayer/domain/transaction/evm/
evm_transaction.rs

1//! This module defines the `EvmRelayerTransaction` struct and its associated
2//! functionality for handling Ethereum Virtual Machine (EVM) transactions.
3//! It includes methods for preparing, submitting, handling status, and
4//! managing notifications for transactions. The module leverages various
5//! services and repositories to perform these operations asynchronously.
6
7use async_trait::async_trait;
8use chrono::Utc;
9use eyre::Result;
10use std::sync::Arc;
11use tracing::{debug, error, info, warn};
12
13use crate::{
14    constants::{DEFAULT_EVM_GAS_LIMIT_ESTIMATION, GAS_LIMIT_BUFFER_MULTIPLIER},
15    domain::{
16        transaction::{
17            evm::{is_pending_transaction, PriceCalculator, PriceCalculatorTrait},
18            Transaction,
19        },
20        EvmTransactionValidator,
21    },
22    jobs::{JobProducer, JobProducerTrait, TransactionSend, TransactionStatusCheck},
23    models::{
24        produce_transaction_update_notification_payload, EvmNetwork, EvmTransactionData,
25        NetworkRepoModel, NetworkTransactionData, NetworkTransactionRequest, NetworkType,
26        RelayerEvmPolicy, RelayerRepoModel, TransactionError, TransactionRepoModel,
27        TransactionStatus, TransactionUpdateRequest,
28    },
29    repositories::{
30        NetworkRepository, NetworkRepositoryStorage, RelayerRepository, RelayerRepositoryStorage,
31        Repository, TransactionCounterRepositoryStorage, TransactionCounterTrait,
32        TransactionRepository, TransactionRepositoryStorage,
33    },
34    services::{
35        gas::evm_gas_price::EvmGasPriceService, EvmProvider, EvmProviderTrait, EvmSigner, Signer,
36    },
37    utils::{calculate_scheduled_timestamp, get_evm_default_gas_limit_for_tx},
38};
39
40use super::PriceParams;
41
42#[allow(dead_code)]
43pub struct EvmRelayerTransaction<P, RR, NR, TR, J, S, TCR, PC>
44where
45    P: EvmProviderTrait,
46    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
47    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
48    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
49    J: JobProducerTrait + Send + Sync + 'static,
50    S: Signer + Send + Sync + 'static,
51    TCR: TransactionCounterTrait + Send + Sync + 'static,
52    PC: PriceCalculatorTrait,
53{
54    provider: P,
55    relayer_repository: Arc<RR>,
56    network_repository: Arc<NR>,
57    transaction_repository: Arc<TR>,
58    job_producer: Arc<J>,
59    signer: S,
60    relayer: RelayerRepoModel,
61    transaction_counter_service: Arc<TCR>,
62    price_calculator: PC,
63}
64
65#[allow(dead_code, clippy::too_many_arguments)]
66impl<P, RR, NR, TR, J, S, TCR, PC> EvmRelayerTransaction<P, RR, NR, TR, J, S, TCR, PC>
67where
68    P: EvmProviderTrait,
69    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
70    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
71    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
72    J: JobProducerTrait + Send + Sync + 'static,
73    S: Signer + Send + Sync + 'static,
74    TCR: TransactionCounterTrait + Send + Sync + 'static,
75    PC: PriceCalculatorTrait,
76{
77    /// Creates a new `EvmRelayerTransaction`.
78    ///
79    /// # Arguments
80    ///
81    /// * `relayer` - The relayer model.
82    /// * `provider` - The EVM provider.
83    /// * `relayer_repository` - Storage for relayer repository.
84    /// * `transaction_repository` - Storage for transaction repository.
85    /// * `transaction_counter_service` - Service for managing transaction counters.
86    /// * `job_producer` - Producer for job queue.
87    /// * `price_calculator` - Price calculator for gas price management.
88    /// * `signer` - The EVM signer.
89    ///
90    /// # Returns
91    ///
92    /// A result containing the new `EvmRelayerTransaction` or a `TransactionError`.
93    pub fn new(
94        relayer: RelayerRepoModel,
95        provider: P,
96        relayer_repository: Arc<RR>,
97        network_repository: Arc<NR>,
98        transaction_repository: Arc<TR>,
99        transaction_counter_service: Arc<TCR>,
100        job_producer: Arc<J>,
101        price_calculator: PC,
102        signer: S,
103    ) -> Result<Self, TransactionError> {
104        Ok(Self {
105            relayer,
106            provider,
107            relayer_repository,
108            network_repository,
109            transaction_repository,
110            transaction_counter_service,
111            job_producer,
112            price_calculator,
113            signer,
114        })
115    }
116
117    /// Returns a reference to the provider.
118    pub fn provider(&self) -> &P {
119        &self.provider
120    }
121
122    /// Returns a reference to the relayer model.
123    pub fn relayer(&self) -> &RelayerRepoModel {
124        &self.relayer
125    }
126
127    /// Returns a reference to the network repository.
128    pub fn network_repository(&self) -> &NR {
129        &self.network_repository
130    }
131
132    /// Returns a reference to the job producer.
133    pub fn job_producer(&self) -> &J {
134        &self.job_producer
135    }
136
137    pub fn transaction_repository(&self) -> &TR {
138        &self.transaction_repository
139    }
140
141    /// Helper method to schedule a transaction status check job.
142    pub(super) async fn schedule_status_check(
143        &self,
144        tx: &TransactionRepoModel,
145        delay_seconds: Option<i64>,
146    ) -> Result<(), TransactionError> {
147        let delay = delay_seconds.map(calculate_scheduled_timestamp);
148        self.job_producer()
149            .produce_check_transaction_status_job(
150                TransactionStatusCheck::new(tx.id.clone(), tx.relayer_id.clone()),
151                delay,
152            )
153            .await
154            .map_err(|e| {
155                TransactionError::UnexpectedError(format!("Failed to schedule status check: {}", e))
156            })
157    }
158
159    /// Helper method to produce a submit transaction job.
160    pub(super) async fn send_transaction_submit_job(
161        &self,
162        tx: &TransactionRepoModel,
163    ) -> Result<(), TransactionError> {
164        let job = TransactionSend::submit(tx.id.clone(), tx.relayer_id.clone());
165
166        self.job_producer()
167            .produce_submit_transaction_job(job, None)
168            .await
169            .map_err(|e| {
170                TransactionError::UnexpectedError(format!("Failed to produce submit job: {}", e))
171            })
172    }
173
174    /// Helper method to produce a resubmit transaction job.
175    pub(super) async fn send_transaction_resubmit_job(
176        &self,
177        tx: &TransactionRepoModel,
178    ) -> Result<(), TransactionError> {
179        let job = TransactionSend::resubmit(tx.id.clone(), tx.relayer_id.clone());
180
181        self.job_producer()
182            .produce_submit_transaction_job(job, None)
183            .await
184            .map_err(|e| {
185                TransactionError::UnexpectedError(format!("Failed to produce resubmit job: {}", e))
186            })
187    }
188
189    /// Updates a transaction's status.
190    pub(super) async fn update_transaction_status(
191        &self,
192        tx: TransactionRepoModel,
193        new_status: TransactionStatus,
194    ) -> Result<TransactionRepoModel, TransactionError> {
195        let confirmed_at = if new_status == TransactionStatus::Confirmed {
196            Some(Utc::now().to_rfc3339())
197        } else {
198            None
199        };
200
201        let update_request = TransactionUpdateRequest {
202            status: Some(new_status),
203            confirmed_at,
204            ..Default::default()
205        };
206
207        let updated_tx = self
208            .transaction_repository()
209            .partial_update(tx.id.clone(), update_request)
210            .await?;
211
212        self.send_transaction_update_notification(&updated_tx)
213            .await?;
214        Ok(updated_tx)
215    }
216
217    /// Sends a transaction update notification if a notification ID is configured.
218    pub(super) async fn send_transaction_update_notification(
219        &self,
220        tx: &TransactionRepoModel,
221    ) -> Result<(), TransactionError> {
222        if let Some(notification_id) = &self.relayer().notification_id {
223            self.job_producer()
224                .produce_send_notification_job(
225                    produce_transaction_update_notification_payload(notification_id, tx),
226                    None,
227                )
228                .await
229                .map_err(|e| {
230                    TransactionError::UnexpectedError(format!("Failed to send notification: {}", e))
231                })?;
232        }
233        Ok(())
234    }
235
236    /// Validates that the relayer has sufficient balance for the transaction.
237    ///
238    /// # Arguments
239    ///
240    /// * `total_cost` - The total cost of the transaction (gas + value)
241    ///
242    /// # Returns
243    ///
244    /// A `Result` indicating success or a `TransactionError` if insufficient balance.
245    async fn ensure_sufficient_balance(
246        &self,
247        total_cost: crate::models::U256,
248    ) -> Result<(), TransactionError> {
249        EvmTransactionValidator::validate_sufficient_relayer_balance(
250            total_cost,
251            &self.relayer().address,
252            &self.relayer().policies.get_evm_policy(),
253            &self.provider,
254        )
255        .await
256        .map_err(|validation_error| {
257            TransactionError::InsufficientBalance(validation_error.to_string())
258        })
259    }
260
261    /// Estimates the gas limit for a transaction.
262    ///
263    /// # Arguments
264    ///
265    /// * `evm_data` - The EVM transaction data.
266    /// * `relayer_policy` - The relayer policy.
267    ///
268    async fn estimate_tx_gas_limit(
269        &self,
270        evm_data: &EvmTransactionData,
271        relayer_policy: &RelayerEvmPolicy,
272    ) -> Result<u64, TransactionError> {
273        if !relayer_policy
274            .gas_limit_estimation
275            .unwrap_or(DEFAULT_EVM_GAS_LIMIT_ESTIMATION)
276        {
277            warn!("gas limit estimation is disabled for relayer");
278            return Err(TransactionError::UnexpectedError(
279                "Gas limit estimation is disabled".to_string(),
280            ));
281        }
282
283        let estimated_gas = self.provider.estimate_gas(evm_data).await.map_err(|e| {
284            warn!(error = ?e, tx_data = ?evm_data, "failed to estimate gas");
285            TransactionError::UnexpectedError(format!("Failed to estimate gas: {}", e))
286        })?;
287
288        Ok(estimated_gas * GAS_LIMIT_BUFFER_MULTIPLIER / 100)
289    }
290}
291
292#[async_trait]
293impl<P, RR, NR, TR, J, S, TCR, PC> Transaction
294    for EvmRelayerTransaction<P, RR, NR, TR, J, S, TCR, PC>
295where
296    P: EvmProviderTrait + Send + Sync + 'static,
297    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
298    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
299    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
300    J: JobProducerTrait + Send + Sync + 'static,
301    S: Signer + Send + Sync + 'static,
302    TCR: TransactionCounterTrait + Send + Sync + 'static,
303    PC: PriceCalculatorTrait + Send + Sync + 'static,
304{
305    /// Prepares a transaction for submission.
306    ///
307    /// # Arguments
308    ///
309    /// * `tx` - The transaction model to prepare.
310    ///
311    /// # Returns
312    ///
313    /// A result containing the updated transaction model or a `TransactionError`.
314    async fn prepare_transaction(
315        &self,
316        tx: TransactionRepoModel,
317    ) -> Result<TransactionRepoModel, TransactionError> {
318        debug!("preparing transaction");
319
320        let mut evm_data = tx.network_data.get_evm_transaction_data()?;
321        let relayer = self.relayer();
322
323        if evm_data.gas_limit.is_none() {
324            match self
325                .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
326                .await
327            {
328                Ok(estimated_gas_limit) => {
329                    evm_data.gas_limit = Some(estimated_gas_limit);
330                }
331                Err(estimation_error) => {
332                    error!(error = ?estimation_error, "failed to estimate gas limit");
333
334                    let default_gas_limit = get_evm_default_gas_limit_for_tx(&evm_data);
335                    debug!(gas_limit = %default_gas_limit, "fallback to default gas limit");
336                    evm_data.gas_limit = Some(default_gas_limit);
337                }
338            }
339        }
340
341        // set the gas price
342        let price_params: PriceParams = self
343            .price_calculator
344            .get_transaction_price_params(&evm_data, relayer)
345            .await?;
346
347        debug!(gas_price = ?price_params.gas_price, "gas price");
348        // increment the nonce
349        let nonce = self
350            .transaction_counter_service
351            .get_and_increment(&self.relayer.id, &self.relayer.address)
352            .await
353            .map_err(|e| TransactionError::UnexpectedError(e.to_string()))?;
354
355        let updated_evm_data = evm_data
356            .with_price_params(price_params.clone())
357            .with_nonce(nonce);
358
359        // sign the transaction
360        let sig_result = self
361            .signer
362            .sign_transaction(NetworkTransactionData::Evm(updated_evm_data.clone()))
363            .await?;
364
365        let updated_evm_data =
366            updated_evm_data.with_signed_transaction_data(sig_result.into_evm()?);
367
368        // Validate the relayer has sufficient balance
369        match self
370            .ensure_sufficient_balance(price_params.total_cost)
371            .await
372        {
373            Ok(()) => {}
374            Err(balance_error) => {
375                info!(error = %balance_error, "insufficient balance for transaction");
376
377                let update = TransactionUpdateRequest {
378                    status: Some(TransactionStatus::Failed),
379                    status_reason: Some(balance_error.to_string()),
380                    ..Default::default()
381                };
382
383                let updated_tx = self
384                    .transaction_repository
385                    .partial_update(tx.id.clone(), update)
386                    .await?;
387
388                let _ = self.send_transaction_update_notification(&updated_tx).await;
389                return Err(balance_error);
390            }
391        }
392
393        // Balance validation passed, continue with normal flow
394        // Track the transaction hash
395        let mut hashes = tx.hashes.clone();
396        if let Some(hash) = updated_evm_data.hash.clone() {
397            hashes.push(hash);
398        }
399
400        let update = TransactionUpdateRequest {
401            status: Some(TransactionStatus::Sent),
402            network_data: Some(NetworkTransactionData::Evm(updated_evm_data)),
403            priced_at: Some(Utc::now().to_rfc3339()),
404            hashes: Some(hashes),
405            ..Default::default()
406        };
407
408        let updated_tx = self
409            .transaction_repository
410            .partial_update(tx.id.clone(), update)
411            .await?;
412
413        // after preparing the transaction, we need to submit it to the job queue
414        self.job_producer
415            .produce_submit_transaction_job(
416                TransactionSend::submit(updated_tx.id.clone(), updated_tx.relayer_id.clone()),
417                None,
418            )
419            .await?;
420
421        self.send_transaction_update_notification(&updated_tx)
422            .await?;
423
424        Ok(updated_tx)
425    }
426
427    /// Submits a transaction for processing.
428    ///
429    /// # Arguments
430    ///
431    /// * `tx` - The transaction model to submit.
432    ///
433    /// # Returns
434    ///
435    /// A result containing the updated transaction model or a `TransactionError`.
436    async fn submit_transaction(
437        &self,
438        tx: TransactionRepoModel,
439    ) -> Result<TransactionRepoModel, TransactionError> {
440        debug!("submitting transaction");
441
442        let evm_tx_data = tx.network_data.get_evm_transaction_data()?;
443        let raw_tx = evm_tx_data.raw.as_ref().ok_or_else(|| {
444            TransactionError::InvalidType("Raw transaction data is missing".to_string())
445        })?;
446
447        self.provider.send_raw_transaction(raw_tx).await?;
448
449        let update = TransactionUpdateRequest {
450            status: Some(TransactionStatus::Submitted),
451            sent_at: Some(Utc::now().to_rfc3339()),
452            ..Default::default()
453        };
454
455        let updated_tx = self
456            .transaction_repository
457            .partial_update(tx.id.clone(), update)
458            .await?;
459
460        // Schedule status check
461        self.job_producer
462            .produce_check_transaction_status_job(
463                TransactionStatusCheck::new(updated_tx.id.clone(), updated_tx.relayer_id.clone()),
464                None,
465            )
466            .await?;
467
468        self.send_transaction_update_notification(&updated_tx)
469            .await?;
470
471        Ok(updated_tx)
472    }
473
474    /// Handles the status of a transaction.
475    ///
476    /// # Arguments
477    ///
478    /// * `tx` - The transaction model to handle.
479    ///
480    /// # Returns
481    ///
482    /// A result containing the updated transaction model or a `TransactionError`.
483    async fn handle_transaction_status(
484        &self,
485        tx: TransactionRepoModel,
486    ) -> Result<TransactionRepoModel, TransactionError> {
487        self.handle_status_impl(tx).await
488    }
489    /// Resubmits a transaction with updated parameters.
490    ///
491    /// # Arguments
492    ///
493    /// * `tx` - The transaction model to resubmit.
494    ///
495    /// # Returns
496    ///
497    /// A result containing the resubmitted transaction model or a `TransactionError`.
498    async fn resubmit_transaction(
499        &self,
500        tx: TransactionRepoModel,
501    ) -> Result<TransactionRepoModel, TransactionError> {
502        debug!("resubmitting transaction");
503
504        // Calculate bumped gas price
505        let bumped_price_params = self
506            .price_calculator
507            .calculate_bumped_gas_price(
508                &tx.network_data.get_evm_transaction_data()?,
509                self.relayer(),
510            )
511            .await?;
512
513        if !bumped_price_params.is_min_bumped.is_some_and(|b| b) {
514            warn!(price_params = ?bumped_price_params, "bumped gas price does not meet minimum requirement, skipping resubmission");
515            return Ok(tx);
516        }
517
518        // Get transaction data
519        let evm_data = tx.network_data.get_evm_transaction_data()?;
520
521        // Create new transaction data with bumped gas price
522        let updated_evm_data = evm_data.with_price_params(bumped_price_params.clone());
523
524        // Sign the transaction
525        let sig_result = self
526            .signer
527            .sign_transaction(NetworkTransactionData::Evm(updated_evm_data.clone()))
528            .await?;
529
530        let final_evm_data = updated_evm_data.with_signed_transaction_data(sig_result.into_evm()?);
531
532        // Validate the relayer has sufficient balance
533        self.ensure_sufficient_balance(bumped_price_params.total_cost)
534            .await?;
535
536        let raw_tx = final_evm_data.raw.as_ref().ok_or_else(|| {
537            TransactionError::InvalidType("Raw transaction data is missing".to_string())
538        })?;
539
540        self.provider.send_raw_transaction(raw_tx).await?;
541
542        // Track attempt count and hash history
543        let mut hashes = tx.hashes.clone();
544        if let Some(hash) = final_evm_data.hash.clone() {
545            hashes.push(hash);
546        }
547
548        // Update the transaction in the repository
549        let update = TransactionUpdateRequest {
550            network_data: Some(NetworkTransactionData::Evm(final_evm_data)),
551            hashes: Some(hashes),
552            priced_at: Some(Utc::now().to_rfc3339()),
553            ..Default::default()
554        };
555
556        let updated_tx = self
557            .transaction_repository
558            .partial_update(tx.id.clone(), update)
559            .await?;
560
561        Ok(updated_tx)
562    }
563
564    /// Cancels a transaction.
565    ///
566    /// # Arguments
567    ///
568    /// * `tx` - The transaction model to cancel.
569    ///
570    /// # Returns
571    ///
572    /// A result containing the transaction model or a `TransactionError`.
573    async fn cancel_transaction(
574        &self,
575        tx: TransactionRepoModel,
576    ) -> Result<TransactionRepoModel, TransactionError> {
577        info!("cancelling transaction");
578        debug!(status = ?tx.status, "transaction status");
579        // Check if the transaction can be cancelled
580        if !is_pending_transaction(&tx.status) {
581            return Err(TransactionError::ValidationError(format!(
582                "Cannot cancel transaction with status: {:?}",
583                tx.status
584            )));
585        }
586
587        // If the transaction is in Pending state, we can just update its status
588        if tx.status == TransactionStatus::Pending {
589            debug!("transaction is in pending state, updating status to canceled");
590            return self
591                .update_transaction_status(tx, TransactionStatus::Canceled)
592                .await;
593        }
594
595        let update = self.prepare_noop_update_request(&tx, true).await?;
596        let updated_tx = self
597            .transaction_repository()
598            .partial_update(tx.id.clone(), update)
599            .await?;
600
601        // Submit the updated transaction to the network using the resubmit job
602        self.send_transaction_resubmit_job(&updated_tx).await?;
603
604        // Send notification for the updated transaction
605        self.send_transaction_update_notification(&updated_tx)
606            .await?;
607
608        debug!("original transaction updated with cancellation data");
609        Ok(updated_tx)
610    }
611
612    /// Replaces a transaction with a new one.
613    ///
614    /// # Arguments
615    ///
616    /// * `old_tx` - The transaction model to replace.
617    /// * `new_tx_request` - The new transaction request data.
618    ///
619    /// # Returns
620    ///
621    /// A result containing the updated transaction model or a `TransactionError`.
622    async fn replace_transaction(
623        &self,
624        old_tx: TransactionRepoModel,
625        new_tx_request: NetworkTransactionRequest,
626    ) -> Result<TransactionRepoModel, TransactionError> {
627        debug!("replacing transaction");
628
629        // Check if the transaction can be replaced
630        if !is_pending_transaction(&old_tx.status) {
631            return Err(TransactionError::ValidationError(format!(
632                "Cannot replace transaction with status: {:?}",
633                old_tx.status
634            )));
635        }
636
637        // Extract EVM data from both old transaction and new request
638        let old_evm_data = old_tx.network_data.get_evm_transaction_data()?;
639        let new_evm_request = match new_tx_request {
640            NetworkTransactionRequest::Evm(evm_req) => evm_req,
641            _ => {
642                return Err(TransactionError::InvalidType(
643                    "New transaction request must be EVM type".to_string(),
644                ))
645            }
646        };
647
648        let network_repo_model = self
649            .network_repository()
650            .get_by_chain_id(NetworkType::Evm, old_evm_data.chain_id)
651            .await
652            .map_err(|e| {
653                TransactionError::NetworkConfiguration(format!(
654                    "Failed to get network by chain_id {}: {}",
655                    old_evm_data.chain_id, e
656                ))
657            })?
658            .ok_or_else(|| {
659                TransactionError::NetworkConfiguration(format!(
660                    "Network with chain_id {} not found",
661                    old_evm_data.chain_id
662                ))
663            })?;
664
665        let network = EvmNetwork::try_from(network_repo_model).map_err(|e| {
666            TransactionError::NetworkConfiguration(format!(
667                "Failed to convert network model: {}",
668                e
669            ))
670        })?;
671
672        // First, create updated EVM data without price parameters
673        let updated_evm_data = EvmTransactionData::for_replacement(&old_evm_data, &new_evm_request);
674
675        // Then determine pricing strategy and calculate price parameters using the updated data
676        let price_params = super::replacement::determine_replacement_pricing(
677            &old_evm_data,
678            &updated_evm_data,
679            self.relayer(),
680            &self.price_calculator,
681            network.lacks_mempool(),
682        )
683        .await?;
684
685        debug!(price_params = ?price_params, "replacement price params");
686
687        // Apply the calculated price parameters to the updated EVM data
688        let evm_data_with_price_params = updated_evm_data.with_price_params(price_params.clone());
689
690        // Validate the relayer has sufficient balance
691        self.ensure_sufficient_balance(price_params.total_cost)
692            .await?;
693
694        let sig_result = self
695            .signer
696            .sign_transaction(NetworkTransactionData::Evm(
697                evm_data_with_price_params.clone(),
698            ))
699            .await?;
700
701        let final_evm_data =
702            evm_data_with_price_params.with_signed_transaction_data(sig_result.into_evm()?);
703
704        // Update the transaction in the repository
705        let updated_tx = self
706            .transaction_repository
707            .update_network_data(
708                old_tx.id.clone(),
709                NetworkTransactionData::Evm(final_evm_data),
710            )
711            .await?;
712
713        self.send_transaction_resubmit_job(&updated_tx).await?;
714
715        // Send notification
716        self.send_transaction_update_notification(&updated_tx)
717            .await?;
718
719        Ok(updated_tx)
720    }
721
722    /// Signs a transaction.
723    ///
724    /// # Arguments
725    ///
726    /// * `tx` - The transaction model to sign.
727    ///
728    /// # Returns
729    ///
730    /// A result containing the transaction model or a `TransactionError`.
731    async fn sign_transaction(
732        &self,
733        tx: TransactionRepoModel,
734    ) -> Result<TransactionRepoModel, TransactionError> {
735        Ok(tx)
736    }
737
738    /// Validates a transaction.
739    ///
740    /// # Arguments
741    ///
742    /// * `_tx` - The transaction model to validate.
743    ///
744    /// # Returns
745    ///
746    /// A result containing a boolean indicating validity or a `TransactionError`.
747    async fn validate_transaction(
748        &self,
749        _tx: TransactionRepoModel,
750    ) -> Result<bool, TransactionError> {
751        Ok(true)
752    }
753}
754// P: EvmProviderTrait,
755// R: Repository<RelayerRepoModel, String>,
756// T: TransactionRepository,
757// J: JobProducerTrait,
758// S: Signer,
759// C: TransactionCounterTrait,
760// PC: PriceCalculatorTrait,
761// we define concrete type for the evm transaction
762pub type DefaultEvmTransaction = EvmRelayerTransaction<
763    EvmProvider,
764    RelayerRepositoryStorage,
765    NetworkRepositoryStorage,
766    TransactionRepositoryStorage,
767    JobProducer,
768    EvmSigner,
769    TransactionCounterRepositoryStorage,
770    PriceCalculator<EvmGasPriceService<EvmProvider>>,
771>;
772#[cfg(test)]
773mod tests {
774
775    use super::*;
776    use crate::{
777        domain::evm::price_calculator::PriceParams,
778        jobs::MockJobProducerTrait,
779        models::{
780            evm::Speed, EvmTransactionData, EvmTransactionRequest, NetworkType,
781            RelayerNetworkPolicy, U256,
782        },
783        repositories::{
784            MockNetworkRepository, MockRelayerRepository, MockTransactionCounterTrait,
785            MockTransactionRepository,
786        },
787        services::{MockEvmProviderTrait, MockSigner},
788    };
789    use chrono::Utc;
790    use futures::future::ready;
791    use mockall::{mock, predicate::*};
792
793    // Create a mock for PriceCalculatorTrait
794    mock! {
795        pub PriceCalculator {}
796        #[async_trait]
797        impl PriceCalculatorTrait for PriceCalculator {
798            async fn get_transaction_price_params(
799                &self,
800                tx_data: &EvmTransactionData,
801                relayer: &RelayerRepoModel
802            ) -> Result<PriceParams, TransactionError>;
803
804            async fn calculate_bumped_gas_price(
805                &self,
806                tx: &EvmTransactionData,
807                relayer: &RelayerRepoModel,
808            ) -> Result<PriceParams, TransactionError>;
809        }
810    }
811
812    // Helper to create a relayer model with specific configuration for these tests
813    fn create_test_relayer() -> RelayerRepoModel {
814        create_test_relayer_with_policy(crate::models::RelayerEvmPolicy {
815            min_balance: Some(100000000000000000u128), // 0.1 ETH
816            gas_limit_estimation: Some(true),
817            gas_price_cap: Some(100000000000), // 100 Gwei
818            whitelist_receivers: Some(vec!["0xRecipient".to_string()]),
819            eip1559_pricing: Some(false),
820            private_transactions: Some(false),
821        })
822    }
823
824    fn create_test_relayer_with_policy(evm_policy: RelayerEvmPolicy) -> RelayerRepoModel {
825        RelayerRepoModel {
826            id: "test-relayer-id".to_string(),
827            name: "Test Relayer".to_string(),
828            network: "1".to_string(), // Ethereum Mainnet
829            address: "0xSender".to_string(),
830            paused: false,
831            system_disabled: false,
832            signer_id: "test-signer-id".to_string(),
833            notification_id: Some("test-notification-id".to_string()),
834            policies: RelayerNetworkPolicy::Evm(evm_policy),
835            network_type: NetworkType::Evm,
836            custom_rpc_urls: None,
837            ..Default::default()
838        }
839    }
840
841    // Helper to create test transaction with specific configuration for these tests
842    fn create_test_transaction() -> TransactionRepoModel {
843        TransactionRepoModel {
844            id: "test-tx-id".to_string(),
845            relayer_id: "test-relayer-id".to_string(),
846            status: TransactionStatus::Pending,
847            status_reason: None,
848            created_at: Utc::now().to_rfc3339(),
849            sent_at: None,
850            confirmed_at: None,
851            valid_until: None,
852            delete_at: None,
853            network_type: NetworkType::Evm,
854            network_data: NetworkTransactionData::Evm(EvmTransactionData {
855                chain_id: 1,
856                from: "0xSender".to_string(),
857                to: Some("0xRecipient".to_string()),
858                value: U256::from(1000000000000000000u64), // 1 ETH
859                data: Some("0xData".to_string()),
860                gas_limit: Some(21000),
861                gas_price: Some(20000000000), // 20 Gwei
862                max_fee_per_gas: None,
863                max_priority_fee_per_gas: None,
864                nonce: None,
865                signature: None,
866                hash: None,
867                speed: Some(Speed::Fast),
868                raw: None,
869            }),
870            priced_at: None,
871            hashes: Vec::new(),
872            noop_count: None,
873            is_canceled: Some(false),
874        }
875    }
876
877    #[tokio::test]
878    async fn test_prepare_transaction_with_sufficient_balance() {
879        let mut mock_transaction = MockTransactionRepository::new();
880        let mock_relayer = MockRelayerRepository::new();
881        let mut mock_provider = MockEvmProviderTrait::new();
882        let mut mock_signer = MockSigner::new();
883        let mut mock_job_producer = MockJobProducerTrait::new();
884        let mut mock_price_calculator = MockPriceCalculator::new();
885        let mut counter_service = MockTransactionCounterTrait::new();
886
887        let relayer = create_test_relayer();
888        let test_tx = create_test_transaction();
889
890        counter_service
891            .expect_get_and_increment()
892            .returning(|_, _| Box::pin(ready(Ok(42))));
893
894        let price_params = PriceParams {
895            gas_price: Some(30000000000),
896            max_fee_per_gas: None,
897            max_priority_fee_per_gas: None,
898            is_min_bumped: None,
899            extra_fee: None,
900            total_cost: U256::from(630000000000000u64),
901        };
902        mock_price_calculator
903            .expect_get_transaction_price_params()
904            .returning(move |_, _| Ok(price_params.clone()));
905
906        mock_signer.expect_sign_transaction().returning(|_| {
907            Box::pin(ready(Ok(
908                crate::domain::relayer::SignTransactionResponse::Evm(
909                    crate::domain::relayer::SignTransactionResponseEvm {
910                        hash: "0xtx_hash".to_string(),
911                        signature: crate::models::EvmTransactionDataSignature {
912                            r: "r".to_string(),
913                            s: "s".to_string(),
914                            v: 1,
915                            sig: "0xsignature".to_string(),
916                        },
917                        raw: vec![1, 2, 3],
918                    },
919                ),
920            )))
921        });
922
923        mock_provider
924            .expect_get_balance()
925            .with(eq("0xSender"))
926            .returning(|_| Box::pin(ready(Ok(U256::from(1000000000000000000u64)))));
927
928        let test_tx_clone = test_tx.clone();
929        mock_transaction
930            .expect_partial_update()
931            .returning(move |_, update| {
932                let mut updated_tx = test_tx_clone.clone();
933                if let Some(status) = &update.status {
934                    updated_tx.status = status.clone();
935                }
936                if let Some(network_data) = &update.network_data {
937                    updated_tx.network_data = network_data.clone();
938                }
939                if let Some(hashes) = &update.hashes {
940                    updated_tx.hashes = hashes.clone();
941                }
942                Ok(updated_tx)
943            });
944
945        mock_job_producer
946            .expect_produce_submit_transaction_job()
947            .returning(|_, _| Box::pin(ready(Ok(()))));
948        mock_job_producer
949            .expect_produce_send_notification_job()
950            .returning(|_, _| Box::pin(ready(Ok(()))));
951
952        let mock_network = MockNetworkRepository::new();
953
954        let evm_transaction = EvmRelayerTransaction {
955            relayer: relayer.clone(),
956            provider: mock_provider,
957            relayer_repository: Arc::new(mock_relayer),
958            network_repository: Arc::new(mock_network),
959            transaction_repository: Arc::new(mock_transaction),
960            transaction_counter_service: Arc::new(counter_service),
961            job_producer: Arc::new(mock_job_producer),
962            price_calculator: mock_price_calculator,
963            signer: mock_signer,
964        };
965
966        let result = evm_transaction.prepare_transaction(test_tx.clone()).await;
967        assert!(result.is_ok());
968        let prepared_tx = result.unwrap();
969        assert_eq!(prepared_tx.status, TransactionStatus::Sent);
970        assert!(!prepared_tx.hashes.is_empty());
971    }
972
973    #[tokio::test]
974    async fn test_prepare_transaction_with_insufficient_balance() {
975        let mut mock_transaction = MockTransactionRepository::new();
976        let mock_relayer = MockRelayerRepository::new();
977        let mut mock_provider = MockEvmProviderTrait::new();
978        let mut mock_signer = MockSigner::new();
979        let mut mock_job_producer = MockJobProducerTrait::new();
980        let mut mock_price_calculator = MockPriceCalculator::new();
981        let mut counter_service = MockTransactionCounterTrait::new();
982
983        let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
984            gas_limit_estimation: Some(false),
985            min_balance: Some(100000000000000000u128),
986            ..Default::default()
987        });
988        let test_tx = create_test_transaction();
989
990        counter_service
991            .expect_get_and_increment()
992            .returning(|_, _| Box::pin(ready(Ok(42))));
993
994        let price_params = PriceParams {
995            gas_price: Some(30000000000),
996            max_fee_per_gas: None,
997            max_priority_fee_per_gas: None,
998            is_min_bumped: None,
999            extra_fee: None,
1000            total_cost: U256::from(630000000000000u64),
1001        };
1002        mock_price_calculator
1003            .expect_get_transaction_price_params()
1004            .returning(move |_, _| Ok(price_params.clone()));
1005
1006        mock_signer.expect_sign_transaction().returning(|_| {
1007            Box::pin(ready(Ok(
1008                crate::domain::relayer::SignTransactionResponse::Evm(
1009                    crate::domain::relayer::SignTransactionResponseEvm {
1010                        hash: "0xtx_hash".to_string(),
1011                        signature: crate::models::EvmTransactionDataSignature {
1012                            r: "r".to_string(),
1013                            s: "s".to_string(),
1014                            v: 1,
1015                            sig: "0xsignature".to_string(),
1016                        },
1017                        raw: vec![1, 2, 3],
1018                    },
1019                ),
1020            )))
1021        });
1022
1023        mock_provider
1024            .expect_get_balance()
1025            .with(eq("0xSender"))
1026            .returning(|_| Box::pin(ready(Ok(U256::from(90000000000000000u64)))));
1027
1028        let test_tx_clone = test_tx.clone();
1029        mock_transaction
1030            .expect_partial_update()
1031            .withf(move |id, update| {
1032                id == "test-tx-id" && update.status == Some(TransactionStatus::Failed)
1033            })
1034            .returning(move |_, update| {
1035                let mut updated_tx = test_tx_clone.clone();
1036                updated_tx.status = update.status.unwrap_or(updated_tx.status);
1037                Ok(updated_tx)
1038            });
1039
1040        mock_job_producer
1041            .expect_produce_send_notification_job()
1042            .returning(|_, _| Box::pin(ready(Ok(()))));
1043
1044        let mock_network = MockNetworkRepository::new();
1045
1046        let evm_transaction = EvmRelayerTransaction {
1047            relayer: relayer.clone(),
1048            provider: mock_provider,
1049            relayer_repository: Arc::new(mock_relayer),
1050            network_repository: Arc::new(mock_network),
1051            transaction_repository: Arc::new(mock_transaction),
1052            transaction_counter_service: Arc::new(counter_service),
1053            job_producer: Arc::new(mock_job_producer),
1054            price_calculator: mock_price_calculator,
1055            signer: mock_signer,
1056        };
1057
1058        let result = evm_transaction.prepare_transaction(test_tx.clone()).await;
1059        assert!(
1060            matches!(result, Err(TransactionError::InsufficientBalance(_))),
1061            "Expected InsufficientBalance error, got: {:?}",
1062            result
1063        );
1064    }
1065
1066    #[tokio::test]
1067    async fn test_cancel_transaction() {
1068        // Test Case 1: Canceling a pending transaction
1069        {
1070            // Create mocks for all dependencies
1071            let mut mock_transaction = MockTransactionRepository::new();
1072            let mock_relayer = MockRelayerRepository::new();
1073            let mock_provider = MockEvmProviderTrait::new();
1074            let mock_signer = MockSigner::new();
1075            let mut mock_job_producer = MockJobProducerTrait::new();
1076            let mock_price_calculator = MockPriceCalculator::new();
1077            let counter_service = MockTransactionCounterTrait::new();
1078
1079            // Create test relayer and pending transaction
1080            let relayer = create_test_relayer();
1081            let mut test_tx = create_test_transaction();
1082            test_tx.status = TransactionStatus::Pending;
1083
1084            // Transaction repository should update the transaction with Canceled status
1085            let test_tx_clone = test_tx.clone();
1086            mock_transaction
1087                .expect_partial_update()
1088                .withf(move |id, update| {
1089                    id == "test-tx-id" && update.status == Some(TransactionStatus::Canceled)
1090                })
1091                .returning(move |_, update| {
1092                    let mut updated_tx = test_tx_clone.clone();
1093                    updated_tx.status = update.status.unwrap_or(updated_tx.status);
1094                    Ok(updated_tx)
1095                });
1096
1097            // Job producer should send notification
1098            mock_job_producer
1099                .expect_produce_send_notification_job()
1100                .returning(|_, _| Box::pin(ready(Ok(()))));
1101
1102            let mock_network = MockNetworkRepository::new();
1103
1104            // Set up EVM transaction with the mocks
1105            let evm_transaction = EvmRelayerTransaction {
1106                relayer: relayer.clone(),
1107                provider: mock_provider,
1108                relayer_repository: Arc::new(mock_relayer),
1109                network_repository: Arc::new(mock_network),
1110                transaction_repository: Arc::new(mock_transaction),
1111                transaction_counter_service: Arc::new(counter_service),
1112                job_producer: Arc::new(mock_job_producer),
1113                price_calculator: mock_price_calculator,
1114                signer: mock_signer,
1115            };
1116
1117            // Call cancel_transaction and verify it succeeds
1118            let result = evm_transaction.cancel_transaction(test_tx.clone()).await;
1119            assert!(result.is_ok());
1120            let cancelled_tx = result.unwrap();
1121            assert_eq!(cancelled_tx.id, "test-tx-id");
1122            assert_eq!(cancelled_tx.status, TransactionStatus::Canceled);
1123        }
1124
1125        // Test Case 2: Canceling a submitted transaction
1126        {
1127            // Create mocks for all dependencies
1128            let mut mock_transaction = MockTransactionRepository::new();
1129            let mock_relayer = MockRelayerRepository::new();
1130            let mock_provider = MockEvmProviderTrait::new();
1131            let mut mock_signer = MockSigner::new();
1132            let mut mock_job_producer = MockJobProducerTrait::new();
1133            let mut mock_price_calculator = MockPriceCalculator::new();
1134            let counter_service = MockTransactionCounterTrait::new();
1135
1136            // Create test relayer and submitted transaction
1137            let relayer = create_test_relayer();
1138            let mut test_tx = create_test_transaction();
1139            test_tx.status = TransactionStatus::Submitted;
1140            test_tx.sent_at = Some(Utc::now().to_rfc3339());
1141            test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
1142                nonce: Some(42),
1143                hash: Some("0xoriginal_hash".to_string()),
1144                ..test_tx.network_data.get_evm_transaction_data().unwrap()
1145            });
1146
1147            // Set up price calculator expectations for cancellation tx
1148            mock_price_calculator
1149                .expect_get_transaction_price_params()
1150                .return_once(move |_, _| {
1151                    Ok(PriceParams {
1152                        gas_price: Some(40000000000), // 40 Gwei (higher than original)
1153                        max_fee_per_gas: None,
1154                        max_priority_fee_per_gas: None,
1155                        is_min_bumped: Some(true),
1156                        extra_fee: Some(U256::ZERO),
1157                        total_cost: U256::ZERO,
1158                    })
1159                });
1160
1161            // Signer should be called to sign the cancellation transaction
1162            mock_signer.expect_sign_transaction().returning(|_| {
1163                Box::pin(ready(Ok(
1164                    crate::domain::relayer::SignTransactionResponse::Evm(
1165                        crate::domain::relayer::SignTransactionResponseEvm {
1166                            hash: "0xcancellation_hash".to_string(),
1167                            signature: crate::models::EvmTransactionDataSignature {
1168                                r: "r".to_string(),
1169                                s: "s".to_string(),
1170                                v: 1,
1171                                sig: "0xsignature".to_string(),
1172                            },
1173                            raw: vec![1, 2, 3],
1174                        },
1175                    ),
1176                )))
1177            });
1178
1179            // Transaction repository should update the transaction
1180            let test_tx_clone = test_tx.clone();
1181            mock_transaction
1182                .expect_partial_update()
1183                .returning(move |tx_id, update| {
1184                    let mut updated_tx = test_tx_clone.clone();
1185                    updated_tx.id = tx_id;
1186                    updated_tx.status = update.status.unwrap_or(updated_tx.status);
1187                    updated_tx.network_data =
1188                        update.network_data.unwrap_or(updated_tx.network_data);
1189                    if let Some(hashes) = update.hashes {
1190                        updated_tx.hashes = hashes;
1191                    }
1192                    Ok(updated_tx)
1193                });
1194
1195            // Job producer expectations
1196            mock_job_producer
1197                .expect_produce_submit_transaction_job()
1198                .returning(|_, _| Box::pin(ready(Ok(()))));
1199            mock_job_producer
1200                .expect_produce_send_notification_job()
1201                .returning(|_, _| Box::pin(ready(Ok(()))));
1202
1203            // Network repository expectations for cancellation NOOP transaction
1204            let mut mock_network = MockNetworkRepository::new();
1205            mock_network
1206                .expect_get_by_chain_id()
1207                .with(eq(NetworkType::Evm), eq(1))
1208                .returning(|_, _| {
1209                    use crate::config::{EvmNetworkConfig, NetworkConfigCommon};
1210                    use crate::models::{NetworkConfigData, NetworkRepoModel};
1211
1212                    let config = EvmNetworkConfig {
1213                        common: NetworkConfigCommon {
1214                            network: "mainnet".to_string(),
1215                            from: None,
1216                            rpc_urls: Some(vec!["https://rpc.example.com".to_string()]),
1217                            explorer_urls: None,
1218                            average_blocktime_ms: Some(12000),
1219                            is_testnet: Some(false),
1220                            tags: Some(vec!["mainnet".to_string()]),
1221                        },
1222                        chain_id: Some(1),
1223                        required_confirmations: Some(12),
1224                        features: Some(vec!["eip1559".to_string()]),
1225                        symbol: Some("ETH".to_string()),
1226                        gas_price_cache: None,
1227                    };
1228                    Ok(Some(NetworkRepoModel {
1229                        id: "evm:mainnet".to_string(),
1230                        name: "mainnet".to_string(),
1231                        network_type: NetworkType::Evm,
1232                        config: NetworkConfigData::Evm(config),
1233                    }))
1234                });
1235
1236            // Set up EVM transaction with the mocks
1237            let evm_transaction = EvmRelayerTransaction {
1238                relayer: relayer.clone(),
1239                provider: mock_provider,
1240                relayer_repository: Arc::new(mock_relayer),
1241                network_repository: Arc::new(mock_network),
1242                transaction_repository: Arc::new(mock_transaction),
1243                transaction_counter_service: Arc::new(counter_service),
1244                job_producer: Arc::new(mock_job_producer),
1245                price_calculator: mock_price_calculator,
1246                signer: mock_signer,
1247            };
1248
1249            // Call cancel_transaction and verify it succeeds
1250            let result = evm_transaction.cancel_transaction(test_tx.clone()).await;
1251            assert!(result.is_ok());
1252            let cancelled_tx = result.unwrap();
1253
1254            // Verify the cancellation transaction was properly created
1255            assert_eq!(cancelled_tx.id, "test-tx-id");
1256            assert_eq!(cancelled_tx.status, TransactionStatus::Submitted);
1257
1258            // Verify the network data was properly updated
1259            if let NetworkTransactionData::Evm(evm_data) = &cancelled_tx.network_data {
1260                assert_eq!(evm_data.nonce, Some(42)); // Same nonce as original
1261            } else {
1262                panic!("Expected EVM transaction data");
1263            }
1264        }
1265
1266        // Test Case 3: Attempting to cancel a confirmed transaction (should fail)
1267        {
1268            // Create minimal mocks for failure case
1269            let mock_transaction = MockTransactionRepository::new();
1270            let mock_relayer = MockRelayerRepository::new();
1271            let mock_provider = MockEvmProviderTrait::new();
1272            let mock_signer = MockSigner::new();
1273            let mock_job_producer = MockJobProducerTrait::new();
1274            let mock_price_calculator = MockPriceCalculator::new();
1275            let counter_service = MockTransactionCounterTrait::new();
1276
1277            // Create test relayer and confirmed transaction
1278            let relayer = create_test_relayer();
1279            let mut test_tx = create_test_transaction();
1280            test_tx.status = TransactionStatus::Confirmed;
1281
1282            let mock_network = MockNetworkRepository::new();
1283
1284            // Set up EVM transaction with the mocks
1285            let evm_transaction = EvmRelayerTransaction {
1286                relayer: relayer.clone(),
1287                provider: mock_provider,
1288                relayer_repository: Arc::new(mock_relayer),
1289                network_repository: Arc::new(mock_network),
1290                transaction_repository: Arc::new(mock_transaction),
1291                transaction_counter_service: Arc::new(counter_service),
1292                job_producer: Arc::new(mock_job_producer),
1293                price_calculator: mock_price_calculator,
1294                signer: mock_signer,
1295            };
1296
1297            // Call cancel_transaction and verify it fails
1298            let result = evm_transaction.cancel_transaction(test_tx.clone()).await;
1299            assert!(result.is_err());
1300            if let Err(TransactionError::ValidationError(msg)) = result {
1301                assert!(msg.contains("Cannot cancel transaction with status"));
1302            } else {
1303                panic!("Expected ValidationError");
1304            }
1305        }
1306    }
1307
1308    #[tokio::test]
1309    async fn test_replace_transaction() {
1310        // Test Case: Replacing a submitted transaction with new gas price
1311        {
1312            // Create mocks for all dependencies
1313            let mut mock_transaction = MockTransactionRepository::new();
1314            let mock_relayer = MockRelayerRepository::new();
1315            let mut mock_provider = MockEvmProviderTrait::new();
1316            let mut mock_signer = MockSigner::new();
1317            let mut mock_job_producer = MockJobProducerTrait::new();
1318            let mut mock_price_calculator = MockPriceCalculator::new();
1319            let counter_service = MockTransactionCounterTrait::new();
1320
1321            // Create test relayer and submitted transaction
1322            let relayer = create_test_relayer();
1323            let mut test_tx = create_test_transaction();
1324            test_tx.status = TransactionStatus::Submitted;
1325            test_tx.sent_at = Some(Utc::now().to_rfc3339());
1326
1327            // Set up price calculator expectations for replacement
1328            mock_price_calculator
1329                .expect_get_transaction_price_params()
1330                .return_once(move |_, _| {
1331                    Ok(PriceParams {
1332                        gas_price: Some(40000000000), // 40 Gwei (higher than original)
1333                        max_fee_per_gas: None,
1334                        max_priority_fee_per_gas: None,
1335                        is_min_bumped: Some(true),
1336                        extra_fee: Some(U256::ZERO),
1337                        total_cost: U256::from(2001000000000000000u64), // 2 ETH + gas costs
1338                    })
1339                });
1340
1341            // Signer should be called to sign the replacement transaction
1342            mock_signer.expect_sign_transaction().returning(|_| {
1343                Box::pin(ready(Ok(
1344                    crate::domain::relayer::SignTransactionResponse::Evm(
1345                        crate::domain::relayer::SignTransactionResponseEvm {
1346                            hash: "0xreplacement_hash".to_string(),
1347                            signature: crate::models::EvmTransactionDataSignature {
1348                                r: "r".to_string(),
1349                                s: "s".to_string(),
1350                                v: 1,
1351                                sig: "0xsignature".to_string(),
1352                            },
1353                            raw: vec![1, 2, 3],
1354                        },
1355                    ),
1356                )))
1357            });
1358
1359            // Provider balance check should pass
1360            mock_provider
1361                .expect_get_balance()
1362                .with(eq("0xSender"))
1363                .returning(|_| Box::pin(ready(Ok(U256::from(3000000000000000000u64)))));
1364
1365            // Transaction repository should update using update_network_data
1366            let test_tx_clone = test_tx.clone();
1367            mock_transaction
1368                .expect_update_network_data()
1369                .returning(move |tx_id, network_data| {
1370                    let mut updated_tx = test_tx_clone.clone();
1371                    updated_tx.id = tx_id;
1372                    updated_tx.network_data = network_data;
1373                    Ok(updated_tx)
1374                });
1375
1376            // Job producer expectations
1377            mock_job_producer
1378                .expect_produce_submit_transaction_job()
1379                .returning(|_, _| Box::pin(ready(Ok(()))));
1380            mock_job_producer
1381                .expect_produce_send_notification_job()
1382                .returning(|_, _| Box::pin(ready(Ok(()))));
1383
1384            // Network repository expectations for mempool check
1385            let mut mock_network = MockNetworkRepository::new();
1386            mock_network
1387                .expect_get_by_chain_id()
1388                .with(eq(NetworkType::Evm), eq(1))
1389                .returning(|_, _| {
1390                    use crate::config::{EvmNetworkConfig, NetworkConfigCommon};
1391                    use crate::models::{NetworkConfigData, NetworkRepoModel};
1392
1393                    let config = EvmNetworkConfig {
1394                        common: NetworkConfigCommon {
1395                            network: "mainnet".to_string(),
1396                            from: None,
1397                            rpc_urls: Some(vec!["https://rpc.example.com".to_string()]),
1398                            explorer_urls: None,
1399                            average_blocktime_ms: Some(12000),
1400                            is_testnet: Some(false),
1401                            tags: Some(vec!["mainnet".to_string()]), // No "no-mempool" tag
1402                        },
1403                        chain_id: Some(1),
1404                        required_confirmations: Some(12),
1405                        features: Some(vec!["eip1559".to_string()]),
1406                        symbol: Some("ETH".to_string()),
1407                        gas_price_cache: None,
1408                    };
1409                    Ok(Some(NetworkRepoModel {
1410                        id: "evm:mainnet".to_string(),
1411                        name: "mainnet".to_string(),
1412                        network_type: NetworkType::Evm,
1413                        config: NetworkConfigData::Evm(config),
1414                    }))
1415                });
1416
1417            // Set up EVM transaction with the mocks
1418            let evm_transaction = EvmRelayerTransaction {
1419                relayer: relayer.clone(),
1420                provider: mock_provider,
1421                relayer_repository: Arc::new(mock_relayer),
1422                network_repository: Arc::new(mock_network),
1423                transaction_repository: Arc::new(mock_transaction),
1424                transaction_counter_service: Arc::new(counter_service),
1425                job_producer: Arc::new(mock_job_producer),
1426                price_calculator: mock_price_calculator,
1427                signer: mock_signer,
1428            };
1429
1430            // Create replacement request with speed-based pricing
1431            let replacement_request = NetworkTransactionRequest::Evm(EvmTransactionRequest {
1432                to: Some("0xNewRecipient".to_string()),
1433                value: U256::from(2000000000000000000u64), // 2 ETH
1434                data: Some("0xNewData".to_string()),
1435                gas_limit: Some(25000),
1436                gas_price: None, // Use speed-based pricing
1437                max_fee_per_gas: None,
1438                max_priority_fee_per_gas: None,
1439                speed: Some(Speed::Fast),
1440                valid_until: None,
1441            });
1442
1443            // Call replace_transaction and verify it succeeds
1444            let result = evm_transaction
1445                .replace_transaction(test_tx.clone(), replacement_request)
1446                .await;
1447            if let Err(ref e) = result {
1448                eprintln!("Replace transaction failed with error: {:?}", e);
1449            }
1450            assert!(result.is_ok());
1451            let replaced_tx = result.unwrap();
1452
1453            // Verify the replacement was properly processed
1454            assert_eq!(replaced_tx.id, "test-tx-id");
1455
1456            // Verify the network data was properly updated
1457            if let NetworkTransactionData::Evm(evm_data) = &replaced_tx.network_data {
1458                assert_eq!(evm_data.to, Some("0xNewRecipient".to_string()));
1459                assert_eq!(evm_data.value, U256::from(2000000000000000000u64));
1460                assert_eq!(evm_data.gas_price, Some(40000000000));
1461                assert_eq!(evm_data.gas_limit, Some(25000));
1462                assert!(evm_data.hash.is_some());
1463                assert!(evm_data.raw.is_some());
1464            } else {
1465                panic!("Expected EVM transaction data");
1466            }
1467        }
1468
1469        // Test Case: Attempting to replace a confirmed transaction (should fail)
1470        {
1471            // Create minimal mocks for failure case
1472            let mock_transaction = MockTransactionRepository::new();
1473            let mock_relayer = MockRelayerRepository::new();
1474            let mock_provider = MockEvmProviderTrait::new();
1475            let mock_signer = MockSigner::new();
1476            let mock_job_producer = MockJobProducerTrait::new();
1477            let mock_price_calculator = MockPriceCalculator::new();
1478            let counter_service = MockTransactionCounterTrait::new();
1479
1480            // Create test relayer and confirmed transaction
1481            let relayer = create_test_relayer();
1482            let mut test_tx = create_test_transaction();
1483            test_tx.status = TransactionStatus::Confirmed;
1484
1485            let mock_network = MockNetworkRepository::new();
1486
1487            // Set up EVM transaction with the mocks
1488            let evm_transaction = EvmRelayerTransaction {
1489                relayer: relayer.clone(),
1490                provider: mock_provider,
1491                relayer_repository: Arc::new(mock_relayer),
1492                network_repository: Arc::new(mock_network),
1493                transaction_repository: Arc::new(mock_transaction),
1494                transaction_counter_service: Arc::new(counter_service),
1495                job_producer: Arc::new(mock_job_producer),
1496                price_calculator: mock_price_calculator,
1497                signer: mock_signer,
1498            };
1499
1500            // Create dummy replacement request
1501            let replacement_request = NetworkTransactionRequest::Evm(EvmTransactionRequest {
1502                to: Some("0xNewRecipient".to_string()),
1503                value: U256::from(1000000000000000000u64),
1504                data: Some("0xData".to_string()),
1505                gas_limit: Some(21000),
1506                gas_price: Some(30000000000),
1507                max_fee_per_gas: None,
1508                max_priority_fee_per_gas: None,
1509                speed: Some(Speed::Fast),
1510                valid_until: None,
1511            });
1512
1513            // Call replace_transaction and verify it fails
1514            let result = evm_transaction
1515                .replace_transaction(test_tx.clone(), replacement_request)
1516                .await;
1517            assert!(result.is_err());
1518            if let Err(TransactionError::ValidationError(msg)) = result {
1519                assert!(msg.contains("Cannot replace transaction with status"));
1520            } else {
1521                panic!("Expected ValidationError");
1522            }
1523        }
1524    }
1525
1526    #[tokio::test]
1527    async fn test_estimate_tx_gas_limit_success() {
1528        let mock_transaction = MockTransactionRepository::new();
1529        let mock_relayer = MockRelayerRepository::new();
1530        let mut mock_provider = MockEvmProviderTrait::new();
1531        let mock_signer = MockSigner::new();
1532        let mock_job_producer = MockJobProducerTrait::new();
1533        let mock_price_calculator = MockPriceCalculator::new();
1534        let counter_service = MockTransactionCounterTrait::new();
1535        let mock_network = MockNetworkRepository::new();
1536
1537        // Create test relayer and pending transaction
1538        let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
1539            gas_limit_estimation: Some(true),
1540            ..Default::default()
1541        });
1542        let evm_data = EvmTransactionData {
1543            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
1544            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
1545            value: U256::from(1000000000000000000u128),
1546            data: Some("0x".to_string()),
1547            gas_limit: None,
1548            gas_price: Some(20_000_000_000),
1549            nonce: Some(1),
1550            chain_id: 1,
1551            hash: None,
1552            signature: None,
1553            speed: Some(Speed::Average),
1554            max_fee_per_gas: None,
1555            max_priority_fee_per_gas: None,
1556            raw: None,
1557        };
1558
1559        // Mock provider to return 21000 as estimated gas
1560        mock_provider
1561            .expect_estimate_gas()
1562            .times(1)
1563            .returning(|_| Box::pin(async { Ok(21000) }));
1564
1565        let transaction = EvmRelayerTransaction::new(
1566            relayer.clone(),
1567            mock_provider,
1568            Arc::new(mock_relayer),
1569            Arc::new(mock_network),
1570            Arc::new(mock_transaction),
1571            Arc::new(counter_service),
1572            Arc::new(mock_job_producer),
1573            mock_price_calculator,
1574            mock_signer,
1575        )
1576        .unwrap();
1577
1578        let result = transaction
1579            .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
1580            .await;
1581
1582        assert!(result.is_ok());
1583        // Expected: 21000 * 110 / 100 = 23100
1584        assert_eq!(result.unwrap(), 23100);
1585    }
1586
1587    #[tokio::test]
1588    async fn test_estimate_tx_gas_limit_disabled() {
1589        let mock_transaction = MockTransactionRepository::new();
1590        let mock_relayer = MockRelayerRepository::new();
1591        let mut mock_provider = MockEvmProviderTrait::new();
1592        let mock_signer = MockSigner::new();
1593        let mock_job_producer = MockJobProducerTrait::new();
1594        let mock_price_calculator = MockPriceCalculator::new();
1595        let counter_service = MockTransactionCounterTrait::new();
1596        let mock_network = MockNetworkRepository::new();
1597
1598        // Create test relayer and pending transaction
1599        let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
1600            gas_limit_estimation: Some(false),
1601            ..Default::default()
1602        });
1603
1604        let evm_data = EvmTransactionData {
1605            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
1606            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
1607            value: U256::from(1000000000000000000u128),
1608            data: Some("0x".to_string()),
1609            gas_limit: None,
1610            gas_price: Some(20_000_000_000),
1611            nonce: Some(1),
1612            chain_id: 1,
1613            hash: None,
1614            signature: None,
1615            speed: Some(Speed::Average),
1616            max_fee_per_gas: None,
1617            max_priority_fee_per_gas: None,
1618            raw: None,
1619        };
1620
1621        // Provider should not be called when estimation is disabled
1622        mock_provider.expect_estimate_gas().times(0);
1623
1624        let transaction = EvmRelayerTransaction::new(
1625            relayer.clone(),
1626            mock_provider,
1627            Arc::new(mock_relayer),
1628            Arc::new(mock_network),
1629            Arc::new(mock_transaction),
1630            Arc::new(counter_service),
1631            Arc::new(mock_job_producer),
1632            mock_price_calculator,
1633            mock_signer,
1634        )
1635        .unwrap();
1636
1637        let result = transaction
1638            .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
1639            .await;
1640
1641        assert!(result.is_err());
1642        assert!(matches!(
1643            result.unwrap_err(),
1644            TransactionError::UnexpectedError(_)
1645        ));
1646    }
1647
1648    #[tokio::test]
1649    async fn test_estimate_tx_gas_limit_default_enabled() {
1650        let mock_transaction = MockTransactionRepository::new();
1651        let mock_relayer = MockRelayerRepository::new();
1652        let mut mock_provider = MockEvmProviderTrait::new();
1653        let mock_signer = MockSigner::new();
1654        let mock_job_producer = MockJobProducerTrait::new();
1655        let mock_price_calculator = MockPriceCalculator::new();
1656        let counter_service = MockTransactionCounterTrait::new();
1657        let mock_network = MockNetworkRepository::new();
1658
1659        let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
1660            gas_limit_estimation: None, // Should default to true
1661            ..Default::default()
1662        });
1663
1664        let evm_data = EvmTransactionData {
1665            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
1666            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
1667            value: U256::from(1000000000000000000u128),
1668            data: Some("0x".to_string()),
1669            gas_limit: None,
1670            gas_price: Some(20_000_000_000),
1671            nonce: Some(1),
1672            chain_id: 1,
1673            hash: None,
1674            signature: None,
1675            speed: Some(Speed::Average),
1676            max_fee_per_gas: None,
1677            max_priority_fee_per_gas: None,
1678            raw: None,
1679        };
1680
1681        // Mock provider to return 50000 as estimated gas
1682        mock_provider
1683            .expect_estimate_gas()
1684            .times(1)
1685            .returning(|_| Box::pin(async { Ok(50000) }));
1686
1687        let transaction = EvmRelayerTransaction::new(
1688            relayer.clone(),
1689            mock_provider,
1690            Arc::new(mock_relayer),
1691            Arc::new(mock_network),
1692            Arc::new(mock_transaction),
1693            Arc::new(counter_service),
1694            Arc::new(mock_job_producer),
1695            mock_price_calculator,
1696            mock_signer,
1697        )
1698        .unwrap();
1699
1700        let result = transaction
1701            .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
1702            .await;
1703
1704        assert!(result.is_ok());
1705        // Expected: 50000 * 110 / 100 = 55000
1706        assert_eq!(result.unwrap(), 55000);
1707    }
1708
1709    #[tokio::test]
1710    async fn test_estimate_tx_gas_limit_provider_error() {
1711        let mock_transaction = MockTransactionRepository::new();
1712        let mock_relayer = MockRelayerRepository::new();
1713        let mut mock_provider = MockEvmProviderTrait::new();
1714        let mock_signer = MockSigner::new();
1715        let mock_job_producer = MockJobProducerTrait::new();
1716        let mock_price_calculator = MockPriceCalculator::new();
1717        let counter_service = MockTransactionCounterTrait::new();
1718        let mock_network = MockNetworkRepository::new();
1719
1720        let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
1721            gas_limit_estimation: Some(true),
1722            ..Default::default()
1723        });
1724
1725        let evm_data = EvmTransactionData {
1726            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
1727            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
1728            value: U256::from(1000000000000000000u128),
1729            data: Some("0x".to_string()),
1730            gas_limit: None,
1731            gas_price: Some(20_000_000_000),
1732            nonce: Some(1),
1733            chain_id: 1,
1734            hash: None,
1735            signature: None,
1736            speed: Some(Speed::Average),
1737            max_fee_per_gas: None,
1738            max_priority_fee_per_gas: None,
1739            raw: None,
1740        };
1741
1742        // Mock provider to return an error
1743        mock_provider.expect_estimate_gas().times(1).returning(|_| {
1744            Box::pin(async {
1745                Err(crate::services::ProviderError::Other(
1746                    "RPC error".to_string(),
1747                ))
1748            })
1749        });
1750
1751        let transaction = EvmRelayerTransaction::new(
1752            relayer.clone(),
1753            mock_provider,
1754            Arc::new(mock_relayer),
1755            Arc::new(mock_network),
1756            Arc::new(mock_transaction),
1757            Arc::new(counter_service),
1758            Arc::new(mock_job_producer),
1759            mock_price_calculator,
1760            mock_signer,
1761        )
1762        .unwrap();
1763
1764        let result = transaction
1765            .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
1766            .await;
1767
1768        assert!(result.is_err());
1769        assert!(matches!(
1770            result.unwrap_err(),
1771            TransactionError::UnexpectedError(_)
1772        ));
1773    }
1774
1775    #[tokio::test]
1776    async fn test_prepare_transaction_uses_gas_estimation_and_stores_result() {
1777        let mut mock_transaction = MockTransactionRepository::new();
1778        let mock_relayer = MockRelayerRepository::new();
1779        let mut mock_provider = MockEvmProviderTrait::new();
1780        let mut mock_signer = MockSigner::new();
1781        let mut mock_job_producer = MockJobProducerTrait::new();
1782        let mut mock_price_calculator = MockPriceCalculator::new();
1783        let mut counter_service = MockTransactionCounterTrait::new();
1784        let mock_network = MockNetworkRepository::new();
1785
1786        // Create test relayer with gas limit estimation enabled
1787        let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
1788            gas_limit_estimation: Some(true),
1789            min_balance: Some(100000000000000000u128),
1790            ..Default::default()
1791        });
1792
1793        // Create test transaction WITHOUT gas_limit (so estimation will be triggered)
1794        let mut test_tx = create_test_transaction();
1795        if let NetworkTransactionData::Evm(ref mut evm_data) = test_tx.network_data {
1796            evm_data.gas_limit = None; // This should trigger gas estimation
1797            evm_data.nonce = None; // This will be set by the counter service
1798        }
1799
1800        // Expected estimated gas from provider
1801        const PROVIDER_GAS_ESTIMATE: u64 = 45000;
1802        const EXPECTED_GAS_WITH_BUFFER: u64 = 49500; // 45000 * 110 / 100
1803
1804        // Mock provider to return specific gas estimate
1805        mock_provider
1806            .expect_estimate_gas()
1807            .times(1)
1808            .returning(move |_| Box::pin(async move { Ok(PROVIDER_GAS_ESTIMATE) }));
1809
1810        // Mock provider for balance check
1811        mock_provider
1812            .expect_get_balance()
1813            .times(1)
1814            .returning(|_| Box::pin(async { Ok(U256::from(2000000000000000000u128)) })); // 2 ETH
1815
1816        let price_params = PriceParams {
1817            gas_price: Some(20_000_000_000), // 20 Gwei
1818            max_fee_per_gas: None,
1819            max_priority_fee_per_gas: None,
1820            is_min_bumped: None,
1821            extra_fee: None,
1822            total_cost: U256::from(1900000000000000000u128), // 1.9 ETH total cost
1823        };
1824
1825        // Mock price calculator
1826        mock_price_calculator
1827            .expect_get_transaction_price_params()
1828            .returning(move |_, _| Ok(price_params.clone()));
1829
1830        // Mock transaction counter to return a nonce
1831        counter_service
1832            .expect_get_and_increment()
1833            .times(1)
1834            .returning(|_, _| Box::pin(async { Ok(42) }));
1835
1836        // Mock signer to return a signed transaction
1837        mock_signer.expect_sign_transaction().returning(|_| {
1838            Box::pin(ready(Ok(
1839                crate::domain::relayer::SignTransactionResponse::Evm(
1840                    crate::domain::relayer::SignTransactionResponseEvm {
1841                        hash: "0xhash".to_string(),
1842                        signature: crate::models::EvmTransactionDataSignature {
1843                            r: "r".to_string(),
1844                            s: "s".to_string(),
1845                            v: 1,
1846                            sig: "0xsignature".to_string(),
1847                        },
1848                        raw: vec![1, 2, 3],
1849                    },
1850                ),
1851            )))
1852        });
1853
1854        // Mock job producer to capture the submission job
1855        mock_job_producer
1856            .expect_produce_submit_transaction_job()
1857            .returning(|_, _| Box::pin(async { Ok(()) }));
1858
1859        mock_job_producer
1860            .expect_produce_send_notification_job()
1861            .returning(|_, _| Box::pin(ready(Ok(()))));
1862
1863        // Mock transaction repository partial_update calls
1864        let expected_gas_limit = EXPECTED_GAS_WITH_BUFFER;
1865
1866        let test_tx_clone = test_tx.clone();
1867        mock_transaction
1868            .expect_partial_update()
1869            .times(1)
1870            .returning(move |_, _update| {
1871                let mut updated_tx = test_tx_clone.clone();
1872                updated_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
1873                    gas_limit: Some(expected_gas_limit),
1874                    ..test_tx_clone
1875                        .network_data
1876                        .get_evm_transaction_data()
1877                        .unwrap()
1878                });
1879                Ok(updated_tx)
1880            });
1881
1882        let transaction = EvmRelayerTransaction::new(
1883            relayer.clone(),
1884            mock_provider,
1885            Arc::new(mock_relayer),
1886            Arc::new(mock_network),
1887            Arc::new(mock_transaction),
1888            Arc::new(counter_service),
1889            Arc::new(mock_job_producer),
1890            mock_price_calculator,
1891            mock_signer,
1892        )
1893        .unwrap();
1894
1895        // Call prepare_transaction
1896        let result = transaction.prepare_transaction(test_tx).await;
1897
1898        // Verify the transaction was prepared successfully
1899        assert!(result.is_ok(), "prepare_transaction should succeed");
1900        let prepared_tx = result.unwrap();
1901
1902        // Verify the final transaction has the estimated gas limit
1903        if let NetworkTransactionData::Evm(evm_data) = prepared_tx.network_data {
1904            assert_eq!(evm_data.gas_limit, Some(EXPECTED_GAS_WITH_BUFFER));
1905        } else {
1906            panic!("Expected EVM network data");
1907        }
1908    }
1909}