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,
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(
187 &self,
188 tx: &TransactionRepoModel,
189 ) -> Result<(bool, Option<String>), TransactionError> {
190 if too_many_noop_attempts(tx) {
191 info!("Transaction has too many NOOP attempts already");
192 return Ok((false, None));
193 }
194
195 let evm_data = tx.network_data.get_evm_transaction_data()?;
196 if is_noop(&evm_data) {
197 return Ok((false, None));
198 }
199
200 let network_model = self
201 .network_repository()
202 .get_by_chain_id(NetworkType::Evm, evm_data.chain_id)
203 .await?
204 .ok_or(TransactionError::UnexpectedError(format!(
205 "Network with chain id {} not found",
206 evm_data.chain_id
207 )))?;
208
209 let network = EvmNetwork::try_from(network_model).map_err(|e| {
210 TransactionError::UnexpectedError(format!(
211 "Error converting network model to EvmNetwork: {e}"
212 ))
213 })?;
214
215 if network.is_rollup() && too_many_attempts(tx) {
216 let reason =
217 "Rollup transaction has too many attempts. Replacing with NOOP.".to_string();
218 info!("{}", reason);
219 return Ok((true, Some(reason)));
220 }
221
222 if !is_transaction_valid(&tx.created_at, &tx.valid_until) {
223 let reason = "Transaction is expired. Replacing with NOOP.".to_string();
224 info!("{}", reason);
225 return Ok((true, Some(reason)));
226 }
227
228 if tx.status == TransactionStatus::Pending {
229 let created_at = &tx.created_at;
230 let created_time = DateTime::parse_from_rfc3339(created_at)
231 .map_err(|e| {
232 TransactionError::UnexpectedError(format!("Invalid created_at timestamp: {e}"))
233 })?
234 .with_timezone(&Utc);
235 let age = Utc::now().signed_duration_since(created_time);
236 if age > get_evm_prepare_timeout() {
237 let reason = format!(
238 "Transaction in Pending state for over {} minutes. Replacing with NOOP.",
239 get_evm_prepare_timeout().num_minutes()
240 );
241 info!("{}", reason);
242 return Ok((true, Some(reason)));
243 }
244 }
245
246 let latest_block = self.provider().get_block_by_number().await;
247 if let Ok(block) = latest_block {
248 let block_gas_limit = block.header.gas_limit;
249 if let Some(gas_limit) = evm_data.gas_limit {
250 if gas_limit > block_gas_limit {
251 let reason = format!(
252 "Transaction gas limit ({gas_limit}) exceeds block gas limit ({block_gas_limit}). Replacing with NOOP.",
253 );
254 warn!(
255 tx_id = %tx.id,
256 tx_gas_limit = %gas_limit,
257 block_gas_limit = %block_gas_limit,
258 "transaction gas limit exceeds block gas limit, replacing with NOOP"
259 );
260 return Ok((true, Some(reason)));
261 }
262 }
263 }
264
265 Ok((false, None))
266 }
267
268 pub(super) async fn update_transaction_status_if_needed(
270 &self,
271 tx: TransactionRepoModel,
272 new_status: TransactionStatus,
273 ) -> Result<TransactionRepoModel, TransactionError> {
274 if tx.status != new_status {
275 return self.update_transaction_status(tx, new_status).await;
276 }
277 Ok(tx)
278 }
279
280 pub(super) async fn prepare_noop_update_request(
282 &self,
283 tx: &TransactionRepoModel,
284 is_cancellation: bool,
285 reason: Option<String>,
286 ) -> Result<TransactionUpdateRequest, TransactionError> {
287 let mut evm_data = tx.network_data.get_evm_transaction_data()?;
288 let network_model = self
289 .network_repository()
290 .get_by_chain_id(NetworkType::Evm, evm_data.chain_id)
291 .await?
292 .ok_or(TransactionError::UnexpectedError(format!(
293 "Network with chain id {} not found",
294 evm_data.chain_id
295 )))?;
296
297 let network = EvmNetwork::try_from(network_model).map_err(|e| {
298 TransactionError::UnexpectedError(format!(
299 "Error converting network model to EvmNetwork: {e}"
300 ))
301 })?;
302
303 make_noop(&mut evm_data, &network, Some(self.provider())).await?;
304
305 let noop_count = tx.noop_count.unwrap_or(0) + 1;
306 let update_request = TransactionUpdateRequest {
307 network_data: Some(NetworkTransactionData::Evm(evm_data)),
308 noop_count: Some(noop_count),
309 status_reason: reason,
310 is_canceled: if is_cancellation {
311 Some(true)
312 } else {
313 tx.is_canceled
314 },
315 ..Default::default()
316 };
317 Ok(update_request)
318 }
319
320 async fn handle_submitted_state(
322 &self,
323 tx: TransactionRepoModel,
324 ) -> Result<TransactionRepoModel, TransactionError> {
325 if self.should_resubmit(&tx).await? {
326 let resubmitted_tx = self.handle_resubmission(tx).await?;
327 return Ok(resubmitted_tx);
328 }
329
330 self.update_transaction_status_if_needed(tx, TransactionStatus::Submitted)
331 .await
332 }
333
334 async fn handle_resubmission(
336 &self,
337 tx: TransactionRepoModel,
338 ) -> Result<TransactionRepoModel, TransactionError> {
339 debug!("scheduling resubmit job for transaction");
340
341 let (should_noop, reason) = self.should_noop(&tx).await?;
343 let tx_to_process = if should_noop {
344 self.process_noop_transaction(&tx, reason).await?
345 } else {
346 tx
347 };
348
349 self.send_transaction_resubmit_job(&tx_to_process).await?;
350 Ok(tx_to_process)
351 }
352
353 async fn process_noop_transaction(
355 &self,
356 tx: &TransactionRepoModel,
357 reason: Option<String>,
358 ) -> Result<TransactionRepoModel, TransactionError> {
359 debug!("preparing transaction NOOP before resubmission");
360 let update = self.prepare_noop_update_request(tx, false, reason).await?;
361 let updated_tx = self
362 .transaction_repository()
363 .partial_update(tx.id.clone(), update)
364 .await?;
365
366 let res = self.send_transaction_update_notification(&updated_tx).await;
367 if let Err(e) = res {
368 error!(
369 tx_id = %updated_tx.id,
370 status = ?updated_tx.status,
371 "sending transaction update notification failed for NOOP transaction: {:?}",
372 e
373 );
374 }
375 Ok(updated_tx)
376 }
377
378 async fn handle_pending_state(
380 &self,
381 tx: TransactionRepoModel,
382 ) -> Result<TransactionRepoModel, TransactionError> {
383 let (should_noop, reason) = self.should_noop(&tx).await?;
384 if should_noop {
385 debug!(
388 tx_id = %tx.id,
389 reason = %reason.as_ref().unwrap_or(&"unknown".to_string()),
390 "marking pending transaction as Failed (nonce not assigned, no NOOP needed)"
391 );
392 let update = TransactionUpdateRequest {
393 status: Some(TransactionStatus::Failed),
394 status_reason: reason,
395 ..Default::default()
396 };
397 let updated_tx = self
398 .transaction_repository()
399 .partial_update(tx.id.clone(), update)
400 .await?;
401
402 let res = self.send_transaction_update_notification(&updated_tx).await;
403 if let Err(e) = res {
404 error!(
405 tx_id = %updated_tx.id,
406 status = ?updated_tx.status,
407 "sending transaction update notification failed: {:?}",
408 e
409 );
410 }
411 return Ok(updated_tx);
412 }
413
414 let age = get_age_since_created(&tx)?;
416 if age > get_evm_pending_recovery_trigger_timeout() {
417 warn!(
418 tx_id = %tx.id,
419 age_seconds = age.num_seconds(),
420 "transaction stuck in Pending, queuing prepare job"
421 );
422
423 self.send_transaction_request_job(&tx).await?;
425 }
426
427 Ok(tx)
428 }
429
430 async fn handle_mined_state(
432 &self,
433 tx: TransactionRepoModel,
434 ) -> Result<TransactionRepoModel, TransactionError> {
435 self.update_transaction_status_if_needed(tx, TransactionStatus::Mined)
436 .await
437 }
438
439 async fn handle_final_state(
441 &self,
442 tx: TransactionRepoModel,
443 status: TransactionStatus,
444 ) -> Result<TransactionRepoModel, TransactionError> {
445 self.update_transaction_status_if_needed(tx, status).await
446 }
447
448 pub async fn handle_status_impl(
453 &self,
454 tx: TransactionRepoModel,
455 ) -> Result<TransactionRepoModel, TransactionError> {
456 debug!("checking transaction status {}", tx.id);
457
458 if is_final_state(&tx.status) {
460 debug!(status = ?tx.status, "transaction already in final state");
461 return Ok(tx);
462 }
463
464 let status = self.check_transaction_status(&tx).await?;
469
470 debug!(
471 tx_id = %tx.id,
472 previous_status = ?tx.status,
473 new_status = ?status,
474 "transaction status check completed"
475 );
476
477 let tx = if status != tx.status {
481 debug!(
482 tx_id = %tx.id,
483 old_status = ?tx.status,
484 new_status = ?status,
485 "status changed during check, reloading transaction from DB to ensure fresh data"
486 );
487 self.transaction_repository()
488 .get_by_id(tx.id.clone())
489 .await?
490 } else {
491 tx
492 };
493
494 if is_too_early_to_resubmit(&tx)? && is_pending_transaction(&status) {
499 return self.update_transaction_status_if_needed(tx, status).await;
501 }
502
503 match status {
505 TransactionStatus::Pending => self.handle_pending_state(tx).await,
506 TransactionStatus::Sent => self.handle_sent_state(tx).await,
507 TransactionStatus::Submitted => self.handle_submitted_state(tx).await,
508 TransactionStatus::Mined => self.handle_mined_state(tx).await,
509 TransactionStatus::Confirmed
510 | TransactionStatus::Failed
511 | TransactionStatus::Expired
512 | TransactionStatus::Canceled => self.handle_final_state(tx, status).await,
513 }
514 }
515
516 async fn handle_sent_state(
518 &self,
519 tx: TransactionRepoModel,
520 ) -> Result<TransactionRepoModel, TransactionError> {
521 debug!(tx_id = %tx.id, "handling Sent state");
522
523 let (should_noop, reason) = self.should_noop(&tx).await?;
525 if should_noop {
526 debug!("preparing NOOP for sent transaction {}", tx.id);
527 let update = self.prepare_noop_update_request(&tx, false, reason).await?;
528 let updated_tx = self
529 .transaction_repository()
530 .partial_update(tx.id.clone(), update)
531 .await?;
532
533 self.send_transaction_submit_job(&updated_tx).await?;
534 let res = self.send_transaction_update_notification(&updated_tx).await;
535 if let Err(e) = res {
536 error!(
537 tx_id = %updated_tx.id,
538 status = ?updated_tx.status,
539 "sending transaction update notification failed for Sent state NOOP: {:?}",
540 e
541 );
542 }
543 return Ok(updated_tx);
544 }
545
546 let age_since_sent = get_age_since_status_change(&tx)?;
549
550 if age_since_sent > get_evm_resend_timeout() {
551 warn!(
552 tx_id = %tx.id,
553 age_seconds = age_since_sent.num_seconds(),
554 "transaction stuck in Sent, queuing resubmit job with repricing"
555 );
556
557 self.send_transaction_resubmit_job(&tx).await?;
559 }
560
561 self.update_transaction_status_if_needed(tx, TransactionStatus::Sent)
562 .await
563 }
564
565 fn should_try_hash_recovery(
572 &self,
573 tx: &TransactionRepoModel,
574 ) -> Result<bool, TransactionError> {
575 if tx.status != TransactionStatus::Submitted {
577 return Ok(false);
578 }
579
580 if tx.hashes.len() <= 1 {
582 return Ok(false);
583 }
584
585 let age = get_age_of_sent_at(tx)?;
587 let min_age_for_recovery = get_evm_min_age_for_hash_recovery();
588
589 if age < min_age_for_recovery {
590 return Ok(false);
591 }
592
593 if tx.hashes.len() < EVM_MIN_HASHES_FOR_RECOVERY {
596 return Ok(false);
597 }
598
599 Ok(true)
600 }
601
602 async fn try_recover_with_historical_hashes(
611 &self,
612 tx: &TransactionRepoModel,
613 evm_data: &crate::models::EvmTransactionData,
614 ) -> Result<Option<TransactionRepoModel>, TransactionError> {
615 warn!(
616 tx_id = %tx.id,
617 current_hash = ?evm_data.hash,
618 total_hashes = %tx.hashes.len(),
619 "attempting hash recovery - checking historical hashes"
620 );
621
622 for (idx, historical_hash) in tx.hashes.iter().rev().enumerate() {
624 if Some(historical_hash) == evm_data.hash.as_ref() {
626 continue;
627 }
628
629 debug!(
630 tx_id = %tx.id,
631 hash = %historical_hash,
632 index = %idx,
633 "checking historical hash"
634 );
635
636 match self
638 .provider()
639 .get_transaction_receipt(historical_hash)
640 .await
641 {
642 Ok(Some(receipt)) => {
643 warn!(
644 tx_id = %tx.id,
645 mined_hash = %historical_hash,
646 wrong_hash = ?evm_data.hash,
647 block_number = ?receipt.block_number,
648 "RECOVERED: found mined transaction with historical hash - correcting database"
649 );
650
651 let updated_tx = self
654 .update_transaction_with_corrected_hash(
655 tx,
656 evm_data,
657 historical_hash,
658 TransactionStatus::Mined,
659 )
660 .await?;
661
662 return Ok(Some(updated_tx));
663 }
664 Ok(None) => {
665 continue;
667 }
668 Err(e) => {
669 warn!(
671 tx_id = %tx.id,
672 hash = %historical_hash,
673 error = %e,
674 "error checking historical hash, continuing to next"
675 );
676 continue;
677 }
678 }
679 }
680
681 debug!(
683 tx_id = %tx.id,
684 "hash recovery completed - no historical hashes found on-chain"
685 );
686 Ok(None)
687 }
688
689 async fn update_transaction_with_corrected_hash(
693 &self,
694 tx: &TransactionRepoModel,
695 evm_data: &crate::models::EvmTransactionData,
696 correct_hash: &str,
697 status: TransactionStatus,
698 ) -> Result<TransactionRepoModel, TransactionError> {
699 let mut corrected_data = evm_data.clone();
700 corrected_data.hash = Some(correct_hash.to_string());
701
702 let updated_tx = self
703 .transaction_repository()
704 .partial_update(
705 tx.id.clone(),
706 TransactionUpdateRequest {
707 network_data: Some(NetworkTransactionData::Evm(corrected_data)),
708 status: Some(status),
709 ..Default::default()
710 },
711 )
712 .await?;
713
714 if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
716 error!(
717 tx_id = %updated_tx.id,
718 error = %e,
719 "failed to send notification after hash recovery"
720 );
721 }
722
723 Ok(updated_tx)
724 }
725}
726
727#[cfg(test)]
728mod tests {
729 use crate::{
730 config::{EvmNetworkConfig, NetworkConfigCommon},
731 domain::transaction::evm::{EvmRelayerTransaction, MockPriceCalculatorTrait},
732 jobs::MockJobProducerTrait,
733 models::{
734 evm::Speed, EvmTransactionData, NetworkConfigData, NetworkRepoModel,
735 NetworkTransactionData, NetworkType, RelayerEvmPolicy, RelayerNetworkPolicy,
736 RelayerRepoModel, TransactionReceipt, TransactionRepoModel, TransactionStatus, U256,
737 },
738 repositories::{
739 MockNetworkRepository, MockRelayerRepository, MockTransactionCounterTrait,
740 MockTransactionRepository,
741 },
742 services::{provider::MockEvmProviderTrait, signer::MockSigner},
743 };
744 use alloy::{
745 consensus::{Eip658Value, Receipt, ReceiptWithBloom},
746 network::AnyReceiptEnvelope,
747 primitives::{b256, Address, BlockHash, Bloom, TxHash},
748 };
749 use chrono::{Duration, Utc};
750 use std::sync::Arc;
751
752 pub struct TestMocks {
754 pub provider: MockEvmProviderTrait,
755 pub relayer_repo: MockRelayerRepository,
756 pub network_repo: MockNetworkRepository,
757 pub tx_repo: MockTransactionRepository,
758 pub job_producer: MockJobProducerTrait,
759 pub signer: MockSigner,
760 pub counter: MockTransactionCounterTrait,
761 pub price_calc: MockPriceCalculatorTrait,
762 }
763
764 pub fn default_test_mocks() -> TestMocks {
767 TestMocks {
768 provider: MockEvmProviderTrait::new(),
769 relayer_repo: MockRelayerRepository::new(),
770 network_repo: MockNetworkRepository::new(),
771 tx_repo: MockTransactionRepository::new(),
772 job_producer: MockJobProducerTrait::new(),
773 signer: MockSigner::new(),
774 counter: MockTransactionCounterTrait::new(),
775 price_calc: MockPriceCalculatorTrait::new(),
776 }
777 }
778
779 pub fn default_test_mocks_with_network() -> TestMocks {
781 let mut mocks = default_test_mocks();
782 mocks
784 .network_repo
785 .expect_get_by_chain_id()
786 .returning(|network_type, chain_id| {
787 if network_type == NetworkType::Evm && chain_id == 1 {
788 Ok(Some(create_test_network_model()))
789 } else {
790 Ok(None)
791 }
792 });
793 mocks
794 }
795
796 pub fn create_test_network_model() -> NetworkRepoModel {
798 let evm_config = EvmNetworkConfig {
799 common: NetworkConfigCommon {
800 network: "mainnet".to_string(),
801 from: None,
802 rpc_urls: Some(vec!["https://rpc.example.com".to_string()]),
803 explorer_urls: Some(vec!["https://explorer.example.com".to_string()]),
804 average_blocktime_ms: Some(12000),
805 is_testnet: Some(false),
806 tags: Some(vec!["mainnet".to_string()]),
807 },
808 chain_id: Some(1),
809 required_confirmations: Some(12),
810 features: Some(vec!["eip1559".to_string()]),
811 symbol: Some("ETH".to_string()),
812 gas_price_cache: None,
813 };
814 NetworkRepoModel {
815 id: "evm:mainnet".to_string(),
816 name: "mainnet".to_string(),
817 network_type: NetworkType::Evm,
818 config: NetworkConfigData::Evm(evm_config),
819 }
820 }
821
822 pub fn create_test_no_mempool_network_model() -> NetworkRepoModel {
824 let evm_config = EvmNetworkConfig {
825 common: NetworkConfigCommon {
826 network: "arbitrum".to_string(),
827 from: None,
828 rpc_urls: Some(vec!["https://arb-rpc.example.com".to_string()]),
829 explorer_urls: Some(vec!["https://arb-explorer.example.com".to_string()]),
830 average_blocktime_ms: Some(1000),
831 is_testnet: Some(false),
832 tags: Some(vec![
833 "arbitrum".to_string(),
834 "rollup".to_string(),
835 "no-mempool".to_string(),
836 ]),
837 },
838 chain_id: Some(42161),
839 required_confirmations: Some(12),
840 features: Some(vec!["eip1559".to_string()]),
841 symbol: Some("ETH".to_string()),
842 gas_price_cache: None,
843 };
844 NetworkRepoModel {
845 id: "evm:arbitrum".to_string(),
846 name: "arbitrum".to_string(),
847 network_type: NetworkType::Evm,
848 config: NetworkConfigData::Evm(evm_config),
849 }
850 }
851
852 pub fn make_test_transaction(status: TransactionStatus) -> TransactionRepoModel {
856 TransactionRepoModel {
857 id: "test-tx-id".to_string(),
858 relayer_id: "test-relayer-id".to_string(),
859 status,
860 status_reason: None,
861 created_at: Utc::now().to_rfc3339(),
862 sent_at: None,
863 confirmed_at: None,
864 valid_until: None,
865 delete_at: None,
866 network_type: NetworkType::Evm,
867 network_data: NetworkTransactionData::Evm(EvmTransactionData {
868 chain_id: 1,
869 from: "0xSender".to_string(),
870 to: Some("0xRecipient".to_string()),
871 value: U256::from(0),
872 data: Some("0xData".to_string()),
873 gas_limit: Some(21000),
874 gas_price: Some(20000000000),
875 max_fee_per_gas: None,
876 max_priority_fee_per_gas: None,
877 nonce: None,
878 signature: None,
879 hash: None,
880 speed: Some(Speed::Fast),
881 raw: None,
882 }),
883 priced_at: None,
884 hashes: Vec::new(),
885 noop_count: None,
886 is_canceled: Some(false),
887 }
888 }
889
890 pub fn make_test_evm_relayer_transaction(
893 relayer: RelayerRepoModel,
894 mocks: TestMocks,
895 ) -> EvmRelayerTransaction<
896 MockEvmProviderTrait,
897 MockRelayerRepository,
898 MockNetworkRepository,
899 MockTransactionRepository,
900 MockJobProducerTrait,
901 MockSigner,
902 MockTransactionCounterTrait,
903 MockPriceCalculatorTrait,
904 > {
905 EvmRelayerTransaction::new(
906 relayer,
907 mocks.provider,
908 Arc::new(mocks.relayer_repo),
909 Arc::new(mocks.network_repo),
910 Arc::new(mocks.tx_repo),
911 Arc::new(mocks.counter),
912 Arc::new(mocks.job_producer),
913 mocks.price_calc,
914 mocks.signer,
915 )
916 .unwrap()
917 }
918
919 fn create_test_relayer() -> RelayerRepoModel {
920 RelayerRepoModel {
921 id: "test-relayer-id".to_string(),
922 name: "Test Relayer".to_string(),
923 paused: false,
924 system_disabled: false,
925 network: "test_network".to_string(),
926 network_type: NetworkType::Evm,
927 policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
928 signer_id: "test_signer".to_string(),
929 address: "0x".to_string(),
930 notification_id: None,
931 custom_rpc_urls: None,
932 ..Default::default()
933 }
934 }
935
936 fn make_mock_receipt(status: bool, block_number: Option<u64>) -> TransactionReceipt {
937 let tx_hash = TxHash::from(b256!(
939 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
940 ));
941 let block_hash = BlockHash::from(b256!(
942 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
943 ));
944 let from_address = Address::from([0x11; 20]);
945
946 TransactionReceipt {
947 inner: alloy::rpc::types::TransactionReceipt {
948 inner: AnyReceiptEnvelope {
949 inner: ReceiptWithBloom {
950 receipt: Receipt {
951 status: Eip658Value::Eip658(status), cumulative_gas_used: 0,
953 logs: vec![],
954 },
955 logs_bloom: Bloom::ZERO,
956 },
957 r#type: 0, },
959 transaction_hash: tx_hash,
960 transaction_index: Some(0),
961 block_hash: block_number.map(|_| block_hash), block_number,
963 gas_used: 21000,
964 effective_gas_price: 1000,
965 blob_gas_used: None,
966 blob_gas_price: None,
967 from: from_address,
968 to: None,
969 contract_address: None,
970 },
971 other: Default::default(),
972 }
973 }
974
975 mod check_transaction_status_tests {
977 use super::*;
978
979 #[tokio::test]
980 async fn test_not_mined() {
981 let mut mocks = default_test_mocks();
982 let relayer = create_test_relayer();
983 let mut tx = make_test_transaction(TransactionStatus::Submitted);
984
985 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
987 evm_data.hash = Some("0xFakeHash".to_string());
988 }
989
990 mocks
992 .provider
993 .expect_get_transaction_receipt()
994 .returning(|_| Box::pin(async { Ok(None) }));
995
996 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
997
998 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
999 assert_eq!(status, TransactionStatus::Submitted);
1000 }
1001
1002 #[tokio::test]
1003 async fn test_mined_but_not_confirmed() {
1004 let mut mocks = default_test_mocks();
1005 let relayer = create_test_relayer();
1006 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1007
1008 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1009 evm_data.hash = Some("0xFakeHash".to_string());
1010 }
1011
1012 mocks
1014 .provider
1015 .expect_get_transaction_receipt()
1016 .returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) }));
1017
1018 mocks
1020 .provider
1021 .expect_get_block_number()
1022 .return_once(|| Box::pin(async { Ok(100) }));
1023
1024 mocks
1026 .network_repo
1027 .expect_get_by_chain_id()
1028 .returning(|_, _| Ok(Some(create_test_network_model())));
1029
1030 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1031
1032 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
1033 assert_eq!(status, TransactionStatus::Mined);
1034 }
1035
1036 #[tokio::test]
1037 async fn test_confirmed() {
1038 let mut mocks = default_test_mocks();
1039 let relayer = create_test_relayer();
1040 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1041
1042 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1043 evm_data.hash = Some("0xFakeHash".to_string());
1044 }
1045
1046 mocks
1048 .provider
1049 .expect_get_transaction_receipt()
1050 .returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) }));
1051
1052 mocks
1054 .provider
1055 .expect_get_block_number()
1056 .return_once(|| Box::pin(async { Ok(113) }));
1057
1058 mocks
1060 .network_repo
1061 .expect_get_by_chain_id()
1062 .returning(|_, _| Ok(Some(create_test_network_model())));
1063
1064 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1065
1066 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
1067 assert_eq!(status, TransactionStatus::Confirmed);
1068 }
1069
1070 #[tokio::test]
1071 async fn test_failed() {
1072 let mut mocks = default_test_mocks();
1073 let relayer = create_test_relayer();
1074 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1075
1076 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1077 evm_data.hash = Some("0xFakeHash".to_string());
1078 }
1079
1080 mocks
1082 .provider
1083 .expect_get_transaction_receipt()
1084 .returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(false, Some(100)))) }));
1085
1086 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1087
1088 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
1089 assert_eq!(status, TransactionStatus::Failed);
1090 }
1091 }
1092
1093 mod should_resubmit_tests {
1095 use super::*;
1096 use crate::models::TransactionError;
1097
1098 #[tokio::test]
1099 async fn test_should_resubmit_true() {
1100 let mut mocks = default_test_mocks();
1101 let relayer = create_test_relayer();
1102
1103 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1105 tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
1106
1107 mocks
1109 .network_repo
1110 .expect_get_by_chain_id()
1111 .returning(|_, _| Ok(Some(create_test_network_model())));
1112
1113 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1114 let res = evm_transaction.should_resubmit(&tx).await.unwrap();
1115 assert!(res, "Transaction should be resubmitted after timeout.");
1116 }
1117
1118 #[tokio::test]
1119 async fn test_should_resubmit_false() {
1120 let mut mocks = default_test_mocks();
1121 let relayer = create_test_relayer();
1122
1123 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1125 tx.sent_at = Some(Utc::now().to_rfc3339());
1126
1127 mocks
1129 .network_repo
1130 .expect_get_by_chain_id()
1131 .returning(|_, _| Ok(Some(create_test_network_model())));
1132
1133 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1134 let res = evm_transaction.should_resubmit(&tx).await.unwrap();
1135 assert!(!res, "Transaction should not be resubmitted immediately.");
1136 }
1137
1138 #[tokio::test]
1139 async fn test_should_resubmit_true_for_no_mempool_network() {
1140 let mut mocks = default_test_mocks();
1141 let relayer = create_test_relayer();
1142
1143 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1145 tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
1146
1147 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1149 evm_data.chain_id = 42161; }
1151
1152 mocks
1154 .network_repo
1155 .expect_get_by_chain_id()
1156 .returning(|_, _| Ok(Some(create_test_no_mempool_network_model())));
1157
1158 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1159 let res = evm_transaction.should_resubmit(&tx).await.unwrap();
1160 assert!(
1161 res,
1162 "Transaction should be resubmitted for no-mempool networks."
1163 );
1164 }
1165
1166 #[tokio::test]
1167 async fn test_should_resubmit_network_not_found() {
1168 let mut mocks = default_test_mocks();
1169 let relayer = create_test_relayer();
1170
1171 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1172 tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
1173
1174 mocks
1176 .network_repo
1177 .expect_get_by_chain_id()
1178 .returning(|_, _| Ok(None));
1179
1180 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1181 let result = evm_transaction.should_resubmit(&tx).await;
1182
1183 assert!(
1184 result.is_err(),
1185 "should_resubmit should return error when network not found"
1186 );
1187 let error = result.unwrap_err();
1188 match error {
1189 TransactionError::UnexpectedError(msg) => {
1190 assert!(msg.contains("Network with chain id 1 not found"));
1191 }
1192 _ => panic!("Expected UnexpectedError for network not found"),
1193 }
1194 }
1195
1196 #[tokio::test]
1197 async fn test_should_resubmit_network_conversion_error() {
1198 let mut mocks = default_test_mocks();
1199 let relayer = create_test_relayer();
1200
1201 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1202 tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
1203
1204 let invalid_evm_config = EvmNetworkConfig {
1206 common: NetworkConfigCommon {
1207 network: "invalid-network".to_string(),
1208 from: None,
1209 rpc_urls: Some(vec!["https://rpc.example.com".to_string()]),
1210 explorer_urls: Some(vec!["https://explorer.example.com".to_string()]),
1211 average_blocktime_ms: Some(12000),
1212 is_testnet: Some(false),
1213 tags: Some(vec!["testnet".to_string()]),
1214 },
1215 chain_id: None, required_confirmations: Some(12),
1217 features: Some(vec!["eip1559".to_string()]),
1218 symbol: Some("ETH".to_string()),
1219 gas_price_cache: None,
1220 };
1221 let invalid_network = NetworkRepoModel {
1222 id: "evm:invalid".to_string(),
1223 name: "invalid-network".to_string(),
1224 network_type: NetworkType::Evm,
1225 config: NetworkConfigData::Evm(invalid_evm_config),
1226 };
1227
1228 mocks
1230 .network_repo
1231 .expect_get_by_chain_id()
1232 .returning(move |_, _| Ok(Some(invalid_network.clone())));
1233
1234 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1235 let result = evm_transaction.should_resubmit(&tx).await;
1236
1237 assert!(
1238 result.is_err(),
1239 "should_resubmit should return error when network conversion fails"
1240 );
1241 let error = result.unwrap_err();
1242 match error {
1243 TransactionError::UnexpectedError(msg) => {
1244 assert!(msg.contains("Error converting network model to EvmNetwork"));
1245 }
1246 _ => panic!("Expected UnexpectedError for network conversion failure"),
1247 }
1248 }
1249 }
1250
1251 mod should_noop_tests {
1253 use super::*;
1254
1255 #[tokio::test]
1256 async fn test_expired_transaction_triggers_noop() {
1257 let mut mocks = default_test_mocks();
1258 let relayer = create_test_relayer();
1259
1260 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1261 tx.valid_until = Some((Utc::now() - Duration::seconds(10)).to_rfc3339());
1263
1264 mocks
1266 .network_repo
1267 .expect_get_by_chain_id()
1268 .returning(|_, _| Ok(Some(create_test_network_model())));
1269
1270 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1271 let (res, reason) = evm_transaction.should_noop(&tx).await.unwrap();
1272 assert!(res, "Expired transaction should be replaced with a NOOP.");
1273 assert!(
1274 reason.is_some(),
1275 "Reason should be provided for expired transaction"
1276 );
1277 assert!(
1278 reason.unwrap().contains("expired"),
1279 "Reason should mention expiration"
1280 );
1281 }
1282
1283 #[tokio::test]
1284 async fn test_too_many_noop_attempts_returns_false() {
1285 let mocks = default_test_mocks();
1286 let relayer = create_test_relayer();
1287
1288 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1289 tx.noop_count = Some(51); let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1292 let (res, reason) = evm_transaction.should_noop(&tx).await.unwrap();
1293 assert!(
1294 !res,
1295 "Transaction with too many NOOP attempts should not be replaced."
1296 );
1297 assert!(
1298 reason.is_none(),
1299 "Reason should not be provided when should_noop is false"
1300 );
1301 }
1302
1303 #[tokio::test]
1304 async fn test_already_noop_returns_false() {
1305 let mut mocks = default_test_mocks();
1306 let relayer = create_test_relayer();
1307
1308 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1309 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1311 evm_data.to = None;
1312 evm_data.value = U256::from(0);
1313 }
1314
1315 mocks
1316 .network_repo
1317 .expect_get_by_chain_id()
1318 .returning(|_, _| Ok(Some(create_test_network_model())));
1319
1320 mocks.provider.expect_get_block_by_number().returning(|| {
1322 Box::pin(async {
1323 use alloy::{network::AnyRpcBlock, rpc::types::Block};
1324 let mut block: Block = Block::default();
1325 block.header.gas_limit = 30_000_000u64;
1326 Ok(AnyRpcBlock::from(block))
1327 })
1328 });
1329
1330 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1331 let (res, reason) = evm_transaction.should_noop(&tx).await.unwrap();
1332 assert!(
1333 !res,
1334 "Transaction that is already a NOOP should not be replaced."
1335 );
1336 assert!(
1337 reason.is_none(),
1338 "Reason should not be provided when should_noop is false"
1339 );
1340 }
1341
1342 #[tokio::test]
1343 async fn test_rollup_with_too_many_attempts_triggers_noop() {
1344 let mut mocks = default_test_mocks();
1345 let relayer = create_test_relayer();
1346
1347 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1348 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1350 evm_data.chain_id = 42161; }
1352 tx.hashes = vec!["0xHash1".to_string(); 51];
1354
1355 mocks
1357 .network_repo
1358 .expect_get_by_chain_id()
1359 .returning(|_, _| Ok(Some(create_test_no_mempool_network_model())));
1360
1361 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1362 let (res, reason) = evm_transaction.should_noop(&tx).await.unwrap();
1363 assert!(
1364 res,
1365 "Rollup transaction with too many attempts should be replaced with NOOP."
1366 );
1367 assert!(
1368 reason.is_some(),
1369 "Reason should be provided for rollup transaction"
1370 );
1371 assert!(
1372 reason.unwrap().contains("too many attempts"),
1373 "Reason should mention too many attempts"
1374 );
1375 }
1376
1377 #[tokio::test]
1378 async fn test_pending_state_timeout_triggers_noop() {
1379 let mut mocks = default_test_mocks();
1380 let relayer = create_test_relayer();
1381
1382 let mut tx = make_test_transaction(TransactionStatus::Pending);
1383 tx.created_at = (Utc::now() - Duration::minutes(3)).to_rfc3339();
1385
1386 mocks
1387 .network_repo
1388 .expect_get_by_chain_id()
1389 .returning(|_, _| Ok(Some(create_test_network_model())));
1390
1391 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1392 let (res, reason) = evm_transaction.should_noop(&tx).await.unwrap();
1393 assert!(
1394 res,
1395 "Pending transaction stuck for >2 minutes should be replaced with NOOP."
1396 );
1397 assert!(
1398 reason.is_some(),
1399 "Reason should be provided for pending timeout"
1400 );
1401 assert!(
1402 reason.unwrap().contains("Pending state"),
1403 "Reason should mention Pending state"
1404 );
1405 }
1406
1407 #[tokio::test]
1408 async fn test_valid_transaction_returns_false() {
1409 let mut mocks = default_test_mocks();
1410 let relayer = create_test_relayer();
1411
1412 let tx = make_test_transaction(TransactionStatus::Submitted);
1413 mocks
1416 .network_repo
1417 .expect_get_by_chain_id()
1418 .returning(|_, _| Ok(Some(create_test_network_model())));
1419
1420 mocks.provider.expect_get_block_by_number().returning(|| {
1422 Box::pin(async {
1423 use alloy::{network::AnyRpcBlock, rpc::types::Block};
1424 let mut block: Block = Block::default();
1425 block.header.gas_limit = 30_000_000u64;
1426 Ok(AnyRpcBlock::from(block))
1427 })
1428 });
1429
1430 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1431 let (res, reason) = evm_transaction.should_noop(&tx).await.unwrap();
1432 assert!(!res, "Valid transaction should not be replaced with NOOP.");
1433 assert!(
1434 reason.is_none(),
1435 "Reason should not be provided when should_noop is false"
1436 );
1437 }
1438 }
1439
1440 mod update_transaction_status_tests {
1442 use super::*;
1443
1444 #[tokio::test]
1445 async fn test_no_update_when_status_is_same() {
1446 let mocks = default_test_mocks();
1448 let relayer = create_test_relayer();
1449 let tx = make_test_transaction(TransactionStatus::Submitted);
1450 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1451
1452 let updated_tx = evm_transaction
1455 .update_transaction_status_if_needed(tx.clone(), TransactionStatus::Submitted)
1456 .await
1457 .unwrap();
1458 assert_eq!(updated_tx.status, TransactionStatus::Submitted);
1459 assert_eq!(updated_tx.id, tx.id);
1460 }
1461
1462 #[tokio::test]
1463 async fn test_updates_when_status_differs() {
1464 let mut mocks = default_test_mocks();
1465 let relayer = create_test_relayer();
1466 let tx = make_test_transaction(TransactionStatus::Submitted);
1467
1468 mocks
1470 .tx_repo
1471 .expect_partial_update()
1472 .returning(|_, update| {
1473 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1474 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1475 Ok(updated_tx)
1476 });
1477
1478 mocks
1480 .job_producer
1481 .expect_produce_send_notification_job()
1482 .returning(|_, _| Box::pin(async { Ok(()) }));
1483
1484 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1485 let updated_tx = evm_transaction
1486 .update_transaction_status_if_needed(tx.clone(), TransactionStatus::Mined)
1487 .await
1488 .unwrap();
1489
1490 assert_eq!(updated_tx.status, TransactionStatus::Mined);
1491 }
1492 }
1493
1494 mod handle_sent_state_tests {
1496 use super::*;
1497
1498 #[tokio::test]
1499 async fn test_sent_state_recent_no_resend() {
1500 let mut mocks = default_test_mocks();
1501 let relayer = create_test_relayer();
1502
1503 let mut tx = make_test_transaction(TransactionStatus::Sent);
1504 tx.sent_at = Some((Utc::now() - Duration::seconds(10)).to_rfc3339());
1506
1507 mocks
1509 .network_repo
1510 .expect_get_by_chain_id()
1511 .returning(|_, _| Ok(Some(create_test_network_model())));
1512
1513 mocks.provider.expect_get_block_by_number().returning(|| {
1515 Box::pin(async {
1516 use alloy::{network::AnyRpcBlock, rpc::types::Block};
1517 let mut block: Block = Block::default();
1518 block.header.gas_limit = 30_000_000u64;
1519 Ok(AnyRpcBlock::from(block))
1520 })
1521 });
1522
1523 mocks
1525 .job_producer
1526 .expect_produce_check_transaction_status_job()
1527 .returning(|_, _| Box::pin(async { Ok(()) }));
1528
1529 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1530 let result = evm_transaction.handle_sent_state(tx.clone()).await.unwrap();
1531
1532 assert_eq!(result.status, TransactionStatus::Sent);
1533 }
1534
1535 #[tokio::test]
1536 async fn test_sent_state_stuck_schedules_resubmit() {
1537 let mut mocks = default_test_mocks();
1538 let relayer = create_test_relayer();
1539
1540 let mut tx = make_test_transaction(TransactionStatus::Sent);
1541 tx.sent_at = Some((Utc::now() - Duration::seconds(60)).to_rfc3339());
1543
1544 mocks
1546 .network_repo
1547 .expect_get_by_chain_id()
1548 .returning(|_, _| Ok(Some(create_test_network_model())));
1549
1550 mocks.provider.expect_get_block_by_number().returning(|| {
1552 Box::pin(async {
1553 use alloy::{network::AnyRpcBlock, rpc::types::Block};
1554 let mut block: Block = Block::default();
1555 block.header.gas_limit = 30_000_000u64;
1556 Ok(AnyRpcBlock::from(block))
1557 })
1558 });
1559
1560 mocks
1562 .job_producer
1563 .expect_produce_submit_transaction_job()
1564 .returning(|_, _| Box::pin(async { Ok(()) }));
1565
1566 mocks
1568 .job_producer
1569 .expect_produce_check_transaction_status_job()
1570 .returning(|_, _| Box::pin(async { Ok(()) }));
1571
1572 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1573 let result = evm_transaction.handle_sent_state(tx.clone()).await.unwrap();
1574
1575 assert_eq!(result.status, TransactionStatus::Sent);
1576 }
1577 }
1578
1579 mod prepare_noop_update_request_tests {
1581 use super::*;
1582
1583 #[tokio::test]
1584 async fn test_noop_request_without_cancellation() {
1585 let mocks = default_test_mocks_with_network();
1587 let relayer = create_test_relayer();
1588 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1589 tx.noop_count = Some(2);
1590 tx.is_canceled = Some(false);
1591
1592 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1593 let update_req = evm_transaction
1594 .prepare_noop_update_request(&tx, false, None)
1595 .await
1596 .unwrap();
1597
1598 assert_eq!(update_req.noop_count, Some(3));
1600 assert_eq!(update_req.is_canceled, Some(false));
1602 }
1603
1604 #[tokio::test]
1605 async fn test_noop_request_with_cancellation() {
1606 let mocks = default_test_mocks_with_network();
1608 let relayer = create_test_relayer();
1609 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1610 tx.noop_count = None;
1611 tx.is_canceled = Some(false);
1612
1613 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1614 let update_req = evm_transaction
1615 .prepare_noop_update_request(&tx, true, None)
1616 .await
1617 .unwrap();
1618
1619 assert_eq!(update_req.noop_count, Some(1));
1621 assert_eq!(update_req.is_canceled, Some(true));
1623 }
1624 }
1625
1626 mod handle_submitted_state_tests {
1628 use super::*;
1629
1630 #[tokio::test]
1631 async fn test_schedules_resubmit_job() {
1632 let mut mocks = default_test_mocks();
1633 let relayer = create_test_relayer();
1634
1635 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1637 tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
1638
1639 mocks
1641 .network_repo
1642 .expect_get_by_chain_id()
1643 .returning(|_, _| Ok(Some(create_test_network_model())));
1644
1645 mocks.provider.expect_get_block_by_number().returning(|| {
1647 Box::pin(async {
1648 use alloy::{network::AnyRpcBlock, rpc::types::Block};
1649 let mut block: Block = Block::default();
1650 block.header.gas_limit = 30_000_000u64;
1651 Ok(AnyRpcBlock::from(block))
1652 })
1653 });
1654
1655 mocks
1657 .job_producer
1658 .expect_produce_submit_transaction_job()
1659 .returning(|_, _| Box::pin(async { Ok(()) }));
1660
1661 mocks
1663 .job_producer
1664 .expect_produce_check_transaction_status_job()
1665 .returning(|_, _| Box::pin(async { Ok(()) }));
1666
1667 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1668 let updated_tx = evm_transaction.handle_submitted_state(tx).await.unwrap();
1669
1670 assert_eq!(updated_tx.status, TransactionStatus::Submitted);
1672 }
1673 }
1674
1675 mod handle_pending_state_tests {
1677 use super::*;
1678
1679 #[tokio::test]
1680 async fn test_pending_state_no_noop() {
1681 let mut mocks = default_test_mocks();
1683 let relayer = create_test_relayer();
1684 let mut tx = make_test_transaction(TransactionStatus::Pending);
1685 tx.created_at = Utc::now().to_rfc3339(); mocks
1689 .network_repo
1690 .expect_get_by_chain_id()
1691 .returning(|_, _| Ok(Some(create_test_network_model())));
1692
1693 mocks.provider.expect_get_block_by_number().returning(|| {
1695 Box::pin(async {
1696 use alloy::{network::AnyRpcBlock, rpc::types::Block};
1697 let mut block: Block = Block::default();
1698 block.header.gas_limit = 30_000_000u64;
1699 Ok(AnyRpcBlock::from(block))
1700 })
1701 });
1702
1703 mocks
1705 .job_producer
1706 .expect_produce_check_transaction_status_job()
1707 .returning(|_, _| Box::pin(async { Ok(()) }));
1708
1709 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1710 let result = evm_transaction
1711 .handle_pending_state(tx.clone())
1712 .await
1713 .unwrap();
1714
1715 assert_eq!(result.id, tx.id);
1717 assert_eq!(result.status, tx.status);
1718 assert_eq!(result.noop_count, tx.noop_count);
1719 }
1720
1721 #[tokio::test]
1722 async fn test_pending_state_with_noop() {
1723 let mut mocks = default_test_mocks();
1725 let relayer = create_test_relayer();
1726 let mut tx = make_test_transaction(TransactionStatus::Pending);
1727 tx.created_at = (Utc::now() - Duration::minutes(2)).to_rfc3339();
1728
1729 mocks
1731 .network_repo
1732 .expect_get_by_chain_id()
1733 .returning(|_, _| Ok(Some(create_test_network_model())));
1734
1735 mocks.provider.expect_get_block_by_number().returning(|| {
1737 Box::pin(async {
1738 use alloy::{network::AnyRpcBlock, rpc::types::Block};
1739 let mut block: Block = Block::default();
1740 block.header.gas_limit = 30_000_000u64;
1741 Ok(AnyRpcBlock::from(block))
1742 })
1743 });
1744
1745 let tx_clone = tx.clone();
1748 mocks
1749 .tx_repo
1750 .expect_partial_update()
1751 .withf(move |id, update| {
1752 id == "test-tx-id"
1753 && update.status == Some(TransactionStatus::Failed)
1754 && update.status_reason.is_some()
1755 })
1756 .returning(move |_, update| {
1757 let mut updated_tx = tx_clone.clone();
1758 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1759 updated_tx.status_reason = update.status_reason.clone();
1760 Ok(updated_tx)
1761 });
1762 mocks
1764 .job_producer
1765 .expect_produce_send_notification_job()
1766 .returning(|_, _| Box::pin(async { Ok(()) }));
1767
1768 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1769 let result = evm_transaction
1770 .handle_pending_state(tx.clone())
1771 .await
1772 .unwrap();
1773
1774 assert_eq!(result.status, TransactionStatus::Failed);
1776 assert!(result.status_reason.is_some());
1777 assert!(result.status_reason.unwrap().contains("Pending state"));
1778 }
1779 }
1780
1781 mod handle_mined_state_tests {
1783 use super::*;
1784
1785 #[tokio::test]
1786 async fn test_updates_status_and_schedules_check() {
1787 let mut mocks = default_test_mocks();
1788 let relayer = create_test_relayer();
1789 let tx = make_test_transaction(TransactionStatus::Submitted);
1791
1792 mocks
1794 .job_producer
1795 .expect_produce_check_transaction_status_job()
1796 .returning(|_, _| Box::pin(async { Ok(()) }));
1797 mocks
1799 .tx_repo
1800 .expect_partial_update()
1801 .returning(|_, update| {
1802 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1803 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1804 Ok(updated_tx)
1805 });
1806
1807 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1808 let result = evm_transaction
1809 .handle_mined_state(tx.clone())
1810 .await
1811 .unwrap();
1812 assert_eq!(result.status, TransactionStatus::Mined);
1813 }
1814 }
1815
1816 mod handle_final_state_tests {
1818 use super::*;
1819
1820 #[tokio::test]
1821 async fn test_final_state_confirmed() {
1822 let mut mocks = default_test_mocks();
1823 let relayer = create_test_relayer();
1824 let tx = make_test_transaction(TransactionStatus::Submitted);
1825
1826 mocks
1828 .tx_repo
1829 .expect_partial_update()
1830 .returning(|_, update| {
1831 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1832 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1833 Ok(updated_tx)
1834 });
1835
1836 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1837 let result = evm_transaction
1838 .handle_final_state(tx.clone(), TransactionStatus::Confirmed)
1839 .await
1840 .unwrap();
1841 assert_eq!(result.status, TransactionStatus::Confirmed);
1842 }
1843
1844 #[tokio::test]
1845 async fn test_final_state_failed() {
1846 let mut mocks = default_test_mocks();
1847 let relayer = create_test_relayer();
1848 let tx = make_test_transaction(TransactionStatus::Submitted);
1849
1850 mocks
1852 .tx_repo
1853 .expect_partial_update()
1854 .returning(|_, update| {
1855 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1856 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1857 Ok(updated_tx)
1858 });
1859
1860 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1861 let result = evm_transaction
1862 .handle_final_state(tx.clone(), TransactionStatus::Failed)
1863 .await
1864 .unwrap();
1865 assert_eq!(result.status, TransactionStatus::Failed);
1866 }
1867
1868 #[tokio::test]
1869 async fn test_final_state_expired() {
1870 let mut mocks = default_test_mocks();
1871 let relayer = create_test_relayer();
1872 let tx = make_test_transaction(TransactionStatus::Submitted);
1873
1874 mocks
1876 .tx_repo
1877 .expect_partial_update()
1878 .returning(|_, update| {
1879 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1880 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1881 Ok(updated_tx)
1882 });
1883
1884 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1885 let result = evm_transaction
1886 .handle_final_state(tx.clone(), TransactionStatus::Expired)
1887 .await
1888 .unwrap();
1889 assert_eq!(result.status, TransactionStatus::Expired);
1890 }
1891 }
1892
1893 mod handle_status_impl_tests {
1895 use super::*;
1896
1897 #[tokio::test]
1898 async fn test_impl_submitted_branch() {
1899 let mut mocks = default_test_mocks();
1900 let relayer = create_test_relayer();
1901 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1902 tx.sent_at = Some((Utc::now() - Duration::seconds(120)).to_rfc3339());
1903 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1905 evm_data.hash = Some("0xFakeHash".to_string());
1906 }
1907 mocks
1909 .provider
1910 .expect_get_transaction_receipt()
1911 .returning(|_| Box::pin(async { Ok(None) }));
1912 mocks
1914 .network_repo
1915 .expect_get_by_chain_id()
1916 .returning(|_, _| Ok(Some(create_test_network_model())));
1917 mocks
1919 .job_producer
1920 .expect_produce_check_transaction_status_job()
1921 .returning(|_, _| Box::pin(async { Ok(()) }));
1922 mocks
1924 .tx_repo
1925 .expect_partial_update()
1926 .returning(|_, update| {
1927 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1928 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1929 Ok(updated_tx)
1930 });
1931
1932 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1933 let result = evm_transaction.handle_status_impl(tx).await.unwrap();
1934 assert_eq!(result.status, TransactionStatus::Submitted);
1935 }
1936
1937 #[tokio::test]
1938 async fn test_impl_mined_branch() {
1939 let mut mocks = default_test_mocks();
1940 let relayer = create_test_relayer();
1941 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1942 tx.created_at = (Utc::now() - Duration::minutes(1)).to_rfc3339();
1944 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1946 evm_data.hash = Some("0xFakeHash".to_string());
1947 }
1948 mocks
1950 .provider
1951 .expect_get_transaction_receipt()
1952 .returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) }));
1953 mocks
1955 .provider
1956 .expect_get_block_number()
1957 .return_once(|| Box::pin(async { Ok(100) }));
1958 mocks
1960 .network_repo
1961 .expect_get_by_chain_id()
1962 .returning(|_, _| Ok(Some(create_test_network_model())));
1963 mocks
1965 .job_producer
1966 .expect_produce_send_notification_job()
1967 .returning(|_, _| Box::pin(async { Ok(()) }));
1968 mocks.tx_repo.expect_get_by_id().returning(|_| {
1970 let updated_tx = make_test_transaction(TransactionStatus::Mined);
1971 Ok(updated_tx)
1972 });
1973 mocks
1975 .tx_repo
1976 .expect_partial_update()
1977 .returning(|_, update| {
1978 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1979 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1980 Ok(updated_tx)
1981 });
1982
1983 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1984 let result = evm_transaction.handle_status_impl(tx).await.unwrap();
1985 assert_eq!(result.status, TransactionStatus::Mined);
1986 }
1987
1988 #[tokio::test]
1989 async fn test_impl_final_confirmed_branch() {
1990 let mut mocks = default_test_mocks();
1991 let relayer = create_test_relayer();
1992 let tx = make_test_transaction(TransactionStatus::Confirmed);
1994
1995 mocks
1998 .tx_repo
1999 .expect_partial_update()
2000 .returning(|_, update| {
2001 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2002 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2003 Ok(updated_tx)
2004 });
2005
2006 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2007 let result = evm_transaction.handle_status_impl(tx).await.unwrap();
2008 assert_eq!(result.status, TransactionStatus::Confirmed);
2009 }
2010
2011 #[tokio::test]
2012 async fn test_impl_final_failed_branch() {
2013 let mut mocks = default_test_mocks();
2014 let relayer = create_test_relayer();
2015 let tx = make_test_transaction(TransactionStatus::Failed);
2017
2018 mocks
2019 .tx_repo
2020 .expect_partial_update()
2021 .returning(|_, update| {
2022 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2023 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2024 Ok(updated_tx)
2025 });
2026
2027 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2028 let result = evm_transaction.handle_status_impl(tx).await.unwrap();
2029 assert_eq!(result.status, TransactionStatus::Failed);
2030 }
2031
2032 #[tokio::test]
2033 async fn test_impl_final_expired_branch() {
2034 let mut mocks = default_test_mocks();
2035 let relayer = create_test_relayer();
2036 let tx = make_test_transaction(TransactionStatus::Expired);
2038
2039 mocks
2040 .tx_repo
2041 .expect_partial_update()
2042 .returning(|_, update| {
2043 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2044 updated_tx.status = update.status.unwrap_or(updated_tx.status);
2045 Ok(updated_tx)
2046 });
2047
2048 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2049 let result = evm_transaction.handle_status_impl(tx).await.unwrap();
2050 assert_eq!(result.status, TransactionStatus::Expired);
2051 }
2052 }
2053
2054 mod hash_recovery_tests {
2056 use super::*;
2057
2058 #[tokio::test]
2059 async fn test_should_try_hash_recovery_not_submitted() {
2060 let mocks = default_test_mocks();
2061 let relayer = create_test_relayer();
2062
2063 let mut tx = make_test_transaction(TransactionStatus::Sent);
2064 tx.hashes = vec![
2065 "0xHash1".to_string(),
2066 "0xHash2".to_string(),
2067 "0xHash3".to_string(),
2068 ];
2069
2070 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2071 let result = evm_transaction.should_try_hash_recovery(&tx).unwrap();
2072
2073 assert!(
2074 !result,
2075 "Should not attempt recovery for non-Submitted transactions"
2076 );
2077 }
2078
2079 #[tokio::test]
2080 async fn test_should_try_hash_recovery_not_enough_hashes() {
2081 let mocks = default_test_mocks();
2082 let relayer = create_test_relayer();
2083
2084 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2085 tx.hashes = vec!["0xHash1".to_string()]; tx.sent_at = Some((Utc::now() - Duration::minutes(3)).to_rfc3339());
2087
2088 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2089 let result = evm_transaction.should_try_hash_recovery(&tx).unwrap();
2090
2091 assert!(
2092 !result,
2093 "Should not attempt recovery with insufficient hashes"
2094 );
2095 }
2096
2097 #[tokio::test]
2098 async fn test_should_try_hash_recovery_too_recent() {
2099 let mocks = default_test_mocks();
2100 let relayer = create_test_relayer();
2101
2102 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2103 tx.hashes = vec![
2104 "0xHash1".to_string(),
2105 "0xHash2".to_string(),
2106 "0xHash3".to_string(),
2107 ];
2108 tx.sent_at = Some(Utc::now().to_rfc3339()); let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2111 let result = evm_transaction.should_try_hash_recovery(&tx).unwrap();
2112
2113 assert!(
2114 !result,
2115 "Should not attempt recovery for recently sent transactions"
2116 );
2117 }
2118
2119 #[tokio::test]
2120 async fn test_should_try_hash_recovery_success() {
2121 let mocks = default_test_mocks();
2122 let relayer = create_test_relayer();
2123
2124 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2125 tx.hashes = vec![
2126 "0xHash1".to_string(),
2127 "0xHash2".to_string(),
2128 "0xHash3".to_string(),
2129 ];
2130 tx.sent_at = Some((Utc::now() - Duration::minutes(3)).to_rfc3339());
2131
2132 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2133 let result = evm_transaction.should_try_hash_recovery(&tx).unwrap();
2134
2135 assert!(
2136 result,
2137 "Should attempt recovery for stuck transactions with multiple hashes"
2138 );
2139 }
2140
2141 #[tokio::test]
2142 async fn test_try_recover_no_historical_hash_found() {
2143 let mut mocks = default_test_mocks();
2144 let relayer = create_test_relayer();
2145
2146 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2147 tx.hashes = vec![
2148 "0xHash1".to_string(),
2149 "0xHash2".to_string(),
2150 "0xHash3".to_string(),
2151 ];
2152
2153 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
2154 evm_data.hash = Some("0xHash3".to_string());
2155 }
2156
2157 mocks
2159 .provider
2160 .expect_get_transaction_receipt()
2161 .returning(|_| Box::pin(async { Ok(None) }));
2162
2163 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2164 let evm_data = tx.network_data.get_evm_transaction_data().unwrap();
2165 let result = evm_transaction
2166 .try_recover_with_historical_hashes(&tx, &evm_data)
2167 .await
2168 .unwrap();
2169
2170 assert!(
2171 result.is_none(),
2172 "Should return None when no historical hash is found"
2173 );
2174 }
2175
2176 #[tokio::test]
2177 async fn test_try_recover_finds_mined_historical_hash() {
2178 let mut mocks = default_test_mocks();
2179 let relayer = create_test_relayer();
2180
2181 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2182 tx.hashes = vec![
2183 "0xHash1".to_string(),
2184 "0xHash2".to_string(), "0xHash3".to_string(),
2186 ];
2187
2188 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
2189 evm_data.hash = Some("0xHash3".to_string()); }
2191
2192 mocks
2194 .provider
2195 .expect_get_transaction_receipt()
2196 .returning(|hash| {
2197 if hash == "0xHash2" {
2198 Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) })
2199 } else {
2200 Box::pin(async { Ok(None) })
2201 }
2202 });
2203
2204 let tx_clone = tx.clone();
2206 mocks
2207 .tx_repo
2208 .expect_partial_update()
2209 .returning(move |_, update| {
2210 let mut updated_tx = tx_clone.clone();
2211 if let Some(status) = update.status {
2212 updated_tx.status = status;
2213 }
2214 if let Some(NetworkTransactionData::Evm(ref evm_data)) = update.network_data {
2215 if let NetworkTransactionData::Evm(ref mut updated_evm) =
2216 updated_tx.network_data
2217 {
2218 updated_evm.hash = evm_data.hash.clone();
2219 }
2220 }
2221 Ok(updated_tx)
2222 });
2223
2224 mocks
2226 .job_producer
2227 .expect_produce_send_notification_job()
2228 .returning(|_, _| Box::pin(async { Ok(()) }));
2229
2230 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2231 let evm_data = tx.network_data.get_evm_transaction_data().unwrap();
2232 let result = evm_transaction
2233 .try_recover_with_historical_hashes(&tx, &evm_data)
2234 .await
2235 .unwrap();
2236
2237 assert!(result.is_some(), "Should recover the transaction");
2238 let recovered_tx = result.unwrap();
2239 assert_eq!(recovered_tx.status, TransactionStatus::Mined);
2240 }
2241
2242 #[tokio::test]
2243 async fn test_try_recover_network_error_continues() {
2244 let mut mocks = default_test_mocks();
2245 let relayer = create_test_relayer();
2246
2247 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2248 tx.hashes = vec![
2249 "0xHash1".to_string(),
2250 "0xHash2".to_string(), "0xHash3".to_string(), ];
2253
2254 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
2255 evm_data.hash = Some("0xHash1".to_string());
2256 }
2257
2258 mocks
2260 .provider
2261 .expect_get_transaction_receipt()
2262 .returning(|hash| {
2263 if hash == "0xHash2" {
2264 Box::pin(async { Err(crate::services::provider::ProviderError::Timeout) })
2265 } else if hash == "0xHash3" {
2266 Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) })
2267 } else {
2268 Box::pin(async { Ok(None) })
2269 }
2270 });
2271
2272 let tx_clone = tx.clone();
2274 mocks
2275 .tx_repo
2276 .expect_partial_update()
2277 .returning(move |_, update| {
2278 let mut updated_tx = tx_clone.clone();
2279 if let Some(status) = update.status {
2280 updated_tx.status = status;
2281 }
2282 Ok(updated_tx)
2283 });
2284
2285 mocks
2287 .job_producer
2288 .expect_produce_send_notification_job()
2289 .returning(|_, _| Box::pin(async { Ok(()) }));
2290
2291 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2292 let evm_data = tx.network_data.get_evm_transaction_data().unwrap();
2293 let result = evm_transaction
2294 .try_recover_with_historical_hashes(&tx, &evm_data)
2295 .await
2296 .unwrap();
2297
2298 assert!(
2299 result.is_some(),
2300 "Should continue checking after network error and find mined hash"
2301 );
2302 }
2303
2304 #[tokio::test]
2305 async fn test_update_transaction_with_corrected_hash() {
2306 let mut mocks = default_test_mocks();
2307 let relayer = create_test_relayer();
2308
2309 let mut tx = make_test_transaction(TransactionStatus::Submitted);
2310 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
2311 evm_data.hash = Some("0xWrongHash".to_string());
2312 }
2313
2314 mocks
2316 .tx_repo
2317 .expect_partial_update()
2318 .returning(move |_, update| {
2319 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
2320 if let Some(status) = update.status {
2321 updated_tx.status = status;
2322 }
2323 if let Some(NetworkTransactionData::Evm(ref evm_data)) = update.network_data {
2324 if let NetworkTransactionData::Evm(ref mut updated_evm) =
2325 updated_tx.network_data
2326 {
2327 updated_evm.hash = evm_data.hash.clone();
2328 }
2329 }
2330 Ok(updated_tx)
2331 });
2332
2333 mocks
2335 .job_producer
2336 .expect_produce_send_notification_job()
2337 .returning(|_, _| Box::pin(async { Ok(()) }));
2338
2339 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2340 let evm_data = tx.network_data.get_evm_transaction_data().unwrap();
2341 let result = evm_transaction
2342 .update_transaction_with_corrected_hash(
2343 &tx,
2344 &evm_data,
2345 "0xCorrectHash",
2346 TransactionStatus::Mined,
2347 )
2348 .await
2349 .unwrap();
2350
2351 assert_eq!(result.status, TransactionStatus::Mined);
2352 if let NetworkTransactionData::Evm(ref updated_evm) = result.network_data {
2353 assert_eq!(updated_evm.hash.as_ref().unwrap(), "0xCorrectHash");
2354 }
2355 }
2356 }
2357
2358 mod check_transaction_status_edge_cases {
2360 use super::*;
2361
2362 #[tokio::test]
2363 async fn test_missing_hash_returns_error() {
2364 let mocks = default_test_mocks();
2365 let relayer = create_test_relayer();
2366
2367 let tx = make_test_transaction(TransactionStatus::Submitted);
2368 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2371 let result = evm_transaction.check_transaction_status(&tx).await;
2372
2373 assert!(result.is_err(), "Should return error when hash is missing");
2374 }
2375
2376 #[tokio::test]
2377 async fn test_pending_status_early_return() {
2378 let mocks = default_test_mocks();
2379 let relayer = create_test_relayer();
2380
2381 let tx = make_test_transaction(TransactionStatus::Pending);
2382
2383 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2384 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
2385
2386 assert_eq!(
2387 status,
2388 TransactionStatus::Pending,
2389 "Should return Pending without querying blockchain"
2390 );
2391 }
2392
2393 #[tokio::test]
2394 async fn test_sent_status_early_return() {
2395 let mocks = default_test_mocks();
2396 let relayer = create_test_relayer();
2397
2398 let tx = make_test_transaction(TransactionStatus::Sent);
2399
2400 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2401 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
2402
2403 assert_eq!(
2404 status,
2405 TransactionStatus::Sent,
2406 "Should return Sent without querying blockchain"
2407 );
2408 }
2409
2410 #[tokio::test]
2411 async fn test_final_state_early_return() {
2412 let mocks = default_test_mocks();
2413 let relayer = create_test_relayer();
2414
2415 let tx = make_test_transaction(TransactionStatus::Confirmed);
2416
2417 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
2418 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
2419
2420 assert_eq!(
2421 status,
2422 TransactionStatus::Confirmed,
2423 "Should return final state without querying blockchain"
2424 );
2425 }
2426 }
2427}