partner_chains_cardano_offchain/
await_tx.rs

1use anyhow::anyhow;
2use ogmios_client::query_ledger_state::QueryUtxoByUtxoId;
3use sidechain_domain::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	/// TODO: make this take a Transaction ID instead of a UtxoId
12	async fn await_tx_output<C: QueryUtxoByUtxoId>(
13		&self,
14		client: &C,
15		utxo_id: UtxoId,
16	) -> anyhow::Result<()>;
17}
18
19/// Transaction awaiting strategy that uses fixed number of retries and a fixed delay.
20pub struct FixedDelayRetries {
21	delay: Duration,
22	retries: usize,
23}
24
25impl FixedDelayRetries {
26	/// Constructs [FixedDelayRetries] with `delay` [Duration] and `retries` number of maximum retries.
27	pub fn new(delay: Duration, retries: usize) -> Self {
28		Self { delay, retries }
29	}
30
31	/// Constructs [FixedDelayRetries] that keeps retrying every 5 seconds for 5 minutes.
32	pub fn five_minutes() -> Self {
33		Self { delay: Duration::from_secs(5), retries: 59 }
34	}
35}
36
37impl AwaitTx for FixedDelayRetries {
38	async fn await_tx_output<C: QueryUtxoByUtxoId>(
39		&self,
40		client: &C,
41		utxo_id: UtxoId,
42	) -> anyhow::Result<()> {
43		let strategy = FixedInterval::new(self.delay).take(self.retries);
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::UtxoId,
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, UtxoId, UtxoIndex};
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_utxo_id())
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_utxo_id())
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_utxo_id())
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_utxo_id())
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 == awaited_utxo_id() {
146				self.responses.borrow_mut().pop().unwrap()
147			} else {
148				Ok(None)
149			}
150		}
151	}
152
153	fn awaited_utxo_id() -> UtxoId {
154		UtxoId { tx_hash: McTxHash([7u8; 32]), index: UtxoIndex(1) }
155	}
156
157	fn awaited_utxo() -> OgmiosUtxo {
158		OgmiosUtxo { transaction: OgmiosTx { id: [7u8; 32] }, index: 1, ..Default::default() }
159	}
160}