partner_chains_cardano_offchain/
await_tx.rs

1use anyhow::anyhow;
2use ogmios_client::query_ledger_state::QueryUtxoByUtxoId;
3use sidechain_domain::{McTxHash, UtxoId};
4use std::time::Duration;
5use tokio_retry::{Retry, strategy::FixedInterval};
6
7/// Trait for different strategies of waiting for a Cardano transaction to complete.
8pub trait AwaitTx {
9	#[allow(async_fn_in_trait)]
10	/// This is used for waiting until the output of a submitted transaction can be observed.
11	async fn await_tx_output<C: QueryUtxoByUtxoId>(
12		&self,
13		client: &C,
14		tx_hash: McTxHash,
15	) -> anyhow::Result<()>;
16}
17
18/// Transaction awaiting strategy that uses fixed number of retries and a fixed delay.
19pub struct FixedDelayRetries {
20	delay: Duration,
21	retries: usize,
22}
23
24impl FixedDelayRetries {
25	/// Constructs [FixedDelayRetries] with `delay` [Duration] and `retries` number of maximum retries.
26	pub fn new(delay: Duration, retries: usize) -> Self {
27		Self { delay, retries }
28	}
29
30	/// Constructs [FixedDelayRetries] that keeps retrying every 5 seconds for 5 minutes.
31	pub fn five_minutes() -> Self {
32		Self { delay: Duration::from_secs(5), retries: 59 }
33	}
34}
35
36impl AwaitTx for FixedDelayRetries {
37	async fn await_tx_output<C: QueryUtxoByUtxoId>(
38		&self,
39		client: &C,
40		tx_hash: McTxHash,
41	) -> anyhow::Result<()> {
42		let strategy = FixedInterval::new(self.delay).take(self.retries);
43		let utxo_id = UtxoId::new(tx_hash.0, 0);
44		let _ = Retry::spawn(strategy, || async {
45			log::info!("Probing for transaction output '{}'", utxo_id);
46			let utxo = client.query_utxo_by_id(utxo_id).await.map_err(|_| ())?;
47			utxo.ok_or(())
48		})
49		.await
50		.map_err(|_| {
51			anyhow!(
52				"Retries for confirmation of transaction '{}' exceeded the limit",
53				hex::encode(utxo_id.tx_hash.0)
54			)
55		})?;
56		log::info!("Transaction output '{}'", hex::encode(utxo_id.tx_hash.0));
57		Ok(())
58	}
59}
60
61#[cfg(test)]
62pub(crate) mod mock {
63	use super::AwaitTx;
64	use ogmios_client::query_ledger_state::QueryUtxoByUtxoId;
65
66	pub(crate) struct ImmediateSuccess;
67
68	impl AwaitTx for ImmediateSuccess {
69		async fn await_tx_output<Q: QueryUtxoByUtxoId>(
70			&self,
71			_query: &Q,
72			_utxo_id: sidechain_domain::McTxHash,
73		) -> anyhow::Result<()> {
74			Ok(())
75		}
76	}
77}
78
79#[cfg(test)]
80mod tests {
81	use super::{AwaitTx, FixedDelayRetries};
82	use ogmios_client::{
83		OgmiosClientError,
84		query_ledger_state::QueryUtxoByUtxoId,
85		types::{OgmiosTx, OgmiosUtxo},
86	};
87	use sidechain_domain::McTxHash;
88	use std::{cell::RefCell, time::Duration};
89
90	#[tokio::test]
91	async fn immediate_success() {
92		let mock =
93			MockQueryUtxoByUtxoId { responses: RefCell::new(vec![Ok(Some(awaited_utxo()))]) };
94		FixedDelayRetries::new(Duration::from_millis(1), 3)
95			.await_tx_output(&mock, awaited_tx_hash())
96			.await
97			.unwrap();
98	}
99
100	#[tokio::test]
101	async fn success_in_2nd_attempt() {
102		let mock = MockQueryUtxoByUtxoId {
103			responses: RefCell::new(vec![Ok(None), Ok(Some(awaited_utxo()))]),
104		};
105		FixedDelayRetries::new(Duration::from_millis(1), 3)
106			.await_tx_output(&mock, awaited_tx_hash())
107			.await
108			.unwrap();
109	}
110
111	#[tokio::test]
112	async fn all_attempts_result_not_found() {
113		let mock =
114			MockQueryUtxoByUtxoId { responses: RefCell::new(vec![Ok(None), Ok(None), Ok(None)]) };
115		let result = FixedDelayRetries::new(Duration::from_millis(1), 2)
116			.await_tx_output(&mock, awaited_tx_hash())
117			.await;
118		assert!(result.is_err())
119	}
120
121	#[tokio::test]
122	async fn all_attempts_failed() {
123		let mock = MockQueryUtxoByUtxoId {
124			responses: RefCell::new(vec![
125				Err(OgmiosClientError::RequestError("test error1".to_string())),
126				Err(OgmiosClientError::RequestError("test error2".to_string())),
127				Err(OgmiosClientError::RequestError("test error3".to_string())),
128			]),
129		};
130		let result = FixedDelayRetries::new(Duration::from_millis(1), 2)
131			.await_tx_output(&mock, awaited_tx_hash())
132			.await;
133		assert!(result.is_err())
134	}
135
136	struct MockQueryUtxoByUtxoId {
137		responses: RefCell<Vec<Result<Option<OgmiosUtxo>, OgmiosClientError>>>,
138	}
139
140	impl QueryUtxoByUtxoId for MockQueryUtxoByUtxoId {
141		async fn query_utxo_by_id(
142			&self,
143			utxo: sidechain_domain::UtxoId,
144		) -> Result<Option<OgmiosUtxo>, OgmiosClientError> {
145			if utxo.tx_hash == awaited_tx_hash() {
146				self.responses.borrow_mut().pop().unwrap()
147			} else {
148				Ok(None)
149			}
150		}
151	}
152
153	fn awaited_tx_hash() -> McTxHash {
154		McTxHash([7u8; 32])
155	}
156
157	fn awaited_utxo() -> OgmiosUtxo {
158		OgmiosUtxo { transaction: OgmiosTx { id: [7u8; 32] }, index: 1, ..Default::default() }
159	}
160}