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