1use chrono::{DateTime, Duration, Utc};
6use eyre::Result;
7use log::info;
8
9use super::EvmRelayerTransaction;
10use super::{
11 get_age_of_sent_at, has_enough_confirmations, is_noop, is_transaction_valid, make_noop,
12 too_many_attempts, too_many_noop_attempts,
13};
14use crate::models::{EvmNetwork, NetworkType};
15use crate::repositories::NetworkRepository;
16use crate::{
17 domain::transaction::evm::price_calculator::PriceCalculatorTrait,
18 jobs::JobProducerTrait,
19 models::{
20 NetworkTransactionData, RelayerRepoModel, TransactionError, TransactionRepoModel,
21 TransactionStatus, TransactionUpdateRequest,
22 },
23 repositories::{Repository, TransactionCounterTrait, TransactionRepository},
24 services::{EvmProviderTrait, Signer},
25 utils::{get_resubmit_timeout_for_speed, get_resubmit_timeout_with_backoff},
26};
27
28impl<P, R, N, T, J, S, C, PC> EvmRelayerTransaction<P, R, N, T, J, S, C, PC>
29where
30 P: EvmProviderTrait + Send + Sync,
31 R: Repository<RelayerRepoModel, String> + Send + Sync,
32 N: NetworkRepository + Send + Sync,
33 T: TransactionRepository + Send + Sync,
34 J: JobProducerTrait + Send + Sync,
35 S: Signer + Send + Sync,
36 C: TransactionCounterTrait + Send + Sync,
37 PC: PriceCalculatorTrait + Send + Sync,
38{
39 pub(super) async fn check_transaction_status(
40 &self,
41 tx: &TransactionRepoModel,
42 ) -> Result<TransactionStatus, TransactionError> {
43 if tx.status == TransactionStatus::Expired
44 || tx.status == TransactionStatus::Failed
45 || tx.status == TransactionStatus::Confirmed
46 {
47 return Ok(tx.status.clone());
48 }
49
50 let evm_data = tx.network_data.get_evm_transaction_data()?;
51 let tx_hash = evm_data
52 .hash
53 .as_ref()
54 .ok_or(TransactionError::UnexpectedError(
55 "Transaction hash is missing".to_string(),
56 ))?;
57
58 let receipt_result = self.provider().get_transaction_receipt(tx_hash).await?;
59
60 if let Some(receipt) = receipt_result {
61 if !receipt.status() {
62 return Ok(TransactionStatus::Failed);
63 }
64 let last_block_number = self.provider().get_block_number().await?;
65 let tx_block_number = receipt
66 .block_number
67 .ok_or(TransactionError::UnexpectedError(
68 "Transaction receipt missing block number".to_string(),
69 ))?;
70
71 let network_model = self
72 .network_repository()
73 .get_by_chain_id(NetworkType::Evm, evm_data.chain_id)
74 .await?
75 .ok_or(TransactionError::UnexpectedError(format!(
76 "Network with chain id {} not found",
77 evm_data.chain_id
78 )))?;
79
80 let network = EvmNetwork::try_from(network_model).map_err(|e| {
81 TransactionError::UnexpectedError(format!(
82 "Error converting network model to EvmNetwork: {}",
83 e
84 ))
85 })?;
86
87 if !has_enough_confirmations(
88 tx_block_number,
89 last_block_number,
90 network.required_confirmations,
91 ) {
92 info!("Transaction mined but not confirmed: {}", tx_hash);
93 return Ok(TransactionStatus::Mined);
94 }
95 Ok(TransactionStatus::Confirmed)
96 } else {
97 info!("Transaction not yet mined: {}", tx_hash);
98 Ok(TransactionStatus::Submitted)
99 }
100 }
101
102 pub(super) async fn should_resubmit(
104 &self,
105 tx: &TransactionRepoModel,
106 ) -> Result<bool, TransactionError> {
107 if tx.status != TransactionStatus::Submitted {
108 return Err(TransactionError::UnexpectedError(format!(
109 "Transaction must be in Submitted status to resubmit, found: {:?}",
110 tx.status
111 )));
112 }
113
114 let age = get_age_of_sent_at(tx)?;
115 let timeout = match tx.network_data.get_evm_transaction_data() {
116 Ok(data) => get_resubmit_timeout_for_speed(&data.speed),
117 Err(e) => return Err(e),
118 };
119
120 let timeout_with_backoff = get_resubmit_timeout_with_backoff(timeout, tx.hashes.len());
121 if age > Duration::milliseconds(timeout_with_backoff) {
122 info!("Transaction has been pending for too long, resubmitting");
123 return Ok(true);
124 }
125 Ok(false)
126 }
127
128 pub(super) async fn should_noop(
130 &self,
131 tx: &TransactionRepoModel,
132 ) -> Result<bool, TransactionError> {
133 if too_many_noop_attempts(tx) {
134 info!("Transaction has too many NOOP attempts already");
135 return Ok(false);
136 }
137
138 let evm_data = tx.network_data.get_evm_transaction_data()?;
139 if is_noop(&evm_data) {
140 return Ok(false);
141 }
142
143 let network_model = self
144 .network_repository()
145 .get_by_chain_id(NetworkType::Evm, evm_data.chain_id)
146 .await?
147 .ok_or(TransactionError::UnexpectedError(format!(
148 "Network with chain id {} not found",
149 evm_data.chain_id
150 )))?;
151
152 let network = EvmNetwork::try_from(network_model).map_err(|e| {
153 TransactionError::UnexpectedError(format!(
154 "Error converting network model to EvmNetwork: {}",
155 e
156 ))
157 })?;
158
159 if network.is_rollup() && too_many_attempts(tx) {
160 info!("Rollup transaction has too many attempts, will replace with NOOP");
161 return Ok(true);
162 }
163
164 if !is_transaction_valid(&tx.created_at, &tx.valid_until) {
165 info!("Transaction is expired, will replace with NOOP");
166 return Ok(true);
167 }
168
169 if tx.status == TransactionStatus::Pending {
170 let created_at = &tx.created_at;
171 let created_time = DateTime::parse_from_rfc3339(created_at)
172 .map_err(|_| {
173 TransactionError::UnexpectedError("Error parsing created_at time".to_string())
174 })?
175 .with_timezone(&Utc);
176 let age = Utc::now().signed_duration_since(created_time);
177 if age > Duration::minutes(1) {
178 info!("Transaction in Pending state for over 1 minute, will replace with NOOP");
179 return Ok(true);
180 }
181 }
182 Ok(false)
183 }
184
185 pub(super) async fn update_transaction_status_if_needed(
187 &self,
188 tx: TransactionRepoModel,
189 new_status: TransactionStatus,
190 ) -> Result<TransactionRepoModel, TransactionError> {
191 if tx.status != new_status {
192 return self.update_transaction_status(tx, new_status).await;
193 }
194 Ok(tx)
195 }
196
197 pub(super) async fn prepare_noop_update_request(
199 &self,
200 tx: &TransactionRepoModel,
201 is_cancellation: bool,
202 ) -> Result<TransactionUpdateRequest, TransactionError> {
203 let mut evm_data = tx.network_data.get_evm_transaction_data()?;
204 make_noop(&mut evm_data).await?;
205
206 let noop_count = tx.noop_count.unwrap_or(0) + 1;
207 let update_request = TransactionUpdateRequest {
208 network_data: Some(NetworkTransactionData::Evm(evm_data)),
209 noop_count: Some(noop_count),
210 is_canceled: if is_cancellation {
211 Some(true)
212 } else {
213 tx.is_canceled
214 },
215 ..Default::default()
216 };
217 Ok(update_request)
218 }
219
220 async fn handle_submitted_state(
222 &self,
223 tx: TransactionRepoModel,
224 ) -> Result<TransactionRepoModel, TransactionError> {
225 if self.should_resubmit(&tx).await? {
226 return self.handle_resubmission(tx).await;
227 }
228
229 self.schedule_status_check(&tx, Some(5)).await?;
230 self.update_transaction_status_if_needed(tx, TransactionStatus::Submitted)
231 .await
232 }
233
234 async fn handle_resubmission(
236 &self,
237 tx: TransactionRepoModel,
238 ) -> Result<TransactionRepoModel, TransactionError> {
239 info!("Scheduling resubmit job for transaction: {}", tx.id);
240
241 let tx_to_process = if self.should_noop(&tx).await? {
242 self.process_noop_transaction(&tx).await?
243 } else {
244 tx
245 };
246
247 self.send_transaction_resubmit_job(&tx_to_process).await?;
248 Ok(tx_to_process)
249 }
250
251 async fn process_noop_transaction(
253 &self,
254 tx: &TransactionRepoModel,
255 ) -> Result<TransactionRepoModel, TransactionError> {
256 info!("Preparing transaction NOOP before resubmission: {}", tx.id);
257 let update = self.prepare_noop_update_request(tx, false).await?;
258 let updated_tx = self
259 .transaction_repository()
260 .partial_update(tx.id.clone(), update)
261 .await?;
262
263 self.send_transaction_update_notification(&updated_tx)
264 .await?;
265 Ok(updated_tx)
266 }
267
268 async fn handle_pending_state(
270 &self,
271 tx: TransactionRepoModel,
272 ) -> Result<TransactionRepoModel, TransactionError> {
273 if self.should_noop(&tx).await? {
274 info!("Preparing NOOP for pending transaction: {}", tx.id);
275 let update = self.prepare_noop_update_request(&tx, false).await?;
276 let updated_tx = self
277 .transaction_repository()
278 .partial_update(tx.id.clone(), update)
279 .await?;
280
281 self.send_transaction_submit_job(&updated_tx).await?;
282 self.send_transaction_update_notification(&updated_tx)
283 .await?;
284 return Ok(updated_tx);
285 }
286 Ok(tx)
287 }
288
289 async fn handle_mined_state(
291 &self,
292 tx: TransactionRepoModel,
293 ) -> Result<TransactionRepoModel, TransactionError> {
294 self.schedule_status_check(&tx, Some(5)).await?;
295 self.update_transaction_status_if_needed(tx, TransactionStatus::Mined)
296 .await
297 }
298
299 async fn handle_final_state(
301 &self,
302 tx: TransactionRepoModel,
303 status: TransactionStatus,
304 ) -> Result<TransactionRepoModel, TransactionError> {
305 self.update_transaction_status_if_needed(tx, status).await
306 }
307
308 pub async fn handle_status_impl(
313 &self,
314 tx: TransactionRepoModel,
315 ) -> Result<TransactionRepoModel, TransactionError> {
316 info!("Checking transaction status for tx: {:?}", tx.id);
317
318 let status = self.check_transaction_status(&tx).await?;
319 info!("Transaction status: {:?}", status);
320
321 match status {
322 TransactionStatus::Submitted => self.handle_submitted_state(tx).await,
323 TransactionStatus::Pending => self.handle_pending_state(tx).await,
324 TransactionStatus::Mined => self.handle_mined_state(tx).await,
325 TransactionStatus::Confirmed
326 | TransactionStatus::Failed
327 | TransactionStatus::Expired => self.handle_final_state(tx, status).await,
328 _ => Err(TransactionError::UnexpectedError(format!(
329 "Unexpected transaction status: {:?}",
330 status
331 ))),
332 }
333 }
334}
335
336#[cfg(test)]
337mod tests {
338 use crate::{
339 config::{EvmNetworkConfig, NetworkConfigCommon},
340 domain::transaction::evm::{EvmRelayerTransaction, MockPriceCalculatorTrait},
341 jobs::MockJobProducerTrait,
342 models::{
343 evm::Speed, EvmTransactionData, NetworkConfigData, NetworkRepoModel,
344 NetworkTransactionData, NetworkType, RelayerEvmPolicy, RelayerNetworkPolicy,
345 RelayerRepoModel, TransactionRepoModel, TransactionStatus, U256,
346 },
347 repositories::{
348 MockNetworkRepository, MockRepository, MockTransactionCounterTrait,
349 MockTransactionRepository,
350 },
351 services::{MockEvmProviderTrait, MockSigner},
352 };
353 use alloy::{
354 consensus::{Eip658Value, Receipt, ReceiptEnvelope, ReceiptWithBloom},
355 primitives::{b256, Address, BlockHash, Bloom, TxHash},
356 rpc::types::TransactionReceipt,
357 };
358 use chrono::{Duration, Utc};
359 use std::sync::Arc;
360
361 pub struct TestMocks {
363 pub provider: MockEvmProviderTrait,
364 pub relayer_repo: MockRepository<RelayerRepoModel, String>,
365 pub network_repo: MockNetworkRepository,
366 pub tx_repo: MockTransactionRepository,
367 pub job_producer: MockJobProducerTrait,
368 pub signer: MockSigner,
369 pub counter: MockTransactionCounterTrait,
370 pub price_calc: MockPriceCalculatorTrait,
371 }
372
373 pub fn default_test_mocks() -> TestMocks {
376 TestMocks {
377 provider: MockEvmProviderTrait::new(),
378 relayer_repo: MockRepository::new(),
379 network_repo: MockNetworkRepository::new(),
380 tx_repo: MockTransactionRepository::new(),
381 job_producer: MockJobProducerTrait::new(),
382 signer: MockSigner::new(),
383 counter: MockTransactionCounterTrait::new(),
384 price_calc: MockPriceCalculatorTrait::new(),
385 }
386 }
387
388 pub fn create_test_network_model() -> NetworkRepoModel {
390 let evm_config = EvmNetworkConfig {
391 common: NetworkConfigCommon {
392 network: "mainnet".to_string(),
393 from: None,
394 rpc_urls: Some(vec!["https://rpc.example.com".to_string()]),
395 explorer_urls: Some(vec!["https://explorer.example.com".to_string()]),
396 average_blocktime_ms: Some(12000),
397 is_testnet: Some(false),
398 tags: Some(vec!["mainnet".to_string()]),
399 },
400 chain_id: Some(1),
401 required_confirmations: Some(12),
402 features: Some(vec!["eip1559".to_string()]),
403 symbol: Some("ETH".to_string()),
404 };
405 NetworkRepoModel {
406 id: "evm:mainnet".to_string(),
407 name: "mainnet".to_string(),
408 network_type: NetworkType::Evm,
409 config: NetworkConfigData::Evm(evm_config),
410 }
411 }
412
413 pub fn make_test_transaction(status: TransactionStatus) -> TransactionRepoModel {
417 TransactionRepoModel {
418 id: "test-tx-id".to_string(),
419 relayer_id: "test-relayer-id".to_string(),
420 status,
421 status_reason: None,
422 created_at: Utc::now().to_rfc3339(),
423 sent_at: None,
424 confirmed_at: None,
425 valid_until: None,
426 network_type: NetworkType::Evm,
427 network_data: NetworkTransactionData::Evm(EvmTransactionData {
428 chain_id: 1,
429 from: "0xSender".to_string(),
430 to: Some("0xRecipient".to_string()),
431 value: U256::from(0),
432 data: Some("0xData".to_string()),
433 gas_limit: 21000,
434 gas_price: Some(20000000000),
435 max_fee_per_gas: None,
436 max_priority_fee_per_gas: None,
437 nonce: None,
438 signature: None,
439 hash: None,
440 speed: Some(Speed::Fast),
441 raw: None,
442 }),
443 priced_at: None,
444 hashes: Vec::new(),
445 noop_count: None,
446 is_canceled: Some(false),
447 }
448 }
449
450 pub fn make_test_evm_relayer_transaction(
453 relayer: RelayerRepoModel,
454 mocks: TestMocks,
455 ) -> EvmRelayerTransaction<
456 MockEvmProviderTrait,
457 MockRepository<RelayerRepoModel, String>,
458 MockNetworkRepository,
459 MockTransactionRepository,
460 MockJobProducerTrait,
461 MockSigner,
462 MockTransactionCounterTrait,
463 MockPriceCalculatorTrait,
464 > {
465 EvmRelayerTransaction::new(
466 relayer,
467 mocks.provider,
468 Arc::new(mocks.relayer_repo),
469 Arc::new(mocks.network_repo),
470 Arc::new(mocks.tx_repo),
471 Arc::new(mocks.counter),
472 Arc::new(mocks.job_producer),
473 mocks.price_calc,
474 mocks.signer,
475 )
476 .unwrap()
477 }
478
479 fn create_test_relayer() -> RelayerRepoModel {
480 RelayerRepoModel {
481 id: "test-relayer-id".to_string(),
482 name: "Test Relayer".to_string(),
483 paused: false,
484 system_disabled: false,
485 network: "test_network".to_string(),
486 network_type: NetworkType::Evm,
487 policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
488 signer_id: "test_signer".to_string(),
489 address: "0x".to_string(),
490 notification_id: None,
491 custom_rpc_urls: None,
492 }
493 }
494
495 fn make_mock_receipt(status: bool, block_number: Option<u64>) -> TransactionReceipt {
496 let tx_hash = TxHash::from(b256!(
498 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
499 ));
500 let block_hash = BlockHash::from(b256!(
501 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
502 ));
503 let from_address = Address::from([0x11; 20]);
504
505 TransactionReceipt {
506 inner: ReceiptEnvelope::Legacy(ReceiptWithBloom {
508 receipt: Receipt {
509 status: Eip658Value::Eip658(status), cumulative_gas_used: 0,
511 logs: vec![],
512 },
513 logs_bloom: Bloom::ZERO,
514 }),
515 transaction_hash: tx_hash,
516 transaction_index: Some(0),
517 block_hash: block_number.map(|_| block_hash), block_number,
519 gas_used: 21000,
520 effective_gas_price: 1000,
521 blob_gas_used: None,
522 blob_gas_price: None,
523 from: from_address,
524 to: None,
525 contract_address: None,
526 authorization_list: None,
527 }
528 }
529
530 mod check_transaction_status_tests {
532 use super::*;
533
534 #[tokio::test]
535 async fn test_not_mined() {
536 let mut mocks = default_test_mocks();
537 let relayer = create_test_relayer();
538 let mut tx = make_test_transaction(TransactionStatus::Submitted);
539
540 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
542 evm_data.hash = Some("0xFakeHash".to_string());
543 }
544
545 mocks
547 .provider
548 .expect_get_transaction_receipt()
549 .returning(|_| Box::pin(async { Ok(None) }));
550
551 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
552
553 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
554 assert_eq!(status, TransactionStatus::Submitted);
555 }
556
557 #[tokio::test]
558 async fn test_mined_but_not_confirmed() {
559 let mut mocks = default_test_mocks();
560 let relayer = create_test_relayer();
561 let mut tx = make_test_transaction(TransactionStatus::Submitted);
562
563 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
564 evm_data.hash = Some("0xFakeHash".to_string());
565 }
566
567 mocks
569 .provider
570 .expect_get_transaction_receipt()
571 .returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) }));
572
573 mocks
575 .provider
576 .expect_get_block_number()
577 .return_once(|| Box::pin(async { Ok(100) }));
578
579 mocks
581 .network_repo
582 .expect_get_by_chain_id()
583 .returning(|_, _| Ok(Some(create_test_network_model())));
584
585 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
586
587 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
588 assert_eq!(status, TransactionStatus::Mined);
589 }
590
591 #[tokio::test]
592 async fn test_confirmed() {
593 let mut mocks = default_test_mocks();
594 let relayer = create_test_relayer();
595 let mut tx = make_test_transaction(TransactionStatus::Submitted);
596
597 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
598 evm_data.hash = Some("0xFakeHash".to_string());
599 }
600
601 mocks
603 .provider
604 .expect_get_transaction_receipt()
605 .returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) }));
606
607 mocks
609 .provider
610 .expect_get_block_number()
611 .return_once(|| Box::pin(async { Ok(113) }));
612
613 mocks
615 .network_repo
616 .expect_get_by_chain_id()
617 .returning(|_, _| Ok(Some(create_test_network_model())));
618
619 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
620
621 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
622 assert_eq!(status, TransactionStatus::Confirmed);
623 }
624
625 #[tokio::test]
626 async fn test_failed() {
627 let mut mocks = default_test_mocks();
628 let relayer = create_test_relayer();
629 let mut tx = make_test_transaction(TransactionStatus::Submitted);
630
631 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
632 evm_data.hash = Some("0xFakeHash".to_string());
633 }
634
635 mocks
637 .provider
638 .expect_get_transaction_receipt()
639 .returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(false, Some(100)))) }));
640
641 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
642
643 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
644 assert_eq!(status, TransactionStatus::Failed);
645 }
646 }
647
648 mod should_resubmit_tests {
650 use super::*;
651
652 #[tokio::test]
653 async fn test_should_resubmit_true() {
654 let mocks = default_test_mocks();
655 let relayer = create_test_relayer();
656
657 let mut tx = make_test_transaction(TransactionStatus::Submitted);
659 tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
660
661 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
662 let res = evm_transaction.should_resubmit(&tx).await.unwrap();
663 assert!(res, "Transaction should be resubmitted after timeout.");
664 }
665
666 #[tokio::test]
667 async fn test_should_resubmit_false() {
668 let mocks = default_test_mocks();
669 let relayer = create_test_relayer();
670
671 let mut tx = make_test_transaction(TransactionStatus::Submitted);
673 tx.sent_at = Some(Utc::now().to_rfc3339());
674
675 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
676 let res = evm_transaction.should_resubmit(&tx).await.unwrap();
677 assert!(!res, "Transaction should not be resubmitted immediately.");
678 }
679 }
680
681 mod should_noop_tests {
683 use super::*;
684
685 #[tokio::test]
686 async fn test_expired_transaction_triggers_noop() {
687 let mut mocks = default_test_mocks();
688 let relayer = create_test_relayer();
689
690 let mut tx = make_test_transaction(TransactionStatus::Submitted);
691 tx.valid_until = Some((Utc::now() - Duration::seconds(10)).to_rfc3339());
693
694 mocks
696 .network_repo
697 .expect_get_by_chain_id()
698 .returning(|_, _| Ok(Some(create_test_network_model())));
699
700 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
701 let res = evm_transaction.should_noop(&tx).await.unwrap();
702 assert!(res, "Expired transaction should be replaced with a NOOP.");
703 }
704 }
705
706 mod update_transaction_status_tests {
708 use super::*;
709
710 #[tokio::test]
711 async fn test_no_update_when_status_is_same() {
712 let mocks = default_test_mocks();
714 let relayer = create_test_relayer();
715 let tx = make_test_transaction(TransactionStatus::Submitted);
716 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
717
718 let updated_tx = evm_transaction
721 .update_transaction_status_if_needed(tx.clone(), TransactionStatus::Submitted)
722 .await
723 .unwrap();
724 assert_eq!(updated_tx.status, TransactionStatus::Submitted);
725 assert_eq!(updated_tx.id, tx.id);
726 }
727 }
728
729 mod prepare_noop_update_request_tests {
731 use super::*;
732
733 #[tokio::test]
734 async fn test_noop_request_without_cancellation() {
735 let mocks = default_test_mocks();
737 let relayer = create_test_relayer();
738 let mut tx = make_test_transaction(TransactionStatus::Submitted);
739 tx.noop_count = Some(2);
740 tx.is_canceled = Some(false);
741
742 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
743 let update_req = evm_transaction
744 .prepare_noop_update_request(&tx, false)
745 .await
746 .unwrap();
747
748 assert_eq!(update_req.noop_count, Some(3));
750 assert_eq!(update_req.is_canceled, Some(false));
752 }
753
754 #[tokio::test]
755 async fn test_noop_request_with_cancellation() {
756 let mocks = default_test_mocks();
758 let relayer = create_test_relayer();
759 let mut tx = make_test_transaction(TransactionStatus::Submitted);
760 tx.noop_count = None;
761 tx.is_canceled = Some(false);
762
763 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
764 let update_req = evm_transaction
765 .prepare_noop_update_request(&tx, true)
766 .await
767 .unwrap();
768
769 assert_eq!(update_req.noop_count, Some(1));
771 assert_eq!(update_req.is_canceled, Some(true));
773 }
774 }
775
776 mod handle_submitted_state_tests {
778 use super::*;
779
780 #[tokio::test]
781 async fn test_schedules_resubmit_job() {
782 let mut mocks = default_test_mocks();
783 let relayer = create_test_relayer();
784
785 let mut tx = make_test_transaction(TransactionStatus::Submitted);
787 tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
788
789 mocks
791 .network_repo
792 .expect_get_by_chain_id()
793 .returning(|_, _| Ok(Some(create_test_network_model())));
794
795 mocks
797 .job_producer
798 .expect_produce_submit_transaction_job()
799 .returning(|_, _| Box::pin(async { Ok(()) }));
800
801 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
802 let updated_tx = evm_transaction.handle_submitted_state(tx).await.unwrap();
803
804 assert_eq!(updated_tx.status, TransactionStatus::Submitted);
806 }
807 }
808
809 mod handle_pending_state_tests {
811 use super::*;
812
813 #[tokio::test]
814 async fn test_pending_state_no_noop() {
815 let mut mocks = default_test_mocks();
817 let relayer = create_test_relayer();
818 let mut tx = make_test_transaction(TransactionStatus::Pending);
819 tx.created_at = Utc::now().to_rfc3339(); mocks
823 .network_repo
824 .expect_get_by_chain_id()
825 .returning(|_, _| Ok(Some(create_test_network_model())));
826
827 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
828 let result = evm_transaction
829 .handle_pending_state(tx.clone())
830 .await
831 .unwrap();
832
833 assert_eq!(result.id, tx.id);
835 assert_eq!(result.status, tx.status);
836 assert_eq!(result.noop_count, tx.noop_count);
837 }
838
839 #[tokio::test]
840 async fn test_pending_state_with_noop() {
841 let mut mocks = default_test_mocks();
843 let relayer = create_test_relayer();
844 let mut tx = make_test_transaction(TransactionStatus::Pending);
845 tx.created_at = (Utc::now() - Duration::minutes(2)).to_rfc3339();
846
847 mocks
849 .network_repo
850 .expect_get_by_chain_id()
851 .returning(|_, _| Ok(Some(create_test_network_model())));
852
853 let tx_clone = tx.clone();
855 mocks
856 .tx_repo
857 .expect_partial_update()
858 .returning(move |_, update| {
859 let mut updated_tx = tx_clone.clone();
860 updated_tx.noop_count = update.noop_count;
861 Ok(updated_tx)
862 });
863 mocks
865 .job_producer
866 .expect_produce_submit_transaction_job()
867 .returning(|_, _| Box::pin(async { Ok(()) }));
868 mocks
869 .job_producer
870 .expect_produce_send_notification_job()
871 .returning(|_, _| Box::pin(async { Ok(()) }));
872
873 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
874 let result = evm_transaction
875 .handle_pending_state(tx.clone())
876 .await
877 .unwrap();
878
879 assert!(result.noop_count.unwrap_or(0) > 0);
881 }
882 }
883
884 mod handle_mined_state_tests {
886 use super::*;
887
888 #[tokio::test]
889 async fn test_updates_status_and_schedules_check() {
890 let mut mocks = default_test_mocks();
891 let relayer = create_test_relayer();
892 let tx = make_test_transaction(TransactionStatus::Submitted);
894
895 mocks
897 .job_producer
898 .expect_produce_check_transaction_status_job()
899 .returning(|_, _| Box::pin(async { Ok(()) }));
900 mocks
902 .tx_repo
903 .expect_partial_update()
904 .returning(|_, update| {
905 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
906 updated_tx.status = update.status.unwrap_or(updated_tx.status);
907 Ok(updated_tx)
908 });
909
910 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
911 let result = evm_transaction
912 .handle_mined_state(tx.clone())
913 .await
914 .unwrap();
915 assert_eq!(result.status, TransactionStatus::Mined);
916 }
917 }
918
919 mod handle_final_state_tests {
921 use super::*;
922
923 #[tokio::test]
924 async fn test_final_state_confirmed() {
925 let mut mocks = default_test_mocks();
926 let relayer = create_test_relayer();
927 let tx = make_test_transaction(TransactionStatus::Submitted);
928
929 mocks
931 .tx_repo
932 .expect_partial_update()
933 .returning(|_, update| {
934 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
935 updated_tx.status = update.status.unwrap_or(updated_tx.status);
936 Ok(updated_tx)
937 });
938
939 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
940 let result = evm_transaction
941 .handle_final_state(tx.clone(), TransactionStatus::Confirmed)
942 .await
943 .unwrap();
944 assert_eq!(result.status, TransactionStatus::Confirmed);
945 }
946
947 #[tokio::test]
948 async fn test_final_state_failed() {
949 let mut mocks = default_test_mocks();
950 let relayer = create_test_relayer();
951 let tx = make_test_transaction(TransactionStatus::Submitted);
952
953 mocks
955 .tx_repo
956 .expect_partial_update()
957 .returning(|_, update| {
958 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
959 updated_tx.status = update.status.unwrap_or(updated_tx.status);
960 Ok(updated_tx)
961 });
962
963 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
964 let result = evm_transaction
965 .handle_final_state(tx.clone(), TransactionStatus::Failed)
966 .await
967 .unwrap();
968 assert_eq!(result.status, TransactionStatus::Failed);
969 }
970
971 #[tokio::test]
972 async fn test_final_state_expired() {
973 let mut mocks = default_test_mocks();
974 let relayer = create_test_relayer();
975 let tx = make_test_transaction(TransactionStatus::Submitted);
976
977 mocks
979 .tx_repo
980 .expect_partial_update()
981 .returning(|_, update| {
982 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
983 updated_tx.status = update.status.unwrap_or(updated_tx.status);
984 Ok(updated_tx)
985 });
986
987 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
988 let result = evm_transaction
989 .handle_final_state(tx.clone(), TransactionStatus::Expired)
990 .await
991 .unwrap();
992 assert_eq!(result.status, TransactionStatus::Expired);
993 }
994 }
995
996 mod handle_status_impl_tests {
998 use super::*;
999
1000 #[tokio::test]
1001 async fn test_impl_submitted_branch() {
1002 let mut mocks = default_test_mocks();
1003 let relayer = create_test_relayer();
1004 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1005 tx.sent_at = Some((Utc::now() - Duration::seconds(120)).to_rfc3339());
1006 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1008 evm_data.hash = Some("0xFakeHash".to_string());
1009 }
1010 mocks
1012 .provider
1013 .expect_get_transaction_receipt()
1014 .returning(|_| Box::pin(async { Ok(None) }));
1015 mocks
1017 .job_producer
1018 .expect_produce_check_transaction_status_job()
1019 .returning(|_, _| Box::pin(async { Ok(()) }));
1020 mocks
1022 .tx_repo
1023 .expect_partial_update()
1024 .returning(|_, update| {
1025 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1026 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1027 Ok(updated_tx)
1028 });
1029
1030 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1031 let result = evm_transaction.handle_status_impl(tx).await.unwrap();
1032 assert_eq!(result.status, TransactionStatus::Submitted);
1033 }
1034
1035 #[tokio::test]
1036 async fn test_impl_mined_branch() {
1037 let mut mocks = default_test_mocks();
1038 let relayer = create_test_relayer();
1039 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1040 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1042 evm_data.hash = Some("0xFakeHash".to_string());
1043 }
1044 mocks
1046 .provider
1047 .expect_get_transaction_receipt()
1048 .returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) }));
1049 mocks
1051 .provider
1052 .expect_get_block_number()
1053 .return_once(|| Box::pin(async { Ok(100) }));
1054 mocks
1056 .network_repo
1057 .expect_get_by_chain_id()
1058 .returning(|_, _| Ok(Some(create_test_network_model())));
1059 mocks
1061 .job_producer
1062 .expect_produce_check_transaction_status_job()
1063 .returning(|_, _| Box::pin(async { Ok(()) }));
1064 mocks
1066 .tx_repo
1067 .expect_partial_update()
1068 .returning(|_, update| {
1069 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1070 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1071 Ok(updated_tx)
1072 });
1073
1074 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1075 let result = evm_transaction.handle_status_impl(tx).await.unwrap();
1076 assert_eq!(result.status, TransactionStatus::Mined);
1077 }
1078
1079 #[tokio::test]
1080 async fn test_impl_final_confirmed_branch() {
1081 let mut mocks = default_test_mocks();
1082 let relayer = create_test_relayer();
1083 let tx = make_test_transaction(TransactionStatus::Confirmed);
1085
1086 mocks
1089 .tx_repo
1090 .expect_partial_update()
1091 .returning(|_, update| {
1092 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1093 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1094 Ok(updated_tx)
1095 });
1096
1097 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1098 let result = evm_transaction.handle_status_impl(tx).await.unwrap();
1099 assert_eq!(result.status, TransactionStatus::Confirmed);
1100 }
1101
1102 #[tokio::test]
1103 async fn test_impl_final_failed_branch() {
1104 let mut mocks = default_test_mocks();
1105 let relayer = create_test_relayer();
1106 let tx = make_test_transaction(TransactionStatus::Failed);
1108
1109 mocks
1110 .tx_repo
1111 .expect_partial_update()
1112 .returning(|_, update| {
1113 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1114 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1115 Ok(updated_tx)
1116 });
1117
1118 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1119 let result = evm_transaction.handle_status_impl(tx).await.unwrap();
1120 assert_eq!(result.status, TransactionStatus::Failed);
1121 }
1122
1123 #[tokio::test]
1124 async fn test_impl_final_expired_branch() {
1125 let mut mocks = default_test_mocks();
1126 let relayer = create_test_relayer();
1127 let tx = make_test_transaction(TransactionStatus::Expired);
1129
1130 mocks
1131 .tx_repo
1132 .expect_partial_update()
1133 .returning(|_, update| {
1134 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1135 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1136 Ok(updated_tx)
1137 });
1138
1139 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1140 let result = evm_transaction.handle_status_impl(tx).await.unwrap();
1141 assert_eq!(result.status, TransactionStatus::Expired);
1142 }
1143 }
1144}