1use async_trait::async_trait;
8use chrono::Utc;
9use eyre::Result;
10use std::sync::Arc;
11use tracing::{debug, error, info, warn};
12
13use crate::{
14 constants::{DEFAULT_EVM_GAS_LIMIT_ESTIMATION, GAS_LIMIT_BUFFER_MULTIPLIER},
15 domain::{
16 transaction::{
17 evm::{ensure_status, ensure_status_one_of, PriceCalculator, PriceCalculatorTrait},
18 Transaction,
19 },
20 EvmTransactionValidationError, EvmTransactionValidator,
21 },
22 jobs::{JobProducer, JobProducerTrait, TransactionSend, TransactionStatusCheck},
23 models::{
24 produce_transaction_update_notification_payload, EvmNetwork, EvmTransactionData,
25 NetworkRepoModel, NetworkTransactionData, NetworkTransactionRequest, NetworkType,
26 RelayerEvmPolicy, RelayerRepoModel, TransactionError, TransactionRepoModel,
27 TransactionStatus, TransactionUpdateRequest,
28 },
29 repositories::{
30 NetworkRepository, NetworkRepositoryStorage, RelayerRepository, RelayerRepositoryStorage,
31 Repository, TransactionCounterRepositoryStorage, TransactionCounterTrait,
32 TransactionRepository, TransactionRepositoryStorage,
33 },
34 services::{
35 gas::evm_gas_price::EvmGasPriceService,
36 provider::{EvmProvider, EvmProviderTrait},
37 signer::{EvmSigner, Signer},
38 },
39 utils::{calculate_scheduled_timestamp, get_evm_default_gas_limit_for_tx},
40};
41
42use super::PriceParams;
43
44#[allow(dead_code)]
45pub struct EvmRelayerTransaction<P, RR, NR, TR, J, S, TCR, PC>
46where
47 P: EvmProviderTrait,
48 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
49 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
50 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
51 J: JobProducerTrait + Send + Sync + 'static,
52 S: Signer + Send + Sync + 'static,
53 TCR: TransactionCounterTrait + Send + Sync + 'static,
54 PC: PriceCalculatorTrait,
55{
56 provider: P,
57 relayer_repository: Arc<RR>,
58 network_repository: Arc<NR>,
59 transaction_repository: Arc<TR>,
60 job_producer: Arc<J>,
61 signer: S,
62 relayer: RelayerRepoModel,
63 transaction_counter_service: Arc<TCR>,
64 price_calculator: PC,
65}
66
67#[allow(dead_code, clippy::too_many_arguments)]
68impl<P, RR, NR, TR, J, S, TCR, PC> EvmRelayerTransaction<P, RR, NR, TR, J, S, TCR, PC>
69where
70 P: EvmProviderTrait,
71 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
72 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
73 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
74 J: JobProducerTrait + Send + Sync + 'static,
75 S: Signer + Send + Sync + 'static,
76 TCR: TransactionCounterTrait + Send + Sync + 'static,
77 PC: PriceCalculatorTrait,
78{
79 pub fn new(
96 relayer: RelayerRepoModel,
97 provider: P,
98 relayer_repository: Arc<RR>,
99 network_repository: Arc<NR>,
100 transaction_repository: Arc<TR>,
101 transaction_counter_service: Arc<TCR>,
102 job_producer: Arc<J>,
103 price_calculator: PC,
104 signer: S,
105 ) -> Result<Self, TransactionError> {
106 Ok(Self {
107 relayer,
108 provider,
109 relayer_repository,
110 network_repository,
111 transaction_repository,
112 transaction_counter_service,
113 job_producer,
114 price_calculator,
115 signer,
116 })
117 }
118
119 pub fn provider(&self) -> &P {
121 &self.provider
122 }
123
124 pub fn relayer(&self) -> &RelayerRepoModel {
126 &self.relayer
127 }
128
129 pub fn network_repository(&self) -> &NR {
131 &self.network_repository
132 }
133
134 pub fn job_producer(&self) -> &J {
136 &self.job_producer
137 }
138
139 pub fn transaction_repository(&self) -> &TR {
140 &self.transaction_repository
141 }
142
143 fn is_already_submitted_error(error: &impl std::fmt::Display) -> bool {
146 let error_msg = error.to_string().to_lowercase();
147 error_msg.contains("already known")
148 || error_msg.contains("nonce too low")
149 || error_msg.contains("replacement transaction underpriced")
150 }
151
152 pub(super) async fn schedule_status_check(
154 &self,
155 tx: &TransactionRepoModel,
156 delay_seconds: Option<i64>,
157 ) -> Result<(), TransactionError> {
158 let delay = delay_seconds.map(calculate_scheduled_timestamp);
159 self.job_producer()
160 .produce_check_transaction_status_job(
161 TransactionStatusCheck::new(
162 tx.id.clone(),
163 tx.relayer_id.clone(),
164 crate::models::NetworkType::Evm,
165 ),
166 delay,
167 )
168 .await
169 .map_err(|e| {
170 TransactionError::UnexpectedError(format!("Failed to schedule status check: {e}"))
171 })
172 }
173
174 pub(super) async fn send_transaction_submit_job(
176 &self,
177 tx: &TransactionRepoModel,
178 ) -> Result<(), TransactionError> {
179 let job = TransactionSend::submit(tx.id.clone(), tx.relayer_id.clone());
180
181 self.job_producer()
182 .produce_submit_transaction_job(job, None)
183 .await
184 .map_err(|e| {
185 TransactionError::UnexpectedError(format!("Failed to produce submit job: {e}"))
186 })
187 }
188
189 pub(super) async fn send_transaction_resubmit_job(
191 &self,
192 tx: &TransactionRepoModel,
193 ) -> Result<(), TransactionError> {
194 let job = TransactionSend::resubmit(tx.id.clone(), tx.relayer_id.clone());
195
196 self.job_producer()
197 .produce_submit_transaction_job(job, None)
198 .await
199 .map_err(|e| {
200 TransactionError::UnexpectedError(format!("Failed to produce resubmit job: {e}"))
201 })
202 }
203
204 pub(super) async fn send_transaction_resend_job(
206 &self,
207 tx: &TransactionRepoModel,
208 ) -> Result<(), TransactionError> {
209 let job = TransactionSend::resend(tx.id.clone(), tx.relayer_id.clone());
210
211 self.job_producer()
212 .produce_submit_transaction_job(job, None)
213 .await
214 .map_err(|e| {
215 TransactionError::UnexpectedError(format!("Failed to produce resend job: {e}"))
216 })
217 }
218
219 pub(super) async fn send_transaction_request_job(
221 &self,
222 tx: &TransactionRepoModel,
223 ) -> Result<(), TransactionError> {
224 use crate::jobs::TransactionRequest;
225
226 let job = TransactionRequest::new(tx.id.clone(), tx.relayer_id.clone());
227
228 self.job_producer()
229 .produce_transaction_request_job(job, None)
230 .await
231 .map_err(|e| {
232 TransactionError::UnexpectedError(format!("Failed to produce request job: {e}"))
233 })
234 }
235
236 pub(super) async fn update_transaction_status(
238 &self,
239 tx: TransactionRepoModel,
240 new_status: TransactionStatus,
241 ) -> Result<TransactionRepoModel, TransactionError> {
242 let confirmed_at = if new_status == TransactionStatus::Confirmed {
243 Some(Utc::now().to_rfc3339())
244 } else {
245 None
246 };
247
248 let update_request = TransactionUpdateRequest {
249 status: Some(new_status),
250 confirmed_at,
251 ..Default::default()
252 };
253
254 let updated_tx = self
255 .transaction_repository()
256 .partial_update(tx.id.clone(), update_request)
257 .await?;
258
259 if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
260 error!(
261 tx_id = %updated_tx.id,
262 status = ?updated_tx.status,
263 "sending transaction update notification failed: {:?}",
264 e
265 );
266 }
267 Ok(updated_tx)
268 }
269
270 pub(super) async fn send_transaction_update_notification(
275 &self,
276 tx: &TransactionRepoModel,
277 ) -> Result<(), eyre::Report> {
278 if let Some(notification_id) = &self.relayer().notification_id {
279 self.job_producer()
280 .produce_send_notification_job(
281 produce_transaction_update_notification_payload(notification_id, tx),
282 None,
283 )
284 .await?;
285 }
286 Ok(())
287 }
288
289 async fn ensure_sufficient_balance(
301 &self,
302 total_cost: crate::models::U256,
303 ) -> Result<(), TransactionError> {
304 EvmTransactionValidator::validate_sufficient_relayer_balance(
305 total_cost,
306 &self.relayer().address,
307 &self.relayer().policies.get_evm_policy(),
308 &self.provider,
309 )
310 .await
311 .map_err(|validation_error| match validation_error {
312 EvmTransactionValidationError::InsufficientBalance(msg) => {
314 TransactionError::InsufficientBalance(msg)
315 }
316 EvmTransactionValidationError::ProviderError(msg) => {
318 TransactionError::UnexpectedError(format!("Failed to check balance: {msg}"))
319 }
320 EvmTransactionValidationError::ValidationError(msg) => {
322 TransactionError::UnexpectedError(format!("Balance validation error: {msg}"))
323 }
324 })
325 }
326
327 async fn estimate_tx_gas_limit(
335 &self,
336 evm_data: &EvmTransactionData,
337 relayer_policy: &RelayerEvmPolicy,
338 ) -> Result<u64, TransactionError> {
339 if !relayer_policy
340 .gas_limit_estimation
341 .unwrap_or(DEFAULT_EVM_GAS_LIMIT_ESTIMATION)
342 {
343 warn!("gas limit estimation is disabled for relayer");
344 return Err(TransactionError::UnexpectedError(
345 "Gas limit estimation is disabled".to_string(),
346 ));
347 }
348
349 let estimated_gas = self.provider.estimate_gas(evm_data).await.map_err(|e| {
350 warn!(error = ?e, tx_data = ?evm_data, "failed to estimate gas");
351 TransactionError::UnexpectedError(format!("Failed to estimate gas: {e}"))
352 })?;
353
354 Ok(estimated_gas * GAS_LIMIT_BUFFER_MULTIPLIER / 100)
355 }
356}
357
358#[async_trait]
359impl<P, RR, NR, TR, J, S, TCR, PC> Transaction
360 for EvmRelayerTransaction<P, RR, NR, TR, J, S, TCR, PC>
361where
362 P: EvmProviderTrait + Send + Sync + 'static,
363 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
364 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
365 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
366 J: JobProducerTrait + Send + Sync + 'static,
367 S: Signer + Send + Sync + 'static,
368 TCR: TransactionCounterTrait + Send + Sync + 'static,
369 PC: PriceCalculatorTrait + Send + Sync + 'static,
370{
371 async fn prepare_transaction(
381 &self,
382 tx: TransactionRepoModel,
383 ) -> Result<TransactionRepoModel, TransactionError> {
384 debug!("preparing transaction {}", tx.id);
385
386 if let Err(e) = ensure_status(&tx, TransactionStatus::Pending, Some("prepare_transaction"))
389 {
390 warn!(
391 tx_id = %tx.id,
392 status = ?tx.status,
393 error = %e,
394 "transaction not in Pending status, skipping preparation"
395 );
396 return Ok(tx);
397 }
398
399 let mut evm_data = tx.network_data.get_evm_transaction_data()?;
400 let relayer = self.relayer();
401
402 if evm_data.gas_limit.is_none() {
403 match self
404 .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
405 .await
406 {
407 Ok(estimated_gas_limit) => {
408 evm_data.gas_limit = Some(estimated_gas_limit);
409 }
410 Err(estimation_error) => {
411 error!(error = ?estimation_error, "failed to estimate gas limit");
412
413 let default_gas_limit = get_evm_default_gas_limit_for_tx(&evm_data);
414 debug!(gas_limit = %default_gas_limit, "fallback to default gas limit");
415 evm_data.gas_limit = Some(default_gas_limit);
416 }
417 }
418 }
419
420 let price_params: PriceParams = self
422 .price_calculator
423 .get_transaction_price_params(&evm_data, relayer)
424 .await?;
425
426 debug!(gas_price = ?price_params.gas_price, "gas price");
427
428 if let Err(balance_error) = self
430 .ensure_sufficient_balance(price_params.total_cost)
431 .await
432 {
433 match &balance_error {
435 TransactionError::InsufficientBalance(_) => {
436 warn!(error = %balance_error, "insufficient balance for transaction");
437
438 let update = TransactionUpdateRequest {
439 status: Some(TransactionStatus::Failed),
440 status_reason: Some(balance_error.to_string()),
441 ..Default::default()
442 };
443
444 let updated_tx = self
445 .transaction_repository
446 .partial_update(tx.id.clone(), update)
447 .await?;
448
449 if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
450 error!(
451 tx_id = %updated_tx.id,
452 status = ?TransactionStatus::Failed,
453 "sending transaction update notification failed for insufficient balance: {:?}",
454 e
455 );
456 }
457
458 return Ok(updated_tx);
460 }
461 _ => {
464 debug!(error = %balance_error, "failed to check balance, will retry");
465 return Err(balance_error);
466 }
467 }
468 }
469
470 let tx_with_nonce = if let Some(existing_nonce) = evm_data.nonce {
472 debug!(
473 nonce = existing_nonce,
474 "transaction already has nonce assigned, reusing for retry"
475 );
476 tx
482 } else {
483 let new_nonce = self
485 .transaction_counter_service
486 .get_and_increment(&self.relayer.id, &self.relayer.address)
487 .await
488 .map_err(|e| TransactionError::UnexpectedError(e.to_string()))?;
489
490 debug!(nonce = new_nonce, "assigned new nonce to transaction");
491
492 let updated_evm_data = evm_data
493 .with_price_params(price_params.clone())
494 .with_nonce(new_nonce);
495
496 let presign_update = TransactionUpdateRequest {
499 network_data: Some(NetworkTransactionData::Evm(updated_evm_data.clone())),
500 priced_at: Some(Utc::now().to_rfc3339()),
501 ..Default::default()
502 };
503
504 self.transaction_repository
505 .partial_update(tx.id.clone(), presign_update)
506 .await?
507 };
508
509 let updated_evm_data = tx_with_nonce
511 .network_data
512 .get_evm_transaction_data()?
513 .with_price_params(price_params.clone());
514
515 let sig_result = self
517 .signer
518 .sign_transaction(NetworkTransactionData::Evm(updated_evm_data.clone()))
519 .await?;
520
521 let updated_evm_data =
522 updated_evm_data.with_signed_transaction_data(sig_result.into_evm()?);
523
524 let mut hashes = tx_with_nonce.hashes.clone();
526 if let Some(hash) = updated_evm_data.hash.clone() {
527 hashes.push(hash);
528 }
529
530 let postsign_update = TransactionUpdateRequest {
532 status: Some(TransactionStatus::Sent),
533 network_data: Some(NetworkTransactionData::Evm(updated_evm_data)),
534 hashes: Some(hashes),
535 ..Default::default()
536 };
537
538 let updated_tx = self
539 .transaction_repository
540 .partial_update(tx_with_nonce.id.clone(), postsign_update)
541 .await?;
542
543 self.job_producer
545 .produce_submit_transaction_job(
546 TransactionSend::submit(updated_tx.id.clone(), updated_tx.relayer_id.clone()),
547 None,
548 )
549 .await?;
550
551 if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
552 error!(
553 tx_id = %updated_tx.id,
554 status = ?TransactionStatus::Sent,
555 "sending transaction update notification failed after prepare: {:?}",
556 e
557 );
558 }
559
560 Ok(updated_tx)
561 }
562
563 async fn submit_transaction(
573 &self,
574 tx: TransactionRepoModel,
575 ) -> Result<TransactionRepoModel, TransactionError> {
576 debug!("submitting transaction {}", tx.id);
577
578 if let Err(e) = ensure_status_one_of(
581 &tx,
582 &[TransactionStatus::Sent, TransactionStatus::Submitted],
583 Some("submit_transaction"),
584 ) {
585 warn!(
586 tx_id = %tx.id,
587 status = ?tx.status,
588 error = %e,
589 "transaction not in expected status for submission, skipping"
590 );
591 return Ok(tx);
592 }
593
594 let evm_tx_data = tx.network_data.get_evm_transaction_data()?;
595 let raw_tx = evm_tx_data.raw.as_ref().ok_or_else(|| {
596 TransactionError::InvalidType("Raw transaction data is missing".to_string())
597 })?;
598
599 match self.provider.send_raw_transaction(raw_tx).await {
602 Ok(_) => {
603 }
605 Err(e) => {
606 if tx.status == TransactionStatus::Sent && Self::is_already_submitted_error(&e) {
610 warn!(
611 tx_id = %tx.id,
612 error = %e,
613 "transaction appears to be already submitted based on RPC error - treating as success"
614 );
615 } else {
617 return Err(e.into());
619 }
620 }
621 }
622
623 let update = TransactionUpdateRequest {
626 status: Some(TransactionStatus::Submitted),
627 sent_at: Some(Utc::now().to_rfc3339()),
628 ..Default::default()
629 };
630
631 let updated_tx = match self
632 .transaction_repository
633 .partial_update(tx.id.clone(), update)
634 .await
635 {
636 Ok(tx) => tx,
637 Err(e) => {
638 error!(
639 error = %e,
640 tx_id = %tx.id,
641 "CRITICAL: transaction sent to blockchain but failed to update database - transaction may not be tracked correctly"
642 );
643 tx
646 }
647 };
648
649 if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
650 error!(
651 tx_id = %updated_tx.id,
652 status = ?TransactionStatus::Submitted,
653 "sending transaction update notification failed after submit: {:?}",
654 e
655 );
656 }
657
658 Ok(updated_tx)
659 }
660
661 async fn handle_transaction_status(
671 &self,
672 tx: TransactionRepoModel,
673 ) -> Result<TransactionRepoModel, TransactionError> {
674 self.handle_status_impl(tx).await
675 }
676 async fn resubmit_transaction(
686 &self,
687 tx: TransactionRepoModel,
688 ) -> Result<TransactionRepoModel, TransactionError> {
689 debug!("resubmitting transaction {}", tx.id);
690
691 if let Err(e) = ensure_status_one_of(
693 &tx,
694 &[TransactionStatus::Sent, TransactionStatus::Submitted],
695 Some("resubmit_transaction"),
696 ) {
697 warn!(
698 tx_id = %tx.id,
699 status = ?tx.status,
700 error = %e,
701 "transaction not in expected status for resubmission, skipping"
702 );
703 return Ok(tx);
704 }
705
706 let bumped_price_params = self
708 .price_calculator
709 .calculate_bumped_gas_price(
710 &tx.network_data.get_evm_transaction_data()?,
711 self.relayer(),
712 )
713 .await?;
714
715 if !bumped_price_params.is_min_bumped.is_some_and(|b| b) {
716 warn!(price_params = ?bumped_price_params, "bumped gas price does not meet minimum requirement, skipping resubmission");
717 return Ok(tx);
718 }
719
720 self.ensure_sufficient_balance(bumped_price_params.total_cost)
722 .await?;
723
724 let evm_data = tx.network_data.get_evm_transaction_data()?;
726
727 let updated_evm_data = evm_data.with_price_params(bumped_price_params.clone());
729
730 let sig_result = self
732 .signer
733 .sign_transaction(NetworkTransactionData::Evm(updated_evm_data.clone()))
734 .await?;
735
736 let final_evm_data = updated_evm_data.with_signed_transaction_data(sig_result.into_evm()?);
737
738 let raw_tx = final_evm_data.raw.as_ref().ok_or_else(|| {
739 TransactionError::InvalidType("Raw transaction data is missing".to_string())
740 })?;
741
742 let was_already_submitted = match self.provider.send_raw_transaction(raw_tx).await {
744 Ok(_) => {
745 false
747 }
748 Err(e) => {
749 let is_already_submitted = Self::is_already_submitted_error(&e);
752
753 if is_already_submitted {
754 warn!(
755 tx_id = %tx.id,
756 error = %e,
757 "resubmission indicates transaction already in mempool/mined - keeping original hash"
758 );
759 true
761 } else {
762 return Err(e.into());
764 }
765 }
766 };
767
768 let update = if was_already_submitted {
770 TransactionUpdateRequest {
772 status: Some(TransactionStatus::Submitted),
773 ..Default::default()
774 }
775 } else {
776 let mut hashes = tx.hashes.clone();
778 if let Some(hash) = final_evm_data.hash.clone() {
779 hashes.push(hash);
780 }
781
782 TransactionUpdateRequest {
783 network_data: Some(NetworkTransactionData::Evm(final_evm_data)),
784 hashes: Some(hashes),
785 priced_at: Some(Utc::now().to_rfc3339()),
786 sent_at: Some(Utc::now().to_rfc3339()),
787 ..Default::default()
788 }
789 };
790
791 let updated_tx = match self
792 .transaction_repository
793 .partial_update(tx.id.clone(), update)
794 .await
795 {
796 Ok(tx) => tx,
797 Err(e) => {
798 error!(
799 error = %e,
800 tx_id = %tx.id,
801 "CRITICAL: resubmitted transaction sent to blockchain but failed to update database"
802 );
803 tx
805 }
806 };
807
808 Ok(updated_tx)
809 }
810
811 async fn cancel_transaction(
821 &self,
822 tx: TransactionRepoModel,
823 ) -> Result<TransactionRepoModel, TransactionError> {
824 info!("cancelling transaction {}", tx.id);
825 debug!(status = ?tx.status, "transaction status");
826
827 ensure_status_one_of(
829 &tx,
830 &[
831 TransactionStatus::Pending,
832 TransactionStatus::Sent,
833 TransactionStatus::Submitted,
834 ],
835 Some("cancel_transaction"),
836 )?;
837
838 if tx.status == TransactionStatus::Pending {
840 debug!("transaction is in pending state, updating status to canceled");
841 return self
842 .update_transaction_status(tx, TransactionStatus::Canceled)
843 .await;
844 }
845
846 let update = self.prepare_noop_update_request(&tx, true).await?;
847 let updated_tx = self
848 .transaction_repository()
849 .partial_update(tx.id.clone(), update)
850 .await?;
851
852 self.send_transaction_resubmit_job(&updated_tx).await?;
854
855 if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
857 error!(
858 tx_id = %updated_tx.id,
859 status = ?updated_tx.status,
860 "sending transaction update notification failed after cancel: {:?}",
861 e
862 );
863 }
864
865 debug!("original transaction updated with cancellation data");
866 Ok(updated_tx)
867 }
868
869 async fn replace_transaction(
880 &self,
881 old_tx: TransactionRepoModel,
882 new_tx_request: NetworkTransactionRequest,
883 ) -> Result<TransactionRepoModel, TransactionError> {
884 debug!("replacing transaction");
885
886 ensure_status_one_of(
888 &old_tx,
889 &[
890 TransactionStatus::Pending,
891 TransactionStatus::Sent,
892 TransactionStatus::Submitted,
893 ],
894 Some("replace_transaction"),
895 )?;
896
897 let old_evm_data = old_tx.network_data.get_evm_transaction_data()?;
899 let new_evm_request = match new_tx_request {
900 NetworkTransactionRequest::Evm(evm_req) => evm_req,
901 _ => {
902 return Err(TransactionError::InvalidType(
903 "New transaction request must be EVM type".to_string(),
904 ))
905 }
906 };
907
908 let network_repo_model = self
909 .network_repository()
910 .get_by_chain_id(NetworkType::Evm, old_evm_data.chain_id)
911 .await
912 .map_err(|e| {
913 TransactionError::NetworkConfiguration(format!(
914 "Failed to get network by chain_id {}: {}",
915 old_evm_data.chain_id, e
916 ))
917 })?
918 .ok_or_else(|| {
919 TransactionError::NetworkConfiguration(format!(
920 "Network with chain_id {} not found",
921 old_evm_data.chain_id
922 ))
923 })?;
924
925 let network = EvmNetwork::try_from(network_repo_model).map_err(|e| {
926 TransactionError::NetworkConfiguration(format!("Failed to convert network model: {e}"))
927 })?;
928
929 let updated_evm_data = EvmTransactionData::for_replacement(&old_evm_data, &new_evm_request);
931
932 let price_params = super::replacement::determine_replacement_pricing(
934 &old_evm_data,
935 &updated_evm_data,
936 self.relayer(),
937 &self.price_calculator,
938 network.lacks_mempool(),
939 )
940 .await?;
941
942 debug!(price_params = ?price_params, "replacement price params");
943
944 let evm_data_with_price_params = updated_evm_data.with_price_params(price_params.clone());
946
947 self.ensure_sufficient_balance(price_params.total_cost)
949 .await?;
950
951 let sig_result = self
952 .signer
953 .sign_transaction(NetworkTransactionData::Evm(
954 evm_data_with_price_params.clone(),
955 ))
956 .await?;
957
958 let final_evm_data =
959 evm_data_with_price_params.with_signed_transaction_data(sig_result.into_evm()?);
960
961 let updated_tx = self
963 .transaction_repository
964 .update_network_data(
965 old_tx.id.clone(),
966 NetworkTransactionData::Evm(final_evm_data),
967 )
968 .await?;
969
970 self.send_transaction_resubmit_job(&updated_tx).await?;
971
972 if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
974 error!(
975 tx_id = %updated_tx.id,
976 status = ?updated_tx.status,
977 "sending transaction update notification failed after replace: {:?}",
978 e
979 );
980 }
981
982 Ok(updated_tx)
983 }
984
985 async fn sign_transaction(
995 &self,
996 tx: TransactionRepoModel,
997 ) -> Result<TransactionRepoModel, TransactionError> {
998 Ok(tx)
999 }
1000
1001 async fn validate_transaction(
1011 &self,
1012 _tx: TransactionRepoModel,
1013 ) -> Result<bool, TransactionError> {
1014 Ok(true)
1015 }
1016}
1017pub type DefaultEvmTransaction = EvmRelayerTransaction<
1026 EvmProvider,
1027 RelayerRepositoryStorage,
1028 NetworkRepositoryStorage,
1029 TransactionRepositoryStorage,
1030 JobProducer,
1031 EvmSigner,
1032 TransactionCounterRepositoryStorage,
1033 PriceCalculator<EvmGasPriceService<EvmProvider>>,
1034>;
1035#[cfg(test)]
1036mod tests {
1037
1038 use super::*;
1039 use crate::{
1040 domain::evm::price_calculator::PriceParams,
1041 jobs::MockJobProducerTrait,
1042 models::{
1043 evm::Speed, EvmTransactionData, EvmTransactionRequest, NetworkType,
1044 RelayerNetworkPolicy, U256,
1045 },
1046 repositories::{
1047 MockNetworkRepository, MockRelayerRepository, MockTransactionCounterTrait,
1048 MockTransactionRepository,
1049 },
1050 services::{provider::MockEvmProviderTrait, signer::MockSigner},
1051 };
1052 use chrono::Utc;
1053 use futures::future::ready;
1054 use mockall::{mock, predicate::*};
1055
1056 mock! {
1058 pub PriceCalculator {}
1059 #[async_trait]
1060 impl PriceCalculatorTrait for PriceCalculator {
1061 async fn get_transaction_price_params(
1062 &self,
1063 tx_data: &EvmTransactionData,
1064 relayer: &RelayerRepoModel
1065 ) -> Result<PriceParams, TransactionError>;
1066
1067 async fn calculate_bumped_gas_price(
1068 &self,
1069 tx: &EvmTransactionData,
1070 relayer: &RelayerRepoModel,
1071 ) -> Result<PriceParams, TransactionError>;
1072 }
1073 }
1074
1075 fn create_test_relayer() -> RelayerRepoModel {
1077 create_test_relayer_with_policy(crate::models::RelayerEvmPolicy {
1078 min_balance: Some(100000000000000000u128), gas_limit_estimation: Some(true),
1080 gas_price_cap: Some(100000000000), whitelist_receivers: Some(vec!["0xRecipient".to_string()]),
1082 eip1559_pricing: Some(false),
1083 private_transactions: Some(false),
1084 })
1085 }
1086
1087 fn create_test_relayer_with_policy(evm_policy: RelayerEvmPolicy) -> RelayerRepoModel {
1088 RelayerRepoModel {
1089 id: "test-relayer-id".to_string(),
1090 name: "Test Relayer".to_string(),
1091 network: "1".to_string(), address: "0xSender".to_string(),
1093 paused: false,
1094 system_disabled: false,
1095 signer_id: "test-signer-id".to_string(),
1096 notification_id: Some("test-notification-id".to_string()),
1097 policies: RelayerNetworkPolicy::Evm(evm_policy),
1098 network_type: NetworkType::Evm,
1099 custom_rpc_urls: None,
1100 ..Default::default()
1101 }
1102 }
1103
1104 fn create_test_transaction() -> TransactionRepoModel {
1106 TransactionRepoModel {
1107 id: "test-tx-id".to_string(),
1108 relayer_id: "test-relayer-id".to_string(),
1109 status: TransactionStatus::Pending,
1110 status_reason: None,
1111 created_at: Utc::now().to_rfc3339(),
1112 sent_at: None,
1113 confirmed_at: None,
1114 valid_until: None,
1115 delete_at: None,
1116 network_type: NetworkType::Evm,
1117 network_data: NetworkTransactionData::Evm(EvmTransactionData {
1118 chain_id: 1,
1119 from: "0xSender".to_string(),
1120 to: Some("0xRecipient".to_string()),
1121 value: U256::from(1000000000000000000u64), data: Some("0xData".to_string()),
1123 gas_limit: Some(21000),
1124 gas_price: Some(20000000000), max_fee_per_gas: None,
1126 max_priority_fee_per_gas: None,
1127 nonce: None,
1128 signature: None,
1129 hash: None,
1130 speed: Some(Speed::Fast),
1131 raw: None,
1132 }),
1133 priced_at: None,
1134 hashes: Vec::new(),
1135 noop_count: None,
1136 is_canceled: Some(false),
1137 }
1138 }
1139
1140 #[tokio::test]
1141 async fn test_prepare_transaction_with_sufficient_balance() {
1142 let mut mock_transaction = MockTransactionRepository::new();
1143 let mock_relayer = MockRelayerRepository::new();
1144 let mut mock_provider = MockEvmProviderTrait::new();
1145 let mut mock_signer = MockSigner::new();
1146 let mut mock_job_producer = MockJobProducerTrait::new();
1147 let mut mock_price_calculator = MockPriceCalculator::new();
1148 let mut counter_service = MockTransactionCounterTrait::new();
1149
1150 let relayer = create_test_relayer();
1151 let test_tx = create_test_transaction();
1152
1153 counter_service
1154 .expect_get_and_increment()
1155 .returning(|_, _| Box::pin(ready(Ok(42))));
1156
1157 let price_params = PriceParams {
1158 gas_price: Some(30000000000),
1159 max_fee_per_gas: None,
1160 max_priority_fee_per_gas: None,
1161 is_min_bumped: None,
1162 extra_fee: None,
1163 total_cost: U256::from(630000000000000u64),
1164 };
1165 mock_price_calculator
1166 .expect_get_transaction_price_params()
1167 .returning(move |_, _| Ok(price_params.clone()));
1168
1169 mock_signer.expect_sign_transaction().returning(|_| {
1170 Box::pin(ready(Ok(
1171 crate::domain::relayer::SignTransactionResponse::Evm(
1172 crate::domain::relayer::SignTransactionResponseEvm {
1173 hash: "0xtx_hash".to_string(),
1174 signature: crate::models::EvmTransactionDataSignature {
1175 r: "r".to_string(),
1176 s: "s".to_string(),
1177 v: 1,
1178 sig: "0xsignature".to_string(),
1179 },
1180 raw: vec![1, 2, 3],
1181 },
1182 ),
1183 )))
1184 });
1185
1186 mock_provider
1187 .expect_get_balance()
1188 .with(eq("0xSender"))
1189 .returning(|_| Box::pin(ready(Ok(U256::from(1000000000000000000u64)))));
1190
1191 let test_tx_clone = test_tx.clone();
1192 mock_transaction
1193 .expect_partial_update()
1194 .returning(move |_, update| {
1195 let mut updated_tx = test_tx_clone.clone();
1196 if let Some(status) = &update.status {
1197 updated_tx.status = status.clone();
1198 }
1199 if let Some(network_data) = &update.network_data {
1200 updated_tx.network_data = network_data.clone();
1201 }
1202 if let Some(hashes) = &update.hashes {
1203 updated_tx.hashes = hashes.clone();
1204 }
1205 Ok(updated_tx)
1206 });
1207
1208 mock_job_producer
1209 .expect_produce_submit_transaction_job()
1210 .returning(|_, _| Box::pin(ready(Ok(()))));
1211 mock_job_producer
1212 .expect_produce_send_notification_job()
1213 .returning(|_, _| Box::pin(ready(Ok(()))));
1214
1215 let mock_network = MockNetworkRepository::new();
1216
1217 let evm_transaction = EvmRelayerTransaction {
1218 relayer: relayer.clone(),
1219 provider: mock_provider,
1220 relayer_repository: Arc::new(mock_relayer),
1221 network_repository: Arc::new(mock_network),
1222 transaction_repository: Arc::new(mock_transaction),
1223 transaction_counter_service: Arc::new(counter_service),
1224 job_producer: Arc::new(mock_job_producer),
1225 price_calculator: mock_price_calculator,
1226 signer: mock_signer,
1227 };
1228
1229 let result = evm_transaction.prepare_transaction(test_tx.clone()).await;
1230 assert!(result.is_ok());
1231 let prepared_tx = result.unwrap();
1232 assert_eq!(prepared_tx.status, TransactionStatus::Sent);
1233 assert!(!prepared_tx.hashes.is_empty());
1234 }
1235
1236 #[tokio::test]
1237 async fn test_prepare_transaction_with_insufficient_balance() {
1238 let mut mock_transaction = MockTransactionRepository::new();
1239 let mock_relayer = MockRelayerRepository::new();
1240 let mut mock_provider = MockEvmProviderTrait::new();
1241 let mut mock_signer = MockSigner::new();
1242 let mut mock_job_producer = MockJobProducerTrait::new();
1243 let mut mock_price_calculator = MockPriceCalculator::new();
1244 let mut counter_service = MockTransactionCounterTrait::new();
1245
1246 let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
1247 gas_limit_estimation: Some(false),
1248 min_balance: Some(100000000000000000u128),
1249 ..Default::default()
1250 });
1251 let test_tx = create_test_transaction();
1252
1253 counter_service
1254 .expect_get_and_increment()
1255 .returning(|_, _| Box::pin(ready(Ok(42))));
1256
1257 let price_params = PriceParams {
1258 gas_price: Some(30000000000),
1259 max_fee_per_gas: None,
1260 max_priority_fee_per_gas: None,
1261 is_min_bumped: None,
1262 extra_fee: None,
1263 total_cost: U256::from(630000000000000u64),
1264 };
1265 mock_price_calculator
1266 .expect_get_transaction_price_params()
1267 .returning(move |_, _| Ok(price_params.clone()));
1268
1269 mock_signer.expect_sign_transaction().returning(|_| {
1270 Box::pin(ready(Ok(
1271 crate::domain::relayer::SignTransactionResponse::Evm(
1272 crate::domain::relayer::SignTransactionResponseEvm {
1273 hash: "0xtx_hash".to_string(),
1274 signature: crate::models::EvmTransactionDataSignature {
1275 r: "r".to_string(),
1276 s: "s".to_string(),
1277 v: 1,
1278 sig: "0xsignature".to_string(),
1279 },
1280 raw: vec![1, 2, 3],
1281 },
1282 ),
1283 )))
1284 });
1285
1286 mock_provider
1287 .expect_get_balance()
1288 .with(eq("0xSender"))
1289 .returning(|_| Box::pin(ready(Ok(U256::from(90000000000000000u64)))));
1290
1291 let test_tx_clone = test_tx.clone();
1292 mock_transaction
1293 .expect_partial_update()
1294 .withf(move |id, update| {
1295 id == "test-tx-id" && update.status == Some(TransactionStatus::Failed)
1296 })
1297 .returning(move |_, update| {
1298 let mut updated_tx = test_tx_clone.clone();
1299 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1300 updated_tx.status_reason = update.status_reason.clone();
1301 Ok(updated_tx)
1302 });
1303
1304 mock_job_producer
1305 .expect_produce_send_notification_job()
1306 .returning(|_, _| Box::pin(ready(Ok(()))));
1307
1308 let mock_network = MockNetworkRepository::new();
1309
1310 let evm_transaction = EvmRelayerTransaction {
1311 relayer: relayer.clone(),
1312 provider: mock_provider,
1313 relayer_repository: Arc::new(mock_relayer),
1314 network_repository: Arc::new(mock_network),
1315 transaction_repository: Arc::new(mock_transaction),
1316 transaction_counter_service: Arc::new(counter_service),
1317 job_producer: Arc::new(mock_job_producer),
1318 price_calculator: mock_price_calculator,
1319 signer: mock_signer,
1320 };
1321
1322 let result = evm_transaction.prepare_transaction(test_tx.clone()).await;
1323 assert!(result.is_ok(), "Expected Ok, got: {:?}", result);
1324
1325 let updated_tx = result.unwrap();
1326 assert_eq!(
1327 updated_tx.status,
1328 TransactionStatus::Failed,
1329 "Transaction should be marked as Failed"
1330 );
1331 assert!(
1332 updated_tx.status_reason.is_some(),
1333 "Status reason should be set"
1334 );
1335 assert!(
1336 updated_tx
1337 .status_reason
1338 .as_ref()
1339 .unwrap()
1340 .to_lowercase()
1341 .contains("insufficient balance"),
1342 "Status reason should contain insufficient balance error, got: {:?}",
1343 updated_tx.status_reason
1344 );
1345 }
1346
1347 #[tokio::test]
1348 async fn test_cancel_transaction() {
1349 {
1351 let mut mock_transaction = MockTransactionRepository::new();
1353 let mock_relayer = MockRelayerRepository::new();
1354 let mock_provider = MockEvmProviderTrait::new();
1355 let mock_signer = MockSigner::new();
1356 let mut mock_job_producer = MockJobProducerTrait::new();
1357 let mock_price_calculator = MockPriceCalculator::new();
1358 let counter_service = MockTransactionCounterTrait::new();
1359
1360 let relayer = create_test_relayer();
1362 let mut test_tx = create_test_transaction();
1363 test_tx.status = TransactionStatus::Pending;
1364
1365 let test_tx_clone = test_tx.clone();
1367 mock_transaction
1368 .expect_partial_update()
1369 .withf(move |id, update| {
1370 id == "test-tx-id" && update.status == Some(TransactionStatus::Canceled)
1371 })
1372 .returning(move |_, update| {
1373 let mut updated_tx = test_tx_clone.clone();
1374 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1375 Ok(updated_tx)
1376 });
1377
1378 mock_job_producer
1380 .expect_produce_send_notification_job()
1381 .returning(|_, _| Box::pin(ready(Ok(()))));
1382
1383 let mock_network = MockNetworkRepository::new();
1384
1385 let evm_transaction = EvmRelayerTransaction {
1387 relayer: relayer.clone(),
1388 provider: mock_provider,
1389 relayer_repository: Arc::new(mock_relayer),
1390 network_repository: Arc::new(mock_network),
1391 transaction_repository: Arc::new(mock_transaction),
1392 transaction_counter_service: Arc::new(counter_service),
1393 job_producer: Arc::new(mock_job_producer),
1394 price_calculator: mock_price_calculator,
1395 signer: mock_signer,
1396 };
1397
1398 let result = evm_transaction.cancel_transaction(test_tx.clone()).await;
1400 assert!(result.is_ok());
1401 let cancelled_tx = result.unwrap();
1402 assert_eq!(cancelled_tx.id, "test-tx-id");
1403 assert_eq!(cancelled_tx.status, TransactionStatus::Canceled);
1404 }
1405
1406 {
1408 let mut mock_transaction = MockTransactionRepository::new();
1410 let mock_relayer = MockRelayerRepository::new();
1411 let mock_provider = MockEvmProviderTrait::new();
1412 let mut mock_signer = MockSigner::new();
1413 let mut mock_job_producer = MockJobProducerTrait::new();
1414 let mut mock_price_calculator = MockPriceCalculator::new();
1415 let counter_service = MockTransactionCounterTrait::new();
1416
1417 let relayer = create_test_relayer();
1419 let mut test_tx = create_test_transaction();
1420 test_tx.status = TransactionStatus::Submitted;
1421 test_tx.sent_at = Some(Utc::now().to_rfc3339());
1422 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
1423 nonce: Some(42),
1424 hash: Some("0xoriginal_hash".to_string()),
1425 ..test_tx.network_data.get_evm_transaction_data().unwrap()
1426 });
1427
1428 mock_price_calculator
1430 .expect_get_transaction_price_params()
1431 .return_once(move |_, _| {
1432 Ok(PriceParams {
1433 gas_price: Some(40000000000), max_fee_per_gas: None,
1435 max_priority_fee_per_gas: None,
1436 is_min_bumped: Some(true),
1437 extra_fee: Some(U256::ZERO),
1438 total_cost: U256::ZERO,
1439 })
1440 });
1441
1442 mock_signer.expect_sign_transaction().returning(|_| {
1444 Box::pin(ready(Ok(
1445 crate::domain::relayer::SignTransactionResponse::Evm(
1446 crate::domain::relayer::SignTransactionResponseEvm {
1447 hash: "0xcancellation_hash".to_string(),
1448 signature: crate::models::EvmTransactionDataSignature {
1449 r: "r".to_string(),
1450 s: "s".to_string(),
1451 v: 1,
1452 sig: "0xsignature".to_string(),
1453 },
1454 raw: vec![1, 2, 3],
1455 },
1456 ),
1457 )))
1458 });
1459
1460 let test_tx_clone = test_tx.clone();
1462 mock_transaction
1463 .expect_partial_update()
1464 .returning(move |tx_id, update| {
1465 let mut updated_tx = test_tx_clone.clone();
1466 updated_tx.id = tx_id;
1467 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1468 updated_tx.network_data =
1469 update.network_data.unwrap_or(updated_tx.network_data);
1470 if let Some(hashes) = update.hashes {
1471 updated_tx.hashes = hashes;
1472 }
1473 Ok(updated_tx)
1474 });
1475
1476 mock_job_producer
1478 .expect_produce_submit_transaction_job()
1479 .returning(|_, _| Box::pin(ready(Ok(()))));
1480 mock_job_producer
1481 .expect_produce_send_notification_job()
1482 .returning(|_, _| Box::pin(ready(Ok(()))));
1483
1484 let mut mock_network = MockNetworkRepository::new();
1486 mock_network
1487 .expect_get_by_chain_id()
1488 .with(eq(NetworkType::Evm), eq(1))
1489 .returning(|_, _| {
1490 use crate::config::{EvmNetworkConfig, NetworkConfigCommon};
1491 use crate::models::{NetworkConfigData, NetworkRepoModel};
1492
1493 let config = EvmNetworkConfig {
1494 common: NetworkConfigCommon {
1495 network: "mainnet".to_string(),
1496 from: None,
1497 rpc_urls: Some(vec!["https://rpc.example.com".to_string()]),
1498 explorer_urls: None,
1499 average_blocktime_ms: Some(12000),
1500 is_testnet: Some(false),
1501 tags: Some(vec!["mainnet".to_string()]),
1502 },
1503 chain_id: Some(1),
1504 required_confirmations: Some(12),
1505 features: Some(vec!["eip1559".to_string()]),
1506 symbol: Some("ETH".to_string()),
1507 gas_price_cache: None,
1508 };
1509 Ok(Some(NetworkRepoModel {
1510 id: "evm:mainnet".to_string(),
1511 name: "mainnet".to_string(),
1512 network_type: NetworkType::Evm,
1513 config: NetworkConfigData::Evm(config),
1514 }))
1515 });
1516
1517 let evm_transaction = EvmRelayerTransaction {
1519 relayer: relayer.clone(),
1520 provider: mock_provider,
1521 relayer_repository: Arc::new(mock_relayer),
1522 network_repository: Arc::new(mock_network),
1523 transaction_repository: Arc::new(mock_transaction),
1524 transaction_counter_service: Arc::new(counter_service),
1525 job_producer: Arc::new(mock_job_producer),
1526 price_calculator: mock_price_calculator,
1527 signer: mock_signer,
1528 };
1529
1530 let result = evm_transaction.cancel_transaction(test_tx.clone()).await;
1532 assert!(result.is_ok());
1533 let cancelled_tx = result.unwrap();
1534
1535 assert_eq!(cancelled_tx.id, "test-tx-id");
1537 assert_eq!(cancelled_tx.status, TransactionStatus::Submitted);
1538
1539 if let NetworkTransactionData::Evm(evm_data) = &cancelled_tx.network_data {
1541 assert_eq!(evm_data.nonce, Some(42)); } else {
1543 panic!("Expected EVM transaction data");
1544 }
1545 }
1546
1547 {
1549 let mock_transaction = MockTransactionRepository::new();
1551 let mock_relayer = MockRelayerRepository::new();
1552 let mock_provider = MockEvmProviderTrait::new();
1553 let mock_signer = MockSigner::new();
1554 let mock_job_producer = MockJobProducerTrait::new();
1555 let mock_price_calculator = MockPriceCalculator::new();
1556 let counter_service = MockTransactionCounterTrait::new();
1557
1558 let relayer = create_test_relayer();
1560 let mut test_tx = create_test_transaction();
1561 test_tx.status = TransactionStatus::Confirmed;
1562
1563 let mock_network = MockNetworkRepository::new();
1564
1565 let evm_transaction = EvmRelayerTransaction {
1567 relayer: relayer.clone(),
1568 provider: mock_provider,
1569 relayer_repository: Arc::new(mock_relayer),
1570 network_repository: Arc::new(mock_network),
1571 transaction_repository: Arc::new(mock_transaction),
1572 transaction_counter_service: Arc::new(counter_service),
1573 job_producer: Arc::new(mock_job_producer),
1574 price_calculator: mock_price_calculator,
1575 signer: mock_signer,
1576 };
1577
1578 let result = evm_transaction.cancel_transaction(test_tx.clone()).await;
1580 assert!(result.is_err());
1581 if let Err(TransactionError::ValidationError(msg)) = result {
1582 assert!(msg.contains("Invalid transaction state for cancel_transaction"));
1583 } else {
1584 panic!("Expected ValidationError");
1585 }
1586 }
1587 }
1588
1589 #[tokio::test]
1590 async fn test_replace_transaction() {
1591 {
1593 let mut mock_transaction = MockTransactionRepository::new();
1595 let mock_relayer = MockRelayerRepository::new();
1596 let mut mock_provider = MockEvmProviderTrait::new();
1597 let mut mock_signer = MockSigner::new();
1598 let mut mock_job_producer = MockJobProducerTrait::new();
1599 let mut mock_price_calculator = MockPriceCalculator::new();
1600 let counter_service = MockTransactionCounterTrait::new();
1601
1602 let relayer = create_test_relayer();
1604 let mut test_tx = create_test_transaction();
1605 test_tx.status = TransactionStatus::Submitted;
1606 test_tx.sent_at = Some(Utc::now().to_rfc3339());
1607
1608 mock_price_calculator
1610 .expect_get_transaction_price_params()
1611 .return_once(move |_, _| {
1612 Ok(PriceParams {
1613 gas_price: Some(40000000000), max_fee_per_gas: None,
1615 max_priority_fee_per_gas: None,
1616 is_min_bumped: Some(true),
1617 extra_fee: Some(U256::ZERO),
1618 total_cost: U256::from(2001000000000000000u64), })
1620 });
1621
1622 mock_signer.expect_sign_transaction().returning(|_| {
1624 Box::pin(ready(Ok(
1625 crate::domain::relayer::SignTransactionResponse::Evm(
1626 crate::domain::relayer::SignTransactionResponseEvm {
1627 hash: "0xreplacement_hash".to_string(),
1628 signature: crate::models::EvmTransactionDataSignature {
1629 r: "r".to_string(),
1630 s: "s".to_string(),
1631 v: 1,
1632 sig: "0xsignature".to_string(),
1633 },
1634 raw: vec![1, 2, 3],
1635 },
1636 ),
1637 )))
1638 });
1639
1640 mock_provider
1642 .expect_get_balance()
1643 .with(eq("0xSender"))
1644 .returning(|_| Box::pin(ready(Ok(U256::from(3000000000000000000u64)))));
1645
1646 let test_tx_clone = test_tx.clone();
1648 mock_transaction
1649 .expect_update_network_data()
1650 .returning(move |tx_id, network_data| {
1651 let mut updated_tx = test_tx_clone.clone();
1652 updated_tx.id = tx_id;
1653 updated_tx.network_data = network_data;
1654 Ok(updated_tx)
1655 });
1656
1657 mock_job_producer
1659 .expect_produce_submit_transaction_job()
1660 .returning(|_, _| Box::pin(ready(Ok(()))));
1661 mock_job_producer
1662 .expect_produce_send_notification_job()
1663 .returning(|_, _| Box::pin(ready(Ok(()))));
1664
1665 let mut mock_network = MockNetworkRepository::new();
1667 mock_network
1668 .expect_get_by_chain_id()
1669 .with(eq(NetworkType::Evm), eq(1))
1670 .returning(|_, _| {
1671 use crate::config::{EvmNetworkConfig, NetworkConfigCommon};
1672 use crate::models::{NetworkConfigData, NetworkRepoModel};
1673
1674 let config = EvmNetworkConfig {
1675 common: NetworkConfigCommon {
1676 network: "mainnet".to_string(),
1677 from: None,
1678 rpc_urls: Some(vec!["https://rpc.example.com".to_string()]),
1679 explorer_urls: None,
1680 average_blocktime_ms: Some(12000),
1681 is_testnet: Some(false),
1682 tags: Some(vec!["mainnet".to_string()]), },
1684 chain_id: Some(1),
1685 required_confirmations: Some(12),
1686 features: Some(vec!["eip1559".to_string()]),
1687 symbol: Some("ETH".to_string()),
1688 gas_price_cache: None,
1689 };
1690 Ok(Some(NetworkRepoModel {
1691 id: "evm:mainnet".to_string(),
1692 name: "mainnet".to_string(),
1693 network_type: NetworkType::Evm,
1694 config: NetworkConfigData::Evm(config),
1695 }))
1696 });
1697
1698 let evm_transaction = EvmRelayerTransaction {
1700 relayer: relayer.clone(),
1701 provider: mock_provider,
1702 relayer_repository: Arc::new(mock_relayer),
1703 network_repository: Arc::new(mock_network),
1704 transaction_repository: Arc::new(mock_transaction),
1705 transaction_counter_service: Arc::new(counter_service),
1706 job_producer: Arc::new(mock_job_producer),
1707 price_calculator: mock_price_calculator,
1708 signer: mock_signer,
1709 };
1710
1711 let replacement_request = NetworkTransactionRequest::Evm(EvmTransactionRequest {
1713 to: Some("0xNewRecipient".to_string()),
1714 value: U256::from(2000000000000000000u64), data: Some("0xNewData".to_string()),
1716 gas_limit: Some(25000),
1717 gas_price: None, max_fee_per_gas: None,
1719 max_priority_fee_per_gas: None,
1720 speed: Some(Speed::Fast),
1721 valid_until: None,
1722 });
1723
1724 let result = evm_transaction
1726 .replace_transaction(test_tx.clone(), replacement_request)
1727 .await;
1728 if let Err(ref e) = result {
1729 eprintln!("Replace transaction failed with error: {:?}", e);
1730 }
1731 assert!(result.is_ok());
1732 let replaced_tx = result.unwrap();
1733
1734 assert_eq!(replaced_tx.id, "test-tx-id");
1736
1737 if let NetworkTransactionData::Evm(evm_data) = &replaced_tx.network_data {
1739 assert_eq!(evm_data.to, Some("0xNewRecipient".to_string()));
1740 assert_eq!(evm_data.value, U256::from(2000000000000000000u64));
1741 assert_eq!(evm_data.gas_price, Some(40000000000));
1742 assert_eq!(evm_data.gas_limit, Some(25000));
1743 assert!(evm_data.hash.is_some());
1744 assert!(evm_data.raw.is_some());
1745 } else {
1746 panic!("Expected EVM transaction data");
1747 }
1748 }
1749
1750 {
1752 let mock_transaction = MockTransactionRepository::new();
1754 let mock_relayer = MockRelayerRepository::new();
1755 let mock_provider = MockEvmProviderTrait::new();
1756 let mock_signer = MockSigner::new();
1757 let mock_job_producer = MockJobProducerTrait::new();
1758 let mock_price_calculator = MockPriceCalculator::new();
1759 let counter_service = MockTransactionCounterTrait::new();
1760
1761 let relayer = create_test_relayer();
1763 let mut test_tx = create_test_transaction();
1764 test_tx.status = TransactionStatus::Confirmed;
1765
1766 let mock_network = MockNetworkRepository::new();
1767
1768 let evm_transaction = EvmRelayerTransaction {
1770 relayer: relayer.clone(),
1771 provider: mock_provider,
1772 relayer_repository: Arc::new(mock_relayer),
1773 network_repository: Arc::new(mock_network),
1774 transaction_repository: Arc::new(mock_transaction),
1775 transaction_counter_service: Arc::new(counter_service),
1776 job_producer: Arc::new(mock_job_producer),
1777 price_calculator: mock_price_calculator,
1778 signer: mock_signer,
1779 };
1780
1781 let replacement_request = NetworkTransactionRequest::Evm(EvmTransactionRequest {
1783 to: Some("0xNewRecipient".to_string()),
1784 value: U256::from(1000000000000000000u64),
1785 data: Some("0xData".to_string()),
1786 gas_limit: Some(21000),
1787 gas_price: Some(30000000000),
1788 max_fee_per_gas: None,
1789 max_priority_fee_per_gas: None,
1790 speed: Some(Speed::Fast),
1791 valid_until: None,
1792 });
1793
1794 let result = evm_transaction
1796 .replace_transaction(test_tx.clone(), replacement_request)
1797 .await;
1798 assert!(result.is_err());
1799 if let Err(TransactionError::ValidationError(msg)) = result {
1800 assert!(msg.contains("Invalid transaction state for replace_transaction"));
1801 } else {
1802 panic!("Expected ValidationError");
1803 }
1804 }
1805 }
1806
1807 #[tokio::test]
1808 async fn test_estimate_tx_gas_limit_success() {
1809 let mock_transaction = MockTransactionRepository::new();
1810 let mock_relayer = MockRelayerRepository::new();
1811 let mut mock_provider = MockEvmProviderTrait::new();
1812 let mock_signer = MockSigner::new();
1813 let mock_job_producer = MockJobProducerTrait::new();
1814 let mock_price_calculator = MockPriceCalculator::new();
1815 let counter_service = MockTransactionCounterTrait::new();
1816 let mock_network = MockNetworkRepository::new();
1817
1818 let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
1820 gas_limit_estimation: Some(true),
1821 ..Default::default()
1822 });
1823 let evm_data = EvmTransactionData {
1824 from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
1825 to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
1826 value: U256::from(1000000000000000000u128),
1827 data: Some("0x".to_string()),
1828 gas_limit: None,
1829 gas_price: Some(20_000_000_000),
1830 nonce: Some(1),
1831 chain_id: 1,
1832 hash: None,
1833 signature: None,
1834 speed: Some(Speed::Average),
1835 max_fee_per_gas: None,
1836 max_priority_fee_per_gas: None,
1837 raw: None,
1838 };
1839
1840 mock_provider
1842 .expect_estimate_gas()
1843 .times(1)
1844 .returning(|_| Box::pin(async { Ok(21000) }));
1845
1846 let transaction = EvmRelayerTransaction::new(
1847 relayer.clone(),
1848 mock_provider,
1849 Arc::new(mock_relayer),
1850 Arc::new(mock_network),
1851 Arc::new(mock_transaction),
1852 Arc::new(counter_service),
1853 Arc::new(mock_job_producer),
1854 mock_price_calculator,
1855 mock_signer,
1856 )
1857 .unwrap();
1858
1859 let result = transaction
1860 .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
1861 .await;
1862
1863 assert!(result.is_ok());
1864 assert_eq!(result.unwrap(), 23100);
1866 }
1867
1868 #[tokio::test]
1869 async fn test_estimate_tx_gas_limit_disabled() {
1870 let mock_transaction = MockTransactionRepository::new();
1871 let mock_relayer = MockRelayerRepository::new();
1872 let mut mock_provider = MockEvmProviderTrait::new();
1873 let mock_signer = MockSigner::new();
1874 let mock_job_producer = MockJobProducerTrait::new();
1875 let mock_price_calculator = MockPriceCalculator::new();
1876 let counter_service = MockTransactionCounterTrait::new();
1877 let mock_network = MockNetworkRepository::new();
1878
1879 let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
1881 gas_limit_estimation: Some(false),
1882 ..Default::default()
1883 });
1884
1885 let evm_data = EvmTransactionData {
1886 from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
1887 to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
1888 value: U256::from(1000000000000000000u128),
1889 data: Some("0x".to_string()),
1890 gas_limit: None,
1891 gas_price: Some(20_000_000_000),
1892 nonce: Some(1),
1893 chain_id: 1,
1894 hash: None,
1895 signature: None,
1896 speed: Some(Speed::Average),
1897 max_fee_per_gas: None,
1898 max_priority_fee_per_gas: None,
1899 raw: None,
1900 };
1901
1902 mock_provider.expect_estimate_gas().times(0);
1904
1905 let transaction = EvmRelayerTransaction::new(
1906 relayer.clone(),
1907 mock_provider,
1908 Arc::new(mock_relayer),
1909 Arc::new(mock_network),
1910 Arc::new(mock_transaction),
1911 Arc::new(counter_service),
1912 Arc::new(mock_job_producer),
1913 mock_price_calculator,
1914 mock_signer,
1915 )
1916 .unwrap();
1917
1918 let result = transaction
1919 .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
1920 .await;
1921
1922 assert!(result.is_err());
1923 assert!(matches!(
1924 result.unwrap_err(),
1925 TransactionError::UnexpectedError(_)
1926 ));
1927 }
1928
1929 #[tokio::test]
1930 async fn test_estimate_tx_gas_limit_default_enabled() {
1931 let mock_transaction = MockTransactionRepository::new();
1932 let mock_relayer = MockRelayerRepository::new();
1933 let mut mock_provider = MockEvmProviderTrait::new();
1934 let mock_signer = MockSigner::new();
1935 let mock_job_producer = MockJobProducerTrait::new();
1936 let mock_price_calculator = MockPriceCalculator::new();
1937 let counter_service = MockTransactionCounterTrait::new();
1938 let mock_network = MockNetworkRepository::new();
1939
1940 let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
1941 gas_limit_estimation: None, ..Default::default()
1943 });
1944
1945 let evm_data = EvmTransactionData {
1946 from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
1947 to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
1948 value: U256::from(1000000000000000000u128),
1949 data: Some("0x".to_string()),
1950 gas_limit: None,
1951 gas_price: Some(20_000_000_000),
1952 nonce: Some(1),
1953 chain_id: 1,
1954 hash: None,
1955 signature: None,
1956 speed: Some(Speed::Average),
1957 max_fee_per_gas: None,
1958 max_priority_fee_per_gas: None,
1959 raw: None,
1960 };
1961
1962 mock_provider
1964 .expect_estimate_gas()
1965 .times(1)
1966 .returning(|_| Box::pin(async { Ok(50000) }));
1967
1968 let transaction = EvmRelayerTransaction::new(
1969 relayer.clone(),
1970 mock_provider,
1971 Arc::new(mock_relayer),
1972 Arc::new(mock_network),
1973 Arc::new(mock_transaction),
1974 Arc::new(counter_service),
1975 Arc::new(mock_job_producer),
1976 mock_price_calculator,
1977 mock_signer,
1978 )
1979 .unwrap();
1980
1981 let result = transaction
1982 .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
1983 .await;
1984
1985 assert!(result.is_ok());
1986 assert_eq!(result.unwrap(), 55000);
1988 }
1989
1990 #[tokio::test]
1991 async fn test_estimate_tx_gas_limit_provider_error() {
1992 let mock_transaction = MockTransactionRepository::new();
1993 let mock_relayer = MockRelayerRepository::new();
1994 let mut mock_provider = MockEvmProviderTrait::new();
1995 let mock_signer = MockSigner::new();
1996 let mock_job_producer = MockJobProducerTrait::new();
1997 let mock_price_calculator = MockPriceCalculator::new();
1998 let counter_service = MockTransactionCounterTrait::new();
1999 let mock_network = MockNetworkRepository::new();
2000
2001 let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2002 gas_limit_estimation: Some(true),
2003 ..Default::default()
2004 });
2005
2006 let evm_data = EvmTransactionData {
2007 from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
2008 to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
2009 value: U256::from(1000000000000000000u128),
2010 data: Some("0x".to_string()),
2011 gas_limit: None,
2012 gas_price: Some(20_000_000_000),
2013 nonce: Some(1),
2014 chain_id: 1,
2015 hash: None,
2016 signature: None,
2017 speed: Some(Speed::Average),
2018 max_fee_per_gas: None,
2019 max_priority_fee_per_gas: None,
2020 raw: None,
2021 };
2022
2023 mock_provider.expect_estimate_gas().times(1).returning(|_| {
2025 Box::pin(async {
2026 Err(crate::services::provider::ProviderError::Other(
2027 "RPC error".to_string(),
2028 ))
2029 })
2030 });
2031
2032 let transaction = EvmRelayerTransaction::new(
2033 relayer.clone(),
2034 mock_provider,
2035 Arc::new(mock_relayer),
2036 Arc::new(mock_network),
2037 Arc::new(mock_transaction),
2038 Arc::new(counter_service),
2039 Arc::new(mock_job_producer),
2040 mock_price_calculator,
2041 mock_signer,
2042 )
2043 .unwrap();
2044
2045 let result = transaction
2046 .estimate_tx_gas_limit(&evm_data, &relayer.policies.get_evm_policy())
2047 .await;
2048
2049 assert!(result.is_err());
2050 assert!(matches!(
2051 result.unwrap_err(),
2052 TransactionError::UnexpectedError(_)
2053 ));
2054 }
2055
2056 #[tokio::test]
2057 async fn test_prepare_transaction_uses_gas_estimation_and_stores_result() {
2058 let mut mock_transaction = MockTransactionRepository::new();
2059 let mock_relayer = MockRelayerRepository::new();
2060 let mut mock_provider = MockEvmProviderTrait::new();
2061 let mut mock_signer = MockSigner::new();
2062 let mut mock_job_producer = MockJobProducerTrait::new();
2063 let mut mock_price_calculator = MockPriceCalculator::new();
2064 let mut counter_service = MockTransactionCounterTrait::new();
2065 let mock_network = MockNetworkRepository::new();
2066
2067 let relayer = create_test_relayer_with_policy(RelayerEvmPolicy {
2069 gas_limit_estimation: Some(true),
2070 min_balance: Some(100000000000000000u128),
2071 ..Default::default()
2072 });
2073
2074 let mut test_tx = create_test_transaction();
2076 if let NetworkTransactionData::Evm(ref mut evm_data) = test_tx.network_data {
2077 evm_data.gas_limit = None; evm_data.nonce = None; }
2080
2081 const PROVIDER_GAS_ESTIMATE: u64 = 45000;
2083 const EXPECTED_GAS_WITH_BUFFER: u64 = 49500; mock_provider
2087 .expect_estimate_gas()
2088 .times(1)
2089 .returning(move |_| Box::pin(async move { Ok(PROVIDER_GAS_ESTIMATE) }));
2090
2091 mock_provider
2093 .expect_get_balance()
2094 .times(1)
2095 .returning(|_| Box::pin(async { Ok(U256::from(2000000000000000000u128)) })); let price_params = PriceParams {
2098 gas_price: Some(20_000_000_000), max_fee_per_gas: None,
2100 max_priority_fee_per_gas: None,
2101 is_min_bumped: None,
2102 extra_fee: None,
2103 total_cost: U256::from(1900000000000000000u128), };
2105
2106 mock_price_calculator
2108 .expect_get_transaction_price_params()
2109 .returning(move |_, _| Ok(price_params.clone()));
2110
2111 counter_service
2113 .expect_get_and_increment()
2114 .times(1)
2115 .returning(|_, _| Box::pin(async { Ok(42) }));
2116
2117 mock_signer.expect_sign_transaction().returning(|_| {
2119 Box::pin(ready(Ok(
2120 crate::domain::relayer::SignTransactionResponse::Evm(
2121 crate::domain::relayer::SignTransactionResponseEvm {
2122 hash: "0xhash".to_string(),
2123 signature: crate::models::EvmTransactionDataSignature {
2124 r: "r".to_string(),
2125 s: "s".to_string(),
2126 v: 1,
2127 sig: "0xsignature".to_string(),
2128 },
2129 raw: vec![1, 2, 3],
2130 },
2131 ),
2132 )))
2133 });
2134
2135 mock_job_producer
2137 .expect_produce_submit_transaction_job()
2138 .returning(|_, _| Box::pin(async { Ok(()) }));
2139
2140 mock_job_producer
2141 .expect_produce_send_notification_job()
2142 .returning(|_, _| Box::pin(ready(Ok(()))));
2143
2144 let expected_gas_limit = EXPECTED_GAS_WITH_BUFFER;
2149
2150 let test_tx_clone = test_tx.clone();
2151 mock_transaction
2152 .expect_partial_update()
2153 .times(2)
2154 .returning(move |_, update| {
2155 let mut updated_tx = test_tx_clone.clone();
2156
2157 if let Some(status) = &update.status {
2159 updated_tx.status = status.clone();
2160 }
2161 if let Some(network_data) = &update.network_data {
2162 updated_tx.network_data = network_data.clone();
2163 } else {
2164 if let NetworkTransactionData::Evm(ref mut evm_data) = updated_tx.network_data {
2166 if evm_data.gas_limit.is_none() {
2167 evm_data.gas_limit = Some(expected_gas_limit);
2168 }
2169 }
2170 }
2171 if let Some(hashes) = &update.hashes {
2172 updated_tx.hashes = hashes.clone();
2173 }
2174
2175 Ok(updated_tx)
2176 });
2177
2178 let transaction = EvmRelayerTransaction::new(
2179 relayer.clone(),
2180 mock_provider,
2181 Arc::new(mock_relayer),
2182 Arc::new(mock_network),
2183 Arc::new(mock_transaction),
2184 Arc::new(counter_service),
2185 Arc::new(mock_job_producer),
2186 mock_price_calculator,
2187 mock_signer,
2188 )
2189 .unwrap();
2190
2191 let result = transaction.prepare_transaction(test_tx).await;
2193
2194 assert!(result.is_ok(), "prepare_transaction should succeed");
2196 let prepared_tx = result.unwrap();
2197
2198 if let NetworkTransactionData::Evm(evm_data) = prepared_tx.network_data {
2200 assert_eq!(evm_data.gas_limit, Some(EXPECTED_GAS_WITH_BUFFER));
2201 } else {
2202 panic!("Expected EVM network data");
2203 }
2204 }
2205
2206 #[test]
2207 fn test_is_already_submitted_error_detection() {
2208 assert!(DefaultEvmTransaction::is_already_submitted_error(
2210 &"already known"
2211 ));
2212 assert!(DefaultEvmTransaction::is_already_submitted_error(
2213 &"Transaction already known"
2214 ));
2215 assert!(DefaultEvmTransaction::is_already_submitted_error(
2216 &"Error: already known"
2217 ));
2218
2219 assert!(DefaultEvmTransaction::is_already_submitted_error(
2221 &"nonce too low"
2222 ));
2223 assert!(DefaultEvmTransaction::is_already_submitted_error(
2224 &"Nonce Too Low"
2225 ));
2226 assert!(DefaultEvmTransaction::is_already_submitted_error(
2227 &"Error: nonce too low"
2228 ));
2229
2230 assert!(DefaultEvmTransaction::is_already_submitted_error(
2232 &"replacement transaction underpriced"
2233 ));
2234 assert!(DefaultEvmTransaction::is_already_submitted_error(
2235 &"Replacement Transaction Underpriced"
2236 ));
2237
2238 assert!(!DefaultEvmTransaction::is_already_submitted_error(
2240 &"insufficient funds"
2241 ));
2242 assert!(!DefaultEvmTransaction::is_already_submitted_error(
2243 &"execution reverted"
2244 ));
2245 assert!(!DefaultEvmTransaction::is_already_submitted_error(
2246 &"gas too low"
2247 ));
2248 assert!(!DefaultEvmTransaction::is_already_submitted_error(
2249 &"timeout"
2250 ));
2251 }
2252
2253 #[tokio::test]
2256 async fn test_submit_transaction_already_known_error_from_sent() {
2257 let mut mock_transaction = MockTransactionRepository::new();
2258 let mock_relayer = MockRelayerRepository::new();
2259 let mut mock_provider = MockEvmProviderTrait::new();
2260 let mock_signer = MockSigner::new();
2261 let mut mock_job_producer = MockJobProducerTrait::new();
2262 let mock_price_calculator = MockPriceCalculator::new();
2263 let counter_service = MockTransactionCounterTrait::new();
2264 let mock_network = MockNetworkRepository::new();
2265
2266 let relayer = create_test_relayer();
2267 let mut test_tx = create_test_transaction();
2268 test_tx.status = TransactionStatus::Sent;
2269 test_tx.sent_at = Some(Utc::now().to_rfc3339());
2270 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
2271 nonce: Some(42),
2272 hash: Some("0xhash".to_string()),
2273 raw: Some(vec![1, 2, 3]),
2274 ..test_tx.network_data.get_evm_transaction_data().unwrap()
2275 });
2276
2277 mock_provider
2279 .expect_send_raw_transaction()
2280 .times(1)
2281 .returning(|_| {
2282 Box::pin(async {
2283 Err(crate::services::provider::ProviderError::Other(
2284 "already known: transaction already in mempool".to_string(),
2285 ))
2286 })
2287 });
2288
2289 let test_tx_clone = test_tx.clone();
2291 mock_transaction
2292 .expect_partial_update()
2293 .times(1)
2294 .withf(|_, update| update.status == Some(TransactionStatus::Submitted))
2295 .returning(move |_, update| {
2296 let mut updated_tx = test_tx_clone.clone();
2297 updated_tx.status = update.status.unwrap();
2298 updated_tx.sent_at = update.sent_at.clone();
2299 Ok(updated_tx)
2300 });
2301
2302 mock_job_producer
2303 .expect_produce_send_notification_job()
2304 .times(1)
2305 .returning(|_, _| Box::pin(ready(Ok(()))));
2306
2307 let evm_transaction = EvmRelayerTransaction {
2308 relayer: relayer.clone(),
2309 provider: mock_provider,
2310 relayer_repository: Arc::new(mock_relayer),
2311 network_repository: Arc::new(mock_network),
2312 transaction_repository: Arc::new(mock_transaction),
2313 transaction_counter_service: Arc::new(counter_service),
2314 job_producer: Arc::new(mock_job_producer),
2315 price_calculator: mock_price_calculator,
2316 signer: mock_signer,
2317 };
2318
2319 let result = evm_transaction.submit_transaction(test_tx).await;
2320 assert!(result.is_ok());
2321 let updated_tx = result.unwrap();
2322 assert_eq!(updated_tx.status, TransactionStatus::Submitted);
2323 }
2324
2325 #[tokio::test]
2327 async fn test_submit_transaction_real_error_fails() {
2328 let mock_transaction = MockTransactionRepository::new();
2329 let mock_relayer = MockRelayerRepository::new();
2330 let mut mock_provider = MockEvmProviderTrait::new();
2331 let mock_signer = MockSigner::new();
2332 let mock_job_producer = MockJobProducerTrait::new();
2333 let mock_price_calculator = MockPriceCalculator::new();
2334 let counter_service = MockTransactionCounterTrait::new();
2335 let mock_network = MockNetworkRepository::new();
2336
2337 let relayer = create_test_relayer();
2338 let mut test_tx = create_test_transaction();
2339 test_tx.status = TransactionStatus::Sent;
2340 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
2341 raw: Some(vec![1, 2, 3]),
2342 ..test_tx.network_data.get_evm_transaction_data().unwrap()
2343 });
2344
2345 mock_provider
2347 .expect_send_raw_transaction()
2348 .times(1)
2349 .returning(|_| {
2350 Box::pin(async {
2351 Err(crate::services::provider::ProviderError::Other(
2352 "insufficient funds for gas * price + value".to_string(),
2353 ))
2354 })
2355 });
2356
2357 let evm_transaction = EvmRelayerTransaction {
2358 relayer: relayer.clone(),
2359 provider: mock_provider,
2360 relayer_repository: Arc::new(mock_relayer),
2361 network_repository: Arc::new(mock_network),
2362 transaction_repository: Arc::new(mock_transaction),
2363 transaction_counter_service: Arc::new(counter_service),
2364 job_producer: Arc::new(mock_job_producer),
2365 price_calculator: mock_price_calculator,
2366 signer: mock_signer,
2367 };
2368
2369 let result = evm_transaction.submit_transaction(test_tx).await;
2370 assert!(result.is_err());
2371 }
2372
2373 #[tokio::test]
2376 async fn test_resubmit_transaction_already_submitted_preserves_hash() {
2377 let mut mock_transaction = MockTransactionRepository::new();
2378 let mock_relayer = MockRelayerRepository::new();
2379 let mut mock_provider = MockEvmProviderTrait::new();
2380 let mut mock_signer = MockSigner::new();
2381 let mock_job_producer = MockJobProducerTrait::new();
2382 let mut mock_price_calculator = MockPriceCalculator::new();
2383 let counter_service = MockTransactionCounterTrait::new();
2384 let mock_network = MockNetworkRepository::new();
2385
2386 let relayer = create_test_relayer();
2387 let mut test_tx = create_test_transaction();
2388 test_tx.status = TransactionStatus::Submitted;
2389 test_tx.sent_at = Some(Utc::now().to_rfc3339());
2390 let original_hash = "0xoriginal_hash".to_string();
2391 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
2392 nonce: Some(42),
2393 hash: Some(original_hash.clone()),
2394 raw: Some(vec![1, 2, 3]),
2395 ..test_tx.network_data.get_evm_transaction_data().unwrap()
2396 });
2397 test_tx.hashes = vec![original_hash.clone()];
2398
2399 mock_price_calculator
2401 .expect_calculate_bumped_gas_price()
2402 .times(1)
2403 .returning(|_, _| {
2404 Ok(PriceParams {
2405 gas_price: Some(25000000000), max_fee_per_gas: None,
2407 max_priority_fee_per_gas: None,
2408 is_min_bumped: Some(true),
2409 extra_fee: None,
2410 total_cost: U256::from(525000000000000u64),
2411 })
2412 });
2413
2414 mock_provider
2416 .expect_get_balance()
2417 .times(1)
2418 .returning(|_| Box::pin(async { Ok(U256::from(1000000000000000000u64)) }));
2419
2420 mock_signer
2422 .expect_sign_transaction()
2423 .times(1)
2424 .returning(|_| {
2425 Box::pin(ready(Ok(
2426 crate::domain::relayer::SignTransactionResponse::Evm(
2427 crate::domain::relayer::SignTransactionResponseEvm {
2428 hash: "0xnew_hash_that_should_not_be_saved".to_string(),
2429 signature: crate::models::EvmTransactionDataSignature {
2430 r: "r".to_string(),
2431 s: "s".to_string(),
2432 v: 1,
2433 sig: "0xsignature".to_string(),
2434 },
2435 raw: vec![4, 5, 6],
2436 },
2437 ),
2438 )))
2439 });
2440
2441 mock_provider
2443 .expect_send_raw_transaction()
2444 .times(1)
2445 .returning(|_| {
2446 Box::pin(async {
2447 Err(crate::services::provider::ProviderError::Other(
2448 "already known: transaction with same nonce already in mempool".to_string(),
2449 ))
2450 })
2451 });
2452
2453 let test_tx_clone = test_tx.clone();
2455 mock_transaction
2456 .expect_partial_update()
2457 .times(1)
2458 .withf(|_, update| {
2459 update.status == Some(TransactionStatus::Submitted)
2461 && update.network_data.is_none()
2462 && update.hashes.is_none()
2463 })
2464 .returning(move |_, _| {
2465 let mut updated_tx = test_tx_clone.clone();
2466 updated_tx.status = TransactionStatus::Submitted;
2467 Ok(updated_tx)
2469 });
2470
2471 let evm_transaction = EvmRelayerTransaction {
2472 relayer: relayer.clone(),
2473 provider: mock_provider,
2474 relayer_repository: Arc::new(mock_relayer),
2475 network_repository: Arc::new(mock_network),
2476 transaction_repository: Arc::new(mock_transaction),
2477 transaction_counter_service: Arc::new(counter_service),
2478 job_producer: Arc::new(mock_job_producer),
2479 price_calculator: mock_price_calculator,
2480 signer: mock_signer,
2481 };
2482
2483 let result = evm_transaction.resubmit_transaction(test_tx.clone()).await;
2484 assert!(result.is_ok());
2485 let updated_tx = result.unwrap();
2486
2487 if let NetworkTransactionData::Evm(evm_data) = &updated_tx.network_data {
2489 assert_eq!(evm_data.hash, Some(original_hash));
2490 } else {
2491 panic!("Expected EVM network data");
2492 }
2493 }
2494
2495 #[tokio::test]
2498 async fn test_submit_transaction_db_failure_after_blockchain_success() {
2499 let mut mock_transaction = MockTransactionRepository::new();
2500 let mock_relayer = MockRelayerRepository::new();
2501 let mut mock_provider = MockEvmProviderTrait::new();
2502 let mock_signer = MockSigner::new();
2503 let mut mock_job_producer = MockJobProducerTrait::new();
2504 let mock_price_calculator = MockPriceCalculator::new();
2505 let counter_service = MockTransactionCounterTrait::new();
2506 let mock_network = MockNetworkRepository::new();
2507
2508 let relayer = create_test_relayer();
2509 let mut test_tx = create_test_transaction();
2510 test_tx.status = TransactionStatus::Sent;
2511 test_tx.network_data = NetworkTransactionData::Evm(EvmTransactionData {
2512 raw: Some(vec![1, 2, 3]),
2513 ..test_tx.network_data.get_evm_transaction_data().unwrap()
2514 });
2515
2516 mock_provider
2518 .expect_send_raw_transaction()
2519 .times(1)
2520 .returning(|_| Box::pin(async { Ok("0xsubmitted_hash".to_string()) }));
2521
2522 mock_transaction
2524 .expect_partial_update()
2525 .times(1)
2526 .returning(|_, _| {
2527 Err(crate::models::RepositoryError::UnexpectedError(
2528 "Redis timeout".to_string(),
2529 ))
2530 });
2531
2532 mock_job_producer
2534 .expect_produce_send_notification_job()
2535 .times(1)
2536 .returning(|_, _| Box::pin(ready(Ok(()))));
2537
2538 let evm_transaction = EvmRelayerTransaction {
2539 relayer: relayer.clone(),
2540 provider: mock_provider,
2541 relayer_repository: Arc::new(mock_relayer),
2542 network_repository: Arc::new(mock_network),
2543 transaction_repository: Arc::new(mock_transaction),
2544 transaction_counter_service: Arc::new(counter_service),
2545 job_producer: Arc::new(mock_job_producer),
2546 price_calculator: mock_price_calculator,
2547 signer: mock_signer,
2548 };
2549
2550 let result = evm_transaction.submit_transaction(test_tx.clone()).await;
2551 assert!(result.is_ok());
2553 let returned_tx = result.unwrap();
2554 assert_eq!(returned_tx.id, test_tx.id);
2556 assert_eq!(returned_tx.status, TransactionStatus::Sent); }
2558
2559 #[tokio::test]
2561 async fn test_send_transaction_resend_job_success() {
2562 let mock_transaction = MockTransactionRepository::new();
2563 let mock_relayer = MockRelayerRepository::new();
2564 let mock_provider = MockEvmProviderTrait::new();
2565 let mock_signer = MockSigner::new();
2566 let mut mock_job_producer = MockJobProducerTrait::new();
2567 let mock_price_calculator = MockPriceCalculator::new();
2568 let counter_service = MockTransactionCounterTrait::new();
2569 let mock_network = MockNetworkRepository::new();
2570
2571 let relayer = create_test_relayer();
2572 let test_tx = create_test_transaction();
2573
2574 mock_job_producer
2576 .expect_produce_submit_transaction_job()
2577 .times(1)
2578 .withf(|job, delay| {
2579 job.transaction_id == "test-tx-id"
2581 && job.relayer_id == "test-relayer-id"
2582 && matches!(job.command, crate::jobs::TransactionCommand::Resend)
2583 && delay.is_none()
2584 })
2585 .returning(|_, _| Box::pin(ready(Ok(()))));
2586
2587 let evm_transaction = EvmRelayerTransaction {
2588 relayer: relayer.clone(),
2589 provider: mock_provider,
2590 relayer_repository: Arc::new(mock_relayer),
2591 network_repository: Arc::new(mock_network),
2592 transaction_repository: Arc::new(mock_transaction),
2593 transaction_counter_service: Arc::new(counter_service),
2594 job_producer: Arc::new(mock_job_producer),
2595 price_calculator: mock_price_calculator,
2596 signer: mock_signer,
2597 };
2598
2599 let result = evm_transaction.send_transaction_resend_job(&test_tx).await;
2600 assert!(result.is_ok());
2601 }
2602
2603 #[tokio::test]
2605 async fn test_send_transaction_resend_job_failure() {
2606 let mock_transaction = MockTransactionRepository::new();
2607 let mock_relayer = MockRelayerRepository::new();
2608 let mock_provider = MockEvmProviderTrait::new();
2609 let mock_signer = MockSigner::new();
2610 let mut mock_job_producer = MockJobProducerTrait::new();
2611 let mock_price_calculator = MockPriceCalculator::new();
2612 let counter_service = MockTransactionCounterTrait::new();
2613 let mock_network = MockNetworkRepository::new();
2614
2615 let relayer = create_test_relayer();
2616 let test_tx = create_test_transaction();
2617
2618 mock_job_producer
2620 .expect_produce_submit_transaction_job()
2621 .times(1)
2622 .returning(|_, _| {
2623 Box::pin(ready(Err(crate::jobs::JobProducerError::QueueError(
2624 "Job queue is full".to_string(),
2625 ))))
2626 });
2627
2628 let evm_transaction = EvmRelayerTransaction {
2629 relayer: relayer.clone(),
2630 provider: mock_provider,
2631 relayer_repository: Arc::new(mock_relayer),
2632 network_repository: Arc::new(mock_network),
2633 transaction_repository: Arc::new(mock_transaction),
2634 transaction_counter_service: Arc::new(counter_service),
2635 job_producer: Arc::new(mock_job_producer),
2636 price_calculator: mock_price_calculator,
2637 signer: mock_signer,
2638 };
2639
2640 let result = evm_transaction.send_transaction_resend_job(&test_tx).await;
2641 assert!(result.is_err());
2642 let err = result.unwrap_err();
2643 match err {
2644 TransactionError::UnexpectedError(msg) => {
2645 assert!(msg.contains("Failed to produce resend job"));
2646 }
2647 _ => panic!("Expected UnexpectedError"),
2648 }
2649 }
2650
2651 #[tokio::test]
2653 async fn test_send_transaction_request_job_success() {
2654 let mock_transaction = MockTransactionRepository::new();
2655 let mock_relayer = MockRelayerRepository::new();
2656 let mock_provider = MockEvmProviderTrait::new();
2657 let mock_signer = MockSigner::new();
2658 let mut mock_job_producer = MockJobProducerTrait::new();
2659 let mock_price_calculator = MockPriceCalculator::new();
2660 let counter_service = MockTransactionCounterTrait::new();
2661 let mock_network = MockNetworkRepository::new();
2662
2663 let relayer = create_test_relayer();
2664 let test_tx = create_test_transaction();
2665
2666 mock_job_producer
2668 .expect_produce_transaction_request_job()
2669 .times(1)
2670 .withf(|job, delay| {
2671 job.transaction_id == "test-tx-id"
2673 && job.relayer_id == "test-relayer-id"
2674 && delay.is_none()
2675 })
2676 .returning(|_, _| Box::pin(ready(Ok(()))));
2677
2678 let evm_transaction = EvmRelayerTransaction {
2679 relayer: relayer.clone(),
2680 provider: mock_provider,
2681 relayer_repository: Arc::new(mock_relayer),
2682 network_repository: Arc::new(mock_network),
2683 transaction_repository: Arc::new(mock_transaction),
2684 transaction_counter_service: Arc::new(counter_service),
2685 job_producer: Arc::new(mock_job_producer),
2686 price_calculator: mock_price_calculator,
2687 signer: mock_signer,
2688 };
2689
2690 let result = evm_transaction.send_transaction_request_job(&test_tx).await;
2691 assert!(result.is_ok());
2692 }
2693
2694 #[tokio::test]
2696 async fn test_send_transaction_request_job_failure() {
2697 let mock_transaction = MockTransactionRepository::new();
2698 let mock_relayer = MockRelayerRepository::new();
2699 let mock_provider = MockEvmProviderTrait::new();
2700 let mock_signer = MockSigner::new();
2701 let mut mock_job_producer = MockJobProducerTrait::new();
2702 let mock_price_calculator = MockPriceCalculator::new();
2703 let counter_service = MockTransactionCounterTrait::new();
2704 let mock_network = MockNetworkRepository::new();
2705
2706 let relayer = create_test_relayer();
2707 let test_tx = create_test_transaction();
2708
2709 mock_job_producer
2711 .expect_produce_transaction_request_job()
2712 .times(1)
2713 .returning(|_, _| {
2714 Box::pin(ready(Err(crate::jobs::JobProducerError::QueueError(
2715 "Redis connection failed".to_string(),
2716 ))))
2717 });
2718
2719 let evm_transaction = EvmRelayerTransaction {
2720 relayer: relayer.clone(),
2721 provider: mock_provider,
2722 relayer_repository: Arc::new(mock_relayer),
2723 network_repository: Arc::new(mock_network),
2724 transaction_repository: Arc::new(mock_transaction),
2725 transaction_counter_service: Arc::new(counter_service),
2726 job_producer: Arc::new(mock_job_producer),
2727 price_calculator: mock_price_calculator,
2728 signer: mock_signer,
2729 };
2730
2731 let result = evm_transaction.send_transaction_request_job(&test_tx).await;
2732 assert!(result.is_err());
2733 let err = result.unwrap_err();
2734 match err {
2735 TransactionError::UnexpectedError(msg) => {
2736 assert!(msg.contains("Failed to produce request job"));
2737 }
2738 _ => panic!("Expected UnexpectedError"),
2739 }
2740 }
2741}