Add subxt-historic crate for accesing historic (non head-of-chain) blocks (#2040)

* WIP subxt-historic

* WIP subxt-historic

* WIP subxt-historic; flesh out basic foundations

* WIP filling in extrinsic decoding functionality

* iter and decode transaction extensions

* Fill in the Online/OfflineClient APIs and move more things to be part of the chain Config

* WIP storage

* clippy, fmt, finish extrinsics example

* prep for 0.0.1 release to claim crate name

* fix README link

* fmt

* WIP thinking about storage APIs

* WIP working out storage APIs

* Storage plain value fetching first pass

* WIP storage: first pass iterating over values done

* First apss finishing storage APIs

* fmt and clippy

* Create a storage example showing fetch and iteration

* Bump to frame-decode 0.9.0

* Bump subxt-historic to 0.0.3 for preview release

* Remove unused deps

* fix import

* clippy

* doc fixes

* tweak CI and fix some cargo hack findings

* Update README: subxt-historic is prerelease
This commit is contained in:
James Wilson
2025-08-26 17:56:21 +01:00
committed by GitHub
parent 23f3ebe6b5
commit 3aabd6dc09
33 changed files with 3220 additions and 55 deletions
+178
View File
@@ -0,0 +1,178 @@
use super::extrinsic_info::{AnyExtrinsicInfo, with_info};
use crate::error::ExtrinsicCallError;
use crate::utils::Either;
use scale_info_legacy::{LookupName, TypeRegistrySet};
/// This represents the call data in the extrinsic.
pub struct ExtrinsicCall<'extrinsics, 'atblock> {
all_bytes: &'extrinsics [u8],
info: &'extrinsics AnyExtrinsicInfo<'atblock>,
}
impl<'extrinsics, 'atblock> ExtrinsicCall<'extrinsics, 'atblock> {
pub(crate) fn new(
all_bytes: &'extrinsics [u8],
info: &'extrinsics AnyExtrinsicInfo<'atblock>,
) -> Self {
Self { all_bytes, info }
}
/// The index of the pallet that this call is for
pub fn pallet_index(&self) -> u8 {
with_info!(&self.info => info.info.pallet_index())
}
/// The name of the pallet that this call is for.
pub fn pallet_name(&self) -> &str {
with_info!(&self.info => info.info.pallet_name())
}
/// The index of this call.
pub fn index(&self) -> u8 {
with_info!(&self.info => info.info.call_index())
}
/// The name of this call.
pub fn name(&self) -> &str {
with_info!(&self.info => info.info.call_name())
}
/// Get the raw bytes for the entire call, which includes the pallet and call index
/// bytes as well as the encoded arguments for each of the fields.
pub fn bytes(&self) -> &'extrinsics [u8] {
with_info!(&self.info => &self.all_bytes[info.info.call_data_range()])
}
/// Work with the fields in this call.
pub fn fields(&self) -> ExtrinsicCallFields<'extrinsics, 'atblock> {
ExtrinsicCallFields::new(self.all_bytes, self.info)
}
}
/// This represents the fields of the call.
pub struct ExtrinsicCallFields<'extrinsics, 'atblock> {
all_bytes: &'extrinsics [u8],
info: &'extrinsics AnyExtrinsicInfo<'atblock>,
}
impl<'extrinsics, 'atblock> ExtrinsicCallFields<'extrinsics, 'atblock> {
pub(crate) fn new(
all_bytes: &'extrinsics [u8],
info: &'extrinsics AnyExtrinsicInfo<'atblock>,
) -> Self {
Self { all_bytes, info }
}
/// Return the bytes representing the fields stored in this extrinsic.
///
/// # Note
///
/// This is a subset of [`ExtrinsicCall::bytes`] that does not include the
/// first two bytes that denote the pallet index and the variant index.
pub fn bytes(&self) -> &'extrinsics [u8] {
with_info!(&self.info => &self.all_bytes[info.info.call_data_args_range()])
}
/// Iterate over each of the fields of the extrinsic call data.
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()],
info: AnyExtrinsicCallFieldInfo::Legacy(ExtrinsicCallFieldInfo {
info: named_arg,
resolver: info.resolver,
}),
}))
}
AnyExtrinsicInfo::Current(info) => {
Either::B(info.info.call_data().map(|named_arg| ExtrinsicCallField {
field_bytes: &self.all_bytes[named_arg.range()],
info: AnyExtrinsicCallFieldInfo::Current(ExtrinsicCallFieldInfo {
info: named_arg,
resolver: info.resolver,
}),
}))
}
}
}
/// Attempt to decode the fields into the given type.
pub fn decode<T: scale_decode::DecodeAsFields>(&self) -> Result<T, ExtrinsicCallError> {
with_info!(&self.info => {
let cursor = &mut self.bytes();
let mut fields = &mut info.info.call_data().map(|named_arg| {
scale_decode::Field::new(named_arg.ty().clone(), Some(named_arg.name()))
});
let decoded = T::decode_as_fields(cursor, &mut fields, info.resolver)
.map_err(|e| ExtrinsicCallError::FieldsDecodeError { reason: e })?;
if !cursor.is_empty() {
return Err(ExtrinsicCallError::FieldsLeftoverBytes {
leftover_bytes: cursor.to_vec(),
})
}
Ok(decoded)
})
}
}
pub struct ExtrinsicCallField<'extrinsics, 'atblock> {
field_bytes: &'extrinsics [u8],
info: AnyExtrinsicCallFieldInfo<'extrinsics, 'atblock>,
}
enum AnyExtrinsicCallFieldInfo<'extrinsics, 'atblock> {
Legacy(ExtrinsicCallFieldInfo<'extrinsics, 'atblock, LookupName, TypeRegistrySet<'atblock>>),
Current(ExtrinsicCallFieldInfo<'extrinsics, 'atblock, u32, scale_info::PortableRegistry>),
}
struct ExtrinsicCallFieldInfo<'extrinsics, 'atblock, TypeId, Resolver> {
info: &'extrinsics frame_decode::extrinsics::NamedArg<'atblock, TypeId>,
resolver: &'atblock Resolver,
}
macro_rules! with_call_field_info {
(&$self:ident.$info:ident => $fn:expr) => {
#[allow(clippy::clone_on_copy)]
match &$self.$info {
AnyExtrinsicCallFieldInfo::Legacy($info) => $fn,
AnyExtrinsicCallFieldInfo::Current($info) => $fn,
}
};
}
impl<'extrinsics, 'atblock> ExtrinsicCallField<'extrinsics, 'atblock> {
/// Get the raw bytes for this field.
pub fn bytes(&self) -> &'extrinsics [u8] {
self.field_bytes
}
/// Get the name of this field.
pub fn name(&self) -> &'extrinsics str {
with_call_field_info!(&self.info => info.info.name())
}
/// Attempt to decode the value of this field into the given type.
pub fn decode<T: scale_decode::DecodeAsType>(&self) -> Result<T, ExtrinsicCallError> {
with_call_field_info!(&self.info => {
let cursor = &mut &*self.field_bytes;
let decoded = T::decode_as_type(cursor, info.info.ty().clone(), info.resolver)
.map_err(|e| ExtrinsicCallError::FieldDecodeError {
name: info.info.name().to_string(),
reason: e,
})?;
if !cursor.is_empty() {
return Err(ExtrinsicCallError::FieldLeftoverBytes {
name: info.info.name().to_string(),
leftover_bytes: cursor.to_vec(),
});
}
Ok(decoded)
})
}
}