Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ 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.1", 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.0", default-features = false }
Expand Down Expand Up @@ -192,3 +192,6 @@ opt-level = 2
opt-level = 2
[profile.test.package.smoldot]
opt-level = 2

[patch.crates-io]
scale-decode = { path = "../scale-decode/scale-decode" }
2 changes: 1 addition & 1 deletion historic/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
108 changes: 103 additions & 5 deletions historic/examples/extrinsics.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
#![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();

// Create an online client for the Polkadot relay chain, pointed at a Polkadot archive node.
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
Expand Down Expand Up @@ -40,10 +40,25 @@ 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 = if let Some(tn) = field.visit(type_name::GetTypeName::new())? {
tn
} else {
"".into()
};

// 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})")
},
);
}

Expand Down Expand Up @@ -81,3 +96,86 @@ 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::{DecodeAsTypeResult, TypeIdFor, Unexpected},
};
use scale_info_legacy::LookupName;
use scale_type_resolver::TypeResolver;
use std::borrow::Cow;

/// 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> Visitor for GetTypeName<R>
where
R: TypeResolver,
R::TypeId: TryInto<LookupName>,
{
type Value<'scale, 'resolver> = Option<Cow<'resolver, str>>;
type Error = scale_decode::Error;
type TypeResolver = R;

// If we can convert the Type ID into `LookupName` then we return that type name
fn unchecked_decode_as_type<'scale, 'resolver>(
self,
_input: &mut &'scale [u8],
type_id: TypeIdFor<Self>,
_types: &'resolver Self::TypeResolver,
) -> DecodeAsTypeResult<Self, Result<Self::Value<'scale, 'resolver>, Self::Error>> {
let Some(id) = type_id.try_into().ok() else {
return DecodeAsTypeResult::Skipped(self);
};
DecodeAsTypeResult::Decoded(Ok(Some(Cow::Owned(id.to_string()))))
}
Comment thread
jsdw marked this conversation as resolved.
Outdated

// Else, we look at the path of types that have paths and return the ident from that.
// For modern metadatas this is all that we have to go on, but equally the path information
// is a lot better in modern metadata than what we'd get from historic metadata.
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().map(Cow::Borrowed))
}
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().map(Cow::Borrowed))
}
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().map(Cow::Borrowed))
}

// 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)
}
}
}
56 changes: 45 additions & 11 deletions historic/src/extrinsics/extrinsic_call.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
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.
pub struct ExtrinsicCall<'extrinsics, 'atblock> {
pub struct ExtrinsicCall<'extrinsic, 'extrinsics, 'atblock> {
all_bytes: &'extrinsics [u8],
info: &'extrinsics AnyExtrinsicInfo<'atblock>,
resolver: &'extrinsic AnyResolver<'atblock>,
}

impl<'extrinsics, 'atblock> ExtrinsicCall<'extrinsics, 'atblock> {
impl<'extrinsic, 'extrinsics, 'atblock> ExtrinsicCall<'extrinsic, 'extrinsics, 'atblock> {
pub(crate) fn new(
all_bytes: &'extrinsics [u8],
info: &'extrinsics AnyExtrinsicInfo<'atblock>,
resolver: &'extrinsic AnyResolver<'atblock>,
) -> Self {
Self { all_bytes, info }
Self {
all_bytes,
info,
resolver,
}
}

/// The index of the pallet that this call is for
Expand Down Expand Up @@ -44,23 +51,29 @@ impl<'extrinsics, 'atblock> ExtrinsicCall<'extrinsics, 'atblock> {
}

/// Work with the fields in this call.
pub fn fields(&self) -> ExtrinsicCallFields<'extrinsics, 'atblock> {
ExtrinsicCallFields::new(self.all_bytes, self.info)
pub fn fields(&self) -> ExtrinsicCallFields<'extrinsic, 'extrinsics, 'atblock> {
ExtrinsicCallFields::new(self.all_bytes, self.info, self.resolver)
}
}

/// This represents the fields of the call.
pub struct ExtrinsicCallFields<'extrinsics, 'atblock> {
pub struct ExtrinsicCallFields<'extrinsic, 'extrinsics, 'atblock> {
all_bytes: &'extrinsics [u8],
info: &'extrinsics AnyExtrinsicInfo<'atblock>,
resolver: &'extrinsic AnyResolver<'atblock>,
}

impl<'extrinsics, 'atblock> ExtrinsicCallFields<'extrinsics, 'atblock> {
impl<'extrinsic, 'extrinsics, 'atblock> ExtrinsicCallFields<'extrinsic, 'extrinsics, 'atblock> {
pub(crate) fn new(
all_bytes: &'extrinsics [u8],
info: &'extrinsics AnyExtrinsicInfo<'atblock>,
resolver: &'extrinsic AnyResolver<'atblock>,
) -> Self {
Self { all_bytes, info }
Self {
all_bytes,
info,
resolver,
}
}

/// Return the bytes representing the fields stored in this extrinsic.
Expand All @@ -74,11 +87,14 @@ 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<'extrinsic, '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,
Expand All @@ -88,6 +104,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,
Expand Down Expand Up @@ -119,9 +136,10 @@ impl<'extrinsics, 'atblock> ExtrinsicCallFields<'extrinsics, 'atblock> {
}
}

pub struct ExtrinsicCallField<'extrinsics, 'atblock> {
pub struct ExtrinsicCallField<'extrinsic, 'extrinsics, 'atblock> {
field_bytes: &'extrinsics [u8],
info: AnyExtrinsicCallFieldInfo<'extrinsics, 'atblock>,
resolver: &'extrinsic AnyResolver<'atblock>,
}

enum AnyExtrinsicCallFieldInfo<'extrinsics, 'atblock> {
Expand All @@ -144,7 +162,7 @@ macro_rules! with_call_field_info {
};
}

impl<'extrinsics, 'atblock> ExtrinsicCallField<'extrinsics, 'atblock> {
impl<'extrinsic, 'extrinsics, 'atblock> ExtrinsicCallField<'extrinsic, 'extrinsics, 'atblock> {
/// Get the raw bytes for this field.
pub fn bytes(&self) -> &'extrinsics [u8] {
self.field_bytes
Expand All @@ -155,6 +173,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, 'extrinsic>, 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 => {
Expand Down
19 changes: 16 additions & 3 deletions historic/src/extrinsics/extrinsics_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use super::extrinsic_transaction_extensions::ExtrinsicTransactionParams;
use crate::client::OfflineClientAtBlockT;
use crate::config::Config;
use crate::error::ExtrinsicsError;
use crate::utils::AnyResolver;

/// This represents some extrinsics in a block, and carries everything that we need to decode information out of them.
pub struct Extrinsics<'atblock> {
Expand Down Expand Up @@ -49,7 +50,18 @@ impl<'atblock> Extrinsics<'atblock> {
.iter()
.zip(self.infos.iter())
.enumerate()
.map(|(idx, (bytes, info))| Extrinsic { idx, bytes, info })
.map(|(idx, (bytes, info))| {
let resolver = match info {
AnyExtrinsicInfo::Legacy(info) => AnyResolver::B(&info.resolver),
AnyExtrinsicInfo::Current(info) => AnyResolver::A(&info.resolver),
};
Extrinsic {
idx,
bytes,
info,
resolver,
}
})
}
}

Expand All @@ -58,6 +70,7 @@ pub struct Extrinsic<'extrinsics, 'atblock> {
idx: usize,
bytes: &'extrinsics [u8],
info: &'extrinsics AnyExtrinsicInfo<'atblock>,
resolver: AnyResolver<'atblock>,
}

impl<'extrinsics, 'atblock> Extrinsic<'extrinsics, 'atblock> {
Expand All @@ -77,8 +90,8 @@ impl<'extrinsics, 'atblock> Extrinsic<'extrinsics, 'atblock> {
}

/// Return information about the call that this extrinsic is making.
pub fn call(&self) -> ExtrinsicCall<'extrinsics, 'atblock> {
ExtrinsicCall::new(self.bytes, self.info)
pub fn call(&self) -> ExtrinsicCall<'_, 'extrinsics, 'atblock> {
ExtrinsicCall::new(self.bytes, self.info, &self.resolver)
}

/// Return only the bytes of the address that signed this extrinsic.
Expand Down
5 changes: 5 additions & 0 deletions historic/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
}
2 changes: 2 additions & 0 deletions historic/src/utils.rs
Original file line number Diff line number Diff line change
@@ -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;
Loading
Loading