diff --git a/examples/examples/block_extrinsics.rs b/examples/examples/block_extrinsics.rs index 6d6d7d61fe..9f244c79ac 100644 --- a/examples/examples/block_extrinsics.rs +++ b/examples/examples/block_extrinsics.rs @@ -58,31 +58,34 @@ async fn main() -> Result<(), Box> { let block = block?; let block_hash = block.hash(); + println!(" Block {:?}", block_hash); - println!(" Block {:?}", block_hash); - println!(" Block {:?}", block_hash); // Ask for the extrinsics for this block. - for extrinsic in block.body().await?.extrinsics() { + let extrinsics = block.extrinsics().await?; + + // Ask for the extrinsics for this block. + for extrinsic in extrinsics.iter() { + let extrinsic = extrinsic?; println!(" Extrinsic index {:?}", extrinsic.index()); - let decoded = extrinsic.decode_generic(); - match decoded { - Ok(decoded) => { - if let Some((address, signature, extra)) = decoded.signature { - println!(" Decoded Signature"); - println!(" Decoded Address: {:?}", address); - println!(" Decoded Sign: {:?}", signature); - println!(" Decoded Extra: {:?}", extra); - }; + // let decoded = extrinsic.decode_generic(); + // match decoded { + // Ok(decoded) => { + // if let Some((address, signature, extra)) = decoded.signature { + // println!(" Decoded Signature"); + // println!(" Decoded Address: {:?}", address); + // println!(" Decoded Sign: {:?}", signature); + // println!(" Decoded Extra: {:?}", extra); + // }; - let call = decoded.function; - println!(" Decoded call:\n {:?}", call.to_value()); - } - Err(err) => { - println!(" Decoded extrinsic with error: {:?}", err); - } - } - println!("\n"); + // let call = decoded.function; + // println!(" Decoded call:\n {:?}", call.to_value()); + // } + // Err(err) => { + // println!(" Decoded extrinsic with error: {:?}", err); + // } + // } + // println!("\n"); } println!("\n"); diff --git a/subxt/src/blocks/block_types.rs b/subxt/src/blocks/block_types.rs index 79e0e1c3e8..3846f65528 100644 --- a/subxt/src/blocks/block_types.rs +++ b/subxt/src/blocks/block_types.rs @@ -6,7 +6,7 @@ use crate::{ config::{Config, Hasher, Header}, dynamic::DecodedValueThunk, error::{BlockError, Error}, - events, + events, extrinsics, metadata::DecodeWithMetadata, rpc::types::ChainBlockResponse, runtime_api::RuntimeApi, @@ -72,13 +72,15 @@ where get_events(&self.client, self.header.hash(), &self.cached_events).await } + /// Return the extrinsics associated with the block. + pub async fn extrinsics(&self) -> Result, Error> { + let block_details = self.block_details().await?; + extrinsics::Extrinsics::new(self.client.metadata(), block_details.block) + } + /// Fetch and return the block body. pub async fn body(&self) -> Result, Error> { - 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 block_details = self.block_details().await?; Ok(BlockBody::new( self.client.clone(), @@ -97,6 +99,15 @@ where pub async fn runtime_api(&self) -> Result, Error> { Ok(RuntimeApi::new(self.client.clone(), self.hash())) } + + /// Fetch the block's body from the chain. + async fn block_details(&self) -> Result, Error> { + let block_hash = self.header.hash(); + match self.client.rpc().block(Some(block_hash)).await? { + Some(block) => Ok(block), + None => Err(BlockError::not_found(block_hash).into()), + } + } } /// Generic type IDs passed to the `UncheckedExtrinsic`. diff --git a/subxt/src/error/mod.rs b/subxt/src/error/mod.rs index 16f3bbfabc..1e16c52e79 100644 --- a/subxt/src/error/mod.rs +++ b/subxt/src/error/mod.rs @@ -57,6 +57,9 @@ pub enum Error { /// Block related error. #[error("Block error: {0}")] Block(#[from] BlockError), + /// Extrinsic related error. + #[error("Extrinsic error: {0}")] + Extrinsic(#[from] ExtrinsicError), /// An error encoding a storage address. #[error("Error encoding storage address: {0}")] StorageAddress(#[from] StorageAddressError), @@ -112,6 +115,25 @@ impl BlockError { } } +/// Extrinsic error. +#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)] +#[non_exhaustive] +pub enum ExtrinsicError { + /// 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, + /// Expected more extrinsic bytes. + #[error("Expected more extrinsic bytes")] + InsufficientData, + /// 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), +} + /// Transaction error. #[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)] #[non_exhaustive] diff --git a/subxt/src/extrinsics/extrinsics_client.rs b/subxt/src/extrinsics/extrinsics_client.rs new file mode 100644 index 0000000000..afc2e1b162 --- /dev/null +++ b/subxt/src/extrinsics/extrinsics_client.rs @@ -0,0 +1,85 @@ +// 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::config::Header; +use crate::{ + client::OnlineClientT, + error::{BlockError, Error}, + extrinsics::Extrinsics, + Config, +}; +use derivative::Derivative; +use std::future::Future; + +/// A client for working with extrinsics. +#[derive(Derivative)] +#[derivative(Clone(bound = "Client: Clone"))] +pub struct ExtrinsicsClient { + client: Client, + _marker: std::marker::PhantomData, +} + +impl ExtrinsicsClient { + /// Create a new [`ExtrinsicsClient`]. + pub fn new(client: Client) -> Self { + Self { + client, + _marker: std::marker::PhantomData, + } + } +} + +impl ExtrinsicsClient +where + T: Config, + Client: OnlineClientT, +{ + /// Obtain extrinsics at some block hash. + /// + /// # Warning + /// + /// This call only supports blocks produced since the most recent + /// runtime upgrade. You can attempt to retrieve extrinsics from older blocks, + /// but may run into errors attempting to work with them. + pub fn at( + &self, + block_hash: T::Hash, + ) -> impl Future, Error>> + Send + 'static { + self.at_or_latest(Some(block_hash)) + } + + /// Obtain extrinsics at the latest block hash. + pub fn at_latest(&self) -> impl Future, Error>> + Send + 'static { + self.at_or_latest(None) + } + + /// Obtain extrinsics at some block hash. + fn at_or_latest( + &self, + block_hash: Option, + ) -> impl Future, Error>> + Send + 'static { + // Clone and pass the client in like this so that we can explicitly + // return a Future that's Send + 'static, rather than tied to &self. + let client = self.client.clone(); + async move { + // If block hash is not provided, get the hash + // for the latest block and use that to create an explicit error. + let block_hash = match block_hash { + Some(hash) => hash, + None => client + .rpc() + .block_hash(None) + .await? + .expect("didn't pass a block number; qed"), + }; + + let result = client.rpc().block(Some(block_hash)).await?; + let Some(block_details) = result else { + return Err(BlockError::not_found(block_hash).into()); + }; + + Extrinsics::new(client.metadata(), block_details.block) + } + } +} diff --git a/subxt/src/extrinsics/extrinsics_type.rs b/subxt/src/extrinsics/extrinsics_type.rs new file mode 100644 index 0000000000..b7a0f90675 --- /dev/null +++ b/subxt/src/extrinsics/extrinsics_type.rs @@ -0,0 +1,316 @@ +// This file is dual-licensed as Apache-2.0 or GPL-3.0. +// see LICENSE for license details. + +use crate::{ + client::{OfflineClientT, OnlineClientT}, + config::{Config, Hasher, Header}, + dynamic::DecodedValueThunk, + error::{BlockError, Error, ExtrinsicError}, + events, + metadata::DecodeWithMetadata, + rpc::types::{ChainBlock, ChainBlockExtrinsic, ChainBlockResponse}, + runtime_api::RuntimeApi, + storage::Storage, + Metadata, +}; +use codec::{Codec, Decode, Encode}; +use derivative::Derivative; +use frame_metadata::v15::RuntimeMetadataV15; +use futures::lock::Mutex as AsyncMutex; +use scale_value::{scale::decode_as_type, Value}; +use std::{collections::HashMap, marker::PhantomData, sync::Arc}; + +/// A collection of extrinsics obtained from a block, bundled with the necessary +/// information needed to decode and iterate over them. +#[derive(Derivative)] +#[derivative(Debug(bound = ""), Clone(bound = ""))] +pub struct Extrinsics { + metadata: Metadata, + /// The block hash. + hash: T::Hash, + /// The accompanying extrinsics. + extrinsics: Vec, + /// Generic extrinsic parameter ids from the metadata. + ids: ExtrinsicIds, +} + +impl Extrinsics { + pub(crate) fn new(metadata: Metadata, block: ChainBlock) -> Result { + let ids = ExtrinsicIds::new(metadata.runtime_metadata())?; + + Ok(Self { + metadata, + hash: block.header.hash(), + extrinsics: block.extrinsics, + ids, + }) + } + + // /// Obtain the extrinsics from a block hash given custom metadata and a client. + // /// + // /// This method gives users the ability to inspect the extrinsics of older blocks, + // /// where the metadata changed. For those cases, the user is responsible for + // /// providing a valid metadata. + // pub async fn new_from_client( + // metadata: Metadata, + // block_hash: T::Hash, + // client: Client, + // ) -> Result + // where + // Client: OnlineClientT, + // { + // // let event_bytes = get_event_bytes(&client, Some(block_hash)).await?; + // // Ok(Events::new(metadata, block_hash, event_bytes)) + // } + + /// The number of extrinsics. + pub fn len(&self) -> usize { + self.extrinsics.len() + } + + /// Are there no extrinsics in this block? + pub fn is_empty(&self) -> bool { + self.extrinsics.len() == 0 + } + + /// Return the block hash that these extrinsics are from. + pub fn block_hash(&self) -> T::Hash { + self.hash + } + + /// Iterate over all of the events, using metadata to dynamically + /// decode them as we go, and returning the raw bytes and other associated + /// details. If an error occurs, all subsequent iterations return `None`. + // Dev note: The returned iterator is 'static + Send so that we can box it up and make + // use of it with our `FilterEvents` stuff. + pub fn iter( + &self, + ) -> impl Iterator> + Send + Sync + 'static { + let metadata = self.metadata.clone(); + // TODO: Dummy clone should use Arc<[u8]>. + let extrinsics = self.extrinsics.clone(); + let ids = self.ids.clone(); + let num_extr = self.extrinsics.len(); + let mut index = 0; + std::iter::from_fn(move || { + if index == num_extr { + None + } else { + match ExtrinsicDetails::decode_from::( + metadata.clone(), + extrinsics[index].clone(), + ids, + index, + ) { + Ok(event_details) => { + // Increment the index: + index += 1; + // Return the event details: + Some(Ok(event_details)) + } + Err(e) => { + // By setting the position to the "end" of the event bytes, + // the cursor len will become 0 and the iterator will return `None` + // from now on: + index = num_extr; + Some(Err(e)) + } + } + } + }) + } + + // /// Iterate through the events using metadata to dynamically decode and skip + // /// them, and return only those which should decode to the provided `Ev` type. + // /// If an error occurs, all subsequent iterations return `None`. + // pub fn find(&self) -> impl Iterator> + '_ { + // self.iter().filter_map(|ev| { + // ev.and_then(|ev| ev.as_event::().map_err(Into::into)) + // .transpose() + // }) + // } + + // /// Iterate through the events using metadata to dynamically decode and skip + // /// them, and return the first event found which decodes to the provided `Ev` type. + // pub fn find_first(&self) -> Result, Error> { + // self.find::().next().transpose() + // } + + // /// Iterate through the events using metadata to dynamically decode and skip + // /// them, and return the last event found which decodes to the provided `Ev` type. + // pub fn find_last(&self) -> Result, Error> { + // self.find::().last().transpose() + // } + + // /// Find an event that decodes to the type provided. Returns true if it was found. + // pub fn has(&self) -> Result { + // Ok(self.find::().next().transpose()?.is_some()) + // } +} + +/// 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)] +struct ExtrinsicIds { + /// The address (source) of the extrinsic. + address: u32, + /// The extrinsic call type. + call: u32, + /// The signature of the extrinsic. + signature: u32, + /// The extra parameters of the extrinsic. + extra: u32, +} + +impl ExtrinsicIds { + /// Extract the generic type parameters IDs from the extrinsic type. + fn new(metadata: &RuntimeMetadataV15) -> Result { + 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(ExtrinsicError::MissingType); + }; + + let params: HashMap<_, _> = ty + .type_params + .iter() + .map(|ty_param| { + let Some(ty) = ty_param.ty else { + return Err(ExtrinsicError::MissingType); + }; + + Ok((ty_param.name.as_str(), ty.id)) + }) + .collect::>()?; + + let Some(address) = params.get(ADDRESS) else { + return Err(ExtrinsicError::MissingType); + }; + let Some(call) = params.get(CALL) else { + return Err(ExtrinsicError::MissingType); + }; + let Some(signature) = params.get(SIGNATURE) else { + return Err(ExtrinsicError::MissingType); + }; + let Some(extra) = params.get(EXTRA) else { + return Err(ExtrinsicError::MissingType); + }; + + Ok(ExtrinsicIds { + address: *address, + call: *call, + signature: *signature, + extra: *extra, + }) + } +} + +/// The extrinsic details. +#[derive(Debug, Clone)] +pub struct ExtrinsicDetails { + ids: ExtrinsicIds, + index: usize, + metadata: Metadata, + signature: Option>, +} + +impl ExtrinsicDetails { + // Attempt to dynamically decode a single event from our events input. + fn decode_from( + metadata: Metadata, + extrinsic: ChainBlockExtrinsic, + ids: ExtrinsicIds, + index: usize, + ) -> Result { + const SIGNATURE_MASK: u8 = 0b1000_0000; + const VERSION_MASK: u8 = 0b0111_1111; + const LATEST_EXTRINSIC_VERSION: u8 = 4; + + let bytes = extrinsic.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 + if bytes.is_empty() { + return Err(ExtrinsicError::InsufficientData.into()); + } + + let version = bytes[0] & VERSION_MASK; + if version != LATEST_EXTRINSIC_VERSION { + return Err(ExtrinsicError::UnsupportedVersion(version).into()); + } + + let is_signed = bytes[0] & SIGNATURE_MASK != 0; + + // Skip over the first byte which denotes the version and signing. + let cursor = &mut &bytes[1..]; + + let signature = if is_signed { + let address = decode_as_type(cursor, ids.address, &metadata.runtime_metadata().types) + .map_err(scale_decode::Error::from)?; + + let _signature = + decode_as_type(cursor, ids.signature, &metadata.runtime_metadata().types) + .map_err(scale_decode::Error::from)?; + + let _extra = decode_as_type(cursor, ids.extra, &metadata.runtime_metadata().types) + .map_err(scale_decode::Error::from)?; + + Some(address) + } else { + None + }; + + // Decode the extrinsic function call. + let extrinsic = ::decode_with_metadata( + cursor, ids.call, &metadata, + )?; + + Ok(ExtrinsicDetails { + ids, + index, + metadata, + signature, + }) + } + + /// The index of the extrinsic in the given block. + /// What index is this event in the stored events for this block. + pub fn index(&self) -> usize { + self.index + } + + // /// Return _all_ of the bytes representing this event, which include, in order: + // /// - The phase. + // /// - Pallet and event index. + // /// - Event fields. + // /// - Event Topics. + // pub fn bytes(&self) -> &[u8] { + // &self.all_bytes[self.start_idx..self.end_idx] + // } + + // /// Attempt to statically decode these [`EventDetails`] into a type representing the event + // /// fields. This leans directly on [`codec::Decode`]. You can also attempt to decode the entirety + // /// of the event using [`EventDetails::as_root_event()`], which is more lenient because it's able + // /// to lean on [`scale_decode::DecodeAsType`]. + // pub fn as_event(&self) -> Result, Error> { + // let ev_metadata = self.event_metadata(); + // if ev_metadata.pallet() == E::PALLET && ev_metadata.event() == E::EVENT { + // let decoded = E::decode_as_fields( + // &mut self.field_bytes(), + // ev_metadata.fields(), + // self.metadata.types(), + // )?; + // Ok(Some(decoded)) + // } else { + // Ok(None) + // } + // } +} diff --git a/subxt/src/extrinsics/mod.rs b/subxt/src/extrinsics/mod.rs new file mode 100644 index 0000000000..00cebd8f7b --- /dev/null +++ b/subxt/src/extrinsics/mod.rs @@ -0,0 +1,23 @@ +// 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. + +//! This module exposes the types and such necessary for working with extrinsics. +//! The two main entry points into events are [`crate::OnlineClient::events()`] +//! and calls like [crate::tx::TxProgress::wait_for_finalized_success()]. + +mod extrinsics_client; +mod extrinsics_type; +use codec::{Decode, Encode}; + +pub use extrinsics_client::ExtrinsicsClient; +pub use extrinsics_type::{ExtrinsicDetails, Extrinsics}; + +// pub use events_client::EventsClient; +// pub use events_type::{ +// EventDetails, +// Events, +// // Used in codegen but hidden from docs: +// RootEvent, +// }; +// use scale_decode::DecodeAsFields; diff --git a/subxt/src/lib.rs b/subxt/src/lib.rs index 3b41f95963..6317f1a266 100644 --- a/subxt/src/lib.rs +++ b/subxt/src/lib.rs @@ -155,6 +155,7 @@ pub mod constants; pub mod dynamic; pub mod error; pub mod events; +pub mod extrinsics; pub mod metadata; pub mod rpc; pub mod runtime_api;