Merge branch 'master' into jsdw-subxt-new

This commit is contained in:
James Wilson
2025-12-09 17:00:42 +00:00
16 changed files with 330 additions and 208 deletions
Generated
+1 -1
View File
@@ -5739,7 +5739,7 @@ dependencies = [
[[package]] [[package]]
name = "subxt-historic" name = "subxt-historic"
version = "0.0.7" version = "0.0.8"
dependencies = [ dependencies = [
"frame-decode", "frame-decode",
"frame-metadata 23.0.0", "frame-metadata 23.0.0",
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
+4
View File
@@ -4,6 +4,10 @@ This is separate from the Subxt changelog as subxt-historic is currently releasa
Eventually this project will merge with Subxt and no longer exist, but until then it's being maintained and updated where needed. Eventually this project will merge with Subxt and no longer exist, but until then it's being maintained and updated where needed.
## 0.0.8 (2025-12-04)
Expose `ClientAtBlock::resolver()`. This hands back a type resolver which is capable of resolving type IDs given by the `.visit()` methods on extrinsic fields and storage values. The extrinsics example has been modified to show how this can be used.
## 0.0.7 (2025-12-03) ## 0.0.7 (2025-12-03)
Expose `OfflineClientAtBlock`, `OfflineClientAtBlockT`, `OnlinelientAtBlock`, `OnlineClientAtBlockT`. Expose `OfflineClientAtBlock`, `OfflineClientAtBlockT`, `OnlinelientAtBlock`, `OnlineClientAtBlockT`.
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "subxt-historic" name = "subxt-historic"
version = "0.0.7" version = "0.0.8"
authors.workspace = true authors.workspace = true
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
+67 -22
View File
@@ -48,7 +48,8 @@ async fn main() -> Result<(), Box<dyn core::error::Error + Send + Sync + 'static
.unwrap_or_default(); .unwrap_or_default();
// When visiting fields we can also decode into a custom shape like so: // When visiting fields we can also decode into a custom shape like so:
let _custom_value = field.visit(value::GetValue::new())?; let _custom_value =
field.visit(value::GetValue::new(&client_at_block.resolver()))?;
// We can also obtain and decode things without the complexity of the above: // We can also obtain and decode things without the complexity of the above:
println!( println!(
@@ -183,7 +184,8 @@ mod value {
I256([u8; 32]), I256([u8; 32]),
U256([u8; 32]), U256([u8; 32]),
Struct(HashMap<String, Value>), Struct(HashMap<String, Value>),
Variant(String, VariantFields), VariantWithoutData(String),
VariantWithData(String, VariantFields),
} }
pub enum VariantFields { pub enum VariantFields {
@@ -198,23 +200,23 @@ mod value {
Decode(#[from] scale_decode::visitor::DecodeError), Decode(#[from] scale_decode::visitor::DecodeError),
#[error("Cannot decode bit sequence: {0}")] #[error("Cannot decode bit sequence: {0}")]
CannotDecodeBitSequence(codec::Error), CannotDecodeBitSequence(codec::Error),
#[error("Cannot resolve variant type information: {0}")]
CannotResolveVariantType(String),
} }
/// This is a visitor which obtains type names. /// This is a visitor which obtains type names.
pub struct GetValue<R> { pub struct GetValue<'r, R> {
marker: core::marker::PhantomData<R>, resolver: &'r R,
} }
impl<R> GetValue<R> { impl<'r, R> GetValue<'r, R> {
/// Construct our TypeName visitor. /// Construct our TypeName visitor.
pub fn new() -> Self { pub fn new(resolver: &'r R) -> Self {
GetValue { GetValue { resolver }
marker: core::marker::PhantomData,
}
} }
} }
impl<R: TypeResolver> Visitor for GetValue<R> { impl<'r, R: TypeResolver> Visitor for GetValue<'r, R> {
type Value<'scale, 'resolver> = Value; type Value<'scale, 'resolver> = Value;
type Error = ValueError; type Error = ValueError;
type TypeResolver = R; type TypeResolver = R;
@@ -346,7 +348,11 @@ mod value {
values: &mut Array<'scale, 'resolver, Self::TypeResolver>, values: &mut Array<'scale, 'resolver, Self::TypeResolver>,
_type_id: TypeIdFor<Self>, _type_id: TypeIdFor<Self>,
) -> Result<Self::Value<'scale, 'resolver>, Self::Error> { ) -> Result<Self::Value<'scale, 'resolver>, Self::Error> {
Ok(Value::Array(to_array(values.remaining(), values)?)) Ok(Value::Array(to_array(
self.resolver,
values.remaining(),
values,
)?))
} }
fn visit_sequence<'scale, 'resolver>( fn visit_sequence<'scale, 'resolver>(
@@ -354,7 +360,11 @@ mod value {
values: &mut Sequence<'scale, 'resolver, Self::TypeResolver>, values: &mut Sequence<'scale, 'resolver, Self::TypeResolver>,
_type_id: TypeIdFor<Self>, _type_id: TypeIdFor<Self>,
) -> Result<Self::Value<'scale, 'resolver>, Self::Error> { ) -> Result<Self::Value<'scale, 'resolver>, Self::Error> {
Ok(Value::Array(to_array(values.remaining(), values)?)) Ok(Value::Array(to_array(
self.resolver,
values.remaining(),
values,
)?))
} }
fn visit_str<'scale, 'resolver>( fn visit_str<'scale, 'resolver>(
@@ -370,7 +380,11 @@ mod value {
values: &mut Tuple<'scale, 'resolver, Self::TypeResolver>, values: &mut Tuple<'scale, 'resolver, Self::TypeResolver>,
_type_id: TypeIdFor<Self>, _type_id: TypeIdFor<Self>,
) -> Result<Self::Value<'scale, 'resolver>, Self::Error> { ) -> Result<Self::Value<'scale, 'resolver>, Self::Error> {
Ok(Value::Array(to_array(values.remaining(), values)?)) Ok(Value::Array(to_array(
self.resolver,
values.remaining(),
values,
)?))
} }
fn visit_bitsequence<'scale, 'resolver>( fn visit_bitsequence<'scale, 'resolver>(
@@ -401,7 +415,7 @@ mod value {
} }
// Reuse logic for decoding variant fields: // Reuse logic for decoding variant fields:
match to_variant_fieldish(value)? { match to_variant_fieldish(self.resolver, value)? {
VariantFields::Named(s) => Ok(Value::Struct(s)), VariantFields::Named(s) => Ok(Value::Struct(s)),
VariantFields::Unnamed(a) => Ok(Value::Array(a)), VariantFields::Unnamed(a) => Ok(Value::Array(a)),
} }
@@ -410,20 +424,50 @@ mod value {
fn visit_variant<'scale, 'resolver>( fn visit_variant<'scale, 'resolver>(
self, self,
value: &mut Variant<'scale, 'resolver, Self::TypeResolver>, value: &mut Variant<'scale, 'resolver, Self::TypeResolver>,
_type_id: TypeIdFor<Self>, type_id: TypeIdFor<Self>,
) -> Result<Self::Value<'scale, 'resolver>, Self::Error> { ) -> Result<Self::Value<'scale, 'resolver>, 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(); let name = value.name().to_owned();
let fields = to_variant_fieldish(value.fields())?;
Ok(Value::Variant(name, fields)) // 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<'scale, 'resolver, R: TypeResolver>( fn to_variant_fieldish<'r, 'scale, 'resolver, R: TypeResolver>(
resolver: &'r R,
value: &mut Composite<'scale, 'resolver, R>, value: &mut Composite<'scale, 'resolver, R>,
) -> Result<VariantFields, ValueError> { ) -> Result<VariantFields, ValueError> {
// If fields are unnamed, treat as array: // If fields are unnamed, treat as array:
if value.fields().iter().all(|f| f.name.is_none()) { if value.fields().iter().all(|f| f.name.is_none()) {
return Ok(VariantFields::Unnamed(to_array(value.remaining(), value)?)); return Ok(VariantFields::Unnamed(to_array(
resolver,
value.remaining(),
value,
)?));
} }
// Otherwise object: // Otherwise object:
@@ -431,18 +475,19 @@ mod value {
for field in value { for field in value {
let field = field?; let field = field?;
let name = field.name().unwrap().to_string(); let name = field.name().unwrap().to_string();
let value = field.decode_with_visitor(GetValue::new())?; let value = field.decode_with_visitor(GetValue::new(resolver))?;
out.insert(name, value); out.insert(name, value);
} }
Ok(VariantFields::Named(out)) Ok(VariantFields::Named(out))
} }
fn to_array<'scale, 'resolver, R: TypeResolver>( fn to_array<'r, 'scale, 'resolver, R: TypeResolver>(
resolver: &'r R,
len: usize, len: usize,
mut values: impl scale_decode::visitor::DecodeItemIterator<'scale, 'resolver, R>, mut values: impl scale_decode::visitor::DecodeItemIterator<'scale, 'resolver, R>,
) -> Result<Vec<Value>, ValueError> { ) -> Result<Vec<Value>, ValueError> {
let mut out = Vec::with_capacity(len); let mut out = Vec::with_capacity(len);
while let Some(value) = values.decode_item(GetValue::new()) { while let Some(value) = values.decode_item(GetValue::new(resolver)) {
out.push(value?); out.push(value?);
} }
Ok(out) Ok(out)
+25
View File
@@ -4,6 +4,7 @@ mod online_client;
use crate::config::Config; use crate::config::Config;
use crate::extrinsics::ExtrinsicsClient; use crate::extrinsics::ExtrinsicsClient;
use crate::storage::StorageClient; use crate::storage::StorageClient;
use crate::utils::AnyResolver;
use frame_metadata::RuntimeMetadata; use frame_metadata::RuntimeMetadata;
use std::marker::PhantomData; use std::marker::PhantomData;
@@ -45,4 +46,28 @@ where
pub fn metadata(&self) -> &RuntimeMetadata { pub fn metadata(&self) -> &RuntimeMetadata {
self.client.metadata() self.client.metadata()
} }
/// Return something which implements [`scale_type_resolver::TypeResolver`] and
/// can be used in conjnction with type IDs in `.visit` methods.
pub fn resolver(&self) -> AnyResolver<'_, 'client> {
match self.client.metadata() {
RuntimeMetadata::V0(_)
| RuntimeMetadata::V1(_)
| RuntimeMetadata::V2(_)
| RuntimeMetadata::V3(_)
| RuntimeMetadata::V4(_)
| RuntimeMetadata::V5(_)
| RuntimeMetadata::V6(_)
| RuntimeMetadata::V7(_)
| RuntimeMetadata::V8(_)
| RuntimeMetadata::V9(_)
| RuntimeMetadata::V10(_)
| RuntimeMetadata::V11(_)
| RuntimeMetadata::V12(_)
| RuntimeMetadata::V13(_) => AnyResolver::B(self.client.legacy_types()),
RuntimeMetadata::V14(m) => AnyResolver::A(&m.types),
RuntimeMetadata::V15(m) => AnyResolver::A(&m.types),
RuntimeMetadata::V16(m) => AnyResolver::A(&m.types),
}
}
} }
+5 -3
View File
@@ -54,7 +54,7 @@ impl<'extrinsics, 'atblock> ExtrinsicCall<'extrinsics, 'atblock> {
pub struct ExtrinsicCallFields<'extrinsics, 'atblock> { pub struct ExtrinsicCallFields<'extrinsics, 'atblock> {
all_bytes: &'extrinsics [u8], all_bytes: &'extrinsics [u8],
info: &'extrinsics AnyExtrinsicInfo<'atblock>, info: &'extrinsics AnyExtrinsicInfo<'atblock>,
resolver: AnyResolver<'atblock>, resolver: AnyResolver<'atblock, 'atblock>,
} }
impl<'extrinsics, 'atblock> ExtrinsicCallFields<'extrinsics, 'atblock> { impl<'extrinsics, 'atblock> ExtrinsicCallFields<'extrinsics, 'atblock> {
@@ -135,7 +135,7 @@ impl<'extrinsics, 'atblock> ExtrinsicCallFields<'extrinsics, 'atblock> {
pub struct ExtrinsicCallField<'fields, 'extrinsics, 'atblock> { pub struct ExtrinsicCallField<'fields, 'extrinsics, 'atblock> {
field_bytes: &'extrinsics [u8], field_bytes: &'extrinsics [u8],
info: AnyExtrinsicCallFieldInfo<'extrinsics, 'atblock>, info: AnyExtrinsicCallFieldInfo<'extrinsics, 'atblock>,
resolver: &'fields AnyResolver<'atblock>, resolver: &'fields AnyResolver<'atblock, 'atblock>,
} }
enum AnyExtrinsicCallFieldInfo<'extrinsics, 'atblock> { enum AnyExtrinsicCallFieldInfo<'extrinsics, 'atblock> {
@@ -172,7 +172,9 @@ impl<'fields, 'extrinsics, 'atblock> ExtrinsicCallField<'fields, 'extrinsics, 'a
/// Visit the given field with a [`scale_decode::visitor::Visitor`]. This is like a lower level /// 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 /// 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`]. /// 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>>>( pub fn visit<
V: scale_decode::visitor::Visitor<TypeResolver = AnyResolver<'atblock, 'atblock>>,
>(
&self, &self,
visitor: V, visitor: V,
) -> Result<V::Value<'extrinsics, 'fields>, V::Error> { ) -> Result<V::Value<'extrinsics, 'fields>, V::Error> {
+4 -2
View File
@@ -10,7 +10,7 @@ use std::sync::Arc;
pub struct StorageValue<'atblock> { pub struct StorageValue<'atblock> {
pub(crate) info: Arc<AnyStorageInfo<'atblock>>, pub(crate) info: Arc<AnyStorageInfo<'atblock>>,
bytes: Cow<'atblock, [u8]>, bytes: Cow<'atblock, [u8]>,
resolver: AnyResolver<'atblock>, resolver: AnyResolver<'atblock, 'atblock>,
} }
impl<'atblock> StorageValue<'atblock> { impl<'atblock> StorageValue<'atblock> {
@@ -41,7 +41,9 @@ impl<'atblock> StorageValue<'atblock> {
/// Visit the given field with a [`scale_decode::visitor::Visitor`]. This is like a lower level /// 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 /// 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`]. /// 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>>>( pub fn visit<
V: scale_decode::visitor::Visitor<TypeResolver = AnyResolver<'atblock, 'atblock>>,
>(
&self, &self,
visitor: V, visitor: V,
) -> Result<V::Value<'_, '_>, V::Error> { ) -> Result<V::Value<'_, '_>, V::Error> {
+3 -5
View File
@@ -3,10 +3,8 @@ use scale_info_legacy::LookupName;
use scale_type_resolver::ResolvedTypeVisitor; use scale_type_resolver::ResolvedTypeVisitor;
/// A type resolver which could either be for modern or historic resolving. /// A type resolver which could either be for modern or historic resolving.
pub type AnyResolver<'resolver> = Either< pub type AnyResolver<'a, 'b> =
&'resolver scale_info::PortableRegistry, Either<&'a scale_info::PortableRegistry, &'a scale_info_legacy::TypeRegistrySet<'b>>;
&'resolver scale_info_legacy::TypeRegistrySet<'resolver>,
>;
/// A type ID which is either a modern or historic ID. /// A type ID which is either a modern or historic ID.
pub type AnyTypeId = Either<u32, scale_info_legacy::LookupName>; pub type AnyTypeId = Either<u32, scale_info_legacy::LookupName>;
@@ -60,7 +58,7 @@ pub enum AnyResolverError {
ScaleInfoLegacy(scale_info_legacy::type_registry::TypeRegistryResolveError), ScaleInfoLegacy(scale_info_legacy::type_registry::TypeRegistryResolveError),
} }
impl<'resolver> scale_type_resolver::TypeResolver for AnyResolver<'resolver> { impl<'a, 'b> scale_type_resolver::TypeResolver for AnyResolver<'a, 'b> {
type TypeId = AnyTypeId; type TypeId = AnyTypeId;
type Error = AnyResolverError; type Error = AnyResolverError;
+19 -6
View File
@@ -212,6 +212,22 @@ fn validate_type_path(path: &syn::Path, metadata: &Metadata) {
} }
} }
/// Resolves a path, handling the $OUT_DIR placeholder if present.
/// If $OUT_DIR is present in the path, it's replaced with the actual OUT_DIR environment variable.
/// Otherwise, the path is resolved relative to CARGO_MANIFEST_DIR.
fn resolve_path(path_str: &str) -> std::path::PathBuf {
if path_str.contains("$OUT_DIR") {
let out_dir = std::env::var("OUT_DIR").unwrap_or_else(|_| {
abort_call_site!("$OUT_DIR is used in path but OUT_DIR environment variable is not set")
});
std::path::Path::new(&path_str.replace("$OUT_DIR", &out_dir)).into()
} else {
let root = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".into());
let root_path = std::path::Path::new(&root);
root_path.join(path_str)
}
}
/// Fetches metadata in a blocking manner, from a url or file path. /// Fetches metadata in a blocking manner, from a url or file path.
fn fetch_metadata(args: &RuntimeMetadataArgs) -> Result<subxt_codegen::Metadata, TokenStream> { fn fetch_metadata(args: &RuntimeMetadataArgs) -> Result<subxt_codegen::Metadata, TokenStream> {
// Do we want to fetch unstable metadata? This only works if fetching from a URL. // Do we want to fetch unstable metadata? This only works if fetching from a URL.
@@ -224,9 +240,7 @@ fn fetch_metadata(args: &RuntimeMetadataArgs) -> Result<subxt_codegen::Metadata,
"Only one of 'runtime_metadata_path', 'runtime_metadata_insecure_url' or `runtime_path` must be provided" "Only one of 'runtime_metadata_path', 'runtime_metadata_insecure_url' or `runtime_path` must be provided"
); );
}; };
let root = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".into()); let path = resolve_path(path);
let root_path = std::path::Path::new(&root);
let path = root_path.join(path);
let metadata = wasm_loader::from_wasm_file(&path).map_err(|e| e.into_compile_error())?; let metadata = wasm_loader::from_wasm_file(&path).map_err(|e| e.into_compile_error())?;
return Ok(metadata); return Ok(metadata);
@@ -243,9 +257,8 @@ fn fetch_metadata(args: &RuntimeMetadataArgs) -> Result<subxt_codegen::Metadata,
) )
} }
let root = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".into()); let path = resolve_path(rest_of_path);
let root_path = std::path::Path::new(&root);
let path = root_path.join(rest_of_path);
subxt_utils_fetchmetadata::from_file_blocking(&path) subxt_utils_fetchmetadata::from_file_blocking(&path)
.and_then(|b| subxt_codegen::Metadata::decode(&mut &*b).map_err(Into::into)) .and_then(|b| subxt_codegen::Metadata::decode(&mut &*b).map_err(Into::into))
.map_err(|e| CodegenError::Other(e.to_string()).into_compile_error())? .map_err(|e| CodegenError::Other(e.to_string()).into_compile_error())?
+18
View File
@@ -146,6 +146,15 @@ pub mod ext {
/// mod polkadot {} /// mod polkadot {}
/// ``` /// ```
/// ///
/// You can use the `$OUT_DIR` placeholder in the path to reference metadata generated at build time:
///
/// ```rust,ignore
/// #[subxt::subxt(
/// runtime_metadata_path = "$OUT_DIR/metadata.scale",
/// )]
/// mod polkadot {}
/// ```
///
/// ## Using a WASM runtime via `runtime_path = "..."` /// ## Using a WASM runtime via `runtime_path = "..."`
/// ///
/// This requires the `runtime-wasm-path` feature flag. /// This requires the `runtime-wasm-path` feature flag.
@@ -159,6 +168,15 @@ pub mod ext {
/// mod polkadot {} /// mod polkadot {}
/// ``` /// ```
/// ///
/// You can also use the `$OUT_DIR` placeholder in the path to reference WASM files generated at build time:
///
/// ```rust,ignore
/// #[subxt::subxt(
/// runtime_path = "$OUT_DIR/runtime.wasm",
/// )]
/// mod polkadot {}
/// ```
///
/// ## Connecting to a node to download metadata via `runtime_metadata_insecure_url = "..."` /// ## Connecting to a node to download metadata via `runtime_metadata_insecure_url = "..."`
/// ///
/// This will, at compile time, connect to the JSON-RPC interface for some node at the URL given, /// This will, at compile time, connect to the JSON-RPC interface for some node at the URL given,
@@ -99,12 +99,18 @@ fn interface_docs(should_gen_docs: bool) -> Vec<String> {
#[test] #[test]
fn check_documentation() { fn check_documentation() {
// Inspect metadata recursively and obtain all associated documentation. // Inspect metadata and obtain all associated documentation.
let raw_docs = metadata_docs(); let raw_docs = metadata_docs();
// Obtain documentation from the generated API. // Obtain documentation from the generated API.
let runtime_docs = interface_docs(true); let runtime_docs = interface_docs(true);
for raw in raw_docs.iter() { for raw in raw_docs.iter() {
if raw.contains(|c: char| !c.is_ascii()) {
// Ignore lines containing on-ascii chars; they are encoded currently
// as "\u{nn}" which doesn't match their input which is the raw non-ascii
// char.
continue;
}
assert!( assert!(
runtime_docs.contains(raw), runtime_docs.contains(raw),
"Documentation not present in runtime API: {raw}" "Documentation not present in runtime API: {raw}"
@@ -114,7 +120,7 @@ fn check_documentation() {
#[test] #[test]
fn check_no_documentation() { fn check_no_documentation() {
// Inspect metadata recursively and obtain all associated documentation. // Inspect metadata and obtain all associated documentation.
let raw_docs = metadata_docs(); let raw_docs = metadata_docs();
// Obtain documentation from the generated API. // Obtain documentation from the generated API.
let runtime_docs = interface_docs(false); let runtime_docs = interface_docs(false);
File diff suppressed because one or more lines are too long