mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-04-25 16:27:56 +00:00
Use frame-decode for core extrinsic decode logic (#1785)
* WIP using frame-decode for core extrinsic decode logic * fmt * Fix dependabot config * clippy * tidy some imports * Fix a couple of tests * Update to frame-decode 0.0.7 * fix docs * Decode exts earlier to avoid doing it every iter/find step * frame-decode to 0.1.0 * fmt * clippy * fix wasm example * doc test fixes * Fix test * Fix a couple of subxt_core tests
This commit is contained in:
@@ -6,6 +6,7 @@ updates:
|
||||
schedule:
|
||||
interval: weekly
|
||||
- package-ecosystem: github-actions
|
||||
directory: "**/*"
|
||||
directories:
|
||||
- "**/*"
|
||||
schedule:
|
||||
interval: weekly
|
||||
|
||||
Generated
+17
@@ -1748,6 +1748,21 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "frame-decode"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed90459016b06a2855321469cb01fbc74208c80c06b085d1ed13162cf8bd7e1b"
|
||||
dependencies = [
|
||||
"frame-metadata 16.0.0",
|
||||
"hex",
|
||||
"parity-scale-codec",
|
||||
"scale-decode",
|
||||
"scale-info",
|
||||
"scale-type-resolver",
|
||||
"sp-crypto-hashing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "frame-metadata"
|
||||
version = "15.1.0"
|
||||
@@ -5313,6 +5328,7 @@ dependencies = [
|
||||
"bitvec",
|
||||
"blake2",
|
||||
"derive-where",
|
||||
"frame-decode",
|
||||
"frame-metadata 16.0.0",
|
||||
"hashbrown 0.14.5",
|
||||
"hex",
|
||||
@@ -5387,6 +5403,7 @@ dependencies = [
|
||||
"assert_matches",
|
||||
"bitvec",
|
||||
"criterion",
|
||||
"frame-decode",
|
||||
"frame-metadata 16.0.0",
|
||||
"hashbrown 0.14.5",
|
||||
"parity-scale-codec",
|
||||
|
||||
@@ -78,6 +78,7 @@ darling = "0.20.10"
|
||||
derive-where = "1.2.7"
|
||||
either = { version = "1.13.0", default-features = false }
|
||||
finito = { version = "0.1.0", default-features = false }
|
||||
frame-decode = { version = "0.3.0", default-features = false }
|
||||
frame-metadata = { version = "16.0.0", default-features = false }
|
||||
futures = { version = "0.3.30", default-features = false, features = ["std"] }
|
||||
getrandom = { version = "0.2", default-features = false }
|
||||
|
||||
@@ -36,6 +36,7 @@ substrate-compat = ["sp-core", "sp-runtime"]
|
||||
|
||||
[dependencies]
|
||||
codec = { package = "parity-scale-codec", workspace = true, default-features = false, features = ["derive"] }
|
||||
frame-decode = { workspace = true }
|
||||
scale-info = { workspace = true, default-features = false, features = ["bit-vec"] }
|
||||
scale-value = { workspace = true, default-features = false }
|
||||
scale-bits = { workspace = true, default-features = false }
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::config::signed_extensions::{
|
||||
use crate::config::SignedExtension;
|
||||
use crate::dynamic::Value;
|
||||
use crate::{config::Config, error::Error, Metadata};
|
||||
use frame_decode::extrinsics::ExtrinsicExtensions;
|
||||
use scale_decode::DecodeAsType;
|
||||
|
||||
/// The signed extensions of an extrinsic.
|
||||
@@ -15,58 +16,32 @@ use scale_decode::DecodeAsType;
|
||||
pub struct ExtrinsicSignedExtensions<'a, T: Config> {
|
||||
bytes: &'a [u8],
|
||||
metadata: &'a Metadata,
|
||||
decoded_info: &'a ExtrinsicExtensions<'static, u32>,
|
||||
_marker: core::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<'a, T: Config> ExtrinsicSignedExtensions<'a, T> {
|
||||
pub(crate) fn new(bytes: &'a [u8], metadata: &'a Metadata) -> Self {
|
||||
pub(crate) fn new(
|
||||
bytes: &'a [u8],
|
||||
metadata: &'a Metadata,
|
||||
decoded_info: &'a ExtrinsicExtensions<'static, u32>,
|
||||
) -> Self {
|
||||
Self {
|
||||
bytes,
|
||||
metadata,
|
||||
decoded_info,
|
||||
_marker: core::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an iterator over each of the signed extension details of the extrinsic.
|
||||
/// If the decoding of any signed extension fails, an error item is yielded and the iterator stops.
|
||||
pub fn iter(&self) -> impl Iterator<Item = Result<ExtrinsicSignedExtension<T>, Error>> {
|
||||
let signed_extension_types = self.metadata.extrinsic().signed_extensions();
|
||||
let num_signed_extensions = signed_extension_types.len();
|
||||
let bytes = self.bytes;
|
||||
let mut index = 0;
|
||||
let mut byte_start_idx = 0;
|
||||
let metadata = &self.metadata;
|
||||
|
||||
core::iter::from_fn(move || {
|
||||
if index == num_signed_extensions {
|
||||
return None;
|
||||
}
|
||||
|
||||
let extension = &signed_extension_types[index];
|
||||
let ty_id = extension.extra_ty();
|
||||
let cursor = &mut &bytes[byte_start_idx..];
|
||||
if let Err(err) = scale_decode::visitor::decode_with_visitor(
|
||||
cursor,
|
||||
ty_id,
|
||||
metadata.types(),
|
||||
scale_decode::visitor::IgnoreVisitor::new(),
|
||||
)
|
||||
.map_err(|e| Error::Decode(e.into()))
|
||||
{
|
||||
index = num_signed_extensions; // (such that None is returned in next iteration)
|
||||
return Some(Err(err));
|
||||
}
|
||||
let byte_end_idx = bytes.len() - cursor.len();
|
||||
let bytes = &bytes[byte_start_idx..byte_end_idx];
|
||||
byte_start_idx = byte_end_idx;
|
||||
index += 1;
|
||||
Some(Ok(ExtrinsicSignedExtension {
|
||||
bytes,
|
||||
ty_id,
|
||||
identifier: extension.identifier(),
|
||||
metadata,
|
||||
_marker: core::marker::PhantomData,
|
||||
}))
|
||||
pub fn iter(&self) -> impl Iterator<Item = ExtrinsicSignedExtension<T>> {
|
||||
self.decoded_info.iter().map(|s| ExtrinsicSignedExtension {
|
||||
bytes: &self.bytes[s.range()],
|
||||
ty_id: *s.ty(),
|
||||
identifier: s.name(),
|
||||
metadata: self.metadata,
|
||||
_marker: core::marker::PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -75,9 +50,6 @@ impl<'a, T: Config> ExtrinsicSignedExtensions<'a, T> {
|
||||
/// If the Signed Extension is found but decoding failed `Err(_)` is returned.
|
||||
pub fn find<S: SignedExtension<T>>(&self) -> Result<Option<S::Decoded>, Error> {
|
||||
for ext in self.iter() {
|
||||
// If we encounter an error while iterating, we won't get any more results
|
||||
// back, so just return that error as we won't find the signed ext anyway.
|
||||
let ext = ext?;
|
||||
match ext.as_signed_extension::<S>() {
|
||||
// We found a match; return it:
|
||||
Ok(Some(e)) => return Ok(Some(e)),
|
||||
|
||||
+88
-212
@@ -2,15 +2,17 @@
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use super::BlockError;
|
||||
use crate::blocks::extrinsic_signed_extensions::ExtrinsicSignedExtensions;
|
||||
use crate::{
|
||||
config::{Config, Hasher},
|
||||
error::{BlockError, Error, MetadataError},
|
||||
error::{Error, MetadataError},
|
||||
Metadata,
|
||||
};
|
||||
use alloc::sync::Arc;
|
||||
use alloc::vec::Vec;
|
||||
use codec::{Compact, CompactLen, Decode};
|
||||
use core::ops::Deref;
|
||||
use frame_decode::extrinsics::Extrinsic;
|
||||
use scale_decode::DecodeAsType;
|
||||
use subxt_metadata::PalletMetadata;
|
||||
|
||||
@@ -18,9 +20,8 @@ pub use crate::blocks::StaticExtrinsic;
|
||||
|
||||
/// The body of a block.
|
||||
pub struct Extrinsics<T: Config> {
|
||||
extrinsics: Vec<Vec<u8>>,
|
||||
extrinsics: Vec<Arc<(Extrinsic<'static, u32>, Vec<u8>)>>,
|
||||
metadata: Metadata,
|
||||
ids: ExtrinsicPartTypeIds,
|
||||
_marker: core::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
@@ -28,13 +29,33 @@ impl<T: Config> Extrinsics<T> {
|
||||
/// Instantiate a new [`Extrinsics`] object, given a vector containing
|
||||
/// each extrinsic hash (in the form of bytes) and some metadata that
|
||||
/// we'll use to decode them.
|
||||
pub fn decode_from(extrinsics: Vec<Vec<u8>>, metadata: Metadata) -> Result<Self, BlockError> {
|
||||
let ids = ExtrinsicPartTypeIds::new(&metadata)?;
|
||||
pub fn decode_from(extrinsics: Vec<Vec<u8>>, metadata: Metadata) -> Result<Self, Error> {
|
||||
let extrinsics = extrinsics
|
||||
.into_iter()
|
||||
.map(|bytes| {
|
||||
let cursor = &mut &*bytes;
|
||||
|
||||
// Try to decode the extrinsic.
|
||||
let decoded_info = frame_decode::extrinsics::decode_extrinsic(
|
||||
cursor,
|
||||
metadata.deref(),
|
||||
metadata.types(),
|
||||
)
|
||||
.map_err(BlockError::ExtrinsicDecodeError)?
|
||||
.into_owned();
|
||||
|
||||
// We didn't consume all bytes, so decoding probably failed.
|
||||
if !cursor.is_empty() {
|
||||
return Err(BlockError::LeftoverBytes(cursor.len()).into());
|
||||
}
|
||||
|
||||
Ok(Arc::new((decoded_info, bytes)))
|
||||
})
|
||||
.collect::<Result<_, Error>>()?;
|
||||
|
||||
Ok(Self {
|
||||
extrinsics,
|
||||
metadata,
|
||||
ids,
|
||||
_marker: core::marker::PhantomData,
|
||||
})
|
||||
}
|
||||
@@ -53,35 +74,13 @@ impl<T: Config> Extrinsics<T> {
|
||||
/// Returns an iterator over the extrinsics in the block body.
|
||||
// Dev note: The returned iterator is 'static + Send so that we can box it up and make
|
||||
// use of it with our `FilterExtrinsic` stuff.
|
||||
pub fn iter(
|
||||
&self,
|
||||
) -> impl Iterator<Item = Result<ExtrinsicDetails<T>, Error>> + Send + Sync + 'static {
|
||||
pub fn iter(&self) -> impl Iterator<Item = ExtrinsicDetails<T>> + Send + Sync + 'static {
|
||||
let extrinsics = self.extrinsics.clone();
|
||||
let num_extrinsics = self.extrinsics.len();
|
||||
let metadata = self.metadata.clone();
|
||||
let ids = self.ids;
|
||||
let mut index = 0;
|
||||
|
||||
core::iter::from_fn(move || {
|
||||
if index == num_extrinsics {
|
||||
None
|
||||
} else {
|
||||
match ExtrinsicDetails::decode_from(
|
||||
index as u32,
|
||||
&extrinsics[index],
|
||||
metadata.clone(),
|
||||
ids,
|
||||
) {
|
||||
Ok(extrinsic_details) => {
|
||||
index += 1;
|
||||
Some(Ok(extrinsic_details))
|
||||
}
|
||||
Err(e) => {
|
||||
index = num_extrinsics;
|
||||
Some(Err(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
(0..num_extrinsics).map(move |index| {
|
||||
ExtrinsicDetails::new(index as u32, extrinsics[index].clone(), metadata.clone())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -91,15 +90,14 @@ impl<T: Config> Extrinsics<T> {
|
||||
pub fn find<E: StaticExtrinsic>(
|
||||
&self,
|
||||
) -> impl Iterator<Item = Result<FoundExtrinsic<T, E>, Error>> + '_ {
|
||||
self.iter().filter_map(|res| match res {
|
||||
Err(err) => Some(Err(err)),
|
||||
Ok(details) => match details.as_extrinsic::<E>() {
|
||||
self.iter().filter_map(|details| {
|
||||
match details.as_extrinsic::<E>() {
|
||||
// Failed to decode extrinsic:
|
||||
Err(err) => Some(Err(err)),
|
||||
// Extrinsic for a different pallet / different call (skip):
|
||||
Ok(None) => None,
|
||||
Ok(Some(value)) => Some(Ok(FoundExtrinsic { details, value })),
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -125,141 +123,41 @@ impl<T: Config> Extrinsics<T> {
|
||||
pub struct ExtrinsicDetails<T: Config> {
|
||||
/// The index of the extrinsic in the block.
|
||||
index: u32,
|
||||
/// Extrinsic bytes.
|
||||
bytes: Arc<[u8]>,
|
||||
/// Some if the extrinsic payload is signed.
|
||||
signed_details: Option<SignedExtrinsicDetails>,
|
||||
/// The start index in the `bytes` from which the call is encoded.
|
||||
call_start_idx: usize,
|
||||
/// The pallet index.
|
||||
pallet_index: u8,
|
||||
/// The variant index.
|
||||
variant_index: u8,
|
||||
/// Extrinsic bytes and decode info.
|
||||
ext: Arc<(Extrinsic<'static, u32>, Vec<u8>)>,
|
||||
/// Subxt metadata to fetch the extrinsic metadata.
|
||||
metadata: Metadata,
|
||||
_marker: core::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
/// Details only available in signed extrinsics.
|
||||
pub struct SignedExtrinsicDetails {
|
||||
/// start index of the range in `bytes` of `ExtrinsicDetails` that encodes the address.
|
||||
address_start_idx: usize,
|
||||
/// end index of the range in `bytes` of `ExtrinsicDetails` that encodes the address. Equivalent to signature_start_idx.
|
||||
address_end_idx: usize,
|
||||
/// end index of the range in `bytes` of `ExtrinsicDetails` that encodes the signature. Equivalent to extra_start_idx.
|
||||
signature_end_idx: usize,
|
||||
/// end index of the range in `bytes` of `ExtrinsicDetails` that encodes the signature.
|
||||
extra_end_idx: usize,
|
||||
}
|
||||
|
||||
impl<T> ExtrinsicDetails<T>
|
||||
where
|
||||
T: Config,
|
||||
{
|
||||
// Attempt to dynamically decode a single extrinsic from the given input.
|
||||
#[doc(hidden)]
|
||||
pub fn decode_from(
|
||||
pub fn new(
|
||||
index: u32,
|
||||
extrinsic_bytes: &[u8],
|
||||
ext: Arc<(Extrinsic<'static, u32>, Vec<u8>)>,
|
||||
metadata: Metadata,
|
||||
ids: ExtrinsicPartTypeIds,
|
||||
) -> Result<ExtrinsicDetails<T>, Error> {
|
||||
const SIGNATURE_MASK: u8 = 0b1000_0000;
|
||||
const VERSION_MASK: u8 = 0b0111_1111;
|
||||
const LATEST_EXTRINSIC_VERSION: u8 = 4;
|
||||
|
||||
// Wrap all of the bytes in Arc for easy sharing.
|
||||
let bytes: Arc<[u8]> = Arc::from(extrinsic_bytes);
|
||||
|
||||
// The compact encoded length prefix.
|
||||
let prefix = <Compact<u64>>::decode(&mut &*extrinsic_bytes)?;
|
||||
let prefix_len = <Compact<u64>>::compact_len(&prefix.0);
|
||||
|
||||
// Extrinsic are encoded in memory in the following way:
|
||||
// - first byte: abbbbbbb (a = 0 for unsigned, 1 for signed, b = version)
|
||||
// - signature: [unknown TBD with metadata].
|
||||
// - extrinsic data
|
||||
let version_byte: u8 = Decode::decode(&mut &bytes[prefix_len..])?;
|
||||
|
||||
let version = version_byte & VERSION_MASK;
|
||||
if version != LATEST_EXTRINSIC_VERSION {
|
||||
return Err(BlockError::UnsupportedVersion(version).into());
|
||||
}
|
||||
|
||||
let is_signed = version_byte & SIGNATURE_MASK != 0;
|
||||
|
||||
// Skip over the prefix and first byte which denotes the version and signing.
|
||||
let cursor = &mut &bytes[prefix_len + 1..];
|
||||
|
||||
let signed_details = is_signed
|
||||
.then(|| -> Result<SignedExtrinsicDetails, Error> {
|
||||
let address_start_idx = bytes.len() - cursor.len();
|
||||
// Skip over the address, signature and extra fields.
|
||||
scale_decode::visitor::decode_with_visitor(
|
||||
cursor,
|
||||
ids.address,
|
||||
metadata.types(),
|
||||
scale_decode::visitor::IgnoreVisitor::new(),
|
||||
)
|
||||
.map_err(scale_decode::Error::from)?;
|
||||
let address_end_idx = bytes.len() - cursor.len();
|
||||
|
||||
scale_decode::visitor::decode_with_visitor(
|
||||
cursor,
|
||||
ids.signature,
|
||||
metadata.types(),
|
||||
scale_decode::visitor::IgnoreVisitor::new(),
|
||||
)
|
||||
.map_err(scale_decode::Error::from)?;
|
||||
let signature_end_idx = bytes.len() - cursor.len();
|
||||
|
||||
scale_decode::visitor::decode_with_visitor(
|
||||
cursor,
|
||||
ids.extra,
|
||||
metadata.types(),
|
||||
scale_decode::visitor::IgnoreVisitor::new(),
|
||||
)
|
||||
.map_err(scale_decode::Error::from)?;
|
||||
let extra_end_idx = bytes.len() - cursor.len();
|
||||
|
||||
Ok(SignedExtrinsicDetails {
|
||||
address_start_idx,
|
||||
address_end_idx,
|
||||
signature_end_idx,
|
||||
extra_end_idx,
|
||||
})
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
let call_start_idx = bytes.len() - cursor.len();
|
||||
|
||||
// Decode the pallet index, then the call variant.
|
||||
let cursor = &mut &bytes[call_start_idx..];
|
||||
|
||||
let pallet_index: u8 = Decode::decode(cursor)?;
|
||||
let variant_index: u8 = Decode::decode(cursor)?;
|
||||
|
||||
Ok(ExtrinsicDetails {
|
||||
) -> ExtrinsicDetails<T> {
|
||||
ExtrinsicDetails {
|
||||
index,
|
||||
bytes,
|
||||
signed_details,
|
||||
call_start_idx,
|
||||
pallet_index,
|
||||
variant_index,
|
||||
ext,
|
||||
metadata,
|
||||
_marker: core::marker::PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate and return the hash of the extrinsic, based on the configured hasher.
|
||||
pub fn hash(&self) -> T::Hash {
|
||||
// Use hash(), not hash_of(), because we don't want to double encode the bytes.
|
||||
T::Hasher::hash(&self.bytes)
|
||||
T::Hasher::hash(self.bytes())
|
||||
}
|
||||
|
||||
/// Is the extrinsic signed?
|
||||
pub fn is_signed(&self) -> bool {
|
||||
self.signed_details.is_some()
|
||||
self.decoded_info().is_signed()
|
||||
}
|
||||
|
||||
/// The index of the extrinsic in the block.
|
||||
@@ -275,7 +173,7 @@ where
|
||||
/// - Extra fields
|
||||
/// - Extrinsic call bytes
|
||||
pub fn bytes(&self) -> &[u8] {
|
||||
&self.bytes
|
||||
&self.ext.1
|
||||
}
|
||||
|
||||
/// Return only the bytes representing this extrinsic call:
|
||||
@@ -287,7 +185,7 @@ where
|
||||
///
|
||||
/// Please use [`Self::bytes`] if you want to get all extrinsic bytes.
|
||||
pub fn call_bytes(&self) -> &[u8] {
|
||||
&self.bytes[self.call_start_idx..]
|
||||
&self.bytes()[self.decoded_info().call_data_range()]
|
||||
}
|
||||
|
||||
/// Return the bytes representing the fields stored in this extrinsic.
|
||||
@@ -299,7 +197,7 @@ where
|
||||
pub fn field_bytes(&self) -> &[u8] {
|
||||
// Note: this cannot panic because we checked the extrinsic bytes
|
||||
// to contain at least two bytes.
|
||||
&self.call_bytes()[2..]
|
||||
&self.bytes()[self.decoded_info().call_data_args_range()]
|
||||
}
|
||||
|
||||
/// Return only the bytes of the address that signed this extrinsic.
|
||||
@@ -308,16 +206,16 @@ where
|
||||
///
|
||||
/// Returns `None` if the extrinsic is not signed.
|
||||
pub fn address_bytes(&self) -> Option<&[u8]> {
|
||||
self.signed_details
|
||||
.as_ref()
|
||||
.map(|e| &self.bytes[e.address_start_idx..e.address_end_idx])
|
||||
self.decoded_info()
|
||||
.signature_payload()
|
||||
.map(|s| &self.bytes()[s.address_range()])
|
||||
}
|
||||
|
||||
/// Returns Some(signature_bytes) if the extrinsic was signed otherwise None is returned.
|
||||
pub fn signature_bytes(&self) -> Option<&[u8]> {
|
||||
self.signed_details
|
||||
.as_ref()
|
||||
.map(|e| &self.bytes[e.address_end_idx..e.signature_end_idx])
|
||||
self.decoded_info()
|
||||
.signature_payload()
|
||||
.map(|s| &self.bytes()[s.signature_range()])
|
||||
}
|
||||
|
||||
/// Returns the signed extension `extra` bytes of the extrinsic.
|
||||
@@ -327,26 +225,26 @@ where
|
||||
///
|
||||
/// Note: Returns `None` if the extrinsic is not signed.
|
||||
pub fn signed_extensions_bytes(&self) -> Option<&[u8]> {
|
||||
self.signed_details
|
||||
.as_ref()
|
||||
.map(|e| &self.bytes[e.signature_end_idx..e.extra_end_idx])
|
||||
self.decoded_info()
|
||||
.transaction_extension_payload()
|
||||
.map(|t| &self.bytes()[t.range()])
|
||||
}
|
||||
|
||||
/// Returns `None` if the extrinsic is not signed.
|
||||
pub fn signed_extensions(&self) -> Option<ExtrinsicSignedExtensions<'_, T>> {
|
||||
let signed = self.signed_details.as_ref()?;
|
||||
let extra_bytes = &self.bytes[signed.signature_end_idx..signed.extra_end_idx];
|
||||
Some(ExtrinsicSignedExtensions::new(extra_bytes, &self.metadata))
|
||||
self.decoded_info()
|
||||
.transaction_extension_payload()
|
||||
.map(|t| ExtrinsicSignedExtensions::new(self.bytes(), &self.metadata, t))
|
||||
}
|
||||
|
||||
/// The index of the pallet that the extrinsic originated from.
|
||||
pub fn pallet_index(&self) -> u8 {
|
||||
self.pallet_index
|
||||
self.decoded_info().pallet_index()
|
||||
}
|
||||
|
||||
/// The index of the extrinsic variant that the extrinsic originated from.
|
||||
pub fn variant_index(&self) -> u8 {
|
||||
self.variant_index
|
||||
self.decoded_info().call_index()
|
||||
}
|
||||
|
||||
/// The name of the pallet from whence the extrinsic originated.
|
||||
@@ -418,6 +316,10 @@ where
|
||||
|
||||
Ok(decoded)
|
||||
}
|
||||
|
||||
fn decoded_info(&self) -> &Extrinsic<'static, u32> {
|
||||
&self.ext.0
|
||||
}
|
||||
}
|
||||
|
||||
/// A Static Extrinsic found in a block coupled with it's details.
|
||||
@@ -436,36 +338,6 @@ pub struct ExtrinsicMetadataDetails<'a> {
|
||||
pub variant: &'a scale_info::Variant<scale_info::form::PortableForm>,
|
||||
}
|
||||
|
||||
/// The type IDs extracted from the metadata that represent the
|
||||
/// generic type parameters passed to the `UncheckedExtrinsic` from
|
||||
/// the substrate-based chain.
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct ExtrinsicPartTypeIds {
|
||||
/// The address (source) of the extrinsic.
|
||||
address: u32,
|
||||
/// The extrinsic call type.
|
||||
// Note: the call type can be used to skip over the extrinsic bytes to check
|
||||
// they are in line with our metadata. This operation is currently postponed.
|
||||
_call: u32,
|
||||
/// The signature of the extrinsic.
|
||||
signature: u32,
|
||||
/// The extra parameters of the extrinsic.
|
||||
extra: u32,
|
||||
}
|
||||
|
||||
impl ExtrinsicPartTypeIds {
|
||||
/// Extract the generic type parameters IDs from the extrinsic type.
|
||||
fn new(metadata: &Metadata) -> Result<Self, BlockError> {
|
||||
Ok(ExtrinsicPartTypeIds {
|
||||
address: metadata.extrinsic().address_ty(),
|
||||
_call: metadata.extrinsic().call_ty(),
|
||||
signature: metadata.extrinsic().signature_ty(),
|
||||
extra: metadata.extrinsic().extra_ty(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -615,26 +487,32 @@ mod tests {
|
||||
#[test]
|
||||
fn insufficient_extrinsic_bytes() {
|
||||
let metadata = metadata();
|
||||
let ids = ExtrinsicPartTypeIds::new(&metadata).unwrap();
|
||||
|
||||
// Decode with empty bytes.
|
||||
let result = ExtrinsicDetails::<SubstrateConfig>::decode_from(0, &[], metadata, ids);
|
||||
assert_matches!(result.err(), Some(crate::Error::Codec(_)));
|
||||
let result = Extrinsics::<SubstrateConfig>::decode_from(vec![vec![]], metadata);
|
||||
assert_matches!(
|
||||
result.err(),
|
||||
Some(crate::Error::Block(
|
||||
crate::error::BlockError::ExtrinsicDecodeError(_)
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_version_extrinsic() {
|
||||
use frame_decode::extrinsics::ExtrinsicDecodeError;
|
||||
|
||||
let metadata = metadata();
|
||||
let ids = ExtrinsicPartTypeIds::new(&metadata).unwrap();
|
||||
|
||||
// Decode with invalid version.
|
||||
let result =
|
||||
ExtrinsicDetails::<SubstrateConfig>::decode_from(0, &vec![3u8].encode(), metadata, ids);
|
||||
let result = Extrinsics::<SubstrateConfig>::decode_from(vec![vec![3u8].encode()], metadata);
|
||||
|
||||
assert_matches!(
|
||||
result.err(),
|
||||
Some(crate::Error::Block(
|
||||
crate::error::BlockError::UnsupportedVersion(3)
|
||||
crate::error::BlockError::ExtrinsicDecodeError(
|
||||
ExtrinsicDecodeError::VersionNotSupported(3)
|
||||
)
|
||||
))
|
||||
);
|
||||
}
|
||||
@@ -642,7 +520,6 @@ mod tests {
|
||||
#[test]
|
||||
fn tx_hashes_line_up() {
|
||||
let metadata = metadata();
|
||||
let ids = ExtrinsicPartTypeIds::new(&metadata).unwrap();
|
||||
|
||||
let tx = crate::dynamic::tx(
|
||||
"Test",
|
||||
@@ -659,14 +536,14 @@ mod tests {
|
||||
.expect("Valid dynamic parameters are provided");
|
||||
|
||||
// Extrinsic details ready to decode.
|
||||
let extrinsic = ExtrinsicDetails::<SubstrateConfig>::decode_from(
|
||||
1,
|
||||
tx_encoded.encoded(),
|
||||
let extrinsics = Extrinsics::<SubstrateConfig>::decode_from(
|
||||
vec![tx_encoded.encoded().to_owned()],
|
||||
metadata,
|
||||
ids,
|
||||
)
|
||||
.expect("Valid extrinsic");
|
||||
|
||||
let extrinsic = extrinsics.iter().next().unwrap();
|
||||
|
||||
// Both of these types should produce the same bytes.
|
||||
assert_eq!(tx_encoded.encoded(), extrinsic.bytes(), "bytes should eq");
|
||||
// Both of these types should produce the same hash.
|
||||
@@ -676,7 +553,6 @@ mod tests {
|
||||
#[test]
|
||||
fn statically_decode_extrinsic() {
|
||||
let metadata = metadata();
|
||||
let ids = ExtrinsicPartTypeIds::new(&metadata).unwrap();
|
||||
|
||||
let tx = crate::dynamic::tx(
|
||||
"Test",
|
||||
@@ -691,18 +567,18 @@ mod tests {
|
||||
.expect("Valid dynamic parameters are provided");
|
||||
|
||||
// Note: `create_unsigned` produces the extrinsic bytes by prefixing the extrinsic length.
|
||||
// The length is handled by deserializing `ChainBlockExtrinsic`, therefore the first byte is not needed.
|
||||
let extrinsic = ExtrinsicDetails::<SubstrateConfig>::decode_from(
|
||||
1,
|
||||
tx_encoded.encoded(),
|
||||
// The length is handled deserializing `ChainBlockExtrinsic`, therefore the first byte is not needed.
|
||||
let extrinsics = Extrinsics::<SubstrateConfig>::decode_from(
|
||||
vec![tx_encoded.encoded().to_owned()],
|
||||
metadata,
|
||||
ids,
|
||||
)
|
||||
.expect("Valid extrinsic");
|
||||
|
||||
let extrinsic = extrinsics.iter().next().unwrap();
|
||||
|
||||
assert!(!extrinsic.is_signed());
|
||||
|
||||
assert_eq!(extrinsic.index(), 1);
|
||||
assert_eq!(extrinsic.index(), 0);
|
||||
|
||||
assert_eq!(extrinsic.pallet_index(), 0);
|
||||
assert_eq!(
|
||||
|
||||
@@ -45,14 +45,12 @@
|
||||
//!
|
||||
//! // We can iterate over them and decode various details out of them.
|
||||
//! for ext in exts.iter() {
|
||||
//! let ext = ext.unwrap();
|
||||
//! println!("Pallet: {}", ext.pallet_name().unwrap());
|
||||
//! println!("Call: {}", ext.variant_name().unwrap());
|
||||
//! }
|
||||
//!
|
||||
//! # let ext_details: Vec<_> = exts.iter()
|
||||
//! # .map(|ext| {
|
||||
//! # let ext = ext.unwrap();
|
||||
//! # let pallet = ext.pallet_name().unwrap().to_string();
|
||||
//! # let call = ext.variant_name().unwrap().to_string();
|
||||
//! # (pallet, call)
|
||||
@@ -71,14 +69,13 @@ mod extrinsics;
|
||||
mod static_extrinsic;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::error::BlockError;
|
||||
use crate::error::Error;
|
||||
use crate::Metadata;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
pub use crate::error::BlockError;
|
||||
pub use extrinsic_signed_extensions::{ExtrinsicSignedExtension, ExtrinsicSignedExtensions};
|
||||
pub use extrinsics::{
|
||||
ExtrinsicDetails, ExtrinsicMetadataDetails, Extrinsics, FoundExtrinsic, SignedExtrinsicDetails,
|
||||
};
|
||||
pub use extrinsics::{ExtrinsicDetails, ExtrinsicMetadataDetails, Extrinsics, FoundExtrinsic};
|
||||
pub use static_extrinsic::StaticExtrinsic;
|
||||
|
||||
/// Instantiate a new [`Extrinsics`] object, given a vector containing each extrinsic hash (in the
|
||||
@@ -88,6 +85,6 @@ pub use static_extrinsic::StaticExtrinsic;
|
||||
pub fn decode_from<T: Config>(
|
||||
extrinsics: Vec<Vec<u8>>,
|
||||
metadata: Metadata,
|
||||
) -> Result<Extrinsics<T>, BlockError> {
|
||||
) -> Result<Extrinsics<T>, Error> {
|
||||
Extrinsics::decode_from(extrinsics, metadata)
|
||||
}
|
||||
|
||||
+17
-14
@@ -56,29 +56,32 @@ impl_from!(StorageAddressError => Error::StorageAddress);
|
||||
impl_from!(codec::Error => Error::Codec);
|
||||
|
||||
/// Block error
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Debug)]
|
||||
pub enum BlockError {
|
||||
/// Extrinsic type ID cannot be resolved with the provided metadata.
|
||||
MissingType,
|
||||
/// Unsupported signature.
|
||||
/// The extrinsic has an unsupported version.
|
||||
UnsupportedVersion(u8),
|
||||
/// Decoding error.
|
||||
DecodingError(codec::Error),
|
||||
/// Leftover bytes found after decoding the extrinsic.
|
||||
LeftoverBytes(usize),
|
||||
/// Something went wrong decoding the extrinsic.
|
||||
ExtrinsicDecodeError(ExtrinsicDecodeError),
|
||||
}
|
||||
|
||||
impl Display for BlockError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
BlockError::MissingType => write!(f, "Extrinsic type ID cannot be resolved with the provided metadata. Make sure this is a valid metadata"),
|
||||
BlockError::UnsupportedVersion(_) => write!(f, "Unsupported extrinsic version, only version 4 is supported currently"),
|
||||
BlockError::DecodingError(e) => write!(f, "Cannot decode extrinsic: {e}"),
|
||||
BlockError::LeftoverBytes(n) => {
|
||||
write!(
|
||||
f,
|
||||
"After decoding, {n} bytes were left, suggesting that decoding may have failed"
|
||||
)
|
||||
}
|
||||
BlockError::ExtrinsicDecodeError(e) => {
|
||||
write!(f, "{e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for BlockError {}
|
||||
/// An alias for [`frame_decode::extrinsics::ExtrinsicDecodeError`].
|
||||
///
|
||||
pub type ExtrinsicDecodeError = frame_decode::extrinsics::ExtrinsicDecodeError;
|
||||
|
||||
/// Something went wrong trying to access details in the metadata.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
|
||||
@@ -49,7 +49,6 @@ pub(crate) async fn subscribe_to_finalized_blocks(
|
||||
writeln!(output, " Extrinsics:").ok();
|
||||
let extrinsics = block.extrinsics().await?;
|
||||
for ext in extrinsics.iter() {
|
||||
let ext = ext?;
|
||||
let idx = ext.index();
|
||||
let events = ext.events().await?;
|
||||
let bytes_hex = format!("0x{}", hex::encode(ext.bytes()));
|
||||
|
||||
@@ -19,6 +19,7 @@ std = ["scale-info/std", "frame-metadata/std"]
|
||||
|
||||
[dependencies]
|
||||
scale-info = { workspace = true, default-features = false }
|
||||
frame-decode = { workspace = true }
|
||||
frame-metadata = { workspace = true, default-features = false, features = ["current", "decode"] }
|
||||
codec = { package = "parity-scale-codec", workspace = true, default-features = false, features = ["derive"] }
|
||||
sp-crypto-hashing = { workspace = true }
|
||||
|
||||
@@ -22,9 +22,13 @@ extern crate alloc;
|
||||
mod from_into;
|
||||
mod utils;
|
||||
|
||||
use alloc::borrow::Cow;
|
||||
use alloc::string::String;
|
||||
use alloc::sync::Arc;
|
||||
use alloc::vec::Vec;
|
||||
use frame_decode::extrinsics::{
|
||||
ExtrinsicInfo, ExtrinsicInfoArg, ExtrinsicInfoError, ExtrinsicSignatureInfo,
|
||||
};
|
||||
use hashbrown::HashMap;
|
||||
use scale_info::{form::PortableForm, PortableRegistry, Variant};
|
||||
use utils::variant_index::VariantIndex;
|
||||
@@ -61,6 +65,63 @@ pub struct Metadata {
|
||||
custom: frame_metadata::v15::CustomMetadata<PortableForm>,
|
||||
}
|
||||
|
||||
// Since we've abstracted away from frame-metadatas, we impl this on our custom Metadata
|
||||
// so that it can be used by `frame-decode` to obtain the relevant extrinsic info.
|
||||
impl frame_decode::extrinsics::ExtrinsicTypeInfo for Metadata {
|
||||
type TypeId = u32;
|
||||
|
||||
fn get_extrinsic_info(
|
||||
&self,
|
||||
pallet_index: u8,
|
||||
call_index: u8,
|
||||
) -> Result<ExtrinsicInfo<'_, Self::TypeId>, ExtrinsicInfoError<'_>> {
|
||||
let pallet = self.pallet_by_index(pallet_index).ok_or({
|
||||
ExtrinsicInfoError::PalletNotFound {
|
||||
index: pallet_index,
|
||||
}
|
||||
})?;
|
||||
|
||||
let call = pallet.call_variant_by_index(call_index).ok_or_else(|| {
|
||||
ExtrinsicInfoError::CallNotFound {
|
||||
index: call_index,
|
||||
pallet_index,
|
||||
pallet_name: Cow::Borrowed(pallet.name()),
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(ExtrinsicInfo {
|
||||
pallet_name: Cow::Borrowed(pallet.name()),
|
||||
call_name: Cow::Borrowed(&call.name),
|
||||
args: call
|
||||
.fields
|
||||
.iter()
|
||||
.map(|f| ExtrinsicInfoArg {
|
||||
name: Cow::Borrowed(f.name.as_deref().unwrap_or("")),
|
||||
id: f.ty.id,
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
fn get_signature_info(
|
||||
&self,
|
||||
) -> Result<ExtrinsicSignatureInfo<'_, Self::TypeId>, ExtrinsicInfoError<'_>> {
|
||||
Ok(ExtrinsicSignatureInfo {
|
||||
address_id: self.extrinsic().address_ty(),
|
||||
signature_id: self.extrinsic().signature_ty(),
|
||||
transaction_extension_ids: self
|
||||
.extrinsic()
|
||||
.signed_extensions()
|
||||
.iter()
|
||||
.map(|f| ExtrinsicInfoArg {
|
||||
name: Cow::Borrowed(f.identifier()),
|
||||
id: f.extra_ty(),
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Metadata {
|
||||
/// Access the underlying type registry.
|
||||
pub fn types(&self) -> &PortableRegistry {
|
||||
|
||||
@@ -17,8 +17,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Decode each signed extrinsic in the block dynamically
|
||||
let extrinsics = block.extrinsics().await?;
|
||||
for ext in extrinsics.iter() {
|
||||
let ext = ext?;
|
||||
|
||||
let Some(signed_extensions) = ext.signed_extensions() else {
|
||||
continue; // we do not look at inherents in this example
|
||||
};
|
||||
@@ -29,7 +27,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!(" {}/{}", meta.pallet.name(), meta.variant.name);
|
||||
println!(" Signed Extensions:");
|
||||
for signed_ext in signed_extensions.iter() {
|
||||
let signed_ext = signed_ext?;
|
||||
// We only want to take a look at these 3 signed extensions, because the others all just have unit fields.
|
||||
if ["CheckMortality", "CheckNonce", "ChargeTransactionPayment"]
|
||||
.contains(&signed_ext.name())
|
||||
|
||||
@@ -26,7 +26,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Log each of the extrinsic with it's associated events:
|
||||
let extrinsics = block.extrinsics().await?;
|
||||
for ext in extrinsics.iter() {
|
||||
let ext = ext?;
|
||||
let idx = ext.index();
|
||||
let events = ext.events().await?;
|
||||
let bytes_hex = format!("0x{}", hex::encode(ext.bytes()));
|
||||
@@ -52,7 +51,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!(" Signed Extensions:");
|
||||
if let Some(signed_extensions) = ext.signed_extensions() {
|
||||
for signed_extension in signed_extensions.iter() {
|
||||
let signed_extension = signed_extension?;
|
||||
let name = signed_extension.name();
|
||||
let value = signed_extension.value()?.to_string();
|
||||
println!(" {name}: {value}");
|
||||
|
||||
@@ -84,12 +84,12 @@ where
|
||||
return Err(BlockError::not_found(block_hash).into());
|
||||
};
|
||||
|
||||
Ok(Extrinsics::new(
|
||||
Extrinsics::new(
|
||||
self.client.clone(),
|
||||
extrinsics,
|
||||
self.cached_events.clone(),
|
||||
block_hash,
|
||||
)?)
|
||||
)
|
||||
}
|
||||
|
||||
/// Work with storage.
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
blocks::block_types::{get_events, CachedEvents},
|
||||
client::{OfflineClientT, OnlineClientT},
|
||||
config::{Config, Hasher},
|
||||
error::{BlockError, Error},
|
||||
error::Error,
|
||||
events,
|
||||
};
|
||||
|
||||
@@ -37,7 +37,7 @@ where
|
||||
extrinsics: Vec<Vec<u8>>,
|
||||
cached_events: CachedEvents<T>,
|
||||
hash: T::Hash,
|
||||
) -> Result<Self, BlockError> {
|
||||
) -> Result<Self, Error> {
|
||||
let inner = CoreExtrinsics::decode_from(extrinsics, client.metadata())?;
|
||||
Ok(Self {
|
||||
inner,
|
||||
@@ -65,21 +65,13 @@ where
|
||||
/// Returns an iterator over the extrinsics in the block body.
|
||||
// Dev note: The returned iterator is 'static + Send so that we can box it up and make
|
||||
// use of it with our `FilterExtrinsic` stuff.
|
||||
pub fn iter(
|
||||
&self,
|
||||
) -> impl Iterator<Item = Result<ExtrinsicDetails<T, C>, Error>> + Send + Sync + 'static {
|
||||
pub fn iter(&self) -> impl Iterator<Item = ExtrinsicDetails<T, C>> + Send + Sync + 'static {
|
||||
let client = self.client.clone();
|
||||
let cached_events = self.cached_events.clone();
|
||||
let block_hash = self.hash;
|
||||
|
||||
self.inner.iter().map(move |res| {
|
||||
let inner = res?;
|
||||
Ok(ExtrinsicDetails::new(
|
||||
inner,
|
||||
client.clone(),
|
||||
block_hash,
|
||||
cached_events.clone(),
|
||||
))
|
||||
self.inner.iter().map(move |inner| {
|
||||
ExtrinsicDetails::new(inner, client.clone(), block_hash, cached_events.clone())
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+7
-12
@@ -165,30 +165,25 @@ impl RpcError {
|
||||
}
|
||||
|
||||
/// Block error
|
||||
#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
|
||||
#[derive(Clone, Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum BlockError {
|
||||
/// An error containing the hash of the block that was not found.
|
||||
#[error("Could not find a block with hash {0} (perhaps it was on a non-finalized fork?)")]
|
||||
NotFound(String),
|
||||
/// Extrinsic type ID cannot be resolved with the provided metadata.
|
||||
#[error("Extrinsic type ID cannot be resolved with the provided metadata. Make sure this is a valid metadata")]
|
||||
MissingType,
|
||||
/// Unsupported signature.
|
||||
#[error("Unsupported extrinsic version, only version 4 is supported currently")]
|
||||
/// The extrinsic has an unsupported version.
|
||||
UnsupportedVersion(u8),
|
||||
/// Leftover bytes found after decoding the extrinsic.
|
||||
#[error("After decoding, {0} bytes were left, suggesting that decoding may have failed")]
|
||||
LeftoverBytes(usize),
|
||||
/// Decoding error.
|
||||
#[error("Cannot decode extrinsic: {0}")]
|
||||
DecodingError(codec::Error),
|
||||
ExtrinsicDecodeError(subxt_core::error::ExtrinsicDecodeError),
|
||||
}
|
||||
|
||||
impl From<CoreBlockError> for BlockError {
|
||||
fn from(value: CoreBlockError) -> Self {
|
||||
match value {
|
||||
CoreBlockError::MissingType => BlockError::MissingType,
|
||||
CoreBlockError::UnsupportedVersion(n) => BlockError::UnsupportedVersion(n),
|
||||
CoreBlockError::DecodingError(e) => BlockError::DecodingError(e),
|
||||
CoreBlockError::LeftoverBytes(n) => BlockError::LeftoverBytes(n),
|
||||
CoreBlockError::ExtrinsicDecodeError(e) => BlockError::ExtrinsicDecodeError(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,10 +222,7 @@ async fn fetch_block_and_decode_extrinsic_details() {
|
||||
.unwrap()
|
||||
.is_some());
|
||||
|
||||
let block_extrinsics = extrinsics
|
||||
.iter()
|
||||
.map(|res| res.unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
let block_extrinsics = extrinsics.iter().collect::<Vec<_>>();
|
||||
|
||||
let mut balance = None;
|
||||
let mut timestamp = None;
|
||||
@@ -297,10 +294,7 @@ async fn decode_signed_extensions_from_blocks() {
|
||||
let block_hash = in_block.block_hash();
|
||||
let block = api.blocks().at(block_hash).await.unwrap();
|
||||
let extrinsics = block.extrinsics().await.unwrap();
|
||||
let extrinsic_details = extrinsics
|
||||
.iter()
|
||||
.find_map(|e| e.ok().filter(|e| e.is_signed()))
|
||||
.unwrap();
|
||||
let extrinsic_details = extrinsics.iter().find(|e| e.is_signed()).unwrap();
|
||||
extrinsic_details
|
||||
}};
|
||||
}
|
||||
@@ -351,12 +345,12 @@ async fn decode_signed_extensions_from_blocks() {
|
||||
|
||||
assert_eq!(extensions1.iter().count(), expected_signed_extensions.len());
|
||||
for (e, expected_name) in extensions1.iter().zip(expected_signed_extensions.iter()) {
|
||||
assert_eq!(e.unwrap().name(), *expected_name);
|
||||
assert_eq!(e.name(), *expected_name);
|
||||
}
|
||||
|
||||
assert_eq!(extensions2.iter().count(), expected_signed_extensions.len());
|
||||
for (e, expected_name) in extensions2.iter().zip(expected_signed_extensions.iter()) {
|
||||
assert_eq!(e.unwrap().name(), *expected_name);
|
||||
assert_eq!(e.name(), *expected_name);
|
||||
}
|
||||
|
||||
// check that era decodes:
|
||||
|
||||
@@ -367,10 +367,7 @@ async fn transaction_v1_broadcast() {
|
||||
}
|
||||
|
||||
let extrinsics = finalized.extrinsics().await.unwrap();
|
||||
let block_extrinsics = extrinsics
|
||||
.iter()
|
||||
.map(|res| res.unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
let block_extrinsics = extrinsics.iter().collect::<Vec<_>>();
|
||||
|
||||
let Some(ext) = block_extrinsics
|
||||
.iter()
|
||||
|
||||
Reference in New Issue
Block a user