mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-12 05:21:08 +00:00
Support decoding signed extensions (#1209)
* skeleton commit * signed extension decoding * fix some minor things * make api more similar to Extrinsics * defer decoding of signed extensions * fix byte slices * add test for nonce signed extension * adjust test and extend for tip * clippy * support both ChargeTransactionPayment and ChargeAssetTxPayment * address PR comments * Extend lifetimes, expose pub structs, remove as_type * add signed extensions to block subscribing example * make example less ugly --------- Co-authored-by: James Wilson <james@jsdw.me>
This commit is contained in:
@@ -48,6 +48,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<T: Config, C> {
|
||||
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<SignedExtrinsicDetails>,
|
||||
/// 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<T: Config, C> {
|
||||
_marker: std::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, C> ExtrinsicDetails<T, C>
|
||||
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;
|
||||
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,
|
||||
)
|
||||
.map_err(scale_decode::Error::from)?;
|
||||
let address_end_idx = bytes.len() - cursor.len();
|
||||
|
||||
if is_signed {
|
||||
address_start_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();
|
||||
|
||||
// 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.extra,
|
||||
metadata.types(),
|
||||
scale_decode::visitor::IgnoreVisitor,
|
||||
)
|
||||
.map_err(scale_decode::Error::from)?;
|
||||
let extra_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)?;
|
||||
}
|
||||
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<ExtrinsicSignedExtensions<'_>> {
|
||||
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<T: Config> ExtrinsicEvents<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<Item = Result<ExtrinsicSignedExtension<'a>, 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<u128> {
|
||||
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::<u128>::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<u64> {
|
||||
let nonce = self
|
||||
.iter()
|
||||
.find_map(|e| e.ok().filter(|e| e.name() == "CheckNonce"))?;
|
||||
let nonce = Compact::<u64>::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<DecodedValue, Error> {
|
||||
let value =
|
||||
DecodedValue::decode_as_type(&mut &self.bytes[..], self.ty_id, self.metadata.types())?;
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user