mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-04-22 10:18:01 +00:00
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:
+77
-211
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user