1use alloy::network::ReceiptResponse;
6use chrono::{DateTime, Duration, Utc};
7use eyre::Result;
8use tracing::{debug, error, info, warn};
9
10use super::EvmRelayerTransaction;
11use super::{
12 ensure_status, get_age_since_status_change, has_enough_confirmations, is_noop,
13 is_too_early_to_resubmit, is_transaction_valid, make_noop, too_many_attempts,
14 too_many_noop_attempts,
15};
16use crate::constants::{
17 get_evm_min_age_for_hash_recovery, get_evm_pending_recovery_trigger_timeout,
18 get_evm_prepare_timeout, get_evm_resend_timeout, ARBITRUM_TIME_TO_RESUBMIT,
19 EVM_MIN_HASHES_FOR_RECOVERY, EVM_PREPARE_TIMEOUT_MINUTES,
20};
21use crate::domain::transaction::common::{
22 get_age_of_sent_at, is_final_state, is_pending_transaction,
23};
24use crate::domain::transaction::util::get_age_since_created;
25use crate::models::{EvmNetwork, NetworkRepoModel, NetworkType};
26use crate::repositories::{NetworkRepository, RelayerRepository};
27use crate::{
28 domain::transaction::evm::price_calculator::PriceCalculatorTrait,
29 jobs::JobProducerTrait,
30 models::{
31 NetworkTransactionData, RelayerRepoModel, TransactionError, TransactionRepoModel,
32 TransactionStatus, TransactionUpdateRequest,
33 },
34 repositories::{Repository, TransactionCounterTrait, TransactionRepository},
35 services::{provider::EvmProviderTrait, signer::Signer},
36 utils::{get_resubmit_timeout_for_speed, get_resubmit_timeout_with_backoff},
37};
38
39impl<P, RR, NR, TR, J, S, TCR, PC> EvmRelayerTransaction<P, RR, NR, TR, J, S, TCR, PC>
40where
41 P: EvmProviderTrait + Send + Sync,
42 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
43 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
44 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
45 J: JobProducerTrait + Send + Sync + 'static,
46 S: Signer + Send + Sync + 'static,
47 TCR: TransactionCounterTrait + Send + Sync + 'static,
48 PC: PriceCalculatorTrait + Send + Sync,
49{
50 pub(super) async fn check_transaction_status(
51 &self,
52 tx: &TransactionRepoModel,
53 ) -> Result<TransactionStatus, TransactionError> {
54 if is_final_state(&tx.status) {
56 return Ok(tx.status.clone());
57 }
58
59 match tx.status {
62 TransactionStatus::Pending | TransactionStatus::Sent => {
63 return Ok(tx.status.clone());
64 }
65 _ => {}
66 }
67
68 let evm_data = tx.network_data.get_evm_transaction_data()?;
69 let tx_hash = evm_data
70 .hash
71 .as_ref()
72 .ok_or(TransactionError::UnexpectedError(
73 "Transaction hash is missing".to_string(),
74 ))?;
75
76 let receipt_result = self.provider().get_transaction_receipt(tx_hash).await?;
77
78 if let Some(receipt) = receipt_result {
79 if !receipt.inner.status() {
80 return Ok(TransactionStatus::Failed);
81 }
82 let last_block_number = self.provider().get_block_number().await?;
83 let tx_block_number = receipt
84 .block_number
85 .ok_or(TransactionError::UnexpectedError(
86 "Transaction receipt missing block number".to_string(),
87 ))?;
88
89 let network_model = self
90 .network_repository()
91 .get_by_chain_id(NetworkType::Evm, evm_data.chain_id)
92 .await?
93 .ok_or(TransactionError::UnexpectedError(format!(
94 "Network with chain id {} not found",
95 evm_data.chain_id
96 )))?;
97
98 let network = EvmNetwork::try_from(network_model).map_err(|e| {
99 TransactionError::UnexpectedError(format!(
100 "Error converting network model to EvmNetwork: {e}"
101 ))
102 })?;
103
104 if !has_enough_confirmations(
105 tx_block_number,
106 last_block_number,
107 network.required_confirmations,
108 ) {
109 debug!(tx_hash = %tx_hash, "transaction mined but not confirmed");
110 return Ok(TransactionStatus::Mined);
111 }
112 Ok(TransactionStatus::Confirmed)
113 } else {
114 debug!(tx_hash = %tx_hash, "transaction not yet mined");
115
116 if tx.hashes.len() > 1 && self.should_try_hash_recovery(tx)? {
120 if let Some(recovered_tx) = self
121 .try_recover_with_historical_hashes(tx, &evm_data)
122 .await?
123 {
124 return Ok(recovered_tx.status);
126 }
127 }
128
129 Ok(TransactionStatus::Submitted)
130 }
131 }
132
133 pub(super) async fn should_resubmit(
135 &self,
136 tx: &TransactionRepoModel,
137 ) -> Result<bool, TransactionError> {
138 ensure_status(tx, TransactionStatus::Submitted, Some("should_resubmit"))?;
140
141 let evm_data = tx.network_data.get_evm_transaction_data()?;
142 let age = get_age_of_sent_at(tx)?;
143
144 let network_model = self
146 .network_repository()
147 .get_by_chain_id(NetworkType::Evm, evm_data.chain_id)
148 .await?
149 .ok_or(TransactionError::UnexpectedError(format!(
150 "Network with chain id {} not found",
151 evm_data.chain_id
152 )))?;
153
154 let network = EvmNetwork::try_from(network_model).map_err(|e| {
155 TransactionError::UnexpectedError(format!(
156 "Error converting network model to EvmNetwork: {e}"
157 ))
158 })?;
159
160 let timeout = match network.is_arbitrum() {
161 true => ARBITRUM_TIME_TO_RESUBMIT,
162 false => get_resubmit_timeout_for_speed(&evm_data.speed),
163 };
164
165 let timeout_with_backoff = match network.is_arbitrum() {
166 true => timeout, false => get_resubmit_timeout_with_backoff(timeout, tx.hashes.len()),
168 };
169
170 if age > Duration::milliseconds(timeout_with_backoff) {
171 info!("Transaction has been pending for too long, resubmitting");
172 return Ok(true);
173 }
174 Ok(false)
175 }
176
177 pub(super) async fn should_noop(
179 &self,
180 tx: &TransactionRepoModel,
181 ) -> Result<bool, TransactionError> {
182 if too_many_noop_attempts(tx) {
183 info!("Transaction has too many NOOP attempts already");
184 return Ok(false);
185 }
186
187 let evm_data = tx.network_data.get_evm_transaction_data()?;
188 if is_noop(&evm_data) {
189 return Ok(false);
190 }
191
192 let network_model = self
193 .network_repository()
194 .get_by_chain_id(NetworkType::Evm, evm_data.chain_id)
195 .await?
196 .ok_or(TransactionError::UnexpectedError(format!(
197 "Network with chain id {} not found",
198 evm_data.chain_id
199 )))?;
200
201 let network = EvmNetwork::try_from(network_model).map_err(|e| {
202 TransactionError::UnexpectedError(format!(
203 "Error converting network model to EvmNetwork: {e}"
204 ))
205 })?;
206
207 if network.is_rollup() && too_many_attempts(tx) {
208 info!("Rollup transaction has too many attempts, will replace with NOOP");
209 return Ok(true);
210 }
211
212 if !is_transaction_valid(&tx.created_at, &tx.valid_until) {
213 info!("Transaction is expired, will replace with NOOP");
214 return Ok(true);
215 }
216
217 if tx.status == TransactionStatus::Pending {
218 let created_at = &tx.created_at;
219 let created_time = DateTime::parse_from_rfc3339(created_at)
220 .map_err(|e| {
221 TransactionError::UnexpectedError(format!("Invalid created_at timestamp: {e}"))
222 })?
223 .with_timezone(&Utc);
224 let age = Utc::now().signed_duration_since(created_time);
225 if age > get_evm_prepare_timeout() {
226 info!("Transaction in Pending state for over {EVM_PREPARE_TIMEOUT_MINUTES} minutes, will replace with NOOP");
227 return Ok(true);
228 }
229 }
230 Ok(false)
231 }
232
233 pub(super) async fn update_transaction_status_if_needed(
235 &self,
236 tx: TransactionRepoModel,
237 new_status: TransactionStatus,
238 ) -> Result<TransactionRepoModel, TransactionError> {
239 if tx.status != new_status {
240 return self.update_transaction_status(tx, new_status).await;
241 }
242 Ok(tx)
243 }
244
245 pub(super) async fn prepare_noop_update_request(
247 &self,
248 tx: &TransactionRepoModel,
249 is_cancellation: bool,
250 ) -> Result<TransactionUpdateRequest, TransactionError> {
251 let mut evm_data = tx.network_data.get_evm_transaction_data()?;
252 let network_model = self
253 .network_repository()
254 .get_by_chain_id(NetworkType::Evm, evm_data.chain_id)
255 .await?
256 .ok_or(TransactionError::UnexpectedError(format!(
257 "Network with chain id {} not found",
258 evm_data.chain_id
259 )))?;
260
261 let network = EvmNetwork::try_from(network_model).map_err(|e| {
262 TransactionError::UnexpectedError(format!(
263 "Error converting network model to EvmNetwork: {e}"
264 ))
265 })?;
266
267 make_noop(&mut evm_data, &network, Some(self.provider())).await?;
268
269 let noop_count = tx.noop_count.unwrap_or(0) + 1;
270 let update_request = TransactionUpdateRequest {
271 network_data: Some(NetworkTransactionData::Evm(evm_data)),
272 noop_count: Some(noop_count),
273 is_canceled: if is_cancellation {
274 Some(true)
275 } else {
276 tx.is_canceled
277 },
278 ..Default::default()
279 };
280 Ok(update_request)
281 }
282
283 async fn handle_submitted_state(
285 &self,
286 tx: TransactionRepoModel,
287 ) -> Result<TransactionRepoModel, TransactionError> {
288 if self.should_resubmit(&tx).await? {
289 let resubmitted_tx = self.handle_resubmission(tx).await?;
290 return Ok(resubmitted_tx);
291 }
292
293 self.update_transaction_status_if_needed(tx, TransactionStatus::Submitted)
294 .await
295 }
296
297 async fn handle_resubmission(
299 &self,
300 tx: TransactionRepoModel,
301 ) -> Result<TransactionRepoModel, TransactionError> {
302 debug!("scheduling resubmit job for transaction");
303
304 let tx_to_process = if self.should_noop(&tx).await? {
305 self.process_noop_transaction(&tx).await?
306 } else {
307 tx
308 };
309
310 self.send_transaction_resubmit_job(&tx_to_process).await?;
311 Ok(tx_to_process)
312 }
313
314 async fn process_noop_transaction(
316 &self,
317 tx: &TransactionRepoModel,
318 ) -> Result<TransactionRepoModel, TransactionError> {
319 debug!("preparing transaction NOOP before resubmission");
320 let update = self.prepare_noop_update_request(tx, false).await?;
321 let updated_tx = self
322 .transaction_repository()
323 .partial_update(tx.id.clone(), update)
324 .await?;
325
326 let res = self.send_transaction_update_notification(&updated_tx).await;
327 if let Err(e) = res {
328 error!(
329 tx_id = %updated_tx.id,
330 status = ?updated_tx.status,
331 "sending transaction update notification failed for NOOP transaction: {:?}",
332 e
333 );
334 }
335 Ok(updated_tx)
336 }
337
338 async fn handle_pending_state(
340 &self,
341 tx: TransactionRepoModel,
342 ) -> Result<TransactionRepoModel, TransactionError> {
343 if self.should_noop(&tx).await? {
344 debug!("preparing NOOP for pending transaction {}", tx.id);
345 let update = self.prepare_noop_update_request(&tx, false).await?;
346 let updated_tx = self
347 .transaction_repository()
348 .partial_update(tx.id.clone(), update)
349 .await?;
350
351 self.send_transaction_submit_job(&updated_tx).await?;
352 let res = self.send_transaction_update_notification(&updated_tx).await;
353 if let Err(e) = res {
354 error!(
355 tx_id = %updated_tx.id,
356 status = ?updated_tx.status,
357 "sending transaction update notification failed for Pending state NOOP: {:?}",
358 e
359 );
360 }
361 return Ok(updated_tx);
362 }
363
364 let age = get_age_since_created(&tx)?;
366 if age > get_evm_pending_recovery_trigger_timeout() {
367 warn!(
368 tx_id = %tx.id,
369 age_seconds = age.num_seconds(),
370 "transaction stuck in Pending, queuing prepare job"
371 );
372
373 self.send_transaction_request_job(&tx).await?;
375 }
376
377 Ok(tx)
378 }
379
380 async fn handle_mined_state(
382 &self,
383 tx: TransactionRepoModel,
384 ) -> Result<TransactionRepoModel, TransactionError> {
385 self.update_transaction_status_if_needed(tx, TransactionStatus::Mined)
386 .await
387 }
388
389 async fn handle_final_state(
391 &self,
392 tx: TransactionRepoModel,
393 status: TransactionStatus,
394 ) -> Result<TransactionRepoModel, TransactionError> {
395 self.update_transaction_status_if_needed(tx, status).await
396 }
397
398 pub async fn handle_status_impl(
403 &self,
404 tx: TransactionRepoModel,
405 ) -> Result<TransactionRepoModel, TransactionError> {
406 debug!("checking transaction status {}", tx.id);
407
408 if is_final_state(&tx.status) {
410 debug!(status = ?tx.status, "transaction already in final state");
411 return Ok(tx);
412 }
413
414 let status = self.check_transaction_status(&tx).await?;
419
420 debug!(
421 tx_id = %tx.id,
422 previous_status = ?tx.status,
423 new_status = ?status,
424 "transaction status check completed"
425 );
426
427 let tx = if status != tx.status {
431 debug!(
432 tx_id = %tx.id,
433 old_status = ?tx.status,
434 new_status = ?status,
435 "status changed during check, reloading transaction from DB to ensure fresh data"
436 );
437 self.transaction_repository()
438 .get_by_id(tx.id.clone())
439 .await?
440 } else {
441 tx
442 };
443
444 if is_too_early_to_resubmit(&tx)? && is_pending_transaction(&status) {
449 return self.update_transaction_status_if_needed(tx, status).await;
451 }
452
453 match status {
455 TransactionStatus::Pending => self.handle_pending_state(tx).await,
456 TransactionStatus::Sent => self.handle_sent_state(tx).await,
457 TransactionStatus::Submitted => self.handle_submitted_state(tx).await,
458 TransactionStatus::Mined => self.handle_mined_state(tx).await,
459 TransactionStatus::Confirmed
460 | TransactionStatus::Failed
461 | TransactionStatus::Expired
462 | TransactionStatus::Canceled => self.handle_final_state(tx, status).await,
463 }
464 }
465
466 async fn handle_sent_state(
468 &self,
469 tx: TransactionRepoModel,
470 ) -> Result<TransactionRepoModel, TransactionError> {
471 debug!(tx_id = %tx.id, "handling Sent state");
472
473 let age_since_sent = get_age_since_status_change(&tx)?;
476
477 if age_since_sent > get_evm_resend_timeout() {
478 warn!(
479 tx_id = %tx.id,
480 age_seconds = age_since_sent.num_seconds(),
481 "transaction stuck in Sent, queuing resubmit job with repricing"
482 );
483
484 self.send_transaction_resubmit_job(&tx).await?;
486 }
487
488 self.update_transaction_status_if_needed(tx, TransactionStatus::Sent)
489 .await
490 }
491
492 fn should_try_hash_recovery(
499 &self,
500 tx: &TransactionRepoModel,
501 ) -> Result<bool, TransactionError> {
502 if tx.status != TransactionStatus::Submitted {
504 return Ok(false);
505 }
506
507 if tx.hashes.len() <= 1 {
509 return Ok(false);
510 }
511
512 let age = get_age_of_sent_at(tx)?;
514 let min_age_for_recovery = get_evm_min_age_for_hash_recovery();
515
516 if age < min_age_for_recovery {
517 return Ok(false);
518 }
519
520 if tx.hashes.len() < EVM_MIN_HASHES_FOR_RECOVERY {
523 return Ok(false);
524 }
525
526 Ok(true)
527 }
528
529 async fn try_recover_with_historical_hashes(
538 &self,
539 tx: &TransactionRepoModel,
540 evm_data: &crate::models::EvmTransactionData,
541 ) -> Result<Option<TransactionRepoModel>, TransactionError> {
542 warn!(
543 tx_id = %tx.id,
544 current_hash = ?evm_data.hash,
545 total_hashes = %tx.hashes.len(),
546 "attempting hash recovery - checking historical hashes"
547 );
548
549 for (idx, historical_hash) in tx.hashes.iter().rev().enumerate() {
551 if Some(historical_hash) == evm_data.hash.as_ref() {
553 continue;
554 }
555
556 debug!(
557 tx_id = %tx.id,
558 hash = %historical_hash,
559 index = %idx,
560 "checking historical hash"
561 );
562
563 match self
565 .provider()
566 .get_transaction_receipt(historical_hash)
567 .await
568 {
569 Ok(Some(receipt)) => {
570 warn!(
571 tx_id = %tx.id,
572 mined_hash = %historical_hash,
573 wrong_hash = ?evm_data.hash,
574 block_number = ?receipt.block_number,
575 "RECOVERED: found mined transaction with historical hash - correcting database"
576 );
577
578 let updated_tx = self
581 .update_transaction_with_corrected_hash(
582 tx,
583 evm_data,
584 historical_hash,
585 TransactionStatus::Mined,
586 )
587 .await?;
588
589 return Ok(Some(updated_tx));
590 }
591 Ok(None) => {
592 continue;
594 }
595 Err(e) => {
596 warn!(
598 tx_id = %tx.id,
599 hash = %historical_hash,
600 error = %e,
601 "error checking historical hash, continuing to next"
602 );
603 continue;
604 }
605 }
606 }
607
608 debug!(
610 tx_id = %tx.id,
611 "hash recovery completed - no historical hashes found on-chain"
612 );
613 Ok(None)
614 }
615
616 async fn update_transaction_with_corrected_hash(
620 &self,
621 tx: &TransactionRepoModel,
622 evm_data: &crate::models::EvmTransactionData,
623 correct_hash: &str,
624 status: TransactionStatus,
625 ) -> Result<TransactionRepoModel, TransactionError> {
626 let mut corrected_data = evm_data.clone();
627 corrected_data.hash = Some(correct_hash.to_string());
628
629 let updated_tx = self
630 .transaction_repository()
631 .partial_update(
632 tx.id.clone(),
633 TransactionUpdateRequest {
634 network_data: Some(NetworkTransactionData::Evm(corrected_data)),
635 status: Some(status),
636 ..Default::default()
637 },
638 )
639 .await?;
640
641 if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
643 error!(
644 tx_id = %updated_tx.id,
645 error = %e,
646 "failed to send notification after hash recovery"
647 );
648 }
649
650 Ok(updated_tx)
651 }
652}
653
654#[cfg(test)]
655mod tests {
656 use crate::{
657 config::{EvmNetworkConfig, NetworkConfigCommon},
658 domain::transaction::evm::{EvmRelayerTransaction, MockPriceCalculatorTrait},
659 jobs::MockJobProducerTrait,
660 models::{
661 evm::Speed, EvmTransactionData, NetworkConfigData, NetworkRepoModel,
662 NetworkTransactionData, NetworkType, RelayerEvmPolicy, RelayerNetworkPolicy,
663 RelayerRepoModel, TransactionReceipt, TransactionRepoModel, TransactionStatus, U256,
664 },
665 repositories::{
666 MockNetworkRepository, MockRelayerRepository, MockTransactionCounterTrait,
667 MockTransactionRepository,
668 },
669 services::{provider::MockEvmProviderTrait, signer::MockSigner},
670 };
671 use alloy::{
672 consensus::{Eip658Value, Receipt, ReceiptWithBloom},
673 network::AnyReceiptEnvelope,
674 primitives::{b256, Address, BlockHash, Bloom, TxHash},
675 };
676 use chrono::{Duration, Utc};
677 use std::sync::Arc;
678
679 pub struct TestMocks {
681 pub provider: MockEvmProviderTrait,
682 pub relayer_repo: MockRelayerRepository,
683 pub network_repo: MockNetworkRepository,
684 pub tx_repo: MockTransactionRepository,
685 pub job_producer: MockJobProducerTrait,
686 pub signer: MockSigner,
687 pub counter: MockTransactionCounterTrait,
688 pub price_calc: MockPriceCalculatorTrait,
689 }
690
691 pub fn default_test_mocks() -> TestMocks {
694 TestMocks {
695 provider: MockEvmProviderTrait::new(),
696 relayer_repo: MockRelayerRepository::new(),
697 network_repo: MockNetworkRepository::new(),
698 tx_repo: MockTransactionRepository::new(),
699 job_producer: MockJobProducerTrait::new(),
700 signer: MockSigner::new(),
701 counter: MockTransactionCounterTrait::new(),
702 price_calc: MockPriceCalculatorTrait::new(),
703 }
704 }
705
706 pub fn default_test_mocks_with_network() -> TestMocks {
708 let mut mocks = default_test_mocks();
709 mocks
711 .network_repo
712 .expect_get_by_chain_id()
713 .returning(|network_type, chain_id| {
714 if network_type == NetworkType::Evm && chain_id == 1 {
715 Ok(Some(create_test_network_model()))
716 } else {
717 Ok(None)
718 }
719 });
720 mocks
721 }
722
723 pub fn create_test_network_model() -> NetworkRepoModel {
725 let evm_config = EvmNetworkConfig {
726 common: NetworkConfigCommon {
727 network: "mainnet".to_string(),
728 from: None,
729 rpc_urls: Some(vec!["https://rpc.example.com".to_string()]),
730 explorer_urls: Some(vec!["https://explorer.example.com".to_string()]),
731 average_blocktime_ms: Some(12000),
732 is_testnet: Some(false),
733 tags: Some(vec!["mainnet".to_string()]),
734 },
735 chain_id: Some(1),
736 required_confirmations: Some(12),
737 features: Some(vec!["eip1559".to_string()]),
738 symbol: Some("ETH".to_string()),
739 gas_price_cache: None,
740 };
741 NetworkRepoModel {
742 id: "evm:mainnet".to_string(),
743 name: "mainnet".to_string(),
744 network_type: NetworkType::Evm,
745 config: NetworkConfigData::Evm(evm_config),
746 }
747 }
748
749 pub fn create_test_no_mempool_network_model() -> NetworkRepoModel {
751 let evm_config = EvmNetworkConfig {
752 common: NetworkConfigCommon {
753 network: "arbitrum".to_string(),
754 from: None,
755 rpc_urls: Some(vec!["https://arb-rpc.example.com".to_string()]),
756 explorer_urls: Some(vec!["https://arb-explorer.example.com".to_string()]),
757 average_blocktime_ms: Some(1000),
758 is_testnet: Some(false),
759 tags: Some(vec![
760 "arbitrum".to_string(),
761 "rollup".to_string(),
762 "no-mempool".to_string(),
763 ]),
764 },
765 chain_id: Some(42161),
766 required_confirmations: Some(12),
767 features: Some(vec!["eip1559".to_string()]),
768 symbol: Some("ETH".to_string()),
769 gas_price_cache: None,
770 };
771 NetworkRepoModel {
772 id: "evm:arbitrum".to_string(),
773 name: "arbitrum".to_string(),
774 network_type: NetworkType::Evm,
775 config: NetworkConfigData::Evm(evm_config),
776 }
777 }
778
779 pub fn make_test_transaction(status: TransactionStatus) -> TransactionRepoModel {
783 TransactionRepoModel {
784 id: "test-tx-id".to_string(),
785 relayer_id: "test-relayer-id".to_string(),
786 status,
787 status_reason: None,
788 created_at: Utc::now().to_rfc3339(),
789 sent_at: None,
790 confirmed_at: None,
791 valid_until: None,
792 delete_at: None,
793 network_type: NetworkType::Evm,
794 network_data: NetworkTransactionData::Evm(EvmTransactionData {
795 chain_id: 1,
796 from: "0xSender".to_string(),
797 to: Some("0xRecipient".to_string()),
798 value: U256::from(0),
799 data: Some("0xData".to_string()),
800 gas_limit: Some(21000),
801 gas_price: Some(20000000000),
802 max_fee_per_gas: None,
803 max_priority_fee_per_gas: None,
804 nonce: None,
805 signature: None,
806 hash: None,
807 speed: Some(Speed::Fast),
808 raw: None,
809 }),
810 priced_at: None,
811 hashes: Vec::new(),
812 noop_count: None,
813 is_canceled: Some(false),
814 }
815 }
816
817 pub fn make_test_evm_relayer_transaction(
820 relayer: RelayerRepoModel,
821 mocks: TestMocks,
822 ) -> EvmRelayerTransaction<
823 MockEvmProviderTrait,
824 MockRelayerRepository,
825 MockNetworkRepository,
826 MockTransactionRepository,
827 MockJobProducerTrait,
828 MockSigner,
829 MockTransactionCounterTrait,
830 MockPriceCalculatorTrait,
831 > {
832 EvmRelayerTransaction::new(
833 relayer,
834 mocks.provider,
835 Arc::new(mocks.relayer_repo),
836 Arc::new(mocks.network_repo),
837 Arc::new(mocks.tx_repo),
838 Arc::new(mocks.counter),
839 Arc::new(mocks.job_producer),
840 mocks.price_calc,
841 mocks.signer,
842 )
843 .unwrap()
844 }
845
846 fn create_test_relayer() -> RelayerRepoModel {
847 RelayerRepoModel {
848 id: "test-relayer-id".to_string(),
849 name: "Test Relayer".to_string(),
850 paused: false,
851 system_disabled: false,
852 network: "test_network".to_string(),
853 network_type: NetworkType::Evm,
854 policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
855 signer_id: "test_signer".to_string(),
856 address: "0x".to_string(),
857 notification_id: None,
858 custom_rpc_urls: None,
859 ..Default::default()
860 }
861 }
862
863 fn make_mock_receipt(status: bool, block_number: Option<u64>) -> TransactionReceipt {
864 let tx_hash = TxHash::from(b256!(
866 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
867 ));
868 let block_hash = BlockHash::from(b256!(
869 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
870 ));
871 let from_address = Address::from([0x11; 20]);
872
873 TransactionReceipt {
874 inner: alloy::rpc::types::TransactionReceipt {
875 inner: AnyReceiptEnvelope {
876 inner: ReceiptWithBloom {
877 receipt: Receipt {
878 status: Eip658Value::Eip658(status), cumulative_gas_used: 0,
880 logs: vec![],
881 },
882 logs_bloom: Bloom::ZERO,
883 },
884 r#type: 0, },
886 transaction_hash: tx_hash,
887 transaction_index: Some(0),
888 block_hash: block_number.map(|_| block_hash), block_number,
890 gas_used: 21000,
891 effective_gas_price: 1000,
892 blob_gas_used: None,
893 blob_gas_price: None,
894 from: from_address,
895 to: None,
896 contract_address: None,
897 },
898 other: Default::default(),
899 }
900 }
901
902 mod check_transaction_status_tests {
904 use super::*;
905
906 #[tokio::test]
907 async fn test_not_mined() {
908 let mut mocks = default_test_mocks();
909 let relayer = create_test_relayer();
910 let mut tx = make_test_transaction(TransactionStatus::Submitted);
911
912 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
914 evm_data.hash = Some("0xFakeHash".to_string());
915 }
916
917 mocks
919 .provider
920 .expect_get_transaction_receipt()
921 .returning(|_| Box::pin(async { Ok(None) }));
922
923 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
924
925 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
926 assert_eq!(status, TransactionStatus::Submitted);
927 }
928
929 #[tokio::test]
930 async fn test_mined_but_not_confirmed() {
931 let mut mocks = default_test_mocks();
932 let relayer = create_test_relayer();
933 let mut tx = make_test_transaction(TransactionStatus::Submitted);
934
935 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
936 evm_data.hash = Some("0xFakeHash".to_string());
937 }
938
939 mocks
941 .provider
942 .expect_get_transaction_receipt()
943 .returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) }));
944
945 mocks
947 .provider
948 .expect_get_block_number()
949 .return_once(|| Box::pin(async { Ok(100) }));
950
951 mocks
953 .network_repo
954 .expect_get_by_chain_id()
955 .returning(|_, _| Ok(Some(create_test_network_model())));
956
957 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
958
959 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
960 assert_eq!(status, TransactionStatus::Mined);
961 }
962
963 #[tokio::test]
964 async fn test_confirmed() {
965 let mut mocks = default_test_mocks();
966 let relayer = create_test_relayer();
967 let mut tx = make_test_transaction(TransactionStatus::Submitted);
968
969 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
970 evm_data.hash = Some("0xFakeHash".to_string());
971 }
972
973 mocks
975 .provider
976 .expect_get_transaction_receipt()
977 .returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) }));
978
979 mocks
981 .provider
982 .expect_get_block_number()
983 .return_once(|| Box::pin(async { Ok(113) }));
984
985 mocks
987 .network_repo
988 .expect_get_by_chain_id()
989 .returning(|_, _| Ok(Some(create_test_network_model())));
990
991 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
992
993 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
994 assert_eq!(status, TransactionStatus::Confirmed);
995 }
996
997 #[tokio::test]
998 async fn test_failed() {
999 let mut mocks = default_test_mocks();
1000 let relayer = create_test_relayer();
1001 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1002
1003 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1004 evm_data.hash = Some("0xFakeHash".to_string());
1005 }
1006
1007 mocks
1009 .provider
1010 .expect_get_transaction_receipt()
1011 .returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(false, Some(100)))) }));
1012
1013 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1014
1015 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
1016 assert_eq!(status, TransactionStatus::Failed);
1017 }
1018 }
1019
1020 mod should_resubmit_tests {
1022 use super::*;
1023 use crate::models::TransactionError;
1024
1025 #[tokio::test]
1026 async fn test_should_resubmit_true() {
1027 let mut mocks = default_test_mocks();
1028 let relayer = create_test_relayer();
1029
1030 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1032 tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
1033
1034 mocks
1036 .network_repo
1037 .expect_get_by_chain_id()
1038 .returning(|_, _| Ok(Some(create_test_network_model())));
1039
1040 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1041 let res = evm_transaction.should_resubmit(&tx).await.unwrap();
1042 assert!(res, "Transaction should be resubmitted after timeout.");
1043 }
1044
1045 #[tokio::test]
1046 async fn test_should_resubmit_false() {
1047 let mut mocks = default_test_mocks();
1048 let relayer = create_test_relayer();
1049
1050 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1052 tx.sent_at = Some(Utc::now().to_rfc3339());
1053
1054 mocks
1056 .network_repo
1057 .expect_get_by_chain_id()
1058 .returning(|_, _| Ok(Some(create_test_network_model())));
1059
1060 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1061 let res = evm_transaction.should_resubmit(&tx).await.unwrap();
1062 assert!(!res, "Transaction should not be resubmitted immediately.");
1063 }
1064
1065 #[tokio::test]
1066 async fn test_should_resubmit_true_for_no_mempool_network() {
1067 let mut mocks = default_test_mocks();
1068 let relayer = create_test_relayer();
1069
1070 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1072 tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
1073
1074 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1076 evm_data.chain_id = 42161; }
1078
1079 mocks
1081 .network_repo
1082 .expect_get_by_chain_id()
1083 .returning(|_, _| Ok(Some(create_test_no_mempool_network_model())));
1084
1085 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1086 let res = evm_transaction.should_resubmit(&tx).await.unwrap();
1087 assert!(
1088 res,
1089 "Transaction should be resubmitted for no-mempool networks."
1090 );
1091 }
1092
1093 #[tokio::test]
1094 async fn test_should_resubmit_network_not_found() {
1095 let mut mocks = default_test_mocks();
1096 let relayer = create_test_relayer();
1097
1098 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1099 tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
1100
1101 mocks
1103 .network_repo
1104 .expect_get_by_chain_id()
1105 .returning(|_, _| Ok(None));
1106
1107 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1108 let result = evm_transaction.should_resubmit(&tx).await;
1109
1110 assert!(
1111 result.is_err(),
1112 "should_resubmit should return error when network not found"
1113 );
1114 let error = result.unwrap_err();
1115 match error {
1116 TransactionError::UnexpectedError(msg) => {
1117 assert!(msg.contains("Network with chain id 1 not found"));
1118 }
1119 _ => panic!("Expected UnexpectedError for network not found"),
1120 }
1121 }
1122
1123 #[tokio::test]
1124 async fn test_should_resubmit_network_conversion_error() {
1125 let mut mocks = default_test_mocks();
1126 let relayer = create_test_relayer();
1127
1128 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1129 tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
1130
1131 let invalid_evm_config = EvmNetworkConfig {
1133 common: NetworkConfigCommon {
1134 network: "invalid-network".to_string(),
1135 from: None,
1136 rpc_urls: Some(vec!["https://rpc.example.com".to_string()]),
1137 explorer_urls: Some(vec!["https://explorer.example.com".to_string()]),
1138 average_blocktime_ms: Some(12000),
1139 is_testnet: Some(false),
1140 tags: Some(vec!["testnet".to_string()]),
1141 },
1142 chain_id: None, required_confirmations: Some(12),
1144 features: Some(vec!["eip1559".to_string()]),
1145 symbol: Some("ETH".to_string()),
1146 gas_price_cache: None,
1147 };
1148 let invalid_network = NetworkRepoModel {
1149 id: "evm:invalid".to_string(),
1150 name: "invalid-network".to_string(),
1151 network_type: NetworkType::Evm,
1152 config: NetworkConfigData::Evm(invalid_evm_config),
1153 };
1154
1155 mocks
1157 .network_repo
1158 .expect_get_by_chain_id()
1159 .returning(move |_, _| Ok(Some(invalid_network.clone())));
1160
1161 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1162 let result = evm_transaction.should_resubmit(&tx).await;
1163
1164 assert!(
1165 result.is_err(),
1166 "should_resubmit should return error when network conversion fails"
1167 );
1168 let error = result.unwrap_err();
1169 match error {
1170 TransactionError::UnexpectedError(msg) => {
1171 assert!(msg.contains("Error converting network model to EvmNetwork"));
1172 }
1173 _ => panic!("Expected UnexpectedError for network conversion failure"),
1174 }
1175 }
1176 }
1177
1178 mod should_noop_tests {
1180 use super::*;
1181
1182 #[tokio::test]
1183 async fn test_expired_transaction_triggers_noop() {
1184 let mut mocks = default_test_mocks();
1185 let relayer = create_test_relayer();
1186
1187 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1188 tx.valid_until = Some((Utc::now() - Duration::seconds(10)).to_rfc3339());
1190
1191 mocks
1193 .network_repo
1194 .expect_get_by_chain_id()
1195 .returning(|_, _| Ok(Some(create_test_network_model())));
1196
1197 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1198 let res = evm_transaction.should_noop(&tx).await.unwrap();
1199 assert!(res, "Expired transaction should be replaced with a NOOP.");
1200 }
1201
1202 #[tokio::test]
1203 async fn test_too_many_noop_attempts_returns_false() {
1204 let mocks = default_test_mocks();
1205 let relayer = create_test_relayer();
1206
1207 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1208 tx.noop_count = Some(51); let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1211 let res = evm_transaction.should_noop(&tx).await.unwrap();
1212 assert!(
1213 !res,
1214 "Transaction with too many NOOP attempts should not be replaced."
1215 );
1216 }
1217
1218 #[tokio::test]
1219 async fn test_already_noop_returns_false() {
1220 let mut mocks = default_test_mocks();
1221 let relayer = create_test_relayer();
1222
1223 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1224 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1226 evm_data.to = None;
1227 evm_data.value = U256::from(0);
1228 }
1229
1230 mocks
1231 .network_repo
1232 .expect_get_by_chain_id()
1233 .returning(|_, _| Ok(Some(create_test_network_model())));
1234
1235 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1236 let res = evm_transaction.should_noop(&tx).await.unwrap();
1237 assert!(
1238 !res,
1239 "Transaction that is already a NOOP should not be replaced."
1240 );
1241 }
1242
1243 #[tokio::test]
1244 async fn test_rollup_with_too_many_attempts_triggers_noop() {
1245 let mut mocks = default_test_mocks();
1246 let relayer = create_test_relayer();
1247
1248 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1249 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1251 evm_data.chain_id = 42161; }
1253 tx.hashes = vec!["0xHash1".to_string(); 51];
1255
1256 mocks
1258 .network_repo
1259 .expect_get_by_chain_id()
1260 .returning(|_, _| Ok(Some(create_test_no_mempool_network_model())));
1261
1262 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1263 let res = evm_transaction.should_noop(&tx).await.unwrap();
1264 assert!(
1265 res,
1266 "Rollup transaction with too many attempts should be replaced with NOOP."
1267 );
1268 }
1269
1270 #[tokio::test]
1271 async fn test_pending_state_timeout_triggers_noop() {
1272 let mut mocks = default_test_mocks();
1273 let relayer = create_test_relayer();
1274
1275 let mut tx = make_test_transaction(TransactionStatus::Pending);
1276 tx.created_at = (Utc::now() - Duration::minutes(3)).to_rfc3339();
1278
1279 mocks
1280 .network_repo
1281 .expect_get_by_chain_id()
1282 .returning(|_, _| Ok(Some(create_test_network_model())));
1283
1284 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1285 let res = evm_transaction.should_noop(&tx).await.unwrap();
1286 assert!(
1287 res,
1288 "Pending transaction stuck for >2 minutes should be replaced with NOOP."
1289 );
1290 }
1291
1292 #[tokio::test]
1293 async fn test_valid_transaction_returns_false() {
1294 let mut mocks = default_test_mocks();
1295 let relayer = create_test_relayer();
1296
1297 let tx = make_test_transaction(TransactionStatus::Submitted);
1298 mocks
1301 .network_repo
1302 .expect_get_by_chain_id()
1303 .returning(|_, _| Ok(Some(create_test_network_model())));
1304
1305 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1306 let res = evm_transaction.should_noop(&tx).await.unwrap();
1307 assert!(!res, "Valid transaction should not be replaced with NOOP.");
1308 }
1309 }
1310
1311 mod update_transaction_status_tests {
1313 use super::*;
1314
1315 #[tokio::test]
1316 async fn test_no_update_when_status_is_same() {
1317 let mocks = default_test_mocks();
1319 let relayer = create_test_relayer();
1320 let tx = make_test_transaction(TransactionStatus::Submitted);
1321 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1322
1323 let updated_tx = evm_transaction
1326 .update_transaction_status_if_needed(tx.clone(), TransactionStatus::Submitted)
1327 .await
1328 .unwrap();
1329 assert_eq!(updated_tx.status, TransactionStatus::Submitted);
1330 assert_eq!(updated_tx.id, tx.id);
1331 }
1332
1333 #[tokio::test]
1334 async fn test_updates_when_status_differs() {
1335 let mut mocks = default_test_mocks();
1336 let relayer = create_test_relayer();
1337 let tx = make_test_transaction(TransactionStatus::Submitted);
1338
1339 mocks
1341 .tx_repo
1342 .expect_partial_update()
1343 .returning(|_, update| {
1344 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1345 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1346 Ok(updated_tx)
1347 });
1348
1349 mocks
1351 .job_producer
1352 .expect_produce_send_notification_job()
1353 .returning(|_, _| Box::pin(async { Ok(()) }));
1354
1355 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1356 let updated_tx = evm_transaction
1357 .update_transaction_status_if_needed(tx.clone(), TransactionStatus::Mined)
1358 .await
1359 .unwrap();
1360
1361 assert_eq!(updated_tx.status, TransactionStatus::Mined);
1362 }
1363 }
1364
1365 mod handle_sent_state_tests {
1367 use super::*;
1368
1369 #[tokio::test]
1370 async fn test_sent_state_recent_no_resend() {
1371 let mut mocks = default_test_mocks();
1372 let relayer = create_test_relayer();
1373
1374 let mut tx = make_test_transaction(TransactionStatus::Sent);
1375 tx.sent_at = Some((Utc::now() - Duration::seconds(10)).to_rfc3339());
1377
1378 mocks
1380 .job_producer
1381 .expect_produce_check_transaction_status_job()
1382 .returning(|_, _| Box::pin(async { Ok(()) }));
1383
1384 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1385 let result = evm_transaction.handle_sent_state(tx.clone()).await.unwrap();
1386
1387 assert_eq!(result.status, TransactionStatus::Sent);
1388 }
1389
1390 #[tokio::test]
1391 async fn test_sent_state_stuck_schedules_resubmit() {
1392 let mut mocks = default_test_mocks();
1393 let relayer = create_test_relayer();
1394
1395 let mut tx = make_test_transaction(TransactionStatus::Sent);
1396 tx.sent_at = Some((Utc::now() - Duration::seconds(60)).to_rfc3339());
1398
1399 mocks
1401 .job_producer
1402 .expect_produce_submit_transaction_job()
1403 .returning(|_, _| Box::pin(async { Ok(()) }));
1404
1405 mocks
1407 .job_producer
1408 .expect_produce_check_transaction_status_job()
1409 .returning(|_, _| Box::pin(async { Ok(()) }));
1410
1411 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1412 let result = evm_transaction.handle_sent_state(tx.clone()).await.unwrap();
1413
1414 assert_eq!(result.status, TransactionStatus::Sent);
1415 }
1416 }
1417
1418 mod prepare_noop_update_request_tests {
1420 use super::*;
1421
1422 #[tokio::test]
1423 async fn test_noop_request_without_cancellation() {
1424 let mocks = default_test_mocks_with_network();
1426 let relayer = create_test_relayer();
1427 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1428 tx.noop_count = Some(2);
1429 tx.is_canceled = Some(false);
1430
1431 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1432 let update_req = evm_transaction
1433 .prepare_noop_update_request(&tx, false)
1434 .await
1435 .unwrap();
1436
1437 assert_eq!(update_req.noop_count, Some(3));
1439 assert_eq!(update_req.is_canceled, Some(false));
1441 }
1442
1443 #[tokio::test]
1444 async fn test_noop_request_with_cancellation() {
1445 let mocks = default_test_mocks_with_network();
1447 let relayer = create_test_relayer();
1448 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1449 tx.noop_count = None;
1450 tx.is_canceled = Some(false);
1451
1452 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1453 let update_req = evm_transaction
1454 .prepare_noop_update_request(&tx, true)
1455 .await
1456 .unwrap();
1457
1458 assert_eq!(update_req.noop_count, Some(1));
1460 assert_eq!(update_req.is_canceled, Some(true));
1462 }
1463 }
1464
1465 mod handle_submitted_state_tests {
1467 use super::*;
1468
1469 #[tokio::test]
1470 async fn test_schedules_resubmit_job() {
1471 let mut mocks = default_test_mocks();
1472 let relayer = create_test_relayer();
1473
1474 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1476 tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
1477
1478 mocks
1480 .network_repo
1481 .expect_get_by_chain_id()
1482 .returning(|_, _| Ok(Some(create_test_network_model())));
1483
1484 mocks
1486 .job_producer
1487 .expect_produce_submit_transaction_job()
1488 .returning(|_, _| Box::pin(async { Ok(()) }));
1489
1490 mocks
1492 .job_producer
1493 .expect_produce_check_transaction_status_job()
1494 .returning(|_, _| Box::pin(async { Ok(()) }));
1495
1496 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1497 let updated_tx = evm_transaction.handle_submitted_state(tx).await.unwrap();
1498
1499 assert_eq!(updated_tx.status, TransactionStatus::Submitted);
1501 }
1502 }
1503
1504 mod handle_pending_state_tests {
1506 use super::*;
1507
1508 #[tokio::test]
1509 async fn test_pending_state_no_noop() {
1510 let mut mocks = default_test_mocks();
1512 let relayer = create_test_relayer();
1513 let mut tx = make_test_transaction(TransactionStatus::Pending);
1514 tx.created_at = Utc::now().to_rfc3339(); mocks
1518 .network_repo
1519 .expect_get_by_chain_id()
1520 .returning(|_, _| Ok(Some(create_test_network_model())));
1521
1522 mocks
1524 .job_producer
1525 .expect_produce_check_transaction_status_job()
1526 .returning(|_, _| Box::pin(async { Ok(()) }));
1527
1528 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1529 let result = evm_transaction
1530 .handle_pending_state(tx.clone())
1531 .await
1532 .unwrap();
1533
1534 assert_eq!(result.id, tx.id);
1536 assert_eq!(result.status, tx.status);
1537 assert_eq!(result.noop_count, tx.noop_count);
1538 }
1539
1540 #[tokio::test]
1541 async fn test_pending_state_with_noop() {
1542 let mut mocks = default_test_mocks();
1544 let relayer = create_test_relayer();
1545 let mut tx = make_test_transaction(TransactionStatus::Pending);
1546 tx.created_at = (Utc::now() - Duration::minutes(2)).to_rfc3339();
1547
1548 mocks
1550 .network_repo
1551 .expect_get_by_chain_id()
1552 .returning(|_, _| Ok(Some(create_test_network_model())));
1553
1554 let tx_clone = tx.clone();
1556 mocks
1557 .tx_repo
1558 .expect_partial_update()
1559 .returning(move |_, update| {
1560 let mut updated_tx = tx_clone.clone();
1561 updated_tx.noop_count = update.noop_count;
1562 Ok(updated_tx)
1563 });
1564 mocks
1566 .job_producer
1567 .expect_produce_submit_transaction_job()
1568 .returning(|_, _| Box::pin(async { Ok(()) }));
1569 mocks
1570 .job_producer
1571 .expect_produce_send_notification_job()
1572 .returning(|_, _| Box::pin(async { Ok(()) }));
1573
1574 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1575 let result = evm_transaction
1576 .handle_pending_state(tx.clone())
1577 .await
1578 .unwrap();
1579
1580 assert!(result.noop_count.unwrap_or(0) > 0);
1582 }
1583 }
1584
1585 mod handle_mined_state_tests {
1587 use super::*;
1588
1589 #[tokio::test]
1590 async fn test_updates_status_and_schedules_check() {
1591 let mut mocks = default_test_mocks();
1592 let relayer = create_test_relayer();
1593 let tx = make_test_transaction(TransactionStatus::Submitted);
1595
1596 mocks
1598 .job_producer
1599 .expect_produce_check_transaction_status_job()
1600 .returning(|_, _| Box::pin(async { Ok(()) }));
1601 mocks
1603 .tx_repo
1604 .expect_partial_update()
1605 .returning(|_, update| {
1606 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1607 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1608 Ok(updated_tx)
1609 });
1610
1611 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1612 let result = evm_transaction
1613 .handle_mined_state(tx.clone())
1614 .await
1615 .unwrap();
1616 assert_eq!(result.status, TransactionStatus::Mined);
1617 }
1618 }
1619
1620 mod handle_final_state_tests {
1622 use super::*;
1623
1624 #[tokio::test]
1625 async fn test_final_state_confirmed() {
1626 let mut mocks = default_test_mocks();
1627 let relayer = create_test_relayer();
1628 let tx = make_test_transaction(TransactionStatus::Submitted);
1629
1630 mocks
1632 .tx_repo
1633 .expect_partial_update()
1634 .returning(|_, update| {
1635 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1636 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1637 Ok(updated_tx)
1638 });
1639
1640 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1641 let result = evm_transaction
1642 .handle_final_state(tx.clone(), TransactionStatus::Confirmed)
1643 .await
1644 .unwrap();
1645 assert_eq!(result.status, TransactionStatus::Confirmed);
1646 }
1647
1648 #[tokio::test]
1649 async fn test_final_state_failed() {
1650 let mut mocks = default_test_mocks();
1651 let relayer = create_test_relayer();
1652 let tx = make_test_transaction(TransactionStatus::Submitted);
1653
1654 mocks
1656 .tx_repo
1657 .expect_partial_update()
1658 .returning(|_, update| {
1659 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1660 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1661 Ok(updated_tx)
1662 });
1663
1664 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1665 let result = evm_transaction
1666 .handle_final_state(tx.clone(), TransactionStatus::Failed)
1667 .await
1668 .unwrap();
1669 assert_eq!(result.status, TransactionStatus::Failed);
1670 }
1671
1672 #[tokio::test]
1673 async fn test_final_state_expired() {
1674 let mut mocks = default_test_mocks();
1675 let relayer = create_test_relayer();
1676 let tx = make_test_transaction(TransactionStatus::Submitted);
1677
1678 mocks
1680 .tx_repo
1681 .expect_partial_update()
1682 .returning(|_, update| {
1683 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1684 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1685 Ok(updated_tx)
1686 });
1687
1688 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1689 let result = evm_transaction
1690 .handle_final_state(tx.clone(), TransactionStatus::Expired)
1691 .await
1692 .unwrap();
1693 assert_eq!(result.status, TransactionStatus::Expired);
1694 }
1695 }
1696
1697 mod handle_status_impl_tests {
1699 use super::*;
1700
1701 #[tokio::test]
1702 async fn test_impl_submitted_branch() {
1703 let mut mocks = default_test_mocks();
1704 let relayer = create_test_relayer();
1705 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1706 tx.sent_at = Some((Utc::now() - Duration::seconds(120)).to_rfc3339());
1707 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1709 evm_data.hash = Some("0xFakeHash".to_string());
1710 }
1711 mocks
1713 .provider
1714 .expect_get_transaction_receipt()
1715 .returning(|_| Box::pin(async { Ok(None) }));
1716 mocks
1718 .network_repo
1719 .expect_get_by_chain_id()
1720 .returning(|_, _| Ok(Some(create_test_network_model())));
1721 mocks
1723 .job_producer
1724 .expect_produce_check_transaction_status_job()
1725 .returning(|_, _| Box::pin(async { Ok(()) }));
1726 mocks
1728 .tx_repo
1729 .expect_partial_update()
1730 .returning(|_, update| {
1731 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1732 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1733 Ok(updated_tx)
1734 });
1735
1736 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1737 let result = evm_transaction.handle_status_impl(tx).await.unwrap();
1738 assert_eq!(result.status, TransactionStatus::Submitted);
1739 }
1740
1741 #[tokio::test]
1742 async fn test_impl_mined_branch() {
1743 let mut mocks = default_test_mocks();
1744 let relayer = create_test_relayer();
1745 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1746 tx.created_at = (Utc::now() - Duration::minutes(1)).to_rfc3339();
1748 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1750 evm_data.hash = Some("0xFakeHash".to_string());
1751 }
1752 mocks
1754 .provider
1755 .expect_get_transaction_receipt()
1756 .returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) }));
1757 mocks
1759 .provider
1760 .expect_get_block_number()
1761 .return_once(|| Box::pin(async { Ok(100) }));
1762 mocks
1764 .network_repo
1765 .expect_get_by_chain_id()
1766 .returning(|_, _| Ok(Some(create_test_network_model())));
1767 mocks
1769 .job_producer
1770 .expect_produce_send_notification_job()
1771 .returning(|_, _| Box::pin(async { Ok(()) }));
1772 mocks.tx_repo.expect_get_by_id().returning(|_| {
1774 let updated_tx = make_test_transaction(TransactionStatus::Mined);
1775 Ok(updated_tx)
1776 });
1777 mocks
1779 .tx_repo
1780 .expect_partial_update()
1781 .returning(|_, update| {
1782 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1783 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1784 Ok(updated_tx)
1785 });
1786
1787 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1788 let result = evm_transaction.handle_status_impl(tx).await.unwrap();
1789 assert_eq!(result.status, TransactionStatus::Mined);
1790 }
1791
1792 #[tokio::test]
1793 async fn test_impl_final_confirmed_branch() {
1794 let mut mocks = default_test_mocks();
1795 let relayer = create_test_relayer();
1796 let tx = make_test_transaction(TransactionStatus::Confirmed);
1798
1799 mocks
1802 .tx_repo
1803 .expect_partial_update()
1804 .returning(|_, update| {
1805 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1806 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1807 Ok(updated_tx)
1808 });
1809
1810 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1811 let result = evm_transaction.handle_status_impl(tx).await.unwrap();
1812 assert_eq!(result.status, TransactionStatus::Confirmed);
1813 }
1814
1815 #[tokio::test]
1816 async fn test_impl_final_failed_branch() {
1817 let mut mocks = default_test_mocks();
1818 let relayer = create_test_relayer();
1819 let tx = make_test_transaction(TransactionStatus::Failed);
1821
1822 mocks
1823 .tx_repo
1824 .expect_partial_update()
1825 .returning(|_, update| {
1826 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1827 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1828 Ok(updated_tx)
1829 });
1830
1831 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1832 let result = evm_transaction.handle_status_impl(tx).await.unwrap();
1833 assert_eq!(result.status, TransactionStatus::Failed);
1834 }
1835
1836 #[tokio::test]
1837 async fn test_impl_final_expired_branch() {
1838 let mut mocks = default_test_mocks();
1839 let relayer = create_test_relayer();
1840 let tx = make_test_transaction(TransactionStatus::Expired);
1842
1843 mocks
1844 .tx_repo
1845 .expect_partial_update()
1846 .returning(|_, update| {
1847 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1848 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1849 Ok(updated_tx)
1850 });
1851
1852 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1853 let result = evm_transaction.handle_status_impl(tx).await.unwrap();
1854 assert_eq!(result.status, TransactionStatus::Expired);
1855 }
1856 }
1857
1858 mod hash_recovery_tests {
1860 use super::*;
1861
1862 #[tokio::test]
1863 async fn test_should_try_hash_recovery_not_submitted() {
1864 let mocks = default_test_mocks();
1865 let relayer = create_test_relayer();
1866
1867 let mut tx = make_test_transaction(TransactionStatus::Sent);
1868 tx.hashes = vec![
1869 "0xHash1".to_string(),
1870 "0xHash2".to_string(),
1871 "0xHash3".to_string(),
1872 ];
1873
1874 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1875 let result = evm_transaction.should_try_hash_recovery(&tx).unwrap();
1876
1877 assert!(
1878 !result,
1879 "Should not attempt recovery for non-Submitted transactions"
1880 );
1881 }
1882
1883 #[tokio::test]
1884 async fn test_should_try_hash_recovery_not_enough_hashes() {
1885 let mocks = default_test_mocks();
1886 let relayer = create_test_relayer();
1887
1888 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1889 tx.hashes = vec!["0xHash1".to_string()]; tx.sent_at = Some((Utc::now() - Duration::minutes(3)).to_rfc3339());
1891
1892 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1893 let result = evm_transaction.should_try_hash_recovery(&tx).unwrap();
1894
1895 assert!(
1896 !result,
1897 "Should not attempt recovery with insufficient hashes"
1898 );
1899 }
1900
1901 #[tokio::test]
1902 async fn test_should_try_hash_recovery_too_recent() {
1903 let mocks = default_test_mocks();
1904 let relayer = create_test_relayer();
1905
1906 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1907 tx.hashes = vec![
1908 "0xHash1".to_string(),
1909 "0xHash2".to_string(),
1910 "0xHash3".to_string(),
1911 ];
1912 tx.sent_at = Some(Utc::now().to_rfc3339()); let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1915 let result = evm_transaction.should_try_hash_recovery(&tx).unwrap();
1916
1917 assert!(
1918 !result,
1919 "Should not attempt recovery for recently sent transactions"
1920 );
1921 }
1922
1923 #[tokio::test]
1924 async fn test_should_try_hash_recovery_success() {
1925 let mocks = default_test_mocks();
1926 let relayer = create_test_relayer();
1927
1928 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1929 tx.hashes = vec![
1930 "0xHash1".to_string(),
1931 "0xHash2".to_string(),
1932 "0xHash3".to_string(),
1933 ];
1934 tx.sent_at = Some((Utc::now() - Duration::minutes(3)).to_rfc3339());
1935
1936 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1937 let result = evm_transaction.should_try_hash_recovery(&tx).unwrap();
1938
1939 assert!(
1940 result,
1941 "Should attempt recovery for stuck transactions with multiple hashes"
1942 );
1943 }
1944
1945 #[tokio::test]
1946 async fn test_try_recover_no_historical_hash_found() {
1947 let mut mocks = default_test_mocks();
1948 let relayer = create_test_relayer();
1949
1950 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1951 tx.hashes = vec![
1952 "0xHash1".to_string(),
1953 "0xHash2".to_string(),
1954 "0xHash3".to_string(),
1955 ];
1956
1957 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1958 evm_data.hash = Some("0xHash3".to_string());
1959 }
1960
1961 mocks
1963 .provider
1964 .expect_get_transaction_receipt()
1965 .returning(|_| Box::pin(async { Ok(None) }));
1966
1967 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1968 let evm_data = tx.network_data.get_evm_transaction_data().unwrap();
1969 let result = evm_transaction
1970 .try_recover_with_historical_hashes(&tx, &evm_data)
1971 .await
1972 .unwrap();
1973
1974 assert!(
1975 result.is_none(),
1976 "Should return None when no historical hash is found"
1977 );
1978 }
1979
1980 #[tokio::test]
1981 async fn test_try_recover_finds_mined_historical_hash() {
1982 let mut mocks = default_test_mocks();
1983 let relayer = create_test_relayer();
1984
1985 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1986 tx.hashes = vec![
1987 "0xHash1".to_string(),
1988 "0xHash2".to_string(), "0xHash3".to_string(),
1990 ];
1991
1992 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1993 evm_data.hash = Some("0xHash3".to_string()); }
1995
1996 mocks
1998 .provider
1999 .expect_get_transaction_receipt()
2000 .returning(|hash| {
2001 if hash == "0xHash2" {
2002 Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) })
2003 } else {
2004 Box::pin(async { Ok(None) })
2005 }
2006 });
2007
2008 let tx_clone = tx.clone();
2010 mocks
2011 .tx_repo
2012 .expect_partial_update()
2013 .returning(move |_, update| {
2014 let mut updated_tx = tx_clone.clone();
2015 if let Some(status) = update.status {
2016 updated_tx.status = status;
2017 }
2018 if let Some(NetworkTransactionData::Evm(ref evm_data)) = update.network_data {
2019 if let NetworkTransactionData::Evm(ref mut updated_evm) =
2020 updated_tx.network_data
2021 {
2022 updated_evm.hash = evm_data.hash.clone();
2023 }
2024 }
2025 Ok(updated_tx)
2026 });
2027
2028 mocks
2030 .job_producer
2031 .expect_produce_send_notification_job()
2032 .returning(|_, _| Box::pin(async { Ok(()) }));
2033
2034 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2035 let evm_data = tx.network_data.get_evm_transaction_data().unwrap();
2036 let result = evm_transaction
2037 .try_recover_with_historical_hashes(&tx, &evm_data)
2038 .await
2039 .unwrap();
2040
2041 assert!(result.is_some(), "Should recover the transaction");
2042 let recovered_tx = result.unwrap();
2043 assert_eq!(recovered_tx.status, TransactionStatus::Mined);
2044 }
2045
2046 #[tokio::test]
2047 async fn test_try_recover_network_error_continues() {
2048 let mut mocks = default_test_mocks();
2049 let relayer = create_test_relayer();
2050
2051 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2052 tx.hashes = vec![
2053 "0xHash1".to_string(),
2054 "0xHash2".to_string(), "0xHash3".to_string(), ];
2057
2058 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
2059 evm_data.hash = Some("0xHash1".to_string());
2060 }
2061
2062 mocks
2064 .provider
2065 .expect_get_transaction_receipt()
2066 .returning(|hash| {
2067 if hash == "0xHash2" {
2068 Box::pin(async { Err(crate::services::provider::ProviderError::Timeout) })
2069 } else if hash == "0xHash3" {
2070 Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) })
2071 } else {
2072 Box::pin(async { Ok(None) })
2073 }
2074 });
2075
2076 let tx_clone = tx.clone();
2078 mocks
2079 .tx_repo
2080 .expect_partial_update()
2081 .returning(move |_, update| {
2082 let mut updated_tx = tx_clone.clone();
2083 if let Some(status) = update.status {
2084 updated_tx.status = status;
2085 }
2086 Ok(updated_tx)
2087 });
2088
2089 mocks
2091 .job_producer
2092 .expect_produce_send_notification_job()
2093 .returning(|_, _| Box::pin(async { Ok(()) }));
2094
2095 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2096 let evm_data = tx.network_data.get_evm_transaction_data().unwrap();
2097 let result = evm_transaction
2098 .try_recover_with_historical_hashes(&tx, &evm_data)
2099 .await
2100 .unwrap();
2101
2102 assert!(
2103 result.is_some(),
2104 "Should continue checking after network error and find mined hash"
2105 );
2106 }
2107
2108 #[tokio::test]
2109 async fn test_update_transaction_with_corrected_hash() {
2110 let mut mocks = default_test_mocks();
2111 let relayer = create_test_relayer();
2112
2113 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2114 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
2115 evm_data.hash = Some("0xWrongHash".to_string());
2116 }
2117
2118 mocks
2120 .tx_repo
2121 .expect_partial_update()
2122 .returning(move |_, update| {
2123 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2124 if let Some(status) = update.status {
2125 updated_tx.status = status;
2126 }
2127 if let Some(NetworkTransactionData::Evm(ref evm_data)) = update.network_data {
2128 if let NetworkTransactionData::Evm(ref mut updated_evm) =
2129 updated_tx.network_data
2130 {
2131 updated_evm.hash = evm_data.hash.clone();
2132 }
2133 }
2134 Ok(updated_tx)
2135 });
2136
2137 mocks
2139 .job_producer
2140 .expect_produce_send_notification_job()
2141 .returning(|_, _| Box::pin(async { Ok(()) }));
2142
2143 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2144 let evm_data = tx.network_data.get_evm_transaction_data().unwrap();
2145 let result = evm_transaction
2146 .update_transaction_with_corrected_hash(
2147 &tx,
2148 &evm_data,
2149 "0xCorrectHash",
2150 TransactionStatus::Mined,
2151 )
2152 .await
2153 .unwrap();
2154
2155 assert_eq!(result.status, TransactionStatus::Mined);
2156 if let NetworkTransactionData::Evm(ref updated_evm) = result.network_data {
2157 assert_eq!(updated_evm.hash.as_ref().unwrap(), "0xCorrectHash");
2158 }
2159 }
2160 }
2161
2162 mod check_transaction_status_edge_cases {
2164 use super::*;
2165
2166 #[tokio::test]
2167 async fn test_missing_hash_returns_error() {
2168 let mocks = default_test_mocks();
2169 let relayer = create_test_relayer();
2170
2171 let tx = make_test_transaction(TransactionStatus::Submitted);
2172 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2175 let result = evm_transaction.check_transaction_status(&tx).await;
2176
2177 assert!(result.is_err(), "Should return error when hash is missing");
2178 }
2179
2180 #[tokio::test]
2181 async fn test_pending_status_early_return() {
2182 let mocks = default_test_mocks();
2183 let relayer = create_test_relayer();
2184
2185 let tx = make_test_transaction(TransactionStatus::Pending);
2186
2187 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2188 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
2189
2190 assert_eq!(
2191 status,
2192 TransactionStatus::Pending,
2193 "Should return Pending without querying blockchain"
2194 );
2195 }
2196
2197 #[tokio::test]
2198 async fn test_sent_status_early_return() {
2199 let mocks = default_test_mocks();
2200 let relayer = create_test_relayer();
2201
2202 let tx = make_test_transaction(TransactionStatus::Sent);
2203
2204 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2205 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
2206
2207 assert_eq!(
2208 status,
2209 TransactionStatus::Sent,
2210 "Should return Sent without querying blockchain"
2211 );
2212 }
2213
2214 #[tokio::test]
2215 async fn test_final_state_early_return() {
2216 let mocks = default_test_mocks();
2217 let relayer = create_test_relayer();
2218
2219 let tx = make_test_transaction(TransactionStatus::Confirmed);
2220
2221 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2222 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
2223
2224 assert_eq!(
2225 status,
2226 TransactionStatus::Confirmed,
2227 "Should return final state without querying blockchain"
2228 );
2229 }
2230 }
2231}