Rework Subxt API to support offline and dynamic transactions (#593)

* WIP API changes

* debug impls

* Get main crate compiling with first round of changes

* Some tidy up

* Add WithExtrinsicParams, and have SubstrateConfig + PolkadotConfig, not DefaultConfig

* move transaction into extrinsic folder

* Add runtime updates back to OnlineClient

* rework to be 'client first' to fit better with storage + events

* add support for events to Client

* tidy dupe trait bound

* Wire storage into client, but need to remove static reliance

* various tidy up and start stripping codegen to remove bits we dont need now

* First pass updating calls and constants codegen

* WIP storage client updates

* First pass migrated runtime storage over to new format

* pass over codegen to generate StorageAddresses and throw other stuff out

* don't need a Call trait any more

* shuffle things around a bit

* Various proc_macro fixes to get 'cargo check' working

* organise what's exposed from subxt

* Get first example working; balance_transfer_with_params

* get balance_transfer example compiling

* get concurrent_storage_requests.rs example compiling

* get fetch_all_accounts example compiling

* get a bunch more of the examples compiling

* almost get final example working; type mismatch to look into

* wee tweaks

* move StorageAddress to separate file

* pass Defaultable/Iterable info to StorageAddress in codegen

* fix storage validation ne, and partial run through example code

* Remove static iteration and strip a generic param from everything

* fix doc tests in subxt crate

* update test utils and start fixing frame tests

* fix frame staking tests

* fix the rest of the test compile issues, Borrow on storage values

* cargo fmt

* remove extra logging during tests

* Appease clippy and no more need for into_iter on events

* cargo fmt

* fix dryRun tests by waiting for blocks

* wait for blocks instead of sleeping or other test hacks

* cargo fmt

* Fix doc links

* Traitify StorageAddress

* remove out-of-date doc comments

* optimise decoding storage a little

* cleanup tx stuff, trait for TxPayload, remove Err type param and decode at runtime

* clippy fixes

* fix doc links

* fix doc example

* constant address trait for consistency

* fix a typo and remove EncodeWithMetadata stuff

* Put EventDetails behind a proper interface and allow decoding into top level event, too

* fix docs

* tweak StorageAddress docs

* re-export StorageAddress at root for consistency

* fix clippy things

* Add support for dynamic values

* fix double encoding of storage map key after refactor

* clippy fix

* Fixes and add a dynamic usage example (needs new scale_value release)

* bump scale_value version

* cargo fmt

* Tweak event bits

* cargo fmt

* Add a test and bump scale-value to 0.4.0 to support this

* remove unnecessary vec from dynamic example

* Various typo/grammar fixes

Co-authored-by: Alexandru Vasile <60601340+lexnv@users.noreply.github.com>

* Address PR nits

* Undo accidental rename in changelog

* Small PR nits/tidyups

* fix tests; codegen change against latest substrate

* tweak storage address util names

* move error decoding to DecodeError and expose

* impl some basic traits on the extrinsic param builder

Co-authored-by: Alexandru Vasile <60601340+lexnv@users.noreply.github.com>
This commit is contained in:
James Wilson
2022-08-08 11:55:20 +01:00
committed by GitHub
parent 7a09ac6cd7
commit e48f0e3b1d
84 changed files with 23097 additions and 35863 deletions
+43 -80
View File
@@ -22,31 +22,8 @@ use quote::{
};
use scale_info::form::PortableForm;
/// Generate calls from the provided pallet's metadata.
///
/// The function creates a new module named `calls` under the pallet's module.
/// ```ignore
/// pub mod PalletName {
/// pub mod calls {
/// ...
/// }
/// }
/// ```
///
/// The function generates the calls as rust structs that implement the `subxt::Call` trait
/// to uniquely identify the call's identity when creating the extrinsic.
///
/// ```ignore
/// pub struct CallName {
/// pub call_param: type,
/// }
/// impl ::subxt::Call for CallName {
/// ...
/// }
/// ```
///
/// Calls are extracted from the API and wrapped into the generated `TransactionApi` of
/// each module.
/// Generate calls from the provided pallet's metadata. Each call returns a `StaticTxPayload`
/// that can be passed to the subxt client to submit/sign/encode.
///
/// # Arguments
///
@@ -76,35 +53,42 @@ pub fn generate_calls(
let (call_structs, call_fns): (Vec<_>, Vec<_>) = struct_defs
.iter_mut()
.map(|(variant_name, struct_def)| {
let (call_fn_args, call_args): (Vec<_>, Vec<_>) =
match struct_def.fields {
CompositeDefFields::Named(ref named_fields) => {
named_fields
.iter()
.map(|(name, field)| {
let fn_arg_type = &field.type_path;
let call_arg = if field.is_boxed() {
quote! { #name: ::std::boxed::Box::new(#name) }
} else {
quote! { #name }
};
(quote!( #name: #fn_arg_type ), call_arg)
})
.unzip()
}
CompositeDefFields::NoFields => Default::default(),
CompositeDefFields::Unnamed(_) =>
abort_call_site!(
"Call variant for type {} must have all named fields",
call.ty.id()
)
};
let (call_fn_args, call_args): (Vec<_>, Vec<_>) = match struct_def.fields {
CompositeDefFields::Named(ref named_fields) => {
named_fields
.iter()
.map(|(name, field)| {
let fn_arg_type = &field.type_path;
let call_arg = if field.is_boxed() {
quote! { #name: ::std::boxed::Box::new(#name) }
} else {
quote! { #name }
};
(quote!( #name: #fn_arg_type ), call_arg)
})
.unzip()
}
CompositeDefFields::NoFields => Default::default(),
CompositeDefFields::Unnamed(_) => {
abort_call_site!(
"Call variant for type {} must have all named fields",
call.ty.id()
)
}
};
let pallet_name = &pallet.name;
let call_name = &variant_name;
let struct_name = &struct_def.name;
let call_hash = subxt_metadata::get_call_hash(metadata, pallet_name, call_name)
.unwrap_or_else(|_| abort_call_site!("Metadata information for the call {}_{} could not be found", pallet_name, call_name));
let call_hash =
subxt_metadata::get_call_hash(metadata, pallet_name, call_name)
.unwrap_or_else(|_| {
abort_call_site!(
"Metadata information for the call {}_{} could not be found",
pallet_name,
call_name
)
});
let fn_name = format_ident!("{}", variant_name.to_snake_case());
// Propagate the documentation just to `TransactionApi` methods, while
@@ -113,29 +97,19 @@ pub fn generate_calls(
// The call structure's documentation was stripped above.
let call_struct = quote! {
#struct_def
impl ::subxt::Call for #struct_name {
const PALLET: &'static str = #pallet_name;
const FUNCTION: &'static str = #call_name;
}
};
let client_fn = quote! {
#docs
pub fn #fn_name(
&self,
#( #call_fn_args, )*
) -> Result<::subxt::SubmittableExtrinsic<'a, T, X, #struct_name, DispatchError, root_mod::Event>, ::subxt::BasicError> {
let runtime_call_hash = {
let locked_metadata = self.client.metadata();
let metadata = locked_metadata.read();
metadata.call_hash::<#struct_name>()?
};
if runtime_call_hash == [#(#call_hash,)*] {
let call = #struct_name { #( #call_args, )* };
Ok(::subxt::SubmittableExtrinsic::new(self.client, call))
} else {
Err(::subxt::MetadataError::IncompatibleMetadata.into())
}
) -> ::subxt::tx::StaticTxPayload<#struct_name> {
::subxt::tx::StaticTxPayload::new(
#pallet_name,
#call_name,
#struct_name { #( #call_args, )* },
[#(#call_hash,)*]
)
}
};
(call_struct, client_fn)
@@ -155,20 +129,9 @@ pub fn generate_calls(
#( #call_structs )*
pub struct TransactionApi<'a, T: ::subxt::Config, X> {
client: &'a ::subxt::Client<T>,
marker: ::core::marker::PhantomData<X>,
}
impl<'a, T, X> TransactionApi<'a, T, X>
where
T: ::subxt::Config,
X: ::subxt::extrinsic::ExtrinsicParams<T>,
{
pub fn new(client: &'a ::subxt::Client<T>) -> Self {
Self { client, marker: ::core::marker::PhantomData }
}
pub struct TransactionApi;
impl TransactionApi {
#( #call_fns )*
}
}
+8 -19
View File
@@ -63,17 +63,12 @@ pub fn generate_constants(
quote! {
#( #[doc = #docs ] )*
pub fn #fn_name(&self) -> ::core::result::Result<#return_ty, ::subxt::BasicError> {
let locked_metadata = self.client.metadata();
let metadata = locked_metadata.read();
if metadata.constant_hash(#pallet_name, #constant_name)? == [#(#constant_hash,)*] {
let pallet = metadata.pallet(#pallet_name)?;
let constant = pallet.constant(#constant_name)?;
let value = ::subxt::codec::Decode::decode(&mut &constant.value[..])?;
Ok(value)
} else {
Err(::subxt::MetadataError::IncompatibleMetadata.into())
}
pub fn #fn_name(&self) -> ::subxt::constants::StaticConstantAddress<::subxt::metadata::DecodeStaticType<#return_ty>> {
::subxt::constants::StaticConstantAddress::new(
#pallet_name,
#constant_name,
[#(#constant_hash,)*]
)
}
}
});
@@ -82,15 +77,9 @@ pub fn generate_constants(
pub mod constants {
use super::#types_mod_ident;
pub struct ConstantsApi<'a, T: ::subxt::Config> {
client: &'a ::subxt::Client<T>,
}
impl<'a, T: ::subxt::Config> ConstantsApi<'a, T> {
pub fn new(client: &'a ::subxt::Client<T>) -> Self {
Self { client }
}
pub struct ConstantsApi;
impl ConstantsApi {
#(#constant_fns)*
}
}
-189
View File
@@ -1,189 +0,0 @@
// Copyright 2019-2022 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use frame_metadata::v14::RuntimeMetadataV14;
use proc_macro2::TokenStream as TokenStream2;
use proc_macro_error::abort_call_site;
use quote::quote;
use scale_info::{
form::PortableForm,
Field,
TypeDef,
TypeDefPrimitive,
};
/// Different substrate versions will have a different `DispatchError::Module`.
/// The following cases are ordered by versions.
enum ModuleErrorType {
/// Case 1: `DispatchError::Module { index: u8, error: u8 }`
///
/// This is the first supported `DispatchError::Module` format.
NamedField,
/// Case 2: `DispatchError::Module ( sp_runtime::ModuleError { index: u8, error: u8 } )`
///
/// Substrate introduced `sp_runtime::ModuleError`, while keeping the error `u8`.
LegacyError,
/// Case 3: `DispatchError::Module ( sp_runtime::ModuleError { index: u8, error: [u8; 4] } )`
///
/// The substrate error evolved into `[u8; 4]`.
ArrayError,
}
impl quote::ToTokens for ModuleErrorType {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let trait_fn_body = match self {
ModuleErrorType::NamedField => {
quote! {
if let &Self::Module { index, error } = self {
Some(::subxt::ModuleErrorData { pallet_index: index, error: [error, 0, 0, 0] })
} else {
None
}
}
}
ModuleErrorType::LegacyError => {
quote! {
if let Self::Module (module_error) = self {
Some(::subxt::ModuleErrorData { pallet_index: module_error.index, error: [module_error.error, 0, 0, 0] })
} else {
None
}
}
}
ModuleErrorType::ArrayError => {
quote! {
if let Self::Module (module_error) = self {
Some(::subxt::ModuleErrorData { pallet_index: module_error.index, error: module_error.error })
} else {
None
}
}
}
};
tokens.extend(trait_fn_body);
}
}
/// Determine the `ModuleError` type for the `ModuleErrorType::LegacyError` and
/// `ModuleErrorType::ErrorArray` cases.
fn module_error_type(
module_field: &Field<PortableForm>,
metadata: &RuntimeMetadataV14,
) -> ModuleErrorType {
// Fields are named.
if module_field.name().is_some() {
return ModuleErrorType::NamedField
}
// Get the `sp_runtime::ModuleError` structure.
let module_err = metadata
.types
.resolve(module_field.ty().id())
.unwrap_or_else(|| {
abort_call_site!("sp_runtime::ModuleError type expected in metadata")
});
let error_type_def = match module_err.type_def() {
TypeDef::Composite(composite) => composite,
_ => abort_call_site!("sp_runtime::ModuleError type should be a composite type"),
};
// Get the error field from the `sp_runtime::ModuleError` structure.
let error_field = error_type_def
.fields()
.iter()
.find(|field| field.name() == Some(&"error".to_string()))
.unwrap_or_else(|| {
abort_call_site!("sp_runtime::ModuleError expected to contain error field")
});
// Resolve the error type from the metadata.
let error_field_ty = metadata
.types
.resolve(error_field.ty().id())
.unwrap_or_else(|| {
abort_call_site!("sp_runtime::ModuleError::error type expected in metadata")
});
match error_field_ty.type_def() {
// Check for legacy error type.
TypeDef::Primitive(TypeDefPrimitive::U8) => ModuleErrorType::LegacyError,
TypeDef::Array(array) => {
// Check new error type of len 4 and type u8.
if array.len() != 4 {
abort_call_site!("sp_runtime::ModuleError::error array length is not 4");
}
let array_ty = metadata
.types
.resolve(array.type_param().id())
.unwrap_or_else(|| {
abort_call_site!(
"sp_runtime::ModuleError::error array type expected in metadata"
)
});
if let TypeDef::Primitive(TypeDefPrimitive::U8) = array_ty.type_def() {
ModuleErrorType::ArrayError
} else {
abort_call_site!(
"sp_runtime::ModuleError::error array type expected to be u8"
)
}
}
_ => {
abort_call_site!(
"sp_runtime::ModuleError::error array type or primitive expected"
)
}
}
}
/// The aim of this is to implement the `::subxt::HasModuleError` trait for
/// the generated `DispatchError`, so that we can obtain the module error details,
/// if applicable, from it.
pub fn generate_has_module_error_impl(
metadata: &RuntimeMetadataV14,
types_mod_ident: &syn::Ident,
) -> TokenStream2 {
let dispatch_error = metadata
.types
.types()
.iter()
.find(|&ty| ty.ty().path().segments() == ["sp_runtime", "DispatchError"])
.unwrap_or_else(|| {
abort_call_site!("sp_runtime::DispatchError type expected in metadata")
})
.ty()
.type_def();
// Get the `DispatchError::Module` variant (either struct or named fields).
let module_variant = match dispatch_error {
TypeDef::Variant(variant) => {
variant
.variants()
.iter()
.find(|variant| variant.name() == "Module")
.unwrap_or_else(|| {
abort_call_site!("DispatchError::Module variant expected in metadata")
})
}
_ => abort_call_site!("DispatchError expected to contain variant in metadata"),
};
let module_field = module_variant.fields().get(0).unwrap_or_else(|| {
abort_call_site!("DispatchError::Module expected to contain 1 or more fields")
});
let error_type = module_error_type(module_field, metadata);
quote! {
impl ::subxt::HasModuleError for #types_mod_ident::sp_runtime::DispatchError {
fn module_error_data(&self) -> Option<::subxt::ModuleErrorData> {
#error_type
}
}
}
}
+4 -3
View File
@@ -11,6 +11,7 @@ use scale_info::form::PortableForm;
/// Generate events from the provided pallet metadata.
///
/// The function creates a new module named `events` under the pallet's module.
///
/// ```ignore
/// pub mod PalletName {
/// pub mod events {
@@ -19,14 +20,14 @@ use scale_info::form::PortableForm;
/// }
/// ```
///
/// The function generates the events as rust structs that implement the `subxt::Event` trait
/// The function generates the events as rust structs that implement the `subxt::event::StaticEvent` trait
/// to uniquely identify the event's identity when creating the extrinsic.
///
/// ```ignore
/// pub struct EventName {
/// pub event_param: type,
/// }
/// impl ::subxt::Event for EventName {
/// impl ::subxt::events::StaticEvent for EventName {
/// ...
/// }
/// ```
@@ -62,7 +63,7 @@ pub fn generate_events(
quote! {
#struct_def
impl ::subxt::Event for #event_struct {
impl ::subxt::events::StaticEvent for #event_struct {
const PALLET: &'static str = #pallet_name;
const EVENT: &'static str = #event_name;
}
+37 -111
View File
@@ -6,7 +6,6 @@
mod calls;
mod constants;
mod errors;
mod events;
mod storage;
@@ -110,33 +109,33 @@ impl RuntimeGenerator {
let mut type_substitutes = [
(
"bitvec::order::Lsb0",
parse_quote!(::subxt::bitvec::order::Lsb0),
parse_quote!(::subxt::ext::bitvec::order::Lsb0),
),
(
"bitvec::order::Msb0",
parse_quote!(::subxt::bitvec::order::Msb0),
parse_quote!(::subxt::ext::bitvec::order::Msb0),
),
(
"sp_core::crypto::AccountId32",
parse_quote!(::subxt::sp_core::crypto::AccountId32),
parse_quote!(::subxt::ext::sp_core::crypto::AccountId32),
),
(
"primitive_types::H256",
parse_quote!(::subxt::sp_core::H256),
parse_quote!(::subxt::ext::sp_core::H256),
),
(
"sp_runtime::multiaddress::MultiAddress",
parse_quote!(::subxt::sp_runtime::MultiAddress),
parse_quote!(::subxt::ext::sp_runtime::MultiAddress),
),
(
"frame_support::traits::misc::WrapperKeepOpaque",
parse_quote!(::subxt::WrapperKeepOpaque),
parse_quote!(::subxt::utils::WrapperKeepOpaque),
),
// BTreeMap and BTreeSet impose an `Ord` constraint on their key types. This
// can cause an issue with generated code that doesn't impl `Ord` by default.
// Decoding them to Vec by default (KeyedVec is just an alias for Vec with
// suitable type params) avoids these issues.
("BTreeMap", parse_quote!(::subxt::KeyedVec)),
("BTreeMap", parse_quote!(::subxt::utils::KeyedVec)),
("BTreeSet", parse_quote!(::std::vec::Vec)),
]
.iter()
@@ -256,9 +255,6 @@ impl RuntimeGenerator {
})
.collect();
let has_module_error_impl =
errors::generate_has_module_error_impl(&self.metadata, types_mod_ident);
quote! {
#[allow(dead_code, unused_imports, non_camel_case_types)]
pub mod #mod_ident {
@@ -271,128 +267,58 @@ impl RuntimeGenerator {
#( #modules )*
#types_mod
/// The default error type returned when there is a runtime issue.
/// The default error type returned when there is a runtime issue,
/// exposed here for ease of use.
pub type DispatchError = #types_mod_ident::sp_runtime::DispatchError;
// Impl HasModuleError on DispatchError so we can pluck out module error details.
#has_module_error_impl
pub struct RuntimeApi<T: ::subxt::Config, X> {
pub client: ::subxt::Client<T>,
marker: ::core::marker::PhantomData<X>,
pub fn constants() -> ConstantsApi {
ConstantsApi
}
impl<T: ::subxt::Config, X> Clone for RuntimeApi<T, X> {
fn clone(&self) -> Self {
Self { client: self.client.clone(), marker: ::core::marker::PhantomData }
}
pub fn storage() -> StorageApi {
StorageApi
}
impl<T, X> ::core::convert::From<::subxt::Client<T>> for RuntimeApi<T, X>
where
T: ::subxt::Config,
X: ::subxt::extrinsic::ExtrinsicParams<T>
{
fn from(client: ::subxt::Client<T>) -> Self {
Self { client, marker: ::core::marker::PhantomData }
}
pub fn tx() -> TransactionApi {
TransactionApi
}
impl<'a, T, X> RuntimeApi<T, X>
where
T: ::subxt::Config,
X: ::subxt::extrinsic::ExtrinsicParams<T>,
{
pub fn validate_metadata(&'a self) -> Result<(), ::subxt::MetadataError> {
let runtime_metadata_hash = {
let locked_metadata = self.client.metadata();
let metadata = locked_metadata.read();
metadata.metadata_hash(&PALLETS)
};
if runtime_metadata_hash != [ #(#metadata_hash,)* ] {
Err(::subxt::MetadataError::IncompatibleMetadata)
} else {
Ok(())
}
}
pub fn constants(&'a self) -> ConstantsApi<'a, T> {
ConstantsApi { client: &self.client }
}
pub fn storage(&'a self) -> StorageApi<'a, T> {
StorageApi { client: &self.client }
}
pub fn tx(&'a self) -> TransactionApi<'a, T, X> {
TransactionApi { client: &self.client, marker: ::core::marker::PhantomData }
}
pub fn events(&'a self) -> EventsApi<'a, T> {
EventsApi { client: &self.client }
}
}
pub struct EventsApi<'a, T: ::subxt::Config> {
client: &'a ::subxt::Client<T>,
}
impl <'a, T: ::subxt::Config> EventsApi<'a, T> {
pub async fn at(&self, block_hash: T::Hash) -> Result<::subxt::events::Events<T, Event>, ::subxt::BasicError> {
::subxt::events::at::<T, Event>(self.client, block_hash).await
}
pub async fn subscribe(&self) -> Result<::subxt::events::EventSubscription<'a, ::subxt::events::EventSub<T::Header>, T, Event>, ::subxt::BasicError> {
::subxt::events::subscribe::<T, Event>(self.client).await
}
pub async fn subscribe_finalized(&self) -> Result<::subxt::events::EventSubscription<'a, ::subxt::events::FinalizedEventSub<'a, T::Header>, T, Event>, ::subxt::BasicError> {
::subxt::events::subscribe_finalized::<T, Event>(self.client).await
}
}
pub struct ConstantsApi<'a, T: ::subxt::Config> {
client: &'a ::subxt::Client<T>,
}
impl<'a, T: ::subxt::Config> ConstantsApi<'a, T> {
pub struct ConstantsApi;
impl ConstantsApi {
#(
pub fn #pallets_with_constants(&self) -> #pallets_with_constants::constants::ConstantsApi<'a, T> {
#pallets_with_constants::constants::ConstantsApi::new(self.client)
pub fn #pallets_with_constants(&self) -> #pallets_with_constants::constants::ConstantsApi {
#pallets_with_constants::constants::ConstantsApi
}
)*
}
pub struct StorageApi<'a, T: ::subxt::Config> {
client: &'a ::subxt::Client<T>,
}
impl<'a, T> StorageApi<'a, T>
where
T: ::subxt::Config,
{
pub struct StorageApi;
impl StorageApi {
#(
pub fn #pallets_with_storage(&self) -> #pallets_with_storage::storage::StorageApi<'a, T> {
#pallets_with_storage::storage::StorageApi::new(self.client)
pub fn #pallets_with_storage(&self) -> #pallets_with_storage::storage::StorageApi {
#pallets_with_storage::storage::StorageApi
}
)*
}
pub struct TransactionApi<'a, T: ::subxt::Config, X> {
client: &'a ::subxt::Client<T>,
marker: ::core::marker::PhantomData<X>,
}
impl<'a, T, X> TransactionApi<'a, T, X>
where
T: ::subxt::Config,
X: ::subxt::extrinsic::ExtrinsicParams<T>,
{
pub struct TransactionApi;
impl TransactionApi {
#(
pub fn #pallets_with_calls(&self) -> #pallets_with_calls::calls::TransactionApi<'a, T, X> {
#pallets_with_calls::calls::TransactionApi::new(self.client)
pub fn #pallets_with_calls(&self) -> #pallets_with_calls::calls::TransactionApi {
#pallets_with_calls::calls::TransactionApi
}
)*
}
/// check whether the Client you are using is aligned with the statically generated codegen.
pub fn validate_codegen<T: ::subxt::Config, C: ::subxt::client::OfflineClientT<T>>(client: &C) -> Result<(), ::subxt::error::MetadataError> {
let runtime_metadata_hash = client.metadata().metadata_hash(&PALLETS);
if runtime_metadata_hash != [ #(#metadata_hash,)* ] {
Err(::subxt::error::MetadataError::IncompatibleMetadata)
} else {
Ok(())
}
}
}
}
}
+77 -211
View File
@@ -23,32 +23,8 @@ use scale_info::{
TypeDef,
};
/// Generate storage from the provided pallet's metadata.
///
/// The function creates a new module named `storage` under the pallet's module.
///
/// ```ignore
/// pub mod PalletName {
/// pub mod storage {
/// ...
/// }
/// }
/// ```
///
/// The function generates the storage as rust structs that implement the `subxt::StorageEntry`
/// trait to uniquely identify the storage's identity when creating the extrinsic.
///
/// ```ignore
/// pub struct StorageName {
/// pub storage_param: type,
/// }
/// impl ::subxt::StorageEntry for StorageName {
/// ...
/// }
/// ```
///
/// Storages are extracted from the API and wrapped into the generated `StorageApi` of
/// each module.
/// Generate functions which create storage addresses from the provided pallet's metadata.
/// These addresses can be used to access and iterate over storage values.
///
/// # Arguments
///
@@ -68,27 +44,19 @@ pub fn generate_storage(
return quote!()
};
let (storage_structs, storage_fns): (Vec<_>, Vec<_>) = storage
let storage_fns: Vec<_> = storage
.entries
.iter()
.map(|entry| generate_storage_entry_fns(metadata, type_gen, pallet, entry))
.unzip();
.collect();
quote! {
pub mod storage {
use super::#types_mod_ident;
#( #storage_structs )*
pub struct StorageApi<'a, T: ::subxt::Config> {
client: &'a ::subxt::Client<T>,
}
impl<'a, T: ::subxt::Config> StorageApi<'a, T> {
pub fn new(client: &'a ::subxt::Client<T>) -> Self {
Self { client }
}
pub struct StorageApi;
impl StorageApi {
#( #storage_fns )*
}
}
@@ -100,16 +68,9 @@ fn generate_storage_entry_fns(
type_gen: &TypeGenerator,
pallet: &PalletMetadata<PortableForm>,
storage_entry: &StorageEntryMetadata<PortableForm>,
) -> (TokenStream2, TokenStream2) {
let entry_struct_ident = format_ident!("{}", storage_entry.name);
let (fields, entry_struct, constructor, key_impl, should_ref) = match storage_entry.ty
{
StorageEntryType::Plain(_) => {
let entry_struct = quote!( pub struct #entry_struct_ident; );
let constructor = quote!( #entry_struct_ident );
let key_impl = quote!(::subxt::StorageEntryKey::Plain);
(vec![], entry_struct, constructor, key_impl, false)
}
) -> TokenStream2 {
let (fields, key_impl) = match storage_entry.ty {
StorageEntryType::Plain(_) => (vec![], quote!(vec![])),
StorageEntryType::Map {
ref key,
ref hashers,
@@ -129,7 +90,7 @@ fn generate_storage_entry_fns(
StorageHasher::Identity => "Identity",
};
let hasher = format_ident!("{}", hasher);
quote!( ::subxt::StorageHasher::#hasher )
quote!( ::subxt::storage::address::StorageHasher::#hasher )
})
.collect::<Vec<_>>();
match key_ty.type_def() {
@@ -145,60 +106,28 @@ fn generate_storage_entry_fns(
})
.collect::<Vec<_>>();
let field_names = fields.iter().map(|(n, _)| n);
let field_types = fields.iter().map(|(_, t)| {
// If the field type is `::std::vec::Vec<T>` obtain the type parameter and
// surround with slice brackets. Otherwise, utilize the field_type as is.
match t.vec_type_param() {
Some(ty) => quote!([#ty]),
None => quote!(#t),
}
});
// There cannot be a reference without a parameter.
let should_ref = !fields.is_empty();
let (entry_struct, constructor) = if should_ref {
(
quote! {
pub struct #entry_struct_ident <'a>( #( pub &'a #field_types ),* );
},
quote!( #entry_struct_ident( #( #field_names ),* ) ),
)
} else {
(
quote!( pub struct #entry_struct_ident; ),
quote!( #entry_struct_ident ),
)
};
let key_impl = if hashers.len() == fields.len() {
// If the number of hashers matches the number of fields, we're dealing with
// something shaped like a StorageNMap, and each field should be hashed separately
// according to the corresponding hasher.
let keys = hashers
.into_iter()
.enumerate()
.map(|(field_idx, hasher)| {
let index = syn::Index::from(field_idx);
quote!( ::subxt::StorageMapKey::new(&self.#index, #hasher) )
.zip(&fields)
.map(|(hasher, (field_name, _))| {
quote!( ::subxt::storage::address::StorageMapKey::new(#field_name.borrow(), #hasher) )
});
quote! {
::subxt::StorageEntryKey::Map(
vec![ #( #keys ),* ]
)
vec![ #( #keys ),* ]
}
} else if hashers.len() == 1 {
// If there is one hasher, then however many fields we have, we want to hash a
// tuple of them using the one hasher we're told about. This corresponds to a
// StorageMap.
let hasher = hashers.get(0).expect("checked for 1 hasher");
let items = (0..fields.len()).map(|field_idx| {
let index = syn::Index::from(field_idx);
quote!( &self.#index )
});
let items =
fields.iter().map(|(field_name, _)| quote!( #field_name ));
quote! {
::subxt::StorageEntryKey::Map(
vec![ ::subxt::StorageMapKey::new(&(#( #items ),*), #hasher) ]
)
vec![ ::subxt::storage::address::StorageMapKey::new(&(#( #items.borrow() ),*), #hasher) ]
}
} else {
// If we hit this condition, we don't know how to handle the number of hashes vs fields
@@ -210,33 +139,18 @@ fn generate_storage_entry_fns(
)
};
(fields, entry_struct, constructor, key_impl, should_ref)
(fields, key_impl)
}
_ => {
let (lifetime_param, lifetime_ref) = (quote!(<'a>), quote!(&'a));
let ty_path = type_gen.resolve_type_path(key.id(), &[]);
let fields = vec![(format_ident!("_0"), ty_path.clone())];
// `ty_path` can be `std::vec::Vec<T>`. In such cases, the entry struct
// should contain a slice reference.
let ty_slice = match ty_path.vec_type_param() {
Some(ty) => quote!([#ty]),
None => quote!(#ty_path),
};
let entry_struct = quote! {
pub struct #entry_struct_ident #lifetime_param( pub #lifetime_ref #ty_slice );
};
let constructor = quote!( #entry_struct_ident(_0) );
let fields = vec![(format_ident!("_0"), ty_path)];
let hasher = hashers.get(0).unwrap_or_else(|| {
abort_call_site!("No hasher found for single key")
});
let key_impl = quote! {
::subxt::StorageEntryKey::Map(
vec![ ::subxt::StorageMapKey::new(&self.0, #hasher) ]
)
vec![ ::subxt::storage::address::StorageMapKey::new(_0.borrow(), #hasher) ]
};
(fields, entry_struct, constructor, key_impl, true)
(fields, key_impl)
}
}
}
@@ -255,133 +169,85 @@ fn generate_storage_entry_fns(
});
let fn_name = format_ident!("{}", storage_entry.name.to_snake_case());
let fn_name_iter = format_ident!("{}_iter", fn_name);
let storage_entry_ty = match storage_entry.ty {
StorageEntryType::Plain(ref ty) => ty,
StorageEntryType::Map { ref value, .. } => value,
};
let storage_entry_value_ty = type_gen.resolve_type_path(storage_entry_ty.id(), &[]);
let (return_ty, fetch) = match storage_entry.modifier {
StorageEntryModifier::Default => {
(quote!( #storage_entry_value_ty ), quote!(fetch_or_default))
}
StorageEntryModifier::Optional => {
(
quote!( ::core::option::Option<#storage_entry_value_ty> ),
quote!(fetch),
)
}
};
let storage_entry_impl = quote! (
const PALLET: &'static str = #pallet_name;
const STORAGE: &'static str = #storage_name;
type Value = #storage_entry_value_ty;
fn key(&self) -> ::subxt::StorageEntryKey {
#key_impl
}
);
let anon_lifetime = match should_ref {
true => quote!(<'_>),
false => quote!(),
};
let storage_entry_type = quote! {
#entry_struct
impl ::subxt::StorageEntry for #entry_struct_ident #anon_lifetime {
#storage_entry_impl
}
};
let docs = &storage_entry.docs;
let docs_token = quote! { #( #[doc = #docs ] )* };
let lifetime_param = match should_ref {
true => quote!(<'a>),
false => quote!(),
let key_args = fields.iter().map(|(field_name, field_type)| {
// The field type is translated from `std::vec::Vec<T>` to `[T]`. We apply
// AsRef to all types, so this just makes it a little more ergonomic.
//
// TODO [jsdw]: Support mappings like `String -> str` too for better borrow
// ergonomics.
let field_ty = match field_type.vec_type_param() {
Some(ty) => quote!([#ty]),
_ => quote!(#field_type),
};
quote!( #field_name: impl ::std::borrow::Borrow<#field_ty> )
});
let is_map_type = matches!(storage_entry.ty, StorageEntryType::Map { .. });
// Is the entry iterable?
let is_iterable_type = if is_map_type {
quote!(::subxt::storage::address::Yes)
} else {
quote!(())
};
let client_iter_fn = if matches!(storage_entry.ty, StorageEntryType::Map { .. }) {
let has_default_value = match storage_entry.modifier {
StorageEntryModifier::Default => true,
StorageEntryModifier::Optional => false,
};
// Does the entry have a default value?
let is_defaultable_type = if has_default_value {
quote!(::subxt::storage::address::Yes)
} else {
quote!(())
};
// If the item is a map, we want a way to access the root entry to do things like iterate over it,
// so expose a function to create this entry, too:
let root_entry_fn = if is_map_type {
let fn_name_root = format_ident!("{}_root", fn_name);
quote! (
#docs_token
pub fn #fn_name_iter(
pub fn #fn_name_root(
&self,
block_hash: ::core::option::Option<T::Hash>,
) -> impl ::core::future::Future<
Output = ::core::result::Result<::subxt::KeyIter<'a, T, #entry_struct_ident #lifetime_param>, ::subxt::BasicError>
> + 'a {
// Instead of an async fn which borrows all of self,
// we make sure that the returned future only borrows
// client, which allows you to chain calls a little better.
let client = self.client;
async move {
let runtime_storage_hash = {
let locked_metadata = client.metadata();
let metadata = locked_metadata.read();
match metadata.storage_hash::<#entry_struct_ident>() {
Ok(hash) => hash,
Err(e) => return Err(e.into())
}
};
if runtime_storage_hash == [#(#storage_hash,)*] {
client.storage().iter(block_hash).await
} else {
Err(::subxt::MetadataError::IncompatibleMetadata.into())
}
}
) -> ::subxt::storage::address::StaticStorageAddress::<::subxt::metadata::DecodeStaticType<#storage_entry_value_ty>, (), #is_defaultable_type, #is_iterable_type> {
::subxt::storage::address::StaticStorageAddress::new(
#pallet_name,
#storage_name,
Vec::new(),
[#(#storage_hash,)*]
)
}
)
} else {
quote!()
};
let key_args_ref = match should_ref {
true => quote!(&'a),
false => quote!(),
};
let key_args = fields.iter().map(|(field_name, field_type)| {
// The field type is translated from `std::vec::Vec<T>` to `[T]`, if the
// interface should generate a reference. In such cases, the vector ultimately is
// a slice.
let field_ty = match field_type.vec_type_param() {
Some(ty) if should_ref => quote!([#ty]),
_ => quote!(#field_type),
};
quote!( #field_name: #key_args_ref #field_ty )
});
let client_fns = quote! {
quote! {
// Access a specific value from a storage entry
#docs_token
pub fn #fn_name(
&self,
#( #key_args, )*
block_hash: ::core::option::Option<T::Hash>,
) -> impl ::core::future::Future<
Output = ::core::result::Result<#return_ty, ::subxt::BasicError>
> + 'a {
// Instead of an async fn which borrows all of self,
// we make sure that the returned future only borrows
// client, which allows you to chain calls a little better.
let client = self.client;
async move {
let runtime_storage_hash = {
let locked_metadata = client.metadata();
let metadata = locked_metadata.read();
match metadata.storage_hash::<#entry_struct_ident>() {
Ok(hash) => hash,
Err(e) => return Err(e.into())
}
};
if runtime_storage_hash == [#(#storage_hash,)*] {
let entry = #constructor;
client.storage().#fetch(&entry, block_hash).await
} else {
Err(::subxt::MetadataError::IncompatibleMetadata.into())
}
}
) -> ::subxt::storage::address::StaticStorageAddress::<::subxt::metadata::DecodeStaticType<#storage_entry_value_ty>, ::subxt::storage::address::Yes, #is_defaultable_type, #is_iterable_type> {
::subxt::storage::address::StaticStorageAddress::new(
#pallet_name,
#storage_name,
#key_impl,
[#(#storage_hash,)*]
)
}
#client_iter_fn
};
(storage_entry_type, client_fns)
#root_entry_fn
}
}