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