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
+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
}
}