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