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