avail_rust_client/
block_api.rs

1//! Convenience helpers for inspecting block data, extrinsics, and events via RPC.
2
3use crate::{Client, Error, UserError};
4use avail_rust_core::{
5	EncodeSelector, Extrinsic, ExtrinsicSignature, H256, HasHeader, HashNumber, MultiAddress, RpcError,
6	TransactionEventDecodable, avail,
7	grandpa::GrandpaJustification,
8	rpc::{self, ExtrinsicFilter, SignerPayload},
9	types::HashStringNumber,
10};
11use codec::Decode;
12
13/// High-level handle bound to a specific block id (height or hash).
14pub struct BlockApi {
15	client: Client,
16	block_id: HashStringNumber,
17	retry_on_error: Option<bool>,
18}
19
20impl BlockApi {
21	/// Creates a block helper for the given height or hash.
22	///
23	/// No network calls are issued up front; the `block_id` is stored for later RPC queries. Use the
24	/// view helpers such as [`BlockApi::tx`] or [`BlockApi::events`] to fetch concrete data.
25	pub fn new(client: Client, block_id: impl Into<HashStringNumber>) -> Self {
26		BlockApi { client, block_id: block_id.into(), retry_on_error: None }
27	}
28
29	/// Returns a view that focuses on decoded transactions.
30	///
31	/// The returned [`BlockWithTx`] shares this helper's retry configuration.
32	pub fn tx(&self) -> BlockWithTx {
33		BlockWithTx::new(self.client.clone(), self.block_id.clone())
34	}
35
36	/// Returns a view that focuses on decoded extrinsics while retaining signature metadata.
37	pub fn ext(&self) -> BlockWithExt {
38		BlockWithExt::new(self.client.clone(), self.block_id.clone())
39	}
40
41	/// Returns a view that keeps extrinsics as raw bytes and optional signer payload information.
42	pub fn raw_ext(&self) -> BlockWithRawExt {
43		BlockWithRawExt::new(self.client.clone(), self.block_id.clone())
44	}
45
46	/// Returns a helper for fetching events from this block.
47	pub fn events(&self) -> BlockEvents {
48		BlockEvents::new(self.client.clone(), self.block_id.clone())
49	}
50
51	/// Controls retry behaviour for follow-up RPC calls made through this block helper.
52	///
53	/// - `Some(true)` forces retries regardless of the client's global setting.
54	/// - `Some(false)` disables retries entirely.
55	/// - `None` keeps the client's default configuration.
56	pub fn set_retry_on_error(&mut self, value: Option<bool>) {
57		self.retry_on_error = value;
58	}
59
60	/// Fetches the GRANDPA justification for this block when available.
61	///
62	/// # Returns
63	/// - `Ok(Some(GrandpaJustification))` when the runtime provides a justification.
64	/// - `Ok(None)` when no justification exists for the requested block.
65	/// - `Err(Error)` if the RPC layer fails or the supplied block id cannot be resolved.
66	pub async fn justification(&self) -> Result<Option<GrandpaJustification>, Error> {
67		let block_id: HashNumber = self.block_id.clone().try_into().map_err(UserError::Decoding)?;
68		let at = match block_id {
69			HashNumber::Hash(h) => self
70				.client
71				.chain()
72				.retry_on(self.retry_on_error, None)
73				.block_height(h)
74				.await?
75				.ok_or(Error::Other("Failed to find block from the provided hash".into()))?,
76			HashNumber::Number(n) => n,
77		};
78
79		self.client
80			.chain()
81			.retry_on(self.retry_on_error, None)
82			.grandpa_block_justification(at)
83			.await
84			.map_err(|e| e.into())
85	}
86
87	/// Returns true when this block view will retry failed RPC calls.
88	pub fn should_retry_on_error(&self) -> bool {
89		self.retry_on_error
90			.unwrap_or_else(|| self.client.is_global_retries_enabled())
91	}
92}
93
94/// View of block extrinsics as raw payloads with associated metadata.
95pub struct BlockWithRawExt {
96	client: Client,
97	block_id: HashStringNumber,
98	retry_on_error: Option<bool>,
99}
100
101impl BlockWithRawExt {
102	/// Builds a raw extrinsic view for the given block.
103	///
104	/// The `block_id` may be a height or hash; conversions happen lazily when RPCs are executed.
105	pub fn new(client: Client, block_id: impl Into<HashStringNumber>) -> Self {
106		Self { client, block_id: block_id.into(), retry_on_error: None }
107	}
108
109	/// Finds a specific extrinsic and returns it in the requested format.
110	///
111	/// # Returns
112	/// - `Ok(Some(BlockRawExtrinsic))` when the extrinsic is found.
113	/// - `Ok(None)` when no extrinsic matches the provided identifier.
114	/// - `Err(Error)` when decoding the identifier fails or the RPC call errors.
115	pub async fn get(
116		&self,
117		extrinsic_id: impl Into<HashStringNumber>,
118		encode_as: EncodeSelector,
119	) -> Result<Option<BlockRawExtrinsic>, Error> {
120		async fn inner(
121			s: &BlockWithRawExt,
122			extrinsic_id: HashStringNumber,
123			encode_as: EncodeSelector,
124		) -> Result<Option<BlockRawExtrinsic>, Error> {
125			let filter = match extrinsic_id {
126				HashStringNumber::Hash(x) => ExtrinsicFilter::from(x),
127				HashStringNumber::String(x) => ExtrinsicFilter::try_from(x).map_err(UserError::Decoding)?,
128				HashStringNumber::Number(x) => ExtrinsicFilter::from(x),
129			};
130			let opts = BlockExtOptionsExpanded {
131				filter: Some(filter),
132				encode_as: Some(encode_as),
133				..Default::default()
134			};
135
136			s.first(opts).await
137		}
138
139		inner(self, extrinsic_id.into(), encode_as).await
140	}
141
142	/// Returns the first matching extrinsic, if any.
143	///
144	/// # Returns
145	/// - `Ok(Some(BlockRawExtrinsic))` with metadata and optional payload.
146	/// - `Ok(None)` when nothing matches the provided filters.
147	/// - `Err(Error)` on RPC failures or when the block identifier cannot be decoded.
148	pub async fn first(&self, mut opts: BlockExtOptionsExpanded) -> Result<Option<BlockRawExtrinsic>, Error> {
149		if opts.encode_as.is_none() {
150			opts.encode_as = Some(EncodeSelector::Extrinsic)
151		}
152
153		let block_id: HashNumber = self.block_id.clone().try_into().map_err(UserError::Decoding)?;
154		let mut result = self
155			.client
156			.chain()
157			.retry_on(self.retry_on_error, None)
158			.system_fetch_extrinsics(block_id, opts.into())
159			.await?;
160
161		let Some(first) = result.first_mut() else {
162			return Ok(None);
163		};
164
165		let metadata =
166			BlockExtrinsicMetadata::new(first.ext_hash, first.ext_index, first.pallet_id, first.variant_id, block_id);
167		let ext = BlockRawExtrinsic::new(first.data.take(), metadata, first.signer_payload.take());
168
169		Ok(Some(ext))
170	}
171
172	/// Returns the last matching extrinsic, if any.
173	///
174	/// Return semantics mirror [`BlockWithRawExt::first`], but the final matching element is returned.
175	pub async fn last(&self, mut opts: BlockExtOptionsExpanded) -> Result<Option<BlockRawExtrinsic>, Error> {
176		if opts.encode_as.is_none() {
177			opts.encode_as = Some(EncodeSelector::Extrinsic)
178		}
179
180		let block_id: HashNumber = self.block_id.clone().try_into().map_err(UserError::Decoding)?;
181		let mut result = self
182			.client
183			.chain()
184			.retry_on(self.retry_on_error, None)
185			.system_fetch_extrinsics(block_id, opts.into())
186			.await?;
187
188		let Some(last) = result.last_mut() else {
189			return Ok(None);
190		};
191
192		let metadata =
193			BlockExtrinsicMetadata::new(last.ext_hash, last.ext_index, last.pallet_id, last.variant_id, block_id);
194		let ext = BlockRawExtrinsic::new(last.data.take(), metadata, last.signer_payload.take());
195
196		Ok(Some(ext))
197	}
198
199	/// Returns all matching extrinsics.
200	///
201	/// The resulting vector may be empty when no extrinsics satisfy the filters.
202	pub async fn all(&self, mut opts: BlockExtOptionsExpanded) -> Result<Vec<BlockRawExtrinsic>, Error> {
203		if opts.encode_as.is_none() {
204			opts.encode_as = Some(EncodeSelector::Extrinsic)
205		}
206
207		let block_id: HashNumber = self.block_id.clone().try_into().map_err(UserError::Decoding)?;
208		let result = self
209			.client
210			.chain()
211			.retry_on(self.retry_on_error, None)
212			.system_fetch_extrinsics(block_id, opts.into())
213			.await?;
214
215		let result = result
216			.into_iter()
217			.map(|x| {
218				let metadata =
219					BlockExtrinsicMetadata::new(x.ext_hash, x.ext_index, x.pallet_id, x.variant_id, block_id);
220				BlockRawExtrinsic::new(x.data, metadata, x.signer_payload)
221			})
222			.collect();
223
224		Ok(result)
225	}
226
227	/// Counts matching extrinsics without fetching the payloads.
228	///
229	/// Equivalent to `self.all(opts).await.map(|v| v.len())` but avoids transferring payload bytes.
230	pub async fn count(&self, mut opts: BlockExtOptionsExpanded) -> Result<usize, Error> {
231		opts.encode_as = Some(EncodeSelector::None);
232
233		let result = self.all(opts).await?;
234		Ok(result.len())
235	}
236
237	/// Checks whether at least one extrinsic matches.
238	pub async fn exists(&self, mut opts: BlockExtOptionsExpanded) -> Result<bool, Error> {
239		opts.encode_as = Some(EncodeSelector::None);
240
241		let result = self.first(opts).await?;
242		Ok(result.is_some())
243	}
244
245	/// Controls retry behaviour for fetching raw extrinsics: `Some(true)` forces retries,
246	/// `Some(false)` disables them, and `None` keeps the client's default.
247	pub fn set_retry_on_error(&mut self, value: Option<bool>) {
248		self.retry_on_error = value;
249	}
250
251	/// Returns true when raw extrinsic lookups retry after RPC errors.
252	pub fn should_retry_on_error(&self) -> bool {
253		self.retry_on_error
254			.unwrap_or_else(|| self.client.is_global_retries_enabled())
255	}
256}
257
258/// View of block extrinsics decoded into calls and optional signatures.
259pub struct BlockWithExt {
260	rxt: BlockWithRawExt,
261}
262
263impl BlockWithExt {
264	/// Builds a decoded extrinsic view for the given block.
265	///
266	/// Decoding happens lazily as individual queries are made.
267	pub fn new(client: Client, block_id: impl Into<HashStringNumber>) -> Self {
268		Self { rxt: BlockWithRawExt::new(client, block_id) }
269	}
270
271	/// Fetches a specific extrinsic by id.
272	///
273	/// # Returns
274	/// - `Ok(Some(BlockExtrinsic<T>))` when the extrinsic exists and decodes as `T`.
275	/// - `Ok(None)` when no extrinsic matches the identifier or filters.
276	/// - `Err(Error)` if the RPC call fails or decoding the identifier/payload fails.
277	pub async fn get<T: HasHeader + Decode>(
278		&self,
279		extrinsic_id: impl Into<HashStringNumber>,
280	) -> Result<Option<BlockExtrinsic<T>>, Error> {
281		async fn inner<T: HasHeader + Decode>(
282			s: &BlockWithExt,
283			extrinsic_id: HashStringNumber,
284		) -> Result<Option<BlockExtrinsic<T>>, Error> {
285			let filter = match extrinsic_id {
286				HashStringNumber::Hash(x) => ExtrinsicFilter::from(x),
287				HashStringNumber::String(x) => ExtrinsicFilter::try_from(x).map_err(UserError::Decoding)?,
288				HashStringNumber::Number(x) => ExtrinsicFilter::from(x),
289			};
290			let filter = Some(filter);
291			s.first::<T>(BlockExtOptionsSimple { filter, ..Default::default() })
292				.await
293		}
294
295		inner::<T>(self, extrinsic_id.into()).await
296	}
297
298	/// Returns the first matching extrinsic decoded into the target type.
299	///
300	/// # Returns
301	/// - `Ok(Some(BlockExtrinsic<T>))` when an extrinsic matches the filters.
302	/// - `Ok(None)` when nothing matches.
303	/// - `Err(Error)` if RPC retrieval fails or decoding the extrinsic as `T` fails.
304	pub async fn first<T: HasHeader + Decode>(
305		&self,
306		opts: BlockExtOptionsSimple,
307	) -> Result<Option<BlockExtrinsic<T>>, Error> {
308		let mut opts: BlockExtOptionsExpanded = opts.into();
309		if opts.filter.is_none() {
310			opts.filter = Some(T::HEADER_INDEX.into())
311		}
312
313		let first = self.rxt.first(opts).await?;
314		let Some(first) = first else {
315			return Ok(None);
316		};
317
318		let Some(data) = first.data else {
319			return Err(RpcError::ExpectedData("Fetched raw extrinsic had no data.".into()).into());
320		};
321
322		let ext = Extrinsic::<T>::try_from(data.as_str()).map_err(UserError::Decoding)?;
323		let ext = BlockExtrinsic::new(ext.signature, ext.call, first.metadata);
324
325		Ok(Some(ext))
326	}
327
328	/// Returns the last matching extrinsic decoded into the target type.
329	///
330	/// Return semantics mirror [`BlockWithExt::first`], but the final matching extrinsic is returned.
331	pub async fn last<T: HasHeader + Decode>(
332		&self,
333		opts: BlockExtOptionsSimple,
334	) -> Result<Option<BlockExtrinsic<T>>, Error> {
335		let mut opts: BlockExtOptionsExpanded = opts.into();
336		if opts.filter.is_none() {
337			opts.filter = Some(T::HEADER_INDEX.into())
338		}
339
340		let last = self.rxt.last(opts).await?;
341		let Some(last) = last else {
342			return Ok(None);
343		};
344
345		let Some(data) = last.data else {
346			return Err(RpcError::ExpectedData("Fetched raw extrinsic had no data.".into()).into());
347		};
348
349		let ext = Extrinsic::<T>::try_from(data.as_str()).map_err(UserError::Decoding)?;
350		let ext = BlockExtrinsic::new(ext.signature, ext.call, last.metadata);
351		Ok(Some(ext))
352	}
353
354	/// Returns every matching extrinsic decoded into the target type.
355	///
356	/// The result may be empty if no extrinsics match. Decoding failures are surfaced as `Err(Error)`.
357	pub async fn all<T: HasHeader + Decode>(
358		&self,
359		opts: BlockExtOptionsSimple,
360	) -> Result<Vec<BlockExtrinsic<T>>, Error> {
361		let mut opts: BlockExtOptionsExpanded = opts.into();
362		if opts.filter.is_none() {
363			opts.filter = Some(T::HEADER_INDEX.into())
364		}
365
366		let all = self.rxt.all(opts).await?;
367		let mut result = Vec::with_capacity(all.len());
368		for raw_ext in all {
369			let Some(data) = raw_ext.data else {
370				return Err(RpcError::ExpectedData("Fetched raw extrinsic had no data.".into()).into());
371			};
372			let ext = Extrinsic::<T>::try_from(data.as_str()).map_err(UserError::Decoding)?;
373			let ext = BlockExtrinsic::new(ext.signature, ext.call, raw_ext.metadata);
374			result.push(ext);
375		}
376
377		Ok(result)
378	}
379
380	/// Counts matching extrinsics without decoding the payloads.
381	///
382	/// This still performs an RPC round-trip but avoids transferring the encoded call data.
383	pub async fn count<T: HasHeader>(&self, opts: BlockExtOptionsSimple) -> Result<usize, Error> {
384		let mut opts: BlockExtOptionsExpanded = opts.into();
385		opts.encode_as = Some(EncodeSelector::None);
386		if opts.filter.is_none() {
387			opts.filter = Some(T::HEADER_INDEX.into())
388		}
389
390		return self.rxt.count(opts).await;
391	}
392
393	/// Checks whether any extrinsic matches the filters.
394	///
395	/// Equivalent to calling [`BlockWithExt::first`] and testing the result for `Some`.
396	pub async fn exists<T: HasHeader>(&self, opts: BlockExtOptionsSimple) -> Result<bool, Error> {
397		let mut opts: BlockExtOptionsExpanded = opts.into();
398		opts.encode_as = Some(EncodeSelector::None);
399		if opts.filter.is_none() {
400			opts.filter = Some(T::HEADER_INDEX.into())
401		}
402
403		return self.rxt.exists(opts).await;
404	}
405
406	/// Controls retry behaviour for decoded-extrinsic lookups: `Some(true)` forces retries,
407	/// `Some(false)` disables them, and `None` keeps the client's default.
408	pub fn set_retry_on_error(&mut self, value: Option<bool>) {
409		self.rxt.set_retry_on_error(value);
410	}
411
412	/// Returns true when decoded extrinsic lookups retry after RPC errors.
413	pub fn should_retry_on_error(&self) -> bool {
414		self.rxt.should_retry_on_error()
415	}
416}
417
418/// View of block extrinsics restricted to signed transactions.
419pub struct BlockWithTx {
420	ext: BlockWithExt,
421}
422
423impl BlockWithTx {
424	/// Builds a signed transaction view for the given block.
425	///
426	/// Only signed extrinsics will be surfaced; unsigned extrinsics produce an `Error` when decoding.
427	pub fn new(client: Client, block_id: impl Into<HashStringNumber>) -> Self {
428		Self { ext: BlockWithExt::new(client, block_id) }
429	}
430
431	/// Fetches a signed transaction by id.
432	///
433	/// # Returns
434	/// - `Ok(Some(BlockTransaction<T>))` when the extrinsic exists and carries a signature.
435	/// - `Ok(None)` when no extrinsic matches the identifier.
436	/// - `Err(Error)` when the extrinsic exists but is unsigned or cannot be decoded as `T`.
437	pub async fn get<T: HasHeader + Decode>(
438		&self,
439		extrinsic_id: impl Into<HashStringNumber>,
440	) -> Result<Option<BlockTransaction<T>>, Error> {
441		let ext = self.ext.get(extrinsic_id).await?;
442		let Some(ext) = ext else {
443			return Ok(None);
444		};
445
446		let Some(signature) = ext.signature else {
447			return Err(
448				UserError::Other("Extrinsic is unsigned; cannot decode it as a signed transaction.".into()).into()
449			);
450		};
451
452		let ext = BlockTransaction::new(signature, ext.call, ext.metadata);
453		Ok(Some(ext))
454	}
455
456	/// Returns the first matching signed transaction.
457	///
458	/// Unsigned extrinsics encountered during decoding produce an `Error`.
459	pub async fn first<T: HasHeader + Decode>(
460		&self,
461		opts: BlockExtOptionsSimple,
462	) -> Result<Option<BlockTransaction<T>>, Error> {
463		let ext = self.ext.first(opts).await?;
464		let Some(ext) = ext else {
465			return Ok(None);
466		};
467
468		let Some(signature) = ext.signature else {
469			return Err(
470				UserError::Other("Extrinsic is unsigned; cannot decode it as a signed transaction.".into()).into()
471			);
472		};
473
474		let ext = BlockTransaction::new(signature, ext.call, ext.metadata);
475		Ok(Some(ext))
476	}
477
478	/// Returns the last matching signed transaction.
479	///
480	/// Return semantics mirror [`BlockWithTx::first`], but returns the final matching transaction.
481	pub async fn last<T: HasHeader + Decode>(
482		&self,
483		opts: BlockExtOptionsSimple,
484	) -> Result<Option<BlockTransaction<T>>, Error> {
485		let ext = self.ext.last(opts).await?;
486		let Some(ext) = ext else {
487			return Ok(None);
488		};
489
490		let Some(signature) = ext.signature else {
491			return Err(
492				UserError::Other("Extrinsic is unsigned; cannot decode it as a signed transaction.".into()).into()
493			);
494		};
495
496		let ext = BlockTransaction::new(signature, ext.call, ext.metadata);
497		Ok(Some(ext))
498	}
499
500	/// Returns every matching signed transaction.
501	///
502	/// Decoding stops early with an `Error` if any extrinsic lacks a signature or fails to decode as `T`.
503	pub async fn all<T: HasHeader + Decode>(
504		&self,
505		opts: BlockExtOptionsSimple,
506	) -> Result<Vec<BlockTransaction<T>>, Error> {
507		let all = self.ext.all::<T>(opts).await?;
508		let mut result = Vec::with_capacity(all.len());
509		for ext in all {
510			let Some(signature) = ext.signature else {
511				return Err(UserError::Other(
512					"Extrinsic is unsigned; cannot decode it as a signed transaction.".into(),
513				)
514				.into());
515			};
516			result.push(BlockTransaction::new(signature, ext.call, ext.metadata));
517		}
518
519		Ok(result)
520	}
521
522	/// Counts matching signed transactions.
523	pub async fn count<T: HasHeader>(&self, opts: BlockExtOptionsSimple) -> Result<usize, Error> {
524		self.ext.count::<T>(opts).await
525	}
526
527	/// Checks whether any signed transaction matches the filters.
528	pub async fn exists<T: HasHeader>(&self, opts: BlockExtOptionsSimple) -> Result<bool, Error> {
529		self.ext.exists::<T>(opts).await
530	}
531
532	/// Controls retry behaviour for signed-transaction lookups: `Some(true)` forces retries,
533	/// `Some(false)` disables them, and `None` keeps the client's default.
534	pub fn set_retry_on_error(&mut self, value: Option<bool>) {
535		self.ext.set_retry_on_error(value);
536	}
537
538	/// Returns true when signed transaction lookups retry after RPC errors.
539	pub fn should_retry_on_error(&self) -> bool {
540		self.ext.should_retry_on_error()
541	}
542}
543
544/// View that fetches events emitted by a block, optionally filtered by extrinsic.
545pub struct BlockEvents {
546	client: Client,
547	block_id: HashStringNumber,
548	retry_on_error: Option<bool>,
549}
550
551impl BlockEvents {
552	/// Creates an event view for the given block.
553	///
554	/// No RPC calls are made until [`BlockEvents::ext`] or [`BlockEvents::block`] is awaited.
555	pub fn new(client: Client, block_id: impl Into<HashStringNumber>) -> Self {
556		BlockEvents { client, block_id: block_id.into(), retry_on_error: None }
557	}
558
559	/// Returns events emitted by a specific extrinsic index.
560	///
561	/// # Returns
562	/// - `Ok(Some(ExtrinsicEvents))` when events exist for the given index.
563	/// - `Ok(None)` when the block contains no events at that index.
564	/// - `Err(Error)` when fetching or decoding event data fails.
565	pub async fn ext(&self, tx_index: u32) -> Result<Option<ExtrinsicEvents>, Error> {
566		let mut events = self
567			.block(BlockEventsOptions {
568				filter: Some(tx_index.into()),
569				enable_encoding: Some(true),
570				enable_decoding: Some(false),
571			})
572			.await?;
573
574		let Some(first) = events.first_mut() else {
575			return Ok(None);
576		};
577
578		let mut result: Vec<ExtrinsicEvent> = Vec::with_capacity(first.events.len());
579		for phase_event in &mut first.events {
580			let Some(data) = phase_event.encoded_data.take() else {
581				return Err(
582					RpcError::ExpectedData("The node did not return encoded data for this event.".into()).into()
583				);
584			};
585
586			let ext_event = ExtrinsicEvent {
587				index: phase_event.index,
588				pallet_id: phase_event.pallet_id,
589				variant_id: phase_event.variant_id,
590				data,
591			};
592			result.push(ext_event);
593		}
594
595		Ok(Some(ExtrinsicEvents::new(result)))
596	}
597
598	/// Fetches events for the block using the given options.
599	///
600	/// By default encoding is enabled; callers can override this via `opts`.
601	pub async fn block(&self, mut opts: BlockEventsOptions) -> Result<Vec<rpc::BlockPhaseEvent>, Error> {
602		if opts.enable_encoding.is_none() {
603			opts.enable_encoding = Some(true);
604		}
605
606		self.client
607			.chain()
608			.retry_on(self.retry_on_error, None)
609			.system_fetch_events(self.block_id.clone(), opts.into())
610			.await
611	}
612
613	/// Controls retry behaviour for event lookups: `Some(true)` forces retries, `Some(false)` disables
614	/// them, and `None` keeps the client's default.
615	pub fn set_retry_on_error(&mut self, value: Option<bool>) {
616		self.retry_on_error = value;
617	}
618
619	/// Returns true when event queries retry after RPC errors.
620	pub fn should_retry_on_error(&self) -> bool {
621		self.retry_on_error
622			.unwrap_or_else(|| self.client.is_global_retries_enabled())
623	}
624}
625
626#[derive(Debug, Default, Clone)]
627pub struct BlockEventsOptions {
628	filter: Option<rpc::EventFilter>,
629	enable_encoding: Option<bool>,
630	enable_decoding: Option<bool>,
631}
632
633impl From<BlockEventsOptions> for rpc::EventOpts {
634	fn from(value: BlockEventsOptions) -> Self {
635		rpc::EventOpts {
636			filter: value.filter,
637			enable_encoding: value.enable_encoding,
638			enable_decoding: value.enable_decoding,
639		}
640	}
641}
642
643#[derive(Debug, Default, Clone)]
644pub struct BlockExtOptionsSimple {
645	pub filter: Option<ExtrinsicFilter>,
646	pub ss58_address: Option<String>,
647	pub app_id: Option<u32>,
648	pub nonce: Option<u32>,
649}
650
651#[derive(Debug, Default, Clone)]
652pub struct BlockExtOptionsExpanded {
653	pub filter: Option<ExtrinsicFilter>,
654	pub ss58_address: Option<String>,
655	pub app_id: Option<u32>,
656	pub nonce: Option<u32>,
657	pub encode_as: Option<EncodeSelector>,
658}
659
660impl From<BlockExtOptionsExpanded> for rpc::ExtrinsicOpts {
661	fn from(value: BlockExtOptionsExpanded) -> Self {
662		rpc::ExtrinsicOpts {
663			transaction_filter: value.filter.unwrap_or_default(),
664			ss58_address: value.ss58_address,
665			app_id: value.app_id,
666			nonce: value.nonce,
667			encode_as: value.encode_as.unwrap_or_default(),
668		}
669	}
670}
671
672impl From<BlockExtOptionsSimple> for BlockExtOptionsExpanded {
673	fn from(value: BlockExtOptionsSimple) -> Self {
674		Self {
675			filter: value.filter,
676			ss58_address: value.ss58_address,
677			app_id: value.app_id,
678			nonce: value.nonce,
679			encode_as: Some(EncodeSelector::Extrinsic),
680		}
681	}
682}
683
684#[derive(Debug, Clone)]
685pub struct BlockExtrinsicMetadata {
686	pub ext_hash: H256,
687	pub ext_index: u32,
688	pub pallet_id: u8,
689	pub variant_id: u8,
690	pub block_id: HashNumber,
691}
692
693impl BlockExtrinsicMetadata {
694	/// Wraps metadata about an extrinsic inside a block.
695	pub fn new(ext_hash: H256, ext_index: u32, pallet_id: u8, variant_id: u8, block_id: HashNumber) -> Self {
696		Self { ext_hash, ext_index, pallet_id, variant_id, block_id }
697	}
698}
699
700#[derive(Debug, Clone)]
701pub struct BlockRawExtrinsic {
702	pub data: Option<String>,
703	pub metadata: BlockExtrinsicMetadata,
704	pub signer_payload: Option<SignerPayload>,
705}
706
707impl BlockRawExtrinsic {
708	/// Creates a raw extrinsic wrapper.
709	pub fn new(data: Option<String>, metadata: BlockExtrinsicMetadata, signer_payload: Option<SignerPayload>) -> Self {
710		Self { data, metadata, signer_payload }
711	}
712
713	/// Fetches events emitted by this extrinsic.
714	///
715	/// # Returns
716	/// - `Ok(ExtrinsicEvents)` when the block exposes matching events.
717	/// - `Err(Error)` when the extrinsic emitted no events or the RPC layer fails.
718	pub async fn events(&self, client: Client) -> Result<ExtrinsicEvents, Error> {
719		let events = BlockEvents::new(client, self.metadata.block_id)
720			.ext(self.ext_index())
721			.await?;
722		let Some(events) = events else {
723			return Err(RpcError::ExpectedData("No events found for the requested extrinsic.".into()).into());
724		};
725
726		Ok(events)
727	}
728
729	/// Returns the index of this extrinsic inside the block.
730	pub fn ext_index(&self) -> u32 {
731		self.metadata.ext_index
732	}
733
734	/// Returns the extrinsic hash.
735	pub fn ext_hash(&self) -> H256 {
736		self.metadata.ext_hash
737	}
738
739	/// Returns the application id if the signer payload provided it.
740	pub fn app_id(&self) -> Option<u32> {
741		Some(self.signer_payload.as_ref()?.app_id)
742	}
743
744	/// Returns the nonce if the signer payload provided it.
745	pub fn nonce(&self) -> Option<u32> {
746		Some(self.signer_payload.as_ref()?.nonce)
747	}
748
749	/// Returns the ss58 address if the signer payload provided it.
750	pub fn ss58_address(&self) -> Option<String> {
751		self.signer_payload.as_ref()?.ss58_address.clone()
752	}
753}
754
755/// Decoded extrinsic along with metadata and optional signature.
756#[derive(Debug, Clone)]
757pub struct BlockExtrinsic<T: HasHeader + Decode> {
758	pub signature: Option<ExtrinsicSignature>,
759	pub call: T,
760	pub metadata: BlockExtrinsicMetadata,
761}
762
763impl<T: HasHeader + Decode> BlockExtrinsic<T> {
764	/// Creates an extrinsic wrapper from decoded data.
765	pub fn new(signature: Option<ExtrinsicSignature>, call: T, metadata: BlockExtrinsicMetadata) -> Self {
766		Self { signature, call, metadata }
767	}
768
769	/// Fetches events emitted by this extrinsic.
770	pub async fn events(&self, client: Client) -> Result<ExtrinsicEvents, Error> {
771		let events = BlockEvents::new(client, self.metadata.block_id)
772			.ext(self.ext_index())
773			.await?;
774		let Some(events) = events else {
775			return Err(RpcError::ExpectedData("No events found for extrinsic".into()).into());
776		};
777
778		Ok(events)
779	}
780
781	/// Returns the index of this extrinsic inside the block.
782	pub fn ext_index(&self) -> u32 {
783		self.metadata.ext_index
784	}
785
786	/// Returns the extrinsic hash.
787	pub fn ext_hash(&self) -> H256 {
788		self.metadata.ext_hash
789	}
790
791	/// Returns the application id if the extrinsic was signed.
792	pub fn app_id(&self) -> Option<u32> {
793		Some(self.signature.as_ref()?.tx_extra.app_id)
794	}
795
796	/// Returns the nonce if the extrinsic was signed.
797	pub fn nonce(&self) -> Option<u32> {
798		Some(self.signature.as_ref()?.tx_extra.nonce)
799	}
800
801	/// Returns the tip if the extrinsic was signed.
802	pub fn tip(&self) -> Option<u128> {
803		Some(self.signature.as_ref()?.tx_extra.tip)
804	}
805
806	/// Returns the signer as an ss58 string when available.
807	pub fn ss58_address(&self) -> Option<String> {
808		match &self.signature.as_ref()?.address {
809			MultiAddress::Id(x) => Some(std::format!("{}", x)),
810			_ => None,
811		}
812	}
813}
814
815impl<T: HasHeader + Decode> TryFrom<BlockRawExtrinsic> for BlockExtrinsic<T> {
816	type Error = String;
817
818	fn try_from(value: BlockRawExtrinsic) -> Result<Self, Self::Error> {
819		let Some(data) = &value.data else {
820			return Err("Encoded extrinsic payload is missing from the RPC response.")?;
821		};
822
823		let extrinsic = Extrinsic::<T>::try_from(data.as_str())?;
824		Ok(Self::new(extrinsic.signature, extrinsic.call, value.metadata))
825	}
826}
827
828/// Block Transaction is the same as Block Signed Extrinsic
829#[derive(Debug, Clone)]
830pub struct BlockTransaction<T: HasHeader + Decode> {
831	pub signature: ExtrinsicSignature,
832	pub call: T,
833	pub metadata: BlockExtrinsicMetadata,
834}
835
836impl<T: HasHeader + Decode> BlockTransaction<T> {
837	/// Creates a transaction wrapper from decoded data.
838	pub fn new(signature: ExtrinsicSignature, call: T, metadata: BlockExtrinsicMetadata) -> Self {
839		Self { signature, call, metadata }
840	}
841
842	/// Fetches events emitted by this transaction.
843	pub async fn events(&self, client: Client) -> Result<ExtrinsicEvents, Error> {
844		let events = BlockEvents::new(client, self.metadata.block_id)
845			.ext(self.ext_index())
846			.await?;
847		let Some(events) = events else {
848			return Err(RpcError::ExpectedData("No events found for the requested extrinsic.".into()).into());
849		};
850
851		Ok(events)
852	}
853
854	/// Returns the index of this transaction inside the block.
855	pub fn ext_index(&self) -> u32 {
856		self.metadata.ext_index
857	}
858
859	/// Returns the transaction hash.
860	pub fn ext_hash(&self) -> H256 {
861		self.metadata.ext_hash
862	}
863
864	/// Returns the application id for this transaction.
865	pub fn app_id(&self) -> u32 {
866		self.signature.tx_extra.app_id
867	}
868
869	/// Returns the signer nonce for this transaction.
870	pub fn nonce(&self) -> u32 {
871		self.signature.tx_extra.nonce
872	}
873
874	/// Returns the paid tip for this transaction.
875	pub fn tip(&self) -> u128 {
876		self.signature.tx_extra.tip
877	}
878
879	/// Returns the signer as an ss58 string when available.
880	pub fn ss58_address(&self) -> Option<String> {
881		match &self.signature.address {
882			MultiAddress::Id(x) => Some(std::format!("{}", x)),
883			_ => None,
884		}
885	}
886}
887
888impl<T: HasHeader + Decode> TryFrom<BlockExtrinsic<T>> for BlockTransaction<T> {
889	type Error = String;
890
891	fn try_from(value: BlockExtrinsic<T>) -> Result<Self, Self::Error> {
892		let Some(signature) = value.signature else {
893			return Err("Extrinsic is unsigned; expected a signature.")?;
894		};
895
896		Ok(Self::new(signature, value.call, value.metadata))
897	}
898}
899
900impl<T: HasHeader + Decode> TryFrom<BlockRawExtrinsic> for BlockTransaction<T> {
901	type Error = String;
902
903	fn try_from(value: BlockRawExtrinsic) -> Result<Self, Self::Error> {
904		let ext = BlockExtrinsic::try_from(value)?;
905		Self::try_from(ext)
906	}
907}
908
909#[derive(Debug, Clone)]
910pub struct ExtrinsicEvent {
911	pub index: u32,
912	pub pallet_id: u8,
913	pub variant_id: u8,
914	pub data: String,
915}
916
917#[derive(Debug, Clone)]
918pub struct ExtrinsicEvents {
919	pub events: Vec<ExtrinsicEvent>,
920}
921
922impl ExtrinsicEvents {
923	/// Wraps decoded events.
924	pub fn new(events: Vec<ExtrinsicEvent>) -> Self {
925		Self { events }
926	}
927
928	/// Returns the first event matching the requested type.
929	pub fn first<T: HasHeader + codec::Decode>(&self) -> Option<T> {
930		let event = self
931			.events
932			.iter()
933			.find(|x| x.pallet_id == T::HEADER_INDEX.0 && x.variant_id == T::HEADER_INDEX.1);
934		let event = event?;
935
936		T::from_event(&event.data).ok()
937	}
938
939	/// Returns every event matching the requested type.
940	pub fn all<T: HasHeader + codec::Decode>(&self) -> Result<Vec<T>, String> {
941		let mut result = Vec::new();
942		for event in &self.events {
943			if event.pallet_id != T::HEADER_INDEX.0 || event.variant_id != T::HEADER_INDEX.1 {
944				continue;
945			}
946
947			let decoded = T::from_event(event.data.as_str())?;
948			result.push(decoded);
949		}
950
951		Ok(result)
952	}
953
954	/// Checks if an `ExtrinsicSuccess` event exists.
955	pub fn is_extrinsic_success_present(&self) -> bool {
956		self.is_present::<avail::system::events::ExtrinsicSuccess>()
957	}
958
959	/// Checks if an `ExtrinsicFailed` event exists.
960	pub fn is_extrinsic_failed_present(&self) -> bool {
961		self.is_present::<avail::system::events::ExtrinsicFailed>()
962	}
963
964	/// Returns whether a proxy call succeeded, when present.
965	pub fn proxy_executed_successfully(&self) -> Option<bool> {
966		let executed = self.first::<avail::proxy::events::ProxyExecuted>()?;
967		Some(executed.result.is_ok())
968	}
969
970	/// Returns whether a multisig call succeeded, when present.
971	pub fn multisig_executed_successfully(&self) -> Option<bool> {
972		let executed = self.first::<avail::multisig::events::MultisigExecuted>()?;
973		Some(executed.result.is_ok())
974	}
975
976	/// Returns true when at least one event of the given type exists.
977	pub fn is_present<T: HasHeader>(&self) -> bool {
978		self.count::<T>() > 0
979	}
980
981	/// Returns true when the given pallet and variant combination appears.
982	pub fn is_present_parts(&self, pallet_id: u8, variant_id: u8) -> bool {
983		self.count_parts(pallet_id, variant_id) > 0
984	}
985
986	/// Counts how many times the given event type appears.
987	pub fn count<T: HasHeader>(&self) -> u32 {
988		self.count_parts(T::HEADER_INDEX.0, T::HEADER_INDEX.1)
989	}
990
991	/// Counts how many events match the pallet and variant combo.
992	pub fn count_parts(&self, pallet_id: u8, variant_id: u8) -> u32 {
993		let mut count = 0;
994		self.events.iter().for_each(|x| {
995			if x.pallet_id == pallet_id && x.variant_id == variant_id {
996				count += 1
997			}
998		});
999
1000		count
1001	}
1002}