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::constants::ARBITRUM_TIME_TO_RESUBMIT;
15use crate::models::{EvmNetwork, NetworkRepoModel, NetworkType};
16use crate::repositories::{NetworkRepository, RelayerRepository};
17use crate::{
18 domain::transaction::evm::price_calculator::PriceCalculatorTrait,
19 jobs::JobProducerTrait,
20 models::{
21 NetworkTransactionData, RelayerRepoModel, TransactionError, TransactionRepoModel,
22 TransactionStatus, TransactionUpdateRequest,
23 },
24 repositories::{Repository, TransactionCounterTrait, TransactionRepository},
25 services::{EvmProviderTrait, Signer},
26 utils::{get_resubmit_timeout_for_speed, get_resubmit_timeout_with_backoff},
27};
28
29impl<P, RR, NR, TR, J, S, TCR, PC> EvmRelayerTransaction<P, RR, NR, TR, J, S, TCR, PC>
30where
31 P: EvmProviderTrait + Send + Sync,
32 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
33 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
34 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
35 J: JobProducerTrait + Send + Sync + 'static,
36 S: Signer + Send + Sync + 'static,
37 TCR: TransactionCounterTrait + Send + Sync + 'static,
38 PC: PriceCalculatorTrait + Send + Sync,
39{
40 pub(super) async fn check_transaction_status(
41 &self,
42 tx: &TransactionRepoModel,
43 ) -> Result<TransactionStatus, TransactionError> {
44 if tx.status == TransactionStatus::Expired
45 || tx.status == TransactionStatus::Failed
46 || tx.status == TransactionStatus::Confirmed
47 {
48 return Ok(tx.status.clone());
49 }
50
51 let evm_data = tx.network_data.get_evm_transaction_data()?;
52 let tx_hash = evm_data
53 .hash
54 .as_ref()
55 .ok_or(TransactionError::UnexpectedError(
56 "Transaction hash is missing".to_string(),
57 ))?;
58
59 let receipt_result = self.provider().get_transaction_receipt(tx_hash).await?;
60
61 if let Some(receipt) = receipt_result {
62 if !receipt.status() {
63 return Ok(TransactionStatus::Failed);
64 }
65 let last_block_number = self.provider().get_block_number().await?;
66 let tx_block_number = receipt
67 .block_number
68 .ok_or(TransactionError::UnexpectedError(
69 "Transaction receipt missing block number".to_string(),
70 ))?;
71
72 let network_model = self
73 .network_repository()
74 .get_by_chain_id(NetworkType::Evm, evm_data.chain_id)
75 .await?
76 .ok_or(TransactionError::UnexpectedError(format!(
77 "Network with chain id {} not found",
78 evm_data.chain_id
79 )))?;
80
81 let network = EvmNetwork::try_from(network_model).map_err(|e| {
82 TransactionError::UnexpectedError(format!(
83 "Error converting network model to EvmNetwork: {}",
84 e
85 ))
86 })?;
87
88 if !has_enough_confirmations(
89 tx_block_number,
90 last_block_number,
91 network.required_confirmations,
92 ) {
93 info!("Transaction mined but not confirmed: {}", tx_hash);
94 return Ok(TransactionStatus::Mined);
95 }
96 Ok(TransactionStatus::Confirmed)
97 } else {
98 info!("Transaction not yet mined: {}", tx_hash);
99 Ok(TransactionStatus::Submitted)
100 }
101 }
102
103 pub(super) async fn should_resubmit(
105 &self,
106 tx: &TransactionRepoModel,
107 ) -> Result<bool, TransactionError> {
108 if tx.status != TransactionStatus::Submitted {
109 return Err(TransactionError::UnexpectedError(format!(
110 "Transaction must be in Submitted status to resubmit, found: {:?}",
111 tx.status
112 )));
113 }
114
115 let evm_data = tx.network_data.get_evm_transaction_data()?;
116 let age = get_age_of_sent_at(tx)?;
117
118 let network_model = self
120 .network_repository()
121 .get_by_chain_id(NetworkType::Evm, evm_data.chain_id)
122 .await?
123 .ok_or(TransactionError::UnexpectedError(format!(
124 "Network with chain id {} not found",
125 evm_data.chain_id
126 )))?;
127
128 let network = EvmNetwork::try_from(network_model).map_err(|e| {
129 TransactionError::UnexpectedError(format!(
130 "Error converting network model to EvmNetwork: {}",
131 e
132 ))
133 })?;
134
135 let timeout = match network.is_arbitrum() {
136 true => ARBITRUM_TIME_TO_RESUBMIT,
137 false => get_resubmit_timeout_for_speed(&evm_data.speed),
138 };
139
140 let timeout_with_backoff = match network.is_arbitrum() {
141 true => timeout, false => get_resubmit_timeout_with_backoff(timeout, tx.hashes.len()),
143 };
144
145 if age > Duration::milliseconds(timeout_with_backoff) {
146 info!("Transaction has been pending for too long, resubmitting");
147 return Ok(true);
148 }
149 Ok(false)
150 }
151
152 pub(super) async fn should_noop(
154 &self,
155 tx: &TransactionRepoModel,
156 ) -> Result<bool, TransactionError> {
157 if too_many_noop_attempts(tx) {
158 info!("Transaction has too many NOOP attempts already");
159 return Ok(false);
160 }
161
162 let evm_data = tx.network_data.get_evm_transaction_data()?;
163 if is_noop(&evm_data) {
164 return Ok(false);
165 }
166
167 let network_model = self
168 .network_repository()
169 .get_by_chain_id(NetworkType::Evm, evm_data.chain_id)
170 .await?
171 .ok_or(TransactionError::UnexpectedError(format!(
172 "Network with chain id {} not found",
173 evm_data.chain_id
174 )))?;
175
176 let network = EvmNetwork::try_from(network_model).map_err(|e| {
177 TransactionError::UnexpectedError(format!(
178 "Error converting network model to EvmNetwork: {}",
179 e
180 ))
181 })?;
182
183 if network.is_rollup() && too_many_attempts(tx) {
184 info!("Rollup transaction has too many attempts, will replace with NOOP");
185 return Ok(true);
186 }
187
188 if !is_transaction_valid(&tx.created_at, &tx.valid_until) {
189 info!("Transaction is expired, will replace with NOOP");
190 return Ok(true);
191 }
192
193 if tx.status == TransactionStatus::Pending {
194 let created_at = &tx.created_at;
195 let created_time = DateTime::parse_from_rfc3339(created_at)
196 .map_err(|_| {
197 TransactionError::UnexpectedError("Error parsing created_at time".to_string())
198 })?
199 .with_timezone(&Utc);
200 let age = Utc::now().signed_duration_since(created_time);
201 if age > Duration::minutes(1) {
202 info!("Transaction in Pending state for over 1 minute, will replace with NOOP");
203 return Ok(true);
204 }
205 }
206 Ok(false)
207 }
208
209 pub(super) async fn update_transaction_status_if_needed(
211 &self,
212 tx: TransactionRepoModel,
213 new_status: TransactionStatus,
214 ) -> Result<TransactionRepoModel, TransactionError> {
215 if tx.status != new_status {
216 return self.update_transaction_status(tx, new_status).await;
217 }
218 Ok(tx)
219 }
220
221 pub(super) async fn prepare_noop_update_request(
223 &self,
224 tx: &TransactionRepoModel,
225 is_cancellation: bool,
226 ) -> Result<TransactionUpdateRequest, TransactionError> {
227 let mut evm_data = tx.network_data.get_evm_transaction_data()?;
228 let network_model = self
229 .network_repository()
230 .get_by_chain_id(NetworkType::Evm, evm_data.chain_id)
231 .await?
232 .ok_or(TransactionError::UnexpectedError(format!(
233 "Network with chain id {} not found",
234 evm_data.chain_id
235 )))?;
236
237 let network = EvmNetwork::try_from(network_model).map_err(|e| {
238 TransactionError::UnexpectedError(format!(
239 "Error converting network model to EvmNetwork: {}",
240 e
241 ))
242 })?;
243
244 make_noop(&mut evm_data, &network, Some(self.provider())).await?;
245
246 let noop_count = tx.noop_count.unwrap_or(0) + 1;
247 let update_request = TransactionUpdateRequest {
248 network_data: Some(NetworkTransactionData::Evm(evm_data)),
249 noop_count: Some(noop_count),
250 is_canceled: if is_cancellation {
251 Some(true)
252 } else {
253 tx.is_canceled
254 },
255 ..Default::default()
256 };
257 Ok(update_request)
258 }
259
260 async fn handle_submitted_state(
262 &self,
263 tx: TransactionRepoModel,
264 ) -> Result<TransactionRepoModel, TransactionError> {
265 if self.should_resubmit(&tx).await? {
266 let resubmitted_tx = self.handle_resubmission(tx).await?;
267 self.schedule_status_check(&resubmitted_tx, None).await?;
268 return Ok(resubmitted_tx);
269 }
270
271 self.schedule_status_check(&tx, Some(5)).await?;
272 self.update_transaction_status_if_needed(tx, TransactionStatus::Submitted)
273 .await
274 }
275
276 async fn handle_resubmission(
278 &self,
279 tx: TransactionRepoModel,
280 ) -> Result<TransactionRepoModel, TransactionError> {
281 info!("Scheduling resubmit job for transaction: {}", tx.id);
282
283 let tx_to_process = if self.should_noop(&tx).await? {
284 self.process_noop_transaction(&tx).await?
285 } else {
286 tx
287 };
288
289 self.send_transaction_resubmit_job(&tx_to_process).await?;
290 Ok(tx_to_process)
291 }
292
293 async fn process_noop_transaction(
295 &self,
296 tx: &TransactionRepoModel,
297 ) -> Result<TransactionRepoModel, TransactionError> {
298 info!("Preparing transaction NOOP before resubmission: {}", tx.id);
299 let update = self.prepare_noop_update_request(tx, false).await?;
300 let updated_tx = self
301 .transaction_repository()
302 .partial_update(tx.id.clone(), update)
303 .await?;
304
305 self.send_transaction_update_notification(&updated_tx)
306 .await?;
307 Ok(updated_tx)
308 }
309
310 async fn handle_pending_state(
312 &self,
313 tx: TransactionRepoModel,
314 ) -> Result<TransactionRepoModel, TransactionError> {
315 if self.should_noop(&tx).await? {
316 info!("Preparing NOOP for pending transaction: {}", tx.id);
317 let update = self.prepare_noop_update_request(&tx, false).await?;
318 let updated_tx = self
319 .transaction_repository()
320 .partial_update(tx.id.clone(), update)
321 .await?;
322
323 self.send_transaction_submit_job(&updated_tx).await?;
324 self.send_transaction_update_notification(&updated_tx)
325 .await?;
326 return Ok(updated_tx);
327 } else {
328 self.schedule_status_check(&tx, Some(5)).await?;
329 }
330 Ok(tx)
331 }
332
333 async fn handle_mined_state(
335 &self,
336 tx: TransactionRepoModel,
337 ) -> Result<TransactionRepoModel, TransactionError> {
338 self.schedule_status_check(&tx, Some(5)).await?;
339 self.update_transaction_status_if_needed(tx, TransactionStatus::Mined)
340 .await
341 }
342
343 async fn handle_final_state(
345 &self,
346 tx: TransactionRepoModel,
347 status: TransactionStatus,
348 ) -> Result<TransactionRepoModel, TransactionError> {
349 self.update_transaction_status_if_needed(tx, status).await
350 }
351
352 pub async fn handle_status_impl(
357 &self,
358 tx: TransactionRepoModel,
359 ) -> Result<TransactionRepoModel, TransactionError> {
360 info!("Checking transaction status for tx: {:?}", tx.id);
361
362 let status = self.check_transaction_status(&tx).await?;
363 info!("Transaction status: {:?}", status);
364
365 match status {
366 TransactionStatus::Submitted => self.handle_submitted_state(tx).await,
367 TransactionStatus::Pending => self.handle_pending_state(tx).await,
368 TransactionStatus::Mined => self.handle_mined_state(tx).await,
369 TransactionStatus::Confirmed
370 | TransactionStatus::Failed
371 | TransactionStatus::Expired => self.handle_final_state(tx, status).await,
372 _ => Err(TransactionError::UnexpectedError(format!(
373 "Unexpected transaction status: {:?}",
374 status
375 ))),
376 }
377 }
378}
379
380#[cfg(test)]
381mod tests {
382 use crate::{
383 config::{EvmNetworkConfig, NetworkConfigCommon},
384 domain::transaction::evm::{EvmRelayerTransaction, MockPriceCalculatorTrait},
385 jobs::MockJobProducerTrait,
386 models::{
387 evm::Speed, EvmTransactionData, NetworkConfigData, NetworkRepoModel,
388 NetworkTransactionData, NetworkType, RelayerEvmPolicy, RelayerNetworkPolicy,
389 RelayerRepoModel, TransactionRepoModel, TransactionStatus, U256,
390 },
391 repositories::{
392 MockNetworkRepository, MockRelayerRepository, MockTransactionCounterTrait,
393 MockTransactionRepository,
394 },
395 services::{MockEvmProviderTrait, MockSigner},
396 };
397 use alloy::{
398 consensus::{Eip658Value, Receipt, ReceiptEnvelope, ReceiptWithBloom},
399 primitives::{b256, Address, BlockHash, Bloom, TxHash},
400 rpc::types::TransactionReceipt,
401 };
402 use chrono::{Duration, Utc};
403 use std::sync::Arc;
404
405 pub struct TestMocks {
407 pub provider: MockEvmProviderTrait,
408 pub relayer_repo: MockRelayerRepository,
409 pub network_repo: MockNetworkRepository,
410 pub tx_repo: MockTransactionRepository,
411 pub job_producer: MockJobProducerTrait,
412 pub signer: MockSigner,
413 pub counter: MockTransactionCounterTrait,
414 pub price_calc: MockPriceCalculatorTrait,
415 }
416
417 pub fn default_test_mocks() -> TestMocks {
420 TestMocks {
421 provider: MockEvmProviderTrait::new(),
422 relayer_repo: MockRelayerRepository::new(),
423 network_repo: MockNetworkRepository::new(),
424 tx_repo: MockTransactionRepository::new(),
425 job_producer: MockJobProducerTrait::new(),
426 signer: MockSigner::new(),
427 counter: MockTransactionCounterTrait::new(),
428 price_calc: MockPriceCalculatorTrait::new(),
429 }
430 }
431
432 pub fn default_test_mocks_with_network() -> TestMocks {
434 let mut mocks = default_test_mocks();
435 mocks
437 .network_repo
438 .expect_get_by_chain_id()
439 .returning(|network_type, chain_id| {
440 if network_type == NetworkType::Evm && chain_id == 1 {
441 Ok(Some(create_test_network_model()))
442 } else {
443 Ok(None)
444 }
445 });
446 mocks
447 }
448
449 pub fn create_test_network_model() -> NetworkRepoModel {
451 let evm_config = EvmNetworkConfig {
452 common: NetworkConfigCommon {
453 network: "mainnet".to_string(),
454 from: None,
455 rpc_urls: Some(vec!["https://rpc.example.com".to_string()]),
456 explorer_urls: Some(vec!["https://explorer.example.com".to_string()]),
457 average_blocktime_ms: Some(12000),
458 is_testnet: Some(false),
459 tags: Some(vec!["mainnet".to_string()]),
460 },
461 chain_id: Some(1),
462 required_confirmations: Some(12),
463 features: Some(vec!["eip1559".to_string()]),
464 symbol: Some("ETH".to_string()),
465 };
466 NetworkRepoModel {
467 id: "evm:mainnet".to_string(),
468 name: "mainnet".to_string(),
469 network_type: NetworkType::Evm,
470 config: NetworkConfigData::Evm(evm_config),
471 }
472 }
473
474 pub fn create_test_no_mempool_network_model() -> NetworkRepoModel {
476 let evm_config = EvmNetworkConfig {
477 common: NetworkConfigCommon {
478 network: "arbitrum".to_string(),
479 from: None,
480 rpc_urls: Some(vec!["https://arb-rpc.example.com".to_string()]),
481 explorer_urls: Some(vec!["https://arb-explorer.example.com".to_string()]),
482 average_blocktime_ms: Some(1000),
483 is_testnet: Some(false),
484 tags: Some(vec!["arbitrum".to_string(), "no-mempool".to_string()]),
485 },
486 chain_id: Some(42161),
487 required_confirmations: Some(12),
488 features: Some(vec!["eip1559".to_string()]),
489 symbol: Some("ETH".to_string()),
490 };
491 NetworkRepoModel {
492 id: "evm:arbitrum".to_string(),
493 name: "arbitrum".to_string(),
494 network_type: NetworkType::Evm,
495 config: NetworkConfigData::Evm(evm_config),
496 }
497 }
498
499 pub fn make_test_transaction(status: TransactionStatus) -> TransactionRepoModel {
503 TransactionRepoModel {
504 id: "test-tx-id".to_string(),
505 relayer_id: "test-relayer-id".to_string(),
506 status,
507 status_reason: None,
508 created_at: Utc::now().to_rfc3339(),
509 sent_at: None,
510 confirmed_at: None,
511 valid_until: None,
512 delete_at: None,
513 network_type: NetworkType::Evm,
514 network_data: NetworkTransactionData::Evm(EvmTransactionData {
515 chain_id: 1,
516 from: "0xSender".to_string(),
517 to: Some("0xRecipient".to_string()),
518 value: U256::from(0),
519 data: Some("0xData".to_string()),
520 gas_limit: Some(21000),
521 gas_price: Some(20000000000),
522 max_fee_per_gas: None,
523 max_priority_fee_per_gas: None,
524 nonce: None,
525 signature: None,
526 hash: None,
527 speed: Some(Speed::Fast),
528 raw: None,
529 }),
530 priced_at: None,
531 hashes: Vec::new(),
532 noop_count: None,
533 is_canceled: Some(false),
534 }
535 }
536
537 pub fn make_test_evm_relayer_transaction(
540 relayer: RelayerRepoModel,
541 mocks: TestMocks,
542 ) -> EvmRelayerTransaction<
543 MockEvmProviderTrait,
544 MockRelayerRepository,
545 MockNetworkRepository,
546 MockTransactionRepository,
547 MockJobProducerTrait,
548 MockSigner,
549 MockTransactionCounterTrait,
550 MockPriceCalculatorTrait,
551 > {
552 EvmRelayerTransaction::new(
553 relayer,
554 mocks.provider,
555 Arc::new(mocks.relayer_repo),
556 Arc::new(mocks.network_repo),
557 Arc::new(mocks.tx_repo),
558 Arc::new(mocks.counter),
559 Arc::new(mocks.job_producer),
560 mocks.price_calc,
561 mocks.signer,
562 )
563 .unwrap()
564 }
565
566 fn create_test_relayer() -> RelayerRepoModel {
567 RelayerRepoModel {
568 id: "test-relayer-id".to_string(),
569 name: "Test Relayer".to_string(),
570 paused: false,
571 system_disabled: false,
572 network: "test_network".to_string(),
573 network_type: NetworkType::Evm,
574 policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
575 signer_id: "test_signer".to_string(),
576 address: "0x".to_string(),
577 notification_id: None,
578 custom_rpc_urls: None,
579 }
580 }
581
582 fn make_mock_receipt(status: bool, block_number: Option<u64>) -> TransactionReceipt {
583 let tx_hash = TxHash::from(b256!(
585 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
586 ));
587 let block_hash = BlockHash::from(b256!(
588 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
589 ));
590 let from_address = Address::from([0x11; 20]);
591
592 TransactionReceipt {
593 inner: ReceiptEnvelope::Legacy(ReceiptWithBloom {
595 receipt: Receipt {
596 status: Eip658Value::Eip658(status), cumulative_gas_used: 0,
598 logs: vec![],
599 },
600 logs_bloom: Bloom::ZERO,
601 }),
602 transaction_hash: tx_hash,
603 transaction_index: Some(0),
604 block_hash: block_number.map(|_| block_hash), block_number,
606 gas_used: 21000,
607 effective_gas_price: 1000,
608 blob_gas_used: None,
609 blob_gas_price: None,
610 from: from_address,
611 to: None,
612 contract_address: None,
613 authorization_list: None,
614 }
615 }
616
617 mod check_transaction_status_tests {
619 use super::*;
620
621 #[tokio::test]
622 async fn test_not_mined() {
623 let mut mocks = default_test_mocks();
624 let relayer = create_test_relayer();
625 let mut tx = make_test_transaction(TransactionStatus::Submitted);
626
627 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
629 evm_data.hash = Some("0xFakeHash".to_string());
630 }
631
632 mocks
634 .provider
635 .expect_get_transaction_receipt()
636 .returning(|_| Box::pin(async { Ok(None) }));
637
638 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
639
640 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
641 assert_eq!(status, TransactionStatus::Submitted);
642 }
643
644 #[tokio::test]
645 async fn test_mined_but_not_confirmed() {
646 let mut mocks = default_test_mocks();
647 let relayer = create_test_relayer();
648 let mut tx = make_test_transaction(TransactionStatus::Submitted);
649
650 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
651 evm_data.hash = Some("0xFakeHash".to_string());
652 }
653
654 mocks
656 .provider
657 .expect_get_transaction_receipt()
658 .returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) }));
659
660 mocks
662 .provider
663 .expect_get_block_number()
664 .return_once(|| Box::pin(async { Ok(100) }));
665
666 mocks
668 .network_repo
669 .expect_get_by_chain_id()
670 .returning(|_, _| Ok(Some(create_test_network_model())));
671
672 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
673
674 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
675 assert_eq!(status, TransactionStatus::Mined);
676 }
677
678 #[tokio::test]
679 async fn test_confirmed() {
680 let mut mocks = default_test_mocks();
681 let relayer = create_test_relayer();
682 let mut tx = make_test_transaction(TransactionStatus::Submitted);
683
684 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
685 evm_data.hash = Some("0xFakeHash".to_string());
686 }
687
688 mocks
690 .provider
691 .expect_get_transaction_receipt()
692 .returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) }));
693
694 mocks
696 .provider
697 .expect_get_block_number()
698 .return_once(|| Box::pin(async { Ok(113) }));
699
700 mocks
702 .network_repo
703 .expect_get_by_chain_id()
704 .returning(|_, _| Ok(Some(create_test_network_model())));
705
706 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
707
708 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
709 assert_eq!(status, TransactionStatus::Confirmed);
710 }
711
712 #[tokio::test]
713 async fn test_failed() {
714 let mut mocks = default_test_mocks();
715 let relayer = create_test_relayer();
716 let mut tx = make_test_transaction(TransactionStatus::Submitted);
717
718 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
719 evm_data.hash = Some("0xFakeHash".to_string());
720 }
721
722 mocks
724 .provider
725 .expect_get_transaction_receipt()
726 .returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(false, Some(100)))) }));
727
728 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
729
730 let status = evm_transaction.check_transaction_status(&tx).await.unwrap();
731 assert_eq!(status, TransactionStatus::Failed);
732 }
733 }
734
735 mod should_resubmit_tests {
737 use super::*;
738 use crate::models::TransactionError;
739
740 #[tokio::test]
741 async fn test_should_resubmit_true() {
742 let mut mocks = default_test_mocks();
743 let relayer = create_test_relayer();
744
745 let mut tx = make_test_transaction(TransactionStatus::Submitted);
747 tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
748
749 mocks
751 .network_repo
752 .expect_get_by_chain_id()
753 .returning(|_, _| Ok(Some(create_test_network_model())));
754
755 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
756 let res = evm_transaction.should_resubmit(&tx).await.unwrap();
757 assert!(res, "Transaction should be resubmitted after timeout.");
758 }
759
760 #[tokio::test]
761 async fn test_should_resubmit_false() {
762 let mut mocks = default_test_mocks();
763 let relayer = create_test_relayer();
764
765 let mut tx = make_test_transaction(TransactionStatus::Submitted);
767 tx.sent_at = Some(Utc::now().to_rfc3339());
768
769 mocks
771 .network_repo
772 .expect_get_by_chain_id()
773 .returning(|_, _| Ok(Some(create_test_network_model())));
774
775 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
776 let res = evm_transaction.should_resubmit(&tx).await.unwrap();
777 assert!(!res, "Transaction should not be resubmitted immediately.");
778 }
779
780 #[tokio::test]
781 async fn test_should_resubmit_true_for_no_mempool_network() {
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 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
791 evm_data.chain_id = 42161; }
793
794 mocks
796 .network_repo
797 .expect_get_by_chain_id()
798 .returning(|_, _| Ok(Some(create_test_no_mempool_network_model())));
799
800 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
801 let res = evm_transaction.should_resubmit(&tx).await.unwrap();
802 assert!(
803 res,
804 "Transaction should be resubmitted for no-mempool networks."
805 );
806 }
807
808 #[tokio::test]
809 async fn test_should_resubmit_network_not_found() {
810 let mut mocks = default_test_mocks();
811 let relayer = create_test_relayer();
812
813 let mut tx = make_test_transaction(TransactionStatus::Submitted);
814 tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
815
816 mocks
818 .network_repo
819 .expect_get_by_chain_id()
820 .returning(|_, _| Ok(None));
821
822 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
823 let result = evm_transaction.should_resubmit(&tx).await;
824
825 assert!(
826 result.is_err(),
827 "should_resubmit should return error when network not found"
828 );
829 let error = result.unwrap_err();
830 match error {
831 TransactionError::UnexpectedError(msg) => {
832 assert!(msg.contains("Network with chain id 1 not found"));
833 }
834 _ => panic!("Expected UnexpectedError for network not found"),
835 }
836 }
837
838 #[tokio::test]
839 async fn test_should_resubmit_network_conversion_error() {
840 let mut mocks = default_test_mocks();
841 let relayer = create_test_relayer();
842
843 let mut tx = make_test_transaction(TransactionStatus::Submitted);
844 tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
845
846 let invalid_evm_config = EvmNetworkConfig {
848 common: NetworkConfigCommon {
849 network: "invalid-network".to_string(),
850 from: None,
851 rpc_urls: Some(vec!["https://rpc.example.com".to_string()]),
852 explorer_urls: Some(vec!["https://explorer.example.com".to_string()]),
853 average_blocktime_ms: Some(12000),
854 is_testnet: Some(false),
855 tags: Some(vec!["testnet".to_string()]),
856 },
857 chain_id: None, required_confirmations: Some(12),
859 features: Some(vec!["eip1559".to_string()]),
860 symbol: Some("ETH".to_string()),
861 };
862 let invalid_network = NetworkRepoModel {
863 id: "evm:invalid".to_string(),
864 name: "invalid-network".to_string(),
865 network_type: NetworkType::Evm,
866 config: NetworkConfigData::Evm(invalid_evm_config),
867 };
868
869 mocks
871 .network_repo
872 .expect_get_by_chain_id()
873 .returning(move |_, _| Ok(Some(invalid_network.clone())));
874
875 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
876 let result = evm_transaction.should_resubmit(&tx).await;
877
878 assert!(
879 result.is_err(),
880 "should_resubmit should return error when network conversion fails"
881 );
882 let error = result.unwrap_err();
883 match error {
884 TransactionError::UnexpectedError(msg) => {
885 assert!(msg.contains("Error converting network model to EvmNetwork"));
886 }
887 _ => panic!("Expected UnexpectedError for network conversion failure"),
888 }
889 }
890 }
891
892 mod should_noop_tests {
894 use super::*;
895
896 #[tokio::test]
897 async fn test_expired_transaction_triggers_noop() {
898 let mut mocks = default_test_mocks();
899 let relayer = create_test_relayer();
900
901 let mut tx = make_test_transaction(TransactionStatus::Submitted);
902 tx.valid_until = Some((Utc::now() - Duration::seconds(10)).to_rfc3339());
904
905 mocks
907 .network_repo
908 .expect_get_by_chain_id()
909 .returning(|_, _| Ok(Some(create_test_network_model())));
910
911 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
912 let res = evm_transaction.should_noop(&tx).await.unwrap();
913 assert!(res, "Expired transaction should be replaced with a NOOP.");
914 }
915 }
916
917 mod update_transaction_status_tests {
919 use super::*;
920
921 #[tokio::test]
922 async fn test_no_update_when_status_is_same() {
923 let mocks = default_test_mocks();
925 let relayer = create_test_relayer();
926 let tx = make_test_transaction(TransactionStatus::Submitted);
927 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
928
929 let updated_tx = evm_transaction
932 .update_transaction_status_if_needed(tx.clone(), TransactionStatus::Submitted)
933 .await
934 .unwrap();
935 assert_eq!(updated_tx.status, TransactionStatus::Submitted);
936 assert_eq!(updated_tx.id, tx.id);
937 }
938 }
939
940 mod prepare_noop_update_request_tests {
942 use super::*;
943
944 #[tokio::test]
945 async fn test_noop_request_without_cancellation() {
946 let mocks = default_test_mocks_with_network();
948 let relayer = create_test_relayer();
949 let mut tx = make_test_transaction(TransactionStatus::Submitted);
950 tx.noop_count = Some(2);
951 tx.is_canceled = Some(false);
952
953 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
954 let update_req = evm_transaction
955 .prepare_noop_update_request(&tx, false)
956 .await
957 .unwrap();
958
959 assert_eq!(update_req.noop_count, Some(3));
961 assert_eq!(update_req.is_canceled, Some(false));
963 }
964
965 #[tokio::test]
966 async fn test_noop_request_with_cancellation() {
967 let mocks = default_test_mocks_with_network();
969 let relayer = create_test_relayer();
970 let mut tx = make_test_transaction(TransactionStatus::Submitted);
971 tx.noop_count = None;
972 tx.is_canceled = Some(false);
973
974 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
975 let update_req = evm_transaction
976 .prepare_noop_update_request(&tx, true)
977 .await
978 .unwrap();
979
980 assert_eq!(update_req.noop_count, Some(1));
982 assert_eq!(update_req.is_canceled, Some(true));
984 }
985 }
986
987 mod handle_submitted_state_tests {
989 use super::*;
990
991 #[tokio::test]
992 async fn test_schedules_resubmit_job() {
993 let mut mocks = default_test_mocks();
994 let relayer = create_test_relayer();
995
996 let mut tx = make_test_transaction(TransactionStatus::Submitted);
998 tx.sent_at = Some((Utc::now() - Duration::seconds(600)).to_rfc3339());
999
1000 mocks
1002 .network_repo
1003 .expect_get_by_chain_id()
1004 .returning(|_, _| Ok(Some(create_test_network_model())));
1005
1006 mocks
1008 .job_producer
1009 .expect_produce_submit_transaction_job()
1010 .returning(|_, _| Box::pin(async { Ok(()) }));
1011
1012 mocks
1014 .job_producer
1015 .expect_produce_check_transaction_status_job()
1016 .returning(|_, _| Box::pin(async { Ok(()) }));
1017
1018 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1019 let updated_tx = evm_transaction.handle_submitted_state(tx).await.unwrap();
1020
1021 assert_eq!(updated_tx.status, TransactionStatus::Submitted);
1023 }
1024 }
1025
1026 mod handle_pending_state_tests {
1028 use super::*;
1029
1030 #[tokio::test]
1031 async fn test_pending_state_no_noop() {
1032 let mut mocks = default_test_mocks();
1034 let relayer = create_test_relayer();
1035 let mut tx = make_test_transaction(TransactionStatus::Pending);
1036 tx.created_at = Utc::now().to_rfc3339(); mocks
1040 .network_repo
1041 .expect_get_by_chain_id()
1042 .returning(|_, _| Ok(Some(create_test_network_model())));
1043
1044 mocks
1046 .job_producer
1047 .expect_produce_check_transaction_status_job()
1048 .returning(|_, _| Box::pin(async { Ok(()) }));
1049
1050 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1051 let result = evm_transaction
1052 .handle_pending_state(tx.clone())
1053 .await
1054 .unwrap();
1055
1056 assert_eq!(result.id, tx.id);
1058 assert_eq!(result.status, tx.status);
1059 assert_eq!(result.noop_count, tx.noop_count);
1060 }
1061
1062 #[tokio::test]
1063 async fn test_pending_state_with_noop() {
1064 let mut mocks = default_test_mocks();
1066 let relayer = create_test_relayer();
1067 let mut tx = make_test_transaction(TransactionStatus::Pending);
1068 tx.created_at = (Utc::now() - Duration::minutes(2)).to_rfc3339();
1069
1070 mocks
1072 .network_repo
1073 .expect_get_by_chain_id()
1074 .returning(|_, _| Ok(Some(create_test_network_model())));
1075
1076 let tx_clone = tx.clone();
1078 mocks
1079 .tx_repo
1080 .expect_partial_update()
1081 .returning(move |_, update| {
1082 let mut updated_tx = tx_clone.clone();
1083 updated_tx.noop_count = update.noop_count;
1084 Ok(updated_tx)
1085 });
1086 mocks
1088 .job_producer
1089 .expect_produce_submit_transaction_job()
1090 .returning(|_, _| Box::pin(async { Ok(()) }));
1091 mocks
1092 .job_producer
1093 .expect_produce_send_notification_job()
1094 .returning(|_, _| Box::pin(async { Ok(()) }));
1095
1096 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1097 let result = evm_transaction
1098 .handle_pending_state(tx.clone())
1099 .await
1100 .unwrap();
1101
1102 assert!(result.noop_count.unwrap_or(0) > 0);
1104 }
1105 }
1106
1107 mod handle_mined_state_tests {
1109 use super::*;
1110
1111 #[tokio::test]
1112 async fn test_updates_status_and_schedules_check() {
1113 let mut mocks = default_test_mocks();
1114 let relayer = create_test_relayer();
1115 let tx = make_test_transaction(TransactionStatus::Submitted);
1117
1118 mocks
1120 .job_producer
1121 .expect_produce_check_transaction_status_job()
1122 .returning(|_, _| Box::pin(async { Ok(()) }));
1123 mocks
1125 .tx_repo
1126 .expect_partial_update()
1127 .returning(|_, update| {
1128 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1129 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1130 Ok(updated_tx)
1131 });
1132
1133 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1134 let result = evm_transaction
1135 .handle_mined_state(tx.clone())
1136 .await
1137 .unwrap();
1138 assert_eq!(result.status, TransactionStatus::Mined);
1139 }
1140 }
1141
1142 mod handle_final_state_tests {
1144 use super::*;
1145
1146 #[tokio::test]
1147 async fn test_final_state_confirmed() {
1148 let mut mocks = default_test_mocks();
1149 let relayer = create_test_relayer();
1150 let tx = make_test_transaction(TransactionStatus::Submitted);
1151
1152 mocks
1154 .tx_repo
1155 .expect_partial_update()
1156 .returning(|_, update| {
1157 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1158 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1159 Ok(updated_tx)
1160 });
1161
1162 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1163 let result = evm_transaction
1164 .handle_final_state(tx.clone(), TransactionStatus::Confirmed)
1165 .await
1166 .unwrap();
1167 assert_eq!(result.status, TransactionStatus::Confirmed);
1168 }
1169
1170 #[tokio::test]
1171 async fn test_final_state_failed() {
1172 let mut mocks = default_test_mocks();
1173 let relayer = create_test_relayer();
1174 let tx = make_test_transaction(TransactionStatus::Submitted);
1175
1176 mocks
1178 .tx_repo
1179 .expect_partial_update()
1180 .returning(|_, update| {
1181 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1182 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1183 Ok(updated_tx)
1184 });
1185
1186 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1187 let result = evm_transaction
1188 .handle_final_state(tx.clone(), TransactionStatus::Failed)
1189 .await
1190 .unwrap();
1191 assert_eq!(result.status, TransactionStatus::Failed);
1192 }
1193
1194 #[tokio::test]
1195 async fn test_final_state_expired() {
1196 let mut mocks = default_test_mocks();
1197 let relayer = create_test_relayer();
1198 let tx = make_test_transaction(TransactionStatus::Submitted);
1199
1200 mocks
1202 .tx_repo
1203 .expect_partial_update()
1204 .returning(|_, update| {
1205 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1206 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1207 Ok(updated_tx)
1208 });
1209
1210 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1211 let result = evm_transaction
1212 .handle_final_state(tx.clone(), TransactionStatus::Expired)
1213 .await
1214 .unwrap();
1215 assert_eq!(result.status, TransactionStatus::Expired);
1216 }
1217 }
1218
1219 mod handle_status_impl_tests {
1221 use super::*;
1222
1223 #[tokio::test]
1224 async fn test_impl_submitted_branch() {
1225 let mut mocks = default_test_mocks();
1226 let relayer = create_test_relayer();
1227 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1228 tx.sent_at = Some((Utc::now() - Duration::seconds(120)).to_rfc3339());
1229 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1231 evm_data.hash = Some("0xFakeHash".to_string());
1232 }
1233 mocks
1235 .provider
1236 .expect_get_transaction_receipt()
1237 .returning(|_| Box::pin(async { Ok(None) }));
1238 mocks
1240 .network_repo
1241 .expect_get_by_chain_id()
1242 .returning(|_, _| Ok(Some(create_test_network_model())));
1243 mocks
1245 .job_producer
1246 .expect_produce_check_transaction_status_job()
1247 .returning(|_, _| Box::pin(async { Ok(()) }));
1248 mocks
1250 .tx_repo
1251 .expect_partial_update()
1252 .returning(|_, update| {
1253 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1254 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1255 Ok(updated_tx)
1256 });
1257
1258 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1259 let result = evm_transaction.handle_status_impl(tx).await.unwrap();
1260 assert_eq!(result.status, TransactionStatus::Submitted);
1261 }
1262
1263 #[tokio::test]
1264 async fn test_impl_mined_branch() {
1265 let mut mocks = default_test_mocks();
1266 let relayer = create_test_relayer();
1267 let mut tx = make_test_transaction(TransactionStatus::Submitted);
1268 if let NetworkTransactionData::Evm(ref mut evm_data) = tx.network_data {
1270 evm_data.hash = Some("0xFakeHash".to_string());
1271 }
1272 mocks
1274 .provider
1275 .expect_get_transaction_receipt()
1276 .returning(|_| Box::pin(async { Ok(Some(make_mock_receipt(true, Some(100)))) }));
1277 mocks
1279 .provider
1280 .expect_get_block_number()
1281 .return_once(|| Box::pin(async { Ok(100) }));
1282 mocks
1284 .network_repo
1285 .expect_get_by_chain_id()
1286 .returning(|_, _| Ok(Some(create_test_network_model())));
1287 mocks
1289 .job_producer
1290 .expect_produce_check_transaction_status_job()
1291 .returning(|_, _| Box::pin(async { Ok(()) }));
1292 mocks
1294 .tx_repo
1295 .expect_partial_update()
1296 .returning(|_, update| {
1297 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1298 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1299 Ok(updated_tx)
1300 });
1301
1302 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1303 let result = evm_transaction.handle_status_impl(tx).await.unwrap();
1304 assert_eq!(result.status, TransactionStatus::Mined);
1305 }
1306
1307 #[tokio::test]
1308 async fn test_impl_final_confirmed_branch() {
1309 let mut mocks = default_test_mocks();
1310 let relayer = create_test_relayer();
1311 let tx = make_test_transaction(TransactionStatus::Confirmed);
1313
1314 mocks
1317 .tx_repo
1318 .expect_partial_update()
1319 .returning(|_, update| {
1320 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1321 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1322 Ok(updated_tx)
1323 });
1324
1325 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1326 let result = evm_transaction.handle_status_impl(tx).await.unwrap();
1327 assert_eq!(result.status, TransactionStatus::Confirmed);
1328 }
1329
1330 #[tokio::test]
1331 async fn test_impl_final_failed_branch() {
1332 let mut mocks = default_test_mocks();
1333 let relayer = create_test_relayer();
1334 let tx = make_test_transaction(TransactionStatus::Failed);
1336
1337 mocks
1338 .tx_repo
1339 .expect_partial_update()
1340 .returning(|_, update| {
1341 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1342 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1343 Ok(updated_tx)
1344 });
1345
1346 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1347 let result = evm_transaction.handle_status_impl(tx).await.unwrap();
1348 assert_eq!(result.status, TransactionStatus::Failed);
1349 }
1350
1351 #[tokio::test]
1352 async fn test_impl_final_expired_branch() {
1353 let mut mocks = default_test_mocks();
1354 let relayer = create_test_relayer();
1355 let tx = make_test_transaction(TransactionStatus::Expired);
1357
1358 mocks
1359 .tx_repo
1360 .expect_partial_update()
1361 .returning(|_, update| {
1362 let mut updated_tx = make_test_transaction(TransactionStatus::Submitted);
1363 updated_tx.status = update.status.unwrap_or(updated_tx.status);
1364 Ok(updated_tx)
1365 });
1366
1367 let evm_transaction = make_test_evm_relayer_transaction(relayer, mocks);
1368 let result = evm_transaction.handle_status_impl(tx).await.unwrap();
1369 assert_eq!(result.status, TransactionStatus::Expired);
1370 }
1371 }
1372}