1use 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 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 pub fn provider(&self) -> &P {
117 &self.provider
118 }
119
120 pub fn relayer(&self) -> &RelayerRepoModel {
122 &self.relayer
123 }
124
125 pub fn network_repository(&self) -> &NR {
127 &self.network_repository
128 }
129
130 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 async fn handle_transaction_status(
494 &self,
495 tx: TransactionRepoModel,
496 ) -> Result<TransactionRepoModel, TransactionError> {
497 self.handle_status_impl(tx).await
498 }
499 async fn resubmit_transaction(
509 &self,
510 tx: TransactionRepoModel,
511 ) -> Result<TransactionRepoModel, TransactionError> {
512 info!("Resubmitting transaction: {:?}", tx.id);
513
514 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 let evm_data = tx.network_data.get_evm_transaction_data()?;
533
534 let updated_evm_data = evm_data.with_price_params(bumped_price_params.clone());
536
537 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 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 let mut hashes = tx.hashes.clone();
557 if let Some(hash) = final_evm_data.hash.clone() {
558 hashes.push(hash);
559 }
560
561 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 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 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 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 self.send_transaction_resubmit_job(&updated_tx).await?;
616
617 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 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 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 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 let updated_evm_data = EvmTransactionData::for_replacement(&old_evm_data, &new_evm_request);
690
691 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 let evm_data_with_price_params = updated_evm_data.with_price_params(price_params.clone());
705
706 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 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 self.send_transaction_update_notification(&updated_tx)
733 .await?;
734
735 Ok(updated_tx)
736 }
737
738 async fn sign_transaction(
748 &self,
749 tx: TransactionRepoModel,
750 ) -> Result<TransactionRepoModel, TransactionError> {
751 Ok(tx)
752 }
753
754 async fn validate_transaction(
764 &self,
765 _tx: TransactionRepoModel,
766 ) -> Result<bool, TransactionError> {
767 Ok(true)
768 }
769}
770pub 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 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 fn create_test_relayer() -> RelayerRepoModel {
830 create_test_relayer_with_policy(crate::models::RelayerEvmPolicy {
831 min_balance: Some(100000000000000000u128), gas_limit_estimation: Some(true),
833 gas_price_cap: Some(100000000000), 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(), 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 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), data: Some("0xData".to_string()),
875 gas_limit: Some(21000),
876 gas_price: Some(20000000000), 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 {
1085 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 let relayer = create_test_relayer();
1096 let mut test_tx = create_test_transaction();
1097 test_tx.status = TransactionStatus::Pending;
1098
1099 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 mock_job_producer
1114 .expect_produce_send_notification_job()
1115 .returning(|_, _| Box::pin(ready(Ok(()))));
1116
1117 let mock_network = MockNetworkRepository::new();
1118
1119 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 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 {
1142 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 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 mock_price_calculator
1164 .expect_get_transaction_price_params()
1165 .return_once(move |_, _| {
1166 Ok(PriceParams {
1167 gas_price: Some(40000000000), 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 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 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 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 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 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 let result = evm_transaction.cancel_transaction(test_tx.clone()).await;
1265 assert!(result.is_ok());
1266 let cancelled_tx = result.unwrap();
1267
1268 assert_eq!(cancelled_tx.id, "test-tx-id");
1270 assert_eq!(cancelled_tx.status, TransactionStatus::Submitted);
1271
1272 if let NetworkTransactionData::Evm(evm_data) = &cancelled_tx.network_data {
1274 assert_eq!(evm_data.nonce, Some(42)); } else {
1276 panic!("Expected EVM transaction data");
1277 }
1278 }
1279
1280 {
1282 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 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 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 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 {
1326 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 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 mock_price_calculator
1343 .expect_get_transaction_price_params()
1344 .return_once(move |_, _| {
1345 Ok(PriceParams {
1346 gas_price: Some(40000000000), 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), })
1353 });
1354
1355 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 mock_provider
1375 .expect_get_balance()
1376 .with(eq("0xSender"))
1377 .returning(|_| Box::pin(ready(Ok(U256::from(3000000000000000000u64)))));
1378
1379 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 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 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()]), },
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 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 let replacement_request = NetworkTransactionRequest::Evm(EvmTransactionRequest {
1445 to: Some("0xNewRecipient".to_string()),
1446 value: U256::from(2000000000000000000u64), data: Some("0xNewData".to_string()),
1448 gas_limit: Some(25000),
1449 gas_price: None, max_fee_per_gas: None,
1451 max_priority_fee_per_gas: None,
1452 speed: Some(Speed::Fast),
1453 valid_until: None,
1454 });
1455
1456 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 assert_eq!(replaced_tx.id, "test-tx-id");
1468
1469 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 {
1484 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 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 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 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 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 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
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 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 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 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, ..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
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 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.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 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 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; evm_data.nonce = None; }
1812
1813 const PROVIDER_GAS_ESTIMATE: u64 = 45000;
1815 const EXPECTED_GAS_WITH_BUFFER: u64 = 49500; mock_provider
1819 .expect_estimate_gas()
1820 .times(1)
1821 .returning(move |_| Box::pin(async move { Ok(PROVIDER_GAS_ESTIMATE) }));
1822
1823 mock_provider
1825 .expect_get_balance()
1826 .times(1)
1827 .returning(|_| Box::pin(async { Ok(U256::from(2000000000000000000u128)) })); let price_params = PriceParams {
1830 gas_price: Some(20_000_000_000), 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), };
1837
1838 mock_price_calculator
1840 .expect_get_transaction_price_params()
1841 .returning(move |_, _| Ok(price_params.clone()));
1842
1843 counter_service
1845 .expect_get_and_increment()
1846 .times(1)
1847 .returning(|_, _| Box::pin(async { Ok(42) }));
1848
1849 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
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 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 let result = transaction.prepare_transaction(test_tx).await;
1910
1911 assert!(result.is_ok(), "prepare_transaction should succeed");
1913 let prepared_tx = result.unwrap();
1914
1915 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}