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