From 853732550b1460ca9e542403fb725ecd54ceaf08 Mon Sep 17 00:00:00 2001 From: James Wilson Date: Mon, 15 Dec 2025 17:24:07 +0000 Subject: [PATCH] Simplify paths to extrinsic and storage types needed for .find --- Cargo.lock | 1 + codegen/src/api/calls.rs | 24 +- codegen/src/api/mod.rs | 4 +- codegen/src/api/storage.rs | 12 +- subxt/Cargo.toml | 1 + subxt/examples/advanced_decoding.rs | 380 ++++++++++++++++++++++++++++ subxt/examples/blocks.rs | 16 +- subxt/examples/storage_entries.rs | 2 +- subxt/src/extrinsics.rs | 2 +- subxt/src/lib.rs | 1 + subxt/src/storage/storage_value.rs | 2 +- 11 files changed, 418 insertions(+), 27 deletions(-) create mode 100644 subxt/examples/advanced_decoding.rs diff --git a/Cargo.lock b/Cargo.lock index 11549a9869..a74c6308e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5634,6 +5634,7 @@ dependencies = [ "scale-encode", "scale-info", "scale-info-legacy", + "scale-type-resolver", "scale-value", "serde", "serde_json", diff --git a/codegen/src/api/calls.rs b/codegen/src/api/calls.rs index 28dfe9ef9b..3c798f0cd1 100644 --- a/codegen/src/api/calls.rs +++ b/codegen/src/api/calls.rs @@ -41,14 +41,14 @@ pub fn generate_calls( CompositeIRKind::Named(named_fields) => named_fields .iter() .map(|(name, field)| { - // Note: fn_arg_type this is relative the type path of the type alias when prefixed with `types::`, e.g. `set_max_code_size::New` + // Note: fn_arg_type this is relative the type path of the type alias when prefixed with `super::`, e.g. `set_max_code_size::New` let fn_arg_type = field.type_path.to_token_stream(type_gen.settings()); let call_arg = if field.is_boxed { quote! { #name: #crate_path::alloc::boxed::Box::new(#name) } } else { quote! { #name } }; - (quote!( #name: types::#fn_arg_type ), call_arg) + (quote!( #name: super::#fn_arg_type ), call_arg) }) .unzip(), CompositeIRKind::NoFields => Default::default(), @@ -97,11 +97,11 @@ pub fn generate_calls( pub fn #fn_name( &self, #( #call_fn_args, )* - ) -> #crate_path::transactions::StaticPayload { + ) -> #crate_path::transactions::StaticPayload { #crate_path::transactions::StaticPayload::new_static( #pallet_name, #call_name, - types::#struct_name { #( #call_args, )* }, + super::#struct_name { #( #call_args, )* }, [#(#call_hash,)*] ) } @@ -128,18 +128,14 @@ pub fn generate_calls( use super::root_mod; use super::#types_mod_ident; - type DispatchError = #types_mod_ident::sp_runtime::DispatchError; + #( #call_structs )* - pub mod types { - use super::#types_mod_ident; + pub mod api { + pub struct TransactionApi; - #( #call_structs )* - } - - pub struct TransactionApi; - - impl TransactionApi { - #( #call_fns )* + impl TransactionApi { + #( #call_fns )* + } } } }) diff --git a/codegen/src/api/mod.rs b/codegen/src/api/mod.rs index 5235f68271..280bdcd0cd 100644 --- a/codegen/src/api/mod.rs +++ b/codegen/src/api/mod.rs @@ -333,8 +333,8 @@ impl RuntimeGenerator { pub struct TransactionApi; impl TransactionApi { #( - pub fn #pallets_with_calls(&self) -> #pallets_with_calls::calls::TransactionApi { - #pallets_with_calls::calls::TransactionApi + pub fn #pallets_with_calls(&self) -> #pallets_with_calls::calls::api::TransactionApi { + #pallets_with_calls::calls::api::TransactionApi } )* } diff --git a/codegen/src/api/storage.rs b/codegen/src/api/storage.rs index 580d0b7cdd..294a283154 100644 --- a/codegen/src/api/storage.rs +++ b/codegen/src/api/storage.rs @@ -114,7 +114,7 @@ fn generate_storage_entry_fns( .iter() .map(|i| { let ty = &i.type_alias; - quote!(#storage_entry_snake_case_ident::#ty) + quote!(#storage_entry_snake_case_ident::input::#ty) }) .collect::>(); @@ -142,12 +142,12 @@ fn generate_storage_entry_fns( use super::root_mod; use super::#types_mod_ident; - #(#storage_key_type_aliases)* - - pub mod output { + pub mod input { use super::#types_mod_ident; - pub type Output = #storage_value_type_path; + #(#storage_key_type_aliases)* } + + pub type Output = #storage_value_type_path; } ); @@ -155,7 +155,7 @@ fn generate_storage_entry_fns( #docs pub fn #storage_entry_snake_case_ident(&self) -> #crate_path::storage::StaticAddress< (#(#storage_key_tuple_types,)*), - #storage_entry_snake_case_ident::output::Output, + #storage_entry_snake_case_ident::Output, #is_plain > { #crate_path::storage::StaticAddress::new_static( diff --git a/subxt/Cargo.toml b/subxt/Cargo.toml index 1e4d902ff5..5979b0e3cf 100644 --- a/subxt/Cargo.toml +++ b/subxt/Cargo.toml @@ -89,6 +89,7 @@ codec = { package = "parity-scale-codec", workspace = true, features = ["derive" derive-where = { workspace = true } scale-info = { workspace = true, features = ["default"] } scale-info-legacy = { workspace = true } +scale-type-resolver = { workspace = true } scale-value = { workspace = true, features = ["default"] } scale-bits = { workspace = true, features = ["default"] } scale-decode = { workspace = true, features = ["default"] } diff --git a/subxt/examples/advanced_decoding.rs b/subxt/examples/advanced_decoding.rs new file mode 100644 index 0000000000..b68bb97460 --- /dev/null +++ b/subxt/examples/advanced_decoding.rs @@ -0,0 +1,380 @@ +//! Use a scale_decode::visitor::Visitor implementation to have more control over decoding. +//! +//! Here we decode extrinsic fields, but anywhere with a `.visit()` method can do the same, +//! for example storage values. +use std::error::Error; +use subxt::{OnlineClient, PolkadotConfig}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Create a new API client, configured to talk to Polkadot nodes. + let config = PolkadotConfig::new(); + let api = OnlineClient::new(config).await?; + + // Stream the finalized blocks. See the OnlineClient docs for how to + // stream best blocks or all new blocks. + let mut blocks = api.stream_blocks().await?; + + while let Some(block) = blocks.next().await { + let block = block?; + let at_block = block.at().await?; + println!("Block #{}", at_block.block_number()); + + // Fetch the block extrinsics to decode: + let extrinsics = at_block.extrinsics().fetch().await?; + for ext in extrinsics.iter() { + let ext = ext?; + + println!(" {}.{}", ext.pallet_name(), ext.call_name()); + for field in ext.iter_call_data_fields() { + // This is a visitor. Here, we pass it type information so that it can internally + // lookup information about types that it's visiting, as an example. + let visitor = value::GetValue::new(at_block.metadata_ref().types()); + + // Use this visitor to decode the extrinsic field into a Value. + // A `visit` method like this is also provided for storage values, allowing for + // the same sort of decoding. + let decoded_value = field.visit(visitor)?; + + println!(" {}: {:?}", field.name(), decoded_value) + } + } + } + + Ok(()) +} + +/// This visitor demonstrates how to decode and return a custom Value shape +mod value { + use scale_decode::{ + Visitor, + visitor::TypeIdFor, + visitor::types::{Array, BitSequence, Composite, Sequence, Str, Tuple, Variant}, + }; + use std::collections::HashMap; + use subxt::ext::scale_type_resolver::TypeResolver; + + /// A value type we're decoding into. + #[derive(Debug)] + #[allow(dead_code)] + pub enum Value { + Number(f64), + BigNumber(String), + Bool(bool), + Char(char), + Array(Vec), + String(String), + Address(Vec), + I256([u8; 32]), + U256([u8; 32]), + Struct(HashMap), + VariantWithoutData(String), + VariantWithData(String, VariantFields), + } + + #[derive(Debug)] + pub enum VariantFields { + Unnamed(Vec), + Named(HashMap), + } + + /// An error we can encounter trying to decode things into a [`Value`] + #[derive(Debug, thiserror::Error)] + pub enum ValueError { + #[error("Decode error: {0}")] + Decode(#[from] scale_decode::visitor::DecodeError), + #[error("Cannot decode bit sequence: {0}")] + CannotDecodeBitSequence(codec::Error), + #[error("Cannot resolve variant type information: {0}")] + CannotResolveVariantType(String), + } + + /// This is a visitor which obtains type names. + pub struct GetValue<'r, R> { + resolver: &'r R, + } + + impl<'r, R> GetValue<'r, R> { + /// Construct our TypeName visitor. + pub fn new(resolver: &'r R) -> Self { + GetValue { resolver } + } + } + + impl<'r, R: TypeResolver> Visitor for GetValue<'r, R> { + type Value<'scale, 'resolver> = Value; + type Error = ValueError; + type TypeResolver = R; + + fn visit_i256<'resolver>( + self, + value: &[u8; 32], + _type_id: TypeIdFor, + ) -> Result, Self::Error> { + Ok(Value::I256(*value)) + } + + fn visit_u256<'resolver>( + self, + value: &[u8; 32], + _type_id: TypeIdFor, + ) -> Result, Self::Error> { + Ok(Value::U256(*value)) + } + + fn visit_i128<'scale, 'resolver>( + self, + value: i128, + _type_id: TypeIdFor, + ) -> Result, Self::Error> { + let attempt = value as f64; + if attempt as i128 == value { + Ok(Value::Number(attempt)) + } else { + Ok(Value::BigNumber(value.to_string())) + } + } + + fn visit_i64<'scale, 'resolver>( + self, + value: i64, + type_id: TypeIdFor, + ) -> Result, Self::Error> { + self.visit_i128(value.into(), type_id) + } + + fn visit_i32<'scale, 'resolver>( + self, + value: i32, + type_id: TypeIdFor, + ) -> Result, Self::Error> { + self.visit_i128(value.into(), type_id) + } + + fn visit_i16<'scale, 'resolver>( + self, + value: i16, + type_id: TypeIdFor, + ) -> Result, Self::Error> { + self.visit_i128(value.into(), type_id) + } + + fn visit_i8<'scale, 'resolver>( + self, + value: i8, + type_id: TypeIdFor, + ) -> Result, Self::Error> { + self.visit_i128(value.into(), type_id) + } + + fn visit_u128<'scale, 'resolver>( + self, + value: u128, + _type_id: TypeIdFor, + ) -> Result, Self::Error> { + let attempt = value as f64; + if attempt as u128 == value { + Ok(Value::Number(attempt)) + } else { + Ok(Value::BigNumber(value.to_string())) + } + } + + fn visit_u64<'scale, 'resolver>( + self, + value: u64, + type_id: TypeIdFor, + ) -> Result, Self::Error> { + self.visit_u128(value.into(), type_id) + } + + fn visit_u32<'scale, 'resolver>( + self, + value: u32, + type_id: TypeIdFor, + ) -> Result, Self::Error> { + self.visit_u128(value.into(), type_id) + } + + fn visit_u16<'scale, 'resolver>( + self, + value: u16, + type_id: TypeIdFor, + ) -> Result, Self::Error> { + self.visit_u128(value.into(), type_id) + } + + fn visit_u8<'scale, 'resolver>( + self, + value: u8, + type_id: TypeIdFor, + ) -> Result, Self::Error> { + self.visit_u128(value.into(), type_id) + } + + fn visit_bool<'scale, 'resolver>( + self, + value: bool, + _type_id: TypeIdFor, + ) -> Result, Self::Error> { + Ok(Value::Bool(value)) + } + + fn visit_char<'scale, 'resolver>( + self, + value: char, + _type_id: TypeIdFor, + ) -> Result, Self::Error> { + Ok(Value::Char(value)) + } + + fn visit_array<'scale, 'resolver>( + self, + values: &mut Array<'scale, 'resolver, Self::TypeResolver>, + _type_id: TypeIdFor, + ) -> Result, Self::Error> { + Ok(Value::Array(to_array( + self.resolver, + values.remaining(), + values, + )?)) + } + + fn visit_sequence<'scale, 'resolver>( + self, + values: &mut Sequence<'scale, 'resolver, Self::TypeResolver>, + _type_id: TypeIdFor, + ) -> Result, Self::Error> { + Ok(Value::Array(to_array( + self.resolver, + values.remaining(), + values, + )?)) + } + + fn visit_str<'scale, 'resolver>( + self, + value: &mut Str<'scale>, + _type_id: TypeIdFor, + ) -> Result, Self::Error> { + Ok(Value::String(value.as_str()?.to_owned())) + } + + fn visit_tuple<'scale, 'resolver>( + self, + values: &mut Tuple<'scale, 'resolver, Self::TypeResolver>, + _type_id: TypeIdFor, + ) -> Result, Self::Error> { + Ok(Value::Array(to_array( + self.resolver, + values.remaining(), + values, + )?)) + } + + fn visit_bitsequence<'scale, 'resolver>( + self, + value: &mut BitSequence<'scale>, + _type_id: TypeIdFor, + ) -> Result, Self::Error> { + let bits = value.decode()?; + let mut out = Vec::with_capacity(bits.len()); + for b in bits { + let b = b.map_err(ValueError::CannotDecodeBitSequence)?; + out.push(Value::Bool(b)); + } + Ok(Value::Array(out)) + } + + fn visit_composite<'scale, 'resolver>( + self, + value: &mut Composite<'scale, 'resolver, Self::TypeResolver>, + _type_id: TypeIdFor, + ) -> Result, Self::Error> { + // Special case for ss58 addresses: + if let Some(n) = value.name() + && n == "AccountId32" + && value.bytes_from_start().len() == 32 + { + return Ok(Value::Address(value.bytes_from_start().to_vec())); + } + + // Reuse logic for decoding variant fields: + match to_variant_fieldish(self.resolver, value)? { + VariantFields::Named(s) => Ok(Value::Struct(s)), + VariantFields::Unnamed(a) => Ok(Value::Array(a)), + } + } + + fn visit_variant<'scale, 'resolver>( + self, + value: &mut Variant<'scale, 'resolver, Self::TypeResolver>, + type_id: TypeIdFor, + ) -> Result, Self::Error> { + // Because we have access to a type resolver on self, we can + // look up the type IDs we're given back and base decode decisions + // on them. here we see whether the enum type has any data attached: + let has_data_visitor = scale_type_resolver::visitor::new((), |_, _| false) + .visit_variant(|_, _, variants| { + for mut variant in variants { + if variant.fields.next().is_some() { + return true; + } + } + false + }); + + // Do any variants have data in this enum type? + let has_data = self + .resolver + .resolve_type(type_id, has_data_visitor) + .map_err(|e| ValueError::CannotResolveVariantType(e.to_string()))?; + + let name = value.name().to_owned(); + + // base our decoding on whether any data in enum type. + if has_data { + let fields = to_variant_fieldish(self.resolver, value.fields())?; + Ok(Value::VariantWithData(name, fields)) + } else { + Ok(Value::VariantWithoutData(name)) + } + } + } + + fn to_variant_fieldish<'r, 'scale, 'resolver, R: TypeResolver>( + resolver: &'r R, + value: &mut Composite<'scale, 'resolver, R>, + ) -> Result { + // If fields are unnamed, treat as array: + if value.fields().iter().all(|f| f.name.is_none()) { + return Ok(VariantFields::Unnamed(to_array( + resolver, + value.remaining(), + value, + )?)); + } + + // Otherwise object: + let mut out = HashMap::new(); + for field in value { + let field = field?; + let name = field.name().unwrap().to_string(); + let value = field.decode_with_visitor(GetValue::new(resolver))?; + out.insert(name, value); + } + Ok(VariantFields::Named(out)) + } + + fn to_array<'r, 'scale, 'resolver, R: TypeResolver>( + resolver: &'r R, + len: usize, + mut values: impl scale_decode::visitor::DecodeItemIterator<'scale, 'resolver, R>, + ) -> Result, ValueError> { + let mut out = Vec::with_capacity(len); + while let Some(value) = values.decode_item(GetValue::new(resolver)) { + out.push(value?); + } + Ok(out) + } +} diff --git a/subxt/examples/blocks.rs b/subxt/examples/blocks.rs index cc83314c01..f9fde29e63 100644 --- a/subxt/examples/blocks.rs +++ b/subxt/examples/blocks.rs @@ -29,7 +29,7 @@ async fn main() -> Result<(), Error> { // same interface as `api.block(block.hash()).await`. let at_block = block.at().await?; - // Here we'll obtain and display the extrinsics: + // Here we'll iterate over the extrinsics and display information about each one: let extrinsics = at_block.extrinsics().fetch().await?; for ext in extrinsics.iter() { let ext = ext?; @@ -40,7 +40,9 @@ async fn main() -> Result<(), Error> { let bytes_hex = format!("0x{}", hex::encode(ext.bytes())); let events = ext.events().await?; - // See the API docs for more ways to decode extrinsics: + // See the API docs for more ways to decode extrinsics. Here we decode into + // a statically generated type, but any type implementing scale_decode::DecodeAsType + // can be used here, for instance subxt::dynamic::Value. let decoded_ext = ext.decode_call_data_as::()?; println!(" #{idx}: {pallet_name}.{call_name}:"); @@ -68,6 +70,16 @@ async fn main() -> Result<(), Error> { } } } + + // Instead of iterating, we can also use the static interface to search & decode specific + // extrinsics if we know what we are looking for: + if let Some(ext) = extrinsics.find_first::() { + let ext = ext?; + + println!("ParaInherent.Enter"); + println!(" backed_candidated: {:?}", ext.data.backed_candidates); + println!(" disputes: {:?}", ext.data.disputes); + } } Ok(()) diff --git a/subxt/examples/storage_entries.rs b/subxt/examples/storage_entries.rs index 0296b83db8..856f3cf6ae 100644 --- a/subxt/examples/storage_entries.rs +++ b/subxt/examples/storage_entries.rs @@ -40,7 +40,7 @@ async fn main() -> Result<(), Error> { // Or we can decode into a known shape. Any type implementing DecodeAsType can // be used here to extract fields you're interested in, or we can use the generated // type which already implements this: - type AccountInfo = polkadot::system::storage::account::output::Output; + type AccountInfo = polkadot::system::storage::account::Output; let balance_info = entry.decode_as::()?; println!( diff --git a/subxt/src/extrinsics.rs b/subxt/src/extrinsics.rs index e82833efd0..e62841a9fe 100644 --- a/subxt/src/extrinsics.rs +++ b/subxt/src/extrinsics.rs @@ -410,7 +410,7 @@ impl<'atblock, 'extrinsic> ExtrinsicCallDataField<'atblock, 'extrinsic> { } /// Visit this field with the provided visitor, returning the output from it. - pub fn visit(&self, visitor: V) -> Result, V::Error> + pub fn visit(&self, visitor: V) -> Result, V::Error> where V: scale_decode::visitor::Visitor, { diff --git a/subxt/src/lib.rs b/subxt/src/lib.rs index 4b9d727a7e..3782b6d649 100644 --- a/subxt/src/lib.rs +++ b/subxt/src/lib.rs @@ -90,6 +90,7 @@ pub mod ext { pub use scale_bits; pub use scale_decode; pub use scale_encode; + pub use scale_type_resolver; pub use scale_value; pub use subxt_rpcs; diff --git a/subxt/src/storage/storage_value.rs b/subxt/src/storage/storage_value.rs index f2e4636661..cbbdaf7d3e 100644 --- a/subxt/src/storage/storage_value.rs +++ b/subxt/src/storage/storage_value.rs @@ -74,7 +74,7 @@ impl<'info, Value: DecodeAsType> StorageValue<'info, Value> { } /// Visit this storage value with the provided visitor, returning the output from it. - pub fn visit(&self, visitor: V) -> Result, V::Error> + pub fn visit(&self, visitor: V) -> Result, V::Error> where V: scale_decode::visitor::Visitor, {