Skip to main content

avail_rust_client/submission/
submitted.rs

1//! Builders for submitting extrinsics and inspecting their on-chain lifecycle.
2
3use crate::{
4	Client, Error, UserError,
5	block::{self, Block},
6	conversions,
7	subscription::Sub,
8	transaction_options::{RefinedMortality, RefinedOptions},
9};
10use avail_rust_core::{
11	AccountId, BlockInfo, EncodeSelector, H256, HasHeader, RpcError, rpc::ExtrinsicOpts,
12	substrate::extrinsic::ExtrinsicAdditional, types::metadata::HashString,
13};
14use codec::Decode;
15#[cfg(feature = "tracing")]
16use tracing::info;
17
18/// Handle to a transaction that has already been submitted to the network along with the contextual
19/// information required to query its lifecycle.
20#[derive(Clone)]
21pub struct SubmittedTransaction {
22	client: Client,
23	pub ext_hash: H256,
24	pub account_id: AccountId,
25	pub options: RefinedOptions,
26	pub additional: ExtrinsicAdditional,
27}
28
29impl SubmittedTransaction {
30	/// Creates a new submitted transaction handle using previously gathered metadata.
31	///
32	/// This does not perform any network calls; it simply stores the information needed to later
33	/// resolve receipts or query status.
34	pub fn new(
35		client: Client,
36		ext_hash: H256,
37		account_id: AccountId,
38		options: RefinedOptions,
39		additional: ExtrinsicAdditional,
40	) -> Self {
41		Self { client, ext_hash, account_id, options, additional }
42	}
43
44	/// Produces a receipt describing how the transaction landed on chain, if it did at all.
45	///
46	/// # Returns
47	/// - `Ok(Some(TransactionReceipt))` when the transaction is found in the searched block range.
48	/// - `Ok(None)` when the transaction cannot be located within the mortality window implied by
49	///   `options`.
50	/// - `Err(Error)` when the underlying RPC or subscription queries fail.
51	///
52	/// Set `use_best_block` to `true` to follow the node's best chain (potentially including
53	/// non-finalized blocks) or `false` to restrict the search to finalized blocks.
54	pub async fn receipt(&self, use_best_block: bool) -> Result<Option<TransactionReceipt>, Error> {
55		Utils::transaction_receipt(
56			self.client.clone(),
57			self.ext_hash,
58			self.options.nonce,
59			&self.account_id,
60			&self.options.mortality,
61			use_best_block,
62		)
63		.await
64	}
65}
66
67/// Indicates what happened to a transaction after it was submitted.
68///
69/// The variants correspond to the states returned by the chain RPC when querying transaction status.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71#[repr(u8)]
72pub enum BlockState {
73	/// The transaction was included in a block but the block may still be re-orged out.
74	Included = 0,
75	/// The block containing the transaction is finalized and immutable under normal circumstances.
76	Finalized = 1,
77	/// The transaction was seen but ended up discarded (e.g. due to invalidation).
78	Discarded = 2,
79	/// The transaction could not be found on chain.
80	DoesNotExist = 3,
81}
82
83impl std::fmt::Display for BlockState {
84	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85		match self {
86			BlockState::Included => std::write!(f, "Included"),
87			BlockState::Finalized => std::write!(f, "Finalized"),
88			BlockState::Discarded => std::write!(f, "Discarded"),
89			BlockState::DoesNotExist => std::write!(f, "DoesNotExist"),
90		}
91	}
92}
93
94/// Detailed information about where a transaction was found on chain.
95#[derive(Clone)]
96pub struct TransactionReceipt {
97	client: Client,
98	pub block_hash: H256,
99	pub block_height: u32,
100	pub ext_hash: H256,
101	pub ext_index: u32,
102}
103
104impl TransactionReceipt {
105	/// Wraps the provided block and transaction references without performing network IO.
106	pub fn new(client: Client, block_hash: H256, block_height: u32, ext_hash: H256, ext_index: u32) -> Self {
107		Self { client, block_hash, block_height, ext_hash, ext_index }
108	}
109
110	/// Returns the current lifecycle state of the containing block.
111	///
112	/// # Returns
113	/// - `Ok(BlockState)` on success.
114	/// - `Err(Error)` if the RPC request fails or the node cannot provide the block state.
115	pub async fn block_state(&self) -> Result<BlockState, Error> {
116		self.client.chain().block_state(self.block_hash).await
117	}
118
119	/// Fetches and decodes the extrinsic at the recorded index within the block.
120	///
121	/// # Returns
122	/// - `Ok(Extrinsic<T>)` when the extrinsic exists and decodes as `T`.
123	/// - `Err(Error)` when the extrinsic is missing, cannot be decoded as `T`, or RPC access fails.
124	pub async fn extrinsic<T: HasHeader + Decode>(&self) -> Result<block::BlockExtrinsic<T>, Error> {
125		let block = Block::new(self.client.clone(), self.block_hash).extrinsics();
126		let ext: Option<block::BlockExtrinsic<T>> = block.get(self.ext_index).await?;
127		let Some(ext) = ext else {
128			return Err(RpcError::ExpectedData("No extrinsic found at the requested index.".into()).into());
129		};
130
131		Ok(ext)
132	}
133
134	/// Returns the raw extrinsic bytes or a different encoding if requested.
135	///
136	/// # Returns
137	/// - `Ok(EncodedExtrinsic)` with the requested encoding.
138	/// - `Err(Error)` when the extrinsic cannot be found or an RPC failure occurs.
139	pub async fn encoded(&self) -> Result<block::BlockEncodedExtrinsic, Error> {
140		let block = Block::new(self.client.clone(), self.block_hash).encoded();
141		let ext = block.get(self.ext_index).await?;
142		let Some(ext) = ext else {
143			return Err(RpcError::ExpectedData("No extrinsic found at the requested index.".into()).into());
144		};
145
146		Ok(ext)
147	}
148
149	/// Fetches the events emitted as part of the transaction execution.
150	///
151	/// # Returns
152	/// - `Ok(ExtrinsicEvents)` when the extrinsic exists and events are available.
153	/// - `Err(Error)` when the events cannot be located or fetched.
154	pub async fn events(&self) -> Result<crate::block::events::BlockEvents, Error> {
155		let block = Block::new(self.client.clone(), self.block_hash).events();
156		let events = block.extrinsic(self.ext_index).await?;
157		if events.is_empty() {
158			return Err(RpcError::ExpectedData("No events found for the requested extrinsic.".into()).into());
159		};
160
161		Ok(events)
162	}
163
164	/// Iterates block-by-block from `block_start` through `block_end` (inclusive) looking for an
165	/// extrinsic whose hash matches `tx_hash`.
166	///
167	/// Returns `Ok(Some(TransactionReceipt))` as soon as a match is found, `Ok(None)` when the
168	/// entire range has been exhausted without a match, and bubbles up any RPC or subscription
169	/// errors encountered along the way.
170	///
171	/// Fails fast with a validation error when `block_start > block_end`. When `use_best_block`
172	/// is `true`, the search follows the node's best chain; otherwise it restricts the iteration to
173	/// finalized blocks only.
174	pub async fn from_range(
175		client: Client,
176		ext_hash: impl Into<HashString>,
177		block_start: u32,
178		block_end: u32,
179		use_best_block: bool,
180	) -> Result<Option<TransactionReceipt>, Error> {
181		if block_start > block_end {
182			return Err(UserError::ValidationFailed("Block Start cannot start after Block End".into()).into());
183		}
184
185		let tx_hash = conversions::hash_string::to_hash(ext_hash)?;
186		let mut sub = Sub::new(client.clone());
187		sub.use_best_block(use_best_block);
188		sub.set_block_height(block_start);
189
190		loop {
191			let block_info = sub.next().await?;
192
193			let block = Block::new(client.clone(), block_info.height);
194			let opts = ExtrinsicOpts::new().filter(tx_hash).encode_as(EncodeSelector::None);
195			let infos = block.extrinsic_infos(opts).await?;
196
197			if let Some(info) = infos.first() {
198				let tr = TransactionReceipt::new(
199					client.clone(),
200					block_info.hash,
201					block_info.height,
202					info.ext_hash,
203					info.ext_index,
204				);
205				return Ok(Some(tr));
206			}
207
208			if block_info.height >= block_end {
209				return Ok(None);
210			}
211		}
212	}
213}
214
215/// Convenience helpers for locating transactions on chain.
216pub struct Utils;
217impl Utils {
218	/// Resolves the canonical receipt for a transaction if it landed on chain within its mortality window.
219	///
220	/// # Returns
221	/// - `Ok(Some(TransactionReceipt))` when a matching inclusion is located.
222	/// - `Ok(None)` when no matching transaction exists in the searched range.
223	/// - `Err(Error)` when RPC queries fail or input validation detects an inconsistency.
224	pub async fn transaction_receipt(
225		client: Client,
226		tx_hash: H256,
227		nonce: u32,
228		account_id: &AccountId,
229		mortality: &RefinedMortality,
230		use_best_block: bool,
231	) -> Result<Option<TransactionReceipt>, Error> {
232		let Some(block_info) =
233			Self::find_correct_block_info(&client, nonce, tx_hash, account_id, mortality, use_best_block).await?
234		else {
235			return Ok(None);
236		};
237
238		let block = Block::new(client.clone(), block_info.hash);
239		let opts = ExtrinsicOpts::new().filter(tx_hash).encode_as(EncodeSelector::None);
240		let ext_info = block.extrinsic_infos(opts).await?;
241
242		let Some(ext_info) = ext_info.first() else {
243			return Ok(None);
244		};
245
246		Ok(Some(TransactionReceipt::new(
247			client, block_info.hash, block_info.height, ext_info.ext_hash, ext_info.ext_index,
248		)))
249	}
250
251	/// Inspects blocks following the transaction's mortality and returns the first matching inclusion.
252	///
253	/// The search starts at `mortality.block_height` and proceeds one block at a time until the
254	/// mortality period expires, optionally following the node's best chain when `use_best_block` is
255	/// `true`.
256	///
257	/// # Returns
258	/// - `Ok(Some(BlockInfo))` once an inclusion is confirmed or a higher nonce proves execution.
259	/// - `Ok(None)` when the mortality period elapses without finding a match.
260	/// - `Err(Error)` if block streaming or nonce queries fail.
261	pub async fn find_correct_block_info(
262		client: &Client,
263		nonce: u32,
264		tx_hash: H256,
265		account_id: &AccountId,
266		mortality: &RefinedMortality,
267		use_best_block: bool,
268	) -> Result<Option<BlockInfo>, Error> {
269		let mortality_ends_height = mortality.block_height.saturating_add(mortality.period as u32);
270
271		let mut sub = Sub::new(client.clone());
272		sub.set_block_height(mortality.block_height);
273		sub.use_best_block(use_best_block);
274
275		let mut current_block_height = mortality.block_height;
276
277		#[cfg(feature = "tracing")]
278		{
279			match use_best_block {
280				true => {
281					let info = client.best().block_info().await?;
282					info!(target: "lib", "Nonce: {} Account address: {} Current Best Height: {} Mortality End Height: {}", nonce, account_id, info.height, mortality_ends_height);
283				},
284				false => {
285					let info = client.finalized().block_info().await?;
286					info!(target: "lib", "Nonce: {} Account address: {} Current Finalized Height: {} Mortality End Height: {}", nonce, account_id, info.height, mortality_ends_height);
287				},
288			};
289		}
290
291		while mortality_ends_height >= current_block_height {
292			let info = sub.next().await?;
293			current_block_height = info.height;
294
295			let state_nonce = client.chain().block_nonce(account_id.clone(), info.hash).await?;
296			if state_nonce > nonce {
297				trace_new_block(nonce, state_nonce, account_id, info, true);
298				return Ok(Some(info));
299			}
300			if state_nonce == 0 {
301				let block = Block::new(client.clone(), info.hash);
302				let opts = ExtrinsicOpts::new().filter(tx_hash).encode_as(EncodeSelector::None);
303				let ext = block.extrinsic_infos(opts).await?;
304				if !ext.is_empty() {
305					trace_new_block(nonce, state_nonce, account_id, info, true);
306					return Ok(Some(info));
307				}
308			}
309
310			trace_new_block(nonce, state_nonce, account_id, info, false);
311		}
312
313		Ok(None)
314	}
315}
316
317/// Emits optional tracing output detailing nonce progression while searching for a transaction.
318///
319/// When the `tracing` feature is disabled this function does nothing; otherwise it records each
320/// inspected block along with whether the search completed.
321fn trace_new_block(nonce: u32, state_nonce: u32, account_id: &AccountId, block_info: BlockInfo, search_done: bool) {
322	#[cfg(feature = "tracing")]
323	{
324		if search_done {
325			info!(target: "lib", "Account ({}, {}). At block ({}, {:?}) found nonce: {}. Search is done", nonce, account_id, block_info.height, block_info.hash, state_nonce);
326		} else {
327			info!(target: "lib", "Account ({}, {}). At block ({}, {:?}) found nonce: {}.", nonce, account_id, block_info.height, block_info.hash, state_nonce);
328		}
329	}
330
331	#[cfg(not(feature = "tracing"))]
332	{
333		let _ = (nonce, state_nonce, account_id, block_info, search_done);
334	}
335}