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