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 evm::is_noop,
17 transaction::{
18 evm::{ensure_status, ensure_status_one_of, PriceCalculator, PriceCalculatorTrait},
19 Transaction,
20 },
21 EvmTransactionValidationError, EvmTransactionValidator,
22 },
23 jobs::{
24 JobProducer, JobProducerTrait, StatusCheckContext, TransactionSend, TransactionStatusCheck,
25 },
26 models::{
27 produce_transaction_update_notification_payload, EvmNetwork, EvmTransactionData,
28 NetworkRepoModel, NetworkTransactionData, NetworkTransactionRequest, NetworkType,
29 RelayerEvmPolicy, RelayerRepoModel, TransactionError, TransactionRepoModel,
30 TransactionStatus, TransactionUpdateRequest,
31 },
32 repositories::{
33 NetworkRepository, NetworkRepositoryStorage, RelayerRepository, RelayerRepositoryStorage,
34 Repository, TransactionCounterRepositoryStorage, TransactionCounterTrait,
35 TransactionRepository, TransactionRepositoryStorage,
36 },
37 services::{
38 gas::evm_gas_price::EvmGasPriceService,
39 provider::{EvmProvider, EvmProviderTrait},
40 signer::{EvmSigner, Signer},
41 },
42 utils::{calculate_scheduled_timestamp, get_evm_default_gas_limit_for_tx},
43};
44
45use super::PriceParams;
46
47#[allow(dead_code)]
48pub struct EvmRelayerTransaction<P, RR, NR, TR, J, S, TCR, PC>
49where
50 P: EvmProviderTrait,
51 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
52 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
53 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
54 J: JobProducerTrait + Send + Sync + 'static,
55 S: Signer + Send + Sync + 'static,
56 TCR: TransactionCounterTrait + Send + Sync + 'static,
57 PC: PriceCalculatorTrait,
58{
59 provider: P,
60 relayer_repository: Arc<RR>,
61 network_repository: Arc<NR>,
62 transaction_repository: Arc<TR>,
63 job_producer: Arc<J>,
64 signer: S,
65 relayer: RelayerRepoModel,
66 transaction_counter_service: Arc<TCR>,
67 price_calculator: PC,
68}
69
70#[allow(dead_code, clippy::too_many_arguments)]
71impl<P, RR, NR, TR, J, S, TCR, PC> EvmRelayerTransaction<P, RR, NR, TR, J, S, TCR, PC>
72where
73 P: EvmProviderTrait,
74 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
75 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
76 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
77 J: JobProducerTrait + Send + Sync + 'static,
78 S: Signer + Send + Sync + 'static,
79 TCR: TransactionCounterTrait + Send + Sync + 'static,
80 PC: PriceCalculatorTrait,
81{
82 pub fn new(
99 relayer: RelayerRepoModel,
100 provider: P,
101 relayer_repository: Arc<RR>,
102 network_repository: Arc<NR>,
103 transaction_repository: Arc<TR>,
104 transaction_counter_service: Arc<TCR>,
105 job_producer: Arc<J>,
106 price_calculator: PC,
107 signer: S,
108 ) -> Result<Self, TransactionError> {
109 Ok(Self {
110 relayer,
111 provider,
112 relayer_repository,
113 network_repository,
114 transaction_repository,
115 transaction_counter_service,
116 job_producer,
117 price_calculator,
118 signer,
119 })
120 }
121
122 pub fn provider(&self) -> &P {
124 &self.provider
125 }
126
127 pub fn relayer(&self) -> &RelayerRepoModel {
129 &self.relayer
130 }
131
132 pub fn network_repository(&self) -> &NR {
134 &self.network_repository
135 }
136
137 pub fn job_producer(&self) -> &J {
139 &self.job_producer
140 }
141
142 pub fn transaction_repository(&self) -> &TR {
143 &self.transaction_repository
144 }
145
146 fn is_already_submitted_error(error: &impl std::fmt::Display) -> bool {
149 let error_msg = error.to_string().to_lowercase();
150 error_msg.contains("already known")
151 || error_msg.contains("nonce too low")
152 || error_msg.contains("replacement transaction underpriced")
153 }
154
155 pub(super) async fn schedule_status_check(
157 &self,
158 tx: &TransactionRepoModel,
159 delay_seconds: Option<i64>,
160 ) -> Result<(), TransactionError> {
161 let delay = delay_seconds.map(calculate_scheduled_timestamp);
162 self.job_producer()
163 .produce_check_transaction_status_job(
164 TransactionStatusCheck::new(
165 tx.id.clone(),
166 tx.relayer_id.clone(),
167 crate::models::NetworkType::Evm,
168 ),
169 delay,
170 )
171 .await
172 .map_err(|e| {
173 TransactionError::UnexpectedError(format!("Failed to schedule status check: {e}"))
174 })
175 }
176
177 pub(super) async fn send_transaction_submit_job(
179 &self,
180 tx: &TransactionRepoModel,
181 ) -> Result<(), TransactionError> {
182 debug!(
183 tx_id = %tx.id,
184 relayer_id = %tx.relayer_id,
185 "enqueueing submit transaction job"
186 );
187 let job = TransactionSend::submit(tx.id.clone(), tx.relayer_id.clone());
188
189 self.job_producer()
190 .produce_submit_transaction_job(job, None)
191 .await
192 .map_err(|e| {
193 TransactionError::UnexpectedError(format!("Failed to produce submit job: {e}"))
194 })
195 }
196
197 pub(super) async fn send_transaction_resubmit_job(
199 &self,
200 tx: &TransactionRepoModel,
201 ) -> Result<(), TransactionError> {
202 debug!(
203 tx_id = %tx.id,
204 relayer_id = %tx.relayer_id,
205 "enqueueing resubmit transaction job"
206 );
207 let job = TransactionSend::resubmit(tx.id.clone(), tx.relayer_id.clone());
208
209 self.job_producer()
210 .produce_submit_transaction_job(job, None)
211 .await
212 .map_err(|e| {
213 TransactionError::UnexpectedError(format!("Failed to produce resubmit job: {e}"))
214 })
215 }
216
217 pub(super) async fn send_transaction_resend_job(
219 &self,
220 tx: &TransactionRepoModel,
221 ) -> Result<(), TransactionError> {
222 debug!(
223 tx_id = %tx.id,
224 relayer_id = %tx.relayer_id,
225 "enqueueing resend transaction job"
226 );
227 let job = TransactionSend::resend(tx.id.clone(), tx.relayer_id.clone());
228
229 self.job_producer()
230 .produce_submit_transaction_job(job, None)
231 .await
232 .map_err(|e| {
233 TransactionError::UnexpectedError(format!("Failed to produce resend job: {e}"))
234 })
235 }
236
237 pub(super) async fn send_transaction_request_job(
239 &self,
240 tx: &TransactionRepoModel,
241 ) -> Result<(), TransactionError> {
242 use crate::jobs::TransactionRequest;
243
244 let job = TransactionRequest::new(tx.id.clone(), tx.relayer_id.clone());
245
246 self.job_producer()
247 .produce_transaction_request_job(job, None)
248 .await
249 .map_err(|e| {
250 TransactionError::UnexpectedError(format!("Failed to produce request job: {e}"))
251 })
252 }
253
254 pub(super) async fn update_transaction_status(
256 &self,
257 tx: TransactionRepoModel,
258 new_status: TransactionStatus,
259 status_reason: Option<String>,
260 ) -> Result<TransactionRepoModel, TransactionError> {
261 let confirmed_at = if new_status == TransactionStatus::Confirmed {
262 Some(Utc::now().to_rfc3339())
263 } else {
264 None
265 };
266
267 let update_request = TransactionUpdateRequest {
268 status: Some(new_status),
269 confirmed_at,
270 status_reason,
271 ..Default::default()
272 };
273
274 let updated_tx = self
275 .transaction_repository()
276 .partial_update(tx.id.clone(), update_request)
277 .await?;
278
279 if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
280 error!(
281 tx_id = %updated_tx.id,
282 status = ?updated_tx.status,
283 "sending transaction update notification failed: {:?}",
284 e
285 );
286 }
287 Ok(updated_tx)
288 }
289
290 pub(super) async fn send_transaction_update_notification(
295 &self,
296 tx: &TransactionRepoModel,
297 ) -> Result<(), eyre::Report> {
298 if let Some(notification_id) = &self.relayer().notification_id {
299 self.job_producer()
300 .produce_send_notification_job(
301 produce_transaction_update_notification_payload(notification_id, tx),
302 None,
303 )
304 .await?;
305 }
306 Ok(())
307 }
308
309 async fn mark_transaction_as_failed(
323 &self,
324 tx: &TransactionRepoModel,
325 reason: String,
326 error_context: &str,
327 ) -> Result<TransactionRepoModel, TransactionError> {
328 let update = TransactionUpdateRequest {
329 status: Some(TransactionStatus::Failed),
330 status_reason: Some(reason.clone()),
331 ..Default::default()
332 };
333
334 let updated_tx = self
335 .transaction_repository
336 .partial_update(tx.id.clone(), update)
337 .await?;
338
339 if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
340 error!(
341 tx_id = %updated_tx.id,
342 status = ?TransactionStatus::Failed,
343 "sending transaction update notification failed for {}: {:?}",
344 error_context,
345 e
346 );
347 }
348
349 Ok(updated_tx)
350 }
351
352 async fn ensure_sufficient_balance(
364 &self,
365 total_cost: crate::models::U256,
366 ) -> Result<(), TransactionError> {
367 EvmTransactionValidator::validate_sufficient_relayer_balance(
368 total_cost,
369 &self.relayer().address,
370 &self.relayer().policies.get_evm_policy(),
371 &self.provider,
372 )
373 .await
374 .map_err(|validation_error| match validation_error {
375 EvmTransactionValidationError::InsufficientBalance(msg) => {
377 TransactionError::InsufficientBalance(msg)
378 }
379 EvmTransactionValidationError::ProviderError(msg) => {
381 TransactionError::UnexpectedError(format!("Failed to check balance: {msg}"))
382 }
383 EvmTransactionValidationError::ValidationError(msg) => {
385 TransactionError::UnexpectedError(format!("Balance validation error: {msg}"))
386 }
387 })
388 }
389
390 async fn estimate_tx_gas_limit(
398 &self,
399 evm_data: &EvmTransactionData,
400 relayer_policy: &RelayerEvmPolicy,
401 ) -> Result<u64, TransactionError> {
402 if !relayer_policy
403 .gas_limit_estimation
404 .unwrap_or(DEFAULT_EVM_GAS_LIMIT_ESTIMATION)
405 {
406 warn!("gas limit estimation is disabled for relayer");
407 return Err(TransactionError::UnexpectedError(
408 "Gas limit estimation is disabled".to_string(),
409 ));
410 }
411
412 let estimated_gas = self.provider.estimate_gas(evm_data).await.map_err(|e| {
413 warn!(error = ?e, tx_data = ?evm_data, "failed to estimate gas");
414 TransactionError::UnexpectedError(format!("Failed to estimate gas: {e}"))
415 })?;
416
417 Ok(estimated_gas * GAS_LIMIT_BUFFER_MULTIPLIER / 100)
418 }
419}
420
421#[async_trait]
422impl<P, RR, NR, TR, J, S, TCR, PC> Transaction
423 for EvmRelayerTransaction<P, RR, NR, TR, J, S, TCR, PC>
424where
425 P: EvmProviderTrait + Send + Sync + 'static,
426 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
427 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
428 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
429 J: JobProducerTrait + Send + Sync + 'static,
430 S: Signer + Send + Sync + 'static,
431 TCR: TransactionCounterTrait + Send + Sync + 'static,
432 PC: PriceCalculatorTrait + Send + Sync + 'static,
433{
434 async fn prepare_transaction(
444 &self,
445 tx: TransactionRepoModel,
446 ) -> Result<TransactionRepoModel, TransactionError> {
447 debug!(
448 tx_id = %tx.id,
449 relayer_id = %tx.relayer_id,
450 status = ?tx.status,
451 "preparing transaction"
452 );
453
454 if let Err(e) = ensure_status(&tx, TransactionStatus::Pending, Some("prepare_transaction"))
457 {
458 warn!(
459 tx_id = %tx.id,
460 status = ?tx.status,
461 error = %e,
462 "transaction not in Pending status, skipping preparation"
463 );
464 return Ok(tx);
465 }
466
467 let mut evm_data = tx.network_data.get_evm_transaction_data()?;
468 let relayer = self.relayer();
469
470 if evm_data.gas_limit.is_none() {
471 match self
472 .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
473 .await
474 {
475 Ok(estimated_gas_limit) => {
476 evm_data.gas_limit = Some(estimated_gas_limit);
477 }
478 Err(estimation_error) => {
479 error!(
480 tx_id = %tx.id,
481 relayer_id = %tx.relayer_id,
482 error = ?estimation_error,
483 "failed to estimate gas limit"
484 );
485
486 let default_gas_limit = get_evm_default_gas_limit_for_tx(&evm_data);
487 debug!(
488 tx_id = %tx.id,
489 gas_limit = %default_gas_limit,
490 "fallback to default gas limit"
491 );
492 evm_data.gas_limit = Some(default_gas_limit);
493 }
494 }
495 } else {
496 let block = self.provider.get_block_by_number().await;
498 if let Ok(block) = block {
499 let block_gas_limit = block.header.gas_limit;
500 if let Some(gas_limit) = evm_data.gas_limit {
501 if gas_limit > block_gas_limit {
502 let reason = format!(
503 "Transaction gas limit ({gas_limit}) exceeds block gas limit ({block_gas_limit})",
504 );
505 warn!(
506 tx_id = %tx.id,
507 tx_gas_limit = %gas_limit,
508 block_gas_limit = %block_gas_limit,
509 "transaction gas limit exceeds block gas limit"
510 );
511
512 let updated_tx = self
513 .mark_transaction_as_failed(
514 &tx,
515 reason,
516 "gas limit exceeds block gas limit",
517 )
518 .await?;
519 return Ok(updated_tx);
520 }
521 }
522 }
523 }
524
525 let price_params: PriceParams = self
527 .price_calculator
528 .get_transaction_price_params(&evm_data, relayer)
529 .await?;
530
531 debug!(
532 tx_id = %tx.id,
533 relayer_id = %tx.relayer_id,
534 gas_price = ?price_params.gas_price,
535 "gas price"
536 );
537
538 if let Err(balance_error) = self
540 .ensure_sufficient_balance(price_params.total_cost)
541 .await
542 {
543 match &balance_error {
545 TransactionError::InsufficientBalance(_) => {
546 warn!(
547 tx_id = %tx.id,
548 relayer_id = %tx.relayer_id,
549 error = %balance_error,
550 "insufficient balance for transaction"
551 );
552
553 let updated_tx = self
554 .mark_transaction_as_failed(
555 &tx,
556 balance_error.to_string(),
557 "insufficient balance",
558 )
559 .await?;
560
561 return Ok(updated_tx);
563 }
564 _ => {
567 debug!(error = %balance_error, "failed to check balance, will retry");
568 return Err(balance_error);
569 }
570 }
571 }
572
573 let tx_with_nonce = if let Some(existing_nonce) = evm_data.nonce {
575 debug!(
576 nonce = existing_nonce,
577 "transaction already has nonce assigned, reusing for retry"
578 );
579 tx
585 } else {
586 let new_nonce = self
588 .transaction_counter_service
589 .get_and_increment(&self.relayer.id, &self.relayer.address)
590 .await
591 .map_err(|e| TransactionError::UnexpectedError(e.to_string()))?;
592
593 debug!(nonce = new_nonce, "assigned new nonce to transaction");
594
595 let updated_evm_data = evm_data
596 .with_price_params(price_params.clone())
597 .with_nonce(new_nonce);
598
599 let presign_update = TransactionUpdateRequest {
602 network_data: Some(NetworkTransactionData::Evm(updated_evm_data.clone())),
603 priced_at: Some(Utc::now().to_rfc3339()),
604 ..Default::default()
605 };
606
607 self.transaction_repository
608 .partial_update(tx.id.clone(), presign_update)
609 .await?
610 };
611
612 let updated_evm_data = tx_with_nonce
614 .network_data
615 .get_evm_transaction_data()?
616 .with_price_params(price_params.clone());
617
618 let sig_result = self
620 .signer
621 .sign_transaction(NetworkTransactionData::Evm(updated_evm_data.clone()))
622 .await?;
623
624 let updated_evm_data =
625 updated_evm_data.with_signed_transaction_data(sig_result.into_evm()?);
626
627 let mut hashes = tx_with_nonce.hashes.clone();
629 if let Some(hash) = updated_evm_data.hash.clone() {
630 hashes.push(hash);
631 }
632
633 let postsign_update = TransactionUpdateRequest {
635 status: Some(TransactionStatus::Sent),
636 network_data: Some(NetworkTransactionData::Evm(updated_evm_data)),
637 hashes: Some(hashes),
638 ..Default::default()
639 };
640
641 let updated_tx = self
642 .transaction_repository
643 .partial_update(tx_with_nonce.id.clone(), postsign_update)
644 .await?;
645
646 debug!(
647 tx_id = %updated_tx.id,
648 relayer_id = %updated_tx.relayer_id,
649 status = ?updated_tx.status,
650 "transaction status updated to Sent"
651 );
652
653 self.job_producer
655 .produce_submit_transaction_job(
656 TransactionSend::submit(updated_tx.id.clone(), updated_tx.relayer_id.clone()),
657 None,
658 )
659 .await?;
660
661 if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
662 error!(
663 tx_id = %updated_tx.id,
664 relayer_id = %updated_tx.relayer_id,
665 status = ?TransactionStatus::Sent,
666 error = %e,
667 "sending transaction update notification failed after prepare"
668 );
669 }
670
671 Ok(updated_tx)
672 }
673
674 async fn submit_transaction(
684 &self,
685 tx: TransactionRepoModel,
686 ) -> Result<TransactionRepoModel, TransactionError> {
687 debug!(
688 tx_id = %tx.id,
689 relayer_id = %tx.relayer_id,
690 status = ?tx.status,
691 "submitting transaction"
692 );
693
694 if let Err(e) = ensure_status_one_of(
697 &tx,
698 &[TransactionStatus::Sent, TransactionStatus::Submitted],
699 Some("submit_transaction"),
700 ) {
701 warn!(
702 tx_id = %tx.id,
703 status = ?tx.status,
704 error = %e,
705 "transaction not in expected status for submission, skipping"
706 );
707 return Ok(tx);
708 }
709
710 let evm_tx_data = tx.network_data.get_evm_transaction_data()?;
711 let raw_tx = evm_tx_data.raw.as_ref().ok_or_else(|| {
712 TransactionError::InvalidType("Raw transaction data is missing".to_string())
713 })?;
714
715 match self.provider.send_raw_transaction(raw_tx).await {
718 Ok(_) => {
719 }
721 Err(e) => {
722 if tx.status == TransactionStatus::Sent && Self::is_already_submitted_error(&e) {
726 warn!(
727 tx_id = %tx.id,
728 error = %e,
729 "transaction appears to be already submitted based on RPC error - treating as success"
730 );
731 } else {
733 return Err(e.into());
735 }
736 }
737 }
738
739 let update = TransactionUpdateRequest {
742 status: Some(TransactionStatus::Submitted),
743 sent_at: Some(Utc::now().to_rfc3339()),
744 ..Default::default()
745 };
746
747 let updated_tx = match self
748 .transaction_repository
749 .partial_update(tx.id.clone(), update)
750 .await
751 {
752 Ok(tx) => tx,
753 Err(e) => {
754 error!(
755 tx_id = %tx.id,
756 relayer_id = %tx.relayer_id,
757 error = %e,
758 "CRITICAL: transaction sent to blockchain but failed to update database - transaction may not be tracked correctly"
759 );
760 tx
763 }
764 };
765
766 if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
767 error!(
768 tx_id = %updated_tx.id,
769 relayer_id = %updated_tx.relayer_id,
770 status = ?TransactionStatus::Submitted,
771 error = %e,
772 "sending transaction update notification failed after submit",
773 );
774 }
775
776 Ok(updated_tx)
777 }
778
779 async fn handle_transaction_status(
789 &self,
790 tx: TransactionRepoModel,
791 context: Option<StatusCheckContext>,
792 ) -> Result<TransactionRepoModel, TransactionError> {
793 self.handle_status_impl(tx, context).await
794 }
795 async fn resubmit_transaction(
805 &self,
806 tx: TransactionRepoModel,
807 ) -> Result<TransactionRepoModel, TransactionError> {
808 debug!(
809 tx_id = %tx.id,
810 relayer_id = %tx.relayer_id,
811 status = ?tx.status,
812 "resubmitting transaction"
813 );
814
815 if let Err(e) = ensure_status_one_of(
817 &tx,
818 &[TransactionStatus::Sent, TransactionStatus::Submitted],
819 Some("resubmit_transaction"),
820 ) {
821 warn!(
822 tx_id = %tx.id,
823 status = ?tx.status,
824 error = %e,
825 "transaction not in expected status for resubmission, skipping"
826 );
827 return Ok(tx);
828 }
829
830 let evm_data = tx.network_data.get_evm_transaction_data()?;
831
832 let bumped_price_params = self
835 .price_calculator
836 .calculate_bumped_gas_price(&evm_data, self.relayer(), is_noop(&evm_data))
837 .await?;
838
839 if !bumped_price_params.is_min_bumped.is_some_and(|b| b) {
840 warn!(
841 tx_id = %tx.id,
842 relayer_id = %tx.relayer_id,
843 price_params = ?bumped_price_params,
844 "bumped gas price does not meet minimum requirement, skipping resubmission"
845 );
846 return Ok(tx);
847 }
848
849 self.ensure_sufficient_balance(bumped_price_params.total_cost)
851 .await?;
852
853 let updated_evm_data = evm_data.with_price_params(bumped_price_params.clone());
855
856 let sig_result = self
858 .signer
859 .sign_transaction(NetworkTransactionData::Evm(updated_evm_data.clone()))
860 .await?;
861
862 let final_evm_data = updated_evm_data.with_signed_transaction_data(sig_result.into_evm()?);
863
864 let raw_tx = final_evm_data.raw.as_ref().ok_or_else(|| {
865 TransactionError::InvalidType("Raw transaction data is missing".to_string())
866 })?;
867
868 let was_already_submitted = match self.provider.send_raw_transaction(raw_tx).await {
870 Ok(_) => {
871 false
873 }
874 Err(e) => {
875 let is_already_submitted = Self::is_already_submitted_error(&e);
878
879 if is_already_submitted {
880 warn!(
881 tx_id = %tx.id,
882 error = %e,
883 "resubmission indicates transaction already in mempool/mined - keeping original hash"
884 );
885 true
887 } else {
888 return Err(e.into());
890 }
891 }
892 };
893
894 let update = if was_already_submitted {
896 TransactionUpdateRequest {
898 status: Some(TransactionStatus::Submitted),
899 ..Default::default()
900 }
901 } else {
902 let mut hashes = tx.hashes.clone();
904 if let Some(hash) = final_evm_data.hash.clone() {
905 hashes.push(hash);
906 }
907
908 TransactionUpdateRequest {
909 network_data: Some(NetworkTransactionData::Evm(final_evm_data)),
910 hashes: Some(hashes),
911 status: Some(TransactionStatus::Submitted),
912 priced_at: Some(Utc::now().to_rfc3339()),
913 sent_at: Some(Utc::now().to_rfc3339()),
914 ..Default::default()
915 }
916 };
917
918 let updated_tx = match self
919 .transaction_repository
920 .partial_update(tx.id.clone(), update)
921 .await
922 {
923 Ok(tx) => tx,
924 Err(e) => {
925 error!(
926 error = %e,
927 tx_id = %tx.id,
928 "CRITICAL: resubmitted transaction sent to blockchain but failed to update database"
929 );
930 tx
932 }
933 };
934
935 Ok(updated_tx)
936 }
937
938 async fn cancel_transaction(
948 &self,
949 tx: TransactionRepoModel,
950 ) -> Result<TransactionRepoModel, TransactionError> {
951 info!(tx_id = %tx.id, status = ?tx.status, "cancelling transaction");
952
953 ensure_status_one_of(
955 &tx,
956 &[
957 TransactionStatus::Pending,
958 TransactionStatus::Sent,
959 TransactionStatus::Submitted,
960 ],
961 Some("cancel_transaction"),
962 )?;
963
964 if tx.status == TransactionStatus::Pending {
966 debug!("transaction is in pending state, updating status to canceled");
967 return self
968 .update_transaction_status(
969 tx,
970 TransactionStatus::Canceled,
971 Some("Transaction canceled by user".to_string()),
972 )
973 .await;
974 }
975
976 let update = self
977 .prepare_noop_update_request(
978 &tx,
979 true,
980 Some("Transaction canceled by user, replacing with NOOP".to_string()),
981 )
982 .await?;
983 let updated_tx = self
984 .transaction_repository()
985 .partial_update(tx.id.clone(), update)
986 .await?;
987
988 self.send_transaction_resubmit_job(&updated_tx).await?;
990
991 if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
993 error!(
994 tx_id = %updated_tx.id,
995 status = ?updated_tx.status,
996 "sending transaction update notification failed after cancel: {:?}",
997 e
998 );
999 }
1000
1001 debug!("original transaction updated with cancellation data");
1002 Ok(updated_tx)
1003 }
1004
1005 async fn replace_transaction(
1016 &self,
1017 old_tx: TransactionRepoModel,
1018 new_tx_request: NetworkTransactionRequest,
1019 ) -> Result<TransactionRepoModel, TransactionError> {
1020 debug!("replacing transaction");
1021
1022 ensure_status_one_of(
1024 &old_tx,
1025 &[
1026 TransactionStatus::Pending,
1027 TransactionStatus::Sent,
1028 TransactionStatus::Submitted,
1029 ],
1030 Some("replace_transaction"),
1031 )?;
1032
1033 let old_evm_data = old_tx.network_data.get_evm_transaction_data()?;
1035 let new_evm_request = match new_tx_request {
1036 NetworkTransactionRequest::Evm(evm_req) => evm_req,
1037 _ => {
1038 return Err(TransactionError::InvalidType(
1039 "New transaction request must be EVM type".to_string(),
1040 ))
1041 }
1042 };
1043
1044 let network_repo_model = self
1045 .network_repository()
1046 .get_by_chain_id(NetworkType::Evm, old_evm_data.chain_id)
1047 .await
1048 .map_err(|e| {
1049 TransactionError::NetworkConfiguration(format!(
1050 "Failed to get network by chain_id {}: {}",
1051 old_evm_data.chain_id, e
1052 ))
1053 })?
1054 .ok_or_else(|| {
1055 TransactionError::NetworkConfiguration(format!(
1056 "Network with chain_id {} not found",
1057 old_evm_data.chain_id
1058 ))
1059 })?;
1060
1061 let network = EvmNetwork::try_from(network_repo_model).map_err(|e| {
1062 TransactionError::NetworkConfiguration(format!("Failed to convert network model: {e}"))
1063 })?;
1064
1065 let updated_evm_data = EvmTransactionData::for_replacement(&old_evm_data, &new_evm_request);
1067
1068 let price_params = super::replacement::determine_replacement_pricing(
1070 &old_evm_data,
1071 &updated_evm_data,
1072 self.relayer(),
1073 &self.price_calculator,
1074 network.lacks_mempool(),
1075 )
1076 .await?;
1077
1078 debug!(price_params = ?price_params, "replacement price params");
1079
1080 let evm_data_with_price_params = updated_evm_data.with_price_params(price_params.clone());
1082
1083 self.ensure_sufficient_balance(price_params.total_cost)
1085 .await?;
1086
1087 let sig_result = self
1088 .signer
1089 .sign_transaction(NetworkTransactionData::Evm(
1090 evm_data_with_price_params.clone(),
1091 ))
1092 .await?;
1093
1094 let final_evm_data =
1095 evm_data_with_price_params.with_signed_transaction_data(sig_result.into_evm()?);
1096
1097 let updated_tx = self
1099 .transaction_repository
1100 .update_network_data(
1101 old_tx.id.clone(),
1102 NetworkTransactionData::Evm(final_evm_data),
1103 )
1104 .await?;
1105
1106 self.send_transaction_resubmit_job(&updated_tx).await?;
1107
1108 if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
1110 error!(
1111 tx_id = %updated_tx.id,
1112 status = ?updated_tx.status,
1113 "sending transaction update notification failed after replace: {:?}",
1114 e
1115 );
1116 }
1117
1118 Ok(updated_tx)
1119 }
1120
1121 async fn sign_transaction(
1131 &self,
1132 tx: TransactionRepoModel,
1133 ) -> Result<TransactionRepoModel, TransactionError> {
1134 Ok(tx)
1135 }
1136
1137 async fn validate_transaction(
1147 &self,
1148 _tx: TransactionRepoModel,
1149 ) -> Result<bool, TransactionError> {
1150 Ok(true)
1151 }
1152}
1153pub type DefaultEvmTransaction = EvmRelayerTransaction<
1162 EvmProvider,
1163 RelayerRepositoryStorage,
1164 NetworkRepositoryStorage,
1165 TransactionRepositoryStorage,
1166 JobProducer,
1167 EvmSigner,
1168 TransactionCounterRepositoryStorage,
1169 PriceCalculator<EvmGasPriceService<EvmProvider>>,
1170>;
1171#[cfg(test)]
1172mod tests {
1173
1174 use super::*;
1175 use crate::{
1176 domain::evm::price_calculator::PriceParams,
1177 jobs::MockJobProducerTrait,
1178 models::{
1179 evm::Speed, EvmTransactionData, EvmTransactionRequest, NetworkType,
1180 RelayerNetworkPolicy, U256,
1181 },
1182 repositories::{
1183 MockNetworkRepository, MockRelayerRepository, MockTransactionCounterTrait,
1184 MockTransactionRepository,
1185 },
1186 services::{provider::MockEvmProviderTrait, signer::MockSigner},
1187 };
1188 use chrono::Utc;
1189 use futures::future::ready;
1190 use mockall::{mock, predicate::*};
1191
1192 mock! {
1194 pub PriceCalculator {}
1195 #[async_trait]
1196 impl PriceCalculatorTrait for PriceCalculator {
1197 async fn get_transaction_price_params(
1198 &self,
1199 tx_data: &EvmTransactionData,
1200 relayer: &RelayerRepoModel
1201 ) -> Result<PriceParams, TransactionError>;
1202
1203 async fn calculate_bumped_gas_price(
1204 &self,
1205 tx: &EvmTransactionData,
1206 relayer: &RelayerRepoModel,
1207 force_bump: bool,
1208 ) -> Result<PriceParams, TransactionError>;
1209 }
1210 }
1211
1212 fn create_test_relayer() -> RelayerRepoModel {
1214 create_test_relayer_with_policy(crate::models::RelayerEvmPolicy {
1215 min_balance: Some(100000000000000000u128), gas_limit_estimation: Some(true),
1217 gas_price_cap: Some(100000000000), whitelist_receivers: Some(vec!["0xRecipient".to_string()]),
1219 eip1559_pricing: Some(false),
1220 private_transactions: Some(false),
1221 })
1222 }
1223
1224 fn create_test_relayer_with_policy(evm_policy: RelayerEvmPolicy) -> RelayerRepoModel {
1225 RelayerRepoModel {
1226 id: "test-relayer-id".to_string(),
1227 name: "Test Relayer".to_string(),
1228 network: "1".to_string(), address: "0xSender".to_string(),
1230 paused: false,
1231 system_disabled: false,
1232 signer_id: "test-signer-id".to_string(),
1233 notification_id: Some("test-notification-id".to_string()),
1234 policies: RelayerNetworkPolicy::Evm(evm_policy),
1235 network_type: NetworkType::Evm,
1236 custom_rpc_urls: None,
1237 ..Default::default()
1238 }
1239 }
1240
1241 fn create_test_transaction() -> TransactionRepoModel {
1243 TransactionRepoModel {
1244 id: "test-tx-id".to_string(),
1245 relayer_id: "test-relayer-id".to_string(),
1246 status: TransactionStatus::Pending,
1247 status_reason: None,
1248 created_at: Utc::now().to_rfc3339(),
1249 sent_at: None,
1250 confirmed_at: None,
1251 valid_until: None,
1252 delete_at: None,
1253 network_type: NetworkType::Evm,
1254 network_data: NetworkTransactionData::Evm(EvmTransactionData {
1255 chain_id: 1,
1256 from: "0xSender".to_string(),
1257 to: Some("0xRecipient".to_string()),
1258 value: U256::from(1000000000000000000u64), data: Some("0xData".to_string()),
1260 gas_limit: Some(21000),
1261 gas_price: Some(20000000000), max_fee_per_gas: None,
1263 max_priority_fee_per_gas: None,
1264 nonce: None,
1265 signature: None,
1266 hash: None,
1267 speed: Some(Speed::Fast),
1268 raw: None,
1269 }),
1270 priced_at: None,
1271 hashes: Vec::new(),
1272 noop_count: None,
1273 is_canceled: Some(false),
1274 metadata: None,
1275 }
1276 }
1277
1278 #[tokio::test]
1279 async fn test_prepare_transaction_with_sufficient_balance() {
1280 let mut mock_transaction = MockTransactionRepository::new();
1281 let mock_relayer = MockRelayerRepository::new();
1282 let mut mock_provider = MockEvmProviderTrait::new();
1283 let mut mock_signer = MockSigner::new();
1284 let mut mock_job_producer = MockJobProducerTrait::new();
1285 let mut mock_price_calculator = MockPriceCalculator::new();
1286 let mut counter_service = MockTransactionCounterTrait::new();
1287
1288 let relayer = create_test_relayer();
1289 let test_tx = create_test_transaction();
1290
1291 counter_service
1292 .expect_get_and_increment()
1293 .returning(|_, _| Box::pin(ready(Ok(42))));
1294
1295 let price_params = PriceParams {
1296 gas_price: Some(30000000000),
1297 max_fee_per_gas: None,
1298 max_priority_fee_per_gas: None,
1299 is_min_bumped: None,
1300 extra_fee: None,
1301 total_cost: U256::from(630000000000000u64),
1302 };
1303 mock_price_calculator
1304 .expect_get_transaction_price_params()
1305 .returning(move |_, _| Ok(price_params.clone()));
1306
1307 mock_signer.expect_sign_transaction().returning(|_| {
1308 Box::pin(ready(Ok(
1309 crate::domain::relayer::SignTransactionResponse::Evm(
1310 crate::domain::relayer::SignTransactionResponseEvm {
1311 hash: "0xtx_hash".to_string(),
1312 signature: crate::models::EvmTransactionDataSignature {
1313 r: "r".to_string(),
1314 s: "s".to_string(),
1315 v: 1,
1316 sig: "0xsignature".to_string(),
1317 },
1318 raw: vec![1, 2, 3],
1319 },
1320 ),
1321 )))
1322 });
1323
1324 mock_provider
1325 .expect_get_balance()
1326 .with(eq("0xSender"))
1327 .returning(|_| Box::pin(ready(Ok(U256::from(1000000000000000000u64)))));
1328
1329 mock_provider
1331 .expect_get_block_by_number()
1332 .times(1)
1333 .returning(|| {
1334 Box::pin(async {
1335 use alloy::{network::AnyRpcBlock, rpc::types::Block};
1336 let mut block: Block = Block::default();
1337 block.header.gas_limit = 30_000_000u64;
1339 Ok(AnyRpcBlock::from(block))
1340 })
1341 });
1342
1343 let test_tx_clone = test_tx.clone();
1344 mock_transaction
1345 .expect_partial_update()
1346 .returning(move |_, update| {
1347 let mut updated_tx = test_tx_clone.clone();
1348 if let Some(status) = &update.status {
1349 updated_tx.status = status.clone();
1350 }
1351 if let Some(network_data) = &update.network_data {
1352 updated_tx.network_data = network_data.clone();
1353 }
1354 if let Some(hashes) = &update.hashes {
1355 updated_tx.hashes = hashes.clone();
1356 }
1357 Ok(updated_tx)
1358 });
1359
1360 mock_job_producer
1361 .expect_produce_submit_transaction_job()
1362 .returning(|_, _| Box::pin(ready(Ok(()))));
1363 mock_job_producer
1364 .expect_produce_send_notification_job()
1365 .returning(|_, _| Box::pin(ready(Ok(()))));
1366
1367 let mock_network = MockNetworkRepository::new();
1368
1369 let evm_transaction = EvmRelayerTransaction {
1370 relayer: relayer.clone(),
1371 provider: mock_provider,
1372 relayer_repository: Arc::new(mock_relayer),
1373 network_repository: Arc::new(mock_network),
1374 transaction_repository: Arc::new(mock_transaction),
1375 transaction_counter_service: Arc::new(counter_service),
1376 job_producer: Arc::new(mock_job_producer),
1377 price_calculator: mock_price_calculator,
1378 signer: mock_signer,
1379 };
1380
1381 let result = evm_transaction.prepare_transaction(test_tx.clone()).await;
1382 assert!(result.is_ok());
1383 let prepared_tx = result.unwrap();
1384 assert_eq!(prepared_tx.status, TransactionStatus::Sent);
1385 assert!(!prepared_tx.hashes.is_empty());
1386 }
1387
1388 #[tokio::test]
1389 async fn test_prepare_transaction_with_insufficient_balance() {
1390 let mut mock_transaction = MockTransactionRepository::new();
1391 let mock_relayer = MockRelayerRepository::new();
1392 let mut mock_provider = MockEvmProviderTrait::new();
1393 let mut mock_signer = MockSigner::new();
1394 let mut mock_job_producer = MockJobProducerTrait::new();
1395 let mut mock_price_calculator = MockPriceCalculator::new();
1396 let mut counter_service = MockTransactionCounterTrait::new();
1397
1398 let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
1399 gas_limit_estimation: Some(false),
1400 min_balance: Some(100000000000000000u128),
1401 ..Default::default()
1402 });
1403 let test_tx = create_test_transaction();
1404
1405 counter_service
1406 .expect_get_and_increment()
1407 .returning(|_, _| Box::pin(ready(Ok(42))));
1408
1409 let price_params = PriceParams {
1410 gas_price: Some(30000000000),
1411 max_fee_per_gas: None,
1412 max_priority_fee_per_gas: None,
1413 is_min_bumped: None,
1414 extra_fee: None,
1415 total_cost: U256::from(630000000000000u64),
1416 };
1417 mock_price_calculator
1418 .expect_get_transaction_price_params()
1419 .returning(move |_, _| Ok(price_params.clone()));
1420
1421 mock_signer.expect_sign_transaction().returning(|_| {
1422 Box::pin(ready(Ok(
1423 crate::domain::relayer::SignTransactionResponse::Evm(
1424 crate::domain::relayer::SignTransactionResponseEvm {
1425 hash: "0xtx_hash".to_string(),
1426 signature: crate::models::EvmTransactionDataSignature {
1427 r: "r".to_string(),
1428 s: "s".to_string(),
1429 v: 1,
1430 sig: "0xsignature".to_string(),
1431 },
1432 raw: vec![1, 2, 3],
1433 },
1434 ),
1435 )))
1436 });
1437
1438 mock_provider
1439 .expect_get_balance()
1440 .with(eq("0xSender"))
1441 .returning(|_| Box::pin(ready(Ok(U256::from(90000000000000000u64)))));
1442
1443 mock_provider
1445 .expect_get_block_by_number()
1446 .times(1)
1447 .returning(|| {
1448 Box::pin(async {
1449 use alloy::{network::AnyRpcBlock, rpc::types::Block};
1450 let mut block: Block = Block::default();
1451 block.header.gas_limit = 30_000_000u64;
1453 Ok(AnyRpcBlock::from(block))
1454 })
1455 });
1456
1457 let test_tx_clone = test_tx.clone();
1458 mock_transaction
1459 .expect_partial_update()
1460 .withf(move |id, update| {
1461 id == "test-tx-id" && update.status == Some(TransactionStatus::Failed)
1462 })
1463 .returning(move |_, update| {
1464 let mut updated_tx = test_tx_clone.clone();
1465 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1466 updated_tx.status_reason = update.status_reason.clone();
1467 Ok(updated_tx)
1468 });
1469
1470 mock_job_producer
1471 .expect_produce_send_notification_job()
1472 .returning(|_, _| Box::pin(ready(Ok(()))));
1473
1474 let mock_network = MockNetworkRepository::new();
1475
1476 let evm_transaction = EvmRelayerTransaction {
1477 relayer: relayer.clone(),
1478 provider: mock_provider,
1479 relayer_repository: Arc::new(mock_relayer),
1480 network_repository: Arc::new(mock_network),
1481 transaction_repository: Arc::new(mock_transaction),
1482 transaction_counter_service: Arc::new(counter_service),
1483 job_producer: Arc::new(mock_job_producer),
1484 price_calculator: mock_price_calculator,
1485 signer: mock_signer,
1486 };
1487
1488 let result = evm_transaction.prepare_transaction(test_tx.clone()).await;
1489 assert!(result.is_ok(), "Expected Ok, got: {result:?}");
1490
1491 let updated_tx = result.unwrap();
1492 assert_eq!(
1493 updated_tx.status,
1494 TransactionStatus::Failed,
1495 "Transaction should be marked as Failed"
1496 );
1497 assert!(
1498 updated_tx.status_reason.is_some(),
1499 "Status reason should be set"
1500 );
1501 assert!(
1502 updated_tx
1503 .status_reason
1504 .as_ref()
1505 .unwrap()
1506 .to_lowercase()
1507 .contains("insufficient balance"),
1508 "Status reason should contain insufficient balance error, got: {:?}",
1509 updated_tx.status_reason
1510 );
1511 }
1512
1513 #[tokio::test]
1514 async fn test_prepare_transaction_with_gas_limit_exceeding_block_limit() {
1515 let mut mock_transaction = MockTransactionRepository::new();
1516 let mock_relayer = MockRelayerRepository::new();
1517 let mut mock_provider = MockEvmProviderTrait::new();
1518 let mock_signer = MockSigner::new();
1519 let mut mock_job_producer = MockJobProducerTrait::new();
1520 let mock_price_calculator = MockPriceCalculator::new();
1521 let mut counter_service = MockTransactionCounterTrait::new();
1522
1523 let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
1524 gas_limit_estimation: Some(false), min_balance: Some(100000000000000000u128),
1526 ..Default::default()
1527 });
1528
1529 let mut test_tx = create_test_transaction();
1531 if let NetworkTransactionData::Evm(ref mut evm_data) = test_tx.network_data {
1532 evm_data.gas_limit = Some(30_000_001); }
1534
1535 counter_service
1536 .expect_get_and_increment()
1537 .returning(|_, _| Box::pin(ready(Ok(42))));
1538
1539 mock_provider
1541 .expect_get_block_by_number()
1542 .times(1)
1543 .returning(|| {
1544 Box::pin(async {
1545 use alloy::{network::AnyRpcBlock, rpc::types::Block};
1546 let mut block: Block = Block::default();
1547 block.header.gas_limit = 30_000_000u64;
1549 Ok(AnyRpcBlock::from(block))
1550 })
1551 });
1552
1553 let test_tx_clone = test_tx.clone();
1555 mock_transaction
1556 .expect_partial_update()
1557 .withf(move |id, update| {
1558 id == "test-tx-id"
1559 && update.status == Some(TransactionStatus::Failed)
1560 && update.status_reason.is_some()
1561 && update
1562 .status_reason
1563 .as_ref()
1564 .unwrap()
1565 .contains("exceeds block gas limit")
1566 })
1567 .returning(move |_, update| {
1568 let mut updated_tx = test_tx_clone.clone();
1569 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1570 updated_tx.status_reason = update.status_reason.clone();
1571 Ok(updated_tx)
1572 });
1573
1574 mock_job_producer
1575 .expect_produce_send_notification_job()
1576 .returning(|_, _| Box::pin(ready(Ok(()))));
1577
1578 let mock_network = MockNetworkRepository::new();
1579
1580 let evm_transaction = EvmRelayerTransaction {
1581 relayer: relayer.clone(),
1582 provider: mock_provider,
1583 relayer_repository: Arc::new(mock_relayer),
1584 network_repository: Arc::new(mock_network),
1585 transaction_repository: Arc::new(mock_transaction),
1586 transaction_counter_service: Arc::new(counter_service),
1587 job_producer: Arc::new(mock_job_producer),
1588 price_calculator: mock_price_calculator,
1589 signer: mock_signer,
1590 };
1591
1592 let result = evm_transaction.prepare_transaction(test_tx.clone()).await;
1593 assert!(result.is_ok(), "Expected Ok, got: {result:?}");
1594
1595 let updated_tx = result.unwrap();
1596 assert_eq!(
1597 updated_tx.status,
1598 TransactionStatus::Failed,
1599 "Transaction should be marked as Failed"
1600 );
1601 assert!(
1602 updated_tx.status_reason.is_some(),
1603 "Status reason should be set"
1604 );
1605 assert!(
1606 updated_tx
1607 .status_reason
1608 .as_ref()
1609 .unwrap()
1610 .contains("exceeds block gas limit"),
1611 "Status reason should mention gas limit exceeds block gas limit, got: {:?}",
1612 updated_tx.status_reason
1613 );
1614 assert!(
1615 updated_tx
1616 .status_reason
1617 .as_ref()
1618 .unwrap()
1619 .contains("30000001"),
1620 "Status reason should contain transaction gas limit, got: {:?}",
1621 updated_tx.status_reason
1622 );
1623 assert!(
1624 updated_tx
1625 .status_reason
1626 .as_ref()
1627 .unwrap()
1628 .contains("30000000"),
1629 "Status reason should contain block gas limit, got: {:?}",
1630 updated_tx.status_reason
1631 );
1632 }
1633
1634 #[tokio::test]
1635 async fn test_prepare_transaction_with_gas_limit_within_block_limit() {
1636 let mut mock_transaction = MockTransactionRepository::new();
1637 let mock_relayer = MockRelayerRepository::new();
1638 let mut mock_provider = MockEvmProviderTrait::new();
1639 let mut mock_signer = MockSigner::new();
1640 let mut mock_job_producer = MockJobProducerTrait::new();
1641 let mut mock_price_calculator = MockPriceCalculator::new();
1642 let mut counter_service = MockTransactionCounterTrait::new();
1643
1644 let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
1645 gas_limit_estimation: Some(false), min_balance: Some(100000000000000000u128),
1647 ..Default::default()
1648 });
1649
1650 let mut test_tx = create_test_transaction();
1652 if let NetworkTransactionData::Evm(ref mut evm_data) = test_tx.network_data {
1653 evm_data.gas_limit = Some(21_000); }
1655
1656 counter_service
1657 .expect_get_and_increment()
1658 .returning(|_, _| Box::pin(ready(Ok(42))));
1659
1660 let price_params = PriceParams {
1661 gas_price: Some(30000000000),
1662 max_fee_per_gas: None,
1663 max_priority_fee_per_gas: None,
1664 is_min_bumped: None,
1665 extra_fee: None,
1666 total_cost: U256::from(630000000000000u64),
1667 };
1668 mock_price_calculator
1669 .expect_get_transaction_price_params()
1670 .returning(move |_, _| Ok(price_params.clone()));
1671
1672 mock_signer.expect_sign_transaction().returning(|_| {
1673 Box::pin(ready(Ok(
1674 crate::domain::relayer::SignTransactionResponse::Evm(
1675 crate::domain::relayer::SignTransactionResponseEvm {
1676 hash: "0xtx_hash".to_string(),
1677 signature: crate::models::EvmTransactionDataSignature {
1678 r: "r".to_string(),
1679 s: "s".to_string(),
1680 v: 1,
1681 sig: "0xsignature".to_string(),
1682 },
1683 raw: vec![1, 2, 3],
1684 },
1685 ),
1686 )))
1687 });
1688
1689 mock_provider
1690 .expect_get_balance()
1691 .with(eq("0xSender"))
1692 .returning(|_| Box::pin(ready(Ok(U256::from(1000000000000000000u64)))));
1693
1694 mock_provider
1696 .expect_get_block_by_number()
1697 .times(1)
1698 .returning(|| {
1699 Box::pin(async {
1700 use alloy::{network::AnyRpcBlock, rpc::types::Block};
1701 let mut block: Block = Block::default();
1702 block.header.gas_limit = 30_000_000u64;
1704 Ok(AnyRpcBlock::from(block))
1705 })
1706 });
1707
1708 let test_tx_clone = test_tx.clone();
1709 mock_transaction
1710 .expect_partial_update()
1711 .returning(move |_, update| {
1712 let mut updated_tx = test_tx_clone.clone();
1713 if let Some(status) = &update.status {
1714 updated_tx.status = status.clone();
1715 }
1716 if let Some(network_data) = &update.network_data {
1717 updated_tx.network_data = network_data.clone();
1718 }
1719 if let Some(hashes) = &update.hashes {
1720 updated_tx.hashes = hashes.clone();
1721 }
1722 Ok(updated_tx)
1723 });
1724
1725 mock_job_producer
1726 .expect_produce_submit_transaction_job()
1727 .returning(|_, _| Box::pin(ready(Ok(()))));
1728 mock_job_producer
1729 .expect_produce_send_notification_job()
1730 .returning(|_, _| Box::pin(ready(Ok(()))));
1731
1732 let mock_network = MockNetworkRepository::new();
1733
1734 let evm_transaction = EvmRelayerTransaction {
1735 relayer: relayer.clone(),
1736 provider: mock_provider,
1737 relayer_repository: Arc::new(mock_relayer),
1738 network_repository: Arc::new(mock_network),
1739 transaction_repository: Arc::new(mock_transaction),
1740 transaction_counter_service: Arc::new(counter_service),
1741 job_producer: Arc::new(mock_job_producer),
1742 price_calculator: mock_price_calculator,
1743 signer: mock_signer,
1744 };
1745
1746 let result = evm_transaction.prepare_transaction(test_tx.clone()).await;
1747 assert!(result.is_ok(), "Expected Ok, got: {result:?}");
1748
1749 let prepared_tx = result.unwrap();
1750 assert_eq!(prepared_tx.status, TransactionStatus::Sent);
1752 assert!(!prepared_tx.hashes.is_empty());
1753 }
1754
1755 #[tokio::test]
1756 async fn test_cancel_transaction() {
1757 {
1759 let mut mock_transaction = MockTransactionRepository::new();
1761 let mock_relayer = MockRelayerRepository::new();
1762 let mock_provider = MockEvmProviderTrait::new();
1763 let mock_signer = MockSigner::new();
1764 let mut mock_job_producer = MockJobProducerTrait::new();
1765 let mock_price_calculator = MockPriceCalculator::new();
1766 let counter_service = MockTransactionCounterTrait::new();
1767
1768 let relayer = create_test_relayer();
1770 let mut test_tx = create_test_transaction();
1771 test_tx.status = TransactionStatus::Pending;
1772
1773 let test_tx_clone = test_tx.clone();
1775 mock_transaction
1776 .expect_partial_update()
1777 .withf(move |id, update| {
1778 id == "test-tx-id" && update.status == Some(TransactionStatus::Canceled)
1779 })
1780 .returning(move |_, update| {
1781 let mut updated_tx = test_tx_clone.clone();
1782 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1783 Ok(updated_tx)
1784 });
1785
1786 mock_job_producer
1788 .expect_produce_send_notification_job()
1789 .returning(|_, _| Box::pin(ready(Ok(()))));
1790
1791 let mock_network = MockNetworkRepository::new();
1792
1793 let evm_transaction = EvmRelayerTransaction {
1795 relayer: relayer.clone(),
1796 provider: mock_provider,
1797 relayer_repository: Arc::new(mock_relayer),
1798 network_repository: Arc::new(mock_network),
1799 transaction_repository: Arc::new(mock_transaction),
1800 transaction_counter_service: Arc::new(counter_service),
1801 job_producer: Arc::new(mock_job_producer),
1802 price_calculator: mock_price_calculator,
1803 signer: mock_signer,
1804 };
1805
1806 let result = evm_transaction.cancel_transaction(test_tx.clone()).await;
1808 assert!(result.is_ok());
1809 let cancelled_tx = result.unwrap();
1810 assert_eq!(cancelled_tx.id, "test-tx-id");
1811 assert_eq!(cancelled_tx.status, TransactionStatus::Canceled);
1812 }
1813
1814 {
1816 let mut mock_transaction = MockTransactionRepository::new();
1818 let mock_relayer = MockRelayerRepository::new();
1819 let mock_provider = MockEvmProviderTrait::new();
1820 let mut mock_signer = MockSigner::new();
1821 let mut mock_job_producer = MockJobProducerTrait::new();
1822 let mut mock_price_calculator = MockPriceCalculator::new();
1823 let counter_service = MockTransactionCounterTrait::new();
1824
1825 let relayer = create_test_relayer();
1827 let mut test_tx = create_test_transaction();
1828 test_tx.status = TransactionStatus::Submitted;
1829 test_tx.sent_at = Some(Utc::now().to_rfc3339());
1830 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
1831 nonce: Some(42),
1832 hash: Some("0xoriginal_hash".to_string()),
1833 ..test_tx.network_data.get_evm_transaction_data().unwrap()
1834 });
1835
1836 mock_price_calculator
1838 .expect_get_transaction_price_params()
1839 .return_once(move |_, _| {
1840 Ok(PriceParams {
1841 gas_price: Some(40000000000), max_fee_per_gas: None,
1843 max_priority_fee_per_gas: None,
1844 is_min_bumped: Some(true),
1845 extra_fee: Some(U256::ZERO),
1846 total_cost: U256::ZERO,
1847 })
1848 });
1849
1850 mock_signer.expect_sign_transaction().returning(|_| {
1852 Box::pin(ready(Ok(
1853 crate::domain::relayer::SignTransactionResponse::Evm(
1854 crate::domain::relayer::SignTransactionResponseEvm {
1855 hash: "0xcancellation_hash".to_string(),
1856 signature: crate::models::EvmTransactionDataSignature {
1857 r: "r".to_string(),
1858 s: "s".to_string(),
1859 v: 1,
1860 sig: "0xsignature".to_string(),
1861 },
1862 raw: vec![1, 2, 3],
1863 },
1864 ),
1865 )))
1866 });
1867
1868 let test_tx_clone = test_tx.clone();
1870 mock_transaction
1871 .expect_partial_update()
1872 .returning(move |tx_id, update| {
1873 let mut updated_tx = test_tx_clone.clone();
1874 updated_tx.id = tx_id;
1875 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1876 updated_tx.network_data =
1877 update.network_data.unwrap_or(updated_tx.network_data);
1878 if let Some(hashes) = update.hashes {
1879 updated_tx.hashes = hashes;
1880 }
1881 Ok(updated_tx)
1882 });
1883
1884 mock_job_producer
1886 .expect_produce_submit_transaction_job()
1887 .returning(|_, _| Box::pin(ready(Ok(()))));
1888 mock_job_producer
1889 .expect_produce_send_notification_job()
1890 .returning(|_, _| Box::pin(ready(Ok(()))));
1891
1892 let mut mock_network = MockNetworkRepository::new();
1894 mock_network
1895 .expect_get_by_chain_id()
1896 .with(eq(NetworkType::Evm), eq(1))
1897 .returning(|_, _| {
1898 use crate::config::{EvmNetworkConfig, NetworkConfigCommon};
1899 use crate::models::{NetworkConfigData, NetworkRepoModel, RpcConfig};
1900
1901 let config = EvmNetworkConfig {
1902 common: NetworkConfigCommon {
1903 network: "mainnet".to_string(),
1904 from: None,
1905 rpc_urls: Some(vec![RpcConfig::new(
1906 "https://rpc.example.com".to_string(),
1907 )]),
1908 explorer_urls: None,
1909 average_blocktime_ms: Some(12000),
1910 is_testnet: Some(false),
1911 tags: Some(vec!["mainnet".to_string()]),
1912 },
1913 chain_id: Some(1),
1914 required_confirmations: Some(12),
1915 features: Some(vec!["eip1559".to_string()]),
1916 symbol: Some("ETH".to_string()),
1917 gas_price_cache: None,
1918 };
1919 Ok(Some(NetworkRepoModel {
1920 id: "evm:mainnet".to_string(),
1921 name: "mainnet".to_string(),
1922 network_type: NetworkType::Evm,
1923 config: NetworkConfigData::Evm(config),
1924 }))
1925 });
1926
1927 let evm_transaction = EvmRelayerTransaction {
1929 relayer: relayer.clone(),
1930 provider: mock_provider,
1931 relayer_repository: Arc::new(mock_relayer),
1932 network_repository: Arc::new(mock_network),
1933 transaction_repository: Arc::new(mock_transaction),
1934 transaction_counter_service: Arc::new(counter_service),
1935 job_producer: Arc::new(mock_job_producer),
1936 price_calculator: mock_price_calculator,
1937 signer: mock_signer,
1938 };
1939
1940 let result = evm_transaction.cancel_transaction(test_tx.clone()).await;
1942 assert!(result.is_ok());
1943 let cancelled_tx = result.unwrap();
1944
1945 assert_eq!(cancelled_tx.id, "test-tx-id");
1947 assert_eq!(cancelled_tx.status, TransactionStatus::Submitted);
1948
1949 if let NetworkTransactionData::Evm(evm_data) = &cancelled_tx.network_data {
1951 assert_eq!(evm_data.nonce, Some(42)); } else {
1953 panic!("Expected EVM transaction data");
1954 }
1955 }
1956
1957 {
1959 let mock_transaction = MockTransactionRepository::new();
1961 let mock_relayer = MockRelayerRepository::new();
1962 let mock_provider = MockEvmProviderTrait::new();
1963 let mock_signer = MockSigner::new();
1964 let mock_job_producer = MockJobProducerTrait::new();
1965 let mock_price_calculator = MockPriceCalculator::new();
1966 let counter_service = MockTransactionCounterTrait::new();
1967
1968 let relayer = create_test_relayer();
1970 let mut test_tx = create_test_transaction();
1971 test_tx.status = TransactionStatus::Confirmed;
1972
1973 let mock_network = MockNetworkRepository::new();
1974
1975 let evm_transaction = EvmRelayerTransaction {
1977 relayer: relayer.clone(),
1978 provider: mock_provider,
1979 relayer_repository: Arc::new(mock_relayer),
1980 network_repository: Arc::new(mock_network),
1981 transaction_repository: Arc::new(mock_transaction),
1982 transaction_counter_service: Arc::new(counter_service),
1983 job_producer: Arc::new(mock_job_producer),
1984 price_calculator: mock_price_calculator,
1985 signer: mock_signer,
1986 };
1987
1988 let result = evm_transaction.cancel_transaction(test_tx.clone()).await;
1990 assert!(result.is_err());
1991 if let Err(TransactionError::ValidationError(msg)) = result {
1992 assert!(msg.contains("Invalid transaction state for cancel_transaction"));
1993 } else {
1994 panic!("Expected ValidationError");
1995 }
1996 }
1997 }
1998
1999 #[tokio::test]
2000 async fn test_replace_transaction() {
2001 {
2003 let mut mock_transaction = MockTransactionRepository::new();
2005 let mock_relayer = MockRelayerRepository::new();
2006 let mut mock_provider = MockEvmProviderTrait::new();
2007 let mut mock_signer = MockSigner::new();
2008 let mut mock_job_producer = MockJobProducerTrait::new();
2009 let mut mock_price_calculator = MockPriceCalculator::new();
2010 let counter_service = MockTransactionCounterTrait::new();
2011
2012 let relayer = create_test_relayer();
2014 let mut test_tx = create_test_transaction();
2015 test_tx.status = TransactionStatus::Submitted;
2016 test_tx.sent_at = Some(Utc::now().to_rfc3339());
2017
2018 mock_price_calculator
2020 .expect_get_transaction_price_params()
2021 .return_once(move |_, _| {
2022 Ok(PriceParams {
2023 gas_price: Some(40000000000), max_fee_per_gas: None,
2025 max_priority_fee_per_gas: None,
2026 is_min_bumped: Some(true),
2027 extra_fee: Some(U256::ZERO),
2028 total_cost: U256::from(2001000000000000000u64), })
2030 });
2031
2032 mock_signer.expect_sign_transaction().returning(|_| {
2034 Box::pin(ready(Ok(
2035 crate::domain::relayer::SignTransactionResponse::Evm(
2036 crate::domain::relayer::SignTransactionResponseEvm {
2037 hash: "0xreplacement_hash".to_string(),
2038 signature: crate::models::EvmTransactionDataSignature {
2039 r: "r".to_string(),
2040 s: "s".to_string(),
2041 v: 1,
2042 sig: "0xsignature".to_string(),
2043 },
2044 raw: vec![1, 2, 3],
2045 },
2046 ),
2047 )))
2048 });
2049
2050 mock_provider
2052 .expect_get_balance()
2053 .with(eq("0xSender"))
2054 .returning(|_| Box::pin(ready(Ok(U256::from(3000000000000000000u64)))));
2055
2056 let test_tx_clone = test_tx.clone();
2058 mock_transaction
2059 .expect_update_network_data()
2060 .returning(move |tx_id, network_data| {
2061 let mut updated_tx = test_tx_clone.clone();
2062 updated_tx.id = tx_id;
2063 updated_tx.network_data = network_data;
2064 Ok(updated_tx)
2065 });
2066
2067 mock_job_producer
2069 .expect_produce_submit_transaction_job()
2070 .returning(|_, _| Box::pin(ready(Ok(()))));
2071 mock_job_producer
2072 .expect_produce_send_notification_job()
2073 .returning(|_, _| Box::pin(ready(Ok(()))));
2074
2075 let mut mock_network = MockNetworkRepository::new();
2077 mock_network
2078 .expect_get_by_chain_id()
2079 .with(eq(NetworkType::Evm), eq(1))
2080 .returning(|_, _| {
2081 use crate::config::{EvmNetworkConfig, NetworkConfigCommon};
2082 use crate::models::{NetworkConfigData, NetworkRepoModel};
2083
2084 let config = EvmNetworkConfig {
2085 common: NetworkConfigCommon {
2086 network: "mainnet".to_string(),
2087 from: None,
2088 rpc_urls: Some(vec![crate::models::RpcConfig::new(
2089 "https://rpc.example.com".to_string(),
2090 )]),
2091 explorer_urls: None,
2092 average_blocktime_ms: Some(12000),
2093 is_testnet: Some(false),
2094 tags: Some(vec!["mainnet".to_string()]), },
2096 chain_id: Some(1),
2097 required_confirmations: Some(12),
2098 features: Some(vec!["eip1559".to_string()]),
2099 symbol: Some("ETH".to_string()),
2100 gas_price_cache: None,
2101 };
2102 Ok(Some(NetworkRepoModel {
2103 id: "evm:mainnet".to_string(),
2104 name: "mainnet".to_string(),
2105 network_type: NetworkType::Evm,
2106 config: NetworkConfigData::Evm(config),
2107 }))
2108 });
2109
2110 let evm_transaction = EvmRelayerTransaction {
2112 relayer: relayer.clone(),
2113 provider: mock_provider,
2114 relayer_repository: Arc::new(mock_relayer),
2115 network_repository: Arc::new(mock_network),
2116 transaction_repository: Arc::new(mock_transaction),
2117 transaction_counter_service: Arc::new(counter_service),
2118 job_producer: Arc::new(mock_job_producer),
2119 price_calculator: mock_price_calculator,
2120 signer: mock_signer,
2121 };
2122
2123 let replacement_request = NetworkTransactionRequest::Evm(EvmTransactionRequest {
2125 to: Some("0xNewRecipient".to_string()),
2126 value: U256::from(2000000000000000000u64), data: Some("0xNewData".to_string()),
2128 gas_limit: Some(25000),
2129 gas_price: None, max_fee_per_gas: None,
2131 max_priority_fee_per_gas: None,
2132 speed: Some(Speed::Fast),
2133 valid_until: None,
2134 });
2135
2136 let result = evm_transaction
2138 .replace_transaction(test_tx.clone(), replacement_request)
2139 .await;
2140 if let Err(ref e) = result {
2141 eprintln!("Replace transaction failed with error: {e:?}");
2142 }
2143 assert!(result.is_ok());
2144 let replaced_tx = result.unwrap();
2145
2146 assert_eq!(replaced_tx.id, "test-tx-id");
2148
2149 if let NetworkTransactionData::Evm(evm_data) = &replaced_tx.network_data {
2151 assert_eq!(evm_data.to, Some("0xNewRecipient".to_string()));
2152 assert_eq!(evm_data.value, U256::from(2000000000000000000u64));
2153 assert_eq!(evm_data.gas_price, Some(40000000000));
2154 assert_eq!(evm_data.gas_limit, Some(25000));
2155 assert!(evm_data.hash.is_some());
2156 assert!(evm_data.raw.is_some());
2157 } else {
2158 panic!("Expected EVM transaction data");
2159 }
2160 }
2161
2162 {
2164 let mock_transaction = MockTransactionRepository::new();
2166 let mock_relayer = MockRelayerRepository::new();
2167 let mock_provider = MockEvmProviderTrait::new();
2168 let mock_signer = MockSigner::new();
2169 let mock_job_producer = MockJobProducerTrait::new();
2170 let mock_price_calculator = MockPriceCalculator::new();
2171 let counter_service = MockTransactionCounterTrait::new();
2172
2173 let relayer = create_test_relayer();
2175 let mut test_tx = create_test_transaction();
2176 test_tx.status = TransactionStatus::Confirmed;
2177
2178 let mock_network = MockNetworkRepository::new();
2179
2180 let evm_transaction = EvmRelayerTransaction {
2182 relayer: relayer.clone(),
2183 provider: mock_provider,
2184 relayer_repository: Arc::new(mock_relayer),
2185 network_repository: Arc::new(mock_network),
2186 transaction_repository: Arc::new(mock_transaction),
2187 transaction_counter_service: Arc::new(counter_service),
2188 job_producer: Arc::new(mock_job_producer),
2189 price_calculator: mock_price_calculator,
2190 signer: mock_signer,
2191 };
2192
2193 let replacement_request = NetworkTransactionRequest::Evm(EvmTransactionRequest {
2195 to: Some("0xNewRecipient".to_string()),
2196 value: U256::from(1000000000000000000u64),
2197 data: Some("0xData".to_string()),
2198 gas_limit: Some(21000),
2199 gas_price: Some(30000000000),
2200 max_fee_per_gas: None,
2201 max_priority_fee_per_gas: None,
2202 speed: Some(Speed::Fast),
2203 valid_until: None,
2204 });
2205
2206 let result = evm_transaction
2208 .replace_transaction(test_tx.clone(), replacement_request)
2209 .await;
2210 assert!(result.is_err());
2211 if let Err(TransactionError::ValidationError(msg)) = result {
2212 assert!(msg.contains("Invalid transaction state for replace_transaction"));
2213 } else {
2214 panic!("Expected ValidationError");
2215 }
2216 }
2217 }
2218
2219 #[tokio::test]
2220 async fn test_estimate_tx_gas_limit_success() {
2221 let mock_transaction = MockTransactionRepository::new();
2222 let mock_relayer = MockRelayerRepository::new();
2223 let mut mock_provider = MockEvmProviderTrait::new();
2224 let mock_signer = MockSigner::new();
2225 let mock_job_producer = MockJobProducerTrait::new();
2226 let mock_price_calculator = MockPriceCalculator::new();
2227 let counter_service = MockTransactionCounterTrait::new();
2228 let mock_network = MockNetworkRepository::new();
2229
2230 let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2232 gas_limit_estimation: Some(true),
2233 ..Default::default()
2234 });
2235 let evm_data = EvmTransactionData {
2236 from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
2237 to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
2238 value: U256::from(1000000000000000000u128),
2239 data: Some("0x".to_string()),
2240 gas_limit: None,
2241 gas_price: Some(20_000_000_000),
2242 nonce: Some(1),
2243 chain_id: 1,
2244 hash: None,
2245 signature: None,
2246 speed: Some(Speed::Average),
2247 max_fee_per_gas: None,
2248 max_priority_fee_per_gas: None,
2249 raw: None,
2250 };
2251
2252 mock_provider
2254 .expect_estimate_gas()
2255 .times(1)
2256 .returning(|_| Box::pin(async { Ok(21000) }));
2257
2258 let transaction = EvmRelayerTransaction::new(
2259 relayer.clone(),
2260 mock_provider,
2261 Arc::new(mock_relayer),
2262 Arc::new(mock_network),
2263 Arc::new(mock_transaction),
2264 Arc::new(counter_service),
2265 Arc::new(mock_job_producer),
2266 mock_price_calculator,
2267 mock_signer,
2268 )
2269 .unwrap();
2270
2271 let result = transaction
2272 .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
2273 .await;
2274
2275 assert!(result.is_ok());
2276 assert_eq!(result.unwrap(), 23100);
2278 }
2279
2280 #[tokio::test]
2281 async fn test_estimate_tx_gas_limit_disabled() {
2282 let mock_transaction = MockTransactionRepository::new();
2283 let mock_relayer = MockRelayerRepository::new();
2284 let mut mock_provider = MockEvmProviderTrait::new();
2285 let mock_signer = MockSigner::new();
2286 let mock_job_producer = MockJobProducerTrait::new();
2287 let mock_price_calculator = MockPriceCalculator::new();
2288 let counter_service = MockTransactionCounterTrait::new();
2289 let mock_network = MockNetworkRepository::new();
2290
2291 let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2293 gas_limit_estimation: Some(false),
2294 ..Default::default()
2295 });
2296
2297 let evm_data = EvmTransactionData {
2298 from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
2299 to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
2300 value: U256::from(1000000000000000000u128),
2301 data: Some("0x".to_string()),
2302 gas_limit: None,
2303 gas_price: Some(20_000_000_000),
2304 nonce: Some(1),
2305 chain_id: 1,
2306 hash: None,
2307 signature: None,
2308 speed: Some(Speed::Average),
2309 max_fee_per_gas: None,
2310 max_priority_fee_per_gas: None,
2311 raw: None,
2312 };
2313
2314 mock_provider.expect_estimate_gas().times(0);
2316
2317 let transaction = EvmRelayerTransaction::new(
2318 relayer.clone(),
2319 mock_provider,
2320 Arc::new(mock_relayer),
2321 Arc::new(mock_network),
2322 Arc::new(mock_transaction),
2323 Arc::new(counter_service),
2324 Arc::new(mock_job_producer),
2325 mock_price_calculator,
2326 mock_signer,
2327 )
2328 .unwrap();
2329
2330 let result = transaction
2331 .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
2332 .await;
2333
2334 assert!(result.is_err());
2335 assert!(matches!(
2336 result.unwrap_err(),
2337 TransactionError::UnexpectedError(_)
2338 ));
2339 }
2340
2341 #[tokio::test]
2342 async fn test_estimate_tx_gas_limit_default_enabled() {
2343 let mock_transaction = MockTransactionRepository::new();
2344 let mock_relayer = MockRelayerRepository::new();
2345 let mut mock_provider = MockEvmProviderTrait::new();
2346 let mock_signer = MockSigner::new();
2347 let mock_job_producer = MockJobProducerTrait::new();
2348 let mock_price_calculator = MockPriceCalculator::new();
2349 let counter_service = MockTransactionCounterTrait::new();
2350 let mock_network = MockNetworkRepository::new();
2351
2352 let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2353 gas_limit_estimation: None, ..Default::default()
2355 });
2356
2357 let evm_data = EvmTransactionData {
2358 from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
2359 to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
2360 value: U256::from(1000000000000000000u128),
2361 data: Some("0x".to_string()),
2362 gas_limit: None,
2363 gas_price: Some(20_000_000_000),
2364 nonce: Some(1),
2365 chain_id: 1,
2366 hash: None,
2367 signature: None,
2368 speed: Some(Speed::Average),
2369 max_fee_per_gas: None,
2370 max_priority_fee_per_gas: None,
2371 raw: None,
2372 };
2373
2374 mock_provider
2376 .expect_estimate_gas()
2377 .times(1)
2378 .returning(|_| Box::pin(async { Ok(50000) }));
2379
2380 let transaction = EvmRelayerTransaction::new(
2381 relayer.clone(),
2382 mock_provider,
2383 Arc::new(mock_relayer),
2384 Arc::new(mock_network),
2385 Arc::new(mock_transaction),
2386 Arc::new(counter_service),
2387 Arc::new(mock_job_producer),
2388 mock_price_calculator,
2389 mock_signer,
2390 )
2391 .unwrap();
2392
2393 let result = transaction
2394 .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
2395 .await;
2396
2397 assert!(result.is_ok());
2398 assert_eq!(result.unwrap(), 55000);
2400 }
2401
2402 #[tokio::test]
2403 async fn test_estimate_tx_gas_limit_provider_error() {
2404 let mock_transaction = MockTransactionRepository::new();
2405 let mock_relayer = MockRelayerRepository::new();
2406 let mut mock_provider = MockEvmProviderTrait::new();
2407 let mock_signer = MockSigner::new();
2408 let mock_job_producer = MockJobProducerTrait::new();
2409 let mock_price_calculator = MockPriceCalculator::new();
2410 let counter_service = MockTransactionCounterTrait::new();
2411 let mock_network = MockNetworkRepository::new();
2412
2413 let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2414 gas_limit_estimation: Some(true),
2415 ..Default::default()
2416 });
2417
2418 let evm_data = EvmTransactionData {
2419 from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
2420 to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
2421 value: U256::from(1000000000000000000u128),
2422 data: Some("0x".to_string()),
2423 gas_limit: None,
2424 gas_price: Some(20_000_000_000),
2425 nonce: Some(1),
2426 chain_id: 1,
2427 hash: None,
2428 signature: None,
2429 speed: Some(Speed::Average),
2430 max_fee_per_gas: None,
2431 max_priority_fee_per_gas: None,
2432 raw: None,
2433 };
2434
2435 mock_provider.expect_estimate_gas().times(1).returning(|_| {
2437 Box::pin(async {
2438 Err(crate::services::provider::ProviderError::Other(
2439 "RPC error".to_string(),
2440 ))
2441 })
2442 });
2443
2444 let transaction = EvmRelayerTransaction::new(
2445 relayer.clone(),
2446 mock_provider,
2447 Arc::new(mock_relayer),
2448 Arc::new(mock_network),
2449 Arc::new(mock_transaction),
2450 Arc::new(counter_service),
2451 Arc::new(mock_job_producer),
2452 mock_price_calculator,
2453 mock_signer,
2454 )
2455 .unwrap();
2456
2457 let result = transaction
2458 .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
2459 .await;
2460
2461 assert!(result.is_err());
2462 assert!(matches!(
2463 result.unwrap_err(),
2464 TransactionError::UnexpectedError(_)
2465 ));
2466 }
2467
2468 #[tokio::test]
2469 async fn test_prepare_transaction_uses_gas_estimation_and_stores_result() {
2470 let mut mock_transaction = MockTransactionRepository::new();
2471 let mock_relayer = MockRelayerRepository::new();
2472 let mut mock_provider = MockEvmProviderTrait::new();
2473 let mut mock_signer = MockSigner::new();
2474 let mut mock_job_producer = MockJobProducerTrait::new();
2475 let mut mock_price_calculator = MockPriceCalculator::new();
2476 let mut counter_service = MockTransactionCounterTrait::new();
2477 let mock_network = MockNetworkRepository::new();
2478
2479 let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2481 gas_limit_estimation: Some(true),
2482 min_balance: Some(100000000000000000u128),
2483 ..Default::default()
2484 });
2485
2486 let mut test_tx = create_test_transaction();
2488 if let NetworkTransactionData::Evm(ref mut evm_data) = test_tx.network_data {
2489 evm_data.gas_limit = None; evm_data.nonce = None; }
2492
2493 const PROVIDER_GAS_ESTIMATE: u64 = 45000;
2495 const EXPECTED_GAS_WITH_BUFFER: u64 = 49500; mock_provider
2499 .expect_estimate_gas()
2500 .times(1)
2501 .returning(move |_| Box::pin(async move { Ok(PROVIDER_GAS_ESTIMATE) }));
2502
2503 mock_provider
2505 .expect_get_balance()
2506 .times(1)
2507 .returning(|_| Box::pin(async { Ok(U256::from(2000000000000000000u128)) })); let price_params = PriceParams {
2510 gas_price: Some(20_000_000_000), max_fee_per_gas: None,
2512 max_priority_fee_per_gas: None,
2513 is_min_bumped: None,
2514 extra_fee: None,
2515 total_cost: U256::from(1900000000000000000u128), };
2517
2518 mock_price_calculator
2520 .expect_get_transaction_price_params()
2521 .returning(move |_, _| Ok(price_params.clone()));
2522
2523 counter_service
2525 .expect_get_and_increment()
2526 .times(1)
2527 .returning(|_, _| Box::pin(async { Ok(42) }));
2528
2529 mock_signer.expect_sign_transaction().returning(|_| {
2531 Box::pin(ready(Ok(
2532 crate::domain::relayer::SignTransactionResponse::Evm(
2533 crate::domain::relayer::SignTransactionResponseEvm {
2534 hash: "0xhash".to_string(),
2535 signature: crate::models::EvmTransactionDataSignature {
2536 r: "r".to_string(),
2537 s: "s".to_string(),
2538 v: 1,
2539 sig: "0xsignature".to_string(),
2540 },
2541 raw: vec![1, 2, 3],
2542 },
2543 ),
2544 )))
2545 });
2546
2547 mock_job_producer
2549 .expect_produce_submit_transaction_job()
2550 .returning(|_, _| Box::pin(async { Ok(()) }));
2551
2552 mock_job_producer
2553 .expect_produce_send_notification_job()
2554 .returning(|_, _| Box::pin(ready(Ok(()))));
2555
2556 let expected_gas_limit = EXPECTED_GAS_WITH_BUFFER;
2561
2562 let test_tx_clone = test_tx.clone();
2563 mock_transaction
2564 .expect_partial_update()
2565 .times(2)
2566 .returning(move |_, update| {
2567 let mut updated_tx = test_tx_clone.clone();
2568
2569 if let Some(status) = &update.status {
2571 updated_tx.status = status.clone();
2572 }
2573 if let Some(network_data) = &update.network_data {
2574 updated_tx.network_data = network_data.clone();
2575 } else {
2576 if let NetworkTransactionData::Evm(ref mut evm_data) = updated_tx.network_data {
2578 if evm_data.gas_limit.is_none() {
2579 evm_data.gas_limit = Some(expected_gas_limit);
2580 }
2581 }
2582 }
2583 if let Some(hashes) = &update.hashes {
2584 updated_tx.hashes = hashes.clone();
2585 }
2586
2587 Ok(updated_tx)
2588 });
2589
2590 let transaction = EvmRelayerTransaction::new(
2591 relayer.clone(),
2592 mock_provider,
2593 Arc::new(mock_relayer),
2594 Arc::new(mock_network),
2595 Arc::new(mock_transaction),
2596 Arc::new(counter_service),
2597 Arc::new(mock_job_producer),
2598 mock_price_calculator,
2599 mock_signer,
2600 )
2601 .unwrap();
2602
2603 let result = transaction.prepare_transaction(test_tx).await;
2605
2606 assert!(result.is_ok(), "prepare_transaction should succeed");
2608 let prepared_tx = result.unwrap();
2609
2610 if let NetworkTransactionData::Evm(evm_data) = prepared_tx.network_data {
2612 assert_eq!(evm_data.gas_limit, Some(EXPECTED_GAS_WITH_BUFFER));
2613 } else {
2614 panic!("Expected EVM network data");
2615 }
2616 }
2617
2618 #[test]
2619 fn test_is_already_submitted_error_detection() {
2620 assert!(DefaultEvmTransaction::is_already_submitted_error(
2622 &"already known"
2623 ));
2624 assert!(DefaultEvmTransaction::is_already_submitted_error(
2625 &"Transaction already known"
2626 ));
2627 assert!(DefaultEvmTransaction::is_already_submitted_error(
2628 &"Error: already known"
2629 ));
2630
2631 assert!(DefaultEvmTransaction::is_already_submitted_error(
2633 &"nonce too low"
2634 ));
2635 assert!(DefaultEvmTransaction::is_already_submitted_error(
2636 &"Nonce Too Low"
2637 ));
2638 assert!(DefaultEvmTransaction::is_already_submitted_error(
2639 &"Error: nonce too low"
2640 ));
2641
2642 assert!(DefaultEvmTransaction::is_already_submitted_error(
2644 &"replacement transaction underpriced"
2645 ));
2646 assert!(DefaultEvmTransaction::is_already_submitted_error(
2647 &"Replacement Transaction Underpriced"
2648 ));
2649
2650 assert!(!DefaultEvmTransaction::is_already_submitted_error(
2652 &"insufficient funds"
2653 ));
2654 assert!(!DefaultEvmTransaction::is_already_submitted_error(
2655 &"execution reverted"
2656 ));
2657 assert!(!DefaultEvmTransaction::is_already_submitted_error(
2658 &"gas too low"
2659 ));
2660 assert!(!DefaultEvmTransaction::is_already_submitted_error(
2661 &"timeout"
2662 ));
2663 }
2664
2665 #[tokio::test]
2668 async fn test_submit_transaction_already_known_error_from_sent() {
2669 let mut mock_transaction = MockTransactionRepository::new();
2670 let mock_relayer = MockRelayerRepository::new();
2671 let mut mock_provider = MockEvmProviderTrait::new();
2672 let mock_signer = MockSigner::new();
2673 let mut mock_job_producer = MockJobProducerTrait::new();
2674 let mock_price_calculator = MockPriceCalculator::new();
2675 let counter_service = MockTransactionCounterTrait::new();
2676 let mock_network = MockNetworkRepository::new();
2677
2678 let relayer = create_test_relayer();
2679 let mut test_tx = create_test_transaction();
2680 test_tx.status = TransactionStatus::Sent;
2681 test_tx.sent_at = Some(Utc::now().to_rfc3339());
2682 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
2683 nonce: Some(42),
2684 hash: Some("0xhash".to_string()),
2685 raw: Some(vec![1, 2, 3]),
2686 ..test_tx.network_data.get_evm_transaction_data().unwrap()
2687 });
2688
2689 mock_provider
2691 .expect_send_raw_transaction()
2692 .times(1)
2693 .returning(|_| {
2694 Box::pin(async {
2695 Err(crate::services::provider::ProviderError::Other(
2696 "already known: transaction already in mempool".to_string(),
2697 ))
2698 })
2699 });
2700
2701 let test_tx_clone = test_tx.clone();
2703 mock_transaction
2704 .expect_partial_update()
2705 .times(1)
2706 .withf(|_, update| update.status == Some(TransactionStatus::Submitted))
2707 .returning(move |_, update| {
2708 let mut updated_tx = test_tx_clone.clone();
2709 updated_tx.status = update.status.unwrap();
2710 updated_tx.sent_at = update.sent_at.clone();
2711 Ok(updated_tx)
2712 });
2713
2714 mock_job_producer
2715 .expect_produce_send_notification_job()
2716 .times(1)
2717 .returning(|_, _| Box::pin(ready(Ok(()))));
2718
2719 let evm_transaction = EvmRelayerTransaction {
2720 relayer: relayer.clone(),
2721 provider: mock_provider,
2722 relayer_repository: Arc::new(mock_relayer),
2723 network_repository: Arc::new(mock_network),
2724 transaction_repository: Arc::new(mock_transaction),
2725 transaction_counter_service: Arc::new(counter_service),
2726 job_producer: Arc::new(mock_job_producer),
2727 price_calculator: mock_price_calculator,
2728 signer: mock_signer,
2729 };
2730
2731 let result = evm_transaction.submit_transaction(test_tx).await;
2732 assert!(result.is_ok());
2733 let updated_tx = result.unwrap();
2734 assert_eq!(updated_tx.status, TransactionStatus::Submitted);
2735 }
2736
2737 #[tokio::test]
2739 async fn test_submit_transaction_real_error_fails() {
2740 let mock_transaction = MockTransactionRepository::new();
2741 let mock_relayer = MockRelayerRepository::new();
2742 let mut mock_provider = MockEvmProviderTrait::new();
2743 let mock_signer = MockSigner::new();
2744 let mock_job_producer = MockJobProducerTrait::new();
2745 let mock_price_calculator = MockPriceCalculator::new();
2746 let counter_service = MockTransactionCounterTrait::new();
2747 let mock_network = MockNetworkRepository::new();
2748
2749 let relayer = create_test_relayer();
2750 let mut test_tx = create_test_transaction();
2751 test_tx.status = TransactionStatus::Sent;
2752 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
2753 raw: Some(vec![1, 2, 3]),
2754 ..test_tx.network_data.get_evm_transaction_data().unwrap()
2755 });
2756
2757 mock_provider
2759 .expect_send_raw_transaction()
2760 .times(1)
2761 .returning(|_| {
2762 Box::pin(async {
2763 Err(crate::services::provider::ProviderError::Other(
2764 "insufficient funds for gas * price + value".to_string(),
2765 ))
2766 })
2767 });
2768
2769 let evm_transaction = EvmRelayerTransaction {
2770 relayer: relayer.clone(),
2771 provider: mock_provider,
2772 relayer_repository: Arc::new(mock_relayer),
2773 network_repository: Arc::new(mock_network),
2774 transaction_repository: Arc::new(mock_transaction),
2775 transaction_counter_service: Arc::new(counter_service),
2776 job_producer: Arc::new(mock_job_producer),
2777 price_calculator: mock_price_calculator,
2778 signer: mock_signer,
2779 };
2780
2781 let result = evm_transaction.submit_transaction(test_tx).await;
2782 assert!(result.is_err());
2783 }
2784
2785 #[tokio::test]
2788 async fn test_resubmit_transaction_already_submitted_preserves_hash() {
2789 let mut mock_transaction = MockTransactionRepository::new();
2790 let mock_relayer = MockRelayerRepository::new();
2791 let mut mock_provider = MockEvmProviderTrait::new();
2792 let mut mock_signer = MockSigner::new();
2793 let mock_job_producer = MockJobProducerTrait::new();
2794 let mut mock_price_calculator = MockPriceCalculator::new();
2795 let counter_service = MockTransactionCounterTrait::new();
2796 let mock_network = MockNetworkRepository::new();
2797
2798 let relayer = create_test_relayer();
2799 let mut test_tx = create_test_transaction();
2800 test_tx.status = TransactionStatus::Submitted;
2801 test_tx.sent_at = Some(Utc::now().to_rfc3339());
2802 let original_hash = "0xoriginal_hash".to_string();
2803 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
2804 nonce: Some(42),
2805 hash: Some(original_hash.clone()),
2806 raw: Some(vec![1, 2, 3]),
2807 ..test_tx.network_data.get_evm_transaction_data().unwrap()
2808 });
2809 test_tx.hashes = vec![original_hash.clone()];
2810
2811 mock_price_calculator
2813 .expect_calculate_bumped_gas_price()
2814 .times(1)
2815 .returning(|_, _, _| {
2816 Ok(PriceParams {
2817 gas_price: Some(25000000000), max_fee_per_gas: None,
2819 max_priority_fee_per_gas: None,
2820 is_min_bumped: Some(true),
2821 extra_fee: None,
2822 total_cost: U256::from(525000000000000u64),
2823 })
2824 });
2825
2826 mock_provider
2828 .expect_get_balance()
2829 .times(1)
2830 .returning(|_| Box::pin(async { Ok(U256::from(1000000000000000000u64)) }));
2831
2832 mock_signer
2834 .expect_sign_transaction()
2835 .times(1)
2836 .returning(|_| {
2837 Box::pin(ready(Ok(
2838 crate::domain::relayer::SignTransactionResponse::Evm(
2839 crate::domain::relayer::SignTransactionResponseEvm {
2840 hash: "0xnew_hash_that_should_not_be_saved".to_string(),
2841 signature: crate::models::EvmTransactionDataSignature {
2842 r: "r".to_string(),
2843 s: "s".to_string(),
2844 v: 1,
2845 sig: "0xsignature".to_string(),
2846 },
2847 raw: vec![4, 5, 6],
2848 },
2849 ),
2850 )))
2851 });
2852
2853 mock_provider
2855 .expect_send_raw_transaction()
2856 .times(1)
2857 .returning(|_| {
2858 Box::pin(async {
2859 Err(crate::services::provider::ProviderError::Other(
2860 "already known: transaction with same nonce already in mempool".to_string(),
2861 ))
2862 })
2863 });
2864
2865 let test_tx_clone = test_tx.clone();
2867 mock_transaction
2868 .expect_partial_update()
2869 .times(1)
2870 .withf(|_, update| {
2871 update.status == Some(TransactionStatus::Submitted)
2873 && update.network_data.is_none()
2874 && update.hashes.is_none()
2875 })
2876 .returning(move |_, _| {
2877 let mut updated_tx = test_tx_clone.clone();
2878 updated_tx.status = TransactionStatus::Submitted;
2879 Ok(updated_tx)
2881 });
2882
2883 let evm_transaction = EvmRelayerTransaction {
2884 relayer: relayer.clone(),
2885 provider: mock_provider,
2886 relayer_repository: Arc::new(mock_relayer),
2887 network_repository: Arc::new(mock_network),
2888 transaction_repository: Arc::new(mock_transaction),
2889 transaction_counter_service: Arc::new(counter_service),
2890 job_producer: Arc::new(mock_job_producer),
2891 price_calculator: mock_price_calculator,
2892 signer: mock_signer,
2893 };
2894
2895 let result = evm_transaction.resubmit_transaction(test_tx.clone()).await;
2896 assert!(result.is_ok());
2897 let updated_tx = result.unwrap();
2898
2899 if let NetworkTransactionData::Evm(evm_data) = &updated_tx.network_data {
2901 assert_eq!(evm_data.hash, Some(original_hash));
2902 } else {
2903 panic!("Expected EVM network data");
2904 }
2905 }
2906
2907 #[tokio::test]
2910 async fn test_submit_transaction_db_failure_after_blockchain_success() {
2911 let mut mock_transaction = MockTransactionRepository::new();
2912 let mock_relayer = MockRelayerRepository::new();
2913 let mut mock_provider = MockEvmProviderTrait::new();
2914 let mock_signer = MockSigner::new();
2915 let mut mock_job_producer = MockJobProducerTrait::new();
2916 let mock_price_calculator = MockPriceCalculator::new();
2917 let counter_service = MockTransactionCounterTrait::new();
2918 let mock_network = MockNetworkRepository::new();
2919
2920 let relayer = create_test_relayer();
2921 let mut test_tx = create_test_transaction();
2922 test_tx.status = TransactionStatus::Sent;
2923 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
2924 raw: Some(vec![1, 2, 3]),
2925 ..test_tx.network_data.get_evm_transaction_data().unwrap()
2926 });
2927
2928 mock_provider
2930 .expect_send_raw_transaction()
2931 .times(1)
2932 .returning(|_| Box::pin(async { Ok("0xsubmitted_hash".to_string()) }));
2933
2934 mock_transaction
2936 .expect_partial_update()
2937 .times(1)
2938 .returning(|_, _| {
2939 Err(crate::models::RepositoryError::UnexpectedError(
2940 "Redis timeout".to_string(),
2941 ))
2942 });
2943
2944 mock_job_producer
2946 .expect_produce_send_notification_job()
2947 .times(1)
2948 .returning(|_, _| Box::pin(ready(Ok(()))));
2949
2950 let evm_transaction = EvmRelayerTransaction {
2951 relayer: relayer.clone(),
2952 provider: mock_provider,
2953 relayer_repository: Arc::new(mock_relayer),
2954 network_repository: Arc::new(mock_network),
2955 transaction_repository: Arc::new(mock_transaction),
2956 transaction_counter_service: Arc::new(counter_service),
2957 job_producer: Arc::new(mock_job_producer),
2958 price_calculator: mock_price_calculator,
2959 signer: mock_signer,
2960 };
2961
2962 let result = evm_transaction.submit_transaction(test_tx.clone()).await;
2963 assert!(result.is_ok());
2965 let returned_tx = result.unwrap();
2966 assert_eq!(returned_tx.id, test_tx.id);
2968 assert_eq!(returned_tx.status, TransactionStatus::Sent); }
2970
2971 #[tokio::test]
2973 async fn test_send_transaction_resend_job_success() {
2974 let mock_transaction = MockTransactionRepository::new();
2975 let mock_relayer = MockRelayerRepository::new();
2976 let mock_provider = MockEvmProviderTrait::new();
2977 let mock_signer = MockSigner::new();
2978 let mut mock_job_producer = MockJobProducerTrait::new();
2979 let mock_price_calculator = MockPriceCalculator::new();
2980 let counter_service = MockTransactionCounterTrait::new();
2981 let mock_network = MockNetworkRepository::new();
2982
2983 let relayer = create_test_relayer();
2984 let test_tx = create_test_transaction();
2985
2986 mock_job_producer
2988 .expect_produce_submit_transaction_job()
2989 .times(1)
2990 .withf(|job, delay| {
2991 job.transaction_id == "test-tx-id"
2993 && job.relayer_id == "test-relayer-id"
2994 && matches!(job.command, crate::jobs::TransactionCommand::Resend)
2995 && delay.is_none()
2996 })
2997 .returning(|_, _| Box::pin(ready(Ok(()))));
2998
2999 let evm_transaction = EvmRelayerTransaction {
3000 relayer: relayer.clone(),
3001 provider: mock_provider,
3002 relayer_repository: Arc::new(mock_relayer),
3003 network_repository: Arc::new(mock_network),
3004 transaction_repository: Arc::new(mock_transaction),
3005 transaction_counter_service: Arc::new(counter_service),
3006 job_producer: Arc::new(mock_job_producer),
3007 price_calculator: mock_price_calculator,
3008 signer: mock_signer,
3009 };
3010
3011 let result = evm_transaction.send_transaction_resend_job(&test_tx).await;
3012 assert!(result.is_ok());
3013 }
3014
3015 #[tokio::test]
3017 async fn test_send_transaction_resend_job_failure() {
3018 let mock_transaction = MockTransactionRepository::new();
3019 let mock_relayer = MockRelayerRepository::new();
3020 let mock_provider = MockEvmProviderTrait::new();
3021 let mock_signer = MockSigner::new();
3022 let mut mock_job_producer = MockJobProducerTrait::new();
3023 let mock_price_calculator = MockPriceCalculator::new();
3024 let counter_service = MockTransactionCounterTrait::new();
3025 let mock_network = MockNetworkRepository::new();
3026
3027 let relayer = create_test_relayer();
3028 let test_tx = create_test_transaction();
3029
3030 mock_job_producer
3032 .expect_produce_submit_transaction_job()
3033 .times(1)
3034 .returning(|_, _| {
3035 Box::pin(ready(Err(crate::jobs::JobProducerError::QueueError(
3036 "Job queue is full".to_string(),
3037 ))))
3038 });
3039
3040 let evm_transaction = EvmRelayerTransaction {
3041 relayer: relayer.clone(),
3042 provider: mock_provider,
3043 relayer_repository: Arc::new(mock_relayer),
3044 network_repository: Arc::new(mock_network),
3045 transaction_repository: Arc::new(mock_transaction),
3046 transaction_counter_service: Arc::new(counter_service),
3047 job_producer: Arc::new(mock_job_producer),
3048 price_calculator: mock_price_calculator,
3049 signer: mock_signer,
3050 };
3051
3052 let result = evm_transaction.send_transaction_resend_job(&test_tx).await;
3053 assert!(result.is_err());
3054 let err = result.unwrap_err();
3055 match err {
3056 TransactionError::UnexpectedError(msg) => {
3057 assert!(msg.contains("Failed to produce resend job"));
3058 }
3059 _ => panic!("Expected UnexpectedError"),
3060 }
3061 }
3062
3063 #[tokio::test]
3065 async fn test_send_transaction_request_job_success() {
3066 let mock_transaction = MockTransactionRepository::new();
3067 let mock_relayer = MockRelayerRepository::new();
3068 let mock_provider = MockEvmProviderTrait::new();
3069 let mock_signer = MockSigner::new();
3070 let mut mock_job_producer = MockJobProducerTrait::new();
3071 let mock_price_calculator = MockPriceCalculator::new();
3072 let counter_service = MockTransactionCounterTrait::new();
3073 let mock_network = MockNetworkRepository::new();
3074
3075 let relayer = create_test_relayer();
3076 let test_tx = create_test_transaction();
3077
3078 mock_job_producer
3080 .expect_produce_transaction_request_job()
3081 .times(1)
3082 .withf(|job, delay| {
3083 job.transaction_id == "test-tx-id"
3085 && job.relayer_id == "test-relayer-id"
3086 && delay.is_none()
3087 })
3088 .returning(|_, _| Box::pin(ready(Ok(()))));
3089
3090 let evm_transaction = EvmRelayerTransaction {
3091 relayer: relayer.clone(),
3092 provider: mock_provider,
3093 relayer_repository: Arc::new(mock_relayer),
3094 network_repository: Arc::new(mock_network),
3095 transaction_repository: Arc::new(mock_transaction),
3096 transaction_counter_service: Arc::new(counter_service),
3097 job_producer: Arc::new(mock_job_producer),
3098 price_calculator: mock_price_calculator,
3099 signer: mock_signer,
3100 };
3101
3102 let result = evm_transaction.send_transaction_request_job(&test_tx).await;
3103 assert!(result.is_ok());
3104 }
3105
3106 #[tokio::test]
3108 async fn test_send_transaction_request_job_failure() {
3109 let mock_transaction = MockTransactionRepository::new();
3110 let mock_relayer = MockRelayerRepository::new();
3111 let mock_provider = MockEvmProviderTrait::new();
3112 let mock_signer = MockSigner::new();
3113 let mut mock_job_producer = MockJobProducerTrait::new();
3114 let mock_price_calculator = MockPriceCalculator::new();
3115 let counter_service = MockTransactionCounterTrait::new();
3116 let mock_network = MockNetworkRepository::new();
3117
3118 let relayer = create_test_relayer();
3119 let test_tx = create_test_transaction();
3120
3121 mock_job_producer
3123 .expect_produce_transaction_request_job()
3124 .times(1)
3125 .returning(|_, _| {
3126 Box::pin(ready(Err(crate::jobs::JobProducerError::QueueError(
3127 "Redis connection failed".to_string(),
3128 ))))
3129 });
3130
3131 let evm_transaction = EvmRelayerTransaction {
3132 relayer: relayer.clone(),
3133 provider: mock_provider,
3134 relayer_repository: Arc::new(mock_relayer),
3135 network_repository: Arc::new(mock_network),
3136 transaction_repository: Arc::new(mock_transaction),
3137 transaction_counter_service: Arc::new(counter_service),
3138 job_producer: Arc::new(mock_job_producer),
3139 price_calculator: mock_price_calculator,
3140 signer: mock_signer,
3141 };
3142
3143 let result = evm_transaction.send_transaction_request_job(&test_tx).await;
3144 assert!(result.is_err());
3145 let err = result.unwrap_err();
3146 match err {
3147 TransactionError::UnexpectedError(msg) => {
3148 assert!(msg.contains("Failed to produce request job"));
3149 }
3150 _ => panic!("Expected UnexpectedError"),
3151 }
3152 }
3153
3154 #[tokio::test]
3156 async fn test_resubmit_transaction_sent_to_submitted() {
3157 let mut mock_transaction = MockTransactionRepository::new();
3158 let mock_relayer = MockRelayerRepository::new();
3159 let mut mock_provider = MockEvmProviderTrait::new();
3160 let mut mock_signer = MockSigner::new();
3161 let mock_job_producer = MockJobProducerTrait::new();
3162 let mut mock_price_calculator = MockPriceCalculator::new();
3163 let counter_service = MockTransactionCounterTrait::new();
3164 let mock_network = MockNetworkRepository::new();
3165
3166 let relayer = create_test_relayer();
3167 let mut test_tx = create_test_transaction();
3168 test_tx.status = TransactionStatus::Sent;
3169 test_tx.sent_at = Some(Utc::now().to_rfc3339());
3170 let original_hash = "0xoriginal_hash".to_string();
3171 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
3172 nonce: Some(42),
3173 hash: Some(original_hash.clone()),
3174 raw: Some(vec![1, 2, 3]),
3175 gas_price: Some(20000000000), ..test_tx.network_data.get_evm_transaction_data().unwrap()
3177 });
3178 test_tx.hashes = vec![original_hash.clone()];
3179
3180 mock_price_calculator
3182 .expect_calculate_bumped_gas_price()
3183 .times(1)
3184 .returning(|_, _, _| {
3185 Ok(PriceParams {
3186 gas_price: Some(25000000000), max_fee_per_gas: None,
3188 max_priority_fee_per_gas: None,
3189 is_min_bumped: Some(true),
3190 extra_fee: None,
3191 total_cost: U256::from(525000000000000u64),
3192 })
3193 });
3194
3195 mock_provider
3197 .expect_get_balance()
3198 .returning(|_| Box::pin(ready(Ok(U256::from(1000000000000000000u64)))));
3199
3200 mock_signer.expect_sign_transaction().returning(|_| {
3202 Box::pin(ready(Ok(
3203 crate::domain::relayer::SignTransactionResponse::Evm(
3204 crate::domain::relayer::SignTransactionResponseEvm {
3205 hash: "0xnew_hash".to_string(),
3206 signature: crate::models::EvmTransactionDataSignature {
3207 r: "r".to_string(),
3208 s: "s".to_string(),
3209 v: 1,
3210 sig: "0xsignature".to_string(),
3211 },
3212 raw: vec![4, 5, 6],
3213 },
3214 ),
3215 )))
3216 });
3217
3218 mock_provider
3220 .expect_send_raw_transaction()
3221 .times(1)
3222 .returning(|_| Box::pin(async { Ok("0xnew_hash".to_string()) }));
3223
3224 let test_tx_clone = test_tx.clone();
3226 mock_transaction
3227 .expect_partial_update()
3228 .times(1)
3229 .withf(|_, update| {
3230 update.status == Some(TransactionStatus::Submitted)
3231 && update.sent_at.is_some()
3232 && update.priced_at.is_some()
3233 && update.hashes.is_some()
3234 })
3235 .returning(move |_, update| {
3236 let mut updated_tx = test_tx_clone.clone();
3237 updated_tx.status = update.status.unwrap();
3238 updated_tx.sent_at = update.sent_at.clone();
3239 updated_tx.priced_at = update.priced_at.clone();
3240 if let Some(hashes) = update.hashes.clone() {
3241 updated_tx.hashes = hashes;
3242 }
3243 if let Some(network_data) = update.network_data.clone() {
3244 updated_tx.network_data = network_data;
3245 }
3246 Ok(updated_tx)
3247 });
3248
3249 let evm_transaction = EvmRelayerTransaction {
3250 relayer: relayer.clone(),
3251 provider: mock_provider,
3252 relayer_repository: Arc::new(mock_relayer),
3253 network_repository: Arc::new(mock_network),
3254 transaction_repository: Arc::new(mock_transaction),
3255 transaction_counter_service: Arc::new(counter_service),
3256 job_producer: Arc::new(mock_job_producer),
3257 price_calculator: mock_price_calculator,
3258 signer: mock_signer,
3259 };
3260
3261 let result = evm_transaction.resubmit_transaction(test_tx.clone()).await;
3262 assert!(result.is_ok(), "Expected Ok, got: {result:?}");
3263 let updated_tx = result.unwrap();
3264 assert_eq!(
3265 updated_tx.status,
3266 TransactionStatus::Submitted,
3267 "Transaction status should transition from Sent to Submitted"
3268 );
3269 }
3270}