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