diff --git a/subxt/examples/blocks_subscribing.rs b/subxt/examples/blocks_subscribing.rs index 4f06221484..54ab2818e6 100644 --- a/subxt/examples/blocks_subscribing.rs +++ b/subxt/examples/blocks_subscribing.rs @@ -48,6 +48,16 @@ async fn main() -> Result<(), Box> { println!(" {pallet_name}_{event_name}"); println!(" {}", event_values); } + + 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}"); + } + } } } diff --git a/subxt/src/blocks/extrinsic_types.rs b/subxt/src/blocks/extrinsic_types.rs index ba83173bbf..6f9d1fbf39 100644 --- a/subxt/src/blocks/extrinsic_types.rs +++ b/subxt/src/blocks/extrinsic_types.rs @@ -12,10 +12,12 @@ use crate::{ Metadata, }; +use crate::dynamic::DecodedValue; use crate::utils::strip_compact_prefix; -use codec::Decode; +use codec::{Compact, Decode}; use derivative::Derivative; use scale_decode::{DecodeAsFields, DecodeAsType}; + use std::sync::Arc; /// Trait to uniquely identify the extrinsic's identity from the runtime metadata. @@ -155,12 +157,8 @@ pub struct ExtrinsicDetails { 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, + /// Some if the extrinsic payload is signed. + signed_details: Option, /// The start index in the `bytes` from which the call is encoded. call_start_idx: usize, /// The pallet index. @@ -178,6 +176,18 @@ pub struct ExtrinsicDetails { _marker: std::marker::PhantomData, } +/// 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 ExtrinsicDetails where T: Config, @@ -217,38 +227,45 @@ where // Skip over the first byte which denotes the version and signing. let cursor = &mut &bytes[1..]; - let mut address_start_idx = 0; - let mut address_end_idx = 0; - - if is_signed { - 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, - ) - .map_err(scale_decode::Error::from)?; - address_end_idx = bytes.len() - cursor.len(); - - scale_decode::visitor::decode_with_visitor( - cursor, - ids.signature, - metadata.types(), - scale_decode::visitor::IgnoreVisitor, - ) - .map_err(scale_decode::Error::from)?; - - scale_decode::visitor::decode_with_visitor( - cursor, - ids.extra, - metadata.types(), - scale_decode::visitor::IgnoreVisitor, - ) - .map_err(scale_decode::Error::from)?; - } + let signed_details = is_signed + .then(|| -> Result { + 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, + ) + .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, + ) + .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, + ) + .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(); @@ -261,9 +278,7 @@ where Ok(ExtrinsicDetails { index, bytes, - is_signed, - address_start_idx, - address_end_idx, + signed_details, call_start_idx, pallet_index, variant_index, @@ -277,7 +292,7 @@ where /// Is the extrinsic signed? pub fn is_signed(&self) -> bool { - self.is_signed + self.signed_details.is_some() } /// The index of the extrinsic in the block. @@ -326,8 +341,38 @@ where /// /// 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]) + self.signed_details + .as_ref() + .map(|e| &self.bytes[e.address_start_idx..e.address_end_idx]) + } + + /// 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]) + } + + /// Returns the signed extension `extra` bytes of the extrinsic. + /// Each signed extension has an `extra` type (May be zero-sized). + /// These bytes are the scale encoded `extra` fields of each signed extension in order of the signed extensions. + /// They do *not* include the `additional` signed bytes that are used as part of the payload that is signed. + /// + /// 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]) + } + + /// Returns `None` if the extrinsic is not signed. + pub fn signed_extensions(&self) -> Option> { + let signed = self.signed_details.as_ref()?; + let extra_bytes = &self.bytes[signed.signature_end_idx..signed.extra_end_idx]; + Some(ExtrinsicSignedExtensions { + bytes: extra_bytes, + metadata: &self.metadata, + }) } /// The index of the pallet that the extrinsic originated from. @@ -558,6 +603,119 @@ impl ExtrinsicEvents { } } +/// The signed extensions of an extrinsic. +#[derive(Debug, Clone)] +pub struct ExtrinsicSignedExtensions<'a> { + bytes: &'a [u8], + metadata: &'a Metadata, +} + +impl<'a> ExtrinsicSignedExtensions<'a> { + /// 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, Error>> { + let signed_extension_types = self.metadata.extrinsic().signed_extensions(); + let num_signed_extensions = signed_extension_types.len(); + let bytes = self.bytes; + let metadata = self.metadata; + let mut index = 0; + let mut byte_start_idx = 0; + + std::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, + ) + .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, + })) + }) + } + + /// The tip of an extrinsic, extracted from the ChargeTransactionPayment or ChargeAssetTxPayment + /// signed extension, depending on which is present. + /// + /// Returns `None` if `tip` was not found or decoding failed. + pub fn tip(&self) -> Option { + let tip = self.iter().find_map(|e| { + e.ok().filter(|e| { + e.name() == "ChargeTransactionPayment" || e.name() == "ChargeAssetTxPayment" + }) + })?; + + // Note: ChargeAssetTxPayment might have addition information in it (asset_id). + // But both should start with a compact encoded u128, so this decoding is fine. + let tip = Compact::::decode(&mut tip.bytes()).ok()?.0; + Some(tip) + } + + /// The nonce of the account that submitted the extrinsic, extracted from the CheckNonce signed extension. + /// + /// Returns `None` if `nonce` was not found or decoding failed. + pub fn nonce(&self) -> Option { + let nonce = self + .iter() + .find_map(|e| e.ok().filter(|e| e.name() == "CheckNonce"))?; + let nonce = Compact::::decode(&mut nonce.bytes()).ok()?.0; + Some(nonce) + } +} + +/// A single signed extension +#[derive(Debug, Clone)] +pub struct ExtrinsicSignedExtension<'a> { + bytes: &'a [u8], + ty_id: u32, + identifier: &'a str, + metadata: &'a Metadata, +} + +impl<'a> ExtrinsicSignedExtension<'a> { + /// The bytes representing this signed extension. + pub fn bytes(&self) -> &'a [u8] { + self.bytes + } + + /// The name of the signed extension. + pub fn name(&self) -> &'a str { + self.identifier + } + + /// The type id of the signed extension. + pub fn type_id(&self) -> u32 { + self.ty_id + } + + /// Signed Extension as a [`scale_value::Value`] + pub fn value(&self) -> Result { + let value = + DecodedValue::decode_as_type(&mut &self.bytes[..], self.ty_id, self.metadata.types())?; + + Ok(value) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/subxt/src/blocks/mod.rs b/subxt/src/blocks/mod.rs index fd06594e60..7cf2501b9d 100644 --- a/subxt/src/blocks/mod.rs +++ b/subxt/src/blocks/mod.rs @@ -13,7 +13,10 @@ pub use crate::backend::BlockRef; pub use block_types::Block; pub use blocks_client::BlocksClient; -pub use extrinsic_types::{ExtrinsicDetails, ExtrinsicEvents, Extrinsics, StaticExtrinsic}; +pub use extrinsic_types::{ + ExtrinsicDetails, ExtrinsicEvents, ExtrinsicSignedExtension, ExtrinsicSignedExtensions, + Extrinsics, StaticExtrinsic, +}; // We get account nonce info in tx_client, too, so re-use the logic: pub(crate) use block_types::get_account_nonce; diff --git a/testing/integration-tests/src/full_client/blocks/mod.rs b/testing/integration-tests/src/full_client/blocks/mod.rs index d48bdd271c..ae2373f035 100644 --- a/testing/integration-tests/src/full_client/blocks/mod.rs +++ b/testing/integration-tests/src/full_client/blocks/mod.rs @@ -5,6 +5,8 @@ use crate::{test_context, utils::node_runtime}; use codec::{Compact, Encode}; use futures::StreamExt; + +use subxt::config::DefaultExtrinsicParamsBuilder; use subxt_metadata::Metadata; use subxt_signer::sr25519::dev; @@ -227,3 +229,81 @@ async fn fetch_block_and_decode_extrinsic_details() { assert_eq!(ext.value, 10_000); assert!(tx.is_signed()); } + +#[tokio::test] +async fn decode_signed_extensions_from_blocks() { + let ctx = test_context().await; + let api = ctx.client(); + let alice = dev::alice(); + let bob = dev::bob(); + + macro_rules! submit_transfer_extrinsic_and_get_it_back { + ($tip:expr) => {{ + let tx = node_runtime::tx() + .balances() + .transfer_allow_death(bob.public_key().into(), 10_000); + + let signed_extrinsic = api + .tx() + .create_signed( + &tx, + &alice, + DefaultExtrinsicParamsBuilder::new().tip($tip).build(), + ) + .await + .unwrap(); + + let in_block = signed_extrinsic + .submit_and_watch() + .await + .unwrap() + .wait_for_finalized() + .await + .unwrap(); + + 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(); + extrinsic_details + }}; + } + + let expected_signed_extensions = [ + "CheckNonZeroSender", + "CheckSpecVersion", + "CheckTxVersion", + "CheckGenesis", + "CheckMortality", + "CheckNonce", + "CheckWeight", + "ChargeAssetTxPayment", + ]; + + let transaction1 = submit_transfer_extrinsic_and_get_it_back!(1234); + let extensions1 = transaction1.signed_extensions().unwrap(); + let nonce1 = extensions1.nonce().unwrap(); + let tip1 = extensions1.tip().unwrap(); + + let transaction2 = submit_transfer_extrinsic_and_get_it_back!(5678); + let extensions2 = transaction2.signed_extensions().unwrap(); + let nonce2 = extensions2.nonce().unwrap(); + let tip2 = extensions2.tip().unwrap(); + + assert_eq!(nonce1, 0); + assert_eq!(tip1, 1234); + assert_eq!(nonce2, 1); + assert_eq!(tip2, 5678); + + 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!(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); + } +}