[v0.50.0] Allow visiting extrinsic fields in subxt_historic (#2124)

* Allow visiting extrinsic fields

* fmt

* Don't use local scale-decode dep

* Clippy and tidy

* Extend 'subxt codegen' CLI to work with legacy metadatas

* Simplify historic extrinsics example now that AccountId32s have paths/names

* clippy

* clippy

* clippy..

* Allow visiting storage values, too, and clean up extrinsic visiting a little by narrowing lifetime

* Try to fix flaky test

* Add custom value decode to extrinsics example

* Remove useless else branch ra thought I needed

* Simplify examples
This commit is contained in:
James Wilson
2025-11-21 15:32:32 +00:00
committed by GitHub
parent 1a0e218d32
commit 3bbba1b005
16 changed files with 871 additions and 37 deletions
Generated
+10 -8
View File
@@ -1953,9 +1953,9 @@ dependencies = [
[[package]]
name = "frame-decode"
version = "0.13.0"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73d29c7f2987ea24ab2eaea315aadb9ba598188823181cdf0476049b625a5844"
checksum = "0fb3bfa2988ef40247e0e0eecfb171a01ad6f4e399485503aad4c413a5f236f3"
dependencies = [
"frame-metadata 23.0.0",
"parity-scale-codec",
@@ -4383,9 +4383,9 @@ dependencies = [
[[package]]
name = "scale-decode"
version = "0.16.0"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d78196772d25b90a98046794ce0fe2588b39ebdfbdc1e45b4c6c85dd43bebad"
checksum = "8d6ed61699ad4d54101ab5a817169259b5b0efc08152f8632e61482d8a27ca3d"
dependencies = [
"parity-scale-codec",
"primitive-types",
@@ -4398,9 +4398,9 @@ dependencies = [
[[package]]
name = "scale-decode-derive"
version = "0.16.0"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f4b54a1211260718b92832b661025d1f1a4b6930fbadd6908e00edd265fa5f7"
checksum = "65cb245f7fdb489e7ba43a616cbd34427fe3ba6fe0edc1d0d250085e6c84f3ec"
dependencies = [
"darling",
"proc-macro2",
@@ -4464,9 +4464,9 @@ dependencies = [
[[package]]
name = "scale-info-legacy"
version = "0.3.2"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06423f0d7ea951547143aff4695c4c3e821e66c9b80729a3ff55fa93d23e93e6"
checksum = "2500adfb429a0ffda37919df92c05d0c1359c10e0444c17253c84b84dfce542f"
dependencies = [
"hashbrown 0.15.3",
"scale-type-resolver",
@@ -5658,6 +5658,7 @@ version = "0.44.0"
dependencies = [
"clap",
"color-eyre",
"frame-decode",
"frame-metadata 23.0.0",
"heck",
"hex",
@@ -5667,6 +5668,7 @@ dependencies = [
"pretty_assertions",
"quote",
"scale-info",
"scale-info-legacy",
"scale-typegen 0.12.0",
"scale-typegen-description",
"scale-value",
+3 -3
View File
@@ -81,7 +81,7 @@ darling = "0.20.10"
derive-where = "1.2.7"
either = { version = "1.13.0", default-features = false }
finito = { version = "0.1.0", default-features = false }
frame-decode = { version = "0.14.0", default-features = false }
frame-decode = { version = "0.15.0", default-features = false }
frame-metadata = { version = "23.0.0", default-features = false }
futures = { version = "0.3.31", default-features = false, features = ["std"] }
getrandom = { version = "0.2", default-features = false }
@@ -100,10 +100,10 @@ regex = { version = "1.11.0", default-features = false }
scale-info = { version = "2.11.4", default-features = false }
scale-value = { version = "0.18.1", default-features = false }
scale-bits = { version = "0.7.0", default-features = false }
scale-decode = { version = "0.16.0", default-features = false }
scale-decode = { version = "0.16.2", default-features = false }
scale-encode = { version = "0.10.0", default-features = false }
scale-type-resolver = { version = "0.2.0" }
scale-info-legacy = { version = "0.3.2", default-features = false }
scale-info-legacy = { version = "0.4.0", default-features = false }
scale-typegen = "0.12.0"
scale-typegen-description = "0.11.0"
serde = { version = "1.0.210", default-features = false, features = ["derive"] }
+3 -1
View File
@@ -30,16 +30,18 @@ subxt-codegen = { workspace = true }
scale-typegen = { workspace = true }
subxt-utils-fetchmetadata = { workspace = true, features = ["url"] }
subxt-utils-stripmetadata = { workspace = true }
subxt-metadata = { workspace = true }
subxt-metadata = { workspace = true, features = ["legacy"] }
subxt = { workspace = true, features = ["default"] }
clap = { workspace = true }
serde = { workspace = true, features = ["derive"] }
color-eyre = { workspace = true }
serde_json = { workspace = true }
hex = { workspace = true }
frame-decode = { workspace = true, features = ["legacy-types"] }
frame-metadata = { workspace = true }
codec = { package = "parity-scale-codec", workspace = true }
scale-info = { workspace = true }
scale-info-legacy = { workspace = true }
scale-value = { workspace = true }
syn = { workspace = true }
quote = { workspace = true }
+93 -3
View File
@@ -4,12 +4,12 @@
use crate::utils::{FileOrUrl, validate_url_security};
use clap::Parser as ClapParser;
use codec::Decode;
use color_eyre::eyre::eyre;
use scale_typegen_description::scale_typegen::typegen::{
settings::substitutes::path_segments,
validation::{registry_contains_type_path, similar_type_paths_in_registry},
};
use std::path::PathBuf;
use subxt_codegen::CodegenBuilder;
use subxt_metadata::Metadata;
@@ -28,6 +28,12 @@ pub struct Opts {
/// Additional attributes
#[clap(long = "attribute")]
attributes: Vec<String>,
/// Path to legacy type definitions (required for metadatas pre-V14)
#[clap(long)]
legacy_types: Option<PathBuf>,
/// The spec version of the legacy metadata (required for metadatas pre-V14)
#[clap(long)]
legacy_spec_version: Option<u64>,
/// Additional derives for a given type.
///
/// Example 1: `--derive-for-type my_module::my_type=serde::Serialize`.
@@ -145,9 +151,20 @@ pub async fn run(opts: Opts, output: &mut impl std::io::Write) -> color_eyre::Re
validate_url_security(opts.file_or_url.url.as_ref(), opts.allow_insecure)?;
let bytes = opts.file_or_url.fetch().await?;
let legacy_types = opts
.legacy_types
.map(|path| {
let bytes = std::fs::read(path).map_err(|e| eyre!("Cannot read legacy_types: {e}"))?;
let types = frame_decode::legacy_types::from_bytes(&bytes)
.map_err(|e| eyre!("Cannot deserialize legacy_types: {e}"))?;
Ok::<_, color_eyre::eyre::Error>(types)
})
.transpose()?;
codegen(
&bytes,
legacy_types,
opts.legacy_spec_version,
opts.derives,
opts.attributes,
opts.derives_for_type,
@@ -175,6 +192,8 @@ impl syn::parse::Parse for OuterAttribute {
#[allow(clippy::too_many_arguments)]
fn codegen(
metadata_bytes: &[u8],
legacy_types: Option<scale_info_legacy::ChainTypeRegistry>,
legacy_spec_version: Option<u64>,
raw_derives: Vec<String>,
raw_attributes: Vec<String>,
derives_for_type: Vec<DeriveForType>,
@@ -211,8 +230,79 @@ fn codegen(
}
let metadata = {
let mut metadata = subxt_metadata::Metadata::decode(&mut &*metadata_bytes)
.map_err(|e| eyre!("Cannot decode the provided metadata: {e}"))?;
let runtime_metadata = subxt_metadata::decode_runtime_metadata(metadata_bytes)?;
let mut metadata = match runtime_metadata {
// Too old to work with:
frame_metadata::RuntimeMetadata::V0(_)
| frame_metadata::RuntimeMetadata::V1(_)
| frame_metadata::RuntimeMetadata::V2(_)
| frame_metadata::RuntimeMetadata::V3(_)
| frame_metadata::RuntimeMetadata::V4(_)
| frame_metadata::RuntimeMetadata::V5(_)
| frame_metadata::RuntimeMetadata::V6(_)
| frame_metadata::RuntimeMetadata::V7(_) => {
Err(eyre!("Metadata V1-V7 cannot be decoded from"))
}
// Converting legacy metadatas:
frame_metadata::RuntimeMetadata::V8(md) => {
let legacy_types = legacy_types
.ok_or_else(|| eyre!("--legacy-types needed to load V8 metadata"))?;
let legacy_spec = legacy_spec_version
.ok_or_else(|| eyre!("--legacy-spec-version needed to load V8 metadata"))?;
Metadata::from_v8(&md, &legacy_types.for_spec_version(legacy_spec))
.map_err(|e| eyre!("Cannot load V8 metadata: {e}"))
}
frame_metadata::RuntimeMetadata::V9(md) => {
let legacy_types = legacy_types
.ok_or_else(|| eyre!("--legacy-types needed to load V9 metadata"))?;
let legacy_spec = legacy_spec_version
.ok_or_else(|| eyre!("--legacy-spec-version needed to load V9 metadata"))?;
Metadata::from_v9(&md, &legacy_types.for_spec_version(legacy_spec))
.map_err(|e| eyre!("Cannot load V9 metadata: {e}"))
}
frame_metadata::RuntimeMetadata::V10(md) => {
let legacy_types = legacy_types
.ok_or_else(|| eyre!("--legacy-types needed to load V10 metadata"))?;
let legacy_spec = legacy_spec_version
.ok_or_else(|| eyre!("--legacy-spec-version needed to load V10 metadata"))?;
Metadata::from_v10(&md, &legacy_types.for_spec_version(legacy_spec))
.map_err(|e| eyre!("Cannot load V10 metadata: {e}"))
}
frame_metadata::RuntimeMetadata::V11(md) => {
let legacy_types = legacy_types
.ok_or_else(|| eyre!("--legacy-types needed to load V11 metadata"))?;
let legacy_spec = legacy_spec_version
.ok_or_else(|| eyre!("--legacy-spec-version needed to load V11 metadata"))?;
Metadata::from_v11(&md, &legacy_types.for_spec_version(legacy_spec))
.map_err(|e| eyre!("Cannot load V11 metadata: {e}"))
}
frame_metadata::RuntimeMetadata::V12(md) => {
let legacy_types = legacy_types
.ok_or_else(|| eyre!("--legacy-types needed to load V12 metadata"))?;
let legacy_spec = legacy_spec_version
.ok_or_else(|| eyre!("--legacy-spec-version needed to load V12 metadata"))?;
Metadata::from_v12(&md, &legacy_types.for_spec_version(legacy_spec))
.map_err(|e| eyre!("Cannot load V12 metadata: {e}"))
}
frame_metadata::RuntimeMetadata::V13(md) => {
let legacy_types = legacy_types
.ok_or_else(|| eyre!("--legacy-types needed to load V13 metadata"))?;
let legacy_spec = legacy_spec_version
.ok_or_else(|| eyre!("--legacy-spec-version needed to load V13 metadata"))?;
Metadata::from_v13(&md, &legacy_types.for_spec_version(legacy_spec))
.map_err(|e| eyre!("Cannot load V13 metadata: {e}"))
}
// Converting modern metadatas:
frame_metadata::RuntimeMetadata::V14(md) => {
Metadata::from_v14(md).map_err(|e| eyre!("Cannot load V14 metadata: {e}"))
}
frame_metadata::RuntimeMetadata::V15(md) => {
Metadata::from_v15(md).map_err(|e| eyre!("Cannot load V15 metadata: {e}"))
}
frame_metadata::RuntimeMetadata::V16(md) => {
Metadata::from_v16(md).map_err(|e| eyre!("Cannot load V16 metadata: {e}"))
}
}?;
// Run this first to ensure type paths are unique (which may result in 1,2,3 suffixes being added
// to type paths), so that when we validate derives/substitutions below, they are allowed for such
+2 -2
View File
@@ -45,7 +45,7 @@ jsonrpsee = [
subxt-rpcs = { workspace = true }
frame-decode = { workspace = true, features = ["legacy", "legacy-types"] }
frame-metadata = { workspace = true, features = ["std", "legacy"] }
scale-type-resolver = { workspace = true }
scale-type-resolver = { workspace = true, features = ["scale-info"] }
codec = { workspace = true }
primitive-types = { workspace = true }
scale-info = { workspace = true }
@@ -60,4 +60,4 @@ futures = { workspace = true }
tokio = { workspace = true, features = ["full"] }
scale-value = { workspace = true }
scale-decode = { workspace = true, features = ["derive"] }
hex = { workspace = true }
hex = { workspace = true }
+372 -5
View File
@@ -1,8 +1,8 @@
#![allow(missing_docs)]
use subxt_historic::{Error, OnlineClient, PolkadotConfig};
use subxt_historic::{OnlineClient, PolkadotConfig};
#[tokio::main]
async fn main() -> Result<(), Error> {
async fn main() -> Result<(), Box<dyn core::error::Error + Send + Sync + 'static>> {
// Configuration for the Polkadot relay chain.
let config = PolkadotConfig::new();
@@ -10,7 +10,7 @@ async fn main() -> Result<(), Error> {
let client = OnlineClient::from_url(config, "wss://rpc.polkadot.io").await?;
// Iterate through some randomly selected old blocks to show how to fetch and decode extrinsics.
for block_number in 123456.. {
for block_number in 1234567.. {
println!("=== Block {block_number} ===");
// Point the client at a specific block number. By default this will download and cache
@@ -40,10 +40,26 @@ async fn main() -> Result<(), Error> {
// scale_value::Value type, which can represent any SCALE encoded data, but if you
// have an idea of the type then you can try to decode into that type instead):
for field in extrinsic.call().fields().iter() {
// We can visit fields, which gives us the ability to inspect and decode information
// from them selectively, returning whatever we like from it. Here we demo our
// type name visitor which is defined below:
let tn = field
.visit(type_name::GetTypeName::new())?
.unwrap_or_default();
// When visiting fields we can also decode into a custom shape like so:
let _custom_value = field.visit(value::GetValue::new())?;
// We can also obtain and decode things without the complexity of the above:
println!(
" {}: {}",
" {}: {} {}",
field.name(),
field.decode_as::<scale_value::Value>().unwrap()
field.decode_as::<scale_value::Value>().unwrap(),
if tn.is_empty() {
String::new()
} else {
format!("(type name: {tn})")
},
);
}
@@ -81,3 +97,354 @@ async fn main() -> Result<(), Error> {
Ok(())
}
/// This module defines an example visitor which retrieves the name of a type.
/// This is a more advanced use case and can typically be avoided.
mod type_name {
use scale_decode::{
Visitor,
visitor::types::{Composite, Sequence, Variant},
visitor::{TypeIdFor, Unexpected},
};
use scale_type_resolver::TypeResolver;
/// This is a visitor which obtains type names.
pub struct GetTypeName<R> {
marker: core::marker::PhantomData<R>,
}
impl<R> GetTypeName<R> {
/// Construct our TypeName visitor.
pub fn new() -> Self {
GetTypeName {
marker: core::marker::PhantomData,
}
}
}
impl<R: TypeResolver> Visitor for GetTypeName<R> {
type Value<'scale, 'resolver> = Option<&'resolver str>;
type Error = scale_decode::Error;
type TypeResolver = R;
// Look at the path of types that have paths and return the ident from that.
fn visit_composite<'scale, 'resolver>(
self,
value: &mut Composite<'scale, 'resolver, Self::TypeResolver>,
_type_id: TypeIdFor<Self>,
) -> Result<Self::Value<'scale, 'resolver>, Self::Error> {
Ok(value.path().last())
}
fn visit_variant<'scale, 'resolver>(
self,
value: &mut Variant<'scale, 'resolver, Self::TypeResolver>,
_type_id: TypeIdFor<Self>,
) -> Result<Self::Value<'scale, 'resolver>, Self::Error> {
Ok(value.path().last())
}
fn visit_sequence<'scale, 'resolver>(
self,
value: &mut Sequence<'scale, 'resolver, Self::TypeResolver>,
_type_id: TypeIdFor<Self>,
) -> Result<Self::Value<'scale, 'resolver>, Self::Error> {
Ok(value.path().last())
}
// Else, we return nothing as we can't find a name for the type.
fn visit_unexpected<'scale, 'resolver>(
self,
_unexpected: Unexpected,
) -> Result<Self::Value<'scale, 'resolver>, Self::Error> {
Ok(None)
}
}
}
/// 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 scale_type_resolver::TypeResolver;
use std::collections::HashMap;
/// A value type we're decoding into.
#[allow(dead_code)]
pub enum Value {
Number(f64),
BigNumber(String),
Bool(bool),
Char(char),
Array(Vec<Value>),
String(String),
Address(Vec<u8>),
I256([u8; 32]),
U256([u8; 32]),
Struct(HashMap<String, Value>),
Variant(String, VariantFields),
}
pub enum VariantFields {
Unnamed(Vec<Value>),
Named(HashMap<String, Value>),
}
/// 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),
}
/// This is a visitor which obtains type names.
pub struct GetValue<R> {
marker: core::marker::PhantomData<R>,
}
impl<R> GetValue<R> {
/// Construct our TypeName visitor.
pub fn new() -> Self {
GetValue {
marker: core::marker::PhantomData,
}
}
}
impl<R: TypeResolver> Visitor for GetValue<R> {
type Value<'scale, 'resolver> = Value;
type Error = ValueError;
type TypeResolver = R;
fn visit_i256<'resolver>(
self,
value: &[u8; 32],
_type_id: TypeIdFor<Self>,
) -> Result<Self::Value<'_, 'resolver>, Self::Error> {
Ok(Value::I256(*value))
}
fn visit_u256<'resolver>(
self,
value: &[u8; 32],
_type_id: TypeIdFor<Self>,
) -> Result<Self::Value<'_, 'resolver>, Self::Error> {
Ok(Value::U256(*value))
}
fn visit_i128<'scale, 'resolver>(
self,
value: i128,
_type_id: TypeIdFor<Self>,
) -> Result<Self::Value<'scale, 'resolver>, 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<Self>,
) -> Result<Self::Value<'scale, 'resolver>, Self::Error> {
self.visit_i128(value.into(), type_id)
}
fn visit_i32<'scale, 'resolver>(
self,
value: i32,
type_id: TypeIdFor<Self>,
) -> Result<Self::Value<'scale, 'resolver>, Self::Error> {
self.visit_i128(value.into(), type_id)
}
fn visit_i16<'scale, 'resolver>(
self,
value: i16,
type_id: TypeIdFor<Self>,
) -> Result<Self::Value<'scale, 'resolver>, Self::Error> {
self.visit_i128(value.into(), type_id)
}
fn visit_i8<'scale, 'resolver>(
self,
value: i8,
type_id: TypeIdFor<Self>,
) -> Result<Self::Value<'scale, 'resolver>, Self::Error> {
self.visit_i128(value.into(), type_id)
}
fn visit_u128<'scale, 'resolver>(
self,
value: u128,
_type_id: TypeIdFor<Self>,
) -> Result<Self::Value<'scale, 'resolver>, 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<Self>,
) -> Result<Self::Value<'scale, 'resolver>, Self::Error> {
self.visit_u128(value.into(), type_id)
}
fn visit_u32<'scale, 'resolver>(
self,
value: u32,
type_id: TypeIdFor<Self>,
) -> Result<Self::Value<'scale, 'resolver>, Self::Error> {
self.visit_u128(value.into(), type_id)
}
fn visit_u16<'scale, 'resolver>(
self,
value: u16,
type_id: TypeIdFor<Self>,
) -> Result<Self::Value<'scale, 'resolver>, Self::Error> {
self.visit_u128(value.into(), type_id)
}
fn visit_u8<'scale, 'resolver>(
self,
value: u8,
type_id: TypeIdFor<Self>,
) -> Result<Self::Value<'scale, 'resolver>, Self::Error> {
self.visit_u128(value.into(), type_id)
}
fn visit_bool<'scale, 'resolver>(
self,
value: bool,
_type_id: TypeIdFor<Self>,
) -> Result<Self::Value<'scale, 'resolver>, Self::Error> {
Ok(Value::Bool(value))
}
fn visit_char<'scale, 'resolver>(
self,
value: char,
_type_id: TypeIdFor<Self>,
) -> Result<Self::Value<'scale, 'resolver>, Self::Error> {
Ok(Value::Char(value))
}
fn visit_array<'scale, 'resolver>(
self,
values: &mut Array<'scale, 'resolver, Self::TypeResolver>,
_type_id: TypeIdFor<Self>,
) -> Result<Self::Value<'scale, 'resolver>, Self::Error> {
Ok(Value::Array(to_array(values.remaining(), values)?))
}
fn visit_sequence<'scale, 'resolver>(
self,
values: &mut Sequence<'scale, 'resolver, Self::TypeResolver>,
_type_id: TypeIdFor<Self>,
) -> Result<Self::Value<'scale, 'resolver>, Self::Error> {
Ok(Value::Array(to_array(values.remaining(), values)?))
}
fn visit_str<'scale, 'resolver>(
self,
value: &mut Str<'scale>,
_type_id: TypeIdFor<Self>,
) -> Result<Self::Value<'scale, 'resolver>, 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<Self>,
) -> Result<Self::Value<'scale, 'resolver>, Self::Error> {
Ok(Value::Array(to_array(values.remaining(), values)?))
}
fn visit_bitsequence<'scale, 'resolver>(
self,
value: &mut BitSequence<'scale>,
_type_id: TypeIdFor<Self>,
) -> Result<Self::Value<'scale, 'resolver>, 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<Self>,
) -> Result<Self::Value<'scale, 'resolver>, 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(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<Self>,
) -> Result<Self::Value<'scale, 'resolver>, Self::Error> {
let name = value.name().to_owned();
let fields = to_variant_fieldish(value.fields())?;
Ok(Value::Variant(name, fields))
}
}
fn to_variant_fieldish<'scale, 'resolver, R: TypeResolver>(
value: &mut Composite<'scale, 'resolver, R>,
) -> Result<VariantFields, ValueError> {
// If fields are unnamed, treat as array:
if value.fields().iter().all(|f| f.name.is_none()) {
return Ok(VariantFields::Unnamed(to_array(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())?;
out.insert(name, value);
}
Ok(VariantFields::Named(out))
}
fn to_array<'scale, 'resolver, R: TypeResolver>(
len: usize,
mut values: impl scale_decode::visitor::DecodeItemIterator<'scale, 'resolver, R>,
) -> Result<Vec<Value>, ValueError> {
let mut out = Vec::with_capacity(len);
while let Some(value) = values.decode_item(GetValue::new()) {
out.push(value?);
}
Ok(out)
}
}
+71 -3
View File
@@ -1,8 +1,8 @@
#![allow(missing_docs)]
use subxt_historic::{Error, OnlineClient, PolkadotConfig, ext::StreamExt};
use subxt_historic::{OnlineClient, PolkadotConfig, ext::StreamExt};
#[tokio::main]
async fn main() -> Result<(), Error> {
async fn main() -> Result<(), Box<dyn core::error::Error + Send + Sync + 'static>> {
// Configuration for the Polkadot relay chain.
let config = PolkadotConfig::new();
@@ -36,6 +36,12 @@ async fn main() -> Result<(), Error> {
// represent any SCALE-encoded value, like so:
let _balance_info = entry.decode_as::<scale_value::Value>()?;
// We can visit the value, which is a more advanced use case and allows us to extract more
// data from the type, here the name of it, if it exists:
let tn = entry
.visit(type_name::GetTypeName::new())?
.unwrap_or("<unknown>");
// Or, if we know what shape to expect, we can decode the parts of the value that we care
// about directly into a static type, which is more efficient and allows easy type-safe
// access, like so:
@@ -53,7 +59,7 @@ async fn main() -> Result<(), Error> {
let balance_info = entry.decode_as::<BalanceInfo>()?;
println!(
" Single balance info from {account_id_hex} => free: {} reserved: {} misc_frozen: {} fee_frozen: {}",
" Single balance info from {account_id_hex} => free: {} reserved: {} misc_frozen: {} fee_frozen: {} (type name: {tn})",
balance_info.data.free,
balance_info.data.reserved,
balance_info.data.misc_frozen,
@@ -105,3 +111,65 @@ async fn main() -> Result<(), Error> {
Ok(())
}
/// This module defines an example visitor which retrieves the name of a type.
/// This is a more advanced use case and can typically be avoided.
mod type_name {
use scale_decode::{
Visitor,
visitor::types::{Composite, Sequence, Variant},
visitor::{TypeIdFor, Unexpected},
};
use scale_type_resolver::TypeResolver;
/// This is a visitor which obtains type names.
pub struct GetTypeName<R> {
marker: core::marker::PhantomData<R>,
}
impl<R> GetTypeName<R> {
/// Construct our TypeName visitor.
pub fn new() -> Self {
GetTypeName {
marker: core::marker::PhantomData,
}
}
}
impl<R: TypeResolver> Visitor for GetTypeName<R> {
type Value<'scale, 'resolver> = Option<&'resolver str>;
type Error = scale_decode::Error;
type TypeResolver = R;
// Look at the path of types that have paths and return the ident from that.
fn visit_composite<'scale, 'resolver>(
self,
value: &mut Composite<'scale, 'resolver, Self::TypeResolver>,
_type_id: TypeIdFor<Self>,
) -> Result<Self::Value<'scale, 'resolver>, Self::Error> {
Ok(value.path().last())
}
fn visit_variant<'scale, 'resolver>(
self,
value: &mut Variant<'scale, 'resolver, Self::TypeResolver>,
_type_id: TypeIdFor<Self>,
) -> Result<Self::Value<'scale, 'resolver>, Self::Error> {
Ok(value.path().last())
}
fn visit_sequence<'scale, 'resolver>(
self,
value: &mut Sequence<'scale, 'resolver, Self::TypeResolver>,
_type_id: TypeIdFor<Self>,
) -> Result<Self::Value<'scale, 'resolver>, Self::Error> {
Ok(value.path().last())
}
// Else, we return nothing as we can't find a name for the type.
fn visit_unexpected<'scale, 'resolver>(
self,
_unexpected: Unexpected,
) -> Result<Self::Value<'scale, 'resolver>, Self::Error> {
Ok(None)
}
}
}
+34 -4
View File
@@ -1,6 +1,7 @@
use super::extrinsic_info::{AnyExtrinsicInfo, with_info};
use crate::error::ExtrinsicCallError;
use crate::utils::Either;
use crate::utils::{AnyResolver, AnyTypeId};
use scale_info_legacy::{LookupName, TypeRegistrySet};
/// This represents the call data in the extrinsic.
@@ -53,6 +54,7 @@ impl<'extrinsics, 'atblock> ExtrinsicCall<'extrinsics, 'atblock> {
pub struct ExtrinsicCallFields<'extrinsics, 'atblock> {
all_bytes: &'extrinsics [u8],
info: &'extrinsics AnyExtrinsicInfo<'atblock>,
resolver: AnyResolver<'atblock>,
}
impl<'extrinsics, 'atblock> ExtrinsicCallFields<'extrinsics, 'atblock> {
@@ -60,7 +62,16 @@ impl<'extrinsics, 'atblock> ExtrinsicCallFields<'extrinsics, 'atblock> {
all_bytes: &'extrinsics [u8],
info: &'extrinsics AnyExtrinsicInfo<'atblock>,
) -> Self {
Self { all_bytes, info }
let resolver = match info {
AnyExtrinsicInfo::Legacy(info) => AnyResolver::B(info.resolver),
AnyExtrinsicInfo::Current(info) => AnyResolver::A(info.resolver),
};
Self {
all_bytes,
info,
resolver,
}
}
/// Return the bytes representing the fields stored in this extrinsic.
@@ -74,11 +85,12 @@ impl<'extrinsics, 'atblock> ExtrinsicCallFields<'extrinsics, 'atblock> {
}
/// Iterate over each of the fields of the extrinsic call data.
pub fn iter(&self) -> impl Iterator<Item = ExtrinsicCallField<'extrinsics, 'atblock>> {
pub fn iter(&self) -> impl Iterator<Item = ExtrinsicCallField<'_, 'extrinsics, 'atblock>> {
match &self.info {
AnyExtrinsicInfo::Legacy(info) => {
Either::A(info.info.call_data().map(|named_arg| ExtrinsicCallField {
field_bytes: &self.all_bytes[named_arg.range()],
resolver: &self.resolver,
info: AnyExtrinsicCallFieldInfo::Legacy(ExtrinsicCallFieldInfo {
info: named_arg,
resolver: info.resolver,
@@ -88,6 +100,7 @@ impl<'extrinsics, 'atblock> ExtrinsicCallFields<'extrinsics, 'atblock> {
AnyExtrinsicInfo::Current(info) => {
Either::B(info.info.call_data().map(|named_arg| ExtrinsicCallField {
field_bytes: &self.all_bytes[named_arg.range()],
resolver: &self.resolver,
info: AnyExtrinsicCallFieldInfo::Current(ExtrinsicCallFieldInfo {
info: named_arg,
resolver: info.resolver,
@@ -119,9 +132,10 @@ impl<'extrinsics, 'atblock> ExtrinsicCallFields<'extrinsics, 'atblock> {
}
}
pub struct ExtrinsicCallField<'extrinsics, 'atblock> {
pub struct ExtrinsicCallField<'fields, 'extrinsics, 'atblock> {
field_bytes: &'extrinsics [u8],
info: AnyExtrinsicCallFieldInfo<'extrinsics, 'atblock>,
resolver: &'fields AnyResolver<'atblock>,
}
enum AnyExtrinsicCallFieldInfo<'extrinsics, 'atblock> {
@@ -144,7 +158,7 @@ macro_rules! with_call_field_info {
};
}
impl<'extrinsics, 'atblock> ExtrinsicCallField<'extrinsics, 'atblock> {
impl<'fields, 'extrinsics, 'atblock> ExtrinsicCallField<'fields, 'extrinsics, 'atblock> {
/// Get the raw bytes for this field.
pub fn bytes(&self) -> &'extrinsics [u8] {
self.field_bytes
@@ -155,6 +169,22 @@ impl<'extrinsics, 'atblock> ExtrinsicCallField<'extrinsics, 'atblock> {
with_call_field_info!(&self.info => info.info.name())
}
/// Visit the given field with a [`scale_decode::visitor::Visitor`]. This is like a lower level
/// version of [`ExtrinsicCallField::decode_as`], as the visitor is able to preserve lifetimes
/// and has access to more type information than is available via [`ExtrinsicCallField::decode_as`].
pub fn visit<V: scale_decode::visitor::Visitor<TypeResolver = AnyResolver<'atblock>>>(
&self,
visitor: V,
) -> Result<V::Value<'extrinsics, 'fields>, V::Error> {
let type_id = match &self.info {
AnyExtrinsicCallFieldInfo::Current(info) => AnyTypeId::A(*info.info.ty()),
AnyExtrinsicCallFieldInfo::Legacy(info) => AnyTypeId::B(info.info.ty().clone()),
};
let cursor = &mut self.bytes();
scale_decode::visitor::decode_with_visitor(cursor, type_id, self.resolver, visitor)
}
/// Attempt to decode the value of this field into the given type.
pub fn decode_as<T: scale_decode::DecodeAsType>(&self) -> Result<T, ExtrinsicCallError> {
with_call_field_info!(&self.info => {
@@ -110,6 +110,3 @@ impl<'extrinsics, 'atblock> Extrinsic<'extrinsics, 'atblock> {
ExtrinsicTransactionParams::new(self.bytes, self.info)
}
}
// TODO add extrinsic.call() with .bytes, and .decode function to make it easy to decode call fields into Value or whatever.
// Then add this to the example. Make sure we can do everything that dot-block-decoder does easily.
+5
View File
@@ -20,3 +20,8 @@ pub use error::Error;
pub mod ext {
pub use futures::stream::{Stream, StreamExt};
}
/// Helper types that could be useful.
pub mod helpers {
pub use crate::utils::{AnyResolver, AnyResolverError, AnyTypeId};
}
+29 -2
View File
@@ -1,6 +1,7 @@
use super::storage_info::AnyStorageInfo;
use super::storage_info::with_info;
use crate::error::StorageValueError;
use crate::utils::{AnyResolver, AnyTypeId};
use scale_decode::DecodeAsType;
use std::borrow::Cow;
use std::sync::Arc;
@@ -9,12 +10,22 @@ use std::sync::Arc;
pub struct StorageValue<'atblock> {
pub(crate) info: Arc<AnyStorageInfo<'atblock>>,
bytes: Cow<'atblock, [u8]>,
resolver: AnyResolver<'atblock>,
}
impl<'atblock> StorageValue<'atblock> {
/// Create a new storage value.
pub fn new(info: Arc<AnyStorageInfo<'atblock>>, bytes: Cow<'atblock, [u8]>) -> Self {
Self { info, bytes }
pub(crate) fn new(info: Arc<AnyStorageInfo<'atblock>>, bytes: Cow<'atblock, [u8]>) -> Self {
let resolver = match &*info {
AnyStorageInfo::Current(info) => AnyResolver::A(info.resolver),
AnyStorageInfo::Legacy(info) => AnyResolver::B(info.resolver),
};
Self {
info,
bytes,
resolver,
}
}
/// Get the raw bytes for this storage value.
@@ -27,6 +38,22 @@ impl<'atblock> StorageValue<'atblock> {
self.bytes.to_vec()
}
/// Visit the given field with a [`scale_decode::visitor::Visitor`]. This is like a lower level
/// version of [`StorageValue::decode_as`], as the visitor is able to preserve lifetimes
/// and has access to more type information than is available via [`StorageValue::decode_as`].
pub fn visit<V: scale_decode::visitor::Visitor<TypeResolver = AnyResolver<'atblock>>>(
&self,
visitor: V,
) -> Result<V::Value<'_, '_>, V::Error> {
let type_id = match &*self.info {
AnyStorageInfo::Current(info) => AnyTypeId::A(info.info.value_id),
AnyStorageInfo::Legacy(info) => AnyTypeId::B(info.info.value_id.clone()),
};
let cursor = &mut self.bytes();
scale_decode::visitor::decode_with_visitor(cursor, type_id, &self.resolver, visitor)
}
/// Decode this storage value.
pub fn decode_as<T: DecodeAsType>(&self) -> Result<T, StorageValueError> {
with_info!(info = &*self.info => {
+2
View File
@@ -1,5 +1,7 @@
mod any_resolver;
mod either;
mod range_map;
pub use any_resolver::{AnyResolver, AnyResolverError, AnyTypeId};
pub use either::Either;
pub use range_map::RangeMap;
+188
View File
@@ -0,0 +1,188 @@
use super::Either;
use scale_info_legacy::LookupName;
use scale_type_resolver::ResolvedTypeVisitor;
/// A type resolver which could either be for modern or historic resolving.
pub type AnyResolver<'resolver> = Either<
&'resolver scale_info::PortableRegistry,
&'resolver scale_info_legacy::TypeRegistrySet<'resolver>,
>;
/// A type ID which is either a modern or historic ID.
pub type AnyTypeId = Either<u32, scale_info_legacy::LookupName>;
impl Default for AnyTypeId {
fn default() -> Self {
// Not a sensible default, but we don't need / can't provide a sensible one.
AnyTypeId::A(u32::MAX)
}
}
impl From<u32> for AnyTypeId {
fn from(value: u32) -> Self {
AnyTypeId::A(value)
}
}
impl From<LookupName> for AnyTypeId {
fn from(value: LookupName) -> Self {
AnyTypeId::B(value)
}
}
impl TryFrom<AnyTypeId> for u32 {
type Error = ();
fn try_from(value: AnyTypeId) -> Result<Self, Self::Error> {
match value {
AnyTypeId::A(v) => Ok(v),
AnyTypeId::B(_) => Err(()),
}
}
}
impl TryFrom<AnyTypeId> for LookupName {
type Error = ();
fn try_from(value: AnyTypeId) -> Result<Self, Self::Error> {
match value {
AnyTypeId::A(_) => Err(()),
AnyTypeId::B(v) => Ok(v),
}
}
}
/// A resolve error that comes from using [`AnyResolver`] to resolve some [`AnyTypeId`] into a type.
#[derive(Debug, thiserror::Error)]
pub enum AnyResolverError {
#[error("got a {got} type ID but expected a {expected} type ID")]
TypeIdMismatch {
got: &'static str,
expected: &'static str,
},
#[error("{0}")]
ScaleInfo(scale_type_resolver::portable_registry::Error),
#[error("{0}")]
ScaleInfoLegacy(scale_info_legacy::type_registry::TypeRegistryResolveError),
}
impl<'resolver> scale_type_resolver::TypeResolver for AnyResolver<'resolver> {
type TypeId = AnyTypeId;
type Error = AnyResolverError;
fn resolve_type<'this, V: ResolvedTypeVisitor<'this, TypeId = Self::TypeId>>(
&'this self,
type_id: Self::TypeId,
visitor: V,
) -> Result<V::Value, Self::Error> {
match (self, type_id) {
(Either::A(resolver), Either::A(id)) => resolver
.resolve_type(id, ModernVisitor(visitor))
.map_err(AnyResolverError::ScaleInfo),
(Either::B(resolver), Either::B(id)) => resolver
.resolve_type(id, LegacyVisitor(visitor))
.map_err(AnyResolverError::ScaleInfoLegacy),
(Either::A(_), Either::B(_)) => Err(AnyResolverError::TypeIdMismatch {
got: "LookupName",
expected: "u32",
}),
(Either::B(_), Either::A(_)) => Err(AnyResolverError::TypeIdMismatch {
got: "u32",
expected: "LookupName",
}),
}
}
}
// We need to have a visitor which understands only modern or legacy types, and can wrap the more generic visitor
// that must be provided to AnyResolver::resolve_type. This then allows us to visit historic _or_ modern types
// using the single visitor provided by the user.
struct LegacyVisitor<V>(V);
struct ModernVisitor<V>(V);
mod impls {
use super::{AnyTypeId, LegacyVisitor, LookupName, ModernVisitor};
use scale_type_resolver::*;
// An ugly implementation which maps from modern or legacy types into our AnyTypeId,
// to make LegacyVisitor and ModernVisitor valid visitors when wrapping a generic "any" visitor.
macro_rules! impl_visitor_mapper {
($struc:ident, $type_id_ty:ident, $variant:ident) => {
impl<'this, V> ResolvedTypeVisitor<'this> for $struc<V>
where
V: ResolvedTypeVisitor<'this, TypeId = AnyTypeId>,
{
type TypeId = $type_id_ty;
type Value = V::Value;
fn visit_unhandled(self, kind: UnhandledKind) -> Self::Value {
self.0.visit_unhandled(kind)
}
fn visit_array(self, type_id: Self::TypeId, len: usize) -> Self::Value {
self.0.visit_array(AnyTypeId::$variant(type_id), len)
}
fn visit_not_found(self) -> Self::Value {
self.0.visit_not_found()
}
fn visit_composite<Path, Fields>(self, path: Path, fields: Fields) -> Self::Value
where
Path: PathIter<'this>,
Fields: FieldIter<'this, Self::TypeId>,
{
self.0.visit_composite(
path,
fields.map(|field| Field {
name: field.name,
id: AnyTypeId::$variant(field.id),
}),
)
}
fn visit_variant<Path, Fields, Var>(self, path: Path, variants: Var) -> Self::Value
where
Path: PathIter<'this>,
Fields: FieldIter<'this, Self::TypeId>,
Var: VariantIter<'this, Fields>,
{
self.0.visit_variant(
path,
variants.map(|variant| Variant {
index: variant.index,
name: variant.name,
fields: variant.fields.map(|field| Field {
name: field.name,
id: AnyTypeId::$variant(field.id),
}),
}),
)
}
fn visit_sequence<Path>(self, path: Path, type_id: Self::TypeId) -> Self::Value
where
Path: PathIter<'this>,
{
self.0.visit_sequence(path, AnyTypeId::$variant(type_id))
}
fn visit_tuple<TypeIds>(self, type_ids: TypeIds) -> Self::Value
where
TypeIds: ExactSizeIterator<Item = Self::TypeId>,
{
self.0
.visit_tuple(type_ids.map(|id| AnyTypeId::$variant(id)))
}
fn visit_primitive(self, primitive: Primitive) -> Self::Value {
self.0.visit_primitive(primitive)
}
fn visit_compact(self, type_id: Self::TypeId) -> Self::Value {
self.0.visit_compact(AnyTypeId::$variant(type_id))
}
fn visit_bit_sequence(
self,
store_format: BitsStoreFormat,
order_format: BitsOrderFormat,
) -> Self::Value {
self.0.visit_bit_sequence(store_format, order_format)
}
}
};
}
impl_visitor_mapper!(ModernVisitor, u32, A);
impl_visitor_mapper!(LegacyVisitor, LookupName, B);
}
+1
View File
@@ -1,5 +1,6 @@
macro_rules! either {
($name:ident( $fst:ident, $($variant:ident),* )) => {
#[derive(Clone, Copy, Debug)]
pub enum $name<$fst, $($variant),*> {
$fst($fst),
$($variant($variant),)*
+57 -2
View File
@@ -374,6 +374,27 @@ impl Metadata {
<Self as codec::Decode>::decode(&mut bytes)
}
/// Convert V16 metadata into [`Metadata`].
pub fn from_v16(
metadata: frame_metadata::v16::RuntimeMetadataV16,
) -> Result<Self, TryFromError> {
metadata.try_into()
}
/// Convert V15 metadata into [`Metadata`].
pub fn from_v15(
metadata: frame_metadata::v15::RuntimeMetadataV15,
) -> Result<Self, TryFromError> {
metadata.try_into()
}
/// Convert V14 metadata into [`Metadata`].
pub fn from_v14(
metadata: frame_metadata::v14::RuntimeMetadataV14,
) -> Result<Self, TryFromError> {
metadata.try_into()
}
/// Convert V13 metadata into [`Metadata`], given the necessary extra type information.
#[cfg(feature = "legacy")]
pub fn from_v13(
@@ -1222,6 +1243,38 @@ impl<'a> CustomValueMetadata<'a> {
}
}
/// Decode SCALE encoded metadata.
///
/// - The default assumption is that metadata is encoded as [`frame_metadata::RuntimeMetadataPrefixed`]. This is the
/// expected format that metadata is encoded into.
/// - if this fails, we also try to decode as [`frame_metadata::RuntimeMetadata`].
/// - If this all fails, we also try to decode as [`frame_metadata::OpaqueMetadata`].
pub fn decode_runtime_metadata(
input: &[u8],
) -> Result<frame_metadata::RuntimeMetadata, codec::Error> {
use codec::Decode;
let err = match frame_metadata::RuntimeMetadataPrefixed::decode(&mut &*input) {
Ok(md) => return Ok(md.1),
Err(e) => e,
};
if let Ok(md) = frame_metadata::RuntimeMetadata::decode(&mut &*input) {
return Ok(md);
}
// frame_metadata::OpaqueMetadata is a vec of bytes. If we can decode the length, AND
// the length definitely corresponds to the number of remaining bytes, then we try to
// decode the inner bytes.
if let Ok(len) = codec::Compact::<u64>::decode(&mut &*input) {
if input.len() == len.0 as usize {
return decode_runtime_metadata(input);
}
}
Err(err)
}
// Support decoding metadata from the "wire" format directly into this.
// Errors may be lost in the case that the metadata content is somehow invalid.
impl codec::Decode for Metadata {
@@ -1231,9 +1284,11 @@ impl codec::Decode for Metadata {
frame_metadata::RuntimeMetadata::V14(md) => md.try_into(),
frame_metadata::RuntimeMetadata::V15(md) => md.try_into(),
frame_metadata::RuntimeMetadata::V16(md) => md.try_into(),
_ => return Err("Cannot try_into() to Metadata: unsupported metadata version".into()),
_ => {
return Err("Metadata::decode failed: Cannot try_into() to Metadata: unsupported metadata version".into())
},
};
metadata.map_err(|_e| "Cannot try_into() to Metadata.".into())
metadata.map_err(|_| "Metadata::decode failed: Cannot try_into() to Metadata".into())
}
}
@@ -401,7 +401,7 @@ async fn decode_block_mortality() {
}
// Explicit Mortal:
for for_n_blocks in [4, 16, 128] {
for for_n_blocks in [16, 64, 128] {
let tx = submit_extrinsic_and_get_it_back(
&api,
DefaultExtrinsicParamsBuilder::new().mortal(for_n_blocks),