[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 authored and GitHub committed 2025-11-21 15:32:32 +00:00
1 parent 1a0e218d32
commit 3bbba1b005
16 files changed
+871 -37

No files matched your search

+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),)*