extrinsics: Decode extrinsics from blocks (#929)

* Update polkadot.scale

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* extrinsics: Add extrinsics client

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* extrinsics: Decode extrinsics

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* subxt: Add extrinsic error

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* blocks: Expose extrinsics

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* examples: Fetch and decode block extrinsics

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Fix clippy

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* extrinsics: Fetch pallet and variant index

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* subxt: Move extrinsics on the subxt::blocks

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* example: Adjust example

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* metadata: Collect ExtrinsicMetadata

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* subxt: Implement StaticExtrinsic for the calls

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Adjust examples

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Add root level Call enum

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Adjust testing

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* subxt: Add new decode interface

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* subxt: Merge ExtrinsicError with BlockError

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* examples: Find first extrinsic

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Move code to extrinsic_types

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Add Extrinsic struct

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Adjust examples

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* test: Decode extinsics

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* extrinsics/test: Add fake metadata for static decoding

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* extrinsics/test: Decode from insufficient bytes

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* extrinsics/test: Check unsupported versions

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* extrinsics/test: Statically decode to root and pallet enums

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* extrinsics/tests: Remove clones

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* blocks: Fetch block body inline

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* blocks: Rename ExtrinsicIds to ExtrinsicPartTypeIds

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* extrinsics/test: Check decode as_extrinsic

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* blocks: Remove InsufficientData error

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* blocks: Return error from extrinsic_metadata

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* extrinsics: Postpone decoding of call bytes

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* metadata_type: Rename variables

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Adjust calls path for example and tests

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* examples: Remove traces

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* book: Add extrinsics documentation

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* book: Improve extrinsics docs

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

---------

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>
Co-authored-by: James Wilson <james@jsdw.me>
This commit is contained in:
Alexandru Vasile
2023-05-10 13:18:39 +03:00
committed by GitHub
parent b632ce7673
commit 3f16bb8d52
19 changed files with 6493 additions and 164 deletions
Generated
+1
View File
@@ -3537,6 +3537,7 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
name = "subxt"
version = "0.28.0"
dependencies = [
"assert_matches",
"base58",
"bitvec",
"blake2",
+7
View File
@@ -83,6 +83,11 @@ pub fn generate_calls(
// The call structure's documentation was stripped above.
let call_struct = quote! {
#struct_def
impl #crate_path::blocks::StaticExtrinsic for #struct_name {
const PALLET: &'static str = #pallet_name;
const CALL: &'static str = #call_name;
}
};
let client_fn = quote! {
@@ -106,6 +111,7 @@ pub fn generate_calls(
.into_iter()
.unzip();
let call_type = type_gen.resolve_type_path(call.ty.id);
let call_ty = type_gen.resolve_type(call.ty.id);
let docs = &call_ty.docs;
let docs = should_gen_docs
@@ -114,6 +120,7 @@ pub fn generate_calls(
Ok(quote! {
#docs
pub type Call = #call_type;
pub mod calls {
use super::root_mod;
use super::#types_mod_ident;
+49
View File
@@ -367,6 +367,26 @@ impl RuntimeGenerator {
}
};
let outer_extrinsic_variants = self.metadata.pallets.iter().filter_map(|p| {
let variant_name = format_ident!("{}", p.name);
let mod_name = format_ident!("{}", p.name.to_string().to_snake_case());
let index = proc_macro2::Literal::u8_unsuffixed(p.index);
p.calls.as_ref().map(|_| {
quote! {
#[codec(index = #index)]
#variant_name(#mod_name::Call),
}
})
});
let outer_extrinsic = quote! {
#default_derives
pub enum Call {
#( #outer_extrinsic_variants )*
}
};
let root_event_if_arms = self.metadata.pallets.iter().filter_map(|p| {
let variant_name_str = &p.name;
let variant_name = format_ident!("{}", variant_name_str);
@@ -385,6 +405,24 @@ impl RuntimeGenerator {
})
});
let root_extrinsic_if_arms = self.metadata.pallets.iter().filter_map(|p| {
let variant_name_str = &p.name;
let variant_name = format_ident!("{}", variant_name_str);
let mod_name = format_ident!("{}", variant_name_str.to_string().to_snake_case());
p.calls.as_ref().map(|_| {
// An 'if' arm for the RootExtrinsic impl to match this variant name:
quote! {
if pallet_name == #variant_name_str {
return Ok(Call::#variant_name(#mod_name::Call::decode_with_metadata(
&mut &*pallet_bytes,
pallet_ty,
metadata
)?));
}
}
})
});
let outer_error_variants = self.metadata.pallets.iter().filter_map(|p| {
let variant_name = format_ident!("{}", p.name);
let mod_name = format_ident!("{}", p.name.to_string().to_snake_case());
@@ -483,7 +521,18 @@ impl RuntimeGenerator {
}
}
#outer_extrinsic
impl #crate_path::blocks::RootExtrinsic for Call {
fn root_extrinsic(pallet_bytes: &[u8], pallet_name: &str, pallet_ty: u32, metadata: &#crate_path::Metadata) -> Result<Self, #crate_path::Error> {
use #crate_path::metadata::DecodeWithMetadata;
#( #root_extrinsic_if_arms )*
Err(#crate_path::ext::scale_decode::Error::custom(format!("Pallet name '{}' not found in root Call enum", pallet_name)).into())
}
}
#outer_error
impl #crate_path::error::RootError for Error {
fn root_error(pallet_bytes: &[u8], pallet_name: &str, metadata: &#crate_path::Metadata) -> Result<Self, #crate_path::Error> {
use #crate_path::metadata::DecodeWithMetadata;
+5
View File
@@ -53,6 +53,11 @@ pub enum CodegenError {
"{0} type should be an variant/enum type. Make sure you are providing a valid substrate-based metadata"
)]
InvalidType(String),
/// Extrinsic call type could not be found.
#[error(
"Extrinsic call type could not be found. Make sure you are providing a valid substrate-based metadata"
)]
MissingCallType,
}
impl CodegenError {
+99
View File
@@ -0,0 +1,99 @@
// Copyright 2019-2023 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! To run this example, a local polkadot node should be running. Example verified against polkadot v0.9.28-9ffe6e9e3da.
//!
//! E.g.
//! ```bash
//! curl "https://github.com/paritytech/polkadot/releases/download/v0.9.28/polkadot" --output /usr/local/bin/polkadot --location
//! polkadot --dev --tmp
//! ```
use futures::StreamExt;
use sp_keyring::AccountKeyring;
use std::time::Duration;
use subxt::{tx::PairSigner, OnlineClient, PolkadotConfig};
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata.scale")]
pub mod polkadot {}
/// Subscribe to all events, and then manually look through them and
/// pluck out the events that we care about.
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a client to use:
let api = OnlineClient::<PolkadotConfig>::new().await?;
// Subscribe to (in this case, finalized) blocks.
let mut block_sub = api.blocks().subscribe_finalized().await?;
// While this subscription is active, balance transfers are made somewhere:
tokio::task::spawn({
let api = api.clone();
async move {
let signer = PairSigner::new(AccountKeyring::Alice.pair());
let mut transfer_amount = 1_000_000_000;
// Make small balance transfers from Alice to Bob in a loop:
loop {
let transfer_tx = polkadot::tx()
.balances()
.transfer(AccountKeyring::Bob.to_account_id().into(), transfer_amount);
api.tx()
.sign_and_submit_default(&transfer_tx, &signer)
.await
.unwrap();
tokio::time::sleep(Duration::from_secs(10)).await;
transfer_amount += 100_000_000;
}
}
});
// Get each finalized block as it arrives.
while let Some(block) = block_sub.next().await {
let block = block?;
let block_hash = block.hash();
println!(" Block {:?}", block_hash);
// Ask for the extrinsics for this block.
let extrinsics = block.body().await?.extrinsics();
let transfer_tx = extrinsics.find_first::<polkadot::balances::calls::types::Transfer>();
println!(" Transfer tx: {:?}", transfer_tx);
// Ask for the extrinsics for this block.
for extrinsic in extrinsics.iter() {
let extrinsic = extrinsic?;
println!(
" Extrinsic block index {:?}, pallet index {:?}, variant index {:?}",
extrinsic.index(),
extrinsic.pallet_index(),
extrinsic.variant_index()
);
let root_extrinsic = extrinsic.as_root_extrinsic::<polkadot::Call>();
println!(" As root extrinsic {:?}\n", root_extrinsic);
let pallet_extrinsic = extrinsic
.as_pallet_extrinsic::<polkadot::runtime_types::pallet_balances::pallet::Call>();
println!(
" Extrinsic as Balances Pallet call: {:?}\n",
pallet_extrinsic
);
let call = extrinsic.as_extrinsic::<polkadot::balances::calls::types::Transfer>()?;
println!(
" Extrinsic as polkadot::balances::calls::Transfer: {:?}\n\n",
call
);
}
println!("\n");
}
Ok(())
}
+2 -1
View File
@@ -25,7 +25,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Log each of the extrinsic with it's associated events:
let body = block.body().await?;
for ext in body.extrinsics() {
for ext in body.extrinsics().iter() {
let ext = ext?;
let idx = ext.index();
let events = ext.events().await?;
let bytes_hex = format!("0x{}", hex::encode(ext.bytes()));
+1
View File
@@ -90,3 +90,4 @@ sp-core = { workspace = true }
sp-runtime = { workspace = true }
sp-keyring = { workspace = true }
sp-version = { workspace = true }
assert_matches = { workspace = true }
+22 -154
View File
@@ -3,15 +3,16 @@
// see LICENSE for license details.
use crate::{
blocks::{extrinsic_types::ExtrinsicPartTypeIds, Extrinsics},
client::{OfflineClientT, OnlineClientT},
config::{Config, Hasher, Header},
config::{Config, Header},
error::{BlockError, Error},
events,
rpc::types::ChainBlockResponse,
runtime_api::RuntimeApi,
storage::Storage,
};
use derivative::Derivative;
use futures::lock::Mutex as AsyncMutex;
use std::sync::Arc;
@@ -26,7 +27,7 @@ pub struct Block<T: Config, C> {
// A cache for our events so we don't fetch them more than once when
// iterating over events for extrinsics.
type CachedEvents<T> = Arc<AsyncMutex<Option<events::Events<T>>>>;
pub(crate) type CachedEvents<T> = Arc<AsyncMutex<Option<events::Events<T>>>>;
impl<T, C> Block<T, C>
where
@@ -69,16 +70,17 @@ where
/// Fetch and return the block body.
pub async fn body(&self) -> Result<BlockBody<T, C>, Error> {
let ids = ExtrinsicPartTypeIds::new(self.client.metadata().runtime_metadata())?;
let block_hash = self.header.hash();
let block_details = match self.client.rpc().block(Some(block_hash)).await? {
Some(block) => block,
None => return Err(BlockError::not_found(block_hash).into()),
let Some(block_details) = self.client.rpc().block(Some(block_hash)).await? else {
return Err(BlockError::not_found(block_hash).into());
};
Ok(BlockBody::new(
self.client.clone(),
block_details,
self.cached_events.clone(),
ids,
))
}
@@ -99,6 +101,7 @@ pub struct BlockBody<T: Config, C> {
details: ChainBlockResponse<T>,
client: C,
cached_events: CachedEvents<T>,
ids: ExtrinsicPartTypeIds,
}
impl<T, C> BlockBody<T, C>
@@ -110,167 +113,32 @@ where
client: C,
details: ChainBlockResponse<T>,
cached_events: CachedEvents<T>,
ids: ExtrinsicPartTypeIds,
) -> Self {
Self {
details,
client,
cached_events,
ids,
}
}
/// Returns an iterator over the extrinsics in the block body.
pub fn extrinsics(&self) -> impl Iterator<Item = Extrinsic<'_, T, C>> {
self.details
.block
.extrinsics
.iter()
.enumerate()
.map(|(idx, e)| Extrinsic {
index: idx as u32,
bytes: &e.0,
client: self.client.clone(),
block_hash: self.details.block.header.hash(),
cached_events: self.cached_events.clone(),
_marker: std::marker::PhantomData,
})
}
}
/// A single extrinsic in a block.
pub struct Extrinsic<'a, T: Config, C> {
index: u32,
bytes: &'a [u8],
client: C,
block_hash: T::Hash,
cached_events: CachedEvents<T>,
_marker: std::marker::PhantomData<T>,
}
impl<'a, T, C> Extrinsic<'a, T, C>
where
T: Config,
C: OfflineClientT<T>,
{
/// The index of the extrinsic in the block.
pub fn index(&self) -> u32 {
self.index
}
/// The bytes of the extrinsic.
pub fn bytes(&self) -> &'a [u8] {
self.bytes
}
}
impl<'a, T, C> Extrinsic<'a, T, C>
where
T: Config,
C: OnlineClientT<T>,
{
/// The events associated with the extrinsic.
pub async fn events(&self) -> Result<ExtrinsicEvents<T>, Error> {
let events = get_events(&self.client, self.block_hash, &self.cached_events).await?;
let ext_hash = T::Hasher::hash_of(&self.bytes);
Ok(ExtrinsicEvents::new(ext_hash, self.index, events))
}
}
/// The events associated with a given extrinsic.
#[derive(Derivative)]
#[derivative(Debug(bound = ""))]
pub struct ExtrinsicEvents<T: Config> {
// The hash of the extrinsic (handy to expose here because
// this type is returned from TxProgress things in the most
// basic flows, so it's the only place people can access it
// without complicating things for themselves).
ext_hash: T::Hash,
// The index of the extrinsic:
idx: u32,
// All of the events in the block:
events: events::Events<T>,
}
impl<T: Config> ExtrinsicEvents<T> {
pub(crate) fn new(ext_hash: T::Hash, idx: u32, events: events::Events<T>) -> Self {
Self {
ext_hash,
idx,
events,
}
}
/// Return the hash of the block that the extrinsic is in.
pub fn block_hash(&self) -> T::Hash {
self.events.block_hash()
}
/// The index of the extrinsic that these events are produced from.
pub fn extrinsic_index(&self) -> u32 {
self.idx
}
/// Return the hash of the extrinsic.
pub fn extrinsic_hash(&self) -> T::Hash {
self.ext_hash
}
/// Return all of the events in the block that the extrinsic is in.
pub fn all_events_in_block(&self) -> &events::Events<T> {
&self.events
}
/// Iterate over all of the raw events associated with this transaction.
///
/// This works in the same way that [`events::Events::iter()`] does, with the
/// exception that it filters out events not related to the submitted extrinsic.
pub fn iter(&self) -> impl Iterator<Item = Result<events::EventDetails, Error>> + '_ {
self.events.iter().filter(|ev| {
ev.as_ref()
.map(|ev| ev.phase() == events::Phase::ApplyExtrinsic(self.idx))
.unwrap_or(true) // Keep any errors.
})
}
/// Find all of the transaction events matching the event type provided as a generic parameter.
///
/// This works in the same way that [`events::Events::find()`] does, with the
/// exception that it filters out events not related to the submitted extrinsic.
pub fn find<Ev: events::StaticEvent>(&self) -> impl Iterator<Item = Result<Ev, Error>> + '_ {
self.iter().filter_map(|ev| {
ev.and_then(|ev| ev.as_event::<Ev>().map_err(Into::into))
.transpose()
})
}
/// Iterate through the transaction events using metadata to dynamically decode and skip
/// them, and return the first event found which decodes to the provided `Ev` type.
///
/// This works in the same way that [`events::Events::find_first()`] does, with the
/// exception that it ignores events not related to the submitted extrinsic.
pub fn find_first<Ev: events::StaticEvent>(&self) -> Result<Option<Ev>, Error> {
self.find::<Ev>().next().transpose()
}
/// Iterate through the transaction events using metadata to dynamically decode and skip
/// them, and return the last event found which decodes to the provided `Ev` type.
///
/// This works in the same way that [`events::Events::find_last()`] does, with the
/// exception that it ignores events not related to the submitted extrinsic.
pub fn find_last<Ev: events::StaticEvent>(&self) -> Result<Option<Ev>, Error> {
self.find::<Ev>().last().transpose()
}
/// Find an event in those associated with this transaction. Returns true if it was found.
///
/// This works in the same way that [`events::Events::has()`] does, with the
/// exception that it ignores events not related to the submitted extrinsic.
pub fn has<Ev: events::StaticEvent>(&self) -> Result<bool, Error> {
Ok(self.find::<Ev>().next().transpose()?.is_some())
// 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 extrinsics(&self) -> Extrinsics<T, C> {
Extrinsics::new(
self.client.clone(),
self.details.block.extrinsics.clone(),
self.cached_events.clone(),
self.ids,
self.details.block.header.hash(),
)
}
}
// Return Events from the cache, or fetch from the node if needed.
async fn get_events<C, T>(
pub(crate) async fn get_events<C, T>(
client: &C,
block_hash: T::Hash,
cached_events: &AsyncMutex<Option<events::Events<T>>>,
+919
View File
@@ -0,0 +1,919 @@
// Copyright 2019-2023 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use crate::{
blocks::block_types::{get_events, CachedEvents},
client::{OfflineClientT, OnlineClientT},
config::{Config, Hasher},
error::{BlockError, Error},
events,
metadata::{DecodeWithMetadata, ExtrinsicMetadata},
rpc::types::ChainBlockExtrinsic,
Metadata,
};
use codec::Decode;
use derivative::Derivative;
use frame_metadata::v15::RuntimeMetadataV15;
use scale_decode::DecodeAsFields;
use std::{collections::HashMap, sync::Arc};
/// Trait to uniquely identify the extrinsic's identity from the runtime metadata.
///
/// Generated API structures that represent an extrinsic implement this trait.
///
/// The trait is utilized to decode emitted extrinsics from a block, via obtaining the
/// form of the `Extrinsic` from the metadata.
pub trait StaticExtrinsic: DecodeAsFields {
/// Pallet name.
const PALLET: &'static str;
/// Call name.
const CALL: &'static str;
/// Returns true if the given pallet and call names match this extrinsic.
fn is_extrinsic(pallet: &str, call: &str) -> bool {
Self::PALLET == pallet && Self::CALL == call
}
}
/// This trait is implemented on the statically generated root extrinsic type, so that we're able
/// to decode it properly via a pallet that impls `DecodeAsMetadata`. This is necessary
/// because the "root extrinsic" type is generated using pallet info but doesn't actually exist in the
/// metadata types, so we have no easy way to decode things into it via type information and need a
/// little help via codegen.
#[doc(hidden)]
pub trait RootExtrinsic: Sized {
/// Given details of the pallet extrinsic we want to decode, and the name of the pallet, try to hand
/// back a "root extrinsic".
fn root_extrinsic(
pallet_bytes: &[u8],
pallet_name: &str,
pallet_extrinsic_ty: u32,
metadata: &Metadata,
) -> Result<Self, Error>;
}
/// The body of a block.
pub struct Extrinsics<T: Config, C> {
client: C,
extrinsics: Vec<ChainBlockExtrinsic>,
cached_events: CachedEvents<T>,
ids: ExtrinsicPartTypeIds,
hash: T::Hash,
}
impl<T, C> Extrinsics<T, C>
where
T: Config,
C: OfflineClientT<T>,
{
pub(crate) fn new(
client: C,
extrinsics: Vec<ChainBlockExtrinsic>,
cached_events: CachedEvents<T>,
ids: ExtrinsicPartTypeIds,
hash: T::Hash,
) -> Self {
Self {
client,
extrinsics,
cached_events,
ids,
hash,
}
}
/// The number of extrinsics.
pub fn len(&self) -> usize {
self.extrinsics.len()
}
/// Are there no extrinsics in this block?
// Note: mainly here to satisfy clippy.
pub fn is_empty(&self) -> bool {
self.extrinsics.is_empty()
}
/// Return the block hash that these extrinsics are from.
pub fn block_hash(&self) -> T::Hash {
self.hash
}
/// 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 {
let extrinsics = self.extrinsics.clone();
let num_extrinsics = self.extrinsics.len();
let client = self.client.clone();
let hash = self.hash;
let cached_events = self.cached_events.clone();
let ids = self.ids;
let mut index = 0;
std::iter::from_fn(move || {
if index == num_extrinsics {
None
} else {
match ExtrinsicDetails::decode_from(
index as u32,
extrinsics[index].0.clone().into(),
client.clone(),
hash,
cached_events.clone(),
ids,
) {
Ok(extrinsic_details) => {
index += 1;
Some(Ok(extrinsic_details))
}
Err(e) => {
index = num_extrinsics;
Some(Err(e))
}
}
}
})
}
/// Iterate through the extrinsics using metadata to dynamically decode and skip
/// them, and return only those which should decode to the provided `E` type.
/// If an error occurs, all subsequent iterations return `None`.
pub fn find<E: StaticExtrinsic>(&self) -> impl Iterator<Item = Result<E, Error>> + '_ {
self.iter().filter_map(|e| {
e.and_then(|e| e.as_extrinsic::<E>().map_err(Into::into))
.transpose()
})
}
/// Iterate through the extrinsics using metadata to dynamically decode and skip
/// them, and return the first extrinsic found which decodes to the provided `E` type.
pub fn find_first<E: StaticExtrinsic>(&self) -> Result<Option<E>, Error> {
self.find::<E>().next().transpose()
}
/// Iterate through the extrinsics using metadata to dynamically decode and skip
/// them, and return the last extrinsic found which decodes to the provided `Ev` type.
pub fn find_last<E: StaticExtrinsic>(&self) -> Result<Option<E>, Error> {
self.find::<E>().last().transpose()
}
/// Find an extrinsics that decodes to the type provided. Returns true if it was found.
pub fn has<E: StaticExtrinsic>(&self) -> Result<bool, Error> {
Ok(self.find::<E>().next().transpose()?.is_some())
}
}
/// A single extrinsic in a block.
pub struct ExtrinsicDetails<T: Config, C> {
/// The index of the extrinsic in the block.
index: u32,
/// Extrinsic bytes.
bytes: Arc<[u8]>,
/// True if the extrinsic payload is signed.
is_signed: bool,
/// The start index in the `bytes` from which the address is encoded.
address_start_idx: usize,
/// The end index of the address in the encoded `bytes`.
address_end_idx: usize,
/// 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,
/// The block hash of this extrinsic (needed to fetch events).
block_hash: T::Hash,
/// Subxt client.
client: C,
/// Cached events.
cached_events: CachedEvents<T>,
/// Subxt metadata to fetch the extrinsic metadata.
metadata: Metadata,
_marker: std::marker::PhantomData<T>,
}
impl<T, C> ExtrinsicDetails<T, C>
where
T: Config,
C: OfflineClientT<T>,
{
// Attempt to dynamically decode a single extrinsic from the given input.
pub(crate) fn decode_from(
index: u32,
extrinsic_bytes: Arc<[u8]>,
client: C,
block_hash: T::Hash,
cached_events: CachedEvents<T>,
ids: ExtrinsicPartTypeIds,
) -> Result<ExtrinsicDetails<T, C>, Error> {
const SIGNATURE_MASK: u8 = 0b1000_0000;
const VERSION_MASK: u8 = 0b0111_1111;
const LATEST_EXTRINSIC_VERSION: u8 = 4;
let metadata = client.metadata();
// 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 first_byte: u8 = Decode::decode(&mut &extrinsic_bytes[..])?;
let version = first_byte & VERSION_MASK;
if version != LATEST_EXTRINSIC_VERSION {
return Err(BlockError::UnsupportedVersion(version).into());
}
let is_signed = first_byte & SIGNATURE_MASK != 0;
// Skip over the first byte which denotes the version and signing.
let cursor = &mut &extrinsic_bytes[1..];
let mut address_start_idx = 0;
let mut address_end_idx = 0;
if is_signed {
address_start_idx = extrinsic_bytes.len() - cursor.len();
// Skip over the address, signature and extra fields.
scale_decode::visitor::decode_with_visitor(
cursor,
ids.address,
&metadata.runtime_metadata().types,
scale_decode::visitor::IgnoreVisitor,
)
.map_err(scale_decode::Error::from)?;
address_end_idx = extrinsic_bytes.len() - cursor.len();
scale_decode::visitor::decode_with_visitor(
cursor,
ids.signature,
&metadata.runtime_metadata().types,
scale_decode::visitor::IgnoreVisitor,
)
.map_err(scale_decode::Error::from)?;
scale_decode::visitor::decode_with_visitor(
cursor,
ids.extra,
&metadata.runtime_metadata().types,
scale_decode::visitor::IgnoreVisitor,
)
.map_err(scale_decode::Error::from)?;
}
let call_start_idx = extrinsic_bytes.len() - cursor.len();
// Decode the pallet index, then the call variant.
let cursor = &mut &extrinsic_bytes[call_start_idx..];
let pallet_index: u8 = Decode::decode(cursor)?;
let variant_index: u8 = Decode::decode(cursor)?;
Ok(ExtrinsicDetails {
index,
bytes: extrinsic_bytes,
is_signed,
address_start_idx,
address_end_idx,
call_start_idx,
pallet_index,
variant_index,
block_hash,
client,
cached_events,
metadata,
_marker: std::marker::PhantomData,
})
}
/// Is the extrinsic signed?
pub fn is_signed(&self) -> bool {
self.is_signed
}
/// The index of the extrinsic in the block.
pub fn index(&self) -> u32 {
self.index
}
/// Return _all_ of the bytes representing this extrinsic, which include, in order:
/// - First byte: abbbbbbb (a = 0 for unsigned, 1 for signed, b = version)
/// - SignatureType (if the payload is signed)
/// - Address
/// - Signature
/// - Extra fields
/// - Extrinsic call bytes
pub fn bytes(&self) -> &[u8] {
&self.bytes
}
/// Return only the bytes representing this extrinsic call:
/// - First byte is the pallet index
/// - Second byte is the variant (call) index
/// - Followed by field bytes.
///
/// # Note
///
/// Please use [`Self::bytes`] if you want to get all extrinsic bytes.
pub fn call_bytes(&self) -> &[u8] {
&self.bytes[self.call_start_idx..]
}
/// Return the bytes representing the fields stored in this extrinsic.
///
/// # Note
///
/// This is a subset of [`Self::call_bytes`] that does not include the
/// first two bytes that denote the pallet index and the variant index.
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..]
}
/// Return only the bytes of the address that signed this extrinsic.
///
/// # Note
///
/// Returns `None` if the extrinsic is not signed.
pub fn address_bytes(&self) -> Option<&[u8]> {
self.is_signed
.then(|| &self.bytes[self.address_start_idx..self.address_end_idx])
}
/// The index of the pallet that the extrinsic originated from.
pub fn pallet_index(&self) -> u8 {
self.pallet_index
}
/// The index of the extrinsic variant that the extrinsic originated from.
pub fn variant_index(&self) -> u8 {
self.variant_index
}
/// The name of the pallet from whence the extrinsic originated.
pub fn pallet_name(&self) -> Result<&str, Error> {
Ok(self.extrinsic_metadata()?.pallet())
}
/// The name of the call (ie the name of the variant that it corresponds to).
pub fn variant_name(&self) -> Result<&str, Error> {
Ok(self.extrinsic_metadata()?.call())
}
/// Fetch the metadata for this extrinsic.
pub fn extrinsic_metadata(&self) -> Result<&ExtrinsicMetadata, Error> {
Ok(self
.metadata
.extrinsic(self.pallet_index(), self.variant_index())?)
}
/// Decode and provide the extrinsic fields back in the form of a [`scale_value::Composite`]
/// type which represents the named or unnamed fields that were
/// present in the extrinsic.
pub fn field_values(
&self,
) -> Result<scale_value::Composite<scale_value::scale::TypeId>, Error> {
let bytes = &mut self.field_bytes();
let extrinsic_metadata = self.extrinsic_metadata()?;
let decoded = <scale_value::Composite<scale_value::scale::TypeId>>::decode_as_fields(
bytes,
extrinsic_metadata.fields(),
&self.metadata.runtime_metadata().types,
)?;
Ok(decoded)
}
/// Attempt to statically decode these [`ExtrinsicDetails`] into a type representing the extrinsic
/// fields. This leans directly on [`codec::Decode`]. You can also attempt to decode the entirety
/// of the extrinsic using [`Self::as_root_extrinsic()`], which is more lenient because it's able
/// to lean on [`scale_decode::DecodeAsType`].
pub fn as_extrinsic<E: StaticExtrinsic>(&self) -> Result<Option<E>, Error> {
let extrinsic_metadata = self.extrinsic_metadata()?;
if extrinsic_metadata.pallet() == E::PALLET && extrinsic_metadata.call() == E::CALL {
let decoded = E::decode_as_fields(
&mut self.field_bytes(),
extrinsic_metadata.fields(),
self.metadata.types(),
)?;
Ok(Some(decoded))
} else {
Ok(None)
}
}
/// Attempt to decode these [`ExtrinsicDetails`] into a pallet extrinsic type (which includes
/// the pallet enum variants as well as the extrinsic fields). These extrinsics can be found in
/// the static codegen under a path like `pallet_name::Call`.
pub fn as_pallet_extrinsic<E: DecodeWithMetadata>(&self) -> Result<E, Error> {
let pallet = self.metadata.pallet(self.pallet_name()?)?;
let extrinsic_ty = pallet.call_ty_id().ok_or_else(|| {
Error::Metadata(crate::metadata::MetadataError::ExtrinsicNotFound(
pallet.index(),
self.variant_index(),
))
})?;
// Ignore the root enum index, so start 1 byte after that:
let decoded =
E::decode_with_metadata(&mut &self.call_bytes()[1..], extrinsic_ty, &self.metadata)?;
Ok(decoded)
}
/// Attempt to decode these [`ExtrinsicDetails`] into a root extrinsic type (which includes
/// the pallet and extrinsic enum variants as well as the extrinsic fields). A compatible
/// type for this is exposed via static codegen as a root level `Call` type.
pub fn as_root_extrinsic<E: RootExtrinsic>(&self) -> Result<E, Error> {
let pallet = self.metadata.pallet(self.pallet_name()?)?;
let pallet_extrinsic_ty = pallet.call_ty_id().ok_or_else(|| {
Error::Metadata(crate::metadata::MetadataError::ExtrinsicNotFound(
pallet.index(),
self.variant_index(),
))
})?;
// Ignore root enum index.
E::root_extrinsic(
&self.call_bytes()[1..],
self.pallet_name()?,
pallet_extrinsic_ty,
&self.metadata,
)
}
}
impl<T, C> ExtrinsicDetails<T, C>
where
T: Config,
C: OnlineClientT<T>,
{
/// The events associated with the extrinsic.
pub async fn events(&self) -> Result<ExtrinsicEvents<T>, Error> {
let events = get_events(&self.client, self.block_hash, &self.cached_events).await?;
let ext_hash = T::Hasher::hash_of(&self.bytes);
Ok(ExtrinsicEvents::new(ext_hash, self.index, events))
}
}
/// The type IDs extracted from the metadata that represent the
/// generic type parameters passed to the `UncheckedExtrinsic` from
/// the substrate-based chain.
#[derive(Debug, Copy, Clone)]
pub(crate) 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.
pub(crate) fn new(metadata: &RuntimeMetadataV15) -> Result<Self, BlockError> {
const ADDRESS: &str = "Address";
const CALL: &str = "Call";
const SIGNATURE: &str = "Signature";
const EXTRA: &str = "Extra";
let id = metadata.extrinsic.ty.id;
let Some(ty) = metadata.types.resolve(id) else {
return Err(BlockError::MissingType);
};
let params: HashMap<_, _> = ty
.type_params
.iter()
.map(|ty_param| {
let Some(ty) = ty_param.ty else {
return Err(BlockError::MissingType);
};
Ok((ty_param.name.as_str(), ty.id))
})
.collect::<Result<_, _>>()?;
let Some(address) = params.get(ADDRESS) else {
return Err(BlockError::MissingType);
};
let Some(call) = params.get(CALL) else {
return Err(BlockError::MissingType);
};
let Some(signature) = params.get(SIGNATURE) else {
return Err(BlockError::MissingType);
};
let Some(extra) = params.get(EXTRA) else {
return Err(BlockError::MissingType);
};
Ok(ExtrinsicPartTypeIds {
address: *address,
_call: *call,
signature: *signature,
extra: *extra,
})
}
}
/// The events associated with a given extrinsic.
#[derive(Derivative)]
#[derivative(Debug(bound = ""))]
pub struct ExtrinsicEvents<T: Config> {
// The hash of the extrinsic (handy to expose here because
// this type is returned from TxProgress things in the most
// basic flows, so it's the only place people can access it
// without complicating things for themselves).
ext_hash: T::Hash,
// The index of the extrinsic:
idx: u32,
// All of the events in the block:
events: events::Events<T>,
}
impl<T: Config> ExtrinsicEvents<T> {
pub(crate) fn new(ext_hash: T::Hash, idx: u32, events: events::Events<T>) -> Self {
Self {
ext_hash,
idx,
events,
}
}
/// Return the hash of the block that the extrinsic is in.
pub fn block_hash(&self) -> T::Hash {
self.events.block_hash()
}
/// The index of the extrinsic that these events are produced from.
pub fn extrinsic_index(&self) -> u32 {
self.idx
}
/// Return the hash of the extrinsic.
pub fn extrinsic_hash(&self) -> T::Hash {
self.ext_hash
}
/// Return all of the events in the block that the extrinsic is in.
pub fn all_events_in_block(&self) -> &events::Events<T> {
&self.events
}
/// Iterate over all of the raw events associated with this transaction.
///
/// This works in the same way that [`events::Events::iter()`] does, with the
/// exception that it filters out events not related to the submitted extrinsic.
pub fn iter(&self) -> impl Iterator<Item = Result<events::EventDetails, Error>> + '_ {
self.events.iter().filter(|ev| {
ev.as_ref()
.map(|ev| ev.phase() == events::Phase::ApplyExtrinsic(self.idx))
.unwrap_or(true) // Keep any errors.
})
}
/// Find all of the transaction events matching the event type provided as a generic parameter.
///
/// This works in the same way that [`events::Events::find()`] does, with the
/// exception that it filters out events not related to the submitted extrinsic.
pub fn find<Ev: events::StaticEvent>(&self) -> impl Iterator<Item = Result<Ev, Error>> + '_ {
self.iter().filter_map(|ev| {
ev.and_then(|ev| ev.as_event::<Ev>().map_err(Into::into))
.transpose()
})
}
/// Iterate through the transaction events using metadata to dynamically decode and skip
/// them, and return the first event found which decodes to the provided `Ev` type.
///
/// This works in the same way that [`events::Events::find_first()`] does, with the
/// exception that it ignores events not related to the submitted extrinsic.
pub fn find_first<Ev: events::StaticEvent>(&self) -> Result<Option<Ev>, Error> {
self.find::<Ev>().next().transpose()
}
/// Iterate through the transaction events using metadata to dynamically decode and skip
/// them, and return the last event found which decodes to the provided `Ev` type.
///
/// This works in the same way that [`events::Events::find_last()`] does, with the
/// exception that it ignores events not related to the submitted extrinsic.
pub fn find_last<Ev: events::StaticEvent>(&self) -> Result<Option<Ev>, Error> {
self.find::<Ev>().last().transpose()
}
/// Find an event in those associated with this transaction. Returns true if it was found.
///
/// This works in the same way that [`events::Events::has()`] does, with the
/// exception that it ignores events not related to the submitted extrinsic.
pub fn has<Ev: events::StaticEvent>(&self) -> Result<bool, Error> {
Ok(self.find::<Ev>().next().transpose()?.is_some())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{rpc::types::RuntimeVersion, OfflineClient, PolkadotConfig};
use assert_matches::assert_matches;
use codec::{Decode, Encode};
use frame_metadata::{
v15::{ExtrinsicMetadata, PalletCallMetadata, PalletMetadata, RuntimeMetadataV15},
RuntimeMetadataPrefixed,
};
use primitive_types::H256;
use scale_info::{meta_type, TypeInfo};
use scale_value::Value;
// Extrinsic needs to contain at least the generic type parameter "Call"
// for the metadata to be valid.
// The "Call" type from the metadata is used to decode extrinsics.
#[allow(unused)]
#[derive(TypeInfo)]
struct ExtrinsicType<Address, Call, Signature, Extra> {
pub signature: Option<(Address, Signature, Extra)>,
pub function: Call,
}
// Because this type is used to decode extrinsics, we expect this to be a TypeDefVariant.
// Each pallet must contain one single variant.
#[allow(unused)]
#[derive(
Encode,
Decode,
TypeInfo,
Clone,
Debug,
PartialEq,
Eq,
scale_encode::EncodeAsType,
scale_decode::DecodeAsType,
)]
enum RuntimeCall {
Test(Pallet),
}
// We need this in order to be able to decode into a root extrinsic type:
impl RootExtrinsic for RuntimeCall {
fn root_extrinsic(
mut pallet_bytes: &[u8],
pallet_name: &str,
pallet_extrinsic_ty: u32,
metadata: &Metadata,
) -> Result<Self, Error> {
if pallet_name == "Test" {
return Ok(RuntimeCall::Test(Pallet::decode_with_metadata(
&mut pallet_bytes,
pallet_extrinsic_ty,
metadata,
)?));
}
panic!(
"Asked for pallet name '{pallet_name}', which isn't in our test RuntimeCall type"
)
}
}
// The calls of the pallet.
#[allow(unused)]
#[derive(
Encode,
Decode,
TypeInfo,
Clone,
Debug,
PartialEq,
Eq,
scale_encode::EncodeAsType,
scale_decode::DecodeAsType,
)]
enum Pallet {
#[allow(unused)]
#[codec(index = 2)]
TestCall {
value: u128,
signed: bool,
name: String,
},
}
#[allow(unused)]
#[derive(
Encode,
Decode,
TypeInfo,
Clone,
Debug,
PartialEq,
Eq,
scale_encode::EncodeAsType,
scale_decode::DecodeAsType,
)]
struct TestCallExtrinsic {
value: u128,
signed: bool,
name: String,
}
impl StaticExtrinsic for TestCallExtrinsic {
const PALLET: &'static str = "Test";
const CALL: &'static str = "TestCall";
}
/// Build fake metadata consisting the types needed to represent an extrinsic.
fn metadata() -> Metadata {
let pallets = vec![PalletMetadata {
name: "Test",
storage: None,
calls: Some(PalletCallMetadata {
ty: meta_type::<Pallet>(),
}),
event: None,
constants: vec![],
error: None,
index: 0,
docs: vec![],
}];
let extrinsic = ExtrinsicMetadata {
ty: meta_type::<ExtrinsicType<(), RuntimeCall, (), ()>>(),
version: 4,
signed_extensions: vec![],
};
let meta = RuntimeMetadataV15::new(pallets, extrinsic, meta_type::<()>(), vec![]);
let runtime_metadata: RuntimeMetadataPrefixed = meta.into();
Metadata::try_from(runtime_metadata).unwrap()
}
/// Build an offline client to work with the test metadata.
fn client(metadata: Metadata) -> OfflineClient<PolkadotConfig> {
// Create the encoded extrinsic bytes.
let rt_version = RuntimeVersion {
spec_version: 1,
transaction_version: 4,
other: Default::default(),
};
let block_hash = H256::random();
OfflineClient::new(block_hash, rt_version, metadata)
}
#[test]
fn extrinsic_metadata_consistency() {
let metadata = metadata();
// Except our metadata to contain the registered types.
let extrinsic = metadata
.extrinsic(0, 2)
.expect("metadata contains the RuntimeCall enum with this pallet");
assert_eq!(extrinsic.pallet(), "Test");
assert_eq!(extrinsic.call(), "TestCall");
}
#[test]
fn insufficient_extrinsic_bytes() {
let metadata = metadata();
let client = client(metadata.clone());
let ids = ExtrinsicPartTypeIds::new(metadata.runtime_metadata()).unwrap();
// Decode with empty bytes.
let result = ExtrinsicDetails::decode_from(
1,
vec![].into(),
client,
H256::random(),
Default::default(),
ids,
);
assert_matches!(result.err(), Some(crate::Error::Codec(_)));
}
#[test]
fn unsupported_version_extrinsic() {
let metadata = metadata();
let client = client(metadata.clone());
let ids = ExtrinsicPartTypeIds::new(metadata.runtime_metadata()).unwrap();
// Decode with invalid version.
let result = ExtrinsicDetails::decode_from(
1,
3u8.encode().into(),
client,
H256::random(),
Default::default(),
ids,
);
assert_matches!(
result.err(),
Some(crate::Error::Block(
crate::error::BlockError::UnsupportedVersion(3)
))
);
}
#[test]
fn statically_decode_extrinsic() {
let metadata = metadata();
let client = client(metadata.clone());
let ids = ExtrinsicPartTypeIds::new(metadata.runtime_metadata()).unwrap();
let tx = crate::tx::dynamic(
"Test",
"TestCall",
vec![
Value::u128(10),
Value::bool(true),
Value::string("SomeValue"),
],
);
let tx_encoded = client
.tx()
.create_unsigned(&tx)
.expect("Valid dynamic parameters are provided");
// Note: `create_unsigned` produces the extrinsic bytes by prefixing the extrinsic length.
// The length is handled deserializing `ChainBlockExtrinsic`, therefore the first byte is not needed.
let extrinsic = ExtrinsicDetails::decode_from(
1,
tx_encoded.encoded()[1..].into(),
client,
H256::random(),
Default::default(),
ids,
)
.expect("Valid extrinsic");
assert!(!extrinsic.is_signed());
assert_eq!(extrinsic.index(), 1);
assert_eq!(extrinsic.pallet_index(), 0);
assert_eq!(
extrinsic
.pallet_name()
.expect("Valid metadata contains pallet name"),
"Test"
);
assert_eq!(extrinsic.variant_index(), 2);
assert_eq!(
extrinsic
.variant_name()
.expect("Valid metadata contains variant name"),
"TestCall"
);
// Decode the extrinsic to the root enum.
let decoded_extrinsic = extrinsic
.as_root_extrinsic::<RuntimeCall>()
.expect("can decode extrinsic to root enum");
assert_eq!(
decoded_extrinsic,
RuntimeCall::Test(Pallet::TestCall {
value: 10,
signed: true,
name: "SomeValue".into(),
})
);
// Decode the extrinsic to the pallet enum.
let decoded_extrinsic = extrinsic
.as_pallet_extrinsic::<Pallet>()
.expect("can decode extrinsic to pallet enum");
assert_eq!(
decoded_extrinsic,
Pallet::TestCall {
value: 10,
signed: true,
name: "SomeValue".into(),
}
);
// Decode the extrinsic to the extrinsic variant.
let decoded_extrinsic = extrinsic
.as_extrinsic::<TestCallExtrinsic>()
.expect("can decode extrinsic to extrinsic variant")
.expect("value cannot be None");
assert_eq!(
decoded_extrinsic,
TestCallExtrinsic {
value: 10,
signed: true,
name: "SomeValue".into(),
}
);
}
}
+5 -1
View File
@@ -6,6 +6,10 @@
mod block_types;
mod blocks_client;
mod extrinsic_types;
pub use block_types::{Block, BlockBody, Extrinsic, ExtrinsicEvents};
pub use block_types::{Block, BlockBody};
pub use blocks_client::{subscribe_to_block_headers_filling_in_gaps, BlocksClient};
pub use extrinsic_types::{
ExtrinsicDetails, ExtrinsicEvents, Extrinsics, RootExtrinsic, StaticExtrinsic,
};
+49
View File
@@ -19,6 +19,8 @@
//! > can construct unsigned extrinsics, but overwhelmingly you'll need to sign them, and so the
//! > documentation tends to use the terms _extrinsic_ and _transaction_ interchangeably.
//!
//! Furthermore, Subxt is capable of decoding extrinsics included in blocks.
//!
//! ## Constructing an extrinsic payload
//!
//! We can use the statically generated interface to build extrinsic payloads:
@@ -173,3 +175,50 @@
//! Take a look at the API docs for [`crate::tx::TxProgress`], [`crate::tx::TxStatus`] and
//! [`crate::tx::TxInBlock`] for more options.
//!
//! ## Decoding Extrinsics
//!
//! The block body is made up of extrinsics representing the generalization of the concept of transactions.
//! Extrinsics can contain any external data the underlying chain wishes to validate and track.
//!
//! The process of decoding extrinsics generally involves the following steps:
//!
//! 1. Retrieve a block from the chain: This can be done directly by providing a specific hash using [crate::blocks::BlocksClient::at()]
//! and [crate::blocks::BlocksClient::at_latest()], or indirectly by subscribing to the latest produced blocks of the chain using
//! [crate::blocks::BlocksClient::subscribe_finalized()].
//!
//! 2. Fetch the block's body using [crate::blocks::Block::body()].
//!
//! 3. Obtain the extrinsics from the block's body using [crate::blocks::BlockBody::extrinsics()].
//!
//! ```rust,no_run
//! # #[tokio::main]
//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
//! use subxt::client::OnlineClient;
//! use subxt::config::PolkadotConfig;
//!
//! // Create client:
//! let client = OnlineClient::<PolkadotConfig>::new().await?;
//!
//! // Get the lastest block (or use .at() to specify a block hash):
//! let block = client.blocks().at_latest().await?;
//!
//! // Get the block's body which contains the extrinsics.
//! let body = block.body().await?;
//!
//! // Fetch the extrinsics of the block's body.
//! let extrinsics = block.body().await?.extrinsics();
//! # Ok(())
//! # }
//! ```
//!
//! Once the extrinsics are loaded, similar to events, you can iterate through the extrinsics or search for specific extrinsics using methods
//! such as [crate::blocks::Extrinsics::iter()] and [crate::blocks::Extrinsics::find()]. For more information, refer to [crate::blocks::ExtrinsicDetails].
//!
//! ### Example
//!
//! Here's an example that demonstrates the usage of these concepts:
//!
//! ```rust,ignore
#![doc = include_str!("../../../../examples/examples/block_extrinsics.rs")]
//! ```
//!
+1 -1
View File
@@ -54,7 +54,7 @@ pub trait Config: 'static {
type Hasher: Debug + Hasher<Output = Self::Hash>;
/// The block header.
type Header: Debug + Header<Hasher = Self::Hasher> + Send + DeserializeOwned;
type Header: Debug + Header<Hasher = Self::Hasher> + Sync + Send + DeserializeOwned;
/// This type defines the extrinsic extra and additional parameters.
type ExtrinsicParams: extrinsic_params::ExtrinsicParams<Self::Index, Self::Hash>;
+10
View File
@@ -102,6 +102,16 @@ 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),
/// Decoding error.
#[error("Cannot decode extrinsic: {0}")]
DecodingError(codec::Error),
}
impl BlockError {
+25 -1
View File
@@ -493,6 +493,30 @@ pub(crate) mod test_utils {
/// Build fake metadata consisting of a single pallet that knows
/// about the event type provided.
pub fn metadata<E: TypeInfo + 'static>() -> Metadata {
// Extrinsic needs to contain at least the generic type parameter "Call"
// for the metadata to be valid.
// The "Call" type from the metadata is used to decode extrinsics.
// In reality, the extrinsic type has "Call", "Address", "Extra", "Signature" generic types.
#[allow(unused)]
#[derive(TypeInfo)]
struct ExtrinsicType<Call> {
call: Call,
}
// Because this type is used to decode extrinsics, we expect this to be a TypeDefVariant.
// Each pallet must contain one single variant.
#[allow(unused)]
#[derive(TypeInfo)]
enum RuntimeCall {
PalletName(Pallet),
}
// The calls of the pallet.
#[allow(unused)]
#[derive(TypeInfo)]
enum Pallet {
#[allow(unused)]
SomeCall,
}
let pallets = vec![PalletMetadata {
name: "Test",
storage: None,
@@ -507,7 +531,7 @@ pub(crate) mod test_utils {
}];
let extrinsic = ExtrinsicMetadata {
ty: meta_type::<()>(),
ty: meta_type::<ExtrinsicType<RuntimeCall>>(),
version: 0,
signed_extensions: vec![],
};
+132 -1
View File
@@ -27,6 +27,9 @@ pub enum MetadataError {
/// Event is not in metadata.
#[error("Pallet {0}, Event {0} not found")]
EventNotFound(u8, u8),
/// Extrinsic is not in metadata.
#[error("Pallet {0}, Extrinsic {0} not found")]
ExtrinsicNotFound(u8, u8),
/// Event is not in metadata.
#[error("Pallet {0}, Error {0} not found")]
ErrorNotFound(u8, u8),
@@ -75,6 +78,8 @@ struct MetadataInner {
// Events are hashed by pallet an error index (decode oriented)
events: HashMap<(u8, u8), EventMetadata>,
// Extrinsics are hashed by pallet an error index (decode oriented)
extrinsics: HashMap<(u8, u8), ExtrinsicMetadata>,
// Errors are hashed by pallet and error index (decode oriented)
errors: HashMap<(u8, u8), ErrorMetadata>,
@@ -135,6 +140,20 @@ impl Metadata {
Ok(event)
}
/// Returns the metadata for the extrinsic at the given pallet and call indices.
pub fn extrinsic(
&self,
pallet_index: u8,
call_index: u8,
) -> Result<&ExtrinsicMetadata, MetadataError> {
let event = self
.inner
.extrinsics
.get(&(pallet_index, call_index))
.ok_or(MetadataError::ExtrinsicNotFound(pallet_index, call_index))?;
Ok(event)
}
/// Returns the metadata for the error at the given pallet and error indices.
pub fn error(
&self,
@@ -386,6 +405,39 @@ impl EventMetadata {
}
}
/// Metadata for specific extrinsics.
#[derive(Clone, Debug)]
pub struct ExtrinsicMetadata {
// The pallet name is shared across every extrinsic, so put it
// behind an Arc to avoid lots of needless clones of it existing.
pallet: Arc<str>,
call: String,
fields: Vec<scale_info::Field<scale_info::form::PortableForm>>,
docs: Vec<String>,
}
impl ExtrinsicMetadata {
/// Get the name of the pallet from which the extrinsic was emitted.
pub fn pallet(&self) -> &str {
&self.pallet
}
/// Get the name of the extrinsic call.
pub fn call(&self) -> &str {
&self.call
}
/// The names, type names & types of each field in the extrinsic.
pub fn fields(&self) -> &[scale_info::Field<scale_info::form::PortableForm>] {
&self.fields
}
/// Documentation for this extrinsic.
pub fn docs(&self) -> &[String] {
&self.docs
}
}
/// Details about a specific runtime error.
#[derive(Clone, Debug)]
pub struct ErrorMetadata {
@@ -426,6 +478,12 @@ pub enum InvalidMetadataError {
/// Type missing from type registry
#[error("Type {0} missing from type registry")]
MissingType(u32),
/// Type missing extrinsic "Call" type
#[error("Missing extrinsic Call type")]
MissingCallType,
/// The extrinsic variant expected to contain a single field.
#[error("Extrinsic variant at index {0} expected to contain a single field")]
InvalidExtrinsicVariant(u8),
/// Type was not a variant/enum type
#[error("Type {0} was not a variant/enum type")]
TypeDefNotVariant(u32),
@@ -596,11 +654,60 @@ impl TryFrom<RuntimeMetadataPrefixed> for Metadata {
.find(|ty| ty.ty.path.segments == ["sp_runtime", "DispatchError"])
.map(|ty| ty.id);
let extrinsic_ty = metadata
.types
.resolve(metadata.extrinsic.ty.id)
.ok_or(InvalidMetadataError::MissingType(metadata.extrinsic.ty.id))?;
let Some(call_id) = extrinsic_ty.type_params
.iter()
.find(|ty| ty.name == "Call")
.and_then(|ty| ty.ty)
.map(|ty| ty.id) else {
return Err(InvalidMetadataError::MissingCallType);
};
let call_type_variants = get_type_def_variant(call_id)?;
let mut extrinsics = HashMap::<(u8, u8), ExtrinsicMetadata>::new();
for variant in &call_type_variants.variants {
let pallet_name: Arc<str> = variant.name.to_string().into();
let pallet_index = variant.index;
// Pallet variants must contain one single call variant.
// In the following form:
//
// enum RuntimeCall {
// Pallet(pallet_call)
// }
if variant.fields.len() != 1 {
return Err(InvalidMetadataError::InvalidExtrinsicVariant(pallet_index));
}
let Some(ty) = variant.fields.first() else {
return Err(InvalidMetadataError::InvalidExtrinsicVariant(pallet_index));
};
// Get the call variant.
let call_type_variant = get_type_def_variant(ty.ty.id)?;
for variant in &call_type_variant.variants {
extrinsics.insert(
(pallet_index, variant.index),
ExtrinsicMetadata {
pallet: pallet_name.clone(),
call: variant.name.to_string(),
fields: variant.fields.clone(),
docs: variant.docs.clone(),
},
);
}
}
Ok(Metadata {
inner: Arc::new(MetadataInner {
metadata,
pallets,
events,
extrinsics,
errors,
dispatch_error_ty,
runtime_apis,
@@ -624,6 +731,30 @@ mod tests {
use scale_info::{meta_type, TypeInfo};
fn load_metadata() -> Metadata {
// Extrinsic needs to contain at least the generic type parameter "Call"
// for the metadata to be valid.
// The "Call" type from the metadata is used to decode extrinsics.
// In reality, the extrinsic type has "Call", "Address", "Extra", "Signature" generic types.
#[allow(unused)]
#[derive(TypeInfo)]
struct ExtrinsicType<Call> {
call: Call,
}
// Because this type is used to decode extrinsics, we expect this to be a TypeDefVariant.
// Each pallet must contain one single variant.
#[allow(unused)]
#[derive(TypeInfo)]
enum RuntimeCall {
PalletName(Pallet),
}
// The calls of the pallet.
#[allow(unused)]
#[derive(TypeInfo)]
enum Pallet {
#[allow(unused)]
SomeCall,
}
#[allow(dead_code)]
#[allow(non_camel_case_types)]
#[derive(TypeInfo)]
@@ -662,7 +793,7 @@ mod tests {
let metadata = RuntimeMetadataV15::new(
vec![pallet],
ExtrinsicMetadata {
ty: meta_type::<()>(),
ty: meta_type::<ExtrinsicType<RuntimeCall>>(),
version: 0,
signed_extensions: vec![],
},
+2 -2
View File
@@ -12,8 +12,8 @@ mod metadata_type;
pub use metadata_location::MetadataLocation;
pub use metadata_type::{
ErrorMetadata, EventMetadata, InvalidMetadataError, Metadata, MetadataError, PalletMetadata,
RuntimeFnMetadata,
ErrorMetadata, EventMetadata, ExtrinsicMetadata, InvalidMetadataError, Metadata, MetadataError,
PalletMetadata, RuntimeFnMetadata,
};
pub use decode_encode_traits::{DecodeWithMetadata, EncodeWithMetadata};
+71 -1
View File
@@ -2,10 +2,12 @@
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use crate::test_context;
use crate::{pair_signer, test_context, utils::node_runtime};
use codec::Compact;
use frame_metadata::RuntimeMetadataPrefixed;
use futures::StreamExt;
use sp_keyring::AccountKeyring;
use subxt::blocks::BlocksClient;
// Check that we can subscribe to non-finalized blocks.
#[tokio::test]
@@ -118,3 +120,71 @@ async fn runtime_api_call() -> Result<(), subxt::Error> {
assert_eq!(&metadata_call, metadata);
Ok(())
}
#[tokio::test]
async fn decode_extrinsics() {
let ctx = test_context().await;
let api = ctx.client();
let alice = pair_signer(AccountKeyring::Alice.pair());
let bob = pair_signer(AccountKeyring::Bob.pair());
// Generate a block that has unsigned and signed transactions.
let tx = node_runtime::tx()
.balances()
.transfer(bob.account_id().clone().into(), 10_000);
let signed_extrinsic = api
.tx()
.create_signed(&tx, &alice, Default::default())
.await
.unwrap();
let in_block = signed_extrinsic
.submit_and_watch()
.await
.unwrap()
.wait_for_in_block()
.await
.unwrap();
let block_hash = in_block.block_hash();
let block = BlocksClient::new(api).at(block_hash).await.unwrap();
let extrinsics = block.body().await.unwrap().extrinsics();
assert_eq!(extrinsics.len(), 2);
assert_eq!(extrinsics.block_hash(), block_hash);
assert!(extrinsics
.has::<node_runtime::balances::calls::types::Transfer>()
.unwrap());
assert!(extrinsics
.find_first::<node_runtime::balances::calls::types::Transfer>()
.unwrap()
.is_some());
let block_extrinsics = extrinsics
.iter()
.map(|res| res.unwrap())
.collect::<Vec<_>>();
assert_eq!(block_extrinsics.len(), 2);
let timestamp = block_extrinsics.get(0).unwrap();
timestamp.as_root_extrinsic::<node_runtime::Call>().unwrap();
timestamp
.as_pallet_extrinsic::<node_runtime::runtime_types::pallet_timestamp::pallet::Call>()
.unwrap();
timestamp
.as_extrinsic::<node_runtime::timestamp::calls::types::Set>()
.unwrap();
assert!(!timestamp.is_signed());
let tx = block_extrinsics.get(1).unwrap();
tx.as_root_extrinsic::<node_runtime::Call>().unwrap();
tx.as_pallet_extrinsic::<node_runtime::runtime_types::pallet_balances::pallet::Call>()
.unwrap();
tx.as_extrinsic::<node_runtime::balances::calls::types::Transfer>()
.unwrap();
assert!(tx.is_signed());
}
File diff suppressed because it is too large Load Diff
@@ -96,10 +96,34 @@ fn default_pallet() -> PalletMetadata {
}
fn pallets_to_metadata(pallets: Vec<PalletMetadata>) -> RuntimeMetadataV15 {
// Extrinsic needs to contain at least the generic type parameter "Call"
// for the metadata to be valid.
// The "Call" type from the metadata is used to decode extrinsics.
// In reality, the extrinsic type has "Call", "Address", "Extra", "Signature" generic types.
#[allow(unused)]
#[derive(TypeInfo)]
struct ExtrinsicType<Call> {
call: Call,
}
// Because this type is used to decode extrinsics, we expect this to be a TypeDefVariant.
// Each pallet must contain one single variant.
#[allow(unused)]
#[derive(TypeInfo)]
enum RuntimeCall {
PalletName(Pallet),
}
// The calls of the pallet.
#[allow(unused)]
#[derive(TypeInfo)]
enum Pallet {
#[allow(unused)]
SomeCall,
}
RuntimeMetadataV15::new(
pallets,
ExtrinsicMetadata {
ty: meta_type::<()>(),
ty: meta_type::<ExtrinsicType<RuntimeCall>>(),
version: 0,
signed_extensions: vec![],
},