Merge v0.50.x to master (#2127)

* v0.50.0: Integrate frame-decode, redo storage APIs and break up Error. (#2100)

* WIP integrating new frame-decode and working out new storage APIS

* WIP: first pass adding new storage things to subxt-core

* Second pass over Address type and start impl in Subxt

* WIP new storage APIs

* WIP New storage APIs roughly completed, lots of errors still

* Remove PlainorMap enum; plain and map values now use same struct to simplify usage

* Begin 'fixing' errors

* WIP splitting errors and tidying payload/address traits

* Get subxt-core compiling

* Small fixes in subxt-core and remove metadata mod

* subxt-core: cargo check --all-targets passes

* Fix test

* WIP starting to update subxt from subxt-core changes

* WIP splitting up subxt errors into smaller variants

* WIP errors: add DispatchError errors

* Port new Storage APIs to subxt-core

* cargo check -p subxt passes

* Quick-fix errors in subxt-cli (explore subcommand)

* fmt

* Finish fixing codegen up and start fixing examples

* get Subxt examples compiling and bytes_at for constants

* Add some arcs to limit lifetimes in subxt/subxt-core storage APIs

* A little Arcing to allow more method chaining in Storage APIs, aligning with Subxt

* Update codegen test

* cargo check --all-targets passing

* cargo check --features 'unstable-light-client' passing

* clippy

* Remove unused dep in subxt

* use published frame-decode

* fix wasm-example

* Add new tx extension to fix daily tests

* Remove unused subxt_core::dynamic::DecodedValue type

* Update book to match changes

* Update docs to fix more broken bits

* Add missing docs

* fmt

* allow larger result errs for now

* Add missing alloc imports in subxt-core

* Fix doc tests and fix bug getting constant info

* Fix V14 -> Metadata transform for storage & constants

* Fix parachain example

* Fix FFI example

* BlockLength decodes t ostruct, not u128

* use fetch/iter shorthands rather than entry in most storage tests

* Fix some integration tests

* Fix Runtime codegen tests

* Expose the dynamic custom_value selecter and use in a UI test

* Update codegen metadata

* Tidy CLI storage query and support (str,str) as a storage address

* Add (str,str) as valid constant address too

* Show string tuple in constants example

* Via the magic of traits, avoid needing any clones of queries/addresses and accept references to them

* clippy

* [v0.50] update scale-info-legacy and frame-decode to latest (#2119)

* bump scale-info-legacy and frame-decode to latest

* Remove something we don't need in this PR

* Fully remove unused for now dep

* [v0.50] Convert historic metadata to subxt::Metadata (#2120)

* First pass converting historic metadatas to our subxt::Metadata type

* use published frame-decode

* fmt and rename legacy metadata macro

* Enable legacy feature where needed in subxt_metadata so it compiles on its own

* Use cargo hack more in CI and fix subxt-metadata features

* Add tests for metadata conversion (need to optimise; some too expensive right now

* Address performance and equality issues in metadata conversion testing

* fmt

* fmt all

* clippy

* Fix a doc link

* Test codegen and fixes to make it work

* Remove local frame-decode patch

* bump frame-decode to latest

* [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

* Prep to release v0.0.5 (#2126)
This commit is contained in:
James Wilson
2025-11-22 10:44:03 +00:00
committed by GitHub
parent 586b814ecd
commit 8203679cbd
158 changed files with 13736 additions and 16451 deletions
+113 -225
View File
@@ -1,3 +1,4 @@
mod list_storage_entries_any;
mod storage_entry;
mod storage_info;
mod storage_key;
@@ -5,16 +6,17 @@ mod storage_value;
use crate::client::{OfflineClientAtBlockT, OnlineClientAtBlockT};
use crate::config::Config;
use crate::error::{StorageEntryIsNotAMap, StorageEntryIsNotAPlainValue, StorageError};
use crate::error::StorageError;
use crate::storage::storage_info::with_info;
use std::borrow::Cow;
use std::sync::Arc;
use storage_info::AnyStorageInfo;
pub use storage_entry::StorageEntry;
pub use storage_key::{StorageHasher, StorageKey, StorageKeyPart};
pub use storage_value::StorageValue;
// We take how storage keys can be passed in from `frame-decode`, so re-export here.
pub use frame_decode::storage::{IntoStorageKeys, StorageKeys};
pub use frame_decode::storage::{EncodableValues, IntoEncodableValues};
/// Work with storage.
pub struct StorageClient<'atblock, Client, T> {
@@ -33,44 +35,34 @@ impl<'atblock, Client, T> StorageClient<'atblock, Client, T> {
}
// Things that we can do offline with storage.
impl<'atblock, 'client: 'atblock, Client, T> StorageClient<'atblock, Client, T>
impl<'atblock, Client, T> StorageClient<'atblock, Client, T>
where
T: Config + 'client,
Client: OfflineClientAtBlockT<'client, T>,
T: Config + 'atblock,
Client: OfflineClientAtBlockT<'atblock, T>,
{
/// Select the storage entry you'd like to work with.
pub fn entry(
&self,
pallet_name: impl Into<String>,
storage_name: impl Into<String>,
entry_name: impl Into<String>,
) -> Result<StorageEntryClient<'atblock, Client, T>, StorageError> {
let pallet_name = pallet_name.into();
let storage_name = storage_name.into();
let entry_name = entry_name.into();
let storage_info = AnyStorageInfo::new(
&pallet_name,
&storage_name,
&entry_name,
self.client.metadata(),
self.client.legacy_types(),
)?;
if storage_info.is_map() {
Ok(StorageEntryClient::Map(StorageEntryMapClient {
client: self.client,
pallet_name,
storage_name,
info: storage_info,
marker: std::marker::PhantomData,
}))
} else {
Ok(StorageEntryClient::Plain(StorageEntryPlainClient {
client: self.client,
pallet_name,
storage_name,
info: storage_info,
marker: std::marker::PhantomData,
}))
}
Ok(StorageEntryClient {
client: self.client,
pallet_name,
entry_name,
info: Arc::new(storage_info),
marker: std::marker::PhantomData,
})
}
/// Iterate over all of the storage entries listed in the metadata for the current block. This does **not** include well known
@@ -78,34 +70,50 @@ where
pub fn entries(&self) -> impl Iterator<Item = StorageEntriesItem<'atblock, Client, T>> {
let client = self.client;
let metadata = client.metadata();
frame_decode::helpers::list_storage_entries_any(metadata).map(|entry| StorageEntriesItem {
entry,
client: self.client,
marker: std::marker::PhantomData,
let mut pallet_name = Cow::Borrowed("");
list_storage_entries_any::list_storage_entries_any(metadata).filter_map(move |entry| {
match entry {
frame_decode::storage::StorageEntry::In(name) => {
// Set the pallet name for upcoming entries:
pallet_name = name;
None
}
frame_decode::storage::StorageEntry::Name(entry_name) => {
// Output each entry with the last seen pallet name:
Some(StorageEntriesItem {
pallet_name: pallet_name.clone(),
entry_name,
client: self.client,
marker: std::marker::PhantomData,
})
}
}
})
}
}
/// Working with a specific storage entry.
pub struct StorageEntriesItem<'atblock, Client, T> {
entry: frame_decode::helpers::StorageEntry<'atblock>,
pallet_name: Cow<'atblock, str>,
entry_name: Cow<'atblock, str>,
client: &'atblock Client,
marker: std::marker::PhantomData<T>,
}
impl<'atblock, 'client: 'atblock, Client, T> StorageEntriesItem<'atblock, Client, T>
impl<'atblock, Client, T> StorageEntriesItem<'atblock, Client, T>
where
T: Config + 'client,
Client: OfflineClientAtBlockT<'client, T>,
T: Config + 'atblock,
Client: OfflineClientAtBlockT<'atblock, T>,
{
/// The pallet name.
pub fn pallet_name(&self) -> &str {
self.entry.pallet()
&self.pallet_name
}
/// The storage entry name.
pub fn storage_name(&self) -> &str {
self.entry.entry()
pub fn entry_name(&self) -> &str {
&self.entry_name
}
/// Extract the relevant storage information so that we can work with this entry.
@@ -114,88 +122,20 @@ where
client: self.client,
marker: std::marker::PhantomData,
}
.entry(
self.entry.pallet().to_owned(),
self.entry.entry().to_owned(),
)
.entry(&*self.pallet_name, &*self.entry_name)
}
}
/// A client for working with a specific storage entry. This is an enum because the storage entry
/// might be either a map or a plain value, and each has a different interface.
pub enum StorageEntryClient<'atblock, Client, T> {
Plain(StorageEntryPlainClient<'atblock, Client, T>),
Map(StorageEntryMapClient<'atblock, Client, T>),
/// A client for working with a specific storage entry.
pub struct StorageEntryClient<'atblock, Client, T> {
client: &'atblock Client,
pallet_name: String,
entry_name: String,
info: Arc<AnyStorageInfo<'atblock>>,
marker: std::marker::PhantomData<T>,
}
impl<'atblock, Client, T> StorageEntryClient<'atblock, Client, T>
where
T: Config + 'atblock,
Client: OfflineClientAtBlockT<'atblock, T>,
{
/// Get the pallet name.
pub fn pallet_name(&self) -> &str {
match self {
StorageEntryClient::Plain(client) => &client.pallet_name,
StorageEntryClient::Map(client) => &client.pallet_name,
}
}
/// Get the storage entry name.
pub fn storage_name(&self) -> &str {
match self {
StorageEntryClient::Plain(client) => &client.storage_name,
StorageEntryClient::Map(client) => &client.storage_name,
}
}
/// Is the storage entry a plain value?
pub fn is_plain(&self) -> bool {
matches!(self, StorageEntryClient::Plain(_))
}
/// Is the storage entry a map?
pub fn is_map(&self) -> bool {
matches!(self, StorageEntryClient::Map(_))
}
/// If this storage entry is a plain value, return the client for working with it. Else return an error.
pub fn into_plain(
self,
) -> Result<StorageEntryPlainClient<'atblock, Client, T>, StorageEntryIsNotAPlainValue> {
match self {
StorageEntryClient::Plain(client) => Ok(client),
StorageEntryClient::Map(_) => Err(StorageEntryIsNotAPlainValue {
pallet_name: self.pallet_name().into(),
storage_name: self.storage_name().into(),
}),
}
}
/// If this storage entry is a map, return the client for working with it. Else return an error.
pub fn into_map(
self,
) -> Result<StorageEntryMapClient<'atblock, Client, T>, StorageEntryIsNotAMap> {
match self {
StorageEntryClient::Plain(_) => Err(StorageEntryIsNotAMap {
pallet_name: self.pallet_name().into(),
storage_name: self.storage_name().into(),
}),
StorageEntryClient::Map(client) => Ok(client),
}
}
}
/// A client for working with a plain storage entry.
pub struct StorageEntryPlainClient<'atblock, Client, T> {
client: &'atblock Client,
pallet_name: String,
storage_name: String,
info: AnyStorageInfo<'atblock>,
marker: std::marker::PhantomData<T>,
}
impl<'atblock, Client, T> StorageEntryPlainClient<'atblock, Client, T>
where
T: Config + 'atblock,
Client: OfflineClientAtBlockT<'atblock, T>,
@@ -206,134 +146,72 @@ where
}
/// Get the storage entry name.
pub fn storage_name(&self) -> &str {
&self.storage_name
pub fn entry_name(&self) -> &str {
&self.entry_name
}
/// Return the default value for this storage entry, if there is one. Returns `None` if there
/// is no default value.
pub fn default(&self) -> Option<StorageValue<'_, 'atblock>> {
with_info!(info = &self.info => {
info.info.default_value.as_ref().map(|default_value| {
StorageValue::new(&self.info, default_value.clone())
})
})
}
}
impl<'atblock, Client, T> StorageEntryPlainClient<'atblock, Client, T>
where
T: Config + 'atblock,
Client: OnlineClientAtBlockT<'atblock, T>,
{
/// Fetch the value for this storage entry.
pub async fn fetch(&self) -> Result<Option<StorageValue<'_, 'atblock>>, StorageError> {
let key_bytes = self.key();
fetch(self.client, &key_bytes)
.await
.map(|v| v.map(|bytes| StorageValue::new(&self.info, Cow::Owned(bytes))))
}
/// Fetch the value for this storage entry as per [`StorageEntryPlainClient::fetch`], but return the default
/// value for the storage entry if one exists and the entry does not exist.
pub async fn fetch_or_default(
&self,
) -> Result<Option<StorageValue<'_, 'atblock>>, StorageError> {
self.fetch()
.await
.map(|option_val| option_val.or_else(|| self.default()))
}
/// The key for this storage entry.
pub fn key(&self) -> [u8; 32] {
/// The key which points to this storage entry (but not necessarily any values within it).
pub fn key_prefix(&self) -> [u8; 32] {
let pallet_name = &*self.pallet_name;
let storage_name = &*self.storage_name;
let entry_name = &*self.entry_name;
frame_decode::storage::encode_prefix(pallet_name, storage_name)
}
}
/// A client for working with a storage entry that is a map.
pub struct StorageEntryMapClient<'atblock, Client, T> {
client: &'atblock Client,
pallet_name: String,
storage_name: String,
info: AnyStorageInfo<'atblock>,
marker: std::marker::PhantomData<T>,
}
impl<'atblock, Client, T> StorageEntryMapClient<'atblock, Client, T>
where
T: Config + 'atblock,
Client: OfflineClientAtBlockT<'atblock, T>,
{
/// Get the pallet name.
pub fn pallet_name(&self) -> &str {
&self.pallet_name
}
/// Get the storage entry name.
pub fn storage_name(&self) -> &str {
&self.storage_name
frame_decode::storage::encode_storage_key_prefix(pallet_name, entry_name)
}
/// Return the default value for this storage entry, if there is one. Returns `None` if there
/// is no default value.
pub fn default(&self) -> Option<StorageValue<'_, 'atblock>> {
with_info!(info = &self.info => {
pub fn default_value(&self) -> Option<StorageValue<'atblock>> {
with_info!(info = &*self.info => {
info.info.default_value.as_ref().map(|default_value| {
StorageValue::new(&self.info, default_value.clone())
StorageValue::new(self.info.clone(), default_value.clone())
})
})
}
}
impl<'atblock, Client, T> StorageEntryMapClient<'atblock, Client, T>
impl<'atblock, Client, T> StorageEntryClient<'atblock, Client, T>
where
T: Config + 'atblock,
Client: OnlineClientAtBlockT<'atblock, T>,
{
/// Fetch a specific key in this map. If the number of keys provided is not equal
/// to the number of keys required to fetch a single value from the map, then an error
/// will be emitted.
pub async fn fetch<Keys: IntoStorageKeys>(
/// will be emitted. If no value exists but there is a default value for this storage
/// entry, then the default value will be returned. Else, `None` will be returned.
pub async fn fetch<Keys: IntoEncodableValues>(
&self,
keys: Keys,
) -> Result<Option<StorageValue<'_, 'atblock>>, StorageError> {
let expected_num_keys = with_info!(info = &self.info => {
) -> Result<Option<StorageValue<'atblock>>, StorageError> {
let expected_num_keys = with_info!(info = &*self.info => {
info.info.keys.len()
});
if expected_num_keys != keys.num_keys() {
return Err(StorageError::WrongNumberOfKeysProvided {
num_keys_provided: keys.num_keys(),
// For fetching, we need exactly as many keys as exist for a storage entry.
if expected_num_keys != keys.num_encodable_values() {
return Err(StorageError::WrongNumberOfKeysProvidedForFetch {
num_keys_provided: keys.num_encodable_values(),
num_keys_expected: expected_num_keys,
});
}
let key_bytes = self.key(keys)?;
fetch(self.client, &key_bytes)
.await
.map(|v| v.map(|bytes| StorageValue::new(&self.info, Cow::Owned(bytes))))
}
let info = self.info.clone();
let value = fetch(self.client, &key_bytes)
.await?
.map(|bytes| StorageValue::new(info, Cow::Owned(bytes)))
.or_else(|| self.default_value());
/// Fetch a specific key in this map as per [`StorageEntryMapClient::fetch`], but return the default
/// value for the storage entry if one exists and the entry was not found.
pub async fn fetch_or_default<Keys: IntoStorageKeys>(
&self,
keys: Keys,
) -> Result<Option<StorageValue<'_, 'atblock>>, StorageError> {
self.fetch(keys)
.await
.map(|option_val| option_val.or_else(|| self.default()))
Ok(value)
}
/// Iterate over the values underneath the provided keys.
pub async fn iter<Keys: IntoStorageKeys>(
pub async fn iter<Keys: IntoEncodableValues>(
&self,
keys: Keys,
) -> Result<
impl futures::Stream<Item = Result<StorageEntry<'_, 'atblock>, StorageError>> + Unpin,
impl futures::Stream<Item = Result<StorageEntry<'atblock>, StorageError>>
+ Unpin
+ use<'atblock, Client, T, Keys>,
StorageError,
> {
use futures::stream::StreamExt;
@@ -341,6 +219,19 @@ where
ArchiveStorageEvent, StorageQuery, StorageQueryType,
};
let expected_num_keys = with_info!(info = &*self.info => {
info.info.keys.len()
});
// For iterating, we need at most one less key than the number that exists for a storage entry.
// TODO: The error message will be confusing if == keys are provided!
if keys.num_encodable_values() >= expected_num_keys {
return Err(StorageError::TooManyKeysProvidedForIter {
num_keys_provided: keys.num_encodable_values(),
max_keys_expected: expected_num_keys - 1,
});
}
let block_hash = self.client.block_hash();
let key_bytes = self.key(keys)?;
@@ -356,23 +247,22 @@ where
.await
.map_err(|e| StorageError::RpcError { reason: e })?;
let sub = sub.filter_map(async |item| {
let item = match item {
Ok(ArchiveStorageEvent::Item(item)) => item,
Ok(ArchiveStorageEvent::Error(err)) => {
return Some(Err(StorageError::StorageEventError { reason: err.error }));
}
Ok(ArchiveStorageEvent::Done) => return None,
Err(e) => return Some(Err(StorageError::RpcError { reason: e })),
};
let info = self.info.clone();
let sub = sub.filter_map(move |item| {
let info = info.clone();
async move {
let item = match item {
Ok(ArchiveStorageEvent::Item(item)) => item,
Ok(ArchiveStorageEvent::Error(err)) => {
return Some(Err(StorageError::StorageEventError { reason: err.error }));
}
Ok(ArchiveStorageEvent::Done) => return None,
Err(e) => return Some(Err(StorageError::RpcError { reason: e })),
};
item.value.map(|value| {
Ok(StorageEntry::new(
&self.info,
item.key.0,
Cow::Owned(value.0),
))
})
item.value
.map(|value| Ok(StorageEntry::new(info, item.key.0, Cow::Owned(value.0))))
}
});
Ok(Box::pin(sub))
@@ -384,16 +274,14 @@ where
// Dev note: We don't have any functions that can take an already-encoded key and fetch an entry from
// it yet, so we don't expose this. If we did expose it, we might want to return some struct that wraps
// the key bytes and some metadata about them. Or maybe just fetch_raw and iter_raw.
fn key<Keys: IntoStorageKeys>(&self, keys: Keys) -> Result<Vec<u8>, StorageError> {
with_info!(info = &self.info => {
let mut key_bytes = Vec::new();
frame_decode::storage::encode_storage_key_with_info_to(
fn key<Keys: IntoEncodableValues>(&self, keys: Keys) -> Result<Vec<u8>, StorageError> {
with_info!(info = &*self.info => {
let key_bytes = frame_decode::storage::encode_storage_key_with_info(
&self.pallet_name,
&self.storage_name,
&self.entry_name,
keys,
&info.info,
info.resolver,
&mut key_bytes,
).map_err(|e| StorageError::KeyEncodeError { reason: e })?;
Ok(key_bytes)
})