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, info, warn};
11use std::sync::Arc;
12
13use crate::{
14    domain::{
15        transaction::{
16            evm::{is_pending_transaction, PriceCalculator, PriceCalculatorTrait},
17            Transaction,
18        },
19        EvmTransactionValidator,
20    },
21    jobs::{JobProducer, JobProducerTrait, TransactionSend, TransactionStatusCheck},
22    models::{
23        produce_transaction_update_notification_payload, EvmNetwork, EvmTransactionData,
24        NetworkTransactionData, NetworkTransactionRequest, NetworkType, RelayerRepoModel,
25        TransactionError, TransactionRepoModel, TransactionStatus, TransactionUpdateRequest,
26    },
27    repositories::{
28        InMemoryNetworkRepository, InMemoryRelayerRepository, InMemoryTransactionCounter,
29        NetworkRepository, RelayerRepositoryStorage, Repository, TransactionCounterTrait,
30        TransactionRepository,
31    },
32    services::{EvmGasPriceService, EvmProvider, EvmProviderTrait, EvmSigner, Signer},
33};
34
35use super::PriceParams;
36
37// Import shared test helpers from status module
38
39// Import shared test helpers from test_helpers module
40
41#[allow(dead_code)]
42pub struct EvmRelayerTransaction<P, R, N, T, J, S, C, PC>
43where
44    P: EvmProviderTrait,
45    R: Repository<RelayerRepoModel, String>,
46    N: NetworkRepository,
47    T: TransactionRepository,
48    J: JobProducerTrait,
49    S: Signer,
50    C: TransactionCounterTrait,
51    PC: PriceCalculatorTrait,
52{
53    provider: P,
54    relayer_repository: Arc<R>,
55    network_repository: Arc<N>,
56    transaction_repository: Arc<T>,
57    job_producer: Arc<J>,
58    signer: S,
59    relayer: RelayerRepoModel,
60    transaction_counter_service: Arc<C>,
61    price_calculator: PC,
62}
63
64#[allow(dead_code, clippy::too_many_arguments)]
65impl<P, R, N, T, J, S, C, PC> EvmRelayerTransaction<P, R, N, T, J, S, C, PC>
66where
67    P: EvmProviderTrait,
68    R: Repository<RelayerRepoModel, String>,
69    N: NetworkRepository,
70    T: TransactionRepository,
71    J: JobProducerTrait,
72    S: Signer,
73    C: TransactionCounterTrait,
74    PC: PriceCalculatorTrait,
75{
76    /// Creates a new `EvmRelayerTransaction`.
77    ///
78    /// # Arguments
79    ///
80    /// * `relayer` - The relayer model.
81    /// * `provider` - The EVM provider.
82    /// * `relayer_repository` - Storage for relayer repository.
83    /// * `transaction_repository` - Storage for transaction repository.
84    /// * `transaction_counter_service` - Service for managing transaction counters.
85    /// * `job_producer` - Producer for job queue.
86    /// * `price_calculator` - Price calculator for gas price management.
87    /// * `signer` - The EVM signer.
88    ///
89    /// # Returns
90    ///
91    /// A result containing the new `EvmRelayerTransaction` or a `TransactionError`.
92    pub fn new(
93        relayer: RelayerRepoModel,
94        provider: P,
95        relayer_repository: Arc<R>,
96        network_repository: Arc<N>,
97        transaction_repository: Arc<T>,
98        transaction_counter_service: Arc<C>,
99        job_producer: Arc<J>,
100        price_calculator: PC,
101        signer: S,
102    ) -> Result<Self, TransactionError> {
103        Ok(Self {
104            relayer,
105            provider,
106            relayer_repository,
107            network_repository,
108            transaction_repository,
109            transaction_counter_service,
110            job_producer,
111            price_calculator,
112            signer,
113        })
114    }
115
116    /// Returns a reference to the provider.
117    pub fn provider(&self) -> &P {
118        &self.provider
119    }
120
121    /// Returns a reference to the relayer model.
122    pub fn relayer(&self) -> &RelayerRepoModel {
123        &self.relayer
124    }
125
126    /// Returns a reference to the network repository.
127    pub fn network_repository(&self) -> &N {
128        &self.network_repository
129    }
130
131    /// Returns a reference to the job producer.
132    pub fn job_producer(&self) -> &J {
133        &self.job_producer
134    }
135
136    pub fn transaction_repository(&self) -> &T {
137        &self.transaction_repository
138    }
139
140    /// Helper method to schedule a transaction status check job.
141    pub(super) async fn schedule_status_check(
142        &self,
143        tx: &TransactionRepoModel,
144        delay_seconds: Option<i64>,
145    ) -> Result<(), TransactionError> {
146        let delay = delay_seconds.map(|seconds| Utc::now().timestamp() + seconds);
147        self.job_producer()
148            .produce_check_transaction_status_job(
149                TransactionStatusCheck::new(tx.id.clone(), tx.relayer_id.clone()),
150                delay,
151            )
152            .await
153            .map_err(|e| {
154                TransactionError::UnexpectedError(format!("Failed to schedule status check: {}", e))
155            })
156    }
157
158    /// Helper method to produce a submit transaction job.
159    pub(super) async fn send_transaction_submit_job(
160        &self,
161        tx: &TransactionRepoModel,
162    ) -> Result<(), TransactionError> {
163        let job = TransactionSend::submit(tx.id.clone(), tx.relayer_id.clone());
164
165        self.job_producer()
166            .produce_submit_transaction_job(job, None)
167            .await
168            .map_err(|e| {
169                TransactionError::UnexpectedError(format!("Failed to produce submit job: {}", e))
170            })
171    }
172
173    /// Helper method to produce a resubmit transaction job.
174    pub(super) async fn send_transaction_resubmit_job(
175        &self,
176        tx: &TransactionRepoModel,
177    ) -> Result<(), TransactionError> {
178        let job = TransactionSend::resubmit(tx.id.clone(), tx.relayer_id.clone());
179
180        self.job_producer()
181            .produce_submit_transaction_job(job, None)
182            .await
183            .map_err(|e| {
184                TransactionError::UnexpectedError(format!("Failed to produce resubmit job: {}", e))
185            })
186    }
187
188    /// Updates a transaction's status.
189    pub(super) async fn update_transaction_status(
190        &self,
191        tx: TransactionRepoModel,
192        new_status: TransactionStatus,
193    ) -> Result<TransactionRepoModel, TransactionError> {
194        let confirmed_at = if new_status == TransactionStatus::Confirmed {
195            Some(Utc::now().to_rfc3339())
196        } else {
197            None
198        };
199
200        let update_request = TransactionUpdateRequest {
201            status: Some(new_status),
202            confirmed_at,
203            ..Default::default()
204        };
205
206        let updated_tx = self
207            .transaction_repository()
208            .partial_update(tx.id.clone(), update_request)
209            .await?;
210
211        self.send_transaction_update_notification(&updated_tx)
212            .await?;
213        Ok(updated_tx)
214    }
215
216    /// Sends a transaction update notification if a notification ID is configured.
217    pub(super) async fn send_transaction_update_notification(
218        &self,
219        tx: &TransactionRepoModel,
220    ) -> Result<(), TransactionError> {
221        if let Some(notification_id) = &self.relayer().notification_id {
222            self.job_producer()
223                .produce_send_notification_job(
224                    produce_transaction_update_notification_payload(notification_id, tx),
225                    None,
226                )
227                .await
228                .map_err(|e| {
229                    TransactionError::UnexpectedError(format!("Failed to send notification: {}", e))
230                })?;
231        }
232        Ok(())
233    }
234
235    /// Validates that the relayer has sufficient balance for the transaction.
236    ///
237    /// # Arguments
238    ///
239    /// * `total_cost` - The total cost of the transaction (gas + value)
240    ///
241    /// # Returns
242    ///
243    /// A `Result` indicating success or a `TransactionError` if insufficient balance.
244    async fn ensure_sufficient_balance(
245        &self,
246        total_cost: crate::models::U256,
247    ) -> Result<(), TransactionError> {
248        EvmTransactionValidator::validate_sufficient_relayer_balance(
249            total_cost,
250            &self.relayer().address,
251            &self.relayer().policies.get_evm_policy(),
252            &self.provider,
253        )
254        .await
255        .map_err(|validation_error| {
256            TransactionError::InsufficientBalance(validation_error.to_string())
257        })
258    }
259
260    /// Signs a transaction data, updates repository with the signed transaction, and optionally sends a resubmit job.
261    ///
262    /// # Arguments
263    ///
264    /// * `tx_id` - The transaction ID to update
265    /// * `evm_data` - The EVM transaction data to sign
266    /// * `send_resubmit` - Whether to send a resubmit job after updating
267    ///
268    /// # Returns
269    ///
270    /// The updated transaction model
271    async fn sign_update_and_notify(
272        &self,
273        tx_id: String,
274        evm_data: EvmTransactionData,
275        send_resubmit: bool,
276    ) -> Result<TransactionRepoModel, TransactionError> {
277        // Sign the transaction
278        let sig_result = self
279            .signer
280            .sign_transaction(NetworkTransactionData::Evm(evm_data.clone()))
281            .await?;
282
283        let final_evm_data = evm_data.with_signed_transaction_data(sig_result.into_evm()?);
284
285        // Update the transaction in the repository
286        let updated_tx = self
287            .transaction_repository
288            .update_network_data(tx_id, NetworkTransactionData::Evm(final_evm_data))
289            .await?;
290
291        // Send resubmit job if requested
292        if send_resubmit {
293            self.send_transaction_resubmit_job(&updated_tx).await?;
294        }
295
296        // Send notification
297        self.send_transaction_update_notification(&updated_tx)
298            .await?;
299
300        Ok(updated_tx)
301    }
302}
303
304#[async_trait]
305impl<P, R, N, T, J, S, C, PC> Transaction for EvmRelayerTransaction<P, R, N, T, J, S, C, PC>
306where
307    P: EvmProviderTrait + Send + Sync,
308    R: Repository<RelayerRepoModel, String> + Send + Sync,
309    N: NetworkRepository + Send + Sync,
310    T: TransactionRepository + Send + Sync,
311    J: JobProducerTrait + Send + Sync,
312    S: Signer + Send + Sync,
313    C: TransactionCounterTrait + Send + Sync,
314    PC: PriceCalculatorTrait + Send + Sync,
315{
316    /// Prepares a transaction for submission.
317    ///
318    /// # Arguments
319    ///
320    /// * `tx` - The transaction model to prepare.
321    ///
322    /// # Returns
323    ///
324    /// A result containing the updated transaction model or a `TransactionError`.
325    async fn prepare_transaction(
326        &self,
327        tx: TransactionRepoModel,
328    ) -> Result<TransactionRepoModel, TransactionError> {
329        info!("Preparing transaction: {:?}", tx.id);
330
331        let evm_data = tx.network_data.get_evm_transaction_data()?;
332        // set the gas price
333        let relayer = self.relayer();
334        let price_params: PriceParams = self
335            .price_calculator
336            .get_transaction_price_params(&evm_data, relayer)
337            .await?;
338
339        debug!("Gas price: {:?}", price_params.gas_price);
340        // increment the nonce
341        let nonce = self
342            .transaction_counter_service
343            .get_and_increment(&self.relayer.id, &self.relayer.address)
344            .map_err(|e| TransactionError::UnexpectedError(e.to_string()))?;
345
346        let updated_evm_data = tx
347            .network_data
348            .get_evm_transaction_data()?
349            .with_price_params(price_params.clone())
350            .with_nonce(nonce);
351
352        // sign the transaction
353        let sig_result = self
354            .signer
355            .sign_transaction(NetworkTransactionData::Evm(updated_evm_data.clone()))
356            .await?;
357
358        let updated_evm_data =
359            updated_evm_data.with_signed_transaction_data(sig_result.into_evm()?);
360
361        // Validate the relayer has sufficient balance
362        match self
363            .ensure_sufficient_balance(price_params.total_cost)
364            .await
365        {
366            Ok(()) => {}
367            Err(balance_error) => {
368                info!(
369                    "Insufficient balance for transaction {}: {}",
370                    tx.id, balance_error
371                );
372
373                let update = TransactionUpdateRequest {
374                    status: Some(TransactionStatus::Failed),
375                    status_reason: Some(balance_error.to_string()),
376                    ..Default::default()
377                };
378
379                let updated_tx = self
380                    .transaction_repository
381                    .partial_update(tx.id.clone(), update)
382                    .await?;
383
384                let _ = self.send_transaction_update_notification(&updated_tx).await;
385                return Err(balance_error);
386            }
387        }
388
389        // Balance validation passed, continue with normal flow
390        // Track the transaction hash
391        let mut hashes = tx.hashes.clone();
392        if let Some(hash) = updated_evm_data.hash.clone() {
393            hashes.push(hash);
394        }
395
396        let update = TransactionUpdateRequest {
397            status: Some(TransactionStatus::Sent),
398            network_data: Some(NetworkTransactionData::Evm(updated_evm_data)),
399            priced_at: Some(Utc::now().to_rfc3339()),
400            hashes: Some(hashes),
401            ..Default::default()
402        };
403
404        let updated_tx = self
405            .transaction_repository
406            .partial_update(tx.id.clone(), update)
407            .await?;
408
409        // after preparing the transaction, we need to submit it to the job queue
410        self.job_producer
411            .produce_submit_transaction_job(
412                TransactionSend::submit(updated_tx.id.clone(), updated_tx.relayer_id.clone()),
413                None,
414            )
415            .await?;
416
417        self.send_transaction_update_notification(&updated_tx)
418            .await?;
419
420        Ok(updated_tx)
421    }
422
423    /// Submits a transaction for processing.
424    ///
425    /// # Arguments
426    ///
427    /// * `tx` - The transaction model to submit.
428    ///
429    /// # Returns
430    ///
431    /// A result containing the updated transaction model or a `TransactionError`.
432    async fn submit_transaction(
433        &self,
434        tx: TransactionRepoModel,
435    ) -> Result<TransactionRepoModel, TransactionError> {
436        info!("submitting transaction for tx: {:?}", tx.id);
437
438        let evm_tx_data = tx.network_data.get_evm_transaction_data()?;
439        let raw_tx = evm_tx_data.raw.as_ref().ok_or_else(|| {
440            TransactionError::InvalidType("Raw transaction data is missing".to_string())
441        })?;
442
443        self.provider.send_raw_transaction(raw_tx).await?;
444
445        let update = TransactionUpdateRequest {
446            status: Some(TransactionStatus::Submitted),
447            sent_at: Some(Utc::now().to_rfc3339()),
448            ..Default::default()
449        };
450
451        let updated_tx = self
452            .transaction_repository
453            .partial_update(tx.id.clone(), update)
454            .await?;
455
456        // Schedule status check
457        self.job_producer
458            .produce_check_transaction_status_job(
459                TransactionStatusCheck::new(updated_tx.id.clone(), updated_tx.relayer_id.clone()),
460                None,
461            )
462            .await?;
463
464        self.send_transaction_update_notification(&updated_tx)
465            .await?;
466
467        Ok(updated_tx)
468    }
469
470    /// Handles the status of a transaction.
471    ///
472    /// # Arguments
473    ///
474    /// * `tx` - The transaction model to handle.
475    ///
476    /// # Returns
477    ///
478    /// A result containing the updated transaction model or a `TransactionError`.
479    async fn handle_transaction_status(
480        &self,
481        tx: TransactionRepoModel,
482    ) -> Result<TransactionRepoModel, TransactionError> {
483        self.handle_status_impl(tx).await
484    }
485    /// Resubmits a transaction with updated parameters.
486    ///
487    /// # Arguments
488    ///
489    /// * `tx` - The transaction model to resubmit.
490    ///
491    /// # Returns
492    ///
493    /// A result containing the resubmitted transaction model or a `TransactionError`.
494    async fn resubmit_transaction(
495        &self,
496        tx: TransactionRepoModel,
497    ) -> Result<TransactionRepoModel, TransactionError> {
498        info!("Resubmitting transaction: {:?}", tx.id);
499
500        // Calculate bumped gas price
501        let bumped_price_params = self
502            .price_calculator
503            .calculate_bumped_gas_price(
504                &tx.network_data.get_evm_transaction_data()?,
505                self.relayer(),
506            )
507            .await?;
508
509        if !bumped_price_params.is_min_bumped.is_some_and(|b| b) {
510            warn!(
511                "Bumped gas price does not meet minimum requirement, skipping resubmission: {:?}",
512                bumped_price_params
513            );
514            return Ok(tx);
515        }
516
517        // Get transaction data
518        let evm_data = tx.network_data.get_evm_transaction_data()?;
519
520        // Create new transaction data with bumped gas price
521        let updated_evm_data = evm_data.with_price_params(bumped_price_params.clone());
522
523        // Sign the transaction
524        let sig_result = self
525            .signer
526            .sign_transaction(NetworkTransactionData::Evm(updated_evm_data.clone()))
527            .await?;
528
529        let final_evm_data = updated_evm_data.with_signed_transaction_data(sig_result.into_evm()?);
530
531        // Validate the relayer has sufficient balance
532        self.ensure_sufficient_balance(bumped_price_params.total_cost)
533            .await?;
534
535        // sign, update and notify
536        let updated_tx = self
537            .sign_update_and_notify(
538                tx.id.clone(),
539                final_evm_data,
540                true, // send_resubmit = true
541            )
542            .await?;
543
544        Ok(updated_tx)
545    }
546
547    /// Cancels a transaction.
548    ///
549    /// # Arguments
550    ///
551    /// * `tx` - The transaction model to cancel.
552    ///
553    /// # Returns
554    ///
555    /// A result containing the transaction model or a `TransactionError`.
556    async fn cancel_transaction(
557        &self,
558        tx: TransactionRepoModel,
559    ) -> Result<TransactionRepoModel, TransactionError> {
560        info!("Cancelling transaction: {:?}", tx.id);
561        info!("Transaction status: {:?}", tx.status);
562        // Check if the transaction can be cancelled
563        if !is_pending_transaction(&tx.status) {
564            return Err(TransactionError::ValidationError(format!(
565                "Cannot cancel transaction with status: {:?}",
566                tx.status
567            )));
568        }
569
570        // If the transaction is in Pending state, we can just update its status
571        if tx.status == TransactionStatus::Pending {
572            info!("Transaction is in Pending state, updating status to Canceled");
573            return self
574                .update_transaction_status(tx, TransactionStatus::Canceled)
575                .await;
576        }
577
578        let update = self.prepare_noop_update_request(&tx, true).await?;
579        let updated_tx = self
580            .transaction_repository()
581            .partial_update(tx.id.clone(), update)
582            .await?;
583
584        // Submit the updated transaction to the network using the resubmit job
585        self.send_transaction_resubmit_job(&updated_tx).await?;
586
587        // Send notification for the updated transaction
588        self.send_transaction_update_notification(&updated_tx)
589            .await?;
590
591        info!(
592            "Original transaction updated with cancellation data: {:?}",
593            updated_tx.id
594        );
595        Ok(updated_tx)
596    }
597
598    /// Replaces a transaction with a new one.
599    ///
600    /// # Arguments
601    ///
602    /// * `old_tx` - The transaction model to replace.
603    /// * `new_tx_request` - The new transaction request data.
604    ///
605    /// # Returns
606    ///
607    /// A result containing the updated transaction model or a `TransactionError`.
608    async fn replace_transaction(
609        &self,
610        old_tx: TransactionRepoModel,
611        new_tx_request: NetworkTransactionRequest,
612    ) -> Result<TransactionRepoModel, TransactionError> {
613        info!("Replacing transaction: {:?}", old_tx.id);
614
615        // Check if the transaction can be replaced
616        if !is_pending_transaction(&old_tx.status) {
617            return Err(TransactionError::ValidationError(format!(
618                "Cannot replace transaction with status: {:?}",
619                old_tx.status
620            )));
621        }
622
623        // Extract EVM data from both old transaction and new request
624        let old_evm_data = old_tx.network_data.get_evm_transaction_data()?;
625        let new_evm_request = match new_tx_request {
626            NetworkTransactionRequest::Evm(evm_req) => evm_req,
627            _ => {
628                return Err(TransactionError::InvalidType(
629                    "New transaction request must be EVM type".to_string(),
630                ))
631            }
632        };
633
634        let network_repo_model = self
635            .network_repository()
636            .get_by_chain_id(NetworkType::Evm, old_evm_data.chain_id)
637            .await
638            .map_err(|e| {
639                TransactionError::NetworkConfiguration(format!(
640                    "Failed to get network by chain_id {}: {}",
641                    old_evm_data.chain_id, e
642                ))
643            })?
644            .ok_or_else(|| {
645                TransactionError::NetworkConfiguration(format!(
646                    "Network with chain_id {} not found",
647                    old_evm_data.chain_id
648                ))
649            })?;
650
651        let network = EvmNetwork::try_from(network_repo_model).map_err(|e| {
652            TransactionError::NetworkConfiguration(format!(
653                "Failed to convert network model: {}",
654                e
655            ))
656        })?;
657
658        // First, create updated EVM data without price parameters
659        let updated_evm_data = EvmTransactionData::for_replacement(&old_evm_data, &new_evm_request);
660
661        // Then determine pricing strategy and calculate price parameters using the updated data
662        let price_params = super::replacement::determine_replacement_pricing(
663            &old_evm_data,
664            &updated_evm_data,
665            self.relayer(),
666            &self.price_calculator,
667            network.lacks_mempool(),
668        )
669        .await?;
670
671        info!("Replacement price params: {:?}", price_params);
672
673        // Apply the calculated price parameters to the updated EVM data
674        let final_evm_data = updated_evm_data.with_price_params(price_params.clone());
675
676        // Validate the relayer has sufficient balance
677        self.ensure_sufficient_balance(price_params.total_cost)
678            .await?;
679
680        // sign, update and notify
681        let updated_tx = self
682            .sign_update_and_notify(
683                old_tx.id.clone(),
684                final_evm_data,
685                true, // send_resubmit = true
686            )
687            .await?;
688
689        Ok(updated_tx)
690    }
691
692    /// Signs a transaction.
693    ///
694    /// # Arguments
695    ///
696    /// * `tx` - The transaction model to sign.
697    ///
698    /// # Returns
699    ///
700    /// A result containing the transaction model or a `TransactionError`.
701    async fn sign_transaction(
702        &self,
703        tx: TransactionRepoModel,
704    ) -> Result<TransactionRepoModel, TransactionError> {
705        Ok(tx)
706    }
707
708    /// Validates a transaction.
709    ///
710    /// # Arguments
711    ///
712    /// * `_tx` - The transaction model to validate.
713    ///
714    /// # Returns
715    ///
716    /// A result containing a boolean indicating validity or a `TransactionError`.
717    async fn validate_transaction(
718        &self,
719        _tx: TransactionRepoModel,
720    ) -> Result<bool, TransactionError> {
721        Ok(true)
722    }
723}
724// P: EvmProviderTrait,
725// R: Repository<RelayerRepoModel, String>,
726// T: TransactionRepository,
727// J: JobProducerTrait,
728// S: Signer,
729// C: TransactionCounterTrait,
730// PC: PriceCalculatorTrait,
731// we define concrete type for the evm transaction
732pub type DefaultEvmTransaction = EvmRelayerTransaction<
733    EvmProvider,
734    RelayerRepositoryStorage<InMemoryRelayerRepository>,
735    InMemoryNetworkRepository,
736    crate::repositories::transaction::InMemoryTransactionRepository,
737    JobProducer,
738    EvmSigner,
739    InMemoryTransactionCounter,
740    PriceCalculator<EvmGasPriceService<EvmProvider>>,
741>;
742#[cfg(test)]
743mod tests {
744
745    use super::*;
746    use crate::{
747        domain::evm::price_calculator::PriceParams,
748        jobs::MockJobProducerTrait,
749        models::{
750            evm::Speed, EvmTransactionData, EvmTransactionRequest, NetworkType,
751            RelayerNetworkPolicy, U256,
752        },
753        repositories::{
754            MockNetworkRepository, MockRepository, MockTransactionCounterTrait,
755            MockTransactionRepository,
756        },
757        services::{MockEvmProviderTrait, MockSigner},
758    };
759    use chrono::Utc;
760    use futures::future::ready;
761    use mockall::{mock, predicate::*};
762
763    // Create a mock for PriceCalculatorTrait
764    mock! {
765        pub PriceCalculator {}
766        #[async_trait]
767        impl PriceCalculatorTrait for PriceCalculator {
768            async fn get_transaction_price_params(
769                &self,
770                tx_data: &EvmTransactionData,
771                relayer: &RelayerRepoModel
772            ) -> Result<PriceParams, TransactionError>;
773
774            async fn calculate_bumped_gas_price(
775                &self,
776                tx: &EvmTransactionData,
777                relayer: &RelayerRepoModel,
778            ) -> Result<PriceParams, TransactionError>;
779        }
780    }
781
782    // Helper to create a relayer model with specific configuration for these tests
783    fn create_test_relayer() -> RelayerRepoModel {
784        RelayerRepoModel {
785            id: "test-relayer-id".to_string(),
786            name: "Test Relayer".to_string(),
787            network: "1".to_string(), // Ethereum Mainnet
788            address: "0xSender".to_string(),
789            paused: false,
790            system_disabled: false,
791            signer_id: "test-signer-id".to_string(),
792            notification_id: Some("test-notification-id".to_string()),
793            policies: RelayerNetworkPolicy::Evm(crate::models::RelayerEvmPolicy {
794                min_balance: 100000000000000000u128, // 0.1 ETH
795                whitelist_receivers: Some(vec!["0xRecipient".to_string()]),
796                gas_price_cap: Some(100000000000), // 100 Gwei
797                eip1559_pricing: Some(false),
798                private_transactions: false,
799            }),
800            network_type: NetworkType::Evm,
801            custom_rpc_urls: None,
802        }
803    }
804
805    // Helper to create test transaction with specific configuration for these tests
806    fn create_test_transaction() -> TransactionRepoModel {
807        TransactionRepoModel {
808            id: "test-tx-id".to_string(),
809            relayer_id: "test-relayer-id".to_string(),
810            status: TransactionStatus::Pending,
811            status_reason: None,
812            created_at: Utc::now().to_rfc3339(),
813            sent_at: None,
814            confirmed_at: None,
815            valid_until: None,
816            network_type: NetworkType::Evm,
817            network_data: NetworkTransactionData::Evm(EvmTransactionData {
818                chain_id: 1,
819                from: "0xSender".to_string(),
820                to: Some("0xRecipient".to_string()),
821                value: U256::from(1000000000000000000u64), // 1 ETH
822                data: Some("0xData".to_string()),
823                gas_limit: 21000,
824                gas_price: Some(20000000000), // 20 Gwei
825                max_fee_per_gas: None,
826                max_priority_fee_per_gas: None,
827                nonce: None,
828                signature: None,
829                hash: None,
830                speed: Some(Speed::Fast),
831                raw: None,
832            }),
833            priced_at: None,
834            hashes: Vec::new(),
835            noop_count: None,
836            is_canceled: Some(false),
837        }
838    }
839
840    #[tokio::test]
841    async fn test_prepare_transaction_with_sufficient_balance() {
842        let mut mock_transaction = MockTransactionRepository::new();
843        let mock_relayer = MockRepository::<RelayerRepoModel, String>::new();
844        let mut mock_provider = MockEvmProviderTrait::new();
845        let mut mock_signer = MockSigner::new();
846        let mut mock_job_producer = MockJobProducerTrait::new();
847        let mut mock_price_calculator = MockPriceCalculator::new();
848        let mut counter_service = MockTransactionCounterTrait::new();
849
850        let relayer = create_test_relayer();
851        let test_tx = create_test_transaction();
852
853        counter_service
854            .expect_get_and_increment()
855            .returning(|_, _| Ok(42));
856
857        let price_params = PriceParams {
858            gas_price: Some(30000000000),
859            max_fee_per_gas: None,
860            max_priority_fee_per_gas: None,
861            is_min_bumped: None,
862            extra_fee: None,
863            total_cost: U256::from(630000000000000u64),
864        };
865        mock_price_calculator
866            .expect_get_transaction_price_params()
867            .returning(move |_, _| Ok(price_params.clone()));
868
869        mock_signer.expect_sign_transaction().returning(|_| {
870            Box::pin(ready(Ok(
871                crate::domain::relayer::SignTransactionResponse::Evm(
872                    crate::domain::relayer::SignTransactionResponseEvm {
873                        hash: "0xtx_hash".to_string(),
874                        signature: crate::models::EvmTransactionDataSignature {
875                            r: "r".to_string(),
876                            s: "s".to_string(),
877                            v: 1,
878                            sig: "0xsignature".to_string(),
879                        },
880                        raw: vec![1, 2, 3],
881                    },
882                ),
883            )))
884        });
885
886        mock_provider
887            .expect_get_balance()
888            .with(eq("0xSender"))
889            .returning(|_| Box::pin(ready(Ok(U256::from(1000000000000000000u64)))));
890
891        let test_tx_clone = test_tx.clone();
892        mock_transaction
893            .expect_partial_update()
894            .returning(move |_, update| {
895                let mut updated_tx = test_tx_clone.clone();
896                if let Some(status) = &update.status {
897                    updated_tx.status = status.clone();
898                }
899                if let Some(network_data) = &update.network_data {
900                    updated_tx.network_data = network_data.clone();
901                }
902                if let Some(hashes) = &update.hashes {
903                    updated_tx.hashes = hashes.clone();
904                }
905                Ok(updated_tx)
906            });
907
908        mock_job_producer
909            .expect_produce_submit_transaction_job()
910            .returning(|_, _| Box::pin(ready(Ok(()))));
911        mock_job_producer
912            .expect_produce_send_notification_job()
913            .returning(|_, _| Box::pin(ready(Ok(()))));
914
915        let mock_network = MockNetworkRepository::new();
916
917        let evm_transaction = EvmRelayerTransaction {
918            relayer: relayer.clone(),
919            provider: mock_provider,
920            relayer_repository: Arc::new(mock_relayer),
921            network_repository: Arc::new(mock_network),
922            transaction_repository: Arc::new(mock_transaction),
923            transaction_counter_service: Arc::new(counter_service),
924            job_producer: Arc::new(mock_job_producer),
925            price_calculator: mock_price_calculator,
926            signer: mock_signer,
927        };
928
929        let result = evm_transaction.prepare_transaction(test_tx.clone()).await;
930        assert!(result.is_ok());
931        let prepared_tx = result.unwrap();
932        assert_eq!(prepared_tx.status, TransactionStatus::Sent);
933        assert!(!prepared_tx.hashes.is_empty());
934    }
935
936    #[tokio::test]
937    async fn test_prepare_transaction_with_insufficient_balance() {
938        let mut mock_transaction = MockTransactionRepository::new();
939        let mock_relayer = MockRepository::<RelayerRepoModel, String>::new();
940        let mut mock_provider = MockEvmProviderTrait::new();
941        let mut mock_signer = MockSigner::new();
942        let mut mock_job_producer = MockJobProducerTrait::new();
943        let mut mock_price_calculator = MockPriceCalculator::new();
944        let mut counter_service = MockTransactionCounterTrait::new();
945
946        let relayer = create_test_relayer();
947        let test_tx = create_test_transaction();
948
949        counter_service
950            .expect_get_and_increment()
951            .returning(|_, _| Ok(42));
952
953        let price_params = PriceParams {
954            gas_price: Some(30000000000),
955            max_fee_per_gas: None,
956            max_priority_fee_per_gas: None,
957            is_min_bumped: None,
958            extra_fee: None,
959            total_cost: U256::from(630000000000000u64),
960        };
961        mock_price_calculator
962            .expect_get_transaction_price_params()
963            .returning(move |_, _| Ok(price_params.clone()));
964
965        mock_signer.expect_sign_transaction().returning(|_| {
966            Box::pin(ready(Ok(
967                crate::domain::relayer::SignTransactionResponse::Evm(
968                    crate::domain::relayer::SignTransactionResponseEvm {
969                        hash: "0xtx_hash".to_string(),
970                        signature: crate::models::EvmTransactionDataSignature {
971                            r: "r".to_string(),
972                            s: "s".to_string(),
973                            v: 1,
974                            sig: "0xsignature".to_string(),
975                        },
976                        raw: vec![1, 2, 3],
977                    },
978                ),
979            )))
980        });
981
982        mock_provider
983            .expect_get_balance()
984            .with(eq("0xSender"))
985            .returning(|_| Box::pin(ready(Ok(U256::from(90000000000000000u64)))));
986
987        let test_tx_clone = test_tx.clone();
988        mock_transaction
989            .expect_partial_update()
990            .withf(move |id, update| {
991                id == "test-tx-id" && update.status == Some(TransactionStatus::Failed)
992            })
993            .returning(move |_, update| {
994                let mut updated_tx = test_tx_clone.clone();
995                updated_tx.status = update.status.unwrap_or(updated_tx.status);
996                Ok(updated_tx)
997            });
998
999        mock_job_producer
1000            .expect_produce_send_notification_job()
1001            .returning(|_, _| Box::pin(ready(Ok(()))));
1002
1003        let mock_network = MockNetworkRepository::new();
1004
1005        let evm_transaction = EvmRelayerTransaction {
1006            relayer: relayer.clone(),
1007            provider: mock_provider,
1008            relayer_repository: Arc::new(mock_relayer),
1009            network_repository: Arc::new(mock_network),
1010            transaction_repository: Arc::new(mock_transaction),
1011            transaction_counter_service: Arc::new(counter_service),
1012            job_producer: Arc::new(mock_job_producer),
1013            price_calculator: mock_price_calculator,
1014            signer: mock_signer,
1015        };
1016
1017        let result = evm_transaction.prepare_transaction(test_tx.clone()).await;
1018        assert!(
1019            matches!(result, Err(TransactionError::InsufficientBalance(_))),
1020            "Expected InsufficientBalance error, got: {:?}",
1021            result
1022        );
1023    }
1024
1025    #[tokio::test]
1026    async fn test_cancel_transaction() {
1027        // Test Case 1: Canceling a pending transaction
1028        {
1029            // Create mocks for all dependencies
1030            let mut mock_transaction = MockTransactionRepository::new();
1031            let mock_relayer = MockRepository::<RelayerRepoModel, String>::new();
1032            let mock_provider = MockEvmProviderTrait::new();
1033            let mock_signer = MockSigner::new();
1034            let mut mock_job_producer = MockJobProducerTrait::new();
1035            let mock_price_calculator = MockPriceCalculator::new();
1036            let counter_service = MockTransactionCounterTrait::new();
1037
1038            // Create test relayer and pending transaction
1039            let relayer = create_test_relayer();
1040            let mut test_tx = create_test_transaction();
1041            test_tx.status = TransactionStatus::Pending;
1042
1043            // Transaction repository should update the transaction with Canceled status
1044            let test_tx_clone = test_tx.clone();
1045            mock_transaction
1046                .expect_partial_update()
1047                .withf(move |id, update| {
1048                    id == "test-tx-id" && update.status == Some(TransactionStatus::Canceled)
1049                })
1050                .returning(move |_, update| {
1051                    let mut updated_tx = test_tx_clone.clone();
1052                    updated_tx.status = update.status.unwrap_or(updated_tx.status);
1053                    Ok(updated_tx)
1054                });
1055
1056            // Job producer should send notification
1057            mock_job_producer
1058                .expect_produce_send_notification_job()
1059                .returning(|_, _| Box::pin(ready(Ok(()))));
1060
1061            let mock_network = MockNetworkRepository::new();
1062
1063            // Set up EVM transaction with the mocks
1064            let evm_transaction = EvmRelayerTransaction {
1065                relayer: relayer.clone(),
1066                provider: mock_provider,
1067                relayer_repository: Arc::new(mock_relayer),
1068                network_repository: Arc::new(mock_network),
1069                transaction_repository: Arc::new(mock_transaction),
1070                transaction_counter_service: Arc::new(counter_service),
1071                job_producer: Arc::new(mock_job_producer),
1072                price_calculator: mock_price_calculator,
1073                signer: mock_signer,
1074            };
1075
1076            // Call cancel_transaction and verify it succeeds
1077            let result = evm_transaction.cancel_transaction(test_tx.clone()).await;
1078            assert!(result.is_ok());
1079            let cancelled_tx = result.unwrap();
1080            assert_eq!(cancelled_tx.id, "test-tx-id");
1081            assert_eq!(cancelled_tx.status, TransactionStatus::Canceled);
1082        }
1083
1084        // Test Case 2: Canceling a submitted transaction
1085        {
1086            // Create mocks for all dependencies
1087            let mut mock_transaction = MockTransactionRepository::new();
1088            let mock_relayer = MockRepository::<RelayerRepoModel, String>::new();
1089            let mock_provider = MockEvmProviderTrait::new();
1090            let mut mock_signer = MockSigner::new();
1091            let mut mock_job_producer = MockJobProducerTrait::new();
1092            let mut mock_price_calculator = MockPriceCalculator::new();
1093            let counter_service = MockTransactionCounterTrait::new();
1094
1095            // Create test relayer and submitted transaction
1096            let relayer = create_test_relayer();
1097            let mut test_tx = create_test_transaction();
1098            test_tx.status = TransactionStatus::Submitted;
1099            test_tx.sent_at = Some(Utc::now().to_rfc3339());
1100            test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
1101                nonce: Some(42),
1102                hash: Some("0xoriginal_hash".to_string()),
1103                ..test_tx.network_data.get_evm_transaction_data().unwrap()
1104            });
1105
1106            // Set up price calculator expectations for cancellation tx
1107            mock_price_calculator
1108                .expect_get_transaction_price_params()
1109                .return_once(move |_, _| {
1110                    Ok(PriceParams {
1111                        gas_price: Some(40000000000), // 40 Gwei (higher than original)
1112                        max_fee_per_gas: None,
1113                        max_priority_fee_per_gas: None,
1114                        is_min_bumped: Some(true),
1115                        extra_fee: Some(0),
1116                        total_cost: U256::ZERO,
1117                    })
1118                });
1119
1120            // Signer should be called to sign the cancellation transaction
1121            mock_signer.expect_sign_transaction().returning(|_| {
1122                Box::pin(ready(Ok(
1123                    crate::domain::relayer::SignTransactionResponse::Evm(
1124                        crate::domain::relayer::SignTransactionResponseEvm {
1125                            hash: "0xcancellation_hash".to_string(),
1126                            signature: crate::models::EvmTransactionDataSignature {
1127                                r: "r".to_string(),
1128                                s: "s".to_string(),
1129                                v: 1,
1130                                sig: "0xsignature".to_string(),
1131                            },
1132                            raw: vec![1, 2, 3],
1133                        },
1134                    ),
1135                )))
1136            });
1137
1138            // Transaction repository should update the transaction
1139            let test_tx_clone = test_tx.clone();
1140            mock_transaction
1141                .expect_partial_update()
1142                .returning(move |tx_id, update| {
1143                    let mut updated_tx = test_tx_clone.clone();
1144                    updated_tx.id = tx_id;
1145                    updated_tx.status = update.status.unwrap_or(updated_tx.status);
1146                    updated_tx.network_data =
1147                        update.network_data.unwrap_or(updated_tx.network_data);
1148                    if let Some(hashes) = update.hashes {
1149                        updated_tx.hashes = hashes;
1150                    }
1151                    Ok(updated_tx)
1152                });
1153
1154            // Job producer expectations
1155            mock_job_producer
1156                .expect_produce_submit_transaction_job()
1157                .returning(|_, _| Box::pin(ready(Ok(()))));
1158            mock_job_producer
1159                .expect_produce_send_notification_job()
1160                .returning(|_, _| Box::pin(ready(Ok(()))));
1161
1162            let mock_network = MockNetworkRepository::new();
1163
1164            // Set up EVM transaction with the mocks
1165            let evm_transaction = EvmRelayerTransaction {
1166                relayer: relayer.clone(),
1167                provider: mock_provider,
1168                relayer_repository: Arc::new(mock_relayer),
1169                network_repository: Arc::new(mock_network),
1170                transaction_repository: Arc::new(mock_transaction),
1171                transaction_counter_service: Arc::new(counter_service),
1172                job_producer: Arc::new(mock_job_producer),
1173                price_calculator: mock_price_calculator,
1174                signer: mock_signer,
1175            };
1176
1177            // Call cancel_transaction and verify it succeeds
1178            let result = evm_transaction.cancel_transaction(test_tx.clone()).await;
1179            assert!(result.is_ok());
1180            let cancelled_tx = result.unwrap();
1181
1182            // Verify the cancellation transaction was properly created
1183            assert_eq!(cancelled_tx.id, "test-tx-id");
1184            assert_eq!(cancelled_tx.status, TransactionStatus::Submitted);
1185
1186            // Verify the network data was properly updated
1187            if let NetworkTransactionData::Evm(evm_data) = &cancelled_tx.network_data {
1188                assert_eq!(evm_data.nonce, Some(42)); // Same nonce as original
1189            } else {
1190                panic!("Expected EVM transaction data");
1191            }
1192        }
1193
1194        // Test Case 3: Attempting to cancel a confirmed transaction (should fail)
1195        {
1196            // Create minimal mocks for failure case
1197            let mock_transaction = MockTransactionRepository::new();
1198            let mock_relayer = MockRepository::<RelayerRepoModel, String>::new();
1199            let mock_provider = MockEvmProviderTrait::new();
1200            let mock_signer = MockSigner::new();
1201            let mock_job_producer = MockJobProducerTrait::new();
1202            let mock_price_calculator = MockPriceCalculator::new();
1203            let counter_service = MockTransactionCounterTrait::new();
1204
1205            // Create test relayer and confirmed transaction
1206            let relayer = create_test_relayer();
1207            let mut test_tx = create_test_transaction();
1208            test_tx.status = TransactionStatus::Confirmed;
1209
1210            let mock_network = MockNetworkRepository::new();
1211
1212            // Set up EVM transaction with the mocks
1213            let evm_transaction = EvmRelayerTransaction {
1214                relayer: relayer.clone(),
1215                provider: mock_provider,
1216                relayer_repository: Arc::new(mock_relayer),
1217                network_repository: Arc::new(mock_network),
1218                transaction_repository: Arc::new(mock_transaction),
1219                transaction_counter_service: Arc::new(counter_service),
1220                job_producer: Arc::new(mock_job_producer),
1221                price_calculator: mock_price_calculator,
1222                signer: mock_signer,
1223            };
1224
1225            // Call cancel_transaction and verify it fails
1226            let result = evm_transaction.cancel_transaction(test_tx.clone()).await;
1227            assert!(result.is_err());
1228            if let Err(TransactionError::ValidationError(msg)) = result {
1229                assert!(msg.contains("Cannot cancel transaction with status"));
1230            } else {
1231                panic!("Expected ValidationError");
1232            }
1233        }
1234    }
1235
1236    #[tokio::test]
1237    async fn test_replace_transaction() {
1238        // Test Case: Replacing a submitted transaction with new gas price
1239        {
1240            // Create mocks for all dependencies
1241            let mut mock_transaction = MockTransactionRepository::new();
1242            let mock_relayer = MockRepository::<RelayerRepoModel, String>::new();
1243            let mut mock_provider = MockEvmProviderTrait::new();
1244            let mut mock_signer = MockSigner::new();
1245            let mut mock_job_producer = MockJobProducerTrait::new();
1246            let mut mock_price_calculator = MockPriceCalculator::new();
1247            let counter_service = MockTransactionCounterTrait::new();
1248
1249            // Create test relayer and submitted transaction
1250            let relayer = create_test_relayer();
1251            let mut test_tx = create_test_transaction();
1252            test_tx.status = TransactionStatus::Submitted;
1253            test_tx.sent_at = Some(Utc::now().to_rfc3339());
1254
1255            // Set up price calculator expectations for replacement
1256            mock_price_calculator
1257                .expect_get_transaction_price_params()
1258                .return_once(move |_, _| {
1259                    Ok(PriceParams {
1260                        gas_price: Some(40000000000), // 40 Gwei (higher than original)
1261                        max_fee_per_gas: None,
1262                        max_priority_fee_per_gas: None,
1263                        is_min_bumped: Some(true),
1264                        extra_fee: Some(0),
1265                        total_cost: U256::from(2001000000000000000u64), // 2 ETH + gas costs
1266                    })
1267                });
1268
1269            // Signer should be called to sign the replacement transaction
1270            mock_signer.expect_sign_transaction().returning(|_| {
1271                Box::pin(ready(Ok(
1272                    crate::domain::relayer::SignTransactionResponse::Evm(
1273                        crate::domain::relayer::SignTransactionResponseEvm {
1274                            hash: "0xreplacement_hash".to_string(),
1275                            signature: crate::models::EvmTransactionDataSignature {
1276                                r: "r".to_string(),
1277                                s: "s".to_string(),
1278                                v: 1,
1279                                sig: "0xsignature".to_string(),
1280                            },
1281                            raw: vec![1, 2, 3],
1282                        },
1283                    ),
1284                )))
1285            });
1286
1287            // Provider balance check should pass
1288            mock_provider
1289                .expect_get_balance()
1290                .with(eq("0xSender"))
1291                .returning(|_| Box::pin(ready(Ok(U256::from(3000000000000000000u64)))));
1292
1293            // Transaction repository should update using update_network_data
1294            let test_tx_clone = test_tx.clone();
1295            mock_transaction
1296                .expect_update_network_data()
1297                .returning(move |tx_id, network_data| {
1298                    let mut updated_tx = test_tx_clone.clone();
1299                    updated_tx.id = tx_id;
1300                    updated_tx.network_data = network_data;
1301                    Ok(updated_tx)
1302                });
1303
1304            // Job producer expectations
1305            mock_job_producer
1306                .expect_produce_submit_transaction_job()
1307                .returning(|_, _| Box::pin(ready(Ok(()))));
1308            mock_job_producer
1309                .expect_produce_send_notification_job()
1310                .returning(|_, _| Box::pin(ready(Ok(()))));
1311
1312            // Network repository expectations for mempool check
1313            let mut mock_network = MockNetworkRepository::new();
1314            mock_network
1315                .expect_get_by_chain_id()
1316                .with(eq(NetworkType::Evm), eq(1))
1317                .returning(|_, _| {
1318                    use crate::config::{EvmNetworkConfig, NetworkConfigCommon};
1319                    use crate::models::{NetworkConfigData, NetworkRepoModel};
1320
1321                    let config = EvmNetworkConfig {
1322                        common: NetworkConfigCommon {
1323                            network: "mainnet".to_string(),
1324                            from: None,
1325                            rpc_urls: Some(vec!["https://rpc.example.com".to_string()]),
1326                            explorer_urls: None,
1327                            average_blocktime_ms: Some(12000),
1328                            is_testnet: Some(false),
1329                            tags: Some(vec!["mainnet".to_string()]), // No "no-mempool" tag
1330                        },
1331                        chain_id: Some(1),
1332                        required_confirmations: Some(12),
1333                        features: Some(vec!["eip1559".to_string()]),
1334                        symbol: Some("ETH".to_string()),
1335                    };
1336                    Ok(Some(NetworkRepoModel {
1337                        id: "evm:mainnet".to_string(),
1338                        name: "mainnet".to_string(),
1339                        network_type: NetworkType::Evm,
1340                        config: NetworkConfigData::Evm(config),
1341                    }))
1342                });
1343
1344            // Set up EVM transaction with the mocks
1345            let evm_transaction = EvmRelayerTransaction {
1346                relayer: relayer.clone(),
1347                provider: mock_provider,
1348                relayer_repository: Arc::new(mock_relayer),
1349                network_repository: Arc::new(mock_network),
1350                transaction_repository: Arc::new(mock_transaction),
1351                transaction_counter_service: Arc::new(counter_service),
1352                job_producer: Arc::new(mock_job_producer),
1353                price_calculator: mock_price_calculator,
1354                signer: mock_signer,
1355            };
1356
1357            // Create replacement request with speed-based pricing
1358            let replacement_request = NetworkTransactionRequest::Evm(EvmTransactionRequest {
1359                to: Some("0xNewRecipient".to_string()),
1360                value: U256::from(2000000000000000000u64), // 2 ETH
1361                data: Some("0xNewData".to_string()),
1362                gas_limit: 25000,
1363                gas_price: None, // Use speed-based pricing
1364                max_fee_per_gas: None,
1365                max_priority_fee_per_gas: None,
1366                speed: Some(Speed::Fast),
1367                valid_until: None,
1368            });
1369
1370            // Call replace_transaction and verify it succeeds
1371            let result = evm_transaction
1372                .replace_transaction(test_tx.clone(), replacement_request)
1373                .await;
1374            if let Err(ref e) = result {
1375                eprintln!("Replace transaction failed with error: {:?}", e);
1376            }
1377            assert!(result.is_ok());
1378            let replaced_tx = result.unwrap();
1379
1380            // Verify the replacement was properly processed
1381            assert_eq!(replaced_tx.id, "test-tx-id");
1382
1383            // Verify the network data was properly updated
1384            if let NetworkTransactionData::Evm(evm_data) = &replaced_tx.network_data {
1385                assert_eq!(evm_data.to, Some("0xNewRecipient".to_string()));
1386                assert_eq!(evm_data.value, U256::from(2000000000000000000u64));
1387                assert_eq!(evm_data.gas_price, Some(40000000000));
1388                assert_eq!(evm_data.gas_limit, 25000);
1389                assert!(evm_data.hash.is_some());
1390                assert!(evm_data.raw.is_some());
1391            } else {
1392                panic!("Expected EVM transaction data");
1393            }
1394        }
1395
1396        // Test Case: Attempting to replace a confirmed transaction (should fail)
1397        {
1398            // Create minimal mocks for failure case
1399            let mock_transaction = MockTransactionRepository::new();
1400            let mock_relayer = MockRepository::<RelayerRepoModel, String>::new();
1401            let mock_provider = MockEvmProviderTrait::new();
1402            let mock_signer = MockSigner::new();
1403            let mock_job_producer = MockJobProducerTrait::new();
1404            let mock_price_calculator = MockPriceCalculator::new();
1405            let counter_service = MockTransactionCounterTrait::new();
1406
1407            // Create test relayer and confirmed transaction
1408            let relayer = create_test_relayer();
1409            let mut test_tx = create_test_transaction();
1410            test_tx.status = TransactionStatus::Confirmed;
1411
1412            let mock_network = MockNetworkRepository::new();
1413
1414            // Set up EVM transaction with the mocks
1415            let evm_transaction = EvmRelayerTransaction {
1416                relayer: relayer.clone(),
1417                provider: mock_provider,
1418                relayer_repository: Arc::new(mock_relayer),
1419                network_repository: Arc::new(mock_network),
1420                transaction_repository: Arc::new(mock_transaction),
1421                transaction_counter_service: Arc::new(counter_service),
1422                job_producer: Arc::new(mock_job_producer),
1423                price_calculator: mock_price_calculator,
1424                signer: mock_signer,
1425            };
1426
1427            // Create dummy replacement request
1428            let replacement_request = NetworkTransactionRequest::Evm(EvmTransactionRequest {
1429                to: Some("0xNewRecipient".to_string()),
1430                value: U256::from(1000000000000000000u64),
1431                data: Some("0xData".to_string()),
1432                gas_limit: 21000,
1433                gas_price: Some(30000000000),
1434                max_fee_per_gas: None,
1435                max_priority_fee_per_gas: None,
1436                speed: Some(Speed::Fast),
1437                valid_until: None,
1438            });
1439
1440            // Call replace_transaction and verify it fails
1441            let result = evm_transaction
1442                .replace_transaction(test_tx.clone(), replacement_request)
1443                .await;
1444            assert!(result.is_err());
1445            if let Err(TransactionError::ValidationError(msg)) = result {
1446                assert!(msg.contains("Cannot replace transaction with status"));
1447            } else {
1448                panic!("Expected ValidationError");
1449            }
1450        }
1451    }
1452}