avail_rust_client/chain/
chain.rs

1use crate::{
2	BlockState, Client, Error, UserError, avail, conversions,
3	submission::SubmittedTransaction,
4	subxt_signer::sr25519::Keypair,
5	transaction_options::Options,
6	utils::{with_retry_on_error, with_retry_on_error_and_none},
7};
8use avail::{
9	balances::types::AccountData,
10	system::{storage as SystemStorage, types::AccountInfo},
11};
12#[cfg(feature = "next")]
13use avail_rust_core::rpc::{
14	blob::{Blob, BlobInfo},
15	kate::DataProof,
16};
17use avail_rust_core::{
18	AccountId, AccountIdLike, AvailHeader, BlockInfo, H256, HashNumber, StorageMap, StorageValue, consensus,
19	ext::subxt_rpcs::client::RpcParams,
20	grandpa::GrandpaJustification,
21	header::DigestItem,
22	rpc::{
23		self, BlockPhaseEvent, Error as RpcError, ExtrinsicInfo, LegacyBlock,
24		kate::{BlockLength, Cell, GCellBlock, GDataProof, GMultiProof, GRow, ProofResponse},
25		runtime_api,
26	},
27	types::{
28		HashString,
29		metadata::{ChainInfo, HashStringNumber},
30		substrate::{FeeDetails, PerDispatchClassWeight, RuntimeDispatchInfo},
31	},
32};
33use codec::Decode;
34
35/// Low-level RPC surface with fine-grained retry controls.
36pub struct Chain {
37	pub(crate) client: Client,
38	retry_on_error: Option<bool>,
39	retry_on_none: Option<bool>,
40}
41impl Chain {
42	/// Creates a chain helper bound to the given client.
43	///
44	/// # Arguments
45	/// * `client` - Client used for all subsequent RPC calls.
46	pub fn new(client: Client) -> Self {
47		Self { client, retry_on_error: None, retry_on_none: None }
48	}
49
50	/// Lets you decide if upcoming calls retry on errors or missing data.
51	///
52	/// # Arguments
53	/// * `error` - Overrides whether transport errors are retried (defaults to the client's global flag).
54	/// * `none` - When `Some(true)`, RPCs returning `None` (for example, missing storage) will also be retried.
55	///
56	/// # Returns
57	/// Returns the helper with updated retry configuration.
58	pub fn retry_on(mut self, error: Option<bool>, none: Option<bool>) -> Self {
59		self.retry_on_error = error;
60		self.retry_on_none = none;
61		self
62	}
63
64	/// Fetches a block hash for the given height when available.
65	///
66	/// # Arguments
67	/// * `block_height` - Optional block number to resolve; `None` queries the best block.
68	///
69	/// # Returns
70	/// - `Ok(Some(H256))` when the chain knows about the requested height.
71	/// - `Ok(None)` when the block does not exist
72	/// - `Err(RpcError)` when the underlying RPC call fails.
73	pub async fn block_hash(&self, block_height: Option<u32>) -> Result<Option<H256>, RpcError> {
74		let retry = self.should_retry_on_error();
75		let retry_on_none = self.retry_on_none.unwrap_or(false);
76
77		let f = || async move { rpc::chain::get_block_hash(&self.client.rpc_client, block_height).await };
78		with_retry_on_error_and_none(f, retry, retry_on_none).await
79	}
80
81	/// Grabs a block header by hash or height.
82	///
83	/// # Arguments
84	/// * `at` - Optional hash or height identifying the target block; `None` queries the best block.
85	///
86	/// # Returns
87	/// - `Ok(Some(AvailHeader))` when the header exists.
88	/// - `Ok(None)` when the header is missing
89	/// - `Err(Error)` when conversions or RPC calls fail.
90	pub async fn block_header(&self, at: Option<impl Into<HashStringNumber>>) -> Result<Option<AvailHeader>, Error> {
91		let retry_on_error = self.should_retry_on_error();
92		let retry_on_none = self.retry_on_none.unwrap_or(false);
93
94		let at = if let Some(at) = at {
95			Some(conversions::hash_string_number::to_hash(self, at).await?)
96		} else {
97			None
98		};
99
100		let f = || async move { rpc::chain::get_header(&self.client.rpc_client, at).await };
101		Ok(with_retry_on_error_and_none(f, retry_on_error, retry_on_none).await?)
102	}
103
104	/// Retrieves the full legacy block
105	///
106	/// # Arguments
107	/// * `at` - Optional block hash; `None` queries the best block.
108	///
109	/// # Returns
110	/// - `Ok(Some(LegacyBlock))` when the block exists.
111	/// - `Ok(None)` when the block is missing
112	/// - `Err(Error)` when RPC calls fail.
113	pub async fn legacy_block(&self, at: Option<H256>) -> Result<Option<LegacyBlock>, RpcError> {
114		let retry = self.should_retry_on_error();
115		let retry_on_none = self.retry_on_none.unwrap_or(false);
116
117		let f = || async move { rpc::chain::get_block(&self.client.rpc_client, at).await };
118		with_retry_on_error_and_none(f, retry, retry_on_none).await
119	}
120
121	/// Looks up an account nonce at a particular block.
122	///
123	/// # Arguments
124	/// * `account_id` - Account identifier convertible into [`AccountIdLike`].
125	/// * `at` - Block reference (hash, height, or string) describing where to query the nonce.
126	///
127	/// # Returns
128	/// Returns the account nonce observed at the specified block.
129	///
130	/// # Errors
131	/// Returns `Err(Error)` when the account id cannot be parsed or the RPC call fails.
132	pub async fn block_nonce(
133		&self,
134		account_id: impl Into<AccountIdLike>,
135		at: impl Into<HashStringNumber>,
136	) -> Result<u32, Error> {
137		self.account_info(account_id, at).await.map(|x| x.nonce)
138	}
139
140	/// Returns the latest account nonce as seen by the node.
141	///
142	/// # Arguments
143	/// * `account_id` - Account identifier convertible into [`AccountIdLike`].
144	///
145	/// # Returns
146	/// Returns the latest account nonce reported by the node.
147	///
148	/// # Errors
149	/// Returns `Err(Error)` when the account id cannot be parsed or the RPC call fails.
150	pub async fn account_nonce(&self, account_id: impl Into<AccountIdLike>) -> Result<u32, Error> {
151		let account_id = conversions::account_id_like::to_account_id(account_id)?;
152
153		let retry_on_error = self.should_retry_on_error();
154		let a = &account_id;
155		let f =
156			|| async move { rpc::system::account_next_index(&self.client.rpc_client, &std::format!("{}", a)).await };
157
158		Ok(with_retry_on_error(f, retry_on_error).await?)
159	}
160
161	/// Reports the free balance for an account at a specific block.
162	///
163	/// # Arguments
164	/// * `account_id` - Account identifier convertible into [`AccountIdLike`].
165	/// * `at` - Block reference describing where to query balances.
166	///
167	/// # Returns
168	/// Returns [`AccountData`] for the requested account at the chosen block.
169	///
170	/// Errors mirror [`Chain::account_info`].
171	pub async fn account_balance(
172		&self,
173		account_id: impl Into<AccountIdLike>,
174		at: impl Into<HashStringNumber>,
175	) -> Result<AccountData, Error> {
176		self.account_info(account_id, at).await.map(|x| x.data)
177	}
178
179	/// Fetches the full account record (nonce, balances, …) at a given block.
180	///
181	/// # Arguments
182	/// * `account_id` - Account identifier convertible into [`AccountIdLike`].
183	/// * `at` - Block reference describing where to query the account.
184	///
185	/// # Returns
186	/// Returns [`AccountInfo`] containing balances, consumers, and nonce data.
187	///
188	/// # Errors
189	/// Returns `Err(Error)` when the account identifier or block id cannot be converted, the block is
190	/// missing, or the RPC call fails.
191	pub async fn account_info(
192		&self,
193		account_id: impl Into<AccountIdLike>,
194		at: impl Into<HashStringNumber>,
195	) -> Result<AccountInfo, Error> {
196		let account_id = conversions::account_id_like::to_account_id(account_id)?;
197		let at = conversions::hash_string_number::to_hash(self, at).await?;
198
199		let retry_on_error = self.should_retry_on_error();
200
201		let a = &account_id;
202		let f = || async move {
203			SystemStorage::Account::fetch(&self.client.rpc_client, a, Some(at))
204				.await
205				.map(|x| x.unwrap_or_default())
206		};
207
208		Ok(with_retry_on_error(f, retry_on_error).await?)
209	}
210
211	/// Tells you if a block is pending, finalized, or missing.
212	///
213	/// # Returns
214	/// Distinguishes between [`BlockState::Included`], [`BlockState::Finalized`], [`BlockState::Discarded`],
215	/// and [`BlockState::DoesNotExist`], depending on chain state.
216	///
217	/// # Errors
218	/// Returns `Err(Error)` if the supplied identifier cannot be converted or RPC calls fail.
219	pub async fn block_state(&self, block_id: impl Into<HashStringNumber>) -> Result<BlockState, Error> {
220		let block_id = conversions::hash_string_number::to_hash_number(block_id)?;
221		let chain_info = self.chain_info().await?;
222		let n = match block_id {
223			HashNumber::Hash(h) => {
224				if h == chain_info.finalized_hash {
225					return Ok(BlockState::Finalized);
226				}
227
228				if h == chain_info.best_hash {
229					return Ok(BlockState::Included);
230				}
231
232				let Some(n) = self.block_height(h).await? else {
233					return Ok(BlockState::DoesNotExist);
234				};
235
236				let Some(block_hash) = self.block_hash(Some(n)).await? else {
237					return Ok(BlockState::DoesNotExist);
238				};
239
240				if block_hash != h {
241					return Ok(BlockState::Discarded);
242				}
243
244				n
245			},
246			HashNumber::Number(n) => n,
247		};
248
249		if n > chain_info.best_height {
250			return Ok(BlockState::DoesNotExist);
251		}
252
253		if n > chain_info.finalized_height {
254			return Ok(BlockState::Included);
255		}
256
257		Ok(BlockState::Finalized)
258	}
259
260	/// Converts a block hash into its block height when possible.
261	///
262	/// # Returns
263	/// - `Ok(Some(u32))` when the block height exists.
264	/// - `Ok(None)` when the block height is missing
265	/// - `Err(Error)` when RPC calls fail.
266	pub async fn block_height(&self, at: impl Into<HashString>) -> Result<Option<u32>, Error> {
267		let at = conversions::hash_string::to_hash(at)?;
268		let retry_on_error = self.should_retry_on_error();
269		let retry_on_none = self.retry_on_none.unwrap_or(false);
270
271		let f = || async move { rpc::system::get_block_number(&self.client.rpc_client, at).await };
272		Ok(with_retry_on_error_and_none(f, retry_on_error, retry_on_none).await?)
273	}
274
275	/// Returns the latest block info, either best or finalized.
276	pub async fn block_info(&self, use_best_block: bool) -> Result<BlockInfo, RpcError> {
277		let retry = self.should_retry_on_error();
278		let f = || async move { rpc::system::latest_block_info(&self.client.rpc_client, use_best_block).await };
279		with_retry_on_error(f, retry).await
280	}
281
282	/// Fetches block metadata for the provided block identifier.
283	///
284	/// # Arguments
285	/// * `block_id` - Hash, height, or string representation of the target block.
286	///
287	/// # Returns
288	/// Returns `BlockInfo` describing the block, or an error if the lookup fails.
289	pub async fn block_info_from(&self, block_id: impl Into<HashStringNumber>) -> Result<BlockInfo, Error> {
290		let block_id = conversions::hash_string_number::to_hash_number(block_id)?;
291		let (height, hash) = match block_id {
292			HashNumber::Hash(hash) => {
293				let height = self.block_height(hash).await?;
294				let Some(height) = height else {
295					return Err(Error::User(UserError::Other(std::format!(
296						"No block height was found for hash: {}",
297						hash
298					))));
299				};
300				(height, hash)
301			},
302			HashNumber::Number(height) => {
303				let hash = self.block_hash(Some(height)).await?;
304				let Some(hash) = hash else {
305					return Err(Error::User(UserError::Other(std::format!(
306						"No block hash was found for height: {}",
307						height
308					))));
309				};
310				(height, hash)
311			},
312		};
313
314		Ok(BlockInfo::from((hash, height)))
315	}
316
317	/// Determines the author of the specified block.
318	///
319	/// # Arguments
320	/// * `block_id` - Hash, height, or string representation of the target block.
321	///
322	/// # Returns
323	/// Returns the account id of the block author or an error if it cannot be determined.
324	pub async fn block_author(&self, block_id: impl Into<HashStringNumber>) -> Result<AccountId, Error> {
325		let hash = conversions::hash_string_number::to_hash(self, block_id).await?;
326
327		let header = self.block_header(Some(hash)).await?;
328		let Some(header) = header else {
329			return Err(Error::Other("No block header was found".into()));
330		};
331
332		for item in &header.digest.logs {
333			let (id, value) = match &item {
334				DigestItem::PreRuntime(id, value) => (id, value),
335				_ => continue,
336			};
337
338			if !id.eq(&consensus::babe::BABE_ENGINE_ID) {
339				continue;
340			}
341
342			let mut v = value.as_slice();
343			let pre_digest = consensus::babe::PreDigest::decode(&mut v).map_err(|e| Error::Other(e.to_string()))?;
344
345			let validators = avail::session::storage::Validators::fetch(&self.client.rpc_client, Some(hash)).await?;
346			let Some(validators) = validators else {
347				return Err(Error::Other(std::format!(
348					"No validators in storage was found for block hash: {:?}",
349					hash
350				)));
351			};
352
353			if let Some(account_id) = validators.get(pre_digest.authority_index() as usize) {
354				return Ok(account_id.clone());
355			}
356		}
357
358		Err(Error::Other(std::format!("Failed to find block author for block hash: {}", hash)))
359	}
360
361	/// Counts the events emitted by the specified block.
362	///
363	/// # Arguments
364	/// * `block_id` - Identifier describing which block to inspect.
365	///
366	/// # Returns
367	/// Returns the number of events as `usize`, or an error if the count cannot be fetched.
368	pub async fn block_event_count(&self, block_id: impl Into<HashStringNumber>) -> Result<usize, Error> {
369		let hash = conversions::hash_string_number::to_hash(self, block_id).await?;
370		let retry_on_error = self.should_retry_on_error();
371
372		let f = || async move { avail::system::storage::EventCount::fetch(&self.client.rpc_client, Some(hash)).await };
373		let count = with_retry_on_error_and_none(f, retry_on_error, false).await?;
374		let Some(count) = count else {
375			return Err(Error::Other(std::format!("Failed to find block event count at block hash: {:?}", hash)));
376		};
377
378		Ok(count as usize)
379	}
380
381	/// Retrieves the dispatch-class weight totals for the specified block.
382	///
383	/// # Arguments
384	/// * `block_id` - Identifier describing which block to inspect.
385	///
386	/// # Returns
387	/// Returns the per-dispatch-class weight totals or an error if unavailable.
388	pub async fn block_weight(&self, block_id: impl Into<HashStringNumber>) -> Result<PerDispatchClassWeight, Error> {
389		let hash = conversions::hash_string_number::to_hash(self, block_id).await?;
390		let retry_on_error = self.should_retry_on_error();
391
392		let f = || async move { avail::system::storage::BlockWeight::fetch(&self.client.rpc_client, Some(hash)).await };
393		let weight = with_retry_on_error_and_none(f, retry_on_error, false).await?;
394		let Some(weight) = weight else {
395			return Err(Error::Other(std::format!("Failed to find block weight at block hash: {:?}", hash)));
396		};
397
398		Ok(weight)
399	}
400
401	/// Quick snapshot of both the best and finalized heads.
402	pub async fn chain_info(&self) -> Result<ChainInfo, RpcError> {
403		let retry = self.should_retry_on_error();
404
405		let f = || async move { rpc::system::latest_chain_info(&self.client.rpc_client).await };
406		with_retry_on_error(f, retry).await
407	}
408
409	/// Builds an unsigned extrinsic payload for the provided account and call.
410	///
411	/// # Arguments
412	/// * `account_id` - Account that will sign the payload.
413	/// * `call` - Runtime call to encode inside the payload.
414	/// * `options` - Transaction options used to refine mortality, nonce, and fees.
415	///
416	/// # Returns
417	/// Returns the constructed payload or an error if option refinement fails.
418	pub async fn build_payload<'a>(
419		&self,
420		account_id: &AccountId,
421		call: &'a avail_rust_core::ExtrinsicCall,
422		options: Options,
423	) -> Result<avail_rust_core::ExtrinsicPayload<'a>, Error> {
424		let refined_options = options.build(&self.client, account_id, self.retry_on_error).await?;
425
426		let extra = avail_rust_core::ExtrinsicExtra::from(&refined_options);
427		let additional = avail_rust_core::ExtrinsicAdditional {
428			spec_version: self.client.online_client().spec_version(),
429			tx_version: self.client.online_client().transaction_version(),
430			genesis_hash: self.client.online_client().genesis_hash(),
431			fork_hash: refined_options.mortality.block_hash,
432		};
433
434		Ok(avail_rust_core::ExtrinsicPayload::new_borrowed(call, extra, additional))
435	}
436
437	/// Builds a payload from a call and signs it with sensible defaults.
438	///
439	/// # Errors
440	/// Returns `Err(Error)` when option refinement fails (e.g., fetching account info) or signing fails.
441	pub async fn build_extrinsic_from_call<'a>(
442		&self,
443		signer: &Keypair,
444		call: &'a avail_rust_core::ExtrinsicCall,
445		options: Options,
446	) -> Result<avail_rust_core::GenericExtrinsic<'a>, Error> {
447		let account_id = signer.public_key().to_account_id();
448
449		let payload = self.build_payload(&account_id, call, options).await?;
450		let signature = payload.sign(signer);
451
452		Ok(avail_rust_core::GenericExtrinsic::new(account_id, signature, payload))
453	}
454
455	/// Submits a signed extrinsic and gives you the transaction hash.
456	///
457	/// # Errors
458	/// Returns `Err(RpcError)` when the node rejects the extrinsic or the RPC transport fails.
459	pub async fn submit(&self, ext: &avail_rust_core::GenericExtrinsic<'_>) -> Result<H256, RpcError> {
460		let retry = self.should_retry_on_error();
461		let encoded = ext.encode();
462
463		#[cfg(feature = "tracing")]
464		if let Some(signed) = &ext.signature {
465			if let avail_rust_core::MultiAddress::Id(account_id) = &signed.address {
466				tracing::info!(target: "tx", "Submitting Transaction. Address: {}, Nonce: {}, App Id: {}", account_id, signed.extra.nonce, signed.extra.app_id);
467			}
468		}
469
470		let enc_slice = encoded.as_slice();
471		let f = || async move { rpc::author::submit_extrinsic(&self.client.rpc_client, enc_slice).await };
472		let tx_hash = with_retry_on_error(f, retry).await?;
473
474		#[cfg(feature = "tracing")]
475		if let Some(signed) = &ext.signature {
476			if let avail_rust_core::MultiAddress::Id(account_id) = &signed.address {
477				tracing::info!(target: "tx", "Transaction Submitted.  Address: {}, Nonce: {}, App Id: {}, Tx Hash: {:?},", account_id, signed.extra.nonce, signed.extra.app_id, tx_hash);
478			}
479		}
480
481		Ok(tx_hash)
482	}
483
484	/// Submits a signed extrinsic and gives you the transaction hash.
485	///
486	/// # Errors
487	/// Returns `Err(RpcError)` when the node rejects the extrinsic or the RPC transport fails.
488	pub async fn submit_raw(&self, ext: &[u8]) -> Result<H256, RpcError> {
489		let retry = self.should_retry_on_error();
490
491		let f = || async move { rpc::author::submit_extrinsic(&self.client.rpc_client, ext).await };
492		let tx_hash = with_retry_on_error(f, retry).await?;
493		Ok(tx_hash)
494	}
495
496	/// Signs the payload and submits it in one step.
497	pub async fn sign_and_submit_payload(
498		&self,
499		signer: &Keypair,
500		tx_payload: avail_rust_core::ExtrinsicPayload<'_>,
501	) -> Result<H256, RpcError> {
502		use avail_rust_core::GenericExtrinsic;
503
504		let account_id = signer.public_key().to_account_id();
505		let signature = tx_payload.sign(signer);
506		let tx = GenericExtrinsic::new(account_id, signature, tx_payload);
507		let tx_hash = self.submit(&tx).await?;
508
509		Ok(tx_hash)
510	}
511
512	/// Signs a call, submits it, and hands back a tracker you can poll.
513	///
514	/// # Returns
515	/// - `Ok(SubmittedTransaction)` containing the transaction hash and refined options for later
516	///   receipt queries.
517	/// - `Err(Error)` when option refinement, signing, or submission fails.
518	pub async fn sign_and_submit_call(
519		&self,
520		signer: &Keypair,
521		tx_call: &avail_rust_core::ExtrinsicCall,
522		options: Options,
523	) -> Result<SubmittedTransaction, Error> {
524		let account_id = signer.public_key().to_account_id();
525		let refined_options = options.build(&self.client, &account_id, self.retry_on_error).await?;
526
527		let extra = avail_rust_core::ExtrinsicExtra::from(&refined_options);
528		let tx_additional = avail_rust_core::ExtrinsicAdditional {
529			spec_version: self.client.online_client().spec_version(),
530			tx_version: self.client.online_client().transaction_version(),
531			genesis_hash: self.client.online_client().genesis_hash(),
532			fork_hash: refined_options.mortality.block_hash,
533		};
534
535		let tx_payload = avail_rust_core::ExtrinsicPayload::new_borrowed(tx_call, extra, tx_additional.clone());
536		let tx_hash = self.sign_and_submit_payload(signer, tx_payload).await?;
537
538		let value = SubmittedTransaction::new(self.client.clone(), tx_hash, account_id, refined_options, tx_additional);
539		Ok(value)
540	}
541
542	/// Runs a `state_call` and returns the raw response string.
543	pub async fn state_call(&self, method: &str, data: &[u8], at: Option<H256>) -> Result<String, RpcError> {
544		let retry = self.should_retry_on_error();
545
546		let f = || async move { rpc::state::call(&self.client.rpc_client, method, data, at).await };
547		with_retry_on_error(f, retry).await
548	}
549
550	/// Downloads runtime metadata as bytes.
551	pub async fn state_get_metadata(&self, at: Option<H256>) -> Result<Vec<u8>, RpcError> {
552		let retry = self.should_retry_on_error();
553
554		let f = || async move { rpc::state::get_metadata(&self.client.rpc_client, at).await };
555		with_retry_on_error(f, retry).await
556	}
557
558	/// Reads a storage entry, returning the raw bytes if present.
559	pub async fn state_get_storage(&self, key: &str, at: Option<H256>) -> Result<Option<Vec<u8>>, RpcError> {
560		let retry = self.should_retry_on_error();
561
562		let f = || async move { rpc::state::get_storage(&self.client.rpc_client, key, at).await };
563		with_retry_on_error(f, retry).await
564	}
565
566	/// Lists storage keys under a prefix, one page at a time.
567	pub async fn state_get_keys_paged(
568		&self,
569		prefix: Option<&str>,
570		count: u32,
571		start_key: Option<&str>,
572		at: Option<H256>,
573	) -> Result<Vec<String>, RpcError> {
574		let retry = self.should_retry_on_error();
575
576		let f =
577			|| async move { rpc::state::get_keys_paged(&self.client.rpc_client, prefix, count, start_key, at).await };
578
579		with_retry_on_error(f, retry).await
580	}
581
582	/// Performs a raw RPC invocation against the connected node and deserializes the response.
583	pub async fn rpc_raw_call<T: serde::de::DeserializeOwned>(
584		&self,
585		method: &str,
586		params: RpcParams,
587	) -> Result<T, RpcError> {
588		let retry = self.should_retry_on_error();
589
590		let p = &params;
591		let f = || async move { rpc::raw_call(&self.client.rpc_client, method, p.clone()).await };
592		with_retry_on_error(f, retry).await
593	}
594
595	/// Calls into the runtime API and decodes the answer for you.
596	pub async fn runtime_api_raw_call<T: codec::Decode>(
597		&self,
598		method: &str,
599		data: &[u8],
600		at: Option<H256>,
601	) -> Result<T, RpcError> {
602		let retry = self.should_retry_on_error();
603
604		let f = || async move { runtime_api::raw_call(&self.client.rpc_client, method, data, at).await };
605		with_retry_on_error(f, retry).await
606	}
607
608	/// Fetches GRANDPA justification for the given block number.
609	///
610	/// # Returns
611	/// - `Ok(Some(GrandpaJustification))` when a justification is present.
612	/// - `Ok(None)` when the runtime returns no justification.
613	/// - `Err(RpcError)` if decoding the response or the RPC call fails.
614	pub async fn grandpa_block_justification(&self, at: u32) -> Result<Option<GrandpaJustification>, RpcError> {
615		let retry = self.should_retry_on_error();
616
617		let f = || async move { rpc::grandpa::block_justification(&self.client.rpc_client, at).await };
618		let result = with_retry_on_error(f, retry).await?;
619
620		let Some(result) = result else {
621			return Ok(None);
622		};
623
624		let justification = const_hex::decode(result.trim_start_matches("0x"))
625			.map_err(|x| RpcError::MalformedResponse(x.to_string()))?;
626
627		let justification = GrandpaJustification::decode(&mut justification.as_slice());
628		let justification = justification.map_err(|e| RpcError::MalformedResponse(e.to_string()))?;
629		Ok(Some(justification))
630	}
631
632	/// Queries the runtime for fee information about an encoded extrinsic.
633	///
634	/// # Arguments
635	/// * `extrinsic` - SCALE-encoded extrinsic bytes.
636	/// * `at` - Optional block hash to query against.
637	///
638	/// # Returns
639	/// Returns dispatch info describing the estimated fee and weight.
640	pub async fn transaction_payment_query_info(
641		&self,
642		extrinsic: Vec<u8>,
643		at: Option<H256>,
644	) -> Result<RuntimeDispatchInfo, RpcError> {
645		let retry = self.should_retry_on_error();
646
647		let ext = &extrinsic;
648		let f = || async move {
649			runtime_api::api_transaction_payment_query_info(&self.client.rpc_client, ext.clone(), at).await
650		};
651		with_retry_on_error(f, retry).await
652	}
653
654	/// Retrieves detailed fee breakdown for an encoded extrinsic.
655	///
656	/// # Arguments
657	/// * `extrinsic` - SCALE-encoded extrinsic bytes.
658	/// * `at` - Optional block hash to query against.
659	///
660	/// # Returns
661	/// Returns fee components such as inclusion and tip fees.
662	pub async fn transaction_payment_query_fee_details(
663		&self,
664		extrinsic: Vec<u8>,
665		at: Option<H256>,
666	) -> Result<FeeDetails, RpcError> {
667		let retry = self.should_retry_on_error();
668
669		let ext = &extrinsic;
670		let f = || async move {
671			runtime_api::api_transaction_payment_query_fee_details(&self.client.rpc_client, ext.clone(), at).await
672		};
673		with_retry_on_error(f, retry).await
674	}
675
676	/// Queries the runtime for fee information about an encoded call.
677	///
678	/// # Arguments
679	/// * `call` - SCALE-encoded call bytes.
680	/// * `at` - Optional block hash to query against.
681	///
682	/// # Returns
683	/// Returns dispatch info describing the estimated fee and weight.
684	pub async fn transaction_payment_query_call_info(
685		&self,
686		call: Vec<u8>,
687		at: Option<H256>,
688	) -> Result<RuntimeDispatchInfo, RpcError> {
689		let retry = self.should_retry_on_error();
690
691		let c = &call;
692		let f = || async move {
693			runtime_api::api_transaction_payment_query_call_info(&self.client.rpc_client, c.clone(), at).await
694		};
695		with_retry_on_error(f, retry).await
696	}
697
698	/// Retrieves detailed fee components for an encoded call.
699	///
700	/// # Arguments
701	/// * `call` - SCALE-encoded call bytes.
702	/// * `at` - Optional block hash to query against.
703	///
704	/// # Returns
705	/// Returns the fee breakdown for executing the call.
706	pub async fn transaction_payment_query_call_fee_details(
707		&self,
708		call: Vec<u8>,
709		at: Option<H256>,
710	) -> Result<FeeDetails, RpcError> {
711		let retry = self.should_retry_on_error();
712
713		let c = &call;
714		let f = || async move {
715			runtime_api::api_transaction_payment_query_call_fee_details(&self.client.rpc_client, c.clone(), at).await
716		};
717		with_retry_on_error(f, retry).await
718	}
719
720	/// Retrieves the KATE block layout metadata (rows, cols, chunk size) for the block at `at`.
721	///
722	/// # Errors
723	/// Returns `Err(RpcError)` when the KATE RPC call fails; respects the helper's retry policy.
724	pub async fn kate_block_length(&self, at: Option<H256>) -> Result<BlockLength, RpcError> {
725		let retry = self.should_retry_on_error();
726
727		let f = || async move { rpc::kate::block_length(&self.client.rpc_client, at).await };
728		with_retry_on_error(f, retry).await
729	}
730
731	/// Produces the KATE data proof (and optional addressed message) for the given extrinsic index.
732	///
733	/// # Errors
734	/// Returns `Err(RpcError)` when the proof cannot be fetched or deserialised; obeys the retry setting.
735	pub async fn kate_query_data_proof(
736		&self,
737		transaction_index: u32,
738		at: Option<H256>,
739	) -> Result<ProofResponse, RpcError> {
740		let retry = self.should_retry_on_error();
741
742		let f = || async move { rpc::kate::query_data_proof(&self.client.rpc_client, transaction_index, at).await };
743		with_retry_on_error(f, retry).await
744	}
745
746	/// Fetches individual KATE proofs for the provided list of cells.
747	///
748	/// # Errors
749	/// Bubbles `Err(RpcError)` if the RPC call fails; retries follow the configured policy.
750	pub async fn kate_query_proof(&self, cells: Vec<Cell>, at: Option<H256>) -> Result<Vec<GDataProof>, RpcError> {
751		let retry = self.should_retry_on_error();
752
753		let cells_ref = &cells;
754		let f = || async move { rpc::kate::query_proof(&self.client.rpc_client, cells_ref.clone(), at).await };
755		with_retry_on_error(f, retry).await
756	}
757
758	/// Returns KATE row data for the requested row indices (up to the chain-imposed limit).
759	///
760	/// # Errors
761	/// Propagates `Err(RpcError)` when the row query fails; adheres to the retry preference.
762	pub async fn kate_query_rows(&self, rows: Vec<u32>, at: Option<H256>) -> Result<Vec<GRow>, RpcError> {
763		let retry = self.should_retry_on_error();
764
765		let rows_ref = &rows;
766		let f = || async move { rpc::kate::query_rows(&self.client.rpc_client, rows_ref.clone(), at).await };
767		with_retry_on_error(f, retry).await
768	}
769
770	/// Requests multi-proofs for the supplied KATE cells, paired with the corresponding cell block metadata.
771	///
772	/// # Errors
773	/// Returns `Err(RpcError)` when the RPC transport or decoding fails; follows the retry configuration.
774	pub async fn kate_query_multi_proof(
775		&self,
776		cells: Vec<Cell>,
777		at: Option<H256>,
778	) -> Result<Vec<(GMultiProof, GCellBlock)>, RpcError> {
779		let retry = self.should_retry_on_error();
780
781		let cells_ref = &cells;
782		let f = || async move { rpc::kate::query_multi_proof(&self.client.rpc_client, cells_ref.clone(), at).await };
783		with_retry_on_error(f, retry).await
784	}
785
786	#[cfg(feature = "next")]
787	/// Submits a blob alongside its signed metadata transaction.
788	///
789	/// # Arguments
790	/// * `metadata_signed_transaction` - Signed extrinsic containing blob metadata.
791	/// * `blob` - Raw blob data to upload.
792	///
793	/// # Returns
794	/// Returns `Ok(())` on success or an error if the submission fails.
795	pub async fn blob_submit_blob(&self, metadata_signed_transaction: &[u8], blob: &[u8]) -> Result<(), Error> {
796		let retry = self.should_retry_on_error();
797
798		let f =
799			|| async move { rpc::blob::submit_blob(&self.client.rpc_client, metadata_signed_transaction, blob).await };
800
801		Ok(with_retry_on_error(f, retry).await?)
802	}
803
804	#[cfg(feature = "next")]
805	pub async fn blob_get_blob(&self, blob_hash: H256, block_hash: Option<H256>) -> Result<Blob, Error> {
806		let retry = self.should_retry_on_error();
807
808		let f = || async move { rpc::blob::get_blob_v2(&self.client.rpc_client, blob_hash, block_hash).await };
809
810		Ok(with_retry_on_error(f, retry).await?)
811	}
812
813	/// Retrieve indexed blob info
814	#[cfg(feature = "next")]
815	pub async fn blob_get_blob_info(&self, blob_hash: H256) -> Result<BlobInfo, Error> {
816		let retry = self.should_retry_on_error();
817
818		let f = || async move { rpc::blob::get_blob_info(&self.client.rpc_client, blob_hash).await };
819
820		Ok(with_retry_on_error(f, retry).await?)
821	}
822
823	/// Return inclusion proof for a blob. If `at` is `Some(hash)` the proof is computed for that block,
824	/// otherwise the node will try to use its indexed finalized block for the blob.
825	#[cfg(feature = "next")]
826	pub async fn blob_inclusion_proof(&self, blob_hash: H256, at: Option<H256>) -> Result<DataProof, Error> {
827		let retry = self.should_retry_on_error();
828
829		let f = || async move { rpc::blob::inclusion_proof(&self.client.rpc_client, blob_hash, at).await };
830
831		Ok(with_retry_on_error(f, retry).await?)
832	}
833
834	/// Fetches extrinsics from a block using the provided filters.
835	///
836	/// # Errors
837	/// Returns `Err(Error)` when the block id cannot be decoded or the RPC request fails.
838	pub async fn system_fetch_extrinsics(
839		&self,
840		block_id: impl Into<HashStringNumber>,
841		opts: rpc::ExtrinsicOpts,
842	) -> Result<Vec<ExtrinsicInfo>, Error> {
843		let block_id = conversions::hash_string_number::to_hash_number(block_id)?;
844		let retry = self.should_retry_on_error();
845
846		let opts2 = &opts;
847		let f = || async move { rpc::system::fetch_extrinsics_v1(&self.client.rpc_client, block_id, opts2).await };
848		with_retry_on_error(f, retry).await.map_err(|e| e.into())
849	}
850
851	/// Pulls events for a block with optional filtering.
852	///
853	/// # Errors
854	/// Returns `Err(Error)` when the block id cannot be resolved or the RPC call fails.
855	pub async fn system_fetch_events(
856		&self,
857		at: impl Into<HashStringNumber>,
858		opts: rpc::EventOpts,
859	) -> Result<Vec<BlockPhaseEvent>, Error> {
860		let at = conversions::hash_string_number::to_hash(self, at).await?;
861		let retry = self.should_retry_on_error();
862
863		let opts2 = &opts;
864		let f = || async move { rpc::system::fetch_events_v1(&self.client.rpc_client, at, opts2).await };
865		with_retry_on_error(f, retry).await.map_err(|e| e.into())
866	}
867
868	/// Reports whether RPC helpers should retry after encountering errors.
869	pub fn should_retry_on_error(&self) -> bool {
870		self.retry_on_error
871			.unwrap_or_else(|| self.client.is_global_retries_enabled())
872	}
873}