mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-13 01:11:10 +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:
Binary file not shown.
+43
-80
@@ -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 )*
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)*
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,9 +69,9 @@ impl FromIterator<syn::Path> for Derives {
|
||||
}
|
||||
|
||||
impl Derives {
|
||||
/// Add `::subxt::codec::CompactAs` to the derives.
|
||||
/// Add `::subxt::ext::codec::CompactAs` to the derives.
|
||||
pub fn insert_codec_compact_as(&mut self) {
|
||||
self.insert(parse_quote!(::subxt::codec::CompactAs));
|
||||
self.insert(parse_quote!(::subxt::ext::codec::CompactAs));
|
||||
}
|
||||
|
||||
pub fn append(&mut self, derives: impl Iterator<Item = syn::Path>) {
|
||||
@@ -88,8 +88,8 @@ impl Derives {
|
||||
impl Default for Derives {
|
||||
fn default() -> Self {
|
||||
let mut derives = HashSet::new();
|
||||
derives.insert(syn::parse_quote!(::subxt::codec::Encode));
|
||||
derives.insert(syn::parse_quote!(::subxt::codec::Decode));
|
||||
derives.insert(syn::parse_quote!(::subxt::ext::codec::Encode));
|
||||
derives.insert(syn::parse_quote!(::subxt::ext::codec::Decode));
|
||||
derives.insert(syn::parse_quote!(Debug));
|
||||
Self { derives }
|
||||
}
|
||||
|
||||
+42
-42
@@ -53,7 +53,7 @@ fn generate_struct_with_primitives() {
|
||||
pub mod tests {
|
||||
use super::root;
|
||||
|
||||
#[derive(::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub struct S {
|
||||
pub a: ::core::primitive::bool,
|
||||
pub b: ::core::primitive::u32,
|
||||
@@ -99,12 +99,12 @@ fn generate_struct_with_a_struct_field() {
|
||||
pub mod tests {
|
||||
use super::root;
|
||||
|
||||
#[derive(::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub struct Child {
|
||||
pub a: ::core::primitive::i32,
|
||||
}
|
||||
|
||||
#[derive(::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub struct Parent {
|
||||
pub a: ::core::primitive::bool,
|
||||
pub b: root::subxt_codegen::types::tests::Child,
|
||||
@@ -144,10 +144,10 @@ fn generate_tuple_struct() {
|
||||
pub mod tests {
|
||||
use super::root;
|
||||
|
||||
#[derive(::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub struct Child(pub ::core::primitive::i32,);
|
||||
|
||||
#[derive(::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub struct Parent(pub ::core::primitive::bool, pub root::subxt_codegen::types::tests::Child,);
|
||||
}
|
||||
}
|
||||
@@ -226,34 +226,34 @@ fn derive_compact_as_for_uint_wrapper_structs() {
|
||||
pub mod tests {
|
||||
use super::root;
|
||||
|
||||
#[derive(::subxt::codec::CompactAs, ::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::CompactAs, ::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub struct Su128 { pub a: ::core::primitive::u128, }
|
||||
|
||||
#[derive(::subxt::codec::CompactAs, ::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::CompactAs, ::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub struct Su16 { pub a: ::core::primitive::u16, }
|
||||
|
||||
#[derive(::subxt::codec::CompactAs, ::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::CompactAs, ::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub struct Su32 { pub a: ::core::primitive::u32, }
|
||||
|
||||
#[derive(::subxt::codec::CompactAs, ::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::CompactAs, ::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub struct Su64 { pub a: ::core::primitive::u64, }
|
||||
|
||||
#[derive(::subxt::codec::CompactAs, ::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::CompactAs, ::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub struct Su8 { pub a: ::core::primitive::u8, }
|
||||
|
||||
#[derive(::subxt::codec::CompactAs, ::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::CompactAs, ::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub struct TSu128(pub ::core::primitive::u128,);
|
||||
|
||||
#[derive(::subxt::codec::CompactAs, ::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::CompactAs, ::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub struct TSu16(pub ::core::primitive::u16,);
|
||||
|
||||
#[derive(::subxt::codec::CompactAs, ::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::CompactAs, ::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub struct TSu32(pub ::core::primitive::u32,);
|
||||
|
||||
#[derive(::subxt::codec::CompactAs, ::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::CompactAs, ::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub struct TSu64(pub ::core::primitive::u64,);
|
||||
|
||||
#[derive(::subxt::codec::CompactAs, ::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::CompactAs, ::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub struct TSu8(pub ::core::primitive::u8,);
|
||||
}
|
||||
}
|
||||
@@ -289,7 +289,7 @@ fn generate_enum() {
|
||||
quote! {
|
||||
pub mod tests {
|
||||
use super::root;
|
||||
#[derive(::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub enum E {
|
||||
# [codec (index = 0)]
|
||||
A,
|
||||
@@ -347,7 +347,7 @@ fn compact_fields() {
|
||||
quote! {
|
||||
pub mod tests {
|
||||
use super::root;
|
||||
#[derive(::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub enum E {
|
||||
# [codec (index = 0)]
|
||||
A {
|
||||
@@ -358,12 +358,12 @@ fn compact_fields() {
|
||||
B( #[codec(compact)] ::core::primitive::u32,),
|
||||
}
|
||||
|
||||
#[derive(::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub struct S {
|
||||
#[codec(compact)] pub a: ::core::primitive::u32,
|
||||
}
|
||||
|
||||
#[derive(::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub struct TupleStruct(#[codec(compact)] pub ::core::primitive::u32,);
|
||||
}
|
||||
}
|
||||
@@ -397,7 +397,7 @@ fn generate_array_field() {
|
||||
quote! {
|
||||
pub mod tests {
|
||||
use super::root;
|
||||
#[derive(::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub struct S {
|
||||
pub a: [::core::primitive::u8; 32usize],
|
||||
}
|
||||
@@ -434,7 +434,7 @@ fn option_fields() {
|
||||
quote! {
|
||||
pub mod tests {
|
||||
use super::root;
|
||||
#[derive(::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub struct S {
|
||||
pub a: ::core::option::Option<::core::primitive::bool>,
|
||||
pub b: ::core::option::Option<::core::primitive::u32>,
|
||||
@@ -474,7 +474,7 @@ fn box_fields_struct() {
|
||||
quote! {
|
||||
pub mod tests {
|
||||
use super::root;
|
||||
#[derive(::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub struct S {
|
||||
pub a: ::std::boxed::Box<::core::primitive::bool>,
|
||||
pub b: ::std::boxed::Box<::core::primitive::u32>,
|
||||
@@ -514,7 +514,7 @@ fn box_fields_enum() {
|
||||
quote! {
|
||||
pub mod tests {
|
||||
use super::root;
|
||||
#[derive(::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub enum E {
|
||||
# [codec (index = 0)]
|
||||
A(::std::boxed::Box<::core::primitive::bool>,),
|
||||
@@ -554,7 +554,7 @@ fn range_fields() {
|
||||
quote! {
|
||||
pub mod tests {
|
||||
use super::root;
|
||||
#[derive(::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub struct S {
|
||||
pub a: ::core::ops::Range<::core::primitive::u32>,
|
||||
pub b: ::core::ops::RangeInclusive<::core::primitive::u32>,
|
||||
@@ -598,12 +598,12 @@ fn generics() {
|
||||
quote! {
|
||||
pub mod tests {
|
||||
use super::root;
|
||||
#[derive(::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub struct Bar {
|
||||
pub b: root::subxt_codegen::types::tests::Foo<::core::primitive::u32>,
|
||||
pub c: root::subxt_codegen::types::tests::Foo<::core::primitive::u8>,
|
||||
}
|
||||
#[derive(::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub struct Foo<_0> {
|
||||
pub a: _0,
|
||||
}
|
||||
@@ -646,12 +646,12 @@ fn generics_nested() {
|
||||
quote! {
|
||||
pub mod tests {
|
||||
use super::root;
|
||||
#[derive(::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub struct Bar<_0> {
|
||||
pub b: root::subxt_codegen::types::tests::Foo<_0, ::core::primitive::u32>,
|
||||
}
|
||||
|
||||
#[derive(::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub struct Foo<_0, _1> {
|
||||
pub a: _0,
|
||||
pub b: ::core::option::Option<(_0, _1,)>,
|
||||
@@ -697,10 +697,10 @@ fn generate_bitvec() {
|
||||
quote! {
|
||||
pub mod tests {
|
||||
use super::root;
|
||||
#[derive(::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub struct S {
|
||||
pub lsb: ::subxt::bitvec::vec::BitVec<::core::primitive::u8, root::bitvec::order::Lsb0>,
|
||||
pub msb: ::subxt::bitvec::vec::BitVec<::core::primitive::u16, root::bitvec::order::Msb0>,
|
||||
pub lsb: ::subxt::ext::bitvec::vec::BitVec<::core::primitive::u8, root::bitvec::order::Lsb0>,
|
||||
pub msb: ::subxt::ext::bitvec::vec::BitVec<::core::primitive::u16, root::bitvec::order::Msb0>,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -750,12 +750,12 @@ fn generics_with_alias_adds_phantom_data_marker() {
|
||||
quote! {
|
||||
pub mod tests {
|
||||
use super::root;
|
||||
#[derive(::subxt::codec::CompactAs, ::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::CompactAs, ::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub struct NamedFields<_0> {
|
||||
pub b: ::core::primitive::u32,
|
||||
#[codec(skip)] pub __subxt_unused_type_params: ::core::marker::PhantomData<_0>
|
||||
}
|
||||
#[derive(::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub struct UnnamedFields<_0, _1> (
|
||||
pub (::core::primitive::u32, ::core::primitive::u32,),
|
||||
#[codec(skip)] pub ::core::marker::PhantomData<(_0, _1)>
|
||||
@@ -818,20 +818,20 @@ fn modules() {
|
||||
pub mod b {
|
||||
use super::root;
|
||||
|
||||
#[derive(::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub struct Bar {
|
||||
pub a: root::subxt_codegen::types::tests::m::a::Foo,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub struct Foo;
|
||||
}
|
||||
|
||||
pub mod c {
|
||||
use super::root;
|
||||
|
||||
#[derive(::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub struct Foo {
|
||||
pub a: root::subxt_codegen::types::tests::m::a::b::Bar,
|
||||
}
|
||||
@@ -868,7 +868,7 @@ fn dont_force_struct_names_camel_case() {
|
||||
pub mod tests {
|
||||
use super::root;
|
||||
|
||||
#[derive(::subxt::codec::Decode, ::subxt::codec::Encode, Debug)]
|
||||
#[derive(::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug)]
|
||||
pub struct AB;
|
||||
}
|
||||
}
|
||||
@@ -905,10 +905,10 @@ fn apply_user_defined_derives_for_all_types() {
|
||||
pub mod tests {
|
||||
use super::root;
|
||||
|
||||
#[derive(::subxt::codec::Decode, ::subxt::codec::Encode, Clone, Debug, Eq)]
|
||||
#[derive(::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Clone, Debug, Eq)]
|
||||
pub struct A(pub root :: subxt_codegen :: types :: tests :: B,);
|
||||
|
||||
#[derive(::subxt::codec::Decode, ::subxt::codec::Encode, Clone, Debug, Eq)]
|
||||
#[derive(::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Clone, Debug, Eq)]
|
||||
pub struct B;
|
||||
}
|
||||
}
|
||||
@@ -964,13 +964,13 @@ fn apply_user_defined_derives_for_specific_types() {
|
||||
pub mod tests {
|
||||
use super::root;
|
||||
|
||||
#[derive(::subxt::codec::Decode, ::subxt::codec::Encode, Debug, Eq)]
|
||||
#[derive(::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug, Eq)]
|
||||
pub struct A(pub root :: subxt_codegen :: types :: tests :: B,);
|
||||
|
||||
#[derive(::subxt::codec::Decode, ::subxt::codec::Encode, Debug, Eq, Hash)]
|
||||
#[derive(::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug, Eq, Hash)]
|
||||
pub struct B(pub root :: subxt_codegen :: types :: tests :: C,);
|
||||
|
||||
#[derive(::subxt::codec::Decode, ::subxt::codec::Encode, Debug, Eq, Ord, PartialOrd)]
|
||||
#[derive(::subxt::ext::codec::Decode, ::subxt::ext::codec::Encode, Debug, Eq, Ord, PartialOrd)]
|
||||
pub struct C;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ impl TypePathType {
|
||||
let bit_order_type = &self.params[0];
|
||||
let bit_store_type = &self.params[1];
|
||||
|
||||
let type_path = parse_quote! { ::subxt::bitvec::vec::BitVec<#bit_store_type, #bit_order_type> };
|
||||
let type_path = parse_quote! { ::subxt::ext::bitvec::vec::BitVec<#bit_store_type, #bit_order_type> };
|
||||
|
||||
syn::Type::Path(type_path)
|
||||
}
|
||||
|
||||
@@ -19,3 +19,4 @@ futures = "0.3.13"
|
||||
codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive", "full", "bit-vec"] }
|
||||
hex = "0.4.3"
|
||||
tracing-subscriber = "0.3.11"
|
||||
|
||||
|
||||
@@ -2,20 +2,19 @@
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! To run this example, a local polkadot node should be running. Example verified against polkadot 0.9.18-4542a603cc-aarch64-macos.
|
||||
//! To run this example, a local polkadot node should be running. Example verified against polkadot polkadot 0.9.25-5174e9ae75b.
|
||||
//!
|
||||
//! E.g.
|
||||
//! ```bash
|
||||
//! curl "https://github.com/paritytech/polkadot/releases/download/v0.9.18/polkadot" --output /usr/local/bin/polkadot --location
|
||||
//! curl "https://github.com/paritytech/polkadot/releases/download/v0.9.25/polkadot" --output /usr/local/bin/polkadot --location
|
||||
//! polkadot --dev --tmp
|
||||
//! ```
|
||||
|
||||
use sp_keyring::AccountKeyring;
|
||||
use subxt::{
|
||||
ClientBuilder,
|
||||
DefaultConfig,
|
||||
PairSigner,
|
||||
PolkadotExtrinsicParams,
|
||||
tx::PairSigner,
|
||||
OnlineClient,
|
||||
PolkadotConfig,
|
||||
};
|
||||
|
||||
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata.scale")]
|
||||
@@ -26,23 +25,20 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let signer = PairSigner::new(AccountKeyring::Alice.pair());
|
||||
|
||||
let api = ClientBuilder::new()
|
||||
.build()
|
||||
.await?
|
||||
.to_runtime_api::<polkadot::RuntimeApi<DefaultConfig, PolkadotExtrinsicParams<DefaultConfig>>>();
|
||||
|
||||
// Submit the `transfer` extrinsic from Alice's account to Bob's.
|
||||
let dest = AccountKeyring::Bob.to_account_id().into();
|
||||
|
||||
// Obtain an extrinsic, calling the "transfer" function in
|
||||
// the "balances" pallet.
|
||||
let extrinsic = api.tx().balances().transfer(dest, 123_456_789_012_345)?;
|
||||
// Create a client to use:
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
|
||||
// Sign and submit the extrinsic, returning its hash.
|
||||
let tx_hash = extrinsic.sign_and_submit_default(&signer).await?;
|
||||
// Create a transaction to submit:
|
||||
let tx = polkadot::tx()
|
||||
.balances()
|
||||
.transfer(dest, 123_456_789_012_345);
|
||||
|
||||
println!("Balance transfer extrinsic submitted: {}", tx_hash);
|
||||
// Submit the transaction with default params:
|
||||
let hash = api.tx().sign_and_submit_default(&tx, &signer).await?;
|
||||
|
||||
println!("Balance transfer extrinsic submitted: {}", hash);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2,25 +2,24 @@
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! To run this example, a local polkadot node should be running. Example verified against polkadot 0.9.18-4542a603cc-aarch64-macos.
|
||||
//! To run this example, a local polkadot node should be running. Example verified against polkadot polkadot 0.9.25-5174e9ae75b.
|
||||
//!
|
||||
//! E.g.
|
||||
//! ```bash
|
||||
//! curl "https://github.com/paritytech/polkadot/releases/download/v0.9.18/polkadot" --output /usr/local/bin/polkadot --location
|
||||
//! curl "https://github.com/paritytech/polkadot/releases/download/v0.9.25/polkadot" --output /usr/local/bin/polkadot --location
|
||||
//! polkadot --dev --tmp
|
||||
//! ```
|
||||
|
||||
use sp_keyring::AccountKeyring;
|
||||
use subxt::{
|
||||
extrinsic::{
|
||||
tx::{
|
||||
Era,
|
||||
PairSigner,
|
||||
PlainTip,
|
||||
PolkadotExtrinsicParamsBuilder as Params,
|
||||
},
|
||||
ClientBuilder,
|
||||
DefaultConfig,
|
||||
PairSigner,
|
||||
PolkadotExtrinsicParams,
|
||||
PolkadotExtrinsicParamsBuilder as Params,
|
||||
OnlineClient,
|
||||
PolkadotConfig,
|
||||
};
|
||||
|
||||
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata.scale")]
|
||||
@@ -33,23 +32,21 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let signer = PairSigner::new(AccountKeyring::Alice.pair());
|
||||
let dest = AccountKeyring::Bob.to_account_id().into();
|
||||
|
||||
let api = ClientBuilder::new()
|
||||
.build()
|
||||
.await?
|
||||
.to_runtime_api::<polkadot::RuntimeApi<DefaultConfig, PolkadotExtrinsicParams<DefaultConfig>>>();
|
||||
// Create a client to use:
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
|
||||
// Create a transaction to submit:
|
||||
let tx = polkadot::tx()
|
||||
.balances()
|
||||
.transfer(dest, 123_456_789_012_345);
|
||||
|
||||
// Configure the transaction tip and era:
|
||||
let tx_params = Params::new()
|
||||
.tip(PlainTip::new(20_000_000_000))
|
||||
.era(Era::Immortal, *api.client.genesis());
|
||||
.era(Era::Immortal, api.genesis_hash());
|
||||
|
||||
// Send the transaction:
|
||||
let hash = api
|
||||
.tx()
|
||||
.balances()
|
||||
.transfer(dest, 123_456_789_012_345)?
|
||||
.sign_and_submit(&signer, tx_params)
|
||||
.await?;
|
||||
// submit the transaction:
|
||||
let hash = api.tx().sign_and_submit(&tx, &signer, tx_params).await?;
|
||||
|
||||
println!("Balance transfer extrinsic submitted: {}", hash);
|
||||
|
||||
|
||||
@@ -2,12 +2,19 @@
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! To run this example, a local polkadot node should be running. Example verified against polkadot polkadot 0.9.25-5174e9ae75b.
|
||||
//!
|
||||
//! E.g.
|
||||
//! ```bash
|
||||
//! curl "https://github.com/paritytech/polkadot/releases/download/v0.9.25/polkadot" --output /usr/local/bin/polkadot --location
|
||||
//! polkadot --dev --tmp
|
||||
//! ```
|
||||
|
||||
use futures::join;
|
||||
use sp_keyring::AccountKeyring;
|
||||
use subxt::{
|
||||
ClientBuilder,
|
||||
DefaultConfig,
|
||||
PolkadotExtrinsicParams,
|
||||
OnlineClient,
|
||||
PolkadotConfig,
|
||||
};
|
||||
|
||||
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata.scale")]
|
||||
@@ -15,17 +22,18 @@ pub mod polkadot {}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let api = ClientBuilder::new()
|
||||
.build()
|
||||
.await?
|
||||
.to_runtime_api::<polkadot::RuntimeApi<DefaultConfig, PolkadotExtrinsicParams<DefaultConfig>>>();
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
|
||||
let addr = AccountKeyring::Bob.to_account_id();
|
||||
|
||||
// Construct storage addresses to access:
|
||||
let staking_bonded = polkadot::storage().staking().bonded(&addr);
|
||||
let staking_ledger = polkadot::storage().staking().ledger(&addr);
|
||||
|
||||
// For storage requests, we can join futures together to
|
||||
// await multiple futures concurrently:
|
||||
let a_fut = api.storage().staking().bonded(&addr, None);
|
||||
let b_fut = api.storage().staking().ledger(&addr, None);
|
||||
let a_fut = api.storage().fetch(&staking_bonded, None);
|
||||
let b_fut = api.storage().fetch(&staking_ledger, None);
|
||||
let (a, b) = join!(a_fut, b_fut);
|
||||
|
||||
println!("{a:?}, {b:?}");
|
||||
|
||||
@@ -2,21 +2,20 @@
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! To run this example, a local polkadot node should be running. Example verified against polkadot 0.9.18-4542a603cc-aarch64-macos.
|
||||
//!
|
||||
//! E.g.
|
||||
//! ```bash
|
||||
//! curl "https://github.com/paritytech/polkadot/releases/download/v0.9.18/polkadot" --output /usr/local/bin/polkadot --location
|
||||
//! polkadot --dev --tmp
|
||||
//! ```
|
||||
//! This example should compile but should aos fail to work, since we've modified the
|
||||
//! config to not align with a Polkadot node.
|
||||
|
||||
use sp_keyring::AccountKeyring;
|
||||
use subxt::{
|
||||
ClientBuilder,
|
||||
Config,
|
||||
DefaultConfig,
|
||||
PairSigner,
|
||||
PolkadotExtrinsicParams,
|
||||
config::{
|
||||
Config,
|
||||
SubstrateConfig,
|
||||
},
|
||||
tx::{
|
||||
PairSigner,
|
||||
SubstrateExtrinsicParams,
|
||||
},
|
||||
OnlineClient,
|
||||
};
|
||||
|
||||
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata.scale")]
|
||||
@@ -33,32 +32,34 @@ impl Config for MyConfig {
|
||||
// polkadot runtime used, so some operations will fail. Normally when using a custom `Config`
|
||||
// impl types MUST match exactly those used in the actual runtime.
|
||||
type Index = u64;
|
||||
type BlockNumber = <DefaultConfig as Config>::BlockNumber;
|
||||
type Hash = <DefaultConfig as Config>::Hash;
|
||||
type Hashing = <DefaultConfig as Config>::Hashing;
|
||||
type AccountId = <DefaultConfig as Config>::AccountId;
|
||||
type Address = <DefaultConfig as Config>::Address;
|
||||
type Header = <DefaultConfig as Config>::Header;
|
||||
type Signature = <DefaultConfig as Config>::Signature;
|
||||
type Extrinsic = <DefaultConfig as Config>::Extrinsic;
|
||||
type BlockNumber = <SubstrateConfig as Config>::BlockNumber;
|
||||
type Hash = <SubstrateConfig as Config>::Hash;
|
||||
type Hashing = <SubstrateConfig as Config>::Hashing;
|
||||
type AccountId = <SubstrateConfig as Config>::AccountId;
|
||||
type Address = <SubstrateConfig as Config>::Address;
|
||||
type Header = <SubstrateConfig as Config>::Header;
|
||||
type Signature = <SubstrateConfig as Config>::Signature;
|
||||
type Extrinsic = <SubstrateConfig as Config>::Extrinsic;
|
||||
// ExtrinsicParams makes use of the index type, so we need to adjust it
|
||||
// too to align with our modified index type, above:
|
||||
type ExtrinsicParams = SubstrateExtrinsicParams<Self>;
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let api = ClientBuilder::new()
|
||||
.build()
|
||||
.await?
|
||||
.to_runtime_api::<polkadot::RuntimeApi<MyConfig, PolkadotExtrinsicParams<MyConfig>>>();
|
||||
|
||||
let signer = PairSigner::new(AccountKeyring::Alice.pair());
|
||||
let dest = AccountKeyring::Bob.to_account_id().into();
|
||||
|
||||
let hash = api
|
||||
.tx()
|
||||
// Create a client to use:
|
||||
let api = OnlineClient::<MyConfig>::new().await?;
|
||||
|
||||
// Create a transaction to submit:
|
||||
let tx = polkadot::tx()
|
||||
.balances()
|
||||
.transfer(dest, 10_000)?
|
||||
.sign_and_submit_default(&signer)
|
||||
.await?;
|
||||
.transfer(dest, 123_456_789_012_345);
|
||||
|
||||
// submit the transaction with default params:
|
||||
let hash = api.tx().sign_and_submit_default(&tx, &signer).await?;
|
||||
|
||||
println!("Balance transfer extrinsic submitted: {}", hash);
|
||||
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Example verified against polkadot 0.9.18-4542a603cc-aarch64-macos.
|
||||
|
||||
//! Example verified against polkadot polkadot 0.9.25-5174e9ae75b.
|
||||
#![allow(clippy::redundant_clone)]
|
||||
|
||||
#[subxt::subxt(
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// 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.
|
||||
|
||||
//! To run this example, a local polkadot node should be running. Example verified against polkadot polkadot 0.9.25-5174e9ae75b.
|
||||
//!
|
||||
//! E.g.
|
||||
//! ```bash
|
||||
//! curl "https://github.com/paritytech/polkadot/releases/download/v0.9.25/polkadot" --output /usr/local/bin/polkadot --location
|
||||
//! polkadot --dev --tmp
|
||||
//! ```
|
||||
|
||||
// This example showcases working with dynamic values rather than those that are generated via the subxt proc macro.
|
||||
|
||||
use sp_keyring::AccountKeyring;
|
||||
use subxt::{
|
||||
dynamic::Value,
|
||||
tx::PairSigner,
|
||||
OnlineClient,
|
||||
PolkadotConfig,
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
|
||||
// 1. Dynamic Balance Transfer (the dynamic equivalent to the balance_transfer example).
|
||||
|
||||
let signer = PairSigner::new(AccountKeyring::Alice.pair());
|
||||
let dest = AccountKeyring::Bob.to_account_id();
|
||||
|
||||
// Create a transaction to submit:
|
||||
let tx = subxt::dynamic::tx(
|
||||
"Balances",
|
||||
"transfer",
|
||||
vec![
|
||||
// A value representing a MultiAddress<AccountId32, _>. We want the "Id" variant, and that
|
||||
// will ultimately contain the bytes for our destination address (there is a new type wrapping
|
||||
// the address, but our encoding will happily ignore such things and do it's best to line up what
|
||||
// we provide with what it needs).
|
||||
Value::unnamed_variant("Id", [Value::from_bytes(&dest)]),
|
||||
// A value representing the amount we'd like to transfer.
|
||||
Value::u128(123_456_789_012_345),
|
||||
],
|
||||
);
|
||||
|
||||
// submit the transaction with default params:
|
||||
let hash = api.tx().sign_and_submit_default(&tx, &signer).await?;
|
||||
println!("Balance transfer extrinsic submitted: {}", hash);
|
||||
|
||||
// 2. Dynamic constant access (the dynamic equivalent to the fetch_constants example).
|
||||
|
||||
let constant_address = subxt::dynamic::constant("Balances", "ExistentialDeposit");
|
||||
let existential_deposit = api.constants().at(&constant_address)?;
|
||||
println!("Existential Deposit: {}", existential_deposit);
|
||||
|
||||
// 3. Dynamic storage access
|
||||
|
||||
let storage_address = subxt::dynamic::storage(
|
||||
"System",
|
||||
"Account",
|
||||
vec![
|
||||
// Something that encodes to an AccountId32 is what we need for the map key here:
|
||||
Value::from_bytes(&dest),
|
||||
],
|
||||
);
|
||||
let account = api
|
||||
.storage()
|
||||
.fetch_or_default(&storage_address, None)
|
||||
.await?;
|
||||
println!("Bob's account details: {account}");
|
||||
|
||||
// 4. Dynamic storage iteration (the dynamic equivalent to the fetch_all_accounts example).
|
||||
|
||||
let storage_address = subxt::dynamic::storage_root("System", "Account");
|
||||
let mut iter = api.storage().iter(storage_address, 10, None).await?;
|
||||
while let Some((key, account)) = iter.next().await? {
|
||||
println!("{}: {}", hex::encode(key), account);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2,18 +2,17 @@
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! To run this example, a local polkadot node should be running. Example verified against polkadot 0.9.18-4542a603cc-aarch64-macos.
|
||||
//! To run this example, a local polkadot node should be running. Example verified against polkadot polkadot 0.9.25-5174e9ae75b.
|
||||
//!
|
||||
//! E.g.
|
||||
//! ```bash
|
||||
//! curl "https://github.com/paritytech/polkadot/releases/download/v0.9.18/polkadot" --output /usr/local/bin/polkadot --location
|
||||
//! curl "https://github.com/paritytech/polkadot/releases/download/v0.9.25/polkadot" --output /usr/local/bin/polkadot --location
|
||||
//! polkadot --dev --tmp
|
||||
//! ```
|
||||
|
||||
use subxt::{
|
||||
ClientBuilder,
|
||||
DefaultConfig,
|
||||
PolkadotExtrinsicParams,
|
||||
OnlineClient,
|
||||
PolkadotConfig,
|
||||
};
|
||||
|
||||
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata.scale")]
|
||||
@@ -23,12 +22,11 @@ pub mod polkadot {}
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let api = ClientBuilder::new()
|
||||
.build()
|
||||
.await?
|
||||
.to_runtime_api::<polkadot::RuntimeApi<DefaultConfig, PolkadotExtrinsicParams<DefaultConfig>>>();
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
|
||||
let mut iter = api.storage().system().account_iter(None).await?;
|
||||
let address = polkadot::storage().system().account_root();
|
||||
|
||||
let mut iter = api.storage().iter(address, 10, None).await?;
|
||||
|
||||
while let Some((key, account)) = iter.next().await? {
|
||||
println!("{}: {}", hex::encode(key), account.data.free);
|
||||
|
||||
@@ -2,18 +2,17 @@
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! To run this example, a local polkadot node should be running. Example verified against polkadot 0.9.18-4542a603cc-aarch64-macos.
|
||||
//! To run this example, a local polkadot node should be running. Example verified against polkadot polkadot 0.9.25-5174e9ae75b.
|
||||
//!
|
||||
//! E.g.
|
||||
//! ```bash
|
||||
//! curl "https://github.com/paritytech/polkadot/releases/download/v0.9.18/polkadot" --output /usr/local/bin/polkadot --location
|
||||
//! curl "https://github.com/paritytech/polkadot/releases/download/v0.9.25/polkadot" --output /usr/local/bin/polkadot --location
|
||||
//! polkadot --dev --tmp
|
||||
//! ```
|
||||
|
||||
use subxt::{
|
||||
ClientBuilder,
|
||||
DefaultConfig,
|
||||
PolkadotExtrinsicParams,
|
||||
OnlineClient,
|
||||
PolkadotConfig,
|
||||
};
|
||||
|
||||
// Generate the API from a static metadata path.
|
||||
@@ -24,22 +23,14 @@ pub mod polkadot {}
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
// Upon connecting to the target polkadot node, the node's metadata is downloaded (referred to
|
||||
// as the runtime metadata).
|
||||
let api = ClientBuilder::new()
|
||||
.build()
|
||||
.await?
|
||||
.to_runtime_api::<polkadot::RuntimeApi<DefaultConfig, PolkadotExtrinsicParams<DefaultConfig>>>();
|
||||
// Create a client to use:
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
|
||||
// Constants are queried from the node's runtime metadata.
|
||||
// Query the `ExistentialDeposit` constant from the `Balances` pallet.
|
||||
let existential_deposit = api
|
||||
// This is the constants query.
|
||||
.constants()
|
||||
// Constant from the `Balances` pallet.
|
||||
.balances()
|
||||
// Constant name.
|
||||
.existential_deposit()?;
|
||||
// Build a constant address to query:
|
||||
let address = polkadot::constants().balances().existential_deposit();
|
||||
|
||||
// Look it up:
|
||||
let existential_deposit = api.constants().at(&address)?;
|
||||
|
||||
println!("Existential Deposit: {}", existential_deposit);
|
||||
|
||||
|
||||
@@ -2,24 +2,25 @@
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! To run this example, a local polkadot node should be running. Example verified against polkadot 0.9.18-4542a603cc-aarch64-macos.
|
||||
//! To run this example, a local polkadot node should be running. Example verified against polkadot polkadot 0.9.25-5174e9ae75b.
|
||||
//!
|
||||
//! E.g.
|
||||
//! ```bash
|
||||
//! curl "https://github.com/paritytech/polkadot/releases/download/v0.9.18/polkadot" --output /usr/local/bin/polkadot --location
|
||||
//! curl "https://github.com/paritytech/polkadot/releases/download/v0.9.25/polkadot" --output /usr/local/bin/polkadot --location
|
||||
//! polkadot --dev --tmp
|
||||
//! ```
|
||||
|
||||
use sp_keyring::AccountKeyring;
|
||||
use subxt::{
|
||||
sp_core::{
|
||||
sr25519,
|
||||
Pair,
|
||||
ext::{
|
||||
sp_core::{
|
||||
sr25519,
|
||||
Pair,
|
||||
},
|
||||
sp_runtime::AccountId32,
|
||||
},
|
||||
sp_runtime::AccountId32,
|
||||
ClientBuilder,
|
||||
DefaultConfig,
|
||||
PolkadotExtrinsicParams,
|
||||
OnlineClient,
|
||||
PolkadotConfig,
|
||||
};
|
||||
|
||||
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata.scale")]
|
||||
@@ -29,12 +30,11 @@ pub mod polkadot {}
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let api = ClientBuilder::new()
|
||||
.build()
|
||||
.await?
|
||||
.to_runtime_api::<polkadot::RuntimeApi<DefaultConfig, PolkadotExtrinsicParams<DefaultConfig>>>();
|
||||
// Create a client to use:
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
|
||||
let era = api.storage().staking().active_era(None).await?.unwrap();
|
||||
let active_era_addr = polkadot::storage().staking().active_era();
|
||||
let era = api.storage().fetch(&active_era_addr, None).await?.unwrap();
|
||||
println!(
|
||||
"Staking active era: index: {:?}, start: {:?}",
|
||||
era.index, era.start
|
||||
@@ -51,19 +51,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!(" Alice//stash account id: {:?}", alice_stash_id);
|
||||
|
||||
// Map from all locked "stash" accounts to the controller account.
|
||||
let controller_acc_addr = polkadot::storage().staking().bonded(&alice_stash_id);
|
||||
let controller_acc = api
|
||||
.storage()
|
||||
.staking()
|
||||
.bonded(&alice_stash_id, None)
|
||||
.fetch(&controller_acc_addr, None)
|
||||
.await?
|
||||
.unwrap();
|
||||
println!(" account controlled by: {:?}", controller_acc);
|
||||
|
||||
let era_result = api
|
||||
.storage()
|
||||
.staking()
|
||||
.eras_reward_points(&era.index, None)
|
||||
.await?;
|
||||
let era_reward_addr = polkadot::storage().staking().eras_reward_points(&era.index);
|
||||
let era_result = api.storage().fetch(&era_reward_addr, None).await?;
|
||||
println!("Era reward points: {:?}", era_result);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -2,18 +2,17 @@
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! To run this example, a local polkadot node should be running. Example verified against polkadot 0.9.18-4542a603cc-aarch64-macos.
|
||||
//! To run this example, a local polkadot node should be running. Example verified against polkadot polkadot 0.9.25-5174e9ae75b.
|
||||
//!
|
||||
//! E.g.
|
||||
//! ```bash
|
||||
//! curl "https://github.com/paritytech/polkadot/releases/download/v0.9.18/polkadot" --output /usr/local/bin/polkadot --location
|
||||
//! curl "https://github.com/paritytech/polkadot/releases/download/v0.9.25/polkadot" --output /usr/local/bin/polkadot --location
|
||||
//! polkadot --dev --tmp
|
||||
//! ```
|
||||
|
||||
use subxt::{
|
||||
ClientBuilder,
|
||||
DefaultConfig,
|
||||
PolkadotExtrinsicParams,
|
||||
OnlineClient,
|
||||
PolkadotConfig,
|
||||
};
|
||||
|
||||
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata.scale")]
|
||||
@@ -23,18 +22,15 @@ pub mod polkadot {}
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let api = ClientBuilder::new()
|
||||
.build()
|
||||
.await?
|
||||
.to_runtime_api::<polkadot::RuntimeApi<DefaultConfig, PolkadotExtrinsicParams<DefaultConfig>>>();
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
|
||||
// Full metadata validation is not enabled by default; instead, the individual calls,
|
||||
// storage requests and constant accesses are runtime type checked against the node
|
||||
// metadata to ensure that they are compatible with the generated code.
|
||||
//
|
||||
// To make sure that all of our statically generated pallets are compatible with the
|
||||
// runtime node, we can run this check:
|
||||
api.validate_metadata()?;
|
||||
// Each individual request will be validated against the static code that was
|
||||
// used to construct it, if possible. We can also validate the entirety of the
|
||||
// statically generated code against some client at a given point in time using
|
||||
// this check. If it fails, then there is some breaking change between the metadata
|
||||
// used to generate this static code, and the runtime metadata from a node that the
|
||||
// client is using.
|
||||
polkadot::validate_codegen(&api)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2,40 +2,28 @@
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! To run this example, a local polkadot node should be running. Example verified against polkadot 0.9.18-4542a603cc-aarch64-macos.
|
||||
//! To run this example, a local polkadot node should be running. Example verified against polkadot polkadot 0.9.25-5174e9ae75b.
|
||||
//!
|
||||
//! E.g.
|
||||
//! ```bash
|
||||
//! curl "https://github.com/paritytech/polkadot/releases/download/v0.9.18/polkadot" --output /usr/local/bin/polkadot --location
|
||||
//! curl "https://github.com/paritytech/polkadot/releases/download/v0.9.25/polkadot" --output /usr/local/bin/polkadot --location
|
||||
//! polkadot --dev --tmp
|
||||
//! ```
|
||||
|
||||
use subxt::{
|
||||
ClientBuilder,
|
||||
DefaultConfig,
|
||||
PolkadotExtrinsicParams,
|
||||
OnlineClient,
|
||||
PolkadotConfig,
|
||||
};
|
||||
|
||||
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata.scale")]
|
||||
pub mod polkadot {}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let api = ClientBuilder::new()
|
||||
.set_url("wss://rpc.polkadot.io:443")
|
||||
.build()
|
||||
.await?
|
||||
.to_runtime_api::<polkadot::RuntimeApi<DefaultConfig, PolkadotExtrinsicParams<DefaultConfig>>>();
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
|
||||
let block_number = 1u32;
|
||||
|
||||
let block_hash = api
|
||||
.client
|
||||
.rpc()
|
||||
.block_hash(Some(block_number.into()))
|
||||
.await?;
|
||||
let block_hash = api.rpc().block_hash(Some(block_number.into())).await?;
|
||||
|
||||
if let Some(hash) = block_hash {
|
||||
println!("Block hash for block number {block_number}: {hash}");
|
||||
|
||||
@@ -2,41 +2,34 @@
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! To run this example, a local polkadot node should be running. Example verified against polkadot 0.9.18-f6d6ab005d-aarch64-macos.
|
||||
//! To run this example, a local polkadot node should be running. Example verified against polkadot polkadot 0.9.25-5174e9ae75b.
|
||||
//!
|
||||
//! E.g.
|
||||
//! ```bash
|
||||
//! curl "https://github.com/paritytech/polkadot/releases/download/v0.9.13/polkadot" --output /usr/local/bin/polkadot --location
|
||||
//! curl "https://github.com/paritytech/polkadot/releases/download/v0.9.25/polkadot" --output /usr/local/bin/polkadot --location
|
||||
//! polkadot --dev --tmp
|
||||
//! ```
|
||||
|
||||
use subxt::{
|
||||
rpc::Subscription,
|
||||
sp_runtime::{
|
||||
ext::sp_runtime::{
|
||||
generic::Header,
|
||||
traits::BlakeTwo256,
|
||||
},
|
||||
ClientBuilder,
|
||||
DefaultConfig,
|
||||
PolkadotExtrinsicParams,
|
||||
rpc::Subscription,
|
||||
OnlineClient,
|
||||
PolkadotConfig,
|
||||
};
|
||||
|
||||
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata.scale")]
|
||||
pub mod polkadot {}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let api = ClientBuilder::new()
|
||||
.set_url("wss://rpc.polkadot.io:443")
|
||||
.build()
|
||||
.await?
|
||||
.to_runtime_api::<polkadot::RuntimeApi<DefaultConfig, PolkadotExtrinsicParams<DefaultConfig>>>();
|
||||
let api =
|
||||
OnlineClient::<PolkadotConfig>::from_url("wss://rpc.polkadot.io:443").await?;
|
||||
|
||||
// For non-finalised blocks use `.subscribe_blocks()`
|
||||
let mut blocks: Subscription<Header<u32, BlakeTwo256>> =
|
||||
api.client.rpc().subscribe_finalized_blocks().await?;
|
||||
api.rpc().subscribe_finalized_blocks().await?;
|
||||
|
||||
while let Some(Ok(block)) = blocks.next().await {
|
||||
println!(
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
// 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.
|
||||
|
||||
//! To run this example, a local polkadot node should be running. Example verified against polkadot polkadot 0.9.25-5174e9ae75b.
|
||||
//!
|
||||
//! E.g.
|
||||
//! ```bash
|
||||
//! curl "https://github.com/paritytech/polkadot/releases/download/v0.9.25/polkadot" --output /usr/local/bin/polkadot --location
|
||||
//! polkadot --dev --tmp
|
||||
//! ```
|
||||
|
||||
use codec::Decode;
|
||||
use subxt::{
|
||||
storage::address::{
|
||||
StorageHasher,
|
||||
StorageMapKey,
|
||||
},
|
||||
OnlineClient,
|
||||
PolkadotConfig,
|
||||
};
|
||||
|
||||
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata.scale")]
|
||||
pub mod polkadot {}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
// Create a client to use:
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
|
||||
// Example 1. Iterate over (keys, value) using the storage client.
|
||||
// This is the standard and most ergonomic approach.
|
||||
{
|
||||
let key_addr = polkadot::storage().xcm_pallet().version_notifiers_root();
|
||||
|
||||
let mut iter = api.storage().iter(key_addr, 10, None).await?;
|
||||
|
||||
println!("\nExample 1. Obtained keys:");
|
||||
while let Some((key, value)) = iter.next().await? {
|
||||
println!("Key: 0x{}", hex::encode(&key));
|
||||
println!(" Value: {}", value);
|
||||
}
|
||||
}
|
||||
|
||||
// Example 2. Iterate over fetched keys manually. Here, you forgo any static type
|
||||
// safety and work directly with the bytes on either side.
|
||||
{
|
||||
let key_addr = polkadot::storage().xcm_pallet().version_notifiers_root();
|
||||
|
||||
// Fetch at most 10 keys from below the prefix XcmPallet' VersionNotifiers.
|
||||
let keys = api
|
||||
.storage()
|
||||
.fetch_keys(&key_addr.to_root_bytes(), 10, None, None)
|
||||
.await?;
|
||||
|
||||
println!("Example 2. Obtained keys:");
|
||||
for key in keys.iter() {
|
||||
println!("Key: 0x{}", hex::encode(&key));
|
||||
|
||||
if let Some(storage_data) = api.storage().fetch_raw(&key.0, None).await? {
|
||||
// We know the return value to be `QueryId` (`u64`) from inspecting either:
|
||||
// - polkadot code
|
||||
// - polkadot.rs generated file under `version_notifiers()` fn
|
||||
// - metadata in json format
|
||||
let value = u64::decode(&mut &*storage_data)?;
|
||||
println!(" Value: {}", value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example 3. Custom iteration over double maps. Here, we manually append one lookup
|
||||
// key to the root and just iterate over the values underneath that.
|
||||
{
|
||||
let key_addr = polkadot::storage().xcm_pallet().version_notifiers_root();
|
||||
|
||||
// Obtain the root bytes (`twox_128("XcmPallet") ++ twox_128("VersionNotifiers")`).
|
||||
let mut query_key = key_addr.to_root_bytes();
|
||||
|
||||
// We know that the first key is a u32 (the `XcmVersion`) and is hashed by twox64_concat.
|
||||
// We can build a `StorageMapKey` that replicates that, and append those bytes to the above.
|
||||
StorageMapKey::new(&2u32, StorageHasher::Twox64Concat).to_bytes(&mut query_key);
|
||||
|
||||
// The final query key is essentially the result of:
|
||||
// `twox_128("XcmPallet") ++ twox_128("VersionNotifiers") ++ twox_64(2u32) ++ 2u32`
|
||||
println!("\nExample 3\nQuery key: 0x{}", hex::encode(&query_key));
|
||||
|
||||
let keys = api.storage().fetch_keys(&query_key, 10, None, None).await?;
|
||||
|
||||
println!("Obtained keys:");
|
||||
for key in keys.iter() {
|
||||
println!("Key: 0x{}", hex::encode(&key));
|
||||
|
||||
if let Some(storage_data) = api.storage().fetch_raw(&key.0, None).await? {
|
||||
// We know the return value to be `QueryId` (`u64`) from inspecting either:
|
||||
// - polkadot code
|
||||
// - polkadot.rs generated file under `version_notifiers()` fn
|
||||
// - metadata in json format
|
||||
let value = u64::decode(&mut &storage_data[..])?;
|
||||
println!(" Value: {}", value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,153 +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.
|
||||
|
||||
//! To run this example, a local polkadot node should be running. Example verified against polkadot 0.9.18-4542a603cc-aarch64-macos.
|
||||
//!
|
||||
//! E.g.
|
||||
//! ```bash
|
||||
//! curl "https://github.com/paritytech/polkadot/releases/download/v0.9.18/polkadot" --output /usr/local/bin/polkadot --location
|
||||
//! polkadot --dev --tmp
|
||||
//! ```
|
||||
|
||||
use codec::Decode;
|
||||
use subxt::{
|
||||
rpc::Rpc,
|
||||
storage::{
|
||||
StorageClient,
|
||||
StorageKeyPrefix,
|
||||
},
|
||||
ClientBuilder,
|
||||
DefaultConfig,
|
||||
PolkadotExtrinsicParams,
|
||||
StorageEntryKey,
|
||||
StorageMapKey,
|
||||
};
|
||||
|
||||
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata.scale")]
|
||||
pub mod polkadot {}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let api = ClientBuilder::new()
|
||||
.build()
|
||||
.await?
|
||||
.to_runtime_api::<polkadot::RuntimeApi<DefaultConfig, PolkadotExtrinsicParams<DefaultConfig>>>();
|
||||
|
||||
// Obtain the storage client wrapper from the API.
|
||||
let storage: StorageClient<_> = api.client.storage();
|
||||
|
||||
// The VersionNotifiers type of the XcmPallet is defined as:
|
||||
//
|
||||
// ```
|
||||
// All locations that we have requested version notifications from.
|
||||
// #[pallet::storage]
|
||||
// pub(super) type VersionNotifiers<T: Config> = StorageDoubleMap<
|
||||
// _,
|
||||
// Twox64Concat,
|
||||
// XcmVersion,
|
||||
// Blake2_128Concat,
|
||||
// VersionedMultiLocation,
|
||||
// QueryId,
|
||||
// OptionQuery,
|
||||
// >;
|
||||
// ```
|
||||
|
||||
// Example 1. Iterate over fetched keys manually.
|
||||
{
|
||||
// Fetch at most 10 keys from below the prefix XcmPallet' VersionNotifiers.
|
||||
let keys = storage
|
||||
.fetch_keys::<polkadot::xcm_pallet::storage::VersionNotifiers>(10, None, None)
|
||||
.await?;
|
||||
|
||||
println!("Example 1. Obtained keys:");
|
||||
for key in keys.iter() {
|
||||
println!("Key: 0x{}", hex::encode(&key));
|
||||
|
||||
if let Some(storage_data) = storage.fetch_raw(key.clone(), None).await? {
|
||||
// We know the return value to be `QueryId` (`u64`) from inspecting either:
|
||||
// - polkadot code
|
||||
// - polkadot.rs generated file under `version_notifiers()` fn
|
||||
// - metadata in json format
|
||||
let value = u64::decode(&mut &storage_data.0[..])?;
|
||||
println!(" Value: {}", value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example 2. Iterate over (keys, value) using the storage client.
|
||||
{
|
||||
let mut iter = storage
|
||||
.iter::<polkadot::xcm_pallet::storage::VersionNotifiers>(None)
|
||||
.await?;
|
||||
|
||||
println!("\nExample 2. Obtained keys:");
|
||||
while let Some((key, value)) = iter.next().await? {
|
||||
println!("Key: 0x{}", hex::encode(&key));
|
||||
println!(" Value: {}", value);
|
||||
}
|
||||
}
|
||||
|
||||
// Example 3. Iterate over (keys, value) using the polkadot API.
|
||||
{
|
||||
let mut iter = api
|
||||
.storage()
|
||||
.xcm_pallet()
|
||||
.version_notifiers_iter(None)
|
||||
.await?;
|
||||
|
||||
println!("\nExample 3. Obtained keys:");
|
||||
while let Some((key, value)) = iter.next().await? {
|
||||
println!("Key: 0x{}", hex::encode(&key));
|
||||
println!(" Value: {}", value);
|
||||
}
|
||||
}
|
||||
|
||||
// Example 4. Custom iteration over double maps.
|
||||
{
|
||||
// Obtain the inner RPC from the API.
|
||||
let rpc: &Rpc<_> = api.client.rpc();
|
||||
|
||||
// Obtain the prefixed `twox_128("XcmPallet") ++ twox_128("VersionNotifiers")`
|
||||
let prefix =
|
||||
StorageKeyPrefix::new::<polkadot::xcm_pallet::storage::VersionNotifiers>();
|
||||
// From the VersionNotifiers definition above, the first key is represented by
|
||||
// ```
|
||||
// Twox64Concat,
|
||||
// XcmVersion,
|
||||
// ```
|
||||
// while `XcmVersion` is `u32`.
|
||||
// Pass `2` as `XcmVersion` and concatenate the key to the prefix.
|
||||
let entry_key = StorageEntryKey::Map(vec![StorageMapKey::new(
|
||||
&2u32,
|
||||
::subxt::StorageHasher::Twox64Concat,
|
||||
)]);
|
||||
|
||||
// The final query key is:
|
||||
// `twox_128("XcmPallet") ++ twox_128("VersionNotifiers") ++ twox_64(2u32) ++ 2u32`
|
||||
let query_key = entry_key.final_key(prefix);
|
||||
println!("\nExample 4\nQuery key: 0x{}", hex::encode(&query_key));
|
||||
|
||||
let keys = rpc
|
||||
.storage_keys_paged(Some(query_key), 10, None, None)
|
||||
.await?;
|
||||
|
||||
println!("Obtained keys:");
|
||||
for key in keys.iter() {
|
||||
println!("Key: 0x{}", hex::encode(&key));
|
||||
|
||||
if let Some(storage_data) = storage.fetch_raw(key.clone(), None).await? {
|
||||
// We know the return value to be `QueryId` (`u64`) from inspecting either:
|
||||
// - polkadot code
|
||||
// - polkadot.rs generated file under `version_notifiers()` fn
|
||||
// - metadata in json format
|
||||
let value = u64::decode(&mut &storage_data.0[..])?;
|
||||
println!(" Value: {}", value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2,21 +2,20 @@
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! To run this example, a local polkadot node should be running. Example verified against polkadot 0.9.18-4542a603cc-aarch64-macos.
|
||||
//! To run this example, a local polkadot node should be running. Example verified against polkadot polkadot 0.9.25-5174e9ae75b.
|
||||
//!
|
||||
//! E.g.
|
||||
//! ```bash
|
||||
//! curl "https://github.com/paritytech/polkadot/releases/download/v0.9.18/polkadot" --output /usr/local/bin/polkadot --location
|
||||
//! curl "https://github.com/paritytech/polkadot/releases/download/v0.9.25/polkadot" --output /usr/local/bin/polkadot --location
|
||||
//! polkadot --dev --tmp
|
||||
//! ```
|
||||
|
||||
use futures::StreamExt;
|
||||
use sp_keyring::AccountKeyring;
|
||||
use subxt::{
|
||||
ClientBuilder,
|
||||
DefaultConfig,
|
||||
PairSigner,
|
||||
PolkadotExtrinsicParams,
|
||||
tx::PairSigner,
|
||||
OnlineClient,
|
||||
PolkadotConfig,
|
||||
};
|
||||
|
||||
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata.scale")]
|
||||
@@ -40,16 +39,13 @@ async fn simple_transfer() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let signer = PairSigner::new(AccountKeyring::Alice.pair());
|
||||
let dest = AccountKeyring::Bob.to_account_id().into();
|
||||
|
||||
let api = ClientBuilder::new()
|
||||
.build()
|
||||
.await?
|
||||
.to_runtime_api::<polkadot::RuntimeApi<DefaultConfig, PolkadotExtrinsicParams<_>>>();
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
|
||||
let balance_transfer_tx = polkadot::tx().balances().transfer(dest, 10_000);
|
||||
|
||||
let balance_transfer = api
|
||||
.tx()
|
||||
.balances()
|
||||
.transfer(dest, 10_000)?
|
||||
.sign_and_submit_then_watch_default(&signer)
|
||||
.sign_and_submit_then_watch_default(&balance_transfer_tx, &signer)
|
||||
.await?
|
||||
.wait_for_finalized_success()
|
||||
.await?;
|
||||
@@ -72,16 +68,13 @@ async fn simple_transfer_separate_events() -> Result<(), Box<dyn std::error::Err
|
||||
let signer = PairSigner::new(AccountKeyring::Alice.pair());
|
||||
let dest = AccountKeyring::Bob.to_account_id().into();
|
||||
|
||||
let api = ClientBuilder::new()
|
||||
.build()
|
||||
.await?
|
||||
.to_runtime_api::<polkadot::RuntimeApi<DefaultConfig, PolkadotExtrinsicParams<_>>>();
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
|
||||
let balance_transfer_tx = polkadot::tx().balances().transfer(dest, 10_000);
|
||||
|
||||
let balance_transfer = api
|
||||
.tx()
|
||||
.balances()
|
||||
.transfer(dest, 10_000)?
|
||||
.sign_and_submit_then_watch_default(&signer)
|
||||
.sign_and_submit_then_watch_default(&balance_transfer_tx, &signer)
|
||||
.await?
|
||||
.wait_for_finalized()
|
||||
.await?;
|
||||
@@ -123,21 +116,18 @@ async fn handle_transfer_events() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let signer = PairSigner::new(AccountKeyring::Alice.pair());
|
||||
let dest = AccountKeyring::Bob.to_account_id().into();
|
||||
|
||||
let api = ClientBuilder::new()
|
||||
.build()
|
||||
.await?
|
||||
.to_runtime_api::<polkadot::RuntimeApi<DefaultConfig, PolkadotExtrinsicParams<_>>>();
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
|
||||
let balance_transfer_tx = polkadot::tx().balances().transfer(dest, 10_000);
|
||||
|
||||
let mut balance_transfer_progress = api
|
||||
.tx()
|
||||
.balances()
|
||||
.transfer(dest, 10_000)?
|
||||
.sign_and_submit_then_watch_default(&signer)
|
||||
.sign_and_submit_then_watch_default(&balance_transfer_tx, &signer)
|
||||
.await?;
|
||||
|
||||
while let Some(ev) = balance_transfer_progress.next().await {
|
||||
let ev = ev?;
|
||||
use subxt::TransactionStatus::*;
|
||||
use subxt::tx::TxStatus::*;
|
||||
|
||||
// Made it into a block, but not finalized.
|
||||
if let InBlock(details) = ev {
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! To run this example, a local polkadot node should be running. Example verified against polkadot 0.9.18-4542a603cc-aarch64-macos.
|
||||
//! To run this example, a local polkadot node should be running. Example verified against polkadot polkadot 0.9.25-5174e9ae75b.
|
||||
//!
|
||||
//! E.g.
|
||||
//! ```bash
|
||||
//! curl "https://github.com/paritytech/polkadot/releases/download/v0.9.18/polkadot" --output /usr/local/bin/polkadot --location
|
||||
//! curl "https://github.com/paritytech/polkadot/releases/download/v0.9.25/polkadot" --output /usr/local/bin/polkadot --location
|
||||
//! polkadot --dev --tmp
|
||||
//! ```
|
||||
|
||||
@@ -14,10 +14,9 @@ use futures::StreamExt;
|
||||
use sp_keyring::AccountKeyring;
|
||||
use std::time::Duration;
|
||||
use subxt::{
|
||||
ClientBuilder,
|
||||
DefaultConfig,
|
||||
PairSigner,
|
||||
PolkadotExtrinsicParams,
|
||||
tx::PairSigner,
|
||||
OnlineClient,
|
||||
PolkadotConfig,
|
||||
};
|
||||
|
||||
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata.scale")]
|
||||
@@ -29,41 +28,33 @@ pub mod polkadot {}
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let api = ClientBuilder::new()
|
||||
.build()
|
||||
.await?
|
||||
.to_runtime_api::<polkadot::RuntimeApi<DefaultConfig, PolkadotExtrinsicParams<DefaultConfig>>>();
|
||||
// Create a client to use:
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
|
||||
// Subscribe to any events that occur:
|
||||
let mut event_sub = api.events().subscribe().await?;
|
||||
|
||||
// While this subscription is active, balance transfers are made somewhere:
|
||||
tokio::task::spawn(async {
|
||||
let signer = PairSigner::new(AccountKeyring::Alice.pair());
|
||||
let api =
|
||||
ClientBuilder::new()
|
||||
.build()
|
||||
.await
|
||||
.unwrap()
|
||||
.to_runtime_api::<polkadot::RuntimeApi<
|
||||
DefaultConfig,
|
||||
PolkadotExtrinsicParams<DefaultConfig>,
|
||||
>>();
|
||||
tokio::task::spawn({
|
||||
let api = api.clone();
|
||||
async move {
|
||||
let signer = PairSigner::new(AccountKeyring::Alice.pair());
|
||||
let mut transfer_amount = 1_000_000_000;
|
||||
|
||||
let mut transfer_amount = 1_000_000_000;
|
||||
// Make small balance transfers from Alice to Bob in a loop:
|
||||
loop {
|
||||
let transfer_tx = polkadot::tx().balances().transfer(
|
||||
AccountKeyring::Bob.to_account_id().into(),
|
||||
transfer_amount,
|
||||
);
|
||||
api.tx()
|
||||
.sign_and_submit_default(&transfer_tx, &signer)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Make small balance transfers from Alice to Bob in a loop:
|
||||
loop {
|
||||
api.tx()
|
||||
.balances()
|
||||
.transfer(AccountKeyring::Bob.to_account_id().into(), transfer_amount)
|
||||
.expect("compatible transfer call on runtime node")
|
||||
.sign_and_submit_default(&signer)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||
transfer_amount += 100_000_000;
|
||||
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||
transfer_amount += 100_000_000;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -72,29 +63,21 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let events = events?;
|
||||
let block_hash = events.block_hash();
|
||||
|
||||
// We can iterate, statically decoding all events if we want:
|
||||
println!("All events in block {block_hash:?}:");
|
||||
println!(" Static event details:");
|
||||
for event in events.iter() {
|
||||
let event = event?;
|
||||
println!(" {event:?}");
|
||||
}
|
||||
|
||||
// Or we can dynamically decode events:
|
||||
// We can dynamically decode events:
|
||||
println!(" Dynamic event details: {block_hash:?}:");
|
||||
for event in events.iter_raw() {
|
||||
for event in events.iter() {
|
||||
let event = event?;
|
||||
let is_balance_transfer = event
|
||||
.as_event::<polkadot::balances::events::Transfer>()?
|
||||
.is_some();
|
||||
let pallet = event.pallet;
|
||||
let variant = event.variant;
|
||||
let pallet = event.pallet_name();
|
||||
let variant = event.variant_name();
|
||||
println!(
|
||||
" {pallet}::{variant} (is balance transfer? {is_balance_transfer})"
|
||||
);
|
||||
}
|
||||
|
||||
// Or we can dynamically find the first transfer event, ignoring any others:
|
||||
// Or we can find the first transfer event, ignoring any others:
|
||||
let transfer_event =
|
||||
events.find_first::<polkadot::balances::events::Transfer>()?;
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! To run this example, a local polkadot node should be running. Example verified against polkadot 0.9.18-4542a603cc-aarch64-macos.
|
||||
//! To run this example, a local polkadot node should be running. Example verified against polkadot polkadot 0.9.25-5174e9ae75b.
|
||||
//!
|
||||
//! E.g.
|
||||
//! ```bash
|
||||
//! curl "https://github.com/paritytech/polkadot/releases/download/v0.9.18/polkadot" --output /usr/local/bin/polkadot --location
|
||||
//! curl "https://github.com/paritytech/polkadot/releases/download/v0.9.25/polkadot" --output /usr/local/bin/polkadot --location
|
||||
//! polkadot --dev --tmp
|
||||
//! ```
|
||||
|
||||
@@ -14,10 +14,9 @@ use futures::StreamExt;
|
||||
use sp_keyring::AccountKeyring;
|
||||
use std::time::Duration;
|
||||
use subxt::{
|
||||
ClientBuilder,
|
||||
DefaultConfig,
|
||||
PairSigner,
|
||||
PolkadotExtrinsicParams,
|
||||
tx::PairSigner,
|
||||
OnlineClient,
|
||||
PolkadotConfig,
|
||||
};
|
||||
|
||||
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata.scale")]
|
||||
@@ -29,11 +28,8 @@ pub mod polkadot {}
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
// Subscribe to any events that occur:
|
||||
let api = ClientBuilder::new()
|
||||
.build()
|
||||
.await?
|
||||
.to_runtime_api::<polkadot::RuntimeApi<DefaultConfig, PolkadotExtrinsicParams<DefaultConfig>>>();
|
||||
// Create a client to use:
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
|
||||
// Subscribe to just balance transfer events, making use of `filter_events`
|
||||
// to select a single event type (note the 1-tuple) to filter out and return.
|
||||
@@ -43,29 +39,27 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.await?
|
||||
.filter_events::<(polkadot::balances::events::Transfer,)>();
|
||||
|
||||
// While this subscription is active, we imagine some balance transfers are made somewhere else:
|
||||
tokio::task::spawn(async {
|
||||
let signer = PairSigner::new(AccountKeyring::Alice.pair());
|
||||
let api =
|
||||
ClientBuilder::new()
|
||||
.build()
|
||||
.await
|
||||
.unwrap()
|
||||
.to_runtime_api::<polkadot::RuntimeApi<
|
||||
DefaultConfig,
|
||||
PolkadotExtrinsicParams<DefaultConfig>,
|
||||
>>();
|
||||
// While this subscription is active, balance transfers are made somewhere:
|
||||
tokio::task::spawn({
|
||||
let api = api.clone();
|
||||
async move {
|
||||
let signer = PairSigner::new(AccountKeyring::Alice.pair());
|
||||
let mut transfer_amount = 1_000_000_000;
|
||||
|
||||
// Make small balance transfers from Alice to Bob in a loop:
|
||||
loop {
|
||||
api.tx()
|
||||
.balances()
|
||||
.transfer(AccountKeyring::Bob.to_account_id().into(), 1_000_000_000)
|
||||
.expect("compatible transfer call on runtime node")
|
||||
.sign_and_submit_default(&signer)
|
||||
.await
|
||||
.unwrap();
|
||||
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||
// Make small balance transfers from Alice to Bob in a loop:
|
||||
loop {
|
||||
let transfer_tx = polkadot::tx().balances().transfer(
|
||||
AccountKeyring::Bob.to_account_id().into(),
|
||||
transfer_amount,
|
||||
);
|
||||
api.tx()
|
||||
.sign_and_submit_default(&transfer_tx, &signer)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||
transfer_amount += 100_000_000;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -2,67 +2,38 @@
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! To run this example, a local polkadot node should be running. Example verified against polkadot 0.9.18-f6d6ab005d-aarch64-macos.
|
||||
//! To run this example, a local polkadot node should be running. Example verified against polkadot polkadot 0.9.25-5174e9ae75b.
|
||||
//!
|
||||
//! E.g.
|
||||
//! ```bash
|
||||
//! curl "https://github.com/paritytech/polkadot/releases/download/v0.9.18/polkadot" --output /usr/local/bin/polkadot --location
|
||||
//! curl "https://github.com/paritytech/polkadot/releases/download/v0.9.25/polkadot" --output /usr/local/bin/polkadot --location
|
||||
//! polkadot --dev --tmp
|
||||
//! ```
|
||||
|
||||
use sp_keyring::AccountKeyring;
|
||||
use std::time::Duration;
|
||||
use subxt::{
|
||||
ClientBuilder,
|
||||
DefaultConfig,
|
||||
PairSigner,
|
||||
PolkadotExtrinsicParams,
|
||||
OnlineClient,
|
||||
PolkadotConfig,
|
||||
};
|
||||
|
||||
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata.scale")]
|
||||
pub mod polkadot {}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let api = ClientBuilder::new()
|
||||
.build()
|
||||
.await?
|
||||
.to_runtime_api::<polkadot::RuntimeApi<DefaultConfig, PolkadotExtrinsicParams<DefaultConfig>>>();
|
||||
// Create a client to use:
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
|
||||
// Start a new tokio task to perform the runtime updates while
|
||||
// utilizing the API for other use cases.
|
||||
let update_client = api.client.updates();
|
||||
let update_client = api.subscribe_to_updates();
|
||||
tokio::spawn(async move {
|
||||
let result = update_client.perform_runtime_updates().await;
|
||||
println!("Runtime update failed with result={:?}", result);
|
||||
});
|
||||
|
||||
// Make multiple transfers to simulate a long running `subxt::Client` use-case.
|
||||
//
|
||||
// Meanwhile, the tokio task above will perform any necessary updates to keep in sync
|
||||
// with the node we've connected to. Transactions submitted in the vicinity of a runtime
|
||||
// update may still fail, however, owing to a race between the update happening and
|
||||
// subxt synchronising its internal state with it.
|
||||
let signer = PairSigner::new(AccountKeyring::Alice.pair());
|
||||
// Make small balance transfers from Alice to Bob:
|
||||
for _ in 0..10 {
|
||||
let hash = api
|
||||
.tx()
|
||||
.balances()
|
||||
.transfer(
|
||||
AccountKeyring::Bob.to_account_id().into(),
|
||||
123_456_789_012_345,
|
||||
)
|
||||
.unwrap()
|
||||
.sign_and_submit_default(&signer)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Balance transfer extrinsic submitted: {}", hash);
|
||||
tokio::time::sleep(Duration::from_secs(30)).await;
|
||||
}
|
||||
// If this client is kept in use a while, it'll update its metadata and such
|
||||
// as needed when the node it's pointed at updates.
|
||||
tokio::time::sleep(Duration::from_secs(10_000)).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! To run this example, a local polkadot node should be running. Example verified against polkadot 0.9.18-4542a603cc-aarch64-macos.
|
||||
//! To run this example, a local polkadot node should be running. Example verified against polkadot polkadot 0.9.25-5174e9ae75b.
|
||||
//!
|
||||
//! E.g.
|
||||
//! ```bash
|
||||
//! curl "https://github.com/paritytech/polkadot/releases/download/v0.9.18/polkadot" --output /usr/local/bin/polkadot --location
|
||||
//! curl "https://github.com/paritytech/polkadot/releases/download/v0.9.25/polkadot" --output /usr/local/bin/polkadot --location
|
||||
//! polkadot --dev --tmp
|
||||
//! ```
|
||||
|
||||
@@ -14,10 +14,9 @@ use futures::StreamExt;
|
||||
use sp_keyring::AccountKeyring;
|
||||
use std::time::Duration;
|
||||
use subxt::{
|
||||
ClientBuilder,
|
||||
DefaultConfig,
|
||||
PairSigner,
|
||||
PolkadotExtrinsicParams,
|
||||
tx::PairSigner,
|
||||
OnlineClient,
|
||||
PolkadotConfig,
|
||||
};
|
||||
|
||||
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata.scale")]
|
||||
@@ -29,11 +28,8 @@ pub mod polkadot {}
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
// Subscribe to any events that occur:
|
||||
let api = ClientBuilder::new()
|
||||
.build()
|
||||
.await?
|
||||
.to_runtime_api::<polkadot::RuntimeApi<DefaultConfig, PolkadotExtrinsicParams<DefaultConfig>>>();
|
||||
// Create a client to use:
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
|
||||
// Subscribe to several balance related events. If we ask for more than one event,
|
||||
// we'll be given a correpsonding tuple of `Option`'s, with exactly one
|
||||
@@ -44,29 +40,27 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
polkadot::balances::events::Deposit,
|
||||
)>();
|
||||
|
||||
// While this subscription is active, we imagine some balance transfers are made somewhere else:
|
||||
tokio::task::spawn(async {
|
||||
let signer = PairSigner::new(AccountKeyring::Alice.pair());
|
||||
let api =
|
||||
ClientBuilder::new()
|
||||
.build()
|
||||
.await
|
||||
.unwrap()
|
||||
.to_runtime_api::<polkadot::RuntimeApi<
|
||||
DefaultConfig,
|
||||
PolkadotExtrinsicParams<DefaultConfig>,
|
||||
>>();
|
||||
// While this subscription is active, balance transfers are made somewhere:
|
||||
tokio::task::spawn({
|
||||
let api = api.clone();
|
||||
async move {
|
||||
let signer = PairSigner::new(AccountKeyring::Alice.pair());
|
||||
let mut transfer_amount = 1_000_000_000;
|
||||
|
||||
// Make small balance transfers from Alice to Bob in a loop:
|
||||
loop {
|
||||
api.tx()
|
||||
.balances()
|
||||
.transfer(AccountKeyring::Bob.to_account_id().into(), 1_000_000_000)
|
||||
.expect("compatible transfer call on runtime node")
|
||||
.sign_and_submit_default(&signer)
|
||||
.await
|
||||
.unwrap();
|
||||
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||
// Make small balance transfers from Alice to Bob in a loop:
|
||||
loop {
|
||||
let transfer_tx = polkadot::tx().balances().transfer(
|
||||
AccountKeyring::Bob.to_account_id().into(),
|
||||
transfer_amount,
|
||||
);
|
||||
api.tx()
|
||||
.sign_and_submit_default(&transfer_tx, &signer)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||
transfer_amount += 100_000_000;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,214 +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 crate::{
|
||||
test_node_process,
|
||||
test_node_process_with,
|
||||
utils::{
|
||||
node_runtime::system,
|
||||
pair_signer,
|
||||
test_context,
|
||||
},
|
||||
};
|
||||
|
||||
use sp_core::{
|
||||
sr25519::Pair,
|
||||
storage::{
|
||||
well_known_keys,
|
||||
StorageKey,
|
||||
},
|
||||
Pair as _,
|
||||
};
|
||||
use sp_keyring::AccountKeyring;
|
||||
use sp_runtime::DispatchOutcome;
|
||||
use subxt::Error;
|
||||
|
||||
#[tokio::test]
|
||||
async fn insert_key() {
|
||||
let test_node_process = test_node_process_with(AccountKeyring::Bob).await;
|
||||
let client = test_node_process.client();
|
||||
let public = AccountKeyring::Alice.public().as_array_ref().to_vec();
|
||||
client
|
||||
.rpc()
|
||||
.insert_key(
|
||||
"aura".to_string(),
|
||||
"//Alice".to_string(),
|
||||
public.clone().into(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(client
|
||||
.rpc()
|
||||
.has_key(public.clone().into(), "aura".to_string())
|
||||
.await
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fetch_block_hash() {
|
||||
let node_process = test_node_process().await;
|
||||
node_process.client().rpc().block_hash(None).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fetch_block() {
|
||||
let node_process = test_node_process().await;
|
||||
let client = node_process.client();
|
||||
let block_hash = client.rpc().block_hash(None).await.unwrap();
|
||||
client.rpc().block(block_hash).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fetch_read_proof() {
|
||||
let node_process = test_node_process().await;
|
||||
let client = node_process.client();
|
||||
let block_hash = client.rpc().block_hash(None).await.unwrap();
|
||||
client
|
||||
.rpc()
|
||||
.read_proof(
|
||||
vec![
|
||||
StorageKey(well_known_keys::HEAP_PAGES.to_vec()),
|
||||
StorageKey(well_known_keys::EXTRINSIC_INDEX.to_vec()),
|
||||
],
|
||||
block_hash,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn chain_subscribe_blocks() {
|
||||
let node_process = test_node_process().await;
|
||||
let client = node_process.client();
|
||||
let mut blocks = client.rpc().subscribe_blocks().await.unwrap();
|
||||
blocks.next().await.unwrap().unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn chain_subscribe_finalized_blocks() {
|
||||
let node_process = test_node_process().await;
|
||||
let client = node_process.client();
|
||||
let mut blocks = client.rpc().subscribe_finalized_blocks().await.unwrap();
|
||||
blocks.next().await.unwrap().unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fetch_keys() {
|
||||
let node_process = test_node_process().await;
|
||||
let client = node_process.client();
|
||||
let keys = client
|
||||
.storage()
|
||||
.fetch_keys::<system::storage::Account>(4, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(keys.len(), 4)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_iter() {
|
||||
let node_process = test_node_process().await;
|
||||
let client = node_process.client();
|
||||
let mut iter = client
|
||||
.storage()
|
||||
.iter::<system::storage::Account>(None)
|
||||
.await
|
||||
.unwrap();
|
||||
let mut i = 0;
|
||||
while iter.next().await.unwrap().is_some() {
|
||||
i += 1;
|
||||
}
|
||||
assert_eq!(i, 13);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fetch_system_info() {
|
||||
let node_process = test_node_process().await;
|
||||
let client = node_process.client();
|
||||
assert_eq!(client.rpc().system_chain().await.unwrap(), "Development");
|
||||
assert_eq!(client.rpc().system_name().await.unwrap(), "Substrate Node");
|
||||
assert!(!client.rpc().system_version().await.unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dry_run_passes() {
|
||||
let node_process = test_node_process().await;
|
||||
let client = node_process.client();
|
||||
|
||||
let alice = pair_signer(AccountKeyring::Alice.pair());
|
||||
let bob = pair_signer(AccountKeyring::Bob.pair());
|
||||
let bob_address = bob.account_id().clone().into();
|
||||
let cxt = test_context().await;
|
||||
let api = &cxt.api;
|
||||
let signed_extrinsic = api
|
||||
.tx()
|
||||
.balances()
|
||||
.transfer(bob_address, 10_000)
|
||||
.unwrap()
|
||||
.create_signed(&alice, Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
client
|
||||
.rpc()
|
||||
.dry_run(signed_extrinsic.encoded(), None)
|
||||
.await
|
||||
.expect("dryrunning failed")
|
||||
.expect("expected dryrunning to be successful")
|
||||
.unwrap();
|
||||
signed_extrinsic
|
||||
.submit_and_watch()
|
||||
.await
|
||||
.unwrap()
|
||||
.wait_for_finalized_success()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dry_run_fails() {
|
||||
let node_process = test_node_process().await;
|
||||
let client = node_process.client();
|
||||
|
||||
let alice = pair_signer(AccountKeyring::Alice.pair());
|
||||
let hans = pair_signer(Pair::generate().0);
|
||||
let hans_address = hans.account_id().clone().into();
|
||||
let cxt = test_context().await;
|
||||
let api = &cxt.api;
|
||||
let signed_extrinsic = api
|
||||
.tx()
|
||||
.balances()
|
||||
.transfer(
|
||||
hans_address,
|
||||
100_000_000_000_000_000_000_000_000_000_000_000,
|
||||
)
|
||||
.unwrap()
|
||||
.create_signed(&alice, Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let dry_run_res: DispatchOutcome = client
|
||||
.rpc()
|
||||
.dry_run(signed_extrinsic.encoded(), None)
|
||||
.await
|
||||
.expect("dryrunning failed")
|
||||
.expect("expected dryrun transaction to be valid");
|
||||
if let Err(sp_runtime::DispatchError::Module(module_error)) = dry_run_res {
|
||||
assert_eq!(module_error.index, 6);
|
||||
assert_eq!(module_error.error, 2);
|
||||
} else {
|
||||
panic!("expected a module error when dryrunning");
|
||||
}
|
||||
let res = signed_extrinsic
|
||||
.submit_and_watch()
|
||||
.await
|
||||
.unwrap()
|
||||
.wait_for_finalized_success()
|
||||
.await;
|
||||
if let Err(Error::Module(err)) = res {
|
||||
assert_eq!(err.pallet, "Balances");
|
||||
assert_eq!(err.error, "InsufficientBalance");
|
||||
} else {
|
||||
panic!("expected a runtime module error");
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -22,7 +22,7 @@ integration-tests = []
|
||||
bitvec = { version = "1.0.0", default-features = false, features = ["alloc"] }
|
||||
codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive", "full", "bit-vec"] }
|
||||
scale-info = { version = "2.0.0", features = ["bit-vec"] }
|
||||
scale-value = "0.2.0"
|
||||
scale-value = "0.4.0"
|
||||
futures = "0.3.13"
|
||||
hex = "0.4.3"
|
||||
jsonrpsee = { version = "0.15.1", features = ["async-client", "client-ws-transport"] }
|
||||
@@ -42,4 +42,4 @@ frame-metadata = "15.0.0"
|
||||
derivative = "2.2.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.8", features = ["macros", "time"] }
|
||||
tokio = { version = "1.8", features = ["macros", "time", "rt-multi-thread"] }
|
||||
|
||||
@@ -1,503 +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 futures::future;
|
||||
pub use sp_runtime::traits::SignedExtension;
|
||||
use sp_runtime::{
|
||||
traits::Hash,
|
||||
ApplyExtrinsicResult,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::{
|
||||
BasicError,
|
||||
HasModuleError,
|
||||
},
|
||||
extrinsic::{
|
||||
ExtrinsicParams,
|
||||
Signer,
|
||||
},
|
||||
rpc::{
|
||||
Rpc,
|
||||
RpcClient,
|
||||
RuntimeVersion,
|
||||
SystemProperties,
|
||||
},
|
||||
storage::StorageClient,
|
||||
transaction::TransactionProgress,
|
||||
updates::UpdateClient,
|
||||
Call,
|
||||
Config,
|
||||
Encoded,
|
||||
Metadata,
|
||||
};
|
||||
use codec::{
|
||||
Compact,
|
||||
Decode,
|
||||
Encode,
|
||||
};
|
||||
use derivative::Derivative;
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// ClientBuilder for constructing a Client.
|
||||
#[derive(Default)]
|
||||
pub struct ClientBuilder {
|
||||
url: Option<String>,
|
||||
client: Option<RpcClient>,
|
||||
metadata: Option<Metadata>,
|
||||
page_size: Option<u32>,
|
||||
}
|
||||
|
||||
impl ClientBuilder {
|
||||
/// Creates a new ClientBuilder.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
url: None,
|
||||
client: None,
|
||||
metadata: None,
|
||||
page_size: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the jsonrpsee client.
|
||||
pub fn set_client<C: Into<RpcClient>>(mut self, client: C) -> Self {
|
||||
self.client = Some(client.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the substrate rpc address.
|
||||
pub fn set_url<P: Into<String>>(mut self, url: P) -> Self {
|
||||
self.url = Some(url.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the page size.
|
||||
pub fn set_page_size(mut self, size: u32) -> Self {
|
||||
self.page_size = Some(size);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the metadata.
|
||||
///
|
||||
/// *Note:* Metadata will no longer be downloaded from the runtime node.
|
||||
#[cfg(feature = "integration-tests")]
|
||||
pub fn set_metadata(mut self, metadata: Metadata) -> Self {
|
||||
self.metadata = Some(metadata);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder for [Client].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use subxt::{ClientBuilder, DefaultConfig};
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// // Build the client.
|
||||
/// let client = ClientBuilder::new()
|
||||
/// .set_url("wss://rpc.polkadot.io:443")
|
||||
/// .build::<DefaultConfig>()
|
||||
/// .await
|
||||
/// .unwrap();
|
||||
/// // Use the client...
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn build<T: Config>(self) -> Result<Client<T>, BasicError> {
|
||||
let client = if let Some(client) = self.client {
|
||||
client
|
||||
} else {
|
||||
let url = self.url.as_deref().unwrap_or("ws://127.0.0.1:9944");
|
||||
crate::rpc::ws_client(url).await?
|
||||
};
|
||||
let rpc = Rpc::new(client);
|
||||
let (genesis_hash, runtime_version, properties) = future::join3(
|
||||
rpc.genesis_hash(),
|
||||
rpc.runtime_version(None),
|
||||
rpc.system_properties(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let metadata = if let Some(metadata) = self.metadata {
|
||||
metadata
|
||||
} else {
|
||||
rpc.metadata().await?
|
||||
};
|
||||
|
||||
Ok(Client {
|
||||
rpc,
|
||||
genesis_hash: genesis_hash?,
|
||||
metadata: Arc::new(RwLock::new(metadata)),
|
||||
properties: properties.unwrap_or_else(|_| Default::default()),
|
||||
runtime_version: Arc::new(RwLock::new(runtime_version?)),
|
||||
iter_page_size: self.page_size.unwrap_or(10),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Client to interface with a substrate node.
|
||||
#[derive(Derivative)]
|
||||
#[derivative(Clone(bound = ""))]
|
||||
pub struct Client<T: Config> {
|
||||
rpc: Rpc<T>,
|
||||
genesis_hash: T::Hash,
|
||||
metadata: Arc<RwLock<Metadata>>,
|
||||
properties: SystemProperties,
|
||||
runtime_version: Arc<RwLock<RuntimeVersion>>,
|
||||
iter_page_size: u32,
|
||||
}
|
||||
|
||||
impl<T: Config> std::fmt::Debug for Client<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Client")
|
||||
.field("rpc", &"<Rpc>")
|
||||
.field("genesis_hash", &self.genesis_hash)
|
||||
.field("metadata", &"<Metadata>")
|
||||
.field("events_decoder", &"<EventsDecoder>")
|
||||
.field("properties", &self.properties)
|
||||
.field("runtime_version", &self.runtime_version)
|
||||
.field("iter_page_size", &self.iter_page_size)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Client<T> {
|
||||
/// Returns the genesis hash.
|
||||
pub fn genesis(&self) -> &T::Hash {
|
||||
&self.genesis_hash
|
||||
}
|
||||
|
||||
/// Returns the chain metadata.
|
||||
pub fn metadata(&self) -> Arc<RwLock<Metadata>> {
|
||||
Arc::clone(&self.metadata)
|
||||
}
|
||||
|
||||
/// Returns the properties defined in the chain spec as a JSON object.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// Many chains use this to define common properties such as `token_decimals` and `token_symbol`
|
||||
/// required for UIs, but this is merely a convention. It is up to the library user to
|
||||
/// deserialize the JSON into the appropriate type or otherwise extract the properties defined
|
||||
/// in the target chain's spec.
|
||||
pub fn properties(&self) -> &SystemProperties {
|
||||
&self.properties
|
||||
}
|
||||
|
||||
/// Returns the rpc client.
|
||||
pub fn rpc(&self) -> &Rpc<T> {
|
||||
&self.rpc
|
||||
}
|
||||
|
||||
/// Create a client for accessing runtime storage
|
||||
pub fn storage(&self) -> StorageClient<T> {
|
||||
StorageClient::new(&self.rpc, self.metadata(), self.iter_page_size)
|
||||
}
|
||||
|
||||
/// Create a wrapper for performing runtime updates on this client.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// The update client is intended to be used in the background for
|
||||
/// performing runtime updates, while the API is still in use.
|
||||
/// Without performing runtime updates the submitted extrinsics may fail.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use subxt::{ClientBuilder, DefaultConfig};
|
||||
/// #
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() {
|
||||
/// # let client = ClientBuilder::new()
|
||||
/// # .set_url("wss://rpc.polkadot.io:443")
|
||||
/// # .build::<DefaultConfig>()
|
||||
/// # .await
|
||||
/// # .unwrap();
|
||||
/// #
|
||||
/// let update_client = client.updates();
|
||||
/// // Spawn a new background task to handle runtime updates.
|
||||
/// tokio::spawn(async move {
|
||||
/// let result = update_client.perform_runtime_updates().await;
|
||||
/// println!("Runtime update finished with result={:?}", result);
|
||||
/// });
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn updates(&self) -> UpdateClient<T> {
|
||||
UpdateClient::new(
|
||||
self.rpc.clone(),
|
||||
self.metadata(),
|
||||
self.runtime_version.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Convert the client to a runtime api wrapper for custom runtime access.
|
||||
///
|
||||
/// The `subxt` proc macro will provide methods to submit extrinsics and read storage specific
|
||||
/// to the target runtime.
|
||||
pub fn to_runtime_api<R: From<Self>>(self) -> R {
|
||||
self.into()
|
||||
}
|
||||
|
||||
/// Returns the client's Runtime Version.
|
||||
pub fn runtime_version(&self) -> Arc<RwLock<RuntimeVersion>> {
|
||||
Arc::clone(&self.runtime_version)
|
||||
}
|
||||
}
|
||||
|
||||
/// A constructed call ready to be signed and submitted.
|
||||
pub struct SubmittableExtrinsic<'client, T: Config, X, C, E: Decode, Evs: Decode> {
|
||||
client: &'client Client<T>,
|
||||
call: C,
|
||||
marker: std::marker::PhantomData<(X, E, Evs)>,
|
||||
}
|
||||
|
||||
impl<'client, T, X, C, E, Evs> SubmittableExtrinsic<'client, T, X, C, E, Evs>
|
||||
where
|
||||
T: Config,
|
||||
X: ExtrinsicParams<T>,
|
||||
C: Call + Send + Sync,
|
||||
E: Decode + HasModuleError,
|
||||
Evs: Decode,
|
||||
{
|
||||
/// Create a new [`SubmittableExtrinsic`].
|
||||
pub fn new(client: &'client Client<T>, call: C) -> Self {
|
||||
Self {
|
||||
client,
|
||||
call,
|
||||
marker: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates and signs an extrinsic and submits it to the chain. Passes default parameters
|
||||
/// to construct the "signed extra" and "additional" payloads needed by the extrinsic.
|
||||
///
|
||||
/// Returns a [`TransactionProgress`], which can be used to track the status of the transaction
|
||||
/// and obtain details about it, once it has made it into a block.
|
||||
pub async fn sign_and_submit_then_watch_default(
|
||||
&self,
|
||||
signer: &(dyn Signer<T> + Send + Sync),
|
||||
) -> Result<TransactionProgress<'client, T, E, Evs>, BasicError>
|
||||
where
|
||||
X::OtherParams: Default,
|
||||
{
|
||||
self.sign_and_submit_then_watch(signer, Default::default())
|
||||
.await
|
||||
}
|
||||
|
||||
/// Creates and signs an extrinsic and submits it to the chain.
|
||||
///
|
||||
/// Returns a [`TransactionProgress`], which can be used to track the status of the transaction
|
||||
/// and obtain details about it, once it has made it into a block.
|
||||
pub async fn sign_and_submit_then_watch(
|
||||
&self,
|
||||
signer: &(dyn Signer<T> + Send + Sync),
|
||||
other_params: X::OtherParams,
|
||||
) -> Result<TransactionProgress<'client, T, E, Evs>, BasicError> {
|
||||
self.create_signed(signer, other_params)
|
||||
.await?
|
||||
.submit_and_watch()
|
||||
.await
|
||||
}
|
||||
|
||||
/// Creates and signs an extrinsic and submits to the chain for block inclusion. Passes
|
||||
/// default parameters to construct the "signed extra" and "additional" payloads needed
|
||||
/// by the extrinsic.
|
||||
///
|
||||
/// Returns `Ok` with the extrinsic hash if it is valid extrinsic.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// Success does not mean the extrinsic has been included in the block, just that it is valid
|
||||
/// and has been included in the transaction pool.
|
||||
pub async fn sign_and_submit_default(
|
||||
&self,
|
||||
signer: &(dyn Signer<T> + Send + Sync),
|
||||
) -> Result<T::Hash, BasicError>
|
||||
where
|
||||
X::OtherParams: Default,
|
||||
{
|
||||
self.sign_and_submit(signer, Default::default()).await
|
||||
}
|
||||
|
||||
/// Creates and signs an extrinsic and submits to the chain for block inclusion.
|
||||
///
|
||||
/// Returns `Ok` with the extrinsic hash if it is valid extrinsic.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// Success does not mean the extrinsic has been included in the block, just that it is valid
|
||||
/// and has been included in the transaction pool.
|
||||
pub async fn sign_and_submit(
|
||||
&self,
|
||||
signer: &(dyn Signer<T> + Send + Sync),
|
||||
other_params: X::OtherParams,
|
||||
) -> Result<T::Hash, BasicError> {
|
||||
self.create_signed(signer, other_params)
|
||||
.await?
|
||||
.submit()
|
||||
.await
|
||||
}
|
||||
|
||||
/// Return the SCALE encoded bytes representing the call data of the transaction.
|
||||
pub fn call_data(&self) -> Result<Vec<u8>, BasicError> {
|
||||
let mut bytes = Vec::new();
|
||||
let locked_metadata = self.client.metadata();
|
||||
let metadata = locked_metadata.read();
|
||||
let pallet = metadata.pallet(C::PALLET)?;
|
||||
bytes.push(pallet.index());
|
||||
bytes.push(pallet.call_index::<C>()?);
|
||||
self.call.encode_to(&mut bytes);
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
/// Creates a returns a raw signed extrinsic, without submitting it.
|
||||
pub async fn create_signed(
|
||||
&self,
|
||||
signer: &(dyn Signer<T> + Send + Sync),
|
||||
other_params: X::OtherParams,
|
||||
) -> Result<SignedSubmittableExtrinsic<'client, T, X, E, Evs>, BasicError> {
|
||||
// 1. Get nonce
|
||||
let account_nonce = if let Some(nonce) = signer.nonce() {
|
||||
nonce
|
||||
} else {
|
||||
self.client
|
||||
.rpc()
|
||||
.system_account_next_index(signer.account_id())
|
||||
.await?
|
||||
};
|
||||
|
||||
// 2. SCALE encode call data to bytes (pallet u8, call u8, call params).
|
||||
let call_data = Encoded(self.call_data()?);
|
||||
|
||||
// 3. Construct our custom additional/extra params.
|
||||
let additional_and_extra_params = {
|
||||
// Obtain spec version and transaction version from the runtime version of the client.
|
||||
let locked_runtime = self.client.runtime_version();
|
||||
let runtime = locked_runtime.read();
|
||||
X::new(
|
||||
runtime.spec_version,
|
||||
runtime.transaction_version,
|
||||
account_nonce,
|
||||
self.client.genesis_hash,
|
||||
other_params,
|
||||
)
|
||||
};
|
||||
|
||||
tracing::debug!(
|
||||
"additional_and_extra_params: {:?}",
|
||||
additional_and_extra_params
|
||||
);
|
||||
|
||||
// 4. Construct signature. This is compatible with the Encode impl
|
||||
// for SignedPayload (which is this payload of bytes that we'd like)
|
||||
// to sign. See:
|
||||
// https://github.com/paritytech/substrate/blob/9a6d706d8db00abb6ba183839ec98ecd9924b1f8/primitives/runtime/src/generic/unchecked_extrinsic.rs#L215)
|
||||
let signature = {
|
||||
let mut bytes = Vec::new();
|
||||
call_data.encode_to(&mut bytes);
|
||||
additional_and_extra_params.encode_extra_to(&mut bytes);
|
||||
additional_and_extra_params.encode_additional_to(&mut bytes);
|
||||
if bytes.len() > 256 {
|
||||
signer.sign(&sp_core::blake2_256(&bytes))
|
||||
} else {
|
||||
signer.sign(&bytes)
|
||||
}
|
||||
};
|
||||
|
||||
tracing::info!("xt signature: {}", hex::encode(signature.encode()));
|
||||
|
||||
// 5. Encode extrinsic, now that we have the parts we need. This is compatible
|
||||
// with the Encode impl for UncheckedExtrinsic (protocol version 4).
|
||||
let extrinsic = {
|
||||
let mut encoded_inner = Vec::new();
|
||||
// "is signed" + transaction protocol version (4)
|
||||
(0b10000000 + 4u8).encode_to(&mut encoded_inner);
|
||||
// from address for signature
|
||||
signer.address().encode_to(&mut encoded_inner);
|
||||
// the signature bytes
|
||||
signature.encode_to(&mut encoded_inner);
|
||||
// attach custom extra params
|
||||
additional_and_extra_params.encode_extra_to(&mut encoded_inner);
|
||||
// and now, call data
|
||||
call_data.encode_to(&mut encoded_inner);
|
||||
// now, prefix byte length:
|
||||
let len = Compact(
|
||||
u32::try_from(encoded_inner.len())
|
||||
.expect("extrinsic size expected to be <4GB"),
|
||||
);
|
||||
let mut encoded = Vec::new();
|
||||
len.encode_to(&mut encoded);
|
||||
encoded.extend(encoded_inner);
|
||||
encoded
|
||||
};
|
||||
|
||||
// Wrap in Encoded to ensure that any more "encode" calls leave it in the right state.
|
||||
// maybe we can just return the raw bytes..
|
||||
Ok(SignedSubmittableExtrinsic {
|
||||
client: self.client,
|
||||
encoded: Encoded(extrinsic),
|
||||
marker: self.marker,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SignedSubmittableExtrinsic<'client, T: Config, X, E: Decode, Evs: Decode> {
|
||||
client: &'client Client<T>,
|
||||
encoded: Encoded,
|
||||
marker: std::marker::PhantomData<(X, E, Evs)>,
|
||||
}
|
||||
|
||||
impl<'client, T, X, E, Evs> SignedSubmittableExtrinsic<'client, T, X, E, Evs>
|
||||
where
|
||||
T: Config,
|
||||
X: ExtrinsicParams<T>,
|
||||
E: Decode + HasModuleError,
|
||||
Evs: Decode,
|
||||
{
|
||||
/// Submits the extrinsic to the chain.
|
||||
///
|
||||
/// Returns a [`TransactionProgress`], which can be used to track the status of the transaction
|
||||
/// and obtain details about it, once it has made it into a block.
|
||||
pub async fn submit_and_watch(
|
||||
&self,
|
||||
) -> Result<TransactionProgress<'client, T, E, Evs>, BasicError> {
|
||||
// Get a hash of the extrinsic (we'll need this later).
|
||||
let ext_hash = T::Hashing::hash_of(&self.encoded);
|
||||
|
||||
// Submit and watch for transaction progress.
|
||||
let sub = self.client.rpc().watch_extrinsic(&self.encoded).await?;
|
||||
|
||||
Ok(TransactionProgress::new(sub, self.client, ext_hash))
|
||||
}
|
||||
|
||||
/// Submits the extrinsic to the chain for block inclusion.
|
||||
///
|
||||
/// Returns `Ok` with the extrinsic hash if it is valid extrinsic.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// Success does not mean the extrinsic has been included in the block, just that it is valid
|
||||
/// and has been included in the transaction pool.
|
||||
pub async fn submit(&self) -> Result<T::Hash, BasicError> {
|
||||
self.client.rpc().submit_extrinsic(&self.encoded).await
|
||||
}
|
||||
|
||||
/// Submits the extrinsic to the dry_run RPC, to test if it would succeed.
|
||||
///
|
||||
/// Returns `Ok` with an [`ApplyExtrinsicResult`], which is the result of applying of an extrinsic.
|
||||
pub async fn dry_run(
|
||||
&self,
|
||||
at: Option<T::Hash>,
|
||||
) -> Result<ApplyExtrinsicResult, BasicError> {
|
||||
self.client.rpc().dry_run(self.encoded(), at).await
|
||||
}
|
||||
|
||||
/// Returns the SCALE encoded extrinsic bytes.
|
||||
pub fn encoded(&self) -> &[u8] {
|
||||
&self.encoded.0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// 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.
|
||||
|
||||
//! This module provides two clients that can be used to work with
|
||||
//! transactions, storage and events. The [`OfflineClient`] works
|
||||
//! entirely offline and can be passed to any function that doesn't
|
||||
//! require network access. The [`OnlineClient`] requires network
|
||||
//! access.
|
||||
|
||||
mod offline_client;
|
||||
mod online_client;
|
||||
|
||||
pub use offline_client::{
|
||||
OfflineClient,
|
||||
OfflineClientT,
|
||||
};
|
||||
pub use online_client::{
|
||||
OnlineClient,
|
||||
OnlineClientT,
|
||||
};
|
||||
@@ -0,0 +1,140 @@
|
||||
// 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 crate::{
|
||||
constants::ConstantsClient,
|
||||
events::EventsClient,
|
||||
rpc::RuntimeVersion,
|
||||
storage::StorageClient,
|
||||
tx::TxClient,
|
||||
Config,
|
||||
Metadata,
|
||||
};
|
||||
use derivative::Derivative;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A trait representing a client that can perform
|
||||
/// offline-only actions.
|
||||
pub trait OfflineClientT<T: Config>: Clone + Send + Sync + 'static {
|
||||
/// Return the provided [`Metadata`].
|
||||
fn metadata(&self) -> Metadata;
|
||||
/// Return the provided genesis hash.
|
||||
fn genesis_hash(&self) -> T::Hash;
|
||||
/// Return the provided [`RuntimeVersion`].
|
||||
fn runtime_version(&self) -> RuntimeVersion;
|
||||
|
||||
/// Work with transactions.
|
||||
fn tx(&self) -> TxClient<T, Self> {
|
||||
TxClient::new(self.clone())
|
||||
}
|
||||
|
||||
/// Work with events.
|
||||
fn events(&self) -> EventsClient<T, Self> {
|
||||
EventsClient::new(self.clone())
|
||||
}
|
||||
|
||||
/// Work with storage.
|
||||
fn storage(&self) -> StorageClient<T, Self> {
|
||||
StorageClient::new(self.clone())
|
||||
}
|
||||
|
||||
/// Access constants.
|
||||
fn constants(&self) -> ConstantsClient<T, Self> {
|
||||
ConstantsClient::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// A client that is capable of performing offline-only operations.
|
||||
/// Can be constructed as long as you can populate the required fields.
|
||||
#[derive(Derivative)]
|
||||
#[derivative(Debug(bound = ""), Clone(bound = ""))]
|
||||
pub struct OfflineClient<T: Config> {
|
||||
inner: Arc<Inner<T>>,
|
||||
}
|
||||
|
||||
#[derive(Derivative)]
|
||||
#[derivative(Debug(bound = ""), Clone(bound = ""))]
|
||||
struct Inner<T: Config> {
|
||||
genesis_hash: T::Hash,
|
||||
runtime_version: RuntimeVersion,
|
||||
metadata: Metadata,
|
||||
}
|
||||
|
||||
impl<T: Config> OfflineClient<T> {
|
||||
/// Construct a new [`OfflineClient`], providing
|
||||
/// the necessary runtime and compile-time arguments.
|
||||
pub fn new(
|
||||
genesis_hash: T::Hash,
|
||||
runtime_version: RuntimeVersion,
|
||||
metadata: Metadata,
|
||||
) -> OfflineClient<T> {
|
||||
OfflineClient {
|
||||
inner: Arc::new(Inner {
|
||||
genesis_hash,
|
||||
runtime_version,
|
||||
metadata,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the genesis hash.
|
||||
pub fn genesis_hash(&self) -> T::Hash {
|
||||
self.inner.genesis_hash
|
||||
}
|
||||
|
||||
/// Return the runtime version.
|
||||
pub fn runtime_version(&self) -> RuntimeVersion {
|
||||
self.inner.runtime_version.clone()
|
||||
}
|
||||
|
||||
/// Return the [`Metadata`] used in this client.
|
||||
pub fn metadata(&self) -> Metadata {
|
||||
self.inner.metadata.clone()
|
||||
}
|
||||
|
||||
// Just a copy of the most important trait methods so that people
|
||||
// don't need to import the trait for most things:
|
||||
|
||||
/// Work with transactions.
|
||||
pub fn tx(&self) -> TxClient<T, Self> {
|
||||
<Self as OfflineClientT<T>>::tx(self)
|
||||
}
|
||||
|
||||
/// Work with events.
|
||||
pub fn events(&self) -> EventsClient<T, Self> {
|
||||
<Self as OfflineClientT<T>>::events(self)
|
||||
}
|
||||
|
||||
/// Work with storage.
|
||||
pub fn storage(&self) -> StorageClient<T, Self> {
|
||||
<Self as OfflineClientT<T>>::storage(self)
|
||||
}
|
||||
|
||||
/// Access constants.
|
||||
pub fn constants(&self) -> ConstantsClient<T, Self> {
|
||||
<Self as OfflineClientT<T>>::constants(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> OfflineClientT<T> for OfflineClient<T> {
|
||||
fn genesis_hash(&self) -> T::Hash {
|
||||
self.genesis_hash()
|
||||
}
|
||||
fn runtime_version(&self) -> RuntimeVersion {
|
||||
self.runtime_version()
|
||||
}
|
||||
fn metadata(&self) -> Metadata {
|
||||
self.metadata()
|
||||
}
|
||||
}
|
||||
|
||||
// For ergonomics; cloning a client is deliberately fairly cheap (via Arc),
|
||||
// so this allows users to pass references to a client rather than explicitly
|
||||
// cloning. This is partly for consistency with OnlineClient, which can be
|
||||
// easily converted into an OfflineClient for ergonomics.
|
||||
impl<'a, T: Config> From<&'a OfflineClient<T>> for OfflineClient<T> {
|
||||
fn from(c: &'a OfflineClient<T>) -> Self {
|
||||
c.clone()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
// 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 super::{
|
||||
OfflineClient,
|
||||
OfflineClientT,
|
||||
};
|
||||
use crate::{
|
||||
constants::ConstantsClient,
|
||||
error::Error,
|
||||
events::EventsClient,
|
||||
rpc::{
|
||||
Rpc,
|
||||
RpcClient,
|
||||
RuntimeVersion,
|
||||
},
|
||||
storage::StorageClient,
|
||||
tx::TxClient,
|
||||
Config,
|
||||
Metadata,
|
||||
};
|
||||
use derivative::Derivative;
|
||||
use futures::future;
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A trait representing a client that can perform
|
||||
/// online actions.
|
||||
pub trait OnlineClientT<T: Config>: OfflineClientT<T> {
|
||||
/// Return an RPC client that can be used to communicate with a node.
|
||||
fn rpc(&self) -> &Rpc<T>;
|
||||
}
|
||||
|
||||
/// A client that can be used to perform API calls (that is, either those
|
||||
/// requiriing an [`OfflineClientT`] or those requiring an [`OnlineClientT`]).
|
||||
#[derive(Derivative)]
|
||||
#[derivative(Clone(bound = ""))]
|
||||
pub struct OnlineClient<T: Config> {
|
||||
inner: Arc<RwLock<Inner<T>>>,
|
||||
rpc: Rpc<T>,
|
||||
}
|
||||
|
||||
#[derive(Derivative)]
|
||||
#[derivative(Debug(bound = ""))]
|
||||
struct Inner<T: Config> {
|
||||
genesis_hash: T::Hash,
|
||||
runtime_version: RuntimeVersion,
|
||||
metadata: Metadata,
|
||||
}
|
||||
|
||||
impl<T: Config> std::fmt::Debug for OnlineClient<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Client")
|
||||
.field("rpc", &"<Rpc>")
|
||||
.field("inner", &self.inner)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> OnlineClient<T> {
|
||||
/// Construct a new [`OnlineClient`] using default settings which
|
||||
/// point to a locally running node on `ws://127.0.0.1:9944`.
|
||||
pub async fn new() -> Result<OnlineClient<T>, Error> {
|
||||
let url = "ws://127.0.0.1:9944";
|
||||
OnlineClient::from_url(url).await
|
||||
}
|
||||
|
||||
/// Construct a new [`OnlineClient`], providing a URL to connect to.
|
||||
pub async fn from_url(url: impl AsRef<str>) -> Result<OnlineClient<T>, Error> {
|
||||
let client = crate::rpc::ws_client(url.as_ref()).await?;
|
||||
OnlineClient::from_rpc_client(client).await
|
||||
}
|
||||
|
||||
/// Construct a new [`OnlineClient`] by providing the underlying [`RpcClient`]
|
||||
/// to use to drive the connection.
|
||||
pub async fn from_rpc_client(
|
||||
rpc_client: impl Into<RpcClient>,
|
||||
) -> Result<OnlineClient<T>, Error> {
|
||||
let rpc = Rpc::new(rpc_client.into());
|
||||
|
||||
let (genesis_hash, runtime_version, metadata) = future::join3(
|
||||
rpc.genesis_hash(),
|
||||
rpc.runtime_version(None),
|
||||
rpc.metadata(),
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(OnlineClient {
|
||||
inner: Arc::new(RwLock::new(Inner {
|
||||
genesis_hash: genesis_hash?,
|
||||
runtime_version: runtime_version?,
|
||||
metadata: metadata?,
|
||||
})),
|
||||
rpc,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create an object which can be used to keep the runtime uptodate
|
||||
/// in a separate thread.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() {
|
||||
/// use subxt::{ OnlineClient, PolkadotConfig };
|
||||
///
|
||||
/// let client = OnlineClient::<PolkadotConfig>::new().await.unwrap();
|
||||
///
|
||||
/// let update_task = client.subscribe_to_updates();
|
||||
/// tokio::spawn(async move {
|
||||
/// update_task.perform_runtime_updates().await;
|
||||
/// });
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn subscribe_to_updates(&self) -> ClientRuntimeUpdater<T> {
|
||||
ClientRuntimeUpdater(self.clone())
|
||||
}
|
||||
|
||||
/// Return the [`Metadata`] used in this client.
|
||||
pub fn metadata(&self) -> Metadata {
|
||||
let inner = self.inner.read();
|
||||
inner.metadata.clone()
|
||||
}
|
||||
|
||||
/// Return the genesis hash.
|
||||
pub fn genesis_hash(&self) -> T::Hash {
|
||||
let inner = self.inner.read();
|
||||
inner.genesis_hash
|
||||
}
|
||||
|
||||
/// Return the runtime version.
|
||||
pub fn runtime_version(&self) -> RuntimeVersion {
|
||||
let inner = self.inner.read();
|
||||
inner.runtime_version.clone()
|
||||
}
|
||||
|
||||
/// Return an RPC client to make raw requests with.
|
||||
pub fn rpc(&self) -> &Rpc<T> {
|
||||
&self.rpc
|
||||
}
|
||||
|
||||
/// Return an offline client with the same configuration as this.
|
||||
pub fn offline(&self) -> OfflineClient<T> {
|
||||
let inner = self.inner.read();
|
||||
OfflineClient::new(
|
||||
inner.genesis_hash,
|
||||
inner.runtime_version.clone(),
|
||||
inner.metadata.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
// Just a copy of the most important trait methods so that people
|
||||
// don't need to import the trait for most things:
|
||||
|
||||
/// Work with transactions.
|
||||
pub fn tx(&self) -> TxClient<T, Self> {
|
||||
<Self as OfflineClientT<T>>::tx(self)
|
||||
}
|
||||
|
||||
/// Work with events.
|
||||
pub fn events(&self) -> EventsClient<T, Self> {
|
||||
<Self as OfflineClientT<T>>::events(self)
|
||||
}
|
||||
|
||||
/// Work with storage.
|
||||
pub fn storage(&self) -> StorageClient<T, Self> {
|
||||
<Self as OfflineClientT<T>>::storage(self)
|
||||
}
|
||||
|
||||
/// Access constants.
|
||||
pub fn constants(&self) -> ConstantsClient<T, Self> {
|
||||
<Self as OfflineClientT<T>>::constants(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> OfflineClientT<T> for OnlineClient<T> {
|
||||
fn metadata(&self) -> Metadata {
|
||||
self.metadata()
|
||||
}
|
||||
fn genesis_hash(&self) -> T::Hash {
|
||||
self.genesis_hash()
|
||||
}
|
||||
fn runtime_version(&self) -> RuntimeVersion {
|
||||
self.runtime_version()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> OnlineClientT<T> for OnlineClient<T> {
|
||||
fn rpc(&self) -> &Rpc<T> {
|
||||
&self.rpc
|
||||
}
|
||||
}
|
||||
|
||||
/// Client wrapper for performing runtime updates. See [`OnlineClient::subscribe_to_updates()`]
|
||||
/// for example usage.
|
||||
pub struct ClientRuntimeUpdater<T: Config>(OnlineClient<T>);
|
||||
|
||||
impl<T: Config> ClientRuntimeUpdater<T> {
|
||||
fn is_runtime_version_different(&self, new: &RuntimeVersion) -> bool {
|
||||
let curr = self.0.inner.read();
|
||||
&curr.runtime_version != new
|
||||
}
|
||||
|
||||
/// Performs runtime updates indefinitely unless encountering an error.
|
||||
///
|
||||
/// *Note:* This will run indefinitely until it errors, so the typical usage
|
||||
/// would be to run it in a separate background task.
|
||||
pub async fn perform_runtime_updates(&self) -> Result<(), Error> {
|
||||
// Obtain an update subscription to further detect changes in the runtime version of the node.
|
||||
let mut update_subscription = self.0.rpc.subscribe_runtime_version().await?;
|
||||
|
||||
while let Some(new_runtime_version) = update_subscription.next().await {
|
||||
// The Runtime Version obtained via subscription.
|
||||
let new_runtime_version = new_runtime_version?;
|
||||
|
||||
// Ignore this update if there is no difference.
|
||||
if !self.is_runtime_version_different(&new_runtime_version) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Fetch new metadata.
|
||||
let new_metadata = self.0.rpc.metadata().await?;
|
||||
|
||||
// Do the update.
|
||||
let mut writable = self.0.inner.write();
|
||||
writable.metadata = new_metadata;
|
||||
writable.runtime_version = new_runtime_version;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+54
-4
@@ -2,6 +2,12 @@
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! This module provides a [`Config`] type, which is used to define various
|
||||
//! types that are important in order to speak to a particular chain.
|
||||
//! [`SubstrateConfig`] provides a default set of these types suitable for the
|
||||
//! default Substrate node implementation, and [`PolkadotConfig`] for a
|
||||
//! Polkadot node.
|
||||
|
||||
use codec::{
|
||||
Codec,
|
||||
Encode,
|
||||
@@ -22,7 +28,7 @@ use sp_runtime::traits::{
|
||||
// Note: the 'static bound isn't strictly required, but currently deriving TypeInfo
|
||||
// automatically applies a 'static bound to all generic types (including this one),
|
||||
// and so until that is resolved, we'll keep the (easy to satisfy) constraint here.
|
||||
pub trait Config: Debug + 'static {
|
||||
pub trait Config: 'static {
|
||||
/// Account index (aka nonce) type. This stores the number of previous
|
||||
/// transactions associated with a sender account.
|
||||
type Index: Parameter
|
||||
@@ -74,6 +80,9 @@ pub trait Config: Debug + 'static {
|
||||
|
||||
/// Extrinsic type within blocks.
|
||||
type Extrinsic: Parameter + Extrinsic + Debug + MaybeSerializeDeserialize;
|
||||
|
||||
/// This type defines the extrinsic extra and additional parameters.
|
||||
type ExtrinsicParams: crate::tx::ExtrinsicParams<Self::Index, Self::Hash>;
|
||||
}
|
||||
|
||||
/// Parameter trait copied from `substrate::frame_support`
|
||||
@@ -83,10 +92,9 @@ impl<T> Parameter for T where T: Codec + EncodeLike + Clone + Eq + Debug {}
|
||||
/// Default set of commonly used types by Substrate runtimes.
|
||||
// Note: We only use this at the type level, so it should be impossible to
|
||||
// create an instance of it.
|
||||
#[derive(Debug)]
|
||||
pub enum DefaultConfig {}
|
||||
pub enum SubstrateConfig {}
|
||||
|
||||
impl Config for DefaultConfig {
|
||||
impl Config for SubstrateConfig {
|
||||
type Index = u32;
|
||||
type BlockNumber = u32;
|
||||
type Hash = sp_core::H256;
|
||||
@@ -97,4 +105,46 @@ impl Config for DefaultConfig {
|
||||
sp_runtime::generic::Header<Self::BlockNumber, sp_runtime::traits::BlakeTwo256>;
|
||||
type Signature = sp_runtime::MultiSignature;
|
||||
type Extrinsic = sp_runtime::OpaqueExtrinsic;
|
||||
type ExtrinsicParams = crate::tx::SubstrateExtrinsicParams<Self>;
|
||||
}
|
||||
|
||||
/// Default set of commonly used types by Polkadot nodes.
|
||||
pub type PolkadotConfig = WithExtrinsicParams<
|
||||
SubstrateConfig,
|
||||
crate::tx::PolkadotExtrinsicParams<SubstrateConfig>,
|
||||
>;
|
||||
|
||||
/// Take a type implementing [`Config`] (eg [`SubstrateConfig`]), and some type which describes the
|
||||
/// additional and extra parameters to pass to an extrinsic (see [`crate::tx::ExtrinsicParams`]),
|
||||
/// and returns a type implementing [`Config`] with those new `ExtrinsicParams`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use subxt::config::{ SubstrateConfig, WithExtrinsicParams };
|
||||
/// use subxt::tx::PolkadotExtrinsicParams;
|
||||
///
|
||||
/// // This is how PolkadotConfig is implemented:
|
||||
/// type PolkadotConfig = WithExtrinsicParams<SubstrateConfig, PolkadotExtrinsicParams<SubstrateConfig>>;
|
||||
/// ```
|
||||
pub struct WithExtrinsicParams<
|
||||
T: Config,
|
||||
E: crate::tx::ExtrinsicParams<T::Index, T::Hash>,
|
||||
> {
|
||||
_marker: std::marker::PhantomData<(T, E)>,
|
||||
}
|
||||
|
||||
impl<T: Config, E: crate::tx::ExtrinsicParams<T::Index, T::Hash>> Config
|
||||
for WithExtrinsicParams<T, E>
|
||||
{
|
||||
type Index = T::Index;
|
||||
type BlockNumber = T::BlockNumber;
|
||||
type Hash = T::Hash;
|
||||
type Hashing = T::Hashing;
|
||||
type AccountId = T::AccountId;
|
||||
type Address = T::Address;
|
||||
type Header = T::Header;
|
||||
type Signature = T::Signature;
|
||||
type Extrinsic = T::Extrinsic;
|
||||
type ExtrinsicParams = E;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// 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 crate::{
|
||||
dynamic::DecodedValue,
|
||||
metadata::DecodeWithMetadata,
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// This represents a constant address. Anything implementing this trait
|
||||
/// can be used to fetch constants.
|
||||
pub trait ConstantAddress {
|
||||
/// The target type of the value that lives at this address.
|
||||
type Target: DecodeWithMetadata;
|
||||
|
||||
/// The name of the pallet that the constant lives under.
|
||||
fn pallet_name(&self) -> &str;
|
||||
|
||||
/// The name of the constant in a given pallet.
|
||||
fn constant_name(&self) -> &str;
|
||||
|
||||
/// An optional hash which, if present, will be checked against
|
||||
/// the node metadata to confirm that the return type matches what
|
||||
/// we are expecting.
|
||||
fn validation_hash(&self) -> Option<[u8; 32]> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// This represents a statically generated constant lookup address.
|
||||
pub struct StaticConstantAddress<ReturnTy> {
|
||||
pallet_name: &'static str,
|
||||
constant_name: &'static str,
|
||||
constant_hash: Option<[u8; 32]>,
|
||||
_marker: std::marker::PhantomData<ReturnTy>,
|
||||
}
|
||||
|
||||
impl<ReturnTy> StaticConstantAddress<ReturnTy> {
|
||||
/// Create a new [`StaticConstantAddress`] that will be validated
|
||||
/// against node metadata using the hash given.
|
||||
pub fn new(
|
||||
pallet_name: &'static str,
|
||||
constant_name: &'static str,
|
||||
hash: [u8; 32],
|
||||
) -> Self {
|
||||
Self {
|
||||
pallet_name,
|
||||
constant_name,
|
||||
constant_hash: Some(hash),
|
||||
_marker: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Do not validate this constant prior to accessing it.
|
||||
pub fn unvalidated(self) -> Self {
|
||||
Self {
|
||||
pallet_name: self.pallet_name,
|
||||
constant_name: self.constant_name,
|
||||
constant_hash: None,
|
||||
_marker: self._marker,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<ReturnTy: DecodeWithMetadata> ConstantAddress for StaticConstantAddress<ReturnTy> {
|
||||
type Target = ReturnTy;
|
||||
|
||||
fn pallet_name(&self) -> &str {
|
||||
self.pallet_name
|
||||
}
|
||||
|
||||
fn constant_name(&self) -> &str {
|
||||
self.constant_name
|
||||
}
|
||||
|
||||
fn validation_hash(&self) -> Option<[u8; 32]> {
|
||||
self.constant_hash
|
||||
}
|
||||
}
|
||||
|
||||
/// This represents a dynamically generated constant address.
|
||||
pub struct DynamicConstantAddress<'a> {
|
||||
pallet_name: Cow<'a, str>,
|
||||
constant_name: Cow<'a, str>,
|
||||
}
|
||||
|
||||
/// Construct a new dynamic constant lookup.
|
||||
pub fn dynamic<'a>(
|
||||
pallet_name: impl Into<Cow<'a, str>>,
|
||||
constant_name: impl Into<Cow<'a, str>>,
|
||||
) -> DynamicConstantAddress<'a> {
|
||||
DynamicConstantAddress {
|
||||
pallet_name: pallet_name.into(),
|
||||
constant_name: constant_name.into(),
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ConstantAddress for DynamicConstantAddress<'a> {
|
||||
type Target = DecodedValue;
|
||||
|
||||
fn pallet_name(&self) -> &str {
|
||||
&self.pallet_name
|
||||
}
|
||||
|
||||
fn constant_name(&self) -> &str {
|
||||
&self.constant_name
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// 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 super::ConstantAddress;
|
||||
use crate::{
|
||||
client::OfflineClientT,
|
||||
error::Error,
|
||||
metadata::{
|
||||
DecodeWithMetadata,
|
||||
MetadataError,
|
||||
},
|
||||
Config,
|
||||
};
|
||||
use derivative::Derivative;
|
||||
|
||||
/// A client for accessing constants.
|
||||
#[derive(Derivative)]
|
||||
#[derivative(Clone(bound = "Client: Clone"))]
|
||||
pub struct ConstantsClient<T, Client> {
|
||||
client: Client,
|
||||
_marker: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T, Client> ConstantsClient<T, Client> {
|
||||
/// Create a new [`ConstantsClient`].
|
||||
pub fn new(client: Client) -> Self {
|
||||
Self {
|
||||
client,
|
||||
_marker: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config, Client: OfflineClientT<T>> ConstantsClient<T, Client> {
|
||||
/// Run the validation logic against some constant address you'd like to access. Returns `Ok(())`
|
||||
/// if the address is valid (or if it's not possible to check since the address has no validation hash).
|
||||
/// Return an error if the address was not valid or something went wrong trying to validate it (ie
|
||||
/// the pallet or constant in question do not exist at all).
|
||||
pub fn validate<Address: ConstantAddress>(
|
||||
&self,
|
||||
address: &Address,
|
||||
) -> Result<(), Error> {
|
||||
if let Some(actual_hash) = address.validation_hash() {
|
||||
let expected_hash = self
|
||||
.client
|
||||
.metadata()
|
||||
.constant_hash(address.pallet_name(), address.constant_name())?;
|
||||
if actual_hash != expected_hash {
|
||||
return Err(MetadataError::IncompatibleMetadata.into())
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Access the constant at the address given, returning the type defined by this address.
|
||||
/// This is probably used with addresses given from static codegen, although you can manually
|
||||
/// construct your own, too.
|
||||
pub fn at<Address: ConstantAddress>(
|
||||
&self,
|
||||
address: &Address,
|
||||
) -> Result<<Address::Target as DecodeWithMetadata>::Target, Error> {
|
||||
let metadata = self.client.metadata();
|
||||
|
||||
// 1. Validate constant shape if hash given:
|
||||
self.validate(address)?;
|
||||
|
||||
// 2. Attempt to decode the constant into the type given:
|
||||
let pallet = metadata.pallet(address.pallet_name())?;
|
||||
let constant = pallet.constant(address.constant_name())?;
|
||||
let value = Address::Target::decode_with_metadata(
|
||||
&mut &*constant.value,
|
||||
constant.ty.id(),
|
||||
&metadata,
|
||||
)?;
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// 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.
|
||||
|
||||
//! Types associated with accessing constants.
|
||||
|
||||
mod constant_address;
|
||||
mod constants_client;
|
||||
|
||||
pub use constant_address::{
|
||||
dynamic,
|
||||
ConstantAddress,
|
||||
DynamicConstantAddress,
|
||||
StaticConstantAddress,
|
||||
};
|
||||
pub use constants_client::ConstantsClient;
|
||||
@@ -0,0 +1,26 @@
|
||||
// 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.
|
||||
|
||||
//! This module provides the entry points to create dynamic
|
||||
//! transactions, storage and constant lookups.
|
||||
|
||||
pub use scale_value::Value;
|
||||
|
||||
/// A [`scale_value::Value`] type endowed with contextual information
|
||||
/// regarding what type was used to decode each part of it. This implements
|
||||
/// [`crate::metadata::DecodeWithMetadata`], and is used as a return type
|
||||
/// for dynamic requests.
|
||||
pub type DecodedValue = scale_value::Value<scale_value::scale::TypeId>;
|
||||
|
||||
// Submit dynamic transactions.
|
||||
pub use crate::tx::dynamic as tx;
|
||||
|
||||
// Lookup constants dynamically.
|
||||
pub use crate::constants::dynamic as constant;
|
||||
|
||||
// Lookup storage values dynamically.
|
||||
pub use crate::storage::{
|
||||
dynamic as storage,
|
||||
dynamic_root as storage_root,
|
||||
};
|
||||
+191
-89
@@ -2,27 +2,32 @@
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use crate::metadata::{
|
||||
//! Types representing the errors that can be returned.
|
||||
|
||||
use crate::metadata::Metadata;
|
||||
use codec::Decode;
|
||||
use core::fmt::Debug;
|
||||
use scale_info::TypeDef;
|
||||
use std::borrow::Cow;
|
||||
|
||||
// Re-expose the errors we use from other crates here:
|
||||
pub use crate::metadata::{
|
||||
InvalidMetadataError,
|
||||
MetadataError,
|
||||
};
|
||||
use core::fmt::Debug;
|
||||
use jsonrpsee::core::error::Error as RequestError;
|
||||
use scale_value::scale::DecodeError;
|
||||
use sp_core::crypto::SecretStringError;
|
||||
use sp_runtime::transaction_validity::TransactionValidityError;
|
||||
|
||||
/// An error that may contain some runtime error `E`
|
||||
pub type Error<E> = GenericError<RuntimeError<E>>;
|
||||
|
||||
/// An error that will never contain a runtime error.
|
||||
pub type BasicError = GenericError<std::convert::Infallible>;
|
||||
pub use jsonrpsee::core::error::Error as RequestError;
|
||||
pub use scale_value::scale::{
|
||||
DecodeError,
|
||||
EncodeError,
|
||||
};
|
||||
pub use sp_core::crypto::SecretStringError;
|
||||
pub use sp_runtime::transaction_validity::TransactionValidityError;
|
||||
|
||||
/// The underlying error enum, generic over the type held by the `Runtime`
|
||||
/// variant. Prefer to use the [`Error<E>`] and [`BasicError`] aliases over
|
||||
/// variant. Prefer to use the [`Error<E>`] and [`Error`] aliases over
|
||||
/// using this type directly.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum GenericError<E> {
|
||||
pub enum Error {
|
||||
/// Io error.
|
||||
#[error("Io error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
@@ -49,99 +54,174 @@ pub enum GenericError<E> {
|
||||
Metadata(#[from] MetadataError),
|
||||
/// Runtime error.
|
||||
#[error("Runtime error: {0:?}")]
|
||||
Runtime(E),
|
||||
/// Events decoding error.
|
||||
#[error("Events decoding error: {0}")]
|
||||
EventsDecoding(#[from] DecodeError),
|
||||
Runtime(DispatchError),
|
||||
/// Error decoding to a [`crate::dynamic::Value`].
|
||||
#[error("Error decoding into dynamic value: {0}")]
|
||||
DecodeValue(#[from] DecodeError),
|
||||
/// Error encoding from a [`crate::dynamic::Value`].
|
||||
#[error("Error encoding from dynamic value: {0}")]
|
||||
EncodeValue(#[from] EncodeError<()>),
|
||||
/// Transaction progress error.
|
||||
#[error("Transaction error: {0}")]
|
||||
Transaction(#[from] TransactionError),
|
||||
#[error("Module error: {0}")]
|
||||
/// An error from the `Module` variant of the generated `DispatchError`.
|
||||
Module(ModuleError),
|
||||
/// An error encoding a storage address.
|
||||
#[error("Error encoding storage address: {0}")]
|
||||
StorageAddress(#[from] StorageAddressError),
|
||||
/// Other error.
|
||||
#[error("Other error: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl<E> GenericError<E> {
|
||||
/// [`GenericError`] is parameterised over the type that it holds in the `Runtime`
|
||||
/// variant. This function allows us to map the Runtime error contained within (if present)
|
||||
/// to a different type.
|
||||
pub fn map_runtime_err<F, NewE>(self, f: F) -> GenericError<NewE>
|
||||
where
|
||||
F: FnOnce(E) -> NewE,
|
||||
{
|
||||
match self {
|
||||
GenericError::Io(e) => GenericError::Io(e),
|
||||
GenericError::Codec(e) => GenericError::Codec(e),
|
||||
GenericError::Rpc(e) => GenericError::Rpc(e),
|
||||
GenericError::Serialization(e) => GenericError::Serialization(e),
|
||||
GenericError::SecretString(e) => GenericError::SecretString(e),
|
||||
GenericError::Invalid(e) => GenericError::Invalid(e),
|
||||
GenericError::InvalidMetadata(e) => GenericError::InvalidMetadata(e),
|
||||
GenericError::Metadata(e) => GenericError::Metadata(e),
|
||||
GenericError::EventsDecoding(e) => GenericError::EventsDecoding(e),
|
||||
GenericError::Transaction(e) => GenericError::Transaction(e),
|
||||
GenericError::Module(e) => GenericError::Module(e),
|
||||
GenericError::Other(e) => GenericError::Other(e),
|
||||
// This is the only branch we really care about:
|
||||
GenericError::Runtime(e) => GenericError::Runtime(f(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BasicError {
|
||||
/// Convert an [`BasicError`] into any
|
||||
/// arbitrary [`Error<E>`].
|
||||
pub fn into_error<E>(self) -> Error<E> {
|
||||
self.map_runtime_err(|e| match e {})
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> From<BasicError> for Error<E> {
|
||||
fn from(err: BasicError) -> Self {
|
||||
err.into_error()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> From<SecretStringError> for GenericError<E> {
|
||||
impl From<SecretStringError> for Error {
|
||||
fn from(error: SecretStringError) -> Self {
|
||||
GenericError::SecretString(error)
|
||||
Error::SecretString(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> From<TransactionValidityError> for GenericError<E> {
|
||||
impl From<TransactionValidityError> for Error {
|
||||
fn from(error: TransactionValidityError) -> Self {
|
||||
GenericError::Invalid(error)
|
||||
Error::Invalid(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> From<&str> for GenericError<E> {
|
||||
impl From<&str> for Error {
|
||||
fn from(error: &str) -> Self {
|
||||
GenericError::Other(error.into())
|
||||
Error::Other(error.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> From<String> for GenericError<E> {
|
||||
impl From<String> for Error {
|
||||
fn from(error: String) -> Self {
|
||||
GenericError::Other(error)
|
||||
Error::Other(error)
|
||||
}
|
||||
}
|
||||
|
||||
/// This is used in the place of the `E` in [`GenericError<E>`] when we may have a
|
||||
/// Runtime Error. We use this wrapper so that it is possible to implement
|
||||
/// `From<Error<Infallible>` for `Error<RuntimeError<E>>`.
|
||||
///
|
||||
/// This should not be used as a type; prefer to use the alias [`Error<E>`] when referring
|
||||
/// to errors which may contain some Runtime error `E`.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct RuntimeError<E>(pub E);
|
||||
impl From<DispatchError> for Error {
|
||||
fn from(error: DispatchError) -> Self {
|
||||
Error::Runtime(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> RuntimeError<E> {
|
||||
/// Extract the actual runtime error from this struct.
|
||||
pub fn inner(self) -> E {
|
||||
self.0
|
||||
/// This is our attempt to decode a runtime DispatchError. We either
|
||||
/// successfully decode it into a [`ModuleError`], or we fail and keep
|
||||
/// hold of the bytes, which we can attempt to decode if we have an
|
||||
/// appropriate static type to hand.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum DispatchError {
|
||||
/// An error was emitted from a specific pallet/module.
|
||||
#[error("Module error: {0}")]
|
||||
Module(ModuleError),
|
||||
/// Some other error was emitted.
|
||||
#[error("Undecoded dispatch error: {0:?}")]
|
||||
Other(Vec<u8>),
|
||||
}
|
||||
|
||||
impl DispatchError {
|
||||
/// Attempt to decode a runtime DispatchError, returning either the [`ModuleError`] it decodes
|
||||
/// to, along with additional details on the error, or returning the raw bytes if it could not
|
||||
/// be decoded.
|
||||
pub fn decode_from<'a>(bytes: impl Into<Cow<'a, [u8]>>, metadata: &Metadata) -> Self {
|
||||
let bytes = bytes.into();
|
||||
|
||||
let dispatch_error_ty_id = match metadata.dispatch_error_ty() {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
tracing::warn!(
|
||||
"Can't decode error: sp_runtime::DispatchError was not found in Metadata"
|
||||
);
|
||||
return DispatchError::Other(bytes.into_owned())
|
||||
}
|
||||
};
|
||||
|
||||
let dispatch_error_ty = match metadata.types().resolve(dispatch_error_ty_id) {
|
||||
Some(ty) => ty,
|
||||
None => {
|
||||
tracing::warn!("Can't decode error: sp_runtime::DispatchError type ID doesn't resolve to a known type");
|
||||
return DispatchError::Other(bytes.into_owned())
|
||||
}
|
||||
};
|
||||
|
||||
let variant = match dispatch_error_ty.type_def() {
|
||||
TypeDef::Variant(var) => var,
|
||||
_ => {
|
||||
tracing::warn!(
|
||||
"Can't decode error: sp_runtime::DispatchError type is not a Variant"
|
||||
);
|
||||
return DispatchError::Other(bytes.into_owned())
|
||||
}
|
||||
};
|
||||
|
||||
let module_variant_idx = variant
|
||||
.variants()
|
||||
.iter()
|
||||
.find(|v| v.name() == "Module")
|
||||
.map(|v| v.index());
|
||||
let module_variant_idx = match module_variant_idx {
|
||||
Some(idx) => idx,
|
||||
None => {
|
||||
tracing::warn!("Can't decode error: sp_runtime::DispatchError does not have a 'Module' variant");
|
||||
return DispatchError::Other(bytes.into_owned())
|
||||
}
|
||||
};
|
||||
|
||||
// If the error bytes don't correspond to a ModuleError, just return the bytes.
|
||||
// This is perfectly reasonable and expected, so no logging.
|
||||
if bytes[0] != module_variant_idx {
|
||||
return DispatchError::Other(bytes.into_owned())
|
||||
}
|
||||
|
||||
// The remaining bytes are the module error, all being well:
|
||||
let bytes = &bytes[1..];
|
||||
|
||||
// The oldest and second oldest type of error decode to this shape:
|
||||
#[derive(Decode)]
|
||||
struct LegacyModuleError {
|
||||
index: u8,
|
||||
error: u8,
|
||||
}
|
||||
|
||||
// The newer case expands the error for forward compat:
|
||||
#[derive(Decode)]
|
||||
struct CurrentModuleError {
|
||||
index: u8,
|
||||
error: [u8; 4],
|
||||
}
|
||||
|
||||
// try to decode into the new shape, or the old if that doesn't work
|
||||
let err = match CurrentModuleError::decode(&mut &*bytes) {
|
||||
Ok(e) => e,
|
||||
Err(_) => {
|
||||
let old_e = match LegacyModuleError::decode(&mut &*bytes) {
|
||||
Ok(err) => err,
|
||||
Err(_) => {
|
||||
tracing::warn!("Can't decode error: sp_runtime::DispatchError does not match known formats");
|
||||
return DispatchError::Other(bytes.to_vec())
|
||||
}
|
||||
};
|
||||
CurrentModuleError {
|
||||
index: old_e.index,
|
||||
error: [old_e.error, 0, 0, 0],
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let error_details = match metadata.error(err.index, err.error[0]) {
|
||||
Ok(details) => details,
|
||||
Err(_) => {
|
||||
tracing::warn!("Can't decode error: sp_runtime::DispatchError::Module details do not match known information");
|
||||
return DispatchError::Other(bytes.to_vec())
|
||||
}
|
||||
};
|
||||
|
||||
DispatchError::Module(ModuleError {
|
||||
pallet: error_details.pallet().to_string(),
|
||||
error: error_details.error().to_string(),
|
||||
description: error_details.docs().to_vec(),
|
||||
error_data: ModuleErrorData {
|
||||
pallet_index: err.index,
|
||||
error: err.error,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,11 +272,33 @@ impl ModuleErrorData {
|
||||
}
|
||||
}
|
||||
|
||||
/// This trait is automatically implemented for the generated `DispatchError`,
|
||||
/// so that we can pluck out information about the `Module` error variant, if`
|
||||
/// it exists.
|
||||
pub trait HasModuleError {
|
||||
/// If the error has a `Module` variant, return a tuple of the
|
||||
/// pallet index and error index. Else, return `None`.
|
||||
fn module_error_data(&self) -> Option<ModuleErrorData>;
|
||||
/// Something went wrong trying to encode a storage address.
|
||||
#[derive(Clone, Debug, thiserror::Error)]
|
||||
pub enum StorageAddressError {
|
||||
/// Storage map type must be a composite type.
|
||||
#[error("Storage map type must be a composite type")]
|
||||
MapTypeMustBeTuple,
|
||||
/// Storage lookup does not have the expected number of keys.
|
||||
#[error("Storage lookup requires {expected} keys but got {actual} keys")]
|
||||
WrongNumberOfKeys {
|
||||
/// The actual number of keys needed, based on the metadata.
|
||||
actual: usize,
|
||||
/// The number of keys provided in the storage address.
|
||||
expected: usize,
|
||||
},
|
||||
/// Storage lookup requires a type that wasn't found in the metadata.
|
||||
#[error(
|
||||
"Storage lookup requires type {0} to exist in the metadata, but it was not found"
|
||||
)]
|
||||
TypeNotFound(u32),
|
||||
/// This storage entry in the metadata does not have the correct number of hashers to fields.
|
||||
#[error(
|
||||
"Storage entry in metadata does not have the correct number of hashers to fields"
|
||||
)]
|
||||
WrongNumberOfHashers {
|
||||
/// The number of hashers in the metadata for this storage entry.
|
||||
hashers: usize,
|
||||
/// The number of fields in the metadata for this storage entry.
|
||||
fields: usize,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -5,18 +5,14 @@
|
||||
//! Subscribing to events.
|
||||
|
||||
use crate::{
|
||||
error::BasicError,
|
||||
Client,
|
||||
client::OnlineClientT,
|
||||
error::Error,
|
||||
events::EventsClient,
|
||||
Config,
|
||||
};
|
||||
use codec::Decode;
|
||||
use derivative::Derivative;
|
||||
use futures::{
|
||||
future::Either,
|
||||
stream::{
|
||||
self,
|
||||
BoxStream,
|
||||
},
|
||||
stream::BoxStream,
|
||||
Future,
|
||||
FutureExt,
|
||||
Stream,
|
||||
@@ -30,112 +26,16 @@ use std::{
|
||||
};
|
||||
|
||||
pub use super::{
|
||||
at,
|
||||
EventDetails,
|
||||
EventFilter,
|
||||
Events,
|
||||
FilterEvents,
|
||||
RawEventDetails,
|
||||
};
|
||||
|
||||
/// Subscribe to events from blocks.
|
||||
///
|
||||
/// **Note:** these blocks haven't necessarily been finalised yet; prefer
|
||||
/// [`Events::subscribe_finalized()`] if that is important.
|
||||
///
|
||||
/// **Note:** This function is hidden from the documentation
|
||||
/// and is exposed only to be called via the codegen. It may
|
||||
/// break between minor releases.
|
||||
#[doc(hidden)]
|
||||
pub async fn subscribe<T: Config, Evs: Decode + 'static>(
|
||||
client: &Client<T>,
|
||||
) -> Result<EventSubscription<EventSub<T::Header>, T, Evs>, BasicError> {
|
||||
let block_subscription = client.rpc().subscribe_blocks().await?;
|
||||
Ok(EventSubscription::new(client, block_subscription))
|
||||
}
|
||||
|
||||
/// Subscribe to events from finalized blocks.
|
||||
///
|
||||
/// **Note:** This function is hidden from the documentation
|
||||
/// and is exposed only to be called via the codegen. It may
|
||||
/// break between minor releases.
|
||||
#[doc(hidden)]
|
||||
pub async fn subscribe_finalized<T: Config, Evs: Decode + 'static>(
|
||||
client: &Client<T>,
|
||||
) -> Result<EventSubscription<FinalizedEventSub<T::Header>, T, Evs>, BasicError> {
|
||||
// fetch the last finalised block details immediately, so that we'll get
|
||||
// events for each block after this one.
|
||||
let last_finalized_block_hash = client.rpc().finalized_head().await?;
|
||||
let last_finalized_block_number = client
|
||||
.rpc()
|
||||
.header(Some(last_finalized_block_hash))
|
||||
.await?
|
||||
.map(|h| (*h.number()).into());
|
||||
|
||||
// Fill in any gaps between the block above and the finalized blocks reported.
|
||||
let block_subscription = subscribe_to_block_headers_filling_in_gaps(
|
||||
client,
|
||||
last_finalized_block_number,
|
||||
client.rpc().subscribe_finalized_blocks().await?,
|
||||
);
|
||||
|
||||
Ok(EventSubscription::new(client, Box::pin(block_subscription)))
|
||||
}
|
||||
|
||||
/// Take a subscription that returns block headers, and if any block numbers are missed out
|
||||
/// betweem the block number provided and what's returned from the subscription, we fill in
|
||||
/// the gaps and get hold of all intermediate block headers.
|
||||
///
|
||||
/// **Note:** This is exposed so that we can run integration tests on it, but otherwise
|
||||
/// should not be used directly and may break between minor releases.
|
||||
#[doc(hidden)]
|
||||
pub fn subscribe_to_block_headers_filling_in_gaps<'a, S, E, T: Config>(
|
||||
client: &'a Client<T>,
|
||||
mut last_block_num: Option<u64>,
|
||||
sub: S,
|
||||
) -> impl Stream<Item = Result<T::Header, BasicError>> + Send + 'a
|
||||
where
|
||||
S: Stream<Item = Result<T::Header, E>> + Send + 'a,
|
||||
E: Into<BasicError> + Send + 'static,
|
||||
{
|
||||
sub.flat_map(move |s| {
|
||||
// Get the header, or return a stream containing just the error. Our EventSubscription
|
||||
// stream will return `None` as soon as it hits an error like this.
|
||||
let header = match s {
|
||||
Ok(header) => header,
|
||||
Err(e) => return Either::Left(stream::once(async { Err(e.into()) })),
|
||||
};
|
||||
|
||||
// We want all previous details up to, but not including this current block num.
|
||||
let end_block_num = (*header.number()).into();
|
||||
|
||||
// This is one after the last block we returned details for last time.
|
||||
let start_block_num = last_block_num.map(|n| n + 1).unwrap_or(end_block_num);
|
||||
|
||||
// Iterate over all of the previous blocks we need headers for, ignoring the current block
|
||||
// (which we already have the header info for):
|
||||
let previous_headers = stream::iter(start_block_num..end_block_num)
|
||||
.then(move |n| {
|
||||
async move {
|
||||
let hash = client.rpc().block_hash(Some(n.into())).await?;
|
||||
let header = client.rpc().header(hash).await?;
|
||||
Ok::<_, BasicError>(header)
|
||||
}
|
||||
})
|
||||
.filter_map(|h| async { h.transpose() });
|
||||
|
||||
// On the next iteration, we'll get details starting just after this end block.
|
||||
last_block_num = Some(end_block_num);
|
||||
|
||||
// Return a combination of any previous headers plus the new header.
|
||||
Either::Right(previous_headers.chain(stream::once(async { Ok(header) })))
|
||||
})
|
||||
}
|
||||
|
||||
/// A `jsonrpsee` Subscription. This forms a part of the `EventSubscription` type handed back
|
||||
/// in codegen from `subscribe_finalized`, and is exposed to be used in codegen.
|
||||
#[doc(hidden)]
|
||||
pub type FinalizedEventSub<'a, Header> = BoxStream<'a, Result<Header, BasicError>>;
|
||||
pub type FinalizedEventSub<Header> = BoxStream<'static, Result<Header, Error>>;
|
||||
|
||||
/// A `jsonrpsee` Subscription. This forms a part of the `EventSubscription` type handed back
|
||||
/// in codegen from `subscribe`, and is exposed to be used in codegen.
|
||||
@@ -144,52 +44,80 @@ pub type EventSub<Item> = Subscription<Item>;
|
||||
|
||||
/// A subscription to events that implements [`Stream`], and returns [`Events`] objects for each block.
|
||||
#[derive(Derivative)]
|
||||
#[derivative(Debug(bound = "Sub: std::fmt::Debug"))]
|
||||
pub struct EventSubscription<'a, Sub, T: Config, Evs: 'static> {
|
||||
#[derivative(Debug(bound = "Sub: std::fmt::Debug, Client: std::fmt::Debug"))]
|
||||
pub struct EventSubscription<T: Config, Client, Sub> {
|
||||
finished: bool,
|
||||
client: &'a Client<T>,
|
||||
client: Client,
|
||||
block_header_subscription: Sub,
|
||||
#[derivative(Debug = "ignore")]
|
||||
at: Option<
|
||||
std::pin::Pin<
|
||||
Box<dyn Future<Output = Result<Events<T, Evs>, BasicError>> + Send + 'a>,
|
||||
>,
|
||||
>,
|
||||
_event_type: std::marker::PhantomData<Evs>,
|
||||
at: Option<std::pin::Pin<Box<dyn Future<Output = Result<Events<T>, Error>> + Send>>>,
|
||||
}
|
||||
|
||||
impl<'a, Sub, T: Config, Evs: Decode, E: Into<BasicError>>
|
||||
EventSubscription<'a, Sub, T, Evs>
|
||||
impl<T: Config, Client, Sub, E: Into<Error>> EventSubscription<T, Client, Sub>
|
||||
where
|
||||
Sub: Stream<Item = Result<T::Header, E>> + Unpin + 'a,
|
||||
Sub: Stream<Item = Result<T::Header, E>> + Unpin,
|
||||
{
|
||||
fn new(client: &'a Client<T>, block_header_subscription: Sub) -> Self {
|
||||
/// Create a new [`EventSubscription`] from a client and a subscription
|
||||
/// which returns block headers.
|
||||
pub fn new(client: Client, block_header_subscription: Sub) -> Self {
|
||||
EventSubscription {
|
||||
finished: false,
|
||||
client,
|
||||
block_header_subscription,
|
||||
at: None,
|
||||
_event_type: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return only specific events matching the tuple of 1 or more event
|
||||
/// types that has been provided as the `Filter` type parameter.
|
||||
pub fn filter_events<Filter: EventFilter>(self) -> FilterEvents<'a, Self, T, Filter> {
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use futures::StreamExt;
|
||||
/// use subxt::{OnlineClient, PolkadotConfig};
|
||||
///
|
||||
/// #[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata.scale")]
|
||||
/// pub mod polkadot {}
|
||||
///
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() {
|
||||
/// let api = OnlineClient::<PolkadotConfig>::new().await.unwrap();
|
||||
///
|
||||
/// let mut events = api
|
||||
/// .events()
|
||||
/// .subscribe()
|
||||
/// .await
|
||||
/// .unwrap()
|
||||
/// .filter_events::<(
|
||||
/// polkadot::balances::events::Transfer,
|
||||
/// polkadot::balances::events::Deposit
|
||||
/// )>();
|
||||
///
|
||||
/// while let Some(ev) = events.next().await {
|
||||
/// let event_details = ev.unwrap();
|
||||
/// match event_details.event {
|
||||
/// (Some(transfer), None) => println!("Balance transfer event: {transfer:?}"),
|
||||
/// (None, Some(deposit)) => println!("Balance deposit event: {deposit:?}"),
|
||||
/// _ => unreachable!()
|
||||
/// }
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn filter_events<Filter: EventFilter>(
|
||||
self,
|
||||
) -> FilterEvents<'static, Self, T, Filter> {
|
||||
FilterEvents::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Config, Sub: Unpin, Evs: Decode> Unpin
|
||||
for EventSubscription<'a, Sub, T, Evs>
|
||||
{
|
||||
}
|
||||
impl<T: Config, Client, Sub: Unpin> Unpin for EventSubscription<T, Client, Sub> {}
|
||||
|
||||
// We want `EventSubscription` to implement Stream. The below implementation is the rather verbose
|
||||
// way to roughly implement the following function:
|
||||
//
|
||||
// ```
|
||||
// fn subscribe_events<T: Config, Evs: Decode>(client: &'_ Client<T>, block_sub: Subscription<T::Header>) -> impl Stream<Item=Result<Events<'_, T, Evs>, BasicError>> + '_ {
|
||||
// fn subscribe_events<T: Config, Evs: Decode>(client: &'_ Client<T>, block_sub: Subscription<T::Header>) -> impl Stream<Item=Result<Events<'_, T, Evs>, Error>> + '_ {
|
||||
// use futures::StreamExt;
|
||||
// block_sub.then(move |block_header_res| async move {
|
||||
// use sp_runtime::traits::Header;
|
||||
@@ -202,14 +130,14 @@ impl<'a, T: Config, Sub: Unpin, Evs: Decode> Unpin
|
||||
//
|
||||
// The advantage of this manual implementation is that we have a named type that we (and others)
|
||||
// can derive things on, store away, alias etc.
|
||||
impl<'a, Sub, T, Evs, E> Stream for EventSubscription<'a, Sub, T, Evs>
|
||||
impl<T, Client, Sub, E> Stream for EventSubscription<T, Client, Sub>
|
||||
where
|
||||
T: Config,
|
||||
Evs: Decode,
|
||||
Sub: Stream<Item = Result<T::Header, E>> + Unpin + 'a,
|
||||
E: Into<BasicError>,
|
||||
Client: OnlineClientT<T>,
|
||||
Sub: Stream<Item = Result<T::Header, E>> + Unpin,
|
||||
E: Into<Error>,
|
||||
{
|
||||
type Item = Result<Events<T, Evs>, BasicError>;
|
||||
type Item = Result<Events<T>, Error>;
|
||||
|
||||
fn poll_next(
|
||||
mut self: std::pin::Pin<&mut Self>,
|
||||
@@ -235,7 +163,9 @@ where
|
||||
Some(Ok(block_header)) => {
|
||||
// Note [jsdw]: We may be able to get rid of the per-item allocation
|
||||
// with https://github.com/oblique/reusable-box-future.
|
||||
self.at = Some(Box::pin(at(self.client, block_header.hash())));
|
||||
let at = EventsClient::new(self.client.clone())
|
||||
.at(Some(block_header.hash()));
|
||||
self.at = Some(Box::pin(at));
|
||||
// Continue, so that we poll this function future we've just created.
|
||||
}
|
||||
}
|
||||
@@ -263,16 +193,16 @@ mod test {
|
||||
fn assert_send<T: Send>() {}
|
||||
assert_send::<
|
||||
EventSubscription<
|
||||
EventSub<<crate::DefaultConfig as Config>::Header>,
|
||||
crate::DefaultConfig,
|
||||
crate::SubstrateConfig,
|
||||
(),
|
||||
EventSub<<crate::SubstrateConfig as Config>::Header>,
|
||||
>,
|
||||
>();
|
||||
assert_send::<
|
||||
EventSubscription<
|
||||
FinalizedEventSub<<crate::DefaultConfig as Config>::Header>,
|
||||
crate::DefaultConfig,
|
||||
crate::SubstrateConfig,
|
||||
(),
|
||||
FinalizedEventSub<<crate::SubstrateConfig as Config>::Header>,
|
||||
>,
|
||||
>();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
// 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 crate::{
|
||||
client::OnlineClientT,
|
||||
error::Error,
|
||||
events::{
|
||||
EventSub,
|
||||
EventSubscription,
|
||||
Events,
|
||||
FinalizedEventSub,
|
||||
},
|
||||
Config,
|
||||
};
|
||||
use derivative::Derivative;
|
||||
use futures::{
|
||||
future::Either,
|
||||
stream,
|
||||
Stream,
|
||||
StreamExt,
|
||||
};
|
||||
use sp_core::{
|
||||
storage::StorageKey,
|
||||
twox_128,
|
||||
};
|
||||
use sp_runtime::traits::Header;
|
||||
use std::future::Future;
|
||||
|
||||
/// A client for working with events.
|
||||
#[derive(Derivative)]
|
||||
#[derivative(Clone(bound = "Client: Clone"))]
|
||||
pub struct EventsClient<T, Client> {
|
||||
client: Client,
|
||||
_marker: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T, Client> EventsClient<T, Client> {
|
||||
/// Create a new [`EventsClient`].
|
||||
pub fn new(client: Client) -> Self {
|
||||
Self {
|
||||
client,
|
||||
_marker: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Client> EventsClient<T, Client>
|
||||
where
|
||||
T: Config,
|
||||
Client: OnlineClientT<T>,
|
||||
{
|
||||
/// Obtain events at some block hash.
|
||||
pub fn at(
|
||||
&self,
|
||||
block_hash: Option<T::Hash>,
|
||||
) -> impl Future<Output = Result<Events<T>, Error>> + Send + 'static {
|
||||
// Clone and pass the client in like this so that we can explicitly
|
||||
// return a Future that's Send + 'static, rather than tied to &self.
|
||||
let client = self.client.clone();
|
||||
async move { at(client, block_hash).await }
|
||||
}
|
||||
|
||||
/// Subscribe to all events from blocks.
|
||||
///
|
||||
/// **Note:** these blocks haven't necessarily been finalised yet; prefer
|
||||
/// [`EventsClient::subscribe_finalized()`] if that is important.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() {
|
||||
/// use futures::StreamExt;
|
||||
/// use subxt::{ OnlineClient, PolkadotConfig };
|
||||
///
|
||||
/// let api = OnlineClient::<PolkadotConfig>::new().await.unwrap();
|
||||
///
|
||||
/// let mut events = api.events().subscribe().await.unwrap();
|
||||
///
|
||||
/// while let Some(ev) = events.next().await {
|
||||
/// // Obtain all events from this block.
|
||||
/// let ev = ev.unwrap();
|
||||
/// // Print block hash.
|
||||
/// println!("Event at block hash {:?}", ev.block_hash());
|
||||
/// // Iterate over all events.
|
||||
/// let mut iter = ev.iter();
|
||||
/// while let Some(event_details) = iter.next() {
|
||||
/// println!("Event details {:?}", event_details);
|
||||
/// }
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn subscribe(
|
||||
&self,
|
||||
) -> impl Future<
|
||||
Output = Result<EventSubscription<T, Client, EventSub<T::Header>>, Error>,
|
||||
> + Send
|
||||
+ 'static {
|
||||
let client = self.client.clone();
|
||||
async move { subscribe(client).await }
|
||||
}
|
||||
|
||||
/// Subscribe to events from finalized blocks. See [`EventsClient::subscribe()`] for details.
|
||||
pub fn subscribe_finalized(
|
||||
&self,
|
||||
) -> impl Future<
|
||||
Output = Result<
|
||||
EventSubscription<T, Client, FinalizedEventSub<T::Header>>,
|
||||
Error,
|
||||
>,
|
||||
> + Send
|
||||
+ 'static
|
||||
where
|
||||
Client: Send + Sync + 'static,
|
||||
{
|
||||
let client = self.client.clone();
|
||||
async move { subscribe_finalized(client).await }
|
||||
}
|
||||
}
|
||||
|
||||
async fn at<T, Client>(
|
||||
client: Client,
|
||||
block_hash: Option<T::Hash>,
|
||||
) -> Result<Events<T>, Error>
|
||||
where
|
||||
T: Config,
|
||||
Client: OnlineClientT<T>,
|
||||
{
|
||||
// If block hash is not provided, get the hash
|
||||
// for the latest block and use that.
|
||||
let block_hash = match block_hash {
|
||||
Some(hash) => hash,
|
||||
None => {
|
||||
client
|
||||
.rpc()
|
||||
.block_hash(None)
|
||||
.await?
|
||||
.expect("didn't pass a block number; qed")
|
||||
}
|
||||
};
|
||||
|
||||
let event_bytes = client
|
||||
.rpc()
|
||||
.storage(&*system_events_key().0, Some(block_hash))
|
||||
.await?
|
||||
.map(|e| e.0)
|
||||
.unwrap_or_else(Vec::new);
|
||||
|
||||
Ok(Events::new(client.metadata(), block_hash, event_bytes))
|
||||
}
|
||||
|
||||
async fn subscribe<T, Client>(
|
||||
client: Client,
|
||||
) -> Result<EventSubscription<T, Client, EventSub<T::Header>>, Error>
|
||||
where
|
||||
T: Config,
|
||||
Client: OnlineClientT<T>,
|
||||
{
|
||||
let block_subscription = client.rpc().subscribe_blocks().await?;
|
||||
Ok(EventSubscription::new(client, block_subscription))
|
||||
}
|
||||
|
||||
/// Subscribe to events from finalized blocks.
|
||||
async fn subscribe_finalized<T, Client>(
|
||||
client: Client,
|
||||
) -> Result<EventSubscription<T, Client, FinalizedEventSub<T::Header>>, Error>
|
||||
where
|
||||
T: Config,
|
||||
Client: OnlineClientT<T>,
|
||||
{
|
||||
// fetch the last finalised block details immediately, so that we'll get
|
||||
// events for each block after this one.
|
||||
let last_finalized_block_hash = client.rpc().finalized_head().await?;
|
||||
let last_finalized_block_number = client
|
||||
.rpc()
|
||||
.header(Some(last_finalized_block_hash))
|
||||
.await?
|
||||
.map(|h| (*h.number()).into());
|
||||
|
||||
let sub = client.rpc().subscribe_finalized_blocks().await?;
|
||||
|
||||
// Fill in any gaps between the block above and the finalized blocks reported.
|
||||
let block_subscription = subscribe_to_block_headers_filling_in_gaps(
|
||||
client.clone(),
|
||||
last_finalized_block_number,
|
||||
sub,
|
||||
);
|
||||
|
||||
Ok(EventSubscription::new(client, Box::pin(block_subscription)))
|
||||
}
|
||||
|
||||
/// Note: This is exposed for testing but is not considered stable and may change
|
||||
/// without notice in a patch release.
|
||||
#[doc(hidden)]
|
||||
pub fn subscribe_to_block_headers_filling_in_gaps<T, Client, S, E>(
|
||||
client: Client,
|
||||
mut last_block_num: Option<u64>,
|
||||
sub: S,
|
||||
) -> impl Stream<Item = Result<T::Header, Error>> + Send
|
||||
where
|
||||
T: Config,
|
||||
Client: OnlineClientT<T> + Send + Sync,
|
||||
S: Stream<Item = Result<T::Header, E>> + Send,
|
||||
E: Into<Error> + Send + 'static,
|
||||
{
|
||||
sub.flat_map(move |s| {
|
||||
let client = client.clone();
|
||||
|
||||
// Get the header, or return a stream containing just the error. Our EventSubscription
|
||||
// stream will return `None` as soon as it hits an error like this.
|
||||
let header = match s {
|
||||
Ok(header) => header,
|
||||
Err(e) => return Either::Left(stream::once(async { Err(e.into()) })),
|
||||
};
|
||||
|
||||
// We want all previous details up to, but not including this current block num.
|
||||
let end_block_num = (*header.number()).into();
|
||||
|
||||
// This is one after the last block we returned details for last time.
|
||||
let start_block_num = last_block_num.map(|n| n + 1).unwrap_or(end_block_num);
|
||||
|
||||
// Iterate over all of the previous blocks we need headers for, ignoring the current block
|
||||
// (which we already have the header info for):
|
||||
let previous_headers = stream::iter(start_block_num..end_block_num)
|
||||
.then(move |n| {
|
||||
let client = client.clone();
|
||||
async move {
|
||||
let hash = client.rpc().block_hash(Some(n.into())).await?;
|
||||
let header = client.rpc().header(hash).await?;
|
||||
Ok::<_, Error>(header)
|
||||
}
|
||||
})
|
||||
.filter_map(|h| async { h.transpose() });
|
||||
|
||||
// On the next iteration, we'll get details starting just after this end block.
|
||||
last_block_num = Some(end_block_num);
|
||||
|
||||
// Return a combination of any previous headers plus the new header.
|
||||
Either::Right(previous_headers.chain(stream::once(async { Ok(header) })))
|
||||
})
|
||||
}
|
||||
|
||||
// The storage key needed to access events.
|
||||
fn system_events_key() -> StorageKey {
|
||||
let mut storage_key = twox_128(b"System").to_vec();
|
||||
storage_key.extend(twox_128(b"Events").to_vec());
|
||||
StorageKey(storage_key)
|
||||
}
|
||||
+208
-435
@@ -4,13 +4,15 @@
|
||||
|
||||
//! A representation of a block of events.
|
||||
|
||||
use crate::{
|
||||
error::BasicError,
|
||||
Client,
|
||||
Config,
|
||||
Event,
|
||||
Metadata,
|
||||
use super::{
|
||||
Phase,
|
||||
StaticEvent,
|
||||
};
|
||||
use crate::{
|
||||
dynamic::DecodedValue,
|
||||
error::Error,
|
||||
Config,
|
||||
Metadata,
|
||||
};
|
||||
use codec::{
|
||||
Compact,
|
||||
@@ -19,77 +21,48 @@ use codec::{
|
||||
Input,
|
||||
};
|
||||
use derivative::Derivative;
|
||||
use parking_lot::RwLock;
|
||||
use sp_core::{
|
||||
storage::StorageKey,
|
||||
twox_128,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Obtain events at some block hash. The generic parameter is what we
|
||||
/// will attempt to decode each event into if using [`Events::iter()`],
|
||||
/// and is expected to be the outermost event enum that contains all of
|
||||
/// the possible events across all pallets.
|
||||
///
|
||||
/// **Note:** This function is hidden from the documentation
|
||||
/// and is exposed only to be called via the codegen. Thus, prefer to use
|
||||
/// `api.events().at(block_hash)` over calling this directly.
|
||||
#[doc(hidden)]
|
||||
pub async fn at<T: Config, Evs: Decode>(
|
||||
client: &'_ Client<T>,
|
||||
block_hash: T::Hash,
|
||||
) -> Result<Events<T, Evs>, BasicError> {
|
||||
let mut event_bytes = client
|
||||
.rpc()
|
||||
.storage(&system_events_key(), Some(block_hash))
|
||||
.await?
|
||||
.map(|s| s.0)
|
||||
.unwrap_or_else(Vec::new);
|
||||
|
||||
// event_bytes is a SCALE encoded vector of events. So, pluck the
|
||||
// compact encoded length from the front, leaving the remaining bytes
|
||||
// for our iterating to decode.
|
||||
//
|
||||
// Note: if we get no bytes back, avoid an error reading vec length
|
||||
// and default to 0 events.
|
||||
let cursor = &mut &*event_bytes;
|
||||
let num_events = <Compact<u32>>::decode(cursor).unwrap_or(Compact(0)).0;
|
||||
let event_bytes_len = event_bytes.len();
|
||||
let remaining_len = cursor.len();
|
||||
event_bytes.drain(0..event_bytes_len - remaining_len);
|
||||
|
||||
Ok(Events {
|
||||
metadata: client.metadata(),
|
||||
block_hash,
|
||||
event_bytes,
|
||||
num_events,
|
||||
_event_type: std::marker::PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
// The storage key needed to access events.
|
||||
fn system_events_key() -> StorageKey {
|
||||
let mut storage_key = twox_128(b"System").to_vec();
|
||||
storage_key.extend(twox_128(b"Events").to_vec());
|
||||
StorageKey(storage_key)
|
||||
}
|
||||
|
||||
/// A collection of events obtained from a block, bundled with the necessary
|
||||
/// information needed to decode and iterate over them.
|
||||
#[derive(Derivative)]
|
||||
#[derivative(Debug(bound = ""))]
|
||||
pub struct Events<T: Config, Evs> {
|
||||
metadata: Arc<RwLock<Metadata>>,
|
||||
pub struct Events<T: Config> {
|
||||
metadata: Metadata,
|
||||
block_hash: T::Hash,
|
||||
// Note; raw event bytes are prefixed with a Compact<u32> containing
|
||||
// the number of events to be decoded. We should have stripped that off
|
||||
// before storing the bytes here.
|
||||
event_bytes: Vec<u8>,
|
||||
event_bytes: Arc<[u8]>,
|
||||
num_events: u32,
|
||||
_event_type: std::marker::PhantomData<Evs>,
|
||||
}
|
||||
|
||||
impl<'a, T: Config, Evs: Decode> Events<T, Evs> {
|
||||
impl<T: Config> Events<T> {
|
||||
pub(crate) fn new(
|
||||
metadata: Metadata,
|
||||
block_hash: T::Hash,
|
||||
mut event_bytes: Vec<u8>,
|
||||
) -> Self {
|
||||
// event_bytes is a SCALE encoded vector of events. So, pluck the
|
||||
// compact encoded length from the front, leaving the remaining bytes
|
||||
// for our iterating to decode.
|
||||
//
|
||||
// Note: if we get no bytes back, avoid an error reading vec length
|
||||
// and default to 0 events.
|
||||
let cursor = &mut &*event_bytes;
|
||||
let num_events = <Compact<u32>>::decode(cursor).unwrap_or(Compact(0)).0;
|
||||
let event_bytes_len = event_bytes.len();
|
||||
let remaining_len = cursor.len();
|
||||
event_bytes.drain(0..event_bytes_len - remaining_len);
|
||||
|
||||
Self {
|
||||
metadata,
|
||||
block_hash,
|
||||
event_bytes: event_bytes.into(),
|
||||
num_events,
|
||||
}
|
||||
}
|
||||
|
||||
/// The number of events.
|
||||
pub fn len(&self) -> u32 {
|
||||
self.num_events
|
||||
@@ -106,83 +79,23 @@ impl<'a, T: Config, Evs: Decode> Events<T, Evs> {
|
||||
self.block_hash
|
||||
}
|
||||
|
||||
/// Iterate over the events, statically decoding them as we go.
|
||||
/// If an event is encountered that cannot be statically decoded,
|
||||
/// a [`codec::Error`] will be returned.
|
||||
///
|
||||
/// If the generated code does not know about all of the pallets that exist
|
||||
/// in the runtime being targeted, it may not know about all of the
|
||||
/// events either, and so this method should be avoided in favout of [`Events::iter_raw()`],
|
||||
/// which uses runtime metadata to skip over unknown events.
|
||||
/// Iterate over all of the events, using metadata to dynamically
|
||||
/// decode them as we go, and returning the raw bytes and other associated
|
||||
/// details. If an error occurs, all subsequent iterations return `None`.
|
||||
pub fn iter(
|
||||
&self,
|
||||
) -> impl Iterator<Item = Result<EventDetails<Evs>, BasicError>> + '_ {
|
||||
let event_bytes = &self.event_bytes;
|
||||
) -> impl Iterator<Item = Result<EventDetails, Error>> + Send + Sync + 'static {
|
||||
let event_bytes = self.event_bytes.clone();
|
||||
let num_events = self.num_events;
|
||||
|
||||
let metadata = self.metadata.clone();
|
||||
let mut pos = 0;
|
||||
let mut index = 0;
|
||||
std::iter::from_fn(move || {
|
||||
let cursor = &mut &event_bytes[pos..];
|
||||
let start_len = cursor.len();
|
||||
|
||||
if start_len == 0 || self.num_events == index {
|
||||
None
|
||||
} else {
|
||||
let mut decode_one_event = || -> Result<_, BasicError> {
|
||||
let phase = Phase::decode(cursor)?;
|
||||
let ev = Evs::decode(cursor)?;
|
||||
let _topics = Vec::<T::Hash>::decode(cursor)?;
|
||||
Ok((phase, ev))
|
||||
};
|
||||
match decode_one_event() {
|
||||
Ok((phase, event)) => {
|
||||
// Skip over decoded bytes in next iteration:
|
||||
pos += start_len - cursor.len();
|
||||
// Gather the event details before incrementing the index for the next iter.
|
||||
let res = Some(Ok(EventDetails {
|
||||
phase,
|
||||
index,
|
||||
event,
|
||||
}));
|
||||
index += 1;
|
||||
res
|
||||
}
|
||||
Err(e) => {
|
||||
// By setting the position to the "end" of the event bytes,
|
||||
// the cursor len will become 0 and the iterator will return `None`
|
||||
// from now on:
|
||||
pos = event_bytes.len();
|
||||
Some(Err(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Iterate over all of the events, using metadata to dynamically
|
||||
/// decode them as we go, and returning the raw bytes and other associated
|
||||
/// details. If an error occurs, all subsequent iterations return `None`.
|
||||
///
|
||||
/// This method is safe to use even if you do not statically know about
|
||||
/// all of the possible events; it splits events up using the metadata
|
||||
/// obtained at runtime, which does.
|
||||
pub fn iter_raw(
|
||||
&self,
|
||||
) -> impl Iterator<Item = Result<RawEventDetails, BasicError>> + '_ {
|
||||
let event_bytes = &self.event_bytes;
|
||||
|
||||
let metadata = {
|
||||
let metadata = self.metadata.read();
|
||||
metadata.clone()
|
||||
};
|
||||
|
||||
let mut pos = 0;
|
||||
let mut index = 0;
|
||||
std::iter::from_fn(move || {
|
||||
let cursor = &mut &event_bytes[pos..];
|
||||
let start_len = cursor.len();
|
||||
|
||||
if start_len == 0 || self.num_events == index {
|
||||
if start_len == 0 || num_events == index {
|
||||
None
|
||||
} else {
|
||||
match decode_raw_event_details::<T>(&metadata, index, cursor) {
|
||||
@@ -206,62 +119,11 @@ impl<'a, T: Config, Evs: Decode> Events<T, Evs> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Iterate over all of the events, using metadata to dynamically
|
||||
/// decode them as we go, and returning the raw bytes and other associated
|
||||
/// details. If an error occurs, all subsequent iterations return `None`.
|
||||
///
|
||||
/// This method is safe to use even if you do not statically know about
|
||||
/// all of the possible events; it splits events up using the metadata
|
||||
/// obtained at runtime, which does.
|
||||
///
|
||||
/// Unlike [`Events::iter_raw()`] this consumes `self`, which can be useful
|
||||
/// if you need to store the iterator somewhere and avoid lifetime issues.
|
||||
pub fn into_iter_raw(
|
||||
self,
|
||||
) -> impl Iterator<Item = Result<RawEventDetails, BasicError>> + 'a {
|
||||
let mut pos = 0;
|
||||
let mut index = 0;
|
||||
let metadata = {
|
||||
let metadata = self.metadata.read();
|
||||
metadata.clone()
|
||||
};
|
||||
|
||||
std::iter::from_fn(move || {
|
||||
let cursor = &mut &self.event_bytes[pos..];
|
||||
let start_len = cursor.len();
|
||||
|
||||
if start_len == 0 || self.num_events == index {
|
||||
None
|
||||
} else {
|
||||
match decode_raw_event_details::<T>(&metadata, index, cursor) {
|
||||
Ok(raw_event) => {
|
||||
// Skip over decoded bytes in next iteration:
|
||||
pos += start_len - cursor.len();
|
||||
// Increment the index:
|
||||
index += 1;
|
||||
// Return the event details:
|
||||
Some(Ok(raw_event))
|
||||
}
|
||||
Err(e) => {
|
||||
// By setting the position to the "end" of the event bytes,
|
||||
// the cursor len will become 0 and the iterator will return `None`
|
||||
// from now on:
|
||||
pos = self.event_bytes.len();
|
||||
Some(Err(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Iterate through the events using metadata to dynamically decode and skip
|
||||
/// them, and return only those which should decode to the provided `Ev` type.
|
||||
/// If an error occurs, all subsequent iterations return `None`.
|
||||
///
|
||||
/// **Note:** This method internally uses [`Events::iter_raw()`], so it is safe to
|
||||
/// use even if you do not statically know about all of the possible events.
|
||||
pub fn find<Ev: Event>(&self) -> impl Iterator<Item = Result<Ev, BasicError>> + '_ {
|
||||
self.iter_raw().filter_map(|ev| {
|
||||
pub fn find<Ev: StaticEvent>(&self) -> impl Iterator<Item = Result<Ev, Error>> + '_ {
|
||||
self.iter().filter_map(|ev| {
|
||||
ev.and_then(|ev| ev.as_event::<Ev>().map_err(Into::into))
|
||||
.transpose()
|
||||
})
|
||||
@@ -269,67 +131,147 @@ impl<'a, T: Config, Evs: Decode> Events<T, Evs> {
|
||||
|
||||
/// Iterate through the events using metadata to dynamically decode and skip
|
||||
/// them, and return the first event found which decodes to the provided `Ev` type.
|
||||
///
|
||||
/// **Note:** This method internally uses [`Events::iter_raw()`], so it is safe to
|
||||
/// use even if you do not statically know about all of the possible events.
|
||||
pub fn find_first<Ev: Event>(&self) -> Result<Option<Ev>, BasicError> {
|
||||
pub fn find_first<Ev: StaticEvent>(&self) -> Result<Option<Ev>, Error> {
|
||||
self.find::<Ev>().next().transpose()
|
||||
}
|
||||
|
||||
/// Find an event that decodes to the type provided. Returns true if it was found.
|
||||
///
|
||||
/// **Note:** This method internally uses [`Events::iter_raw()`], so it is safe to
|
||||
/// use even if you do not statically know about all of the possible events.
|
||||
pub fn has<Ev: crate::Event>(&self) -> Result<bool, BasicError> {
|
||||
pub fn has<Ev: StaticEvent>(&self) -> Result<bool, Error> {
|
||||
Ok(self.find::<Ev>().next().transpose()?.is_some())
|
||||
}
|
||||
}
|
||||
|
||||
/// A decoded event and associated details.
|
||||
/// The event details.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct EventDetails<Evs> {
|
||||
/// During which [`Phase`] was the event produced?
|
||||
pub phase: Phase,
|
||||
/// What index is this event in the stored events for this block.
|
||||
pub index: u32,
|
||||
/// The event itself.
|
||||
pub event: Evs,
|
||||
pub struct EventDetails {
|
||||
phase: Phase,
|
||||
index: u32,
|
||||
pallet: String,
|
||||
variant: String,
|
||||
bytes: Vec<u8>,
|
||||
// Dev note: this is here because we've pretty much had to generate it
|
||||
// anyway, but expect it to be generated on the fly in future versions,
|
||||
// and so don't expose it.
|
||||
fields: Vec<(Option<String>, DecodedValue)>,
|
||||
}
|
||||
|
||||
/// A Value which has been decoded from some raw bytes.
|
||||
pub type DecodedValue = scale_value::Value<scale_value::scale::TypeId>;
|
||||
|
||||
/// The raw bytes for an event with associated details about
|
||||
/// where and when it was emitted.
|
||||
/// The raw data associated with some event.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct RawEventDetails {
|
||||
pub struct EventDetailParts {
|
||||
/// When was the event produced?
|
||||
pub phase: Phase,
|
||||
/// What index is this event in the stored events for this block.
|
||||
pub index: u32,
|
||||
/// The name of the pallet from whence the Event originated.
|
||||
pub pallet: String,
|
||||
/// The index of the pallet from whence the Event originated.
|
||||
pub pallet_index: u8,
|
||||
/// The name of the pallet Event variant.
|
||||
/// The name of the pallet's Event variant.
|
||||
pub variant: String,
|
||||
/// The index of the pallet Event variant.
|
||||
pub variant_index: u8,
|
||||
/// The bytes representing the fields contained within the event.
|
||||
/// All of the bytes representing this event, including the pallet
|
||||
/// and variant index that the event originated from.
|
||||
pub bytes: Vec<u8>,
|
||||
/// Generic values representing each field of the event.
|
||||
pub fields: Vec<DecodedValue>,
|
||||
}
|
||||
|
||||
impl RawEventDetails {
|
||||
/// Attempt to decode this [`RawEventDetails`] into a specific event.
|
||||
pub fn as_event<E: Event>(&self) -> Result<Option<E>, CodecError> {
|
||||
impl EventDetails {
|
||||
/// Return the raw data associated with this event. Useful if you want
|
||||
/// ownership over parts of the event data.
|
||||
pub fn parts(self) -> EventDetailParts {
|
||||
EventDetailParts {
|
||||
phase: self.phase,
|
||||
index: self.index,
|
||||
pallet: self.pallet,
|
||||
variant: self.variant,
|
||||
bytes: self.bytes,
|
||||
}
|
||||
}
|
||||
|
||||
/// When was the event produced?
|
||||
pub fn phase(&self) -> Phase {
|
||||
self.phase
|
||||
}
|
||||
|
||||
/// What index is this event in the stored events for this block.
|
||||
pub fn index(&self) -> u32 {
|
||||
self.index
|
||||
}
|
||||
|
||||
/// The index of the pallet that the event originated from.
|
||||
pub fn pallet_index(&self) -> u8 {
|
||||
// Note: never panics because we set the first two bytes
|
||||
// in `decode_event_details` to build this.
|
||||
self.bytes[0]
|
||||
}
|
||||
|
||||
/// The index of the event variant that the event originated from.
|
||||
pub fn variant_index(&self) -> u8 {
|
||||
// Note: never panics because we set the first two bytes
|
||||
// in `decode_event_details` to build this.
|
||||
self.bytes[1]
|
||||
}
|
||||
|
||||
/// The name of the pallet from whence the Event originated.
|
||||
pub fn pallet_name(&self) -> &str {
|
||||
&self.pallet
|
||||
}
|
||||
|
||||
/// The name of the pallet's Event variant.
|
||||
pub fn variant_name(&self) -> &str {
|
||||
&self.variant
|
||||
}
|
||||
|
||||
/// Return the bytes representing this event, which include the pallet
|
||||
/// and variant index that the event originated from.
|
||||
pub fn bytes(&self) -> &[u8] {
|
||||
&self.bytes
|
||||
}
|
||||
|
||||
/// Return the bytes representing the fields stored in this event.
|
||||
pub fn field_bytes(&self) -> &[u8] {
|
||||
&self.bytes[2..]
|
||||
}
|
||||
|
||||
/// Decode and provide the event fields back in the form of a composite
|
||||
/// type, which represents either the named or unnamed fields that were
|
||||
/// present.
|
||||
// Dev note: if we can optimise Value decoding to avoid allocating
|
||||
// while working through events, or if the event structure changes
|
||||
// to allow us to skip over them, we'll no longer keep a copy of the
|
||||
// decoded events in the event, and the actual decoding will happen
|
||||
// when this method is called. This is why we return an owned vec and
|
||||
// not a reference.
|
||||
pub fn field_values(&self) -> scale_value::Composite<scale_value::scale::TypeId> {
|
||||
if self.fields.is_empty() {
|
||||
scale_value::Composite::Unnamed(vec![])
|
||||
} else if self.fields[0].0.is_some() {
|
||||
let named = self
|
||||
.fields
|
||||
.iter()
|
||||
.map(|(n, f)| (n.clone().unwrap_or_default(), f.clone()))
|
||||
.collect();
|
||||
scale_value::Composite::Named(named)
|
||||
} else {
|
||||
let unnamed = self.fields.iter().map(|(_n, f)| f.clone()).collect();
|
||||
scale_value::Composite::Unnamed(unnamed)
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to decode these [`EventDetails`] into a specific static event.
|
||||
/// This targets the fields within the event directly. You can also attempt to
|
||||
/// decode the entirety of the event type (including the pallet and event
|
||||
/// variants) using [`EventDetails::as_root_event()`].
|
||||
pub fn as_event<E: StaticEvent>(&self) -> Result<Option<E>, CodecError> {
|
||||
if self.pallet == E::PALLET && self.variant == E::EVENT {
|
||||
Ok(Some(E::decode(&mut &self.bytes[..])?))
|
||||
Ok(Some(E::decode(&mut &self.bytes[2..])?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to decode these [`EventDetails`] into a root event type (which includes
|
||||
/// the pallet and event enum variants as well as the event fields). A compatible
|
||||
/// type for this is exposed via static codegen as a root level `Event` type.
|
||||
pub fn as_root_event<E: Decode>(&self) -> Result<E, CodecError> {
|
||||
E::decode(&mut &self.bytes[..])
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to dynamically decode a single event from our events input.
|
||||
@@ -337,7 +279,7 @@ fn decode_raw_event_details<T: Config>(
|
||||
metadata: &Metadata,
|
||||
index: u32,
|
||||
input: &mut &[u8],
|
||||
) -> Result<RawEventDetails, BasicError> {
|
||||
) -> Result<EventDetails, Error> {
|
||||
// Decode basic event details:
|
||||
let phase = Phase::decode(input)?;
|
||||
let pallet_index = input.read_byte()?;
|
||||
@@ -358,19 +300,20 @@ fn decode_raw_event_details<T: Config>(
|
||||
event_metadata.event()
|
||||
);
|
||||
|
||||
// Use metadata to figure out which bytes belong to this event:
|
||||
let mut event_bytes = Vec::new();
|
||||
// Use metadata to figure out which bytes belong to this event.
|
||||
// the event bytes also include the pallet/variant index so that, if we
|
||||
// like, we can decode them quite easily into a top level event type.
|
||||
let mut event_bytes = vec![pallet_index, variant_index];
|
||||
let mut event_fields = Vec::new();
|
||||
for arg in event_metadata.variant().fields() {
|
||||
let type_id = arg.ty().id();
|
||||
for (name, type_id) in event_metadata.fields() {
|
||||
let all_bytes = *input;
|
||||
// consume some bytes for each event field, moving the cursor forward:
|
||||
let value = scale_value::scale::decode_as_type(
|
||||
input,
|
||||
type_id,
|
||||
*type_id,
|
||||
&metadata.runtime_metadata().types,
|
||||
)?;
|
||||
event_fields.push(value);
|
||||
event_fields.push((name.clone(), value));
|
||||
// count how many bytes were consumed based on remaining length:
|
||||
let consumed_len = all_bytes.len() - input.len();
|
||||
// move those consumed bytes to the output vec unaltered:
|
||||
@@ -382,12 +325,10 @@ fn decode_raw_event_details<T: Config>(
|
||||
let topics = Vec::<T::Hash>::decode(input)?;
|
||||
tracing::debug!("topics: {:?}", topics);
|
||||
|
||||
Ok(RawEventDetails {
|
||||
Ok(EventDetails {
|
||||
phase,
|
||||
index,
|
||||
pallet_index,
|
||||
pallet: event_metadata.pallet().to_string(),
|
||||
variant_index,
|
||||
variant: event_metadata.event().to_string(),
|
||||
bytes: event_bytes,
|
||||
fields: event_fields,
|
||||
@@ -400,8 +341,7 @@ pub(crate) mod test_utils {
|
||||
use super::*;
|
||||
use crate::{
|
||||
Config,
|
||||
DefaultConfig,
|
||||
Phase,
|
||||
SubstrateConfig,
|
||||
};
|
||||
use codec::Encode;
|
||||
use frame_metadata::{
|
||||
@@ -431,7 +371,7 @@ pub(crate) mod test_utils {
|
||||
pub struct EventRecord<E: Encode> {
|
||||
phase: Phase,
|
||||
event: AllEvents<E>,
|
||||
topics: Vec<<DefaultConfig as Config>::Hash>,
|
||||
topics: Vec<<SubstrateConfig as Config>::Hash>,
|
||||
}
|
||||
|
||||
/// Build an EventRecord, which encoded events in the format expected
|
||||
@@ -474,9 +414,9 @@ pub(crate) mod test_utils {
|
||||
/// Build an `Events` object for test purposes, based on the details provided,
|
||||
/// and with a default block hash.
|
||||
pub fn events<E: Decode + Encode>(
|
||||
metadata: Arc<RwLock<Metadata>>,
|
||||
metadata: Metadata,
|
||||
event_records: Vec<EventRecord<E>>,
|
||||
) -> Events<DefaultConfig, AllEvents<E>> {
|
||||
) -> Events<SubstrateConfig> {
|
||||
let num_events = event_records.len() as u32;
|
||||
let mut event_bytes = Vec::new();
|
||||
for ev in event_records {
|
||||
@@ -487,17 +427,16 @@ pub(crate) mod test_utils {
|
||||
|
||||
/// Much like [`events`], but takes pre-encoded events and event count, so that we can
|
||||
/// mess with the bytes in tests if we need to.
|
||||
pub fn events_raw<E: Decode + Encode>(
|
||||
metadata: Arc<RwLock<Metadata>>,
|
||||
pub fn events_raw(
|
||||
metadata: Metadata,
|
||||
event_bytes: Vec<u8>,
|
||||
num_events: u32,
|
||||
) -> Events<DefaultConfig, AllEvents<E>> {
|
||||
) -> Events<SubstrateConfig> {
|
||||
Events {
|
||||
block_hash: <DefaultConfig as Config>::Hash::default(),
|
||||
event_bytes,
|
||||
block_hash: <SubstrateConfig as Config>::Hash::default(),
|
||||
event_bytes: event_bytes.into(),
|
||||
metadata,
|
||||
num_events,
|
||||
_event_type: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -509,18 +448,16 @@ mod tests {
|
||||
event_record,
|
||||
events,
|
||||
events_raw,
|
||||
AllEvents,
|
||||
},
|
||||
*,
|
||||
};
|
||||
use crate::Phase;
|
||||
use codec::Encode;
|
||||
use scale_info::TypeInfo;
|
||||
use scale_value::Value;
|
||||
|
||||
/// Build a fake wrapped metadata.
|
||||
fn metadata<E: TypeInfo + 'static>() -> Arc<RwLock<Metadata>> {
|
||||
Arc::new(RwLock::new(test_utils::metadata::<E>()))
|
||||
fn metadata<E: TypeInfo + 'static>() -> Metadata {
|
||||
test_utils::metadata::<E>()
|
||||
}
|
||||
|
||||
/// [`RawEventDetails`] can be annoying to test, because it contains
|
||||
@@ -542,170 +479,42 @@ mod tests {
|
||||
pub fn assert_raw_events_match(
|
||||
// Just for convenience, pass in the metadata type constructed
|
||||
// by the `metadata` function above to simplify caller code.
|
||||
metadata: &Arc<RwLock<Metadata>>,
|
||||
actual: RawEventDetails,
|
||||
metadata: &Metadata,
|
||||
actual: EventDetails,
|
||||
expected: TestRawEventDetails,
|
||||
) {
|
||||
let metadata = metadata.read();
|
||||
let types = &metadata.runtime_metadata().types;
|
||||
|
||||
// Make sure that the bytes handed back line up with the fields handed back;
|
||||
// encode the fields back into bytes and they should be equal.
|
||||
let mut actual_bytes = vec![];
|
||||
for field in &actual.fields {
|
||||
for (_name, field) in &actual.fields {
|
||||
scale_value::scale::encode_as_type(
|
||||
field.clone(),
|
||||
field,
|
||||
field.context,
|
||||
types,
|
||||
&mut actual_bytes,
|
||||
)
|
||||
.expect("should be able to encode properly");
|
||||
}
|
||||
assert_eq!(actual_bytes, actual.bytes);
|
||||
assert_eq!(actual_bytes, actual.field_bytes());
|
||||
|
||||
let actual_fields_no_context: Vec<_> = actual
|
||||
.fields
|
||||
.into_iter()
|
||||
.map(|f| f.remove_context())
|
||||
.field_values()
|
||||
.into_values()
|
||||
.map(|value| value.remove_context())
|
||||
.collect();
|
||||
|
||||
// Check each of the other fields:
|
||||
assert_eq!(actual.phase, expected.phase);
|
||||
assert_eq!(actual.index, expected.index);
|
||||
assert_eq!(actual.pallet, expected.pallet);
|
||||
assert_eq!(actual.pallet_index, expected.pallet_index);
|
||||
assert_eq!(actual.variant, expected.variant);
|
||||
assert_eq!(actual.variant_index, expected.variant_index);
|
||||
assert_eq!(actual.phase(), expected.phase);
|
||||
assert_eq!(actual.index(), expected.index);
|
||||
assert_eq!(actual.pallet_name(), expected.pallet);
|
||||
assert_eq!(actual.pallet_index(), expected.pallet_index);
|
||||
assert_eq!(actual.variant_name(), expected.variant);
|
||||
assert_eq!(actual.variant_index(), expected.variant_index);
|
||||
assert_eq!(actual_fields_no_context, expected.fields);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn statically_decode_single_event() {
|
||||
#[derive(Clone, Debug, PartialEq, Decode, Encode, TypeInfo)]
|
||||
enum Event {
|
||||
A(u8),
|
||||
}
|
||||
|
||||
// Create fake metadata that knows about our single event, above:
|
||||
let metadata = metadata::<Event>();
|
||||
// Encode our events in the format we expect back from a node, and
|
||||
// construct an Events object to iterate them:
|
||||
let events = events::<Event>(
|
||||
metadata,
|
||||
vec![event_record(Phase::Finalization, Event::A(1))],
|
||||
);
|
||||
|
||||
let event_details: Vec<EventDetails<AllEvents<Event>>> =
|
||||
events.iter().collect::<Result<_, _>>().unwrap();
|
||||
assert_eq!(
|
||||
event_details,
|
||||
vec![EventDetails {
|
||||
index: 0,
|
||||
phase: Phase::Finalization,
|
||||
event: AllEvents::Test(Event::A(1))
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn statically_decode_multiple_events() {
|
||||
#[derive(Clone, Debug, PartialEq, Decode, Encode, TypeInfo)]
|
||||
enum Event {
|
||||
A(u8),
|
||||
B(bool),
|
||||
}
|
||||
|
||||
// Create fake metadata that knows about our single event, above:
|
||||
let metadata = metadata::<Event>();
|
||||
|
||||
// Encode our events in the format we expect back from a node, and
|
||||
// construst an Events object to iterate them:
|
||||
let events = events::<Event>(
|
||||
metadata,
|
||||
vec![
|
||||
event_record(Phase::Initialization, Event::A(1)),
|
||||
event_record(Phase::ApplyExtrinsic(123), Event::B(true)),
|
||||
event_record(Phase::Finalization, Event::A(234)),
|
||||
],
|
||||
);
|
||||
|
||||
let event_details: Vec<EventDetails<AllEvents<Event>>> =
|
||||
events.iter().collect::<Result<_, _>>().unwrap();
|
||||
assert_eq!(
|
||||
event_details,
|
||||
vec![
|
||||
EventDetails {
|
||||
index: 0,
|
||||
phase: Phase::Initialization,
|
||||
event: AllEvents::Test(Event::A(1))
|
||||
},
|
||||
EventDetails {
|
||||
index: 1,
|
||||
phase: Phase::ApplyExtrinsic(123),
|
||||
event: AllEvents::Test(Event::B(true))
|
||||
},
|
||||
EventDetails {
|
||||
index: 2,
|
||||
phase: Phase::Finalization,
|
||||
event: AllEvents::Test(Event::A(234))
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn statically_decode_multiple_events_until_error() {
|
||||
#[derive(Clone, Debug, PartialEq, Decode, Encode, TypeInfo)]
|
||||
enum Event {
|
||||
A(u8),
|
||||
B(bool),
|
||||
}
|
||||
|
||||
// Create fake metadata that knows about our single event, above:
|
||||
let metadata = metadata::<Event>();
|
||||
|
||||
// Encode 2 events:
|
||||
let mut event_bytes = vec![];
|
||||
event_record(Phase::Initialization, Event::A(1)).encode_to(&mut event_bytes);
|
||||
event_record(Phase::ApplyExtrinsic(123), Event::B(true))
|
||||
.encode_to(&mut event_bytes);
|
||||
|
||||
// Push a few naff bytes to the end (a broken third event):
|
||||
event_bytes.extend_from_slice(&[3, 127, 45, 0, 2]);
|
||||
|
||||
// Encode our events in the format we expect back from a node, and
|
||||
// construst an Events object to iterate them:
|
||||
let events = events_raw::<Event>(
|
||||
metadata,
|
||||
event_bytes,
|
||||
3, // 2 "good" events, and then it'll hit the naff bytes.
|
||||
);
|
||||
|
||||
let mut events_iter = events.iter();
|
||||
assert_eq!(
|
||||
events_iter.next().unwrap().unwrap(),
|
||||
EventDetails {
|
||||
index: 0,
|
||||
phase: Phase::Initialization,
|
||||
event: AllEvents::Test(Event::A(1))
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
events_iter.next().unwrap().unwrap(),
|
||||
EventDetails {
|
||||
index: 1,
|
||||
phase: Phase::ApplyExtrinsic(123),
|
||||
event: AllEvents::Test(Event::B(true))
|
||||
}
|
||||
);
|
||||
|
||||
// We'll hit an error trying to decode the third event:
|
||||
assert!(events_iter.next().unwrap().is_err());
|
||||
// ... and then "None" from then on.
|
||||
assert!(events_iter.next().is_none());
|
||||
assert!(events_iter.next().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dynamically_decode_single_event() {
|
||||
#[derive(Clone, Debug, PartialEq, Decode, Encode, TypeInfo)]
|
||||
@@ -724,7 +533,7 @@ mod tests {
|
||||
vec![event_record(Phase::ApplyExtrinsic(123), event)],
|
||||
);
|
||||
|
||||
let mut event_details = events.iter_raw();
|
||||
let mut event_details = events.iter();
|
||||
assert_raw_events_match(
|
||||
&metadata,
|
||||
event_details.next().unwrap().unwrap(),
|
||||
@@ -736,7 +545,7 @@ mod tests {
|
||||
variant: "A".to_string(),
|
||||
variant_index: 0,
|
||||
fields: vec![
|
||||
Value::uint(1u8),
|
||||
Value::u128(1),
|
||||
Value::bool(true),
|
||||
Value::unnamed_composite(vec![Value::string("Hi")]),
|
||||
],
|
||||
@@ -771,7 +580,7 @@ mod tests {
|
||||
],
|
||||
);
|
||||
|
||||
let mut event_details = events.iter_raw();
|
||||
let mut event_details = events.iter();
|
||||
|
||||
assert_raw_events_match(
|
||||
&metadata,
|
||||
@@ -783,7 +592,7 @@ mod tests {
|
||||
pallet_index: 0,
|
||||
variant: "A".to_string(),
|
||||
variant_index: 0,
|
||||
fields: vec![Value::uint(1u8)],
|
||||
fields: vec![Value::u128(1)],
|
||||
},
|
||||
);
|
||||
assert_raw_events_match(
|
||||
@@ -809,7 +618,7 @@ mod tests {
|
||||
pallet_index: 0,
|
||||
variant: "A".to_string(),
|
||||
variant_index: 0,
|
||||
fields: vec![Value::uint(234u8)],
|
||||
fields: vec![Value::u128(234)],
|
||||
},
|
||||
);
|
||||
assert!(event_details.next().is_none());
|
||||
@@ -837,13 +646,13 @@ mod tests {
|
||||
|
||||
// Encode our events in the format we expect back from a node, and
|
||||
// construst an Events object to iterate them:
|
||||
let events = events_raw::<Event>(
|
||||
let events = events_raw(
|
||||
metadata.clone(),
|
||||
event_bytes,
|
||||
3, // 2 "good" events, and then it'll hit the naff bytes.
|
||||
);
|
||||
|
||||
let mut events_iter = events.iter_raw();
|
||||
let mut events_iter = events.iter();
|
||||
assert_raw_events_match(
|
||||
&metadata,
|
||||
events_iter.next().unwrap().unwrap(),
|
||||
@@ -854,7 +663,7 @@ mod tests {
|
||||
pallet_index: 0,
|
||||
variant: "A".to_string(),
|
||||
variant_index: 0,
|
||||
fields: vec![Value::uint(1u8)],
|
||||
fields: vec![Value::u128(1)],
|
||||
},
|
||||
);
|
||||
assert_raw_events_match(
|
||||
@@ -895,20 +704,8 @@ mod tests {
|
||||
vec![event_record(Phase::Finalization, Event::A(1))],
|
||||
);
|
||||
|
||||
// Statically decode:
|
||||
let event_details: Vec<EventDetails<AllEvents<Event>>> =
|
||||
events.iter().collect::<Result<_, _>>().unwrap();
|
||||
assert_eq!(
|
||||
event_details,
|
||||
vec![EventDetails {
|
||||
index: 0,
|
||||
phase: Phase::Finalization,
|
||||
event: AllEvents::Test(Event::A(1))
|
||||
}]
|
||||
);
|
||||
|
||||
// Dynamically decode:
|
||||
let mut event_details = events.iter_raw();
|
||||
let mut event_details = events.iter();
|
||||
assert_raw_events_match(
|
||||
&metadata,
|
||||
event_details.next().unwrap().unwrap(),
|
||||
@@ -919,7 +716,7 @@ mod tests {
|
||||
pallet_index: 0,
|
||||
variant: "A".to_string(),
|
||||
variant_index: 0,
|
||||
fields: vec![Value::uint(1u8)],
|
||||
fields: vec![Value::u128(1)],
|
||||
},
|
||||
);
|
||||
assert!(event_details.next().is_none());
|
||||
@@ -948,20 +745,8 @@ mod tests {
|
||||
)],
|
||||
);
|
||||
|
||||
// Statically decode:
|
||||
let event_details: Vec<EventDetails<AllEvents<Event>>> =
|
||||
events.iter().collect::<Result<_, _>>().unwrap();
|
||||
assert_eq!(
|
||||
event_details,
|
||||
vec![EventDetails {
|
||||
index: 0,
|
||||
phase: Phase::Finalization,
|
||||
event: AllEvents::Test(Event::A(CompactWrapper(1)))
|
||||
}]
|
||||
);
|
||||
|
||||
// Dynamically decode:
|
||||
let mut event_details = events.iter_raw();
|
||||
let mut event_details = events.iter();
|
||||
assert_raw_events_match(
|
||||
&metadata,
|
||||
event_details.next().unwrap().unwrap(),
|
||||
@@ -972,7 +757,7 @@ mod tests {
|
||||
pallet_index: 0,
|
||||
variant: "A".to_string(),
|
||||
variant_index: 0,
|
||||
fields: vec![Value::unnamed_composite(vec![Value::uint(1u8)])],
|
||||
fields: vec![Value::unnamed_composite(vec![Value::u128(1)])],
|
||||
},
|
||||
);
|
||||
assert!(event_details.next().is_none());
|
||||
@@ -1002,20 +787,8 @@ mod tests {
|
||||
vec![event_record(Phase::Finalization, Event::A(MyType::B))],
|
||||
);
|
||||
|
||||
// Statically decode:
|
||||
let event_details: Vec<EventDetails<AllEvents<Event>>> =
|
||||
events.iter().collect::<Result<_, _>>().unwrap();
|
||||
assert_eq!(
|
||||
event_details,
|
||||
vec![EventDetails {
|
||||
index: 0,
|
||||
phase: Phase::Finalization,
|
||||
event: AllEvents::Test(Event::A(MyType::B))
|
||||
}]
|
||||
);
|
||||
|
||||
// Dynamically decode:
|
||||
let mut event_details = events.iter_raw();
|
||||
let mut event_details = events.iter();
|
||||
assert_raw_events_match(
|
||||
&metadata,
|
||||
event_details.next().unwrap().unwrap(),
|
||||
|
||||
@@ -4,14 +4,15 @@
|
||||
|
||||
//! Filtering individual events from subscriptions.
|
||||
|
||||
use super::Events;
|
||||
use crate::{
|
||||
BasicError,
|
||||
Config,
|
||||
Event,
|
||||
use super::{
|
||||
Events,
|
||||
Phase,
|
||||
StaticEvent,
|
||||
};
|
||||
use crate::{
|
||||
Config,
|
||||
Error,
|
||||
};
|
||||
use codec::Decode;
|
||||
use futures::{
|
||||
Stream,
|
||||
StreamExt,
|
||||
@@ -28,7 +29,7 @@ use std::{
|
||||
/// exactly one of these will be `Some(event)` each iteration.
|
||||
pub struct FilterEvents<'a, Sub: 'a, T: Config, Filter: EventFilter> {
|
||||
// A subscription; in order for the Stream impl to apply, this will
|
||||
// impl `Stream<Item = Result<Events<'a, T, Evs>, BasicError>> + Unpin + 'a`.
|
||||
// impl `Stream<Item = Result<Events<'a, T, Evs>, Error>> + Unpin + 'a`.
|
||||
sub: Sub,
|
||||
// Each time we get Events from our subscription, they are stored here
|
||||
// and iterated through in future stream iterations until exhausted.
|
||||
@@ -37,7 +38,7 @@ pub struct FilterEvents<'a, Sub: 'a, T: Config, Filter: EventFilter> {
|
||||
dyn Iterator<
|
||||
Item = Result<
|
||||
FilteredEventDetails<T::Hash, Filter::ReturnType>,
|
||||
BasicError,
|
||||
Error,
|
||||
>,
|
||||
> + Send
|
||||
+ 'a,
|
||||
@@ -56,14 +57,13 @@ impl<'a, Sub: 'a, T: Config, Filter: EventFilter> FilterEvents<'a, Sub, T, Filte
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Sub, T, Evs, Filter> Stream for FilterEvents<'a, Sub, T, Filter>
|
||||
impl<'a, Sub, T, Filter> Stream for FilterEvents<'a, Sub, T, Filter>
|
||||
where
|
||||
Sub: Stream<Item = Result<Events<T, Evs>, BasicError>> + Unpin + 'a,
|
||||
Sub: Stream<Item = Result<Events<T>, Error>> + Unpin + 'a,
|
||||
T: Config,
|
||||
Evs: Decode + 'static,
|
||||
Filter: EventFilter,
|
||||
{
|
||||
type Item = Result<FilteredEventDetails<T::Hash, Filter::ReturnType>, BasicError>;
|
||||
type Item = Result<FilteredEventDetails<T::Hash, Filter::ReturnType>, Error>;
|
||||
fn poll_next(
|
||||
mut self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
@@ -112,14 +112,11 @@ pub trait EventFilter: private::Sealed {
|
||||
/// The type we'll be handed back from filtering.
|
||||
type ReturnType;
|
||||
/// Filter the events based on the type implementing this trait.
|
||||
fn filter<'a, T: Config, Evs: Decode + 'static>(
|
||||
events: Events<T, Evs>,
|
||||
fn filter<'a, T: Config>(
|
||||
events: Events<T>,
|
||||
) -> Box<
|
||||
dyn Iterator<
|
||||
Item = Result<
|
||||
FilteredEventDetails<T::Hash, Self::ReturnType>,
|
||||
BasicError,
|
||||
>,
|
||||
Item = Result<FilteredEventDetails<T::Hash, Self::ReturnType>, Error>,
|
||||
> + Send
|
||||
+ 'a,
|
||||
>;
|
||||
@@ -134,18 +131,16 @@ pub(crate) mod private {
|
||||
|
||||
// A special case impl for searching for a tuple of exactly one event (in this case, we don't
|
||||
// need to return an `(Option<Event>,)`; we can just return `Event`.
|
||||
impl<Ev: Event> private::Sealed for (Ev,) {}
|
||||
impl<Ev: Event> EventFilter for (Ev,) {
|
||||
impl<Ev: StaticEvent> private::Sealed for (Ev,) {}
|
||||
impl<Ev: StaticEvent> EventFilter for (Ev,) {
|
||||
type ReturnType = Ev;
|
||||
fn filter<'a, T: Config, Evs: Decode + 'static>(
|
||||
events: Events<T, Evs>,
|
||||
fn filter<'a, T: Config>(
|
||||
events: Events<T>,
|
||||
) -> Box<
|
||||
dyn Iterator<Item = Result<FilteredEventDetails<T::Hash, Ev>, BasicError>>
|
||||
+ Send
|
||||
+ 'a,
|
||||
dyn Iterator<Item = Result<FilteredEventDetails<T::Hash, Ev>, Error>> + Send + 'a,
|
||||
> {
|
||||
let block_hash = events.block_hash();
|
||||
let mut iter = events.into_iter_raw();
|
||||
let mut iter = events.iter();
|
||||
Box::new(std::iter::from_fn(move || {
|
||||
for ev in iter.by_ref() {
|
||||
// Forward any error immediately:
|
||||
@@ -158,7 +153,7 @@ impl<Ev: Event> EventFilter for (Ev,) {
|
||||
if let Ok(Some(event)) = ev {
|
||||
// We found a match; return our tuple.
|
||||
return Some(Ok(FilteredEventDetails {
|
||||
phase: raw_event.phase,
|
||||
phase: raw_event.phase(),
|
||||
block_hash,
|
||||
event,
|
||||
}))
|
||||
@@ -176,14 +171,14 @@ impl<Ev: Event> EventFilter for (Ev,) {
|
||||
// A generalised impl for tuples of sizes greater than 1:
|
||||
macro_rules! impl_event_filter {
|
||||
($($ty:ident $idx:tt),+) => {
|
||||
impl <$($ty: Event),+> private::Sealed for ( $($ty,)+ ) {}
|
||||
impl <$($ty: Event),+> EventFilter for ( $($ty,)+ ) {
|
||||
impl <$($ty: StaticEvent),+> private::Sealed for ( $($ty,)+ ) {}
|
||||
impl <$($ty: StaticEvent),+> EventFilter for ( $($ty,)+ ) {
|
||||
type ReturnType = ( $(Option<$ty>,)+ );
|
||||
fn filter<'a, T: Config, Evs: Decode + 'static>(
|
||||
events: Events<T, Evs>
|
||||
) -> Box<dyn Iterator<Item=Result<FilteredEventDetails<T::Hash,Self::ReturnType>, BasicError>> + Send + 'a> {
|
||||
fn filter<'a, T: Config>(
|
||||
events: Events<T>
|
||||
) -> Box<dyn Iterator<Item=Result<FilteredEventDetails<T::Hash,Self::ReturnType>, Error>> + Send + 'a> {
|
||||
let block_hash = events.block_hash();
|
||||
let mut iter = events.into_iter_raw();
|
||||
let mut iter = events.iter();
|
||||
Box::new(std::iter::from_fn(move || {
|
||||
let mut out: ( $(Option<$ty>,)+ ) = Default::default();
|
||||
for ev in iter.by_ref() {
|
||||
@@ -199,7 +194,7 @@ macro_rules! impl_event_filter {
|
||||
// We found a match; return our tuple.
|
||||
out.$idx = Some(ev);
|
||||
return Some(Ok(FilteredEventDetails {
|
||||
phase: raw_event.phase,
|
||||
phase: raw_event.phase(),
|
||||
block_hash,
|
||||
event: out
|
||||
}))
|
||||
@@ -232,24 +227,24 @@ mod test {
|
||||
event_record,
|
||||
events,
|
||||
metadata,
|
||||
AllEvents,
|
||||
},
|
||||
*,
|
||||
};
|
||||
use crate::{
|
||||
Config,
|
||||
DefaultConfig,
|
||||
Metadata,
|
||||
SubstrateConfig,
|
||||
};
|
||||
use codec::{
|
||||
Decode,
|
||||
Encode,
|
||||
};
|
||||
use codec::Encode;
|
||||
use futures::{
|
||||
stream,
|
||||
Stream,
|
||||
StreamExt,
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
use scale_info::TypeInfo;
|
||||
use std::sync::Arc;
|
||||
|
||||
// Some pretend events in a pallet
|
||||
#[derive(Clone, Debug, PartialEq, Decode, Encode, TypeInfo)]
|
||||
@@ -262,7 +257,7 @@ mod test {
|
||||
// An event in our pallet that we can filter on.
|
||||
#[derive(Clone, Debug, PartialEq, Decode, Encode, TypeInfo)]
|
||||
struct EventA(u8);
|
||||
impl crate::Event for EventA {
|
||||
impl StaticEvent for EventA {
|
||||
const PALLET: &'static str = "Test";
|
||||
const EVENT: &'static str = "A";
|
||||
}
|
||||
@@ -270,7 +265,7 @@ mod test {
|
||||
// An event in our pallet that we can filter on.
|
||||
#[derive(Clone, Debug, PartialEq, Decode, Encode, TypeInfo)]
|
||||
struct EventB(bool);
|
||||
impl crate::Event for EventB {
|
||||
impl StaticEvent for EventB {
|
||||
const PALLET: &'static str = "Test";
|
||||
const EVENT: &'static str = "B";
|
||||
}
|
||||
@@ -278,16 +273,15 @@ mod test {
|
||||
// An event in our pallet that we can filter on.
|
||||
#[derive(Clone, Debug, PartialEq, Decode, Encode, TypeInfo)]
|
||||
struct EventC(u8, bool);
|
||||
impl crate::Event for EventC {
|
||||
impl StaticEvent for EventC {
|
||||
const PALLET: &'static str = "Test";
|
||||
const EVENT: &'static str = "C";
|
||||
}
|
||||
|
||||
// A stream of fake events for us to try filtering on.
|
||||
fn events_stream(
|
||||
metadata: Arc<RwLock<Metadata>>,
|
||||
) -> impl Stream<Item = Result<Events<DefaultConfig, AllEvents<PalletEvents>>, BasicError>>
|
||||
{
|
||||
metadata: Metadata,
|
||||
) -> impl Stream<Item = Result<Events<SubstrateConfig>, Error>> {
|
||||
stream::iter(vec![
|
||||
events::<PalletEvents>(
|
||||
metadata.clone(),
|
||||
@@ -312,16 +306,16 @@ mod test {
|
||||
],
|
||||
),
|
||||
])
|
||||
.map(Ok::<_, BasicError>)
|
||||
.map(Ok::<_, Error>)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn filter_one_event_from_stream() {
|
||||
let metadata = Arc::new(RwLock::new(metadata::<PalletEvents>()));
|
||||
let metadata = metadata::<PalletEvents>();
|
||||
|
||||
// Filter out fake event stream to select events matching `EventA` only.
|
||||
let actual: Vec<_> =
|
||||
FilterEvents::<_, DefaultConfig, (EventA,)>::new(events_stream(metadata))
|
||||
FilterEvents::<_, SubstrateConfig, (EventA,)>::new(events_stream(metadata))
|
||||
.map(|e| e.unwrap())
|
||||
.collect()
|
||||
.await;
|
||||
@@ -329,17 +323,17 @@ mod test {
|
||||
let expected = vec![
|
||||
FilteredEventDetails {
|
||||
phase: Phase::Initialization,
|
||||
block_hash: <DefaultConfig as Config>::Hash::default(),
|
||||
block_hash: <SubstrateConfig as Config>::Hash::default(),
|
||||
event: EventA(1),
|
||||
},
|
||||
FilteredEventDetails {
|
||||
phase: Phase::Finalization,
|
||||
block_hash: <DefaultConfig as Config>::Hash::default(),
|
||||
block_hash: <SubstrateConfig as Config>::Hash::default(),
|
||||
event: EventA(2),
|
||||
},
|
||||
FilteredEventDetails {
|
||||
phase: Phase::ApplyExtrinsic(3),
|
||||
block_hash: <DefaultConfig as Config>::Hash::default(),
|
||||
block_hash: <SubstrateConfig as Config>::Hash::default(),
|
||||
event: EventA(3),
|
||||
},
|
||||
];
|
||||
@@ -349,10 +343,10 @@ mod test {
|
||||
|
||||
#[tokio::test]
|
||||
async fn filter_some_events_from_stream() {
|
||||
let metadata = Arc::new(RwLock::new(metadata::<PalletEvents>()));
|
||||
let metadata = metadata::<PalletEvents>();
|
||||
|
||||
// Filter out fake event stream to select events matching `EventA` or `EventB`.
|
||||
let actual: Vec<_> = FilterEvents::<_, DefaultConfig, (EventA, EventB)>::new(
|
||||
let actual: Vec<_> = FilterEvents::<_, SubstrateConfig, (EventA, EventB)>::new(
|
||||
events_stream(metadata),
|
||||
)
|
||||
.map(|e| e.unwrap())
|
||||
@@ -362,32 +356,32 @@ mod test {
|
||||
let expected = vec![
|
||||
FilteredEventDetails {
|
||||
phase: Phase::Initialization,
|
||||
block_hash: <DefaultConfig as Config>::Hash::default(),
|
||||
block_hash: <SubstrateConfig as Config>::Hash::default(),
|
||||
event: (Some(EventA(1)), None),
|
||||
},
|
||||
FilteredEventDetails {
|
||||
phase: Phase::ApplyExtrinsic(0),
|
||||
block_hash: <DefaultConfig as Config>::Hash::default(),
|
||||
block_hash: <SubstrateConfig as Config>::Hash::default(),
|
||||
event: (None, Some(EventB(true))),
|
||||
},
|
||||
FilteredEventDetails {
|
||||
phase: Phase::Finalization,
|
||||
block_hash: <DefaultConfig as Config>::Hash::default(),
|
||||
block_hash: <SubstrateConfig as Config>::Hash::default(),
|
||||
event: (Some(EventA(2)), None),
|
||||
},
|
||||
FilteredEventDetails {
|
||||
phase: Phase::ApplyExtrinsic(1),
|
||||
block_hash: <DefaultConfig as Config>::Hash::default(),
|
||||
block_hash: <SubstrateConfig as Config>::Hash::default(),
|
||||
event: (None, Some(EventB(false))),
|
||||
},
|
||||
FilteredEventDetails {
|
||||
phase: Phase::ApplyExtrinsic(2),
|
||||
block_hash: <DefaultConfig as Config>::Hash::default(),
|
||||
block_hash: <SubstrateConfig as Config>::Hash::default(),
|
||||
event: (None, Some(EventB(true))),
|
||||
},
|
||||
FilteredEventDetails {
|
||||
phase: Phase::ApplyExtrinsic(3),
|
||||
block_hash: <DefaultConfig as Config>::Hash::default(),
|
||||
block_hash: <SubstrateConfig as Config>::Hash::default(),
|
||||
event: (Some(EventA(3)), None),
|
||||
},
|
||||
];
|
||||
@@ -397,11 +391,11 @@ mod test {
|
||||
|
||||
#[tokio::test]
|
||||
async fn filter_no_events_from_stream() {
|
||||
let metadata = Arc::new(RwLock::new(metadata::<PalletEvents>()));
|
||||
let metadata = metadata::<PalletEvents>();
|
||||
|
||||
// Filter out fake event stream to select events matching `EventC` (none exist).
|
||||
let actual: Vec<_> =
|
||||
FilterEvents::<_, DefaultConfig, (EventC,)>::new(events_stream(metadata))
|
||||
FilterEvents::<_, SubstrateConfig, (EventC,)>::new(events_stream(metadata))
|
||||
.map(|e| e.unwrap())
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
+43
-86
@@ -2,108 +2,65 @@
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! This module exposes the ability to work with events generated by a given block.
|
||||
//! Subxt can either attempt to statically decode events into known types, or it
|
||||
//! can hand back details of the raw event without knowing what shape its contents
|
||||
//! are (this may be useful if we don't know what exactly we're looking for).
|
||||
//!
|
||||
//! This module is wrapped by the generated API in `RuntimeAPI::EventsApi`.
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ## Subscribe to all events
|
||||
//!
|
||||
//! Users can subscribe to all emitted events from blocks using `subscribe()`.
|
||||
//!
|
||||
//! To subscribe to all events from just the finalized blocks use `subscribe_finalized()`.
|
||||
//!
|
||||
//! To obtain the events from a given block use `at()`.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # use futures::StreamExt;
|
||||
//! # use subxt::{ClientBuilder, DefaultConfig, PolkadotExtrinsicParams};
|
||||
//! # #[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata.scale")]
|
||||
//! # pub mod polkadot {}
|
||||
//! # #[tokio::main]
|
||||
//! # async fn main() {
|
||||
//! # let api = ClientBuilder::new()
|
||||
//! # .build()
|
||||
//! # .await
|
||||
//! # .unwrap()
|
||||
//! # .to_runtime_api::<polkadot::RuntimeApi<DefaultConfig, PolkadotExtrinsicParams<DefaultConfig>>>();
|
||||
//! let mut events = api.events().subscribe().await.unwrap();
|
||||
//!
|
||||
//! while let Some(ev) = events.next().await {
|
||||
//! // Obtain all events from this block.
|
||||
//! let ev: subxt::Events<_, _> = ev.unwrap();
|
||||
//! // Print block hash.
|
||||
//! println!("Event at block hash {:?}", ev.block_hash());
|
||||
//! // Iterate over all events.
|
||||
//! let mut iter = ev.iter();
|
||||
//! while let Some(event_details) = iter.next() {
|
||||
//! println!("Event details {:?}", event_details);
|
||||
//! }
|
||||
//! }
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Filter events
|
||||
//!
|
||||
//! The subxt exposes the ability to filter events via the `filter_events()` function.
|
||||
//!
|
||||
//! The function filters events from the provided tuple. If 1-tuple is provided, the events are
|
||||
//! returned directly. Otherwise, we'll be given a corresponding tuple of `Option`'s, with exactly
|
||||
//! one variant populated each time.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # use futures::StreamExt;
|
||||
//! # use subxt::{ClientBuilder, DefaultConfig, PolkadotExtrinsicParams};
|
||||
//!
|
||||
//! #[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata.scale")]
|
||||
//! pub mod polkadot {}
|
||||
//!
|
||||
//! # #[tokio::main]
|
||||
//! # async fn main() {
|
||||
//! # let api = ClientBuilder::new()
|
||||
//! # .build()
|
||||
//! # .await
|
||||
//! # .unwrap()
|
||||
//! # .to_runtime_api::<polkadot::RuntimeApi<DefaultConfig, PolkadotExtrinsicParams<DefaultConfig>>>();
|
||||
//!
|
||||
//! let mut transfer_events = api
|
||||
//! .events()
|
||||
//! .subscribe()
|
||||
//! .await
|
||||
//! .unwrap()
|
||||
//! .filter_events::<(polkadot::balances::events::Transfer,)>();
|
||||
//!
|
||||
//! while let Some(transfer_event) = transfer_events.next().await {
|
||||
//! println!("Balance transfer event: {transfer_event:?}");
|
||||
//! }
|
||||
//! # }
|
||||
//! ```
|
||||
//! This module exposes the types and such necessary for working with events.
|
||||
//! The two main entry points into events are [`crate::OnlineClient::events()`]
|
||||
//! and calls like [crate::tx::TxProgress::wait_for_finalized_success()].
|
||||
|
||||
mod event_subscription;
|
||||
mod events_client;
|
||||
mod events_type;
|
||||
mod filter_events;
|
||||
|
||||
pub use event_subscription::{
|
||||
subscribe,
|
||||
subscribe_finalized,
|
||||
subscribe_to_block_headers_filling_in_gaps,
|
||||
EventSub,
|
||||
EventSubscription,
|
||||
FinalizedEventSub,
|
||||
};
|
||||
pub use events_client::{
|
||||
// Exposed only for testing:
|
||||
subscribe_to_block_headers_filling_in_gaps,
|
||||
EventsClient,
|
||||
};
|
||||
pub use events_type::{
|
||||
at,
|
||||
DecodedValue,
|
||||
EventDetails,
|
||||
Events,
|
||||
RawEventDetails,
|
||||
};
|
||||
pub use filter_events::{
|
||||
EventFilter,
|
||||
FilterEvents,
|
||||
FilteredEventDetails,
|
||||
};
|
||||
|
||||
use codec::{
|
||||
Decode,
|
||||
Encode,
|
||||
};
|
||||
|
||||
/// Trait to uniquely identify the events's identity from the runtime metadata.
|
||||
///
|
||||
/// Generated API structures that represent an event implement this trait.
|
||||
///
|
||||
/// The trait is utilized to decode emitted events from a block, via obtaining the
|
||||
/// form of the `Event` from the metadata.
|
||||
pub trait StaticEvent: Decode {
|
||||
/// Pallet name.
|
||||
const PALLET: &'static str;
|
||||
/// Event name.
|
||||
const EVENT: &'static str;
|
||||
|
||||
/// Returns true if the given pallet and event names match this event.
|
||||
fn is_event(pallet: &str, event: &str) -> bool {
|
||||
Self::PALLET == pallet && Self::EVENT == event
|
||||
}
|
||||
}
|
||||
|
||||
/// A phase of a block's execution.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Decode, Encode)]
|
||||
pub enum Phase {
|
||||
/// Applying an extrinsic.
|
||||
ApplyExtrinsic(u32),
|
||||
/// Finalizing the block.
|
||||
Finalization,
|
||||
/// Initializing the block.
|
||||
Initialization,
|
||||
}
|
||||
|
||||
+75
-338
@@ -10,164 +10,80 @@
|
||||
//! - [Query constants](https://docs.substrate.io/how-to-guides/v3/basics/configurable-constants/) (Constants)
|
||||
//! - [Subscribe to events](https://docs.substrate.io/v3/runtime/events-and-errors/) (Events)
|
||||
//!
|
||||
//! # Initializing the API client
|
||||
//!
|
||||
//! # Generate the runtime API
|
||||
//! To interact with a node, you'll need to construct a client.
|
||||
//!
|
||||
//! Subxt generates a runtime API from downloaded static metadata. The metadata can be downloaded using the
|
||||
//! ```no_run
|
||||
//! use subxt::{OnlineClient, PolkadotConfig};
|
||||
//!
|
||||
//! # #[tokio::main]
|
||||
//! # async fn main() {
|
||||
//! let api = OnlineClient::<PolkadotConfig>::new().await.unwrap();
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! This default client connects to a locally running node, but can be configured to point anywhere.
|
||||
//! Additionally, an [`crate::OfflineClient`] is available to perform operations that don't require a
|
||||
//! network connection to a node.
|
||||
//!
|
||||
//! The client takes a type parameter, here [`crate::PolkadotConfig`], which bakes in assumptions about
|
||||
//! the structure of extrinsics and the underlying types used by the node for things like block numbers.
|
||||
//! If the node you'd like to interact with deviates from Polkadot or the default Substrate node in these
|
||||
//! areas, you'll need to configure them by implementing the [`crate::config::Config`] type yourself.
|
||||
//!
|
||||
//! # Generating runtime types
|
||||
//!
|
||||
//! Subxt can generate types at compile time to help you interact with a node. The metadata can be downloaded using the
|
||||
//! [subxt-cli](https://crates.io/crates/subxt-cli) tool.
|
||||
//!
|
||||
//! To generate the runtime API, use the `subxt` attribute which points at downloaded static metadata.
|
||||
//! To generate the types, use the `subxt` attribute which points at downloaded static metadata.
|
||||
//!
|
||||
//! ```ignore
|
||||
//! #[subxt::subxt(runtime_metadata_path = "metadata.scale")]
|
||||
//! pub mod node_runtime { }
|
||||
//! ```
|
||||
//!
|
||||
//! The `node_runtime` has the following hierarchy:
|
||||
//! For more information, please visit the [subxt-codegen](https://docs.rs/subxt-codegen/latest/subxt_codegen/)
|
||||
//! documentation.
|
||||
//!
|
||||
//! ```rust
|
||||
//! pub mod node_runtime {
|
||||
//! pub mod PalletName {
|
||||
//! pub mod calls { }
|
||||
//! pub mod storage { }
|
||||
//! pub mod constants { }
|
||||
//! pub mod events { }
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//! # Interacting with the API
|
||||
//!
|
||||
//! For more information regarding the `node_runtime` hierarchy, please visit the
|
||||
//! [subxt-codegen](https://docs.rs/subxt-codegen/latest/subxt_codegen/) documentation.
|
||||
//! Once instantiated, a client, exposes four functions:
|
||||
//! - `.tx()` for submitting extrinsics/transactions. See [`crate::tx::TxClient`] for more details, or see
|
||||
//! the [balance_transfer](../examples/examples/balance_transfer.rs) example.
|
||||
//! - `.storage()` for fetching and iterating over storage entries. See [`crate::storage::StorageClient`] for more details, or see
|
||||
//! the [fetch_staking_details](../examples/examples/fetch_staking_details.rs) example.
|
||||
//! - `.constants()` for getting hold of constants. See [`crate::constants::ConstantsClient`] for more details, or see
|
||||
//! the [fetch_constants](../examples/examples/fetch_constants.rs) example.
|
||||
//! - `.events()` for subscribing/obtaining events. See [`crate::events::EventsClient`] for more details, or see:
|
||||
//! - [subscribe_all_events](../examples/examples/subscribe_all_events.rs): Subscribe to events emitted from blocks.
|
||||
//! - [subscribe_one_event](../examples/examples/subscribe_one_event.rs): Subscribe and filter by one event.
|
||||
//! - [subscribe_some_events](../examples/examples/subscribe_some_events.rs): Subscribe and filter event.
|
||||
//!
|
||||
//! # Static Metadata Validation
|
||||
//!
|
||||
//! # Initializing the API client
|
||||
//! If you use types generated by the [`crate::subxt`] macro, there is a chance that they will fall out of sync
|
||||
//! with the actual state of the node you're trying to interact with.
|
||||
//!
|
||||
//! When you attempt to use any of these static types to interact with a node, Subxt will validate that they are
|
||||
//! still compatible and issue an error if they have deviated.
|
||||
//!
|
||||
//! Additionally, you can validate that the entirety of the statically generated code aligns with a node like so:
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use subxt::{ClientBuilder, DefaultConfig, PolkadotExtrinsicParams};
|
||||
//! use subxt::{OnlineClient, PolkadotConfig};
|
||||
//!
|
||||
//! #[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata.scale")]
|
||||
//! pub mod polkadot {}
|
||||
//!
|
||||
//! # #[tokio::main]
|
||||
//! # async fn main() {
|
||||
//! let api = ClientBuilder::new()
|
||||
//! .set_url("wss://rpc.polkadot.io:443")
|
||||
//! .build()
|
||||
//! .await
|
||||
//! .unwrap()
|
||||
//! .to_runtime_api::<polkadot::RuntimeApi<DefaultConfig, PolkadotExtrinsicParams<DefaultConfig>>>();
|
||||
//! # }
|
||||
//! ```
|
||||
//! let api = OnlineClient::<PolkadotConfig>::new().await.unwrap();
|
||||
//!
|
||||
//! The `RuntimeApi` type is generated by the `subxt` macro from the supplied metadata. This can be parameterized with user
|
||||
//! supplied implementations for the `Config` and `Extra` types, if the default implementation differs from the target
|
||||
//! chain.
|
||||
//!
|
||||
//! To ensure that extrinsics are properly submitted, during the build phase of the Client the
|
||||
//! runtime metadata of the node is downloaded. If the URL is not specified (`set_url`), the local host is used instead.
|
||||
//!
|
||||
//!
|
||||
//! # Submit Extrinsics
|
||||
//!
|
||||
//! Extrinsics are obtained using the API's `RuntimeApi::tx()` method, followed by `pallet_name()` and then the
|
||||
//! `call_item_name()`.
|
||||
//!
|
||||
//! Submit an extrinsic, returning success once the transaction is validated and accepted into the pool:
|
||||
//!
|
||||
//! Please visit the [balance_transfer](../examples/examples/balance_transfer.rs) example for more details.
|
||||
//!
|
||||
//!
|
||||
//! # Querying Storage
|
||||
//!
|
||||
//! The runtime storage is queried via the generated `RuntimeApi::storage()` method, followed by the `pallet_name()` and
|
||||
//! then the `storage_item_name()`.
|
||||
//!
|
||||
//! Please visit the [fetch_staking_details](../examples/examples/fetch_staking_details.rs) example for more details.
|
||||
//!
|
||||
//! # Query Constants
|
||||
//!
|
||||
//! Constants are embedded into the node's metadata.
|
||||
//!
|
||||
//! The subxt offers the ability to query constants from the runtime metadata (metadata downloaded when constructing
|
||||
//! the client, *not* the one provided for API generation).
|
||||
//!
|
||||
//! To query constants use the generated `RuntimeApi::constants()` method, followed by the `pallet_name()` and then the
|
||||
//! `constant_item_name()`.
|
||||
//!
|
||||
//! Please visit the [fetch_constants](../examples/examples/fetch_constants.rs) example for more details.
|
||||
//!
|
||||
//! # Subscribe to Events
|
||||
//!
|
||||
//! To subscribe to events, use the generated `RuntimeApi::events()` method which exposes:
|
||||
//! - `subscribe()` - Subscribe to events emitted from blocks. These blocks haven't necessarily been finalised.
|
||||
//! - `subscribe_finalized()` - Subscribe to events from finalized blocks.
|
||||
//! - `at()` - Obtain events at a given block hash.
|
||||
//!
|
||||
//!
|
||||
//! *Examples*
|
||||
//! - [subscribe_all_events](../examples/examples/subscribe_all_events.rs): Subscribe to events emitted from blocks.
|
||||
//! - [subscribe_one_event](../examples/examples/subscribe_one_event.rs): Subscribe and filter by one event.
|
||||
//! - [subscribe_some_events](../examples/examples/subscribe_some_events.rs): Subscribe and filter event.
|
||||
//!
|
||||
//! # Static Metadata Validation
|
||||
//!
|
||||
//! There are two types of metadata that the subxt is aware of:
|
||||
//! - static metadata: Metadata used for generating the API.
|
||||
//! - runtime metadata: Metadata downloaded from the target node when a subxt client is created.
|
||||
//!
|
||||
//! There are cases when the static metadata is different from the runtime metadata of a node.
|
||||
//! Such is the case when the node performs a runtime update.
|
||||
//!
|
||||
//! To ensure that subxt can properly communicate with the target node the static metadata is validated
|
||||
//! against the runtime metadata of the node.
|
||||
//!
|
||||
//! This validation is performed at the Call, Constant, and Storage levels, as well for the entire metadata.
|
||||
//! The level of granularity ensures that the users can still submit a given call, even if another
|
||||
//! call suffered changes.
|
||||
//!
|
||||
//! Full metadata validation:
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # use subxt::{ClientBuilder, DefaultConfig, PolkadotExtrinsicParams};
|
||||
//! # #[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata.scale")]
|
||||
//! # pub mod polkadot {}
|
||||
//! # #[tokio::main]
|
||||
//! # async fn main() {
|
||||
//! # let api = ClientBuilder::new()
|
||||
//! # .build()
|
||||
//! # .await
|
||||
//! # .unwrap()
|
||||
//! # .to_runtime_api::<polkadot::RuntimeApi<DefaultConfig, PolkadotExtrinsicParams<DefaultConfig>>>();
|
||||
//! // To make sure that all of our statically generated pallets are compatible with the
|
||||
//! // runtime node, we can run this check:
|
||||
//! api.validate_metadata().unwrap();
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! Call level validation:
|
||||
//!
|
||||
//! ```ignore
|
||||
//! # use sp_keyring::AccountKeyring;
|
||||
//! # use subxt::{ClientBuilder, DefaultConfig, PolkadotExtrinsicParams};
|
||||
//! # #[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata.scale")]
|
||||
//! # pub mod polkadot {}
|
||||
//! # #[tokio::main]
|
||||
//! # async fn main() {
|
||||
//! # let api = ClientBuilder::new()
|
||||
//! # .build()
|
||||
//! # .await
|
||||
//! # .unwrap()
|
||||
//! # .to_runtime_api::<polkadot::RuntimeApi<DefaultConfig, PolkadotExtrinsicParams<DefaultConfig>>>();
|
||||
//! // Submit the `transfer` extrinsic from Alice's account to Bob's.
|
||||
//! let dest = AccountKeyring::Bob.to_account_id().into();
|
||||
//!
|
||||
//! let extrinsic = api
|
||||
//! .tx()
|
||||
//! .balances()
|
||||
//! // Constructing an extrinsic will fail if the metadata
|
||||
//! // is not in sync with the generated API.
|
||||
//! .transfer(dest, 123_456_789_012_345)
|
||||
//! .unwrap();
|
||||
//! if let Err(_e) = polkadot::validate_codegen(&api) {
|
||||
//! println!("Generated code is not up to date with node we're connected to");
|
||||
//! }
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
@@ -204,221 +120,42 @@
|
||||
)]
|
||||
#![allow(clippy::type_complexity)]
|
||||
|
||||
pub use frame_metadata::StorageHasher;
|
||||
pub use subxt_macro::subxt;
|
||||
|
||||
pub use bitvec;
|
||||
pub use codec;
|
||||
pub use sp_core;
|
||||
pub use sp_runtime;
|
||||
|
||||
use codec::{
|
||||
Decode,
|
||||
DecodeAll,
|
||||
Encode,
|
||||
};
|
||||
use core::fmt::Debug;
|
||||
use derivative::Derivative;
|
||||
|
||||
mod client;
|
||||
mod config;
|
||||
mod error;
|
||||
pub mod client;
|
||||
pub mod config;
|
||||
pub mod constants;
|
||||
pub mod dynamic;
|
||||
pub mod error;
|
||||
pub mod events;
|
||||
pub mod extrinsic;
|
||||
mod metadata;
|
||||
pub mod metadata;
|
||||
pub mod rpc;
|
||||
pub mod storage;
|
||||
mod transaction;
|
||||
pub mod updates;
|
||||
pub mod tx;
|
||||
pub mod utils;
|
||||
|
||||
// Expose a few of the most common types at root,
|
||||
// but leave most types behind their respoctive modules.
|
||||
pub use crate::{
|
||||
client::{
|
||||
Client,
|
||||
ClientBuilder,
|
||||
SubmittableExtrinsic,
|
||||
OfflineClient,
|
||||
OnlineClient,
|
||||
},
|
||||
config::{
|
||||
Config,
|
||||
DefaultConfig,
|
||||
},
|
||||
error::{
|
||||
BasicError,
|
||||
Error,
|
||||
GenericError,
|
||||
HasModuleError,
|
||||
ModuleError,
|
||||
ModuleErrorData,
|
||||
RuntimeError,
|
||||
TransactionError,
|
||||
},
|
||||
events::{
|
||||
EventDetails,
|
||||
Events,
|
||||
RawEventDetails,
|
||||
},
|
||||
extrinsic::{
|
||||
PairSigner,
|
||||
PolkadotExtrinsicParams,
|
||||
PolkadotExtrinsicParamsBuilder,
|
||||
SubstrateExtrinsicParams,
|
||||
SubstrateExtrinsicParamsBuilder,
|
||||
},
|
||||
metadata::{
|
||||
ErrorMetadata,
|
||||
Metadata,
|
||||
MetadataError,
|
||||
PalletMetadata,
|
||||
},
|
||||
rpc::{
|
||||
BlockNumber,
|
||||
ReadProof,
|
||||
RpcClient,
|
||||
SystemProperties,
|
||||
},
|
||||
storage::{
|
||||
KeyIter,
|
||||
StorageEntry,
|
||||
StorageEntryKey,
|
||||
StorageMapKey,
|
||||
},
|
||||
transaction::{
|
||||
TransactionEvents,
|
||||
TransactionInBlock,
|
||||
TransactionProgress,
|
||||
TransactionStatus,
|
||||
PolkadotConfig,
|
||||
SubstrateConfig,
|
||||
},
|
||||
error::Error,
|
||||
metadata::Metadata,
|
||||
};
|
||||
|
||||
/// Trait to uniquely identify the call (extrinsic)'s identity from the runtime metadata.
|
||||
///
|
||||
/// Generated API structures that represent each of the different possible
|
||||
/// calls to a node each implement this trait.
|
||||
///
|
||||
/// When encoding an extrinsic, we use this information to know how to map
|
||||
/// the call to the specific pallet and call index needed by a particular node.
|
||||
pub trait Call: Encode {
|
||||
/// Pallet name.
|
||||
const PALLET: &'static str;
|
||||
/// Function name.
|
||||
const FUNCTION: &'static str;
|
||||
|
||||
/// Returns true if the given pallet and function names match this call.
|
||||
fn is_call(pallet: &str, function: &str) -> bool {
|
||||
Self::PALLET == pallet && Self::FUNCTION == function
|
||||
}
|
||||
/// Re-export external crates that are made use of in the subxt API.
|
||||
pub mod ext {
|
||||
pub use bitvec;
|
||||
pub use codec;
|
||||
pub use frame_metadata;
|
||||
pub use scale_value;
|
||||
pub use sp_core;
|
||||
pub use sp_runtime;
|
||||
}
|
||||
|
||||
/// Trait to uniquely identify the events's identity from the runtime metadata.
|
||||
///
|
||||
/// Generated API structures that represent an event implement this trait.
|
||||
///
|
||||
/// The trait is utilized to decode emitted events from a block, via obtaining the
|
||||
/// form of the `Event` from the metadata.
|
||||
pub trait Event: Decode {
|
||||
/// Pallet name.
|
||||
const PALLET: &'static str;
|
||||
/// Event name.
|
||||
const EVENT: &'static str;
|
||||
|
||||
/// Returns true if the given pallet and event names match this event.
|
||||
fn is_event(pallet: &str, event: &str) -> bool {
|
||||
Self::PALLET == pallet && Self::EVENT == event
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps an already encoded byte vector, prevents being encoded as a raw byte vector as part of
|
||||
/// the transaction payload
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Encoded(pub Vec<u8>);
|
||||
|
||||
impl codec::Encode for Encoded {
|
||||
fn encode(&self) -> Vec<u8> {
|
||||
self.0.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
/// A phase of a block's execution.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Decode, Encode)]
|
||||
pub enum Phase {
|
||||
/// Applying an extrinsic.
|
||||
ApplyExtrinsic(u32),
|
||||
/// Finalizing the block.
|
||||
Finalization,
|
||||
/// Initializing the block.
|
||||
Initialization,
|
||||
}
|
||||
|
||||
/// A wrapper for any type `T` which implement encode/decode in a way compatible with `Vec<u8>`.
|
||||
///
|
||||
/// [`WrapperKeepOpaque`] stores the type only in its opaque format, aka as a `Vec<u8>`. To
|
||||
/// access the real type `T` [`Self::try_decode`] needs to be used.
|
||||
#[derive(Derivative, Encode, Decode)]
|
||||
#[derivative(
|
||||
Debug(bound = ""),
|
||||
Clone(bound = ""),
|
||||
PartialEq(bound = ""),
|
||||
Eq(bound = ""),
|
||||
Default(bound = ""),
|
||||
Hash(bound = "")
|
||||
)]
|
||||
pub struct WrapperKeepOpaque<T> {
|
||||
data: Vec<u8>,
|
||||
_phantom: PhantomDataSendSync<T>,
|
||||
}
|
||||
|
||||
impl<T: Decode> WrapperKeepOpaque<T> {
|
||||
/// Try to decode the wrapped type from the inner `data`.
|
||||
///
|
||||
/// Returns `None` if the decoding failed.
|
||||
pub fn try_decode(&self) -> Option<T> {
|
||||
T::decode_all(&mut &self.data[..]).ok()
|
||||
}
|
||||
|
||||
/// Returns the length of the encoded `T`.
|
||||
pub fn encoded_len(&self) -> usize {
|
||||
self.data.len()
|
||||
}
|
||||
|
||||
/// Returns the encoded data.
|
||||
pub fn encoded(&self) -> &[u8] {
|
||||
&self.data
|
||||
}
|
||||
|
||||
/// Create from the given encoded `data`.
|
||||
pub fn from_encoded(data: Vec<u8>) -> Self {
|
||||
Self {
|
||||
data,
|
||||
_phantom: PhantomDataSendSync::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A version of [`std::marker::PhantomData`] that is also Send and Sync (which is fine
|
||||
/// because regardless of the generic param, it is always possible to Send + Sync this
|
||||
/// 0 size type).
|
||||
#[derive(Derivative, Encode, Decode, scale_info::TypeInfo)]
|
||||
#[derivative(
|
||||
Clone(bound = ""),
|
||||
PartialEq(bound = ""),
|
||||
Debug(bound = ""),
|
||||
Eq(bound = ""),
|
||||
Default(bound = ""),
|
||||
Hash(bound = "")
|
||||
)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
#[doc(hidden)]
|
||||
pub struct PhantomDataSendSync<T>(core::marker::PhantomData<T>);
|
||||
|
||||
impl<T> PhantomDataSendSync<T> {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self(core::marker::PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<T> Send for PhantomDataSendSync<T> {}
|
||||
unsafe impl<T> Sync for PhantomDataSendSync<T> {}
|
||||
|
||||
/// This represents a key-value collection and is SCALE compatible
|
||||
/// with collections like BTreeMap. This has the same type params
|
||||
/// as `BTreeMap` which allows us to easily swap the two during codegen.
|
||||
pub type KeyedVec<K, V> = Vec<(K, V)>;
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
// 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 super::Metadata;
|
||||
use crate::{
|
||||
dynamic::DecodedValue,
|
||||
error::Error,
|
||||
};
|
||||
use codec::Decode;
|
||||
use frame_metadata::StorageEntryType;
|
||||
|
||||
/// This trait is implemented for types which can be decoded with the help of metadata.
|
||||
pub trait DecodeWithMetadata {
|
||||
/// The type that we'll get back from decoding.
|
||||
type Target;
|
||||
/// Given some metadata and a type ID, attempt to SCALE decode the provided bytes into `Self`.
|
||||
fn decode_with_metadata(
|
||||
bytes: &mut &[u8],
|
||||
type_id: u32,
|
||||
metadata: &Metadata,
|
||||
) -> Result<Self::Target, Error>;
|
||||
|
||||
/// Decode a storage item using metadata. By default, this uses the metadata to
|
||||
/// work out the type ID to use, but for static items we can short circuit this
|
||||
/// lookup.
|
||||
fn decode_storage_with_metadata(
|
||||
bytes: &mut &[u8],
|
||||
pallet_name: &str,
|
||||
storage_entry: &str,
|
||||
metadata: &Metadata,
|
||||
) -> Result<Self::Target, Error> {
|
||||
let ty = &metadata.pallet(pallet_name)?.storage(storage_entry)?.ty;
|
||||
|
||||
let id = match ty {
|
||||
StorageEntryType::Plain(ty) => ty.id(),
|
||||
StorageEntryType::Map { value, .. } => value.id(),
|
||||
};
|
||||
|
||||
Self::decode_with_metadata(bytes, id, metadata)
|
||||
}
|
||||
}
|
||||
|
||||
// Things can be dynamically decoded to our Value type:
|
||||
impl DecodeWithMetadata for DecodedValue {
|
||||
type Target = Self;
|
||||
fn decode_with_metadata(
|
||||
bytes: &mut &[u8],
|
||||
type_id: u32,
|
||||
metadata: &Metadata,
|
||||
) -> Result<Self::Target, Error> {
|
||||
let res = scale_value::scale::decode_as_type(bytes, type_id, metadata.types())?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
/// Any type implementing [`Decode`] can also be decoded with the help of metadata.
|
||||
pub struct DecodeStaticType<T>(std::marker::PhantomData<T>);
|
||||
|
||||
impl<T: Decode> DecodeWithMetadata for DecodeStaticType<T> {
|
||||
type Target = T;
|
||||
|
||||
fn decode_with_metadata(
|
||||
bytes: &mut &[u8],
|
||||
_type_id: u32,
|
||||
_metadata: &Metadata,
|
||||
) -> Result<Self::Target, Error> {
|
||||
T::decode(bytes).map_err(|e| e.into())
|
||||
}
|
||||
|
||||
fn decode_storage_with_metadata(
|
||||
bytes: &mut &[u8],
|
||||
_pallet_name: &str,
|
||||
_storage_entry: &str,
|
||||
_metadata: &Metadata,
|
||||
) -> Result<Self::Target, Error> {
|
||||
T::decode(bytes).map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// 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 crate::{
|
||||
dynamic::Value,
|
||||
error::Error,
|
||||
metadata::Metadata,
|
||||
};
|
||||
use codec::Encode;
|
||||
|
||||
/// This trait is implemented for types which can be encoded with the help of metadata.
|
||||
pub trait EncodeWithMetadata {
|
||||
/// SCALE encode this type to bytes, possibly with the help of metadata.
|
||||
fn encode_with_metadata(
|
||||
&self,
|
||||
type_id: u32,
|
||||
metadata: &Metadata,
|
||||
bytes: &mut Vec<u8>,
|
||||
) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
impl EncodeWithMetadata for Value<()> {
|
||||
fn encode_with_metadata(
|
||||
&self,
|
||||
type_id: u32,
|
||||
metadata: &Metadata,
|
||||
bytes: &mut Vec<u8>,
|
||||
) -> Result<(), Error> {
|
||||
scale_value::scale::encode_as_type(self, type_id, metadata.types(), bytes)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Any type implementing [`Encode`] can also be encoded with the help of metadata.
|
||||
pub struct EncodeStaticType<T>(pub T);
|
||||
|
||||
impl<T: Encode> EncodeWithMetadata for EncodeStaticType<T> {
|
||||
fn encode_with_metadata(
|
||||
&self,
|
||||
_type_id: u32,
|
||||
_metadata: &Metadata,
|
||||
bytes: &mut Vec<u8>,
|
||||
) -> Result<(), Error> {
|
||||
self.0.encode_to(bytes);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// We can transparently Encode anything wrapped in EncodeStaticType, too.
|
||||
impl<E: Encode> Encode for EncodeStaticType<E> {
|
||||
fn size_hint(&self) -> usize {
|
||||
self.0.size_hint()
|
||||
}
|
||||
fn encode_to<T: codec::Output + ?Sized>(&self, dest: &mut T) {
|
||||
self.0.encode_to(dest)
|
||||
}
|
||||
fn encode(&self) -> Vec<u8> {
|
||||
self.0.encode()
|
||||
}
|
||||
fn using_encoded<R, F: FnOnce(&[u8]) -> R>(&self, f: F) -> R {
|
||||
self.0.using_encoded(f)
|
||||
}
|
||||
fn encoded_size(&self) -> usize {
|
||||
self.0.encoded_size()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// 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.
|
||||
|
||||
/// Locate an item of a known type in the metadata.
|
||||
/// We should already know that the item we're looking
|
||||
/// for is a call or event for instance, and then with this,
|
||||
/// we can dig up details for that item in the metadata.
|
||||
pub trait MetadataLocation {
|
||||
/// The pallet in which the item lives.
|
||||
fn pallet(&self) -> &str;
|
||||
/// The name of the item.
|
||||
fn item(&self) -> &str;
|
||||
}
|
||||
+146
-133
@@ -3,7 +3,6 @@
|
||||
// see LICENSE for license details.
|
||||
|
||||
use super::hash_cache::HashCache;
|
||||
use crate::Call;
|
||||
use codec::Error as CodecError;
|
||||
use frame_metadata::{
|
||||
PalletConstantMetadata,
|
||||
@@ -16,8 +15,8 @@ use frame_metadata::{
|
||||
use parking_lot::RwLock;
|
||||
use scale_info::{
|
||||
form::PortableForm,
|
||||
PortableRegistry,
|
||||
Type,
|
||||
Variant,
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
@@ -75,7 +74,11 @@ struct MetadataInner {
|
||||
metadata: RuntimeMetadataV14,
|
||||
pallets: HashMap<String, PalletMetadata>,
|
||||
events: HashMap<(u8, u8), EventMetadata>,
|
||||
// Errors are hashed by pallet index.
|
||||
errors: HashMap<(u8, u8), ErrorMetadata>,
|
||||
// Type of the DispatchError type, which is what comes back if
|
||||
// an extrinsic fails.
|
||||
dispatch_error_ty: Option<u32>,
|
||||
// The hashes uniquely identify parts of the metadata; different
|
||||
// hashes mean some type difference exists between static and runtime
|
||||
// versions. We cache them here to avoid recalculating:
|
||||
@@ -93,7 +96,7 @@ pub struct Metadata {
|
||||
|
||||
impl Metadata {
|
||||
/// Returns a reference to [`PalletMetadata`].
|
||||
pub fn pallet(&self, name: &'static str) -> Result<&PalletMetadata, MetadataError> {
|
||||
pub fn pallet(&self, name: &str) -> Result<&PalletMetadata, MetadataError> {
|
||||
self.inner
|
||||
.pallets
|
||||
.get(name)
|
||||
@@ -128,6 +131,16 @@ impl Metadata {
|
||||
Ok(error)
|
||||
}
|
||||
|
||||
/// Return the DispatchError type ID if it exists.
|
||||
pub fn dispatch_error_ty(&self) -> Option<u32> {
|
||||
self.inner.dispatch_error_ty
|
||||
}
|
||||
|
||||
/// Return the type registry embedded within the metadata.
|
||||
pub fn types(&self) -> &PortableRegistry {
|
||||
&self.inner.metadata.types
|
||||
}
|
||||
|
||||
/// Resolve a type definition.
|
||||
pub fn resolve_type(&self, id: u32) -> Option<&Type<PortableForm>> {
|
||||
self.inner.metadata.types.resolve(id)
|
||||
@@ -139,23 +152,25 @@ impl Metadata {
|
||||
}
|
||||
|
||||
/// Obtain the unique hash for a specific storage entry.
|
||||
pub fn storage_hash<S: crate::StorageEntry>(
|
||||
pub fn storage_hash(
|
||||
&self,
|
||||
pallet: &str,
|
||||
storage: &str,
|
||||
) -> Result<[u8; 32], MetadataError> {
|
||||
self.inner
|
||||
.cached_storage_hashes
|
||||
.get_or_insert(S::PALLET, S::STORAGE, || {
|
||||
subxt_metadata::get_storage_hash(
|
||||
&self.inner.metadata,
|
||||
S::PALLET,
|
||||
S::STORAGE,
|
||||
)
|
||||
.map_err(|e| {
|
||||
match e {
|
||||
subxt_metadata::NotFound::Pallet => MetadataError::PalletNotFound,
|
||||
subxt_metadata::NotFound::Item => MetadataError::StorageNotFound,
|
||||
}
|
||||
})
|
||||
.get_or_insert(pallet, storage, || {
|
||||
subxt_metadata::get_storage_hash(&self.inner.metadata, pallet, storage)
|
||||
.map_err(|e| {
|
||||
match e {
|
||||
subxt_metadata::NotFound::Pallet => {
|
||||
MetadataError::PalletNotFound
|
||||
}
|
||||
subxt_metadata::NotFound::Item => {
|
||||
MetadataError::StorageNotFound
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -183,21 +198,23 @@ impl Metadata {
|
||||
}
|
||||
|
||||
/// Obtain the unique hash for a call.
|
||||
pub fn call_hash<C: crate::Call>(&self) -> Result<[u8; 32], MetadataError> {
|
||||
pub fn call_hash(
|
||||
&self,
|
||||
pallet: &str,
|
||||
function: &str,
|
||||
) -> Result<[u8; 32], MetadataError> {
|
||||
self.inner
|
||||
.cached_call_hashes
|
||||
.get_or_insert(C::PALLET, C::FUNCTION, || {
|
||||
subxt_metadata::get_call_hash(
|
||||
&self.inner.metadata,
|
||||
C::PALLET,
|
||||
C::FUNCTION,
|
||||
)
|
||||
.map_err(|e| {
|
||||
match e {
|
||||
subxt_metadata::NotFound::Pallet => MetadataError::PalletNotFound,
|
||||
subxt_metadata::NotFound::Item => MetadataError::CallNotFound,
|
||||
}
|
||||
})
|
||||
.get_or_insert(pallet, function, || {
|
||||
subxt_metadata::get_call_hash(&self.inner.metadata, pallet, function)
|
||||
.map_err(|e| {
|
||||
match e {
|
||||
subxt_metadata::NotFound::Pallet => {
|
||||
MetadataError::PalletNotFound
|
||||
}
|
||||
subxt_metadata::NotFound::Item => MetadataError::CallNotFound,
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -222,7 +239,8 @@ impl Metadata {
|
||||
pub struct PalletMetadata {
|
||||
index: u8,
|
||||
name: String,
|
||||
calls: HashMap<String, u8>,
|
||||
call_indexes: HashMap<String, u8>,
|
||||
call_ty_id: Option<u32>,
|
||||
storage: HashMap<String, StorageEntryMetadata<PortableForm>>,
|
||||
constants: HashMap<String, PalletConstantMetadata<PortableForm>>,
|
||||
}
|
||||
@@ -238,15 +256,18 @@ impl PalletMetadata {
|
||||
self.index
|
||||
}
|
||||
|
||||
/// If calls exist for this pallet, this returns the type ID of the variant
|
||||
/// representing the different possible calls.
|
||||
pub fn call_ty_id(&self) -> Option<u32> {
|
||||
self.call_ty_id
|
||||
}
|
||||
|
||||
/// Attempt to resolve a call into an index in this pallet, failing
|
||||
/// if the call is not found in this pallet.
|
||||
pub fn call_index<C>(&self) -> Result<u8, MetadataError>
|
||||
where
|
||||
C: Call,
|
||||
{
|
||||
pub fn call_index(&self, function: &str) -> Result<u8, MetadataError> {
|
||||
let fn_index = *self
|
||||
.calls
|
||||
.get(C::FUNCTION)
|
||||
.call_indexes
|
||||
.get(function)
|
||||
.ok_or(MetadataError::CallNotFound)?;
|
||||
Ok(fn_index)
|
||||
}
|
||||
@@ -273,9 +294,12 @@ impl PalletMetadata {
|
||||
/// Metadata for specific events.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct EventMetadata {
|
||||
pallet: String,
|
||||
// The pallet name is shared across every event, so put it
|
||||
// behind an Arc to avoid lots of needless clones of it existing.
|
||||
pallet: Arc<str>,
|
||||
event: String,
|
||||
variant: Variant<PortableForm>,
|
||||
fields: Vec<(Option<String>, u32)>,
|
||||
docs: Vec<String>,
|
||||
}
|
||||
|
||||
impl EventMetadata {
|
||||
@@ -289,21 +313,25 @@ impl EventMetadata {
|
||||
&self.event
|
||||
}
|
||||
|
||||
/// Get the type def variant for the pallet event.
|
||||
pub fn variant(&self) -> &Variant<PortableForm> {
|
||||
&self.variant
|
||||
/// The names and types of each field in the event.
|
||||
pub fn fields(&self) -> &[(Option<String>, u32)] {
|
||||
&self.fields
|
||||
}
|
||||
|
||||
/// Documentation for this event.
|
||||
pub fn docs(&self) -> &[String] {
|
||||
&self.docs
|
||||
}
|
||||
}
|
||||
|
||||
/// Metadata for specific errors obtained from the pallet's `PalletErrorMetadata`.
|
||||
///
|
||||
/// This holds in memory information regarding the Pallet's name, Error's name, and the underlying
|
||||
/// metadata representation.
|
||||
/// Details about a specific runtime error.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ErrorMetadata {
|
||||
pallet: String,
|
||||
// The pallet name is shared across every event, so put it
|
||||
// behind an Arc to avoid lots of needless clones of it existing.
|
||||
pallet: Arc<str>,
|
||||
error: String,
|
||||
variant: Variant<PortableForm>,
|
||||
docs: Vec<String>,
|
||||
}
|
||||
|
||||
impl ErrorMetadata {
|
||||
@@ -312,29 +340,31 @@ impl ErrorMetadata {
|
||||
&self.pallet
|
||||
}
|
||||
|
||||
/// Get the name of the specific pallet error.
|
||||
/// The name of the error.
|
||||
pub fn error(&self) -> &str {
|
||||
&self.error
|
||||
}
|
||||
|
||||
/// Get the description of the specific pallet error.
|
||||
pub fn description(&self) -> &[String] {
|
||||
self.variant.docs()
|
||||
/// Documentation for the error.
|
||||
pub fn docs(&self) -> &[String] {
|
||||
&self.docs
|
||||
}
|
||||
}
|
||||
|
||||
/// Error originated from converting a runtime metadata [RuntimeMetadataPrefixed] to
|
||||
/// the internal [Metadata] representation.
|
||||
///
|
||||
/// The runtime metadata is converted when building the [crate::client::Client].
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum InvalidMetadataError {
|
||||
/// Invalid prefix
|
||||
#[error("Invalid prefix")]
|
||||
InvalidPrefix,
|
||||
/// Invalid version
|
||||
#[error("Invalid version")]
|
||||
InvalidVersion,
|
||||
/// Type missing from type registry
|
||||
#[error("Type {0} missing from type registry")]
|
||||
MissingType(u32),
|
||||
/// Type was not a variant/enum type
|
||||
#[error("Type {0} was not a variant/enum type")]
|
||||
TypeDefNotVariant(u32),
|
||||
}
|
||||
@@ -366,15 +396,18 @@ impl TryFrom<RuntimeMetadataPrefixed> for Metadata {
|
||||
.pallets
|
||||
.iter()
|
||||
.map(|pallet| {
|
||||
let calls = pallet.calls.as_ref().map_or(Ok(HashMap::new()), |call| {
|
||||
let type_def_variant = get_type_def_variant(call.ty.id())?;
|
||||
let calls = type_def_variant
|
||||
.variants()
|
||||
.iter()
|
||||
.map(|v| (v.name().clone(), v.index()))
|
||||
.collect();
|
||||
Ok(calls)
|
||||
})?;
|
||||
let call_ty_id = pallet.calls.as_ref().map(|c| c.ty.id());
|
||||
|
||||
let call_indexes =
|
||||
pallet.calls.as_ref().map_or(Ok(HashMap::new()), |call| {
|
||||
let type_def_variant = get_type_def_variant(call.ty.id())?;
|
||||
let call_indexes = type_def_variant
|
||||
.variants()
|
||||
.iter()
|
||||
.map(|v| (v.name().clone(), v.index()))
|
||||
.collect();
|
||||
Ok(call_indexes)
|
||||
})?;
|
||||
|
||||
let storage = pallet.storage.as_ref().map_or(HashMap::new(), |storage| {
|
||||
storage
|
||||
@@ -393,7 +426,8 @@ impl TryFrom<RuntimeMetadataPrefixed> for Metadata {
|
||||
let pallet_metadata = PalletMetadata {
|
||||
index: pallet.index,
|
||||
name: pallet.name.to_string(),
|
||||
calls,
|
||||
call_indexes,
|
||||
call_ty_id,
|
||||
storage,
|
||||
constants,
|
||||
};
|
||||
@@ -402,55 +436,54 @@ impl TryFrom<RuntimeMetadataPrefixed> for Metadata {
|
||||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
let pallet_events = metadata
|
||||
.pallets
|
||||
.iter()
|
||||
.filter_map(|pallet| {
|
||||
pallet.event.as_ref().map(|event| {
|
||||
let type_def_variant = get_type_def_variant(event.ty.id())?;
|
||||
Ok((pallet, type_def_variant))
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let events = pallet_events
|
||||
.iter()
|
||||
.flat_map(|(pallet, type_def_variant)| {
|
||||
type_def_variant.variants().iter().map(move |var| {
|
||||
let key = (pallet.index, var.index());
|
||||
let value = EventMetadata {
|
||||
pallet: pallet.name.clone(),
|
||||
event: var.name().clone(),
|
||||
variant: var.clone(),
|
||||
};
|
||||
(key, value)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let mut events = HashMap::<(u8, u8), EventMetadata>::new();
|
||||
for pallet in &metadata.pallets {
|
||||
if let Some(event) = &pallet.event {
|
||||
let pallet_name: Arc<str> = pallet.name.to_string().into();
|
||||
let event_type_id = event.ty.id();
|
||||
let event_variant = get_type_def_variant(event_type_id)?;
|
||||
for variant in event_variant.variants() {
|
||||
events.insert(
|
||||
(pallet.index, variant.index()),
|
||||
EventMetadata {
|
||||
pallet: pallet_name.clone(),
|
||||
event: variant.name().to_owned(),
|
||||
fields: variant
|
||||
.fields()
|
||||
.iter()
|
||||
.map(|f| (f.name().map(|n| n.to_owned()), f.ty().id()))
|
||||
.collect(),
|
||||
docs: variant.docs().to_vec(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let pallet_errors = metadata
|
||||
.pallets
|
||||
let mut errors = HashMap::<(u8, u8), ErrorMetadata>::new();
|
||||
for pallet in &metadata.pallets {
|
||||
if let Some(error) = &pallet.error {
|
||||
let pallet_name: Arc<str> = pallet.name.to_string().into();
|
||||
let error_variant = get_type_def_variant(error.ty.id())?;
|
||||
for variant in error_variant.variants() {
|
||||
errors.insert(
|
||||
(pallet.index, variant.index()),
|
||||
ErrorMetadata {
|
||||
pallet: pallet_name.clone(),
|
||||
error: variant.name().clone(),
|
||||
docs: variant.docs().to_vec(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let dispatch_error_ty = metadata
|
||||
.types
|
||||
.types()
|
||||
.iter()
|
||||
.filter_map(|pallet| {
|
||||
pallet.error.as_ref().map(|error| {
|
||||
let type_def_variant = get_type_def_variant(error.ty.id())?;
|
||||
Ok((pallet, type_def_variant))
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let errors = pallet_errors
|
||||
.iter()
|
||||
.flat_map(|(pallet, type_def_variant)| {
|
||||
type_def_variant.variants().iter().map(move |var| {
|
||||
let key = (pallet.index, var.index());
|
||||
let value = ErrorMetadata {
|
||||
pallet: pallet.name.clone(),
|
||||
error: var.name().clone(),
|
||||
variant: var.clone(),
|
||||
};
|
||||
(key, value)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
.find(|ty| ty.ty().path().segments() == ["sp_runtime", "DispatchError"])
|
||||
.map(|ty| ty.id());
|
||||
|
||||
Ok(Metadata {
|
||||
inner: Arc::new(MetadataInner {
|
||||
@@ -458,6 +491,7 @@ impl TryFrom<RuntimeMetadataPrefixed> for Metadata {
|
||||
pallets,
|
||||
events,
|
||||
errors,
|
||||
dispatch_error_ty,
|
||||
cached_metadata_hash: Default::default(),
|
||||
cached_call_hashes: Default::default(),
|
||||
cached_constant_hashes: Default::default(),
|
||||
@@ -470,7 +504,6 @@ impl TryFrom<RuntimeMetadataPrefixed> for Metadata {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::StorageEntryKey;
|
||||
use frame_metadata::{
|
||||
ExtrinsicMetadata,
|
||||
PalletStorageMetadata,
|
||||
@@ -553,14 +586,7 @@ mod tests {
|
||||
fn metadata_call_inner_cache() {
|
||||
let metadata = load_metadata();
|
||||
|
||||
#[derive(codec::Encode)]
|
||||
struct ValidCall;
|
||||
impl crate::Call for ValidCall {
|
||||
const PALLET: &'static str = "System";
|
||||
const FUNCTION: &'static str = "fill_block";
|
||||
}
|
||||
|
||||
let hash = metadata.call_hash::<ValidCall>();
|
||||
let hash = metadata.call_hash("System", "fill_block");
|
||||
|
||||
let mut call_number = 0;
|
||||
let hash_cached = metadata.inner.cached_call_hashes.get_or_insert(
|
||||
@@ -601,20 +627,7 @@ mod tests {
|
||||
#[test]
|
||||
fn metadata_storage_inner_cache() {
|
||||
let metadata = load_metadata();
|
||||
|
||||
#[derive(codec::Encode)]
|
||||
struct ValidStorage;
|
||||
impl crate::StorageEntry for ValidStorage {
|
||||
const PALLET: &'static str = "System";
|
||||
const STORAGE: &'static str = "Account";
|
||||
type Value = ();
|
||||
|
||||
fn key(&self) -> StorageEntryKey {
|
||||
unreachable!("Should not be called");
|
||||
}
|
||||
}
|
||||
|
||||
let hash = metadata.storage_hash::<ValidStorage>();
|
||||
let hash = metadata.storage_hash("System", "Account");
|
||||
|
||||
let mut call_number = 0;
|
||||
let hash_cached = metadata.inner.cached_storage_hashes.get_or_insert(
|
||||
|
||||
@@ -2,9 +2,16 @@
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Types representing the metadata obtained from a node.
|
||||
|
||||
mod decode_with_metadata;
|
||||
mod encode_with_metadata;
|
||||
mod hash_cache;
|
||||
mod metadata_location;
|
||||
mod metadata_type;
|
||||
|
||||
pub use metadata_location::MetadataLocation;
|
||||
|
||||
pub use metadata_type::{
|
||||
ErrorMetadata,
|
||||
EventMetadata,
|
||||
@@ -13,3 +20,13 @@ pub use metadata_type::{
|
||||
MetadataError,
|
||||
PalletMetadata,
|
||||
};
|
||||
|
||||
pub use decode_with_metadata::{
|
||||
DecodeStaticType,
|
||||
DecodeWithMetadata,
|
||||
};
|
||||
|
||||
pub use encode_with_metadata::{
|
||||
EncodeStaticType,
|
||||
EncodeWithMetadata,
|
||||
};
|
||||
|
||||
+61
-95
@@ -7,67 +7,29 @@
|
||||
//! This is used behind the scenes by various `subxt` APIs, but can
|
||||
//! also be used directly.
|
||||
//!
|
||||
//! # Examples
|
||||
//! # Example
|
||||
//!
|
||||
//! ## Fetch Storage
|
||||
//! Fetching storage keys
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # use subxt::{ClientBuilder, DefaultConfig, PolkadotExtrinsicParams};
|
||||
//! # use subxt::storage::StorageKeyPrefix;
|
||||
//! # use subxt::rpc::Rpc;
|
||||
//! use subxt::{ PolkadotConfig, OnlineClient, storage::StorageKey };
|
||||
//!
|
||||
//! #[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata.scale")]
|
||||
//! pub mod polkadot {}
|
||||
//!
|
||||
//! # #[tokio::main]
|
||||
//! # async fn main() {
|
||||
//! # let api = ClientBuilder::new()
|
||||
//! # .build()
|
||||
//! # .await
|
||||
//! # .unwrap()
|
||||
//! # .to_runtime_api::<polkadot::RuntimeApi<DefaultConfig, PolkadotExtrinsicParams<DefaultConfig>>>();
|
||||
//! // Storage prefix is `twox_128("System") ++ twox_128("ExtrinsicCount")`.
|
||||
//! let key = StorageKeyPrefix::new::<polkadot::system::storage::ExtrinsicCount>()
|
||||
//! .to_storage_key();
|
||||
//! let api = OnlineClient::<PolkadotConfig>::new().await.unwrap();
|
||||
//!
|
||||
//! // Obtain the RPC from a generated API
|
||||
//! let rpc: &Rpc<_> = api
|
||||
//! .client
|
||||
//! .rpc();
|
||||
//!
|
||||
//! let result = rpc.storage(&key, None).await.unwrap();
|
||||
//! println!("Storage result: {:?}", result);
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Fetch Keys
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # use subxt::{ClientBuilder, DefaultConfig, PolkadotExtrinsicParams};
|
||||
//! # use subxt::storage::StorageKeyPrefix;
|
||||
//! # use subxt::rpc::Rpc;
|
||||
//!
|
||||
//! #[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata.scale")]
|
||||
//! pub mod polkadot {}
|
||||
//!
|
||||
//! # #[tokio::main]
|
||||
//! # async fn main() {
|
||||
//! # let api = ClientBuilder::new()
|
||||
//! # .build()
|
||||
//! # .await
|
||||
//! # .unwrap()
|
||||
//! # .to_runtime_api::<polkadot::RuntimeApi<DefaultConfig, PolkadotExtrinsicParams<DefaultConfig>>>();
|
||||
//! let key = StorageKeyPrefix::new::<polkadot::xcm_pallet::storage::VersionNotifiers>()
|
||||
//! .to_storage_key();
|
||||
//!
|
||||
//! // Obtain the RPC from a generated API
|
||||
//! let rpc: &Rpc<_> = api
|
||||
//! .client
|
||||
//! .rpc();
|
||||
//! let key = polkadot::storage()
|
||||
//! .xcm_pallet()
|
||||
//! .version_notifiers_root()
|
||||
//! .to_bytes();
|
||||
//!
|
||||
//! // Fetch up to 10 keys.
|
||||
//! let keys = rpc
|
||||
//! .storage_keys_paged(Some(key), 10, None, None)
|
||||
//! let keys = api
|
||||
//! .rpc()
|
||||
//! .storage_keys_paged(&key, 10, None, None)
|
||||
//! .await
|
||||
//! .unwrap();
|
||||
//!
|
||||
@@ -88,10 +50,10 @@ use std::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::BasicError,
|
||||
error::Error,
|
||||
utils::PhantomDataSendSync,
|
||||
Config,
|
||||
Metadata,
|
||||
PhantomDataSendSync,
|
||||
};
|
||||
use codec::{
|
||||
Decode,
|
||||
@@ -198,7 +160,7 @@ pub type SystemProperties = serde_json::Map<String, serde_json::Value>;
|
||||
/// must be kept compatible with that type from the target substrate version.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum SubstrateTransactionStatus<Hash, BlockHash> {
|
||||
pub enum SubstrateTxStatus<Hash, BlockHash> {
|
||||
/// Transaction is part of the future queue.
|
||||
Future,
|
||||
/// Transaction is part of the ready queue.
|
||||
@@ -324,13 +286,13 @@ impl<T: Config> Rpc<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch a storage key
|
||||
/// Fetch the raw bytes for a given storage key
|
||||
pub async fn storage(
|
||||
&self,
|
||||
key: &StorageKey,
|
||||
key: &[u8],
|
||||
hash: Option<T::Hash>,
|
||||
) -> Result<Option<StorageData>, BasicError> {
|
||||
let params = rpc_params![key, hash];
|
||||
) -> Result<Option<StorageData>, Error> {
|
||||
let params = rpc_params![to_hex(key), hash];
|
||||
let data = self.client.request("state_getStorage", params).await?;
|
||||
Ok(data)
|
||||
}
|
||||
@@ -340,12 +302,13 @@ impl<T: Config> Rpc<T> {
|
||||
/// If `start_key` is passed, return next keys in storage in lexicographic order.
|
||||
pub async fn storage_keys_paged(
|
||||
&self,
|
||||
key: Option<StorageKey>,
|
||||
key: &[u8],
|
||||
count: u32,
|
||||
start_key: Option<StorageKey>,
|
||||
start_key: Option<&[u8]>,
|
||||
hash: Option<T::Hash>,
|
||||
) -> Result<Vec<StorageKey>, BasicError> {
|
||||
let params = rpc_params![key, count, start_key, hash];
|
||||
) -> Result<Vec<StorageKey>, Error> {
|
||||
let start_key = start_key.map(to_hex);
|
||||
let params = rpc_params![to_hex(key), count, start_key, hash];
|
||||
let data = self.client.request("state_getKeysPaged", params).await?;
|
||||
Ok(data)
|
||||
}
|
||||
@@ -353,10 +316,11 @@ impl<T: Config> Rpc<T> {
|
||||
/// Query historical storage entries
|
||||
pub async fn query_storage(
|
||||
&self,
|
||||
keys: Vec<StorageKey>,
|
||||
keys: impl IntoIterator<Item = &[u8]>,
|
||||
from: T::Hash,
|
||||
to: Option<T::Hash>,
|
||||
) -> Result<Vec<StorageChangeSet<T::Hash>>, BasicError> {
|
||||
) -> Result<Vec<StorageChangeSet<T::Hash>>, Error> {
|
||||
let keys: Vec<String> = keys.into_iter().map(to_hex).collect();
|
||||
let params = rpc_params![keys, from, to];
|
||||
self.client
|
||||
.request("state_queryStorage", params)
|
||||
@@ -367,9 +331,10 @@ impl<T: Config> Rpc<T> {
|
||||
/// Query historical storage entries
|
||||
pub async fn query_storage_at(
|
||||
&self,
|
||||
keys: &[StorageKey],
|
||||
keys: impl IntoIterator<Item = &[u8]>,
|
||||
at: Option<T::Hash>,
|
||||
) -> Result<Vec<StorageChangeSet<T::Hash>>, BasicError> {
|
||||
) -> Result<Vec<StorageChangeSet<T::Hash>>, Error> {
|
||||
let keys: Vec<String> = keys.into_iter().map(to_hex).collect();
|
||||
let params = rpc_params![keys, at];
|
||||
self.client
|
||||
.request("state_queryStorageAt", params)
|
||||
@@ -378,7 +343,7 @@ impl<T: Config> Rpc<T> {
|
||||
}
|
||||
|
||||
/// Fetch the genesis hash
|
||||
pub async fn genesis_hash(&self) -> Result<T::Hash, BasicError> {
|
||||
pub async fn genesis_hash(&self) -> Result<T::Hash, Error> {
|
||||
let block_zero = 0u32;
|
||||
let params = rpc_params![block_zero];
|
||||
let genesis_hash: Option<T::Hash> =
|
||||
@@ -387,7 +352,7 @@ impl<T: Config> Rpc<T> {
|
||||
}
|
||||
|
||||
/// Fetch the metadata
|
||||
pub async fn metadata(&self) -> Result<Metadata, BasicError> {
|
||||
pub async fn metadata(&self) -> Result<Metadata, Error> {
|
||||
let bytes: Bytes = self
|
||||
.client
|
||||
.request("state_getMetadata", rpc_params![])
|
||||
@@ -398,7 +363,7 @@ impl<T: Config> Rpc<T> {
|
||||
}
|
||||
|
||||
/// Fetch system properties
|
||||
pub async fn system_properties(&self) -> Result<SystemProperties, BasicError> {
|
||||
pub async fn system_properties(&self) -> Result<SystemProperties, Error> {
|
||||
Ok(self
|
||||
.client
|
||||
.request("system_properties", rpc_params![])
|
||||
@@ -406,22 +371,22 @@ impl<T: Config> Rpc<T> {
|
||||
}
|
||||
|
||||
/// Fetch system health
|
||||
pub async fn system_health(&self) -> Result<Health, BasicError> {
|
||||
pub async fn system_health(&self) -> Result<Health, Error> {
|
||||
Ok(self.client.request("system_health", rpc_params![]).await?)
|
||||
}
|
||||
|
||||
/// Fetch system chain
|
||||
pub async fn system_chain(&self) -> Result<String, BasicError> {
|
||||
pub async fn system_chain(&self) -> Result<String, Error> {
|
||||
Ok(self.client.request("system_chain", rpc_params![]).await?)
|
||||
}
|
||||
|
||||
/// Fetch system name
|
||||
pub async fn system_name(&self) -> Result<String, BasicError> {
|
||||
pub async fn system_name(&self) -> Result<String, Error> {
|
||||
Ok(self.client.request("system_name", rpc_params![]).await?)
|
||||
}
|
||||
|
||||
/// Fetch system version
|
||||
pub async fn system_version(&self) -> Result<String, BasicError> {
|
||||
pub async fn system_version(&self) -> Result<String, Error> {
|
||||
Ok(self.client.request("system_version", rpc_params![]).await?)
|
||||
}
|
||||
|
||||
@@ -429,7 +394,7 @@ impl<T: Config> Rpc<T> {
|
||||
pub async fn system_account_next_index(
|
||||
&self,
|
||||
account: &T::AccountId,
|
||||
) -> Result<T::Index, BasicError> {
|
||||
) -> Result<T::Index, Error> {
|
||||
Ok(self
|
||||
.client
|
||||
.request("system_accountNextIndex", rpc_params![account])
|
||||
@@ -440,7 +405,7 @@ impl<T: Config> Rpc<T> {
|
||||
pub async fn header(
|
||||
&self,
|
||||
hash: Option<T::Hash>,
|
||||
) -> Result<Option<T::Header>, BasicError> {
|
||||
) -> Result<Option<T::Header>, Error> {
|
||||
let params = rpc_params![hash];
|
||||
let header = self.client.request("chain_getHeader", params).await?;
|
||||
Ok(header)
|
||||
@@ -450,14 +415,14 @@ impl<T: Config> Rpc<T> {
|
||||
pub async fn block_hash(
|
||||
&self,
|
||||
block_number: Option<BlockNumber>,
|
||||
) -> Result<Option<T::Hash>, BasicError> {
|
||||
) -> Result<Option<T::Hash>, Error> {
|
||||
let params = rpc_params![block_number];
|
||||
let block_hash = self.client.request("chain_getBlockHash", params).await?;
|
||||
Ok(block_hash)
|
||||
}
|
||||
|
||||
/// Get a block hash of the latest finalized block
|
||||
pub async fn finalized_head(&self) -> Result<T::Hash, BasicError> {
|
||||
pub async fn finalized_head(&self) -> Result<T::Hash, Error> {
|
||||
let hash = self
|
||||
.client
|
||||
.request("chain_getFinalizedHead", rpc_params![])
|
||||
@@ -469,7 +434,7 @@ impl<T: Config> Rpc<T> {
|
||||
pub async fn block(
|
||||
&self,
|
||||
hash: Option<T::Hash>,
|
||||
) -> Result<Option<ChainBlock<T>>, BasicError> {
|
||||
) -> Result<Option<ChainBlock<T>>, Error> {
|
||||
let params = rpc_params![hash];
|
||||
let block = self.client.request("chain_getBlock", params).await?;
|
||||
Ok(block)
|
||||
@@ -483,7 +448,7 @@ impl<T: Config> Rpc<T> {
|
||||
pub async fn block_stats(
|
||||
&self,
|
||||
block_hash: T::Hash,
|
||||
) -> Result<Option<BlockStats>, BasicError> {
|
||||
) -> Result<Option<BlockStats>, Error> {
|
||||
let params = rpc_params![block_hash];
|
||||
let stats = self.client.request("dev_getBlockStats", params).await?;
|
||||
Ok(stats)
|
||||
@@ -492,9 +457,10 @@ impl<T: Config> Rpc<T> {
|
||||
/// Get proof of storage entries at a specific block's state.
|
||||
pub async fn read_proof(
|
||||
&self,
|
||||
keys: Vec<StorageKey>,
|
||||
keys: impl IntoIterator<Item = &[u8]>,
|
||||
hash: Option<T::Hash>,
|
||||
) -> Result<ReadProof<T::Hash>, BasicError> {
|
||||
) -> Result<ReadProof<T::Hash>, Error> {
|
||||
let keys: Vec<String> = keys.into_iter().map(to_hex).collect();
|
||||
let params = rpc_params![keys, hash];
|
||||
let proof = self.client.request("state_getReadProof", params).await?;
|
||||
Ok(proof)
|
||||
@@ -504,7 +470,7 @@ impl<T: Config> Rpc<T> {
|
||||
pub async fn runtime_version(
|
||||
&self,
|
||||
at: Option<T::Hash>,
|
||||
) -> Result<RuntimeVersion, BasicError> {
|
||||
) -> Result<RuntimeVersion, Error> {
|
||||
let params = rpc_params![at];
|
||||
let version = self
|
||||
.client
|
||||
@@ -514,7 +480,7 @@ impl<T: Config> Rpc<T> {
|
||||
}
|
||||
|
||||
/// Subscribe to blocks.
|
||||
pub async fn subscribe_blocks(&self) -> Result<Subscription<T::Header>, BasicError> {
|
||||
pub async fn subscribe_blocks(&self) -> Result<Subscription<T::Header>, Error> {
|
||||
let subscription = self
|
||||
.client
|
||||
.subscribe(
|
||||
@@ -530,7 +496,7 @@ impl<T: Config> Rpc<T> {
|
||||
/// Subscribe to finalized blocks.
|
||||
pub async fn subscribe_finalized_blocks(
|
||||
&self,
|
||||
) -> Result<Subscription<T::Header>, BasicError> {
|
||||
) -> Result<Subscription<T::Header>, Error> {
|
||||
let subscription = self
|
||||
.client
|
||||
.subscribe(
|
||||
@@ -545,7 +511,7 @@ impl<T: Config> Rpc<T> {
|
||||
/// Subscribe to runtime version updates that produce changes in the metadata.
|
||||
pub async fn subscribe_runtime_version(
|
||||
&self,
|
||||
) -> Result<Subscription<RuntimeVersion>, BasicError> {
|
||||
) -> Result<Subscription<RuntimeVersion>, Error> {
|
||||
let subscription = self
|
||||
.client
|
||||
.subscribe(
|
||||
@@ -561,7 +527,7 @@ impl<T: Config> Rpc<T> {
|
||||
pub async fn submit_extrinsic<X: Encode>(
|
||||
&self,
|
||||
extrinsic: X,
|
||||
) -> Result<T::Hash, BasicError> {
|
||||
) -> Result<T::Hash, Error> {
|
||||
let bytes: Bytes = extrinsic.encode().into();
|
||||
let params = rpc_params![bytes];
|
||||
let xt_hash = self
|
||||
@@ -575,8 +541,7 @@ impl<T: Config> Rpc<T> {
|
||||
pub async fn watch_extrinsic<X: Encode>(
|
||||
&self,
|
||||
extrinsic: X,
|
||||
) -> Result<Subscription<SubstrateTransactionStatus<T::Hash, T::Hash>>, BasicError>
|
||||
{
|
||||
) -> Result<Subscription<SubstrateTxStatus<T::Hash, T::Hash>>, Error> {
|
||||
let bytes: Bytes = extrinsic.encode().into();
|
||||
let params = rpc_params![bytes];
|
||||
let subscription = self
|
||||
@@ -596,14 +561,14 @@ impl<T: Config> Rpc<T> {
|
||||
key_type: String,
|
||||
suri: String,
|
||||
public: Bytes,
|
||||
) -> Result<(), BasicError> {
|
||||
) -> Result<(), Error> {
|
||||
let params = rpc_params![key_type, suri, public];
|
||||
self.client.request("author_insertKey", params).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate new session keys and returns the corresponding public keys.
|
||||
pub async fn rotate_keys(&self) -> Result<Bytes, BasicError> {
|
||||
pub async fn rotate_keys(&self) -> Result<Bytes, Error> {
|
||||
Ok(self
|
||||
.client
|
||||
.request("author_rotateKeys", rpc_params![])
|
||||
@@ -615,10 +580,7 @@ impl<T: Config> Rpc<T> {
|
||||
/// `session_keys` is the SCALE encoded session keys object from the runtime.
|
||||
///
|
||||
/// Returns `true` iff all private keys could be found.
|
||||
pub async fn has_session_keys(
|
||||
&self,
|
||||
session_keys: Bytes,
|
||||
) -> Result<bool, BasicError> {
|
||||
pub async fn has_session_keys(&self, session_keys: Bytes) -> Result<bool, Error> {
|
||||
let params = rpc_params![session_keys];
|
||||
Ok(self.client.request("author_hasSessionKeys", params).await?)
|
||||
}
|
||||
@@ -630,7 +592,7 @@ impl<T: Config> Rpc<T> {
|
||||
&self,
|
||||
public_key: Bytes,
|
||||
key_type: String,
|
||||
) -> Result<bool, BasicError> {
|
||||
) -> Result<bool, Error> {
|
||||
let params = rpc_params![public_key, key_type];
|
||||
Ok(self.client.request("author_hasKey", params).await?)
|
||||
}
|
||||
@@ -642,8 +604,8 @@ impl<T: Config> Rpc<T> {
|
||||
&self,
|
||||
encoded_signed: &[u8],
|
||||
at: Option<T::Hash>,
|
||||
) -> Result<ApplyExtrinsicResult, BasicError> {
|
||||
let params = rpc_params![format!("0x{}", hex::encode(encoded_signed)), at];
|
||||
) -> Result<ApplyExtrinsicResult, Error> {
|
||||
let params = rpc_params![to_hex(encoded_signed), at];
|
||||
let result_bytes: Bytes = self.client.request("system_dryRun", params).await?;
|
||||
let data: ApplyExtrinsicResult =
|
||||
codec::Decode::decode(&mut result_bytes.0.as_slice())?;
|
||||
@@ -669,6 +631,10 @@ async fn ws_transport(url: &str) -> Result<(WsSender, WsReceiver), RpcError> {
|
||||
.map_err(|e| RpcError::Transport(e.into()))
|
||||
}
|
||||
|
||||
fn to_hex(bytes: impl AsRef<[u8]>) -> String {
|
||||
format!("0x{}", hex::encode(bytes.as_ref()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
@@ -1,376 +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.
|
||||
|
||||
//! Query the runtime storage using [StorageClient].
|
||||
//!
|
||||
//! This module is the core of performing runtime storage queries. While you can
|
||||
//! work with it directly, it's prefer to use the generated `storage()` interface where
|
||||
//! possible.
|
||||
//!
|
||||
//! The exposed API is performing RPC calls to `state_getStorage` and `state_getKeysPaged`.
|
||||
//!
|
||||
//! A runtime storage entry can be of type:
|
||||
//! - [StorageEntryKey::Plain] for keys constructed just from the prefix
|
||||
//! `twox_128(pallet) ++ twox_128(storage_item)`
|
||||
//! - [StorageEntryKey::Map] for mapped keys constructed from the prefix,
|
||||
//! plus other arguments `twox_128(pallet) ++ twox_128(storage_item) ++ hash(arg1) ++ arg1`
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ## Fetch Storage Keys
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # use subxt::{ClientBuilder, DefaultConfig, PolkadotExtrinsicParams};
|
||||
//! # use subxt::storage::StorageClient;
|
||||
//!
|
||||
//! #[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata.scale")]
|
||||
//! pub mod polkadot {}
|
||||
//!
|
||||
//! # #[tokio::main]
|
||||
//! # async fn main() {
|
||||
//! # let api = ClientBuilder::new()
|
||||
//! # .build()
|
||||
//! # .await
|
||||
//! # .unwrap()
|
||||
//! # .to_runtime_api::<polkadot::RuntimeApi<DefaultConfig, PolkadotExtrinsicParams<DefaultConfig>>>();
|
||||
//! # // Obtain the storage client wrapper from the API.
|
||||
//! # let storage: StorageClient<_> = api.client.storage();
|
||||
//! // Fetch just the keys, returning up to 10 keys.
|
||||
//! let keys = storage
|
||||
//! .fetch_keys::<polkadot::xcm_pallet::storage::VersionNotifiers>(10, None, None)
|
||||
//! .await
|
||||
//! .unwrap();
|
||||
//! // Iterate over each key
|
||||
//! for key in keys.iter() {
|
||||
//! println!("Key: 0x{}", hex::encode(&key));
|
||||
//! }
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Iterate over Storage
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # use subxt::{ClientBuilder, DefaultConfig, PolkadotExtrinsicParams};
|
||||
//! # use subxt::storage::StorageClient;
|
||||
//!
|
||||
//! #[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata.scale")]
|
||||
//! pub mod polkadot {}
|
||||
//!
|
||||
//! # #[tokio::main]
|
||||
//! # async fn main() {
|
||||
//! # let api = ClientBuilder::new()
|
||||
//! # .build()
|
||||
//! # .await
|
||||
//! # .unwrap()
|
||||
//! # .to_runtime_api::<polkadot::RuntimeApi<DefaultConfig, PolkadotExtrinsicParams<DefaultConfig>>>();
|
||||
//! # // Obtain the storage client wrapper from the API.
|
||||
//! # let storage: StorageClient<_> = api.client.storage();
|
||||
//! // Iterate over keys and values.
|
||||
//! let mut iter = storage
|
||||
//! .iter::<polkadot::xcm_pallet::storage::VersionNotifiers>(None)
|
||||
//! .await
|
||||
//! .unwrap();
|
||||
//! while let Some((key, value)) = iter.next().await.unwrap() {
|
||||
//! println!("Key: 0x{}", hex::encode(&key));
|
||||
//! println!("Value: {}", value);
|
||||
//! }
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
use codec::{
|
||||
Decode,
|
||||
Encode,
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
use sp_core::storage::{
|
||||
StorageChangeSet,
|
||||
StorageData,
|
||||
StorageKey,
|
||||
};
|
||||
pub use sp_runtime::traits::SignedExtension;
|
||||
use std::{
|
||||
marker::PhantomData,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::BasicError,
|
||||
metadata::{
|
||||
Metadata,
|
||||
MetadataError,
|
||||
},
|
||||
rpc::Rpc,
|
||||
Config,
|
||||
StorageHasher,
|
||||
};
|
||||
|
||||
/// Storage entry trait.
|
||||
pub trait StorageEntry {
|
||||
/// Pallet name.
|
||||
const PALLET: &'static str;
|
||||
/// Storage name.
|
||||
const STORAGE: &'static str;
|
||||
/// Type of the storage entry value.
|
||||
type Value: Decode;
|
||||
/// Get the key data for the storage.
|
||||
fn key(&self) -> StorageEntryKey;
|
||||
}
|
||||
|
||||
/// The prefix of the key to a [`StorageEntry`]
|
||||
pub struct StorageKeyPrefix(Vec<u8>);
|
||||
|
||||
impl StorageKeyPrefix {
|
||||
/// Create the storage key prefix for a [`StorageEntry`]
|
||||
pub fn new<T: StorageEntry>() -> Self {
|
||||
let mut bytes = sp_core::twox_128(T::PALLET.as_bytes()).to_vec();
|
||||
bytes.extend(&sp_core::twox_128(T::STORAGE.as_bytes())[..]);
|
||||
Self(bytes)
|
||||
}
|
||||
|
||||
/// Convert the prefix into a [`StorageKey`]
|
||||
pub fn to_storage_key(self) -> StorageKey {
|
||||
StorageKey(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Storage key.
|
||||
pub enum StorageEntryKey {
|
||||
/// Plain key.
|
||||
Plain,
|
||||
/// Map key(s).
|
||||
Map(Vec<StorageMapKey>),
|
||||
}
|
||||
|
||||
impl StorageEntryKey {
|
||||
/// Construct the final [`sp_core::storage::StorageKey`] for the storage entry.
|
||||
pub fn final_key(&self, prefix: StorageKeyPrefix) -> sp_core::storage::StorageKey {
|
||||
let mut bytes = prefix.0;
|
||||
if let Self::Map(map_keys) = self {
|
||||
for map_key in map_keys {
|
||||
bytes.extend(Self::hash(&map_key.hasher, &map_key.value))
|
||||
}
|
||||
}
|
||||
sp_core::storage::StorageKey(bytes)
|
||||
}
|
||||
|
||||
fn hash(hasher: &StorageHasher, bytes: &[u8]) -> Vec<u8> {
|
||||
match hasher {
|
||||
StorageHasher::Identity => bytes.to_vec(),
|
||||
StorageHasher::Blake2_128 => sp_core::blake2_128(bytes).to_vec(),
|
||||
StorageHasher::Blake2_128Concat => {
|
||||
// copied from substrate Blake2_128Concat::hash since StorageHasher is not public
|
||||
sp_core::blake2_128(bytes)
|
||||
.iter()
|
||||
.chain(bytes)
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
StorageHasher::Blake2_256 => sp_core::blake2_256(bytes).to_vec(),
|
||||
StorageHasher::Twox128 => sp_core::twox_128(bytes).to_vec(),
|
||||
StorageHasher::Twox256 => sp_core::twox_256(bytes).to_vec(),
|
||||
StorageHasher::Twox64Concat => {
|
||||
sp_core::twox_64(bytes)
|
||||
.iter()
|
||||
.chain(bytes)
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Storage key for a Map.
|
||||
pub struct StorageMapKey {
|
||||
value: Vec<u8>,
|
||||
hasher: StorageHasher,
|
||||
}
|
||||
|
||||
impl StorageMapKey {
|
||||
/// Create a new [`StorageMapKey`] with the encoded data and the hasher.
|
||||
pub fn new<T: Encode>(value: &T, hasher: StorageHasher) -> Self {
|
||||
Self {
|
||||
value: value.encode(),
|
||||
hasher,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Client for querying runtime storage.
|
||||
pub struct StorageClient<'a, T: Config> {
|
||||
rpc: &'a Rpc<T>,
|
||||
metadata: Arc<RwLock<Metadata>>,
|
||||
iter_page_size: u32,
|
||||
}
|
||||
|
||||
impl<'a, T: Config> Clone for StorageClient<'a, T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
rpc: self.rpc,
|
||||
metadata: Arc::clone(&self.metadata),
|
||||
iter_page_size: self.iter_page_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Config> StorageClient<'a, T> {
|
||||
/// Create a new [`StorageClient`]
|
||||
pub fn new(
|
||||
rpc: &'a Rpc<T>,
|
||||
metadata: Arc<RwLock<Metadata>>,
|
||||
iter_page_size: u32,
|
||||
) -> Self {
|
||||
Self {
|
||||
rpc,
|
||||
metadata,
|
||||
iter_page_size,
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch the value under an unhashed storage key
|
||||
pub async fn fetch_unhashed<V: Decode>(
|
||||
&self,
|
||||
key: StorageKey,
|
||||
hash: Option<T::Hash>,
|
||||
) -> Result<Option<V>, BasicError> {
|
||||
if let Some(data) = self.rpc.storage(&key, hash).await? {
|
||||
Ok(Some(Decode::decode(&mut &data.0[..])?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch the raw encoded value under the raw storage key.
|
||||
pub async fn fetch_raw(
|
||||
&self,
|
||||
key: StorageKey,
|
||||
hash: Option<T::Hash>,
|
||||
) -> Result<Option<StorageData>, BasicError> {
|
||||
self.rpc.storage(&key, hash).await
|
||||
}
|
||||
|
||||
/// Fetch a StorageKey with an optional block hash.
|
||||
pub async fn fetch<F: StorageEntry>(
|
||||
&self,
|
||||
store: &F,
|
||||
hash: Option<T::Hash>,
|
||||
) -> Result<Option<F::Value>, BasicError> {
|
||||
let prefix = StorageKeyPrefix::new::<F>();
|
||||
let key = store.key().final_key(prefix);
|
||||
self.fetch_unhashed::<F::Value>(key, hash).await
|
||||
}
|
||||
|
||||
/// Fetch a StorageKey that has a default value with an optional block hash.
|
||||
pub async fn fetch_or_default<F: StorageEntry>(
|
||||
&self,
|
||||
store: &F,
|
||||
hash: Option<T::Hash>,
|
||||
) -> Result<F::Value, BasicError> {
|
||||
if let Some(data) = self.fetch(store, hash).await? {
|
||||
Ok(data)
|
||||
} else {
|
||||
let metadata = self.metadata.read();
|
||||
let pallet_metadata = metadata.pallet(F::PALLET)?;
|
||||
let storage_metadata = pallet_metadata.storage(F::STORAGE)?;
|
||||
let default = Decode::decode(&mut &storage_metadata.default[..])
|
||||
.map_err(MetadataError::DefaultError)?;
|
||||
Ok(default)
|
||||
}
|
||||
}
|
||||
|
||||
/// Query historical storage entries
|
||||
pub async fn query_storage(
|
||||
&self,
|
||||
keys: Vec<StorageKey>,
|
||||
from: T::Hash,
|
||||
to: Option<T::Hash>,
|
||||
) -> Result<Vec<StorageChangeSet<T::Hash>>, BasicError> {
|
||||
self.rpc.query_storage(keys, from, to).await
|
||||
}
|
||||
|
||||
/// Fetch up to `count` keys for a storage map in lexicographic order.
|
||||
///
|
||||
/// Supports pagination by passing a value to `start_key`.
|
||||
pub async fn fetch_keys<F: StorageEntry>(
|
||||
&self,
|
||||
count: u32,
|
||||
start_key: Option<StorageKey>,
|
||||
hash: Option<T::Hash>,
|
||||
) -> Result<Vec<StorageKey>, BasicError> {
|
||||
let key = StorageKeyPrefix::new::<F>().to_storage_key();
|
||||
let keys = self
|
||||
.rpc
|
||||
.storage_keys_paged(Some(key), count, start_key, hash)
|
||||
.await?;
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
/// Returns an iterator of key value pairs.
|
||||
pub async fn iter<F: StorageEntry>(
|
||||
&self,
|
||||
hash: Option<T::Hash>,
|
||||
) -> Result<KeyIter<'a, T, F>, BasicError> {
|
||||
let hash = if let Some(hash) = hash {
|
||||
hash
|
||||
} else {
|
||||
self.rpc
|
||||
.block_hash(None)
|
||||
.await?
|
||||
.expect("didn't pass a block number; qed")
|
||||
};
|
||||
Ok(KeyIter {
|
||||
client: self.clone(),
|
||||
hash,
|
||||
count: self.iter_page_size,
|
||||
start_key: None,
|
||||
buffer: Default::default(),
|
||||
_marker: PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterates over key value pairs in a map.
|
||||
pub struct KeyIter<'a, T: Config, F: StorageEntry> {
|
||||
client: StorageClient<'a, T>,
|
||||
_marker: PhantomData<F>,
|
||||
count: u32,
|
||||
hash: T::Hash,
|
||||
start_key: Option<StorageKey>,
|
||||
buffer: Vec<(StorageKey, StorageData)>,
|
||||
}
|
||||
|
||||
impl<'a, T: Config, F: StorageEntry> KeyIter<'a, T, F> {
|
||||
/// Returns the next key value pair from a map.
|
||||
pub async fn next(&mut self) -> Result<Option<(StorageKey, F::Value)>, BasicError> {
|
||||
loop {
|
||||
if let Some((k, v)) = self.buffer.pop() {
|
||||
return Ok(Some((k, Decode::decode(&mut &v.0[..])?)))
|
||||
} else {
|
||||
let keys = self
|
||||
.client
|
||||
.fetch_keys::<F>(self.count, self.start_key.take(), Some(self.hash))
|
||||
.await?;
|
||||
|
||||
if keys.is_empty() {
|
||||
return Ok(None)
|
||||
}
|
||||
|
||||
self.start_key = keys.last().cloned();
|
||||
|
||||
let change_sets = self
|
||||
.client
|
||||
.rpc
|
||||
.query_storage_at(&keys, Some(self.hash))
|
||||
.await?;
|
||||
for change_set in change_sets {
|
||||
for (k, v) in change_set.changes {
|
||||
if let Some(v) = v {
|
||||
self.buffer.push((k, v));
|
||||
}
|
||||
}
|
||||
}
|
||||
debug_assert_eq!(self.buffer.len(), keys.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// 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.
|
||||
|
||||
//! Types associated with accessing and working with storage items.
|
||||
|
||||
mod storage_address;
|
||||
mod storage_client;
|
||||
mod storage_map_key;
|
||||
|
||||
pub mod utils;
|
||||
|
||||
pub use storage_client::{
|
||||
KeyIter,
|
||||
StorageClient,
|
||||
};
|
||||
|
||||
// Re-export as this is used in the public API:
|
||||
pub use sp_core::storage::StorageKey;
|
||||
|
||||
/// Types representing an address which describes where a storage
|
||||
/// entry lives and how to properly decode it.
|
||||
pub mod address {
|
||||
pub use super::{
|
||||
storage_address::{
|
||||
dynamic,
|
||||
dynamic_root,
|
||||
DynamicStorageAddress,
|
||||
StaticStorageAddress,
|
||||
StorageAddress,
|
||||
Yes,
|
||||
},
|
||||
storage_map_key::{
|
||||
StorageHasher,
|
||||
StorageMapKey,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// For consistency with other modules, also expose
|
||||
// the basic address stuff at the root of the module.
|
||||
pub use storage_address::{
|
||||
dynamic,
|
||||
dynamic_root,
|
||||
DynamicStorageAddress,
|
||||
StaticStorageAddress,
|
||||
StorageAddress,
|
||||
};
|
||||
@@ -0,0 +1,286 @@
|
||||
// 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 super::storage_map_key::StorageMapKey;
|
||||
use crate::{
|
||||
dynamic::{
|
||||
DecodedValue,
|
||||
Value,
|
||||
},
|
||||
error::{
|
||||
Error,
|
||||
StorageAddressError,
|
||||
},
|
||||
metadata::{
|
||||
DecodeWithMetadata,
|
||||
EncodeWithMetadata,
|
||||
Metadata,
|
||||
},
|
||||
};
|
||||
use frame_metadata::StorageEntryType;
|
||||
use scale_info::TypeDef;
|
||||
use std::borrow::Cow;
|
||||
|
||||
// We use this type a bunch, so export it from here.
|
||||
pub use frame_metadata::StorageHasher;
|
||||
|
||||
/// This represents a storage address. Anything implementing this trait
|
||||
/// can be used to fetch and iterate over storage entries.
|
||||
pub trait StorageAddress {
|
||||
/// The target type of the value that lives at this address.
|
||||
type Target: DecodeWithMetadata;
|
||||
/// Can an entry be fetched from this address?
|
||||
/// Set this type to [`Yes`] to enable the corresponding calls to be made.
|
||||
type IsFetchable;
|
||||
/// Can a default entry be obtained from this address?
|
||||
/// Set this type to [`Yes`] to enable the corresponding calls to be made.
|
||||
type IsDefaultable;
|
||||
/// Can this address be iterated over?
|
||||
/// Set this type to [`Yes`] to enable the corresponding calls to be made.
|
||||
type IsIterable;
|
||||
|
||||
/// The name of the pallet that the entry lives under.
|
||||
fn pallet_name(&self) -> &str;
|
||||
|
||||
/// The name of the entry in a given pallet that the item is at.
|
||||
fn entry_name(&self) -> &str;
|
||||
|
||||
/// Output the non-prefix bytes; that is, any additional bytes that need
|
||||
/// to be appended to the key to dig into maps.
|
||||
fn append_entry_bytes(
|
||||
&self,
|
||||
metadata: &Metadata,
|
||||
bytes: &mut Vec<u8>,
|
||||
) -> Result<(), Error>;
|
||||
|
||||
/// An optional hash which, if present, will be checked against
|
||||
/// the node metadata to confirm that the return type matches what
|
||||
/// we are expecting.
|
||||
fn validation_hash(&self) -> Option<[u8; 32]> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to signal whether a [`StorageAddress`] can be iterated,
|
||||
/// fetched and returned with a default value in the type system.
|
||||
pub struct Yes;
|
||||
|
||||
/// This represents a statically generated storage lookup address.
|
||||
pub struct StaticStorageAddress<ReturnTy, Fetchable, Defaultable, Iterable> {
|
||||
pallet_name: &'static str,
|
||||
entry_name: &'static str,
|
||||
// How to access the specific value at that storage address.
|
||||
storage_entry_keys: Vec<StorageMapKey>,
|
||||
// Hash provided from static code for validation.
|
||||
validation_hash: Option<[u8; 32]>,
|
||||
_marker: std::marker::PhantomData<(ReturnTy, Fetchable, Defaultable, Iterable)>,
|
||||
}
|
||||
|
||||
impl<ReturnTy, Fetchable, Defaultable, Iterable>
|
||||
StaticStorageAddress<ReturnTy, Fetchable, Defaultable, Iterable>
|
||||
where
|
||||
ReturnTy: DecodeWithMetadata,
|
||||
{
|
||||
/// Create a new [`StaticStorageAddress`] that will be validated
|
||||
/// against node metadata using the hash given.
|
||||
pub fn new(
|
||||
pallet_name: &'static str,
|
||||
entry_name: &'static str,
|
||||
storage_entry_keys: Vec<StorageMapKey>,
|
||||
hash: [u8; 32],
|
||||
) -> Self {
|
||||
Self {
|
||||
pallet_name,
|
||||
entry_name,
|
||||
storage_entry_keys,
|
||||
validation_hash: Some(hash),
|
||||
_marker: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Do not validate this storage entry prior to accessing it.
|
||||
pub fn unvalidated(self) -> Self {
|
||||
Self {
|
||||
validation_hash: None,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Return bytes representing this storage entry.
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut bytes = Vec::new();
|
||||
super::utils::write_storage_address_root_bytes(self, &mut bytes);
|
||||
for entry in &self.storage_entry_keys {
|
||||
entry.to_bytes(&mut bytes);
|
||||
}
|
||||
bytes
|
||||
}
|
||||
|
||||
/// Return bytes representing the root of this storage entry (ie a hash of
|
||||
/// the pallet and entry name).
|
||||
pub fn to_root_bytes(&self) -> Vec<u8> {
|
||||
super::utils::storage_address_root_bytes(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<ReturnTy, Fetchable, Defaultable, Iterable> StorageAddress
|
||||
for StaticStorageAddress<ReturnTy, Fetchable, Defaultable, Iterable>
|
||||
where
|
||||
ReturnTy: DecodeWithMetadata,
|
||||
{
|
||||
type Target = ReturnTy;
|
||||
type IsDefaultable = Defaultable;
|
||||
type IsIterable = Iterable;
|
||||
type IsFetchable = Fetchable;
|
||||
|
||||
fn pallet_name(&self) -> &str {
|
||||
self.pallet_name
|
||||
}
|
||||
|
||||
fn entry_name(&self) -> &str {
|
||||
self.entry_name
|
||||
}
|
||||
|
||||
fn append_entry_bytes(
|
||||
&self,
|
||||
_metadata: &Metadata,
|
||||
bytes: &mut Vec<u8>,
|
||||
) -> Result<(), Error> {
|
||||
for entry in &self.storage_entry_keys {
|
||||
entry.to_bytes(bytes);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validation_hash(&self) -> Option<[u8; 32]> {
|
||||
self.validation_hash
|
||||
}
|
||||
}
|
||||
|
||||
/// This represents a dynamically generated storage address.
|
||||
pub struct DynamicStorageAddress<'a, Encodable> {
|
||||
pallet_name: Cow<'a, str>,
|
||||
entry_name: Cow<'a, str>,
|
||||
storage_entry_keys: Vec<Encodable>,
|
||||
}
|
||||
|
||||
/// Construct a new dynamic storage lookup to the root of some entry.
|
||||
pub fn dynamic_root<'a>(
|
||||
pallet_name: impl Into<Cow<'a, str>>,
|
||||
entry_name: impl Into<Cow<'a, str>>,
|
||||
) -> DynamicStorageAddress<'a, Value> {
|
||||
DynamicStorageAddress {
|
||||
pallet_name: pallet_name.into(),
|
||||
entry_name: entry_name.into(),
|
||||
storage_entry_keys: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a new dynamic storage lookup.
|
||||
pub fn dynamic<'a, Encodable: EncodeWithMetadata>(
|
||||
pallet_name: impl Into<Cow<'a, str>>,
|
||||
entry_name: impl Into<Cow<'a, str>>,
|
||||
storage_entry_keys: Vec<Encodable>,
|
||||
) -> DynamicStorageAddress<'a, Encodable> {
|
||||
DynamicStorageAddress {
|
||||
pallet_name: pallet_name.into(),
|
||||
entry_name: entry_name.into(),
|
||||
storage_entry_keys,
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Encodable> StorageAddress for DynamicStorageAddress<'a, Encodable>
|
||||
where
|
||||
Encodable: EncodeWithMetadata,
|
||||
{
|
||||
type Target = DecodedValue;
|
||||
|
||||
// For dynamic types, we have no static guarantees about any of
|
||||
// this stuff, so we just allow it and let it fail at runtime:
|
||||
type IsFetchable = Yes;
|
||||
type IsDefaultable = Yes;
|
||||
type IsIterable = Yes;
|
||||
|
||||
fn pallet_name(&self) -> &str {
|
||||
&self.pallet_name
|
||||
}
|
||||
|
||||
fn entry_name(&self) -> &str {
|
||||
&self.entry_name
|
||||
}
|
||||
|
||||
fn append_entry_bytes(
|
||||
&self,
|
||||
metadata: &Metadata,
|
||||
bytes: &mut Vec<u8>,
|
||||
) -> Result<(), Error> {
|
||||
let pallet = metadata.pallet(&self.pallet_name)?;
|
||||
let storage = pallet.storage(&self.entry_name)?;
|
||||
|
||||
match &storage.ty {
|
||||
StorageEntryType::Plain(_) => {
|
||||
if !self.storage_entry_keys.is_empty() {
|
||||
Err(StorageAddressError::WrongNumberOfKeys {
|
||||
expected: 0,
|
||||
actual: self.storage_entry_keys.len(),
|
||||
}
|
||||
.into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
StorageEntryType::Map { hashers, key, .. } => {
|
||||
let ty = metadata
|
||||
.resolve_type(key.id())
|
||||
.ok_or_else(|| StorageAddressError::TypeNotFound(key.id()))?;
|
||||
|
||||
// If the key is a tuple, we encode each value to the corresponding tuple type.
|
||||
// If the key is not a tuple, encode a single value to the key type.
|
||||
let type_ids = match ty.type_def() {
|
||||
TypeDef::Tuple(tuple) => {
|
||||
tuple.fields().iter().map(|f| f.id()).collect()
|
||||
}
|
||||
_other => {
|
||||
vec![key.id()]
|
||||
}
|
||||
};
|
||||
|
||||
if type_ids.len() != self.storage_entry_keys.len() {
|
||||
return Err(StorageAddressError::WrongNumberOfKeys {
|
||||
expected: type_ids.len(),
|
||||
actual: self.storage_entry_keys.len(),
|
||||
}
|
||||
.into())
|
||||
}
|
||||
|
||||
if hashers.len() == 1 {
|
||||
// One hasher; hash a tuple of all SCALE encoded bytes with the one hash function.
|
||||
let mut input = Vec::new();
|
||||
for (key, type_id) in self.storage_entry_keys.iter().zip(type_ids) {
|
||||
key.encode_with_metadata(type_id, metadata, &mut input)?;
|
||||
}
|
||||
super::storage_map_key::hash_bytes(&input, &hashers[0], bytes);
|
||||
Ok(())
|
||||
} else if hashers.len() == type_ids.len() {
|
||||
// A hasher per field; encode and hash each field independently.
|
||||
for ((key, type_id), hasher) in
|
||||
self.storage_entry_keys.iter().zip(type_ids).zip(hashers)
|
||||
{
|
||||
let mut input = Vec::new();
|
||||
key.encode_with_metadata(type_id, metadata, &mut input)?;
|
||||
super::storage_map_key::hash_bytes(&input, hasher, bytes);
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
// Mismatch; wrong number of hashers/fields.
|
||||
Err(StorageAddressError::WrongNumberOfHashers {
|
||||
hashers: hashers.len(),
|
||||
fields: type_ids.len(),
|
||||
}
|
||||
.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
// 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 super::storage_address::{
|
||||
StorageAddress,
|
||||
Yes,
|
||||
};
|
||||
use crate::{
|
||||
client::{
|
||||
OfflineClientT,
|
||||
OnlineClientT,
|
||||
},
|
||||
error::Error,
|
||||
metadata::{
|
||||
DecodeWithMetadata,
|
||||
Metadata,
|
||||
},
|
||||
Config,
|
||||
};
|
||||
use derivative::Derivative;
|
||||
use frame_metadata::StorageEntryType;
|
||||
use scale_info::form::PortableForm;
|
||||
use sp_core::storage::{
|
||||
StorageData,
|
||||
StorageKey,
|
||||
};
|
||||
use std::{
|
||||
future::Future,
|
||||
marker::PhantomData,
|
||||
};
|
||||
|
||||
/// Query the runtime storage.
|
||||
#[derive(Derivative)]
|
||||
#[derivative(Clone(bound = "Client: Clone"))]
|
||||
pub struct StorageClient<T, Client> {
|
||||
client: Client,
|
||||
_marker: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T, Client> StorageClient<T, Client> {
|
||||
/// Create a new [`StorageClient`]
|
||||
pub fn new(client: Client) -> Self {
|
||||
Self {
|
||||
client,
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Client> StorageClient<T, Client>
|
||||
where
|
||||
T: Config,
|
||||
Client: OfflineClientT<T>,
|
||||
{
|
||||
/// Run the validation logic against some storage address you'd like to access. Returns `Ok(())`
|
||||
/// if the address is valid (or if it's not possible to check since the address has no validation hash).
|
||||
/// Return an error if the address was not valid or something went wrong trying to validate it (ie
|
||||
/// the pallet or storage entry in question do not exist at all).
|
||||
pub fn validate<Address: StorageAddress>(
|
||||
&self,
|
||||
address: &Address,
|
||||
) -> Result<(), Error> {
|
||||
if let Some(hash) = address.validation_hash() {
|
||||
validate_storage(
|
||||
address.pallet_name(),
|
||||
address.entry_name(),
|
||||
hash,
|
||||
&self.client.metadata(),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Client> StorageClient<T, Client>
|
||||
where
|
||||
T: Config,
|
||||
Client: OnlineClientT<T>,
|
||||
{
|
||||
/// Fetch the raw encoded value at the address/key given.
|
||||
pub fn fetch_raw<'a>(
|
||||
&self,
|
||||
key: &'a [u8],
|
||||
hash: Option<T::Hash>,
|
||||
) -> impl Future<Output = Result<Option<Vec<u8>>, Error>> + 'a {
|
||||
let client = self.client.clone();
|
||||
// Ensure that the returned future doesn't have a lifetime tied to api.storage(),
|
||||
// which is a temporary thing we'll be throwing away quickly:
|
||||
async move {
|
||||
let data = client.rpc().storage(key, hash).await?;
|
||||
Ok(data.map(|d| d.0))
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch a decoded value from storage at a given address and optional block hash.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use subxt::{ PolkadotConfig, OnlineClient };
|
||||
///
|
||||
/// #[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata.scale")]
|
||||
/// pub mod polkadot {}
|
||||
///
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() {
|
||||
/// let api = OnlineClient::<PolkadotConfig>::new().await.unwrap();
|
||||
///
|
||||
/// // Address to a storage entry we'd like to access.
|
||||
/// let address = polkadot::storage().xcm_pallet().queries(&12345);
|
||||
///
|
||||
/// // Fetch just the keys, returning up to 10 keys.
|
||||
/// let value = api
|
||||
/// .storage()
|
||||
/// .fetch(&address, None)
|
||||
/// .await
|
||||
/// .unwrap();
|
||||
///
|
||||
/// println!("Value: {:?}", value);
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn fetch<'a, Address>(
|
||||
&self,
|
||||
address: &'a Address,
|
||||
hash: Option<T::Hash>,
|
||||
) -> impl Future<
|
||||
Output = Result<Option<<Address::Target as DecodeWithMetadata>::Target>, Error>,
|
||||
> + 'a
|
||||
where
|
||||
Address: StorageAddress<IsFetchable = Yes> + 'a,
|
||||
{
|
||||
let client = self.clone();
|
||||
async move {
|
||||
// Metadata validation checks whether the static address given
|
||||
// is likely to actually correspond to a real storage entry or not.
|
||||
// if not, it means static codegen doesn't line up with runtime
|
||||
// metadata.
|
||||
client.validate(address)?;
|
||||
|
||||
// Look up the return type ID to enable DecodeWithMetadata:
|
||||
let metadata = client.client.metadata();
|
||||
let lookup_bytes = super::utils::storage_address_bytes(address, &metadata)?;
|
||||
if let Some(data) = client
|
||||
.client
|
||||
.storage()
|
||||
.fetch_raw(&lookup_bytes, hash)
|
||||
.await?
|
||||
{
|
||||
let val = <Address::Target as DecodeWithMetadata>::decode_storage_with_metadata(
|
||||
&mut &*data,
|
||||
address.pallet_name(),
|
||||
address.entry_name(),
|
||||
&metadata,
|
||||
)?;
|
||||
Ok(Some(val))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch a StorageKey that has a default value with an optional block hash.
|
||||
pub fn fetch_or_default<'a, Address>(
|
||||
&self,
|
||||
address: &'a Address,
|
||||
hash: Option<T::Hash>,
|
||||
) -> impl Future<Output = Result<<Address::Target as DecodeWithMetadata>::Target, Error>>
|
||||
+ 'a
|
||||
where
|
||||
Address: StorageAddress<IsFetchable = Yes, IsDefaultable = Yes> + 'a,
|
||||
{
|
||||
let client = self.client.clone();
|
||||
async move {
|
||||
let pallet_name = address.pallet_name();
|
||||
let storage_name = address.entry_name();
|
||||
// Metadata validation happens via .fetch():
|
||||
if let Some(data) = client.storage().fetch(address, hash).await? {
|
||||
Ok(data)
|
||||
} else {
|
||||
let metadata = client.metadata();
|
||||
|
||||
// We have to dig into metadata already, so no point using the optimised `decode_storage_with_metadata` call.
|
||||
let pallet_metadata = metadata.pallet(pallet_name)?;
|
||||
let storage_metadata = pallet_metadata.storage(storage_name)?;
|
||||
let return_ty_id =
|
||||
return_type_from_storage_entry_type(&storage_metadata.ty);
|
||||
let bytes = &mut &storage_metadata.default[..];
|
||||
|
||||
let val = <Address::Target as DecodeWithMetadata>::decode_with_metadata(
|
||||
bytes,
|
||||
return_ty_id,
|
||||
&metadata,
|
||||
)?;
|
||||
Ok(val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch up to `count` keys for a storage map in lexicographic order.
|
||||
///
|
||||
/// Supports pagination by passing a value to `start_key`.
|
||||
pub fn fetch_keys<'a>(
|
||||
&self,
|
||||
key: &'a [u8],
|
||||
count: u32,
|
||||
start_key: Option<&'a [u8]>,
|
||||
hash: Option<T::Hash>,
|
||||
) -> impl Future<Output = Result<Vec<StorageKey>, Error>> + 'a {
|
||||
let client = self.client.clone();
|
||||
async move {
|
||||
let keys = client
|
||||
.rpc()
|
||||
.storage_keys_paged(key, count, start_key, hash)
|
||||
.await?;
|
||||
Ok(keys)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an iterator of key value pairs.
|
||||
///
|
||||
/// ```no_run
|
||||
/// use subxt::{ PolkadotConfig, OnlineClient };
|
||||
///
|
||||
/// #[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata.scale")]
|
||||
/// pub mod polkadot {}
|
||||
///
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() {
|
||||
/// let api = OnlineClient::<PolkadotConfig>::new().await.unwrap();
|
||||
///
|
||||
/// // Address to the root of a storage entry that we'd like to iterate over.
|
||||
/// let address = polkadot::storage().xcm_pallet().version_notifiers_root();
|
||||
///
|
||||
/// // Iterate over keys and values at that address.
|
||||
/// let mut iter = api
|
||||
/// .storage()
|
||||
/// .iter(address, 10, None)
|
||||
/// .await
|
||||
/// .unwrap();
|
||||
///
|
||||
/// while let Some((key, value)) = iter.next().await.unwrap() {
|
||||
/// println!("Key: 0x{}", hex::encode(&key));
|
||||
/// println!("Value: {}", value);
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn iter<Address>(
|
||||
&self,
|
||||
address: Address,
|
||||
page_size: u32,
|
||||
hash: Option<T::Hash>,
|
||||
) -> impl Future<Output = Result<KeyIter<T, Client, Address::Target>, Error>> + 'static
|
||||
where
|
||||
Address: StorageAddress<IsIterable = Yes> + 'static,
|
||||
{
|
||||
let client = self.clone();
|
||||
async move {
|
||||
// Metadata validation checks whether the static address given
|
||||
// is likely to actually correspond to a real storage entry or not.
|
||||
// if not, it means static codegen doesn't line up with runtime
|
||||
// metadata.
|
||||
client.validate(&address)?;
|
||||
|
||||
// Fetch a concrete block hash to iterate over. We do this so that if new blocks
|
||||
// are produced midway through iteration, we continue to iterate at the block
|
||||
// we started with and not the new block.
|
||||
let hash = if let Some(hash) = hash {
|
||||
hash
|
||||
} else {
|
||||
client
|
||||
.client
|
||||
.rpc()
|
||||
.block_hash(None)
|
||||
.await?
|
||||
.expect("didn't pass a block number; qed")
|
||||
};
|
||||
|
||||
let metadata = client.client.metadata();
|
||||
|
||||
// Look up the return type for flexible decoding. Do this once here to avoid
|
||||
// potentially doing it every iteration if we used `decode_storage_with_metadata`
|
||||
// in the iterator.
|
||||
let return_type_id = lookup_storage_return_type(
|
||||
&metadata,
|
||||
address.pallet_name(),
|
||||
address.entry_name(),
|
||||
)?;
|
||||
|
||||
// The root pallet/entry bytes for this storage entry:
|
||||
let address_root_bytes = super::utils::storage_address_root_bytes(&address);
|
||||
|
||||
Ok(KeyIter {
|
||||
client,
|
||||
address_root_bytes,
|
||||
metadata,
|
||||
return_type_id,
|
||||
block_hash: hash,
|
||||
count: page_size,
|
||||
start_key: None,
|
||||
buffer: Default::default(),
|
||||
_marker: std::marker::PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterates over key value pairs in a map.
|
||||
pub struct KeyIter<T: Config, Client, ReturnTy> {
|
||||
client: StorageClient<T, Client>,
|
||||
address_root_bytes: Vec<u8>,
|
||||
return_type_id: u32,
|
||||
metadata: Metadata,
|
||||
count: u32,
|
||||
block_hash: T::Hash,
|
||||
start_key: Option<StorageKey>,
|
||||
buffer: Vec<(StorageKey, StorageData)>,
|
||||
_marker: std::marker::PhantomData<ReturnTy>,
|
||||
}
|
||||
|
||||
impl<'a, T: Config, Client: OnlineClientT<T>, ReturnTy> KeyIter<T, Client, ReturnTy>
|
||||
where
|
||||
T: Config,
|
||||
Client: OnlineClientT<T>,
|
||||
ReturnTy: DecodeWithMetadata,
|
||||
{
|
||||
/// Returns the next key value pair from a map.
|
||||
pub async fn next(
|
||||
&mut self,
|
||||
) -> Result<Option<(StorageKey, ReturnTy::Target)>, Error> {
|
||||
loop {
|
||||
if let Some((k, v)) = self.buffer.pop() {
|
||||
let val = ReturnTy::decode_with_metadata(
|
||||
&mut &v.0[..],
|
||||
self.return_type_id,
|
||||
&self.metadata,
|
||||
)?;
|
||||
return Ok(Some((k, val)))
|
||||
} else {
|
||||
let start_key = self.start_key.take();
|
||||
let keys = self
|
||||
.client
|
||||
.fetch_keys(
|
||||
&self.address_root_bytes,
|
||||
self.count,
|
||||
start_key.as_ref().map(|k| &*k.0),
|
||||
Some(self.block_hash),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if keys.is_empty() {
|
||||
return Ok(None)
|
||||
}
|
||||
|
||||
self.start_key = keys.last().cloned();
|
||||
|
||||
let change_sets = self
|
||||
.client
|
||||
.client
|
||||
.rpc()
|
||||
.query_storage_at(keys.iter().map(|k| &*k.0), Some(self.block_hash))
|
||||
.await?;
|
||||
for change_set in change_sets {
|
||||
for (k, v) in change_set.changes {
|
||||
if let Some(v) = v {
|
||||
self.buffer.push((k, v));
|
||||
}
|
||||
}
|
||||
}
|
||||
debug_assert_eq!(self.buffer.len(), keys.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate a storage entry against the metadata.
|
||||
fn validate_storage(
|
||||
pallet_name: &str,
|
||||
storage_name: &str,
|
||||
hash: [u8; 32],
|
||||
metadata: &Metadata,
|
||||
) -> Result<(), Error> {
|
||||
let expected_hash = match metadata.storage_hash(pallet_name, storage_name) {
|
||||
Ok(hash) => hash,
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
match expected_hash == hash {
|
||||
true => Ok(()),
|
||||
false => Err(crate::error::MetadataError::IncompatibleMetadata.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// look up a return type ID for some storage entry.
|
||||
fn lookup_storage_return_type(
|
||||
metadata: &Metadata,
|
||||
pallet: &str,
|
||||
entry: &str,
|
||||
) -> Result<u32, Error> {
|
||||
let storage_entry_type = &metadata.pallet(pallet)?.storage(entry)?.ty;
|
||||
|
||||
Ok(return_type_from_storage_entry_type(storage_entry_type))
|
||||
}
|
||||
|
||||
/// Fetch the return type out of a [`StorageEntryType`].
|
||||
fn return_type_from_storage_entry_type(entry: &StorageEntryType<PortableForm>) -> u32 {
|
||||
match entry {
|
||||
StorageEntryType::Plain(ty) => ty.id(),
|
||||
StorageEntryType::Map { value, .. } => value.id(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// 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 codec::Encode;
|
||||
pub use sp_runtime::traits::SignedExtension;
|
||||
|
||||
// We use this type a bunch, so export it from here.
|
||||
pub use frame_metadata::StorageHasher;
|
||||
|
||||
/// Storage key for a Map.
|
||||
#[derive(Clone)]
|
||||
pub struct StorageMapKey {
|
||||
value: Vec<u8>,
|
||||
hasher: StorageHasher,
|
||||
}
|
||||
|
||||
impl StorageMapKey {
|
||||
/// Create a new [`StorageMapKey`] by pre-encoding static data and pairing it with a hasher.
|
||||
pub fn new<Encodable: Encode>(
|
||||
value: Encodable,
|
||||
hasher: StorageHasher,
|
||||
) -> StorageMapKey {
|
||||
Self {
|
||||
value: value.encode(),
|
||||
hasher,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert this [`StorageMapKey`] into bytes and append them to some existing bytes.
|
||||
pub fn to_bytes(&self, bytes: &mut Vec<u8>) {
|
||||
hash_bytes(&self.value, &self.hasher, bytes)
|
||||
}
|
||||
}
|
||||
|
||||
/// Take some SCALE encoded bytes and a [`StorageHasher`] and hash the bytes accordingly.
|
||||
pub(super) fn hash_bytes(input: &[u8], hasher: &StorageHasher, bytes: &mut Vec<u8>) {
|
||||
match hasher {
|
||||
StorageHasher::Identity => bytes.extend(input),
|
||||
StorageHasher::Blake2_128 => bytes.extend(sp_core::blake2_128(input)),
|
||||
StorageHasher::Blake2_128Concat => {
|
||||
bytes.extend(sp_core::blake2_128(input));
|
||||
bytes.extend(input);
|
||||
}
|
||||
StorageHasher::Blake2_256 => bytes.extend(sp_core::blake2_256(input)),
|
||||
StorageHasher::Twox128 => bytes.extend(sp_core::twox_128(input)),
|
||||
StorageHasher::Twox256 => bytes.extend(sp_core::twox_256(input)),
|
||||
StorageHasher::Twox64Concat => {
|
||||
bytes.extend(sp_core::twox_64(input));
|
||||
bytes.extend(input);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// 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.
|
||||
|
||||
//! these utility methods complement the [`StorageAddress`] trait, but
|
||||
//! aren't things that should ever be overridden, and so don't exist on
|
||||
//! the trait itself.
|
||||
|
||||
use super::StorageAddress;
|
||||
use crate::{
|
||||
error::Error,
|
||||
metadata::Metadata,
|
||||
};
|
||||
|
||||
/// Return the root of a given [`StorageAddress`]: hash the pallet name and entry name
|
||||
/// and append those bytes to the output.
|
||||
pub fn write_storage_address_root_bytes<Address: StorageAddress>(
|
||||
addr: &Address,
|
||||
out: &mut Vec<u8>,
|
||||
) {
|
||||
out.extend(&sp_core::twox_128(addr.pallet_name().as_bytes()));
|
||||
out.extend(&sp_core::twox_128(addr.entry_name().as_bytes()));
|
||||
}
|
||||
|
||||
/// Outputs the [`storage_address_root_bytes`] as well as any additional bytes that represent
|
||||
/// a lookup in a storage map at that location.
|
||||
pub fn storage_address_bytes<Address: StorageAddress>(
|
||||
addr: &Address,
|
||||
metadata: &Metadata,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
let mut bytes = Vec::new();
|
||||
write_storage_address_root_bytes(addr, &mut bytes);
|
||||
addr.append_entry_bytes(metadata, &mut bytes)?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
/// Outputs a vector containing the bytes written by [`write_storage_address_root_bytes`].
|
||||
pub fn storage_address_root_bytes<Address: StorageAddress>(addr: &Address) -> Vec<u8> {
|
||||
let mut bytes = Vec::new();
|
||||
write_storage_address_root_bytes(addr, &mut bytes);
|
||||
bytes
|
||||
}
|
||||
@@ -20,6 +20,9 @@
|
||||
|
||||
mod params;
|
||||
mod signer;
|
||||
mod tx_client;
|
||||
mod tx_payload;
|
||||
mod tx_progress;
|
||||
|
||||
pub use self::{
|
||||
params::{
|
||||
@@ -38,4 +41,20 @@ pub use self::{
|
||||
PairSigner,
|
||||
Signer,
|
||||
},
|
||||
tx_client::{
|
||||
SignedSubmittableExtrinsic,
|
||||
TxClient,
|
||||
},
|
||||
tx_payload::{
|
||||
dynamic,
|
||||
DynamicTxPayload,
|
||||
StaticTxPayload,
|
||||
TxPayload,
|
||||
},
|
||||
tx_progress::{
|
||||
TxEvents,
|
||||
TxInBlock,
|
||||
TxProgress,
|
||||
TxStatus,
|
||||
},
|
||||
};
|
||||
@@ -2,16 +2,16 @@
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use crate::{
|
||||
utils::Encoded,
|
||||
Config,
|
||||
};
|
||||
use codec::{
|
||||
Compact,
|
||||
Encode,
|
||||
};
|
||||
use core::fmt::Debug;
|
||||
|
||||
use crate::{
|
||||
Config,
|
||||
Encoded,
|
||||
};
|
||||
use derivative::Derivative;
|
||||
|
||||
// We require Era as a param below, so make it available from here.
|
||||
pub use sp_runtime::generic::Era;
|
||||
@@ -20,7 +20,7 @@ pub use sp_runtime::generic::Era;
|
||||
/// "additional" parameters that are signed and used in transactions.
|
||||
/// see [`BaseExtrinsicParams`] for an implementation that is compatible with
|
||||
/// a Polkadot node.
|
||||
pub trait ExtrinsicParams<T: Config>: Debug {
|
||||
pub trait ExtrinsicParams<Index, Hash>: Debug + 'static {
|
||||
/// These parameters can be provided to the constructor along with
|
||||
/// some default parameters that `subxt` understands, in order to
|
||||
/// help construct your [`ExtrinsicParams`] object.
|
||||
@@ -30,8 +30,8 @@ pub trait ExtrinsicParams<T: Config>: Debug {
|
||||
fn new(
|
||||
spec_version: u32,
|
||||
tx_version: u32,
|
||||
nonce: T::Index,
|
||||
genesis_hash: T::Hash,
|
||||
nonce: Index,
|
||||
genesis_hash: Hash,
|
||||
other_params: Self::OtherParams,
|
||||
) -> Self;
|
||||
|
||||
@@ -73,7 +73,8 @@ pub type PolkadotExtrinsicParamsBuilder<T> = BaseExtrinsicParamsBuilder<T, Plain
|
||||
/// If your node differs in the "signed extra" and "additional" parameters expected
|
||||
/// to be sent/signed with a transaction, then you can define your own type which
|
||||
/// implements the [`ExtrinsicParams`] trait.
|
||||
#[derive(Debug)]
|
||||
#[derive(Derivative)]
|
||||
#[derivative(Debug(bound = "Tip: Debug"))]
|
||||
pub struct BaseExtrinsicParams<T: Config, Tip: Debug> {
|
||||
era: Era,
|
||||
nonce: T::Index,
|
||||
@@ -91,6 +92,13 @@ pub struct BaseExtrinsicParams<T: Config, Tip: Debug> {
|
||||
///
|
||||
/// Prefer to use [`SubstrateExtrinsicParamsBuilder`] for a version of this tailored towards
|
||||
/// Substrate, or [`PolkadotExtrinsicParamsBuilder`] for a version tailored to Polkadot.
|
||||
#[derive(Derivative)]
|
||||
#[derivative(
|
||||
Debug(bound = "Tip: Debug"),
|
||||
Clone(bound = "Tip: Clone"),
|
||||
Copy(bound = "Tip: Copy"),
|
||||
PartialEq(bound = "Tip: PartialEq")
|
||||
)]
|
||||
pub struct BaseExtrinsicParamsBuilder<T: Config, Tip> {
|
||||
era: Era,
|
||||
mortality_checkpoint: Option<T::Hash>,
|
||||
@@ -132,7 +140,9 @@ impl<T: Config, Tip: Default> Default for BaseExtrinsicParamsBuilder<T, Tip> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config, Tip: Debug + Encode> ExtrinsicParams<T> for BaseExtrinsicParams<T, Tip> {
|
||||
impl<T: Config, Tip: Debug + Encode + 'static> ExtrinsicParams<T::Index, T::Hash>
|
||||
for BaseExtrinsicParams<T, Tip>
|
||||
{
|
||||
type OtherParams = BaseExtrinsicParamsBuilder<T, Tip>;
|
||||
|
||||
fn new(
|
||||
@@ -0,0 +1,331 @@
|
||||
// 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 super::TxPayload;
|
||||
use crate::{
|
||||
client::{
|
||||
OfflineClientT,
|
||||
OnlineClientT,
|
||||
},
|
||||
error::Error,
|
||||
tx::{
|
||||
ExtrinsicParams,
|
||||
Signer,
|
||||
TxProgress,
|
||||
},
|
||||
utils::{
|
||||
Encoded,
|
||||
PhantomDataSendSync,
|
||||
},
|
||||
Config,
|
||||
};
|
||||
use codec::{
|
||||
Compact,
|
||||
Encode,
|
||||
};
|
||||
use derivative::Derivative;
|
||||
use sp_runtime::{
|
||||
traits::Hash,
|
||||
ApplyExtrinsicResult,
|
||||
};
|
||||
|
||||
/// A client for working with transactions.
|
||||
#[derive(Derivative)]
|
||||
#[derivative(Clone(bound = "Client: Clone"))]
|
||||
pub struct TxClient<T: Config, Client> {
|
||||
client: Client,
|
||||
_marker: PhantomDataSendSync<T>,
|
||||
}
|
||||
|
||||
impl<T: Config, Client> TxClient<T, Client> {
|
||||
/// Create a new [`TxClient`]
|
||||
pub fn new(client: Client) -> Self {
|
||||
Self {
|
||||
client,
|
||||
_marker: PhantomDataSendSync::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config, C: OfflineClientT<T>> TxClient<T, C> {
|
||||
/// Run the validation logic against some extrinsic you'd like to submit. Returns `Ok(())`
|
||||
/// if the call is valid (or if it's not possible to check since the call has no validation hash).
|
||||
/// Return an error if the call was not valid or something went wrong trying to validate it (ie
|
||||
/// the pallet or call in question do not exist at all).
|
||||
pub fn validate<Call>(&self, call: &Call) -> Result<(), Error>
|
||||
where
|
||||
Call: TxPayload,
|
||||
{
|
||||
if let Some(actual_hash) = call.validation_hash() {
|
||||
let metadata = self.client.metadata();
|
||||
let expected_hash =
|
||||
metadata.call_hash(call.pallet_name(), call.call_name())?;
|
||||
if actual_hash != expected_hash {
|
||||
return Err(crate::metadata::MetadataError::IncompatibleMetadata.into())
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return the SCALE encoded bytes representing the call data of the transaction.
|
||||
pub fn call_data<Call>(&self, call: &Call) -> Result<Vec<u8>, Error>
|
||||
where
|
||||
Call: TxPayload,
|
||||
{
|
||||
let metadata = self.client.metadata();
|
||||
let mut bytes = Vec::new();
|
||||
call.encode_call_data(&metadata, &mut bytes)?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
/// Creates a raw signed extrinsic, without submitting it.
|
||||
pub async fn create_signed_with_nonce<Call>(
|
||||
&self,
|
||||
call: &Call,
|
||||
signer: &(dyn Signer<T> + Send + Sync),
|
||||
account_nonce: T::Index,
|
||||
other_params: <T::ExtrinsicParams as ExtrinsicParams<T::Index, T::Hash>>::OtherParams,
|
||||
) -> Result<SignedSubmittableExtrinsic<T, C>, Error>
|
||||
where
|
||||
Call: TxPayload,
|
||||
{
|
||||
// 1. Validate this call against the current node metadata if the call comes
|
||||
// with a hash allowing us to do so.
|
||||
self.validate(call)?;
|
||||
|
||||
// 2. SCALE encode call data to bytes (pallet u8, call u8, call params).
|
||||
let call_data = Encoded(self.call_data(call)?);
|
||||
|
||||
// 3. Construct our custom additional/extra params.
|
||||
let additional_and_extra_params = {
|
||||
// Obtain spec version and transaction version from the runtime version of the client.
|
||||
let runtime = self.client.runtime_version();
|
||||
<T::ExtrinsicParams as ExtrinsicParams<T::Index, T::Hash>>::new(
|
||||
runtime.spec_version,
|
||||
runtime.transaction_version,
|
||||
account_nonce,
|
||||
self.client.genesis_hash(),
|
||||
other_params,
|
||||
)
|
||||
};
|
||||
|
||||
tracing::debug!(
|
||||
"tx additional_and_extra_params: {:?}",
|
||||
additional_and_extra_params
|
||||
);
|
||||
|
||||
// 4. Construct signature. This is compatible with the Encode impl
|
||||
// for SignedPayload (which is this payload of bytes that we'd like)
|
||||
// to sign. See:
|
||||
// https://github.com/paritytech/substrate/blob/9a6d706d8db00abb6ba183839ec98ecd9924b1f8/primitives/runtime/src/generic/unchecked_extrinsic.rs#L215)
|
||||
let signature = {
|
||||
let mut bytes = Vec::new();
|
||||
call_data.encode_to(&mut bytes);
|
||||
additional_and_extra_params.encode_extra_to(&mut bytes);
|
||||
additional_and_extra_params.encode_additional_to(&mut bytes);
|
||||
if bytes.len() > 256 {
|
||||
signer.sign(&sp_core::blake2_256(&bytes))
|
||||
} else {
|
||||
signer.sign(&bytes)
|
||||
}
|
||||
};
|
||||
|
||||
tracing::debug!("tx signature: {}", hex::encode(signature.encode()));
|
||||
|
||||
// 5. Encode extrinsic, now that we have the parts we need. This is compatible
|
||||
// with the Encode impl for UncheckedExtrinsic (protocol version 4).
|
||||
let extrinsic = {
|
||||
let mut encoded_inner = Vec::new();
|
||||
// "is signed" + transaction protocol version (4)
|
||||
(0b10000000 + 4u8).encode_to(&mut encoded_inner);
|
||||
// from address for signature
|
||||
signer.address().encode_to(&mut encoded_inner);
|
||||
// the signature bytes
|
||||
signature.encode_to(&mut encoded_inner);
|
||||
// attach custom extra params
|
||||
additional_and_extra_params.encode_extra_to(&mut encoded_inner);
|
||||
// and now, call data
|
||||
call_data.encode_to(&mut encoded_inner);
|
||||
// now, prefix byte length:
|
||||
let len = Compact(
|
||||
u32::try_from(encoded_inner.len())
|
||||
.expect("extrinsic size expected to be <4GB"),
|
||||
);
|
||||
let mut encoded = Vec::new();
|
||||
len.encode_to(&mut encoded);
|
||||
encoded.extend(encoded_inner);
|
||||
encoded
|
||||
};
|
||||
|
||||
// Wrap in Encoded to ensure that any more "encode" calls leave it in the right state.
|
||||
// maybe we can just return the raw bytes..
|
||||
Ok(SignedSubmittableExtrinsic {
|
||||
client: self.client.clone(),
|
||||
encoded: Encoded(extrinsic),
|
||||
marker: std::marker::PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config, C: OnlineClientT<T>> TxClient<T, C> {
|
||||
/// Creates a raw signed extrinsic, without submitting it.
|
||||
pub async fn create_signed<Call>(
|
||||
&self,
|
||||
call: &Call,
|
||||
signer: &(dyn Signer<T> + Send + Sync),
|
||||
other_params: <T::ExtrinsicParams as ExtrinsicParams<T::Index, T::Hash>>::OtherParams,
|
||||
) -> Result<SignedSubmittableExtrinsic<T, C>, Error>
|
||||
where
|
||||
Call: TxPayload,
|
||||
{
|
||||
// Get nonce from the node.
|
||||
let account_nonce = if let Some(nonce) = signer.nonce() {
|
||||
nonce
|
||||
} else {
|
||||
self.client
|
||||
.rpc()
|
||||
.system_account_next_index(signer.account_id())
|
||||
.await?
|
||||
};
|
||||
|
||||
self.create_signed_with_nonce(call, signer, account_nonce, other_params)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Creates and signs an extrinsic and submits it to the chain. Passes default parameters
|
||||
/// to construct the "signed extra" and "additional" payloads needed by the extrinsic.
|
||||
///
|
||||
/// Returns a [`TxProgress`], which can be used to track the status of the transaction
|
||||
/// and obtain details about it, once it has made it into a block.
|
||||
pub async fn sign_and_submit_then_watch_default<Call>(
|
||||
&self,
|
||||
call: &Call,
|
||||
signer: &(dyn Signer<T> + Send + Sync),
|
||||
) -> Result<TxProgress<T, C>, Error>
|
||||
where
|
||||
Call: TxPayload,
|
||||
<T::ExtrinsicParams as ExtrinsicParams<T::Index, T::Hash>>::OtherParams: Default,
|
||||
{
|
||||
self.sign_and_submit_then_watch(call, signer, Default::default())
|
||||
.await
|
||||
}
|
||||
|
||||
/// Creates and signs an extrinsic and submits it to the chain.
|
||||
///
|
||||
/// Returns a [`TxProgress`], which can be used to track the status of the transaction
|
||||
/// and obtain details about it, once it has made it into a block.
|
||||
pub async fn sign_and_submit_then_watch<Call>(
|
||||
&self,
|
||||
call: &Call,
|
||||
signer: &(dyn Signer<T> + Send + Sync),
|
||||
other_params: <T::ExtrinsicParams as ExtrinsicParams<T::Index, T::Hash>>::OtherParams,
|
||||
) -> Result<TxProgress<T, C>, Error>
|
||||
where
|
||||
Call: TxPayload,
|
||||
{
|
||||
self.create_signed(call, signer, other_params)
|
||||
.await?
|
||||
.submit_and_watch()
|
||||
.await
|
||||
}
|
||||
|
||||
/// Creates and signs an extrinsic and submits to the chain for block inclusion. Passes
|
||||
/// default parameters to construct the "signed extra" and "additional" payloads needed
|
||||
/// by the extrinsic.
|
||||
///
|
||||
/// Returns `Ok` with the extrinsic hash if it is valid extrinsic.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// Success does not mean the extrinsic has been included in the block, just that it is valid
|
||||
/// and has been included in the transaction pool.
|
||||
pub async fn sign_and_submit_default<Call>(
|
||||
&self,
|
||||
call: &Call,
|
||||
signer: &(dyn Signer<T> + Send + Sync),
|
||||
) -> Result<T::Hash, Error>
|
||||
where
|
||||
Call: TxPayload,
|
||||
<T::ExtrinsicParams as ExtrinsicParams<T::Index, T::Hash>>::OtherParams: Default,
|
||||
{
|
||||
self.sign_and_submit(call, signer, Default::default()).await
|
||||
}
|
||||
|
||||
/// Creates and signs an extrinsic and submits to the chain for block inclusion.
|
||||
///
|
||||
/// Returns `Ok` with the extrinsic hash if it is valid extrinsic.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// Success does not mean the extrinsic has been included in the block, just that it is valid
|
||||
/// and has been included in the transaction pool.
|
||||
pub async fn sign_and_submit<Call>(
|
||||
&self,
|
||||
call: &Call,
|
||||
signer: &(dyn Signer<T> + Send + Sync),
|
||||
other_params: <T::ExtrinsicParams as ExtrinsicParams<T::Index, T::Hash>>::OtherParams,
|
||||
) -> Result<T::Hash, Error>
|
||||
where
|
||||
Call: TxPayload,
|
||||
{
|
||||
self.create_signed(call, signer, other_params)
|
||||
.await?
|
||||
.submit()
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// This represents an extrinsic that has been signed and is ready to submit.
|
||||
pub struct SignedSubmittableExtrinsic<T, C> {
|
||||
client: C,
|
||||
encoded: Encoded,
|
||||
marker: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T, C> SignedSubmittableExtrinsic<T, C>
|
||||
where
|
||||
T: Config,
|
||||
C: OnlineClientT<T>,
|
||||
{
|
||||
/// Submits the extrinsic to the chain.
|
||||
///
|
||||
/// Returns a [`TxProgress`], which can be used to track the status of the transaction
|
||||
/// and obtain details about it, once it has made it into a block.
|
||||
pub async fn submit_and_watch(&self) -> Result<TxProgress<T, C>, Error> {
|
||||
// Get a hash of the extrinsic (we'll need this later).
|
||||
let ext_hash = T::Hashing::hash_of(&self.encoded);
|
||||
|
||||
// Submit and watch for transaction progress.
|
||||
let sub = self.client.rpc().watch_extrinsic(&self.encoded).await?;
|
||||
|
||||
Ok(TxProgress::new(sub, self.client.clone(), ext_hash))
|
||||
}
|
||||
|
||||
/// Submits the extrinsic to the chain for block inclusion.
|
||||
///
|
||||
/// Returns `Ok` with the extrinsic hash if it is valid extrinsic.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// Success does not mean the extrinsic has been included in the block, just that it is valid
|
||||
/// and has been included in the transaction pool.
|
||||
pub async fn submit(&self) -> Result<T::Hash, Error> {
|
||||
self.client.rpc().submit_extrinsic(&self.encoded).await
|
||||
}
|
||||
|
||||
/// Submits the extrinsic to the dry_run RPC, to test if it would succeed.
|
||||
///
|
||||
/// Returns `Ok` with an [`ApplyExtrinsicResult`], which is the result of applying of an extrinsic.
|
||||
pub async fn dry_run(
|
||||
&self,
|
||||
at: Option<T::Hash>,
|
||||
) -> Result<ApplyExtrinsicResult, Error> {
|
||||
self.client.rpc().dry_run(self.encoded(), at).await
|
||||
}
|
||||
|
||||
/// Returns the SCALE encoded extrinsic bytes.
|
||||
pub fn encoded(&self) -> &[u8] {
|
||||
&self.encoded.0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
// 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.
|
||||
|
||||
//! This module contains the trait and types used to represent
|
||||
//! transactions that can be submitted.
|
||||
|
||||
use crate::{
|
||||
dynamic::Value,
|
||||
error::{
|
||||
Error,
|
||||
MetadataError,
|
||||
},
|
||||
metadata::Metadata,
|
||||
};
|
||||
use codec::Encode;
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// This represents a transaction payload that can be submitted
|
||||
/// to a node.
|
||||
pub trait TxPayload {
|
||||
/// The name of the pallet that the call lives under.
|
||||
fn pallet_name(&self) -> &str;
|
||||
|
||||
/// The name of the call.
|
||||
fn call_name(&self) -> &str;
|
||||
|
||||
/// Encode call data to the provided output.
|
||||
fn encode_call_data(
|
||||
&self,
|
||||
metadata: &Metadata,
|
||||
out: &mut Vec<u8>,
|
||||
) -> Result<(), Error>;
|
||||
|
||||
/// An optional validation hash that can be provided
|
||||
/// to verify that the shape of the call on the node
|
||||
/// aligns with our expectations.
|
||||
fn validation_hash(&self) -> Option<[u8; 32]> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// This represents a statically generated transaction payload.
|
||||
pub struct StaticTxPayload<CallData> {
|
||||
pallet_name: &'static str,
|
||||
call_name: &'static str,
|
||||
call_data: CallData,
|
||||
validation_hash: Option<[u8; 32]>,
|
||||
}
|
||||
|
||||
impl<CallData> StaticTxPayload<CallData> {
|
||||
/// Create a new [`StaticTxPayload`] from static data.
|
||||
pub fn new(
|
||||
pallet_name: &'static str,
|
||||
call_name: &'static str,
|
||||
call_data: CallData,
|
||||
validation_hash: [u8; 32],
|
||||
) -> Self {
|
||||
StaticTxPayload {
|
||||
pallet_name,
|
||||
call_name,
|
||||
call_data,
|
||||
validation_hash: Some(validation_hash),
|
||||
}
|
||||
}
|
||||
|
||||
/// Do not validate this call prior to submitting it.
|
||||
pub fn unvalidated(self) -> Self {
|
||||
Self {
|
||||
validation_hash: None,
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<CallData: Encode> TxPayload for StaticTxPayload<CallData> {
|
||||
fn pallet_name(&self) -> &str {
|
||||
self.pallet_name
|
||||
}
|
||||
|
||||
fn call_name(&self) -> &str {
|
||||
self.call_name
|
||||
}
|
||||
|
||||
fn encode_call_data(
|
||||
&self,
|
||||
metadata: &Metadata,
|
||||
out: &mut Vec<u8>,
|
||||
) -> Result<(), Error> {
|
||||
let pallet = metadata.pallet(self.pallet_name)?;
|
||||
let pallet_index = pallet.index();
|
||||
let call_index = pallet.call_index(self.call_name)?;
|
||||
|
||||
pallet_index.encode_to(out);
|
||||
call_index.encode_to(out);
|
||||
self.call_data.encode_to(out);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validation_hash(&self) -> Option<[u8; 32]> {
|
||||
self.validation_hash
|
||||
}
|
||||
}
|
||||
|
||||
/// This represents a dynamically generated transaction payload.
|
||||
pub struct DynamicTxPayload<'a> {
|
||||
pallet_name: Cow<'a, str>,
|
||||
call_name: Cow<'a, str>,
|
||||
fields: Vec<Value<()>>,
|
||||
}
|
||||
|
||||
/// Construct a new dynamic transaction payload to submit to a node.
|
||||
pub fn dynamic<'a>(
|
||||
pallet_name: impl Into<Cow<'a, str>>,
|
||||
call_name: impl Into<Cow<'a, str>>,
|
||||
fields: Vec<Value<()>>,
|
||||
) -> DynamicTxPayload<'a> {
|
||||
DynamicTxPayload {
|
||||
pallet_name: pallet_name.into(),
|
||||
call_name: call_name.into(),
|
||||
fields,
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TxPayload for DynamicTxPayload<'a> {
|
||||
fn pallet_name(&self) -> &str {
|
||||
&self.pallet_name
|
||||
}
|
||||
|
||||
fn call_name(&self) -> &str {
|
||||
&self.call_name
|
||||
}
|
||||
|
||||
fn encode_call_data(
|
||||
&self,
|
||||
metadata: &Metadata,
|
||||
out: &mut Vec<u8>,
|
||||
) -> Result<(), Error> {
|
||||
let pallet = metadata.pallet(&self.pallet_name)?;
|
||||
let call_id = pallet.call_ty_id().ok_or(MetadataError::CallNotFound)?;
|
||||
let call_value =
|
||||
Value::unnamed_variant(self.call_name.to_owned(), self.fields.clone());
|
||||
|
||||
pallet.index().encode_to(out);
|
||||
scale_value::scale::encode_as_type(&call_value, call_id, metadata.types(), out)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -2,32 +2,27 @@
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Types representing extrinsics/transactions that have been submitted to a node.
|
||||
|
||||
use std::task::Poll;
|
||||
|
||||
use crate::PhantomDataSendSync;
|
||||
use codec::Decode;
|
||||
use sp_runtime::traits::Hash;
|
||||
pub use sp_runtime::traits::SignedExtension;
|
||||
|
||||
use crate::{
|
||||
client::Client,
|
||||
client::OnlineClientT,
|
||||
error::{
|
||||
BasicError,
|
||||
DispatchError,
|
||||
Error,
|
||||
HasModuleError,
|
||||
ModuleError,
|
||||
RuntimeError,
|
||||
TransactionError,
|
||||
},
|
||||
events::{
|
||||
self,
|
||||
EventDetails,
|
||||
Events,
|
||||
RawEventDetails,
|
||||
EventsClient,
|
||||
Phase,
|
||||
StaticEvent,
|
||||
},
|
||||
rpc::SubstrateTransactionStatus,
|
||||
rpc::SubstrateTxStatus,
|
||||
Config,
|
||||
Phase,
|
||||
};
|
||||
use derivative::Derivative;
|
||||
use futures::{
|
||||
@@ -38,71 +33,65 @@ use jsonrpsee::core::{
|
||||
client::Subscription as RpcSubscription,
|
||||
Error as RpcError,
|
||||
};
|
||||
use sp_runtime::traits::Hash;
|
||||
|
||||
/// This struct represents a subscription to the progress of some transaction, and is
|
||||
/// returned from [`crate::SubmittableExtrinsic::sign_and_submit_then_watch()`].
|
||||
pub use sp_runtime::traits::SignedExtension;
|
||||
|
||||
/// This struct represents a subscription to the progress of some transaction.
|
||||
#[derive(Derivative)]
|
||||
#[derivative(Debug(bound = ""))]
|
||||
pub struct TransactionProgress<'client, T: Config, E, Evs> {
|
||||
sub: Option<RpcSubscription<SubstrateTransactionStatus<T::Hash, T::Hash>>>,
|
||||
#[derivative(Debug(bound = "C: std::fmt::Debug"))]
|
||||
pub struct TxProgress<T: Config, C> {
|
||||
sub: Option<RpcSubscription<SubstrateTxStatus<T::Hash, T::Hash>>>,
|
||||
ext_hash: T::Hash,
|
||||
client: &'client Client<T>,
|
||||
_error: PhantomDataSendSync<(E, Evs)>,
|
||||
client: C,
|
||||
}
|
||||
|
||||
// The above type is not `Unpin` by default unless the generic param `T` is,
|
||||
// so we manually make it clear that Unpin is actually fine regardless of `T`
|
||||
// (we don't care if this moves around in memory while it's "pinned").
|
||||
impl<'client, T: Config, E, Evs> Unpin for TransactionProgress<'client, T, E, Evs> {}
|
||||
impl<T: Config, C> Unpin for TxProgress<T, C> {}
|
||||
|
||||
impl<'client, T: Config, E: Decode + HasModuleError, Evs: Decode>
|
||||
TransactionProgress<'client, T, E, Evs>
|
||||
{
|
||||
/// Instantiate a new [`TransactionProgress`] from a custom subscription.
|
||||
impl<T: Config, C> TxProgress<T, C> {
|
||||
/// Instantiate a new [`TxProgress`] from a custom subscription.
|
||||
pub fn new(
|
||||
sub: RpcSubscription<SubstrateTransactionStatus<T::Hash, T::Hash>>,
|
||||
client: &'client Client<T>,
|
||||
sub: RpcSubscription<SubstrateTxStatus<T::Hash, T::Hash>>,
|
||||
client: C,
|
||||
ext_hash: T::Hash,
|
||||
) -> Self {
|
||||
Self {
|
||||
sub: Some(sub),
|
||||
client,
|
||||
ext_hash,
|
||||
_error: PhantomDataSendSync::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config, C: OnlineClientT<T>> TxProgress<T, C> {
|
||||
/// Return the next transaction status when it's emitted. This just delegates to the
|
||||
/// [`futures::Stream`] implementation for [`TransactionProgress`], but allows you to
|
||||
/// [`futures::Stream`] implementation for [`TxProgress`], but allows you to
|
||||
/// avoid importing that trait if you don't otherwise need it.
|
||||
pub async fn next_item(
|
||||
&mut self,
|
||||
) -> Option<Result<TransactionStatus<'client, T, E, Evs>, BasicError>> {
|
||||
pub async fn next_item(&mut self) -> Option<Result<TxStatus<T, C>, Error>> {
|
||||
self.next().await
|
||||
}
|
||||
|
||||
/// Wait for the transaction to be in a block (but not necessarily finalized), and return
|
||||
/// an [`TransactionInBlock`] instance when this happens, or an error if there was a problem
|
||||
/// an [`TxInBlock`] instance when this happens, or an error if there was a problem
|
||||
/// waiting for this to happen.
|
||||
///
|
||||
/// **Note:** consumes `self`. If you'd like to perform multiple actions as the state of the
|
||||
/// transaction progresses, use [`TransactionProgress::next_item()`] instead.
|
||||
/// transaction progresses, use [`TxProgress::next_item()`] instead.
|
||||
///
|
||||
/// **Note:** transaction statuses like `Invalid` and `Usurped` are ignored, because while they
|
||||
/// may well indicate with some probability that the transaction will not make it into a block,
|
||||
/// there is no guarantee that this is true. Thus, we prefer to "play it safe" here. Use the lower
|
||||
/// level [`TransactionProgress::next_item()`] API if you'd like to handle these statuses yourself.
|
||||
pub async fn wait_for_in_block(
|
||||
mut self,
|
||||
) -> Result<TransactionInBlock<'client, T, E, Evs>, BasicError> {
|
||||
/// level [`TxProgress::next_item()`] API if you'd like to handle these statuses yourself.
|
||||
pub async fn wait_for_in_block(mut self) -> Result<TxInBlock<T, C>, Error> {
|
||||
while let Some(status) = self.next_item().await {
|
||||
match status? {
|
||||
// Finalized or otherwise in a block! Return.
|
||||
TransactionStatus::InBlock(s) | TransactionStatus::Finalized(s) => {
|
||||
return Ok(s)
|
||||
}
|
||||
TxStatus::InBlock(s) | TxStatus::Finalized(s) => return Ok(s),
|
||||
// Error scenarios; return the error.
|
||||
TransactionStatus::FinalityTimeout(_) => {
|
||||
TxStatus::FinalityTimeout(_) => {
|
||||
return Err(TransactionError::FinalitySubscriptionTimeout.into())
|
||||
}
|
||||
// Ignore anything else and wait for next status event:
|
||||
@@ -112,25 +101,23 @@ impl<'client, T: Config, E: Decode + HasModuleError, Evs: Decode>
|
||||
Err(RpcError::Custom("RPC subscription dropped".into()).into())
|
||||
}
|
||||
|
||||
/// Wait for the transaction to be finalized, and return a [`TransactionInBlock`]
|
||||
/// Wait for the transaction to be finalized, and return a [`TxInBlock`]
|
||||
/// instance when it is, or an error if there was a problem waiting for finalization.
|
||||
///
|
||||
/// **Note:** consumes `self`. If you'd like to perform multiple actions as the state of the
|
||||
/// transaction progresses, use [`TransactionProgress::next_item()`] instead.
|
||||
/// transaction progresses, use [`TxProgress::next_item()`] instead.
|
||||
///
|
||||
/// **Note:** transaction statuses like `Invalid` and `Usurped` are ignored, because while they
|
||||
/// may well indicate with some probability that the transaction will not make it into a block,
|
||||
/// there is no guarantee that this is true. Thus, we prefer to "play it safe" here. Use the lower
|
||||
/// level [`TransactionProgress::next_item()`] API if you'd like to handle these statuses yourself.
|
||||
pub async fn wait_for_finalized(
|
||||
mut self,
|
||||
) -> Result<TransactionInBlock<'client, T, E, Evs>, BasicError> {
|
||||
/// level [`TxProgress::next_item()`] API if you'd like to handle these statuses yourself.
|
||||
pub async fn wait_for_finalized(mut self) -> Result<TxInBlock<T, C>, Error> {
|
||||
while let Some(status) = self.next_item().await {
|
||||
match status? {
|
||||
// Finalized! Return.
|
||||
TransactionStatus::Finalized(s) => return Ok(s),
|
||||
TxStatus::Finalized(s) => return Ok(s),
|
||||
// Error scenarios; return the error.
|
||||
TransactionStatus::FinalityTimeout(_) => {
|
||||
TxStatus::FinalityTimeout(_) => {
|
||||
return Err(TransactionError::FinalitySubscriptionTimeout.into())
|
||||
}
|
||||
// Ignore and wait for next status event:
|
||||
@@ -145,24 +132,20 @@ impl<'client, T: Config, E: Decode + HasModuleError, Evs: Decode>
|
||||
/// as well as a couple of other details (block hash and extrinsic hash).
|
||||
///
|
||||
/// **Note:** consumes self. If you'd like to perform multiple actions as progress is made,
|
||||
/// use [`TransactionProgress::next_item()`] instead.
|
||||
/// use [`TxProgress::next_item()`] instead.
|
||||
///
|
||||
/// **Note:** transaction statuses like `Invalid` and `Usurped` are ignored, because while they
|
||||
/// may well indicate with some probability that the transaction will not make it into a block,
|
||||
/// there is no guarantee that this is true. Thus, we prefer to "play it safe" here. Use the lower
|
||||
/// level [`TransactionProgress::next_item()`] API if you'd like to handle these statuses yourself.
|
||||
pub async fn wait_for_finalized_success(
|
||||
self,
|
||||
) -> Result<TransactionEvents<T, Evs>, Error<E>> {
|
||||
/// level [`TxProgress::next_item()`] API if you'd like to handle these statuses yourself.
|
||||
pub async fn wait_for_finalized_success(self) -> Result<TxEvents<T>, Error> {
|
||||
let evs = self.wait_for_finalized().await?.wait_for_success().await?;
|
||||
Ok(evs)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'client, T: Config, E: Decode + HasModuleError, Evs: Decode> Stream
|
||||
for TransactionProgress<'client, T, E, Evs>
|
||||
{
|
||||
type Item = Result<TransactionStatus<'client, T, E, Evs>, BasicError>;
|
||||
impl<T: Config, C: OnlineClientT<T>> Stream for TxProgress<T, C> {
|
||||
type Item = Result<TxStatus<T, C>, Error>;
|
||||
|
||||
fn poll_next(
|
||||
mut self: std::pin::Pin<&mut Self>,
|
||||
@@ -177,28 +160,22 @@ impl<'client, T: Config, E: Decode + HasModuleError, Evs: Decode> Stream
|
||||
.map_err(|e| e.into())
|
||||
.map_ok(|status| {
|
||||
match status {
|
||||
SubstrateTransactionStatus::Future => TransactionStatus::Future,
|
||||
SubstrateTransactionStatus::Ready => TransactionStatus::Ready,
|
||||
SubstrateTransactionStatus::Broadcast(peers) => {
|
||||
TransactionStatus::Broadcast(peers)
|
||||
}
|
||||
SubstrateTransactionStatus::InBlock(hash) => {
|
||||
TransactionStatus::InBlock(TransactionInBlock::new(
|
||||
SubstrateTxStatus::Future => TxStatus::Future,
|
||||
SubstrateTxStatus::Ready => TxStatus::Ready,
|
||||
SubstrateTxStatus::Broadcast(peers) => TxStatus::Broadcast(peers),
|
||||
SubstrateTxStatus::InBlock(hash) => {
|
||||
TxStatus::InBlock(TxInBlock::new(
|
||||
hash,
|
||||
self.ext_hash,
|
||||
self.client,
|
||||
self.client.clone(),
|
||||
))
|
||||
}
|
||||
SubstrateTransactionStatus::Retracted(hash) => {
|
||||
TransactionStatus::Retracted(hash)
|
||||
}
|
||||
SubstrateTransactionStatus::Usurped(hash) => {
|
||||
TransactionStatus::Usurped(hash)
|
||||
}
|
||||
SubstrateTransactionStatus::Dropped => TransactionStatus::Dropped,
|
||||
SubstrateTransactionStatus::Invalid => TransactionStatus::Invalid,
|
||||
SubstrateTxStatus::Retracted(hash) => TxStatus::Retracted(hash),
|
||||
SubstrateTxStatus::Usurped(hash) => TxStatus::Usurped(hash),
|
||||
SubstrateTxStatus::Dropped => TxStatus::Dropped,
|
||||
SubstrateTxStatus::Invalid => TxStatus::Invalid,
|
||||
// Only the following statuses are actually considered "final" (see the substrate
|
||||
// docs on `TransactionStatus`). Basically, either the transaction makes it into a
|
||||
// docs on `TxStatus`). Basically, either the transaction makes it into a
|
||||
// block, or we eventually give up on waiting for it to make it into a block.
|
||||
// Even `Dropped`/`Invalid`/`Usurped` transactions might make it into a block eventually.
|
||||
//
|
||||
@@ -206,16 +183,16 @@ impl<'client, T: Config, E: Decode + HasModuleError, Evs: Decode> Stream
|
||||
// nonce might still be valid on some fork on another node which ends up being finalized.
|
||||
// Equally, a transaction `Dropped` from one node may still be in the transaction pool,
|
||||
// and make it into a block, on another node. Likewise with `Usurped`.
|
||||
SubstrateTransactionStatus::FinalityTimeout(hash) => {
|
||||
SubstrateTxStatus::FinalityTimeout(hash) => {
|
||||
self.sub = None;
|
||||
TransactionStatus::FinalityTimeout(hash)
|
||||
TxStatus::FinalityTimeout(hash)
|
||||
}
|
||||
SubstrateTransactionStatus::Finalized(hash) => {
|
||||
SubstrateTxStatus::Finalized(hash) => {
|
||||
self.sub = None;
|
||||
TransactionStatus::Finalized(TransactionInBlock::new(
|
||||
TxStatus::Finalized(TxInBlock::new(
|
||||
hash,
|
||||
self.ext_hash,
|
||||
self.client,
|
||||
self.client.clone(),
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -223,12 +200,12 @@ impl<'client, T: Config, E: Decode + HasModuleError, Evs: Decode> Stream
|
||||
}
|
||||
}
|
||||
|
||||
//* Dev note: The below is adapted from the substrate docs on `TransactionStatus`, which this
|
||||
//* enum was adapted from (and which is an exact copy of `SubstrateTransactionStatus` in this crate).
|
||||
//* Dev note: The below is adapted from the substrate docs on `TxStatus`, which this
|
||||
//* enum was adapted from (and which is an exact copy of `SubstrateTxStatus` in this crate).
|
||||
//* Note that the number of finality watchers is, at the time of writing, found in the constant
|
||||
//* `MAX_FINALITY_WATCHERS` in the `sc_transaction_pool` crate.
|
||||
//*
|
||||
/// Possible transaction statuses returned from our [`TransactionProgress::next_item()`] call.
|
||||
/// Possible transaction statuses returned from our [`TxProgress::next_item()`] call.
|
||||
///
|
||||
/// These status events can be grouped based on their kinds as:
|
||||
///
|
||||
@@ -270,8 +247,8 @@ impl<'client, T: Config, E: Decode + HasModuleError, Evs: Decode> Stream
|
||||
/// within 512 blocks. This either indicates that finality is not available for your chain,
|
||||
/// or that finality gadget is lagging behind.
|
||||
#[derive(Derivative)]
|
||||
#[derivative(Debug(bound = ""))]
|
||||
pub enum TransactionStatus<'client, T: Config, E: Decode, Evs: Decode> {
|
||||
#[derivative(Debug(bound = "C: std::fmt::Debug"))]
|
||||
pub enum TxStatus<T: Config, C> {
|
||||
/// The transaction is part of the "future" queue.
|
||||
Future,
|
||||
/// The transaction is part of the "ready" queue.
|
||||
@@ -279,7 +256,7 @@ pub enum TransactionStatus<'client, T: Config, E: Decode, Evs: Decode> {
|
||||
/// The transaction has been broadcast to the given peers.
|
||||
Broadcast(Vec<String>),
|
||||
/// The transaction has been included in a block with given hash.
|
||||
InBlock(TransactionInBlock<'client, T, E, Evs>),
|
||||
InBlock(TxInBlock<T, C>),
|
||||
/// The block this transaction was included in has been retracted,
|
||||
/// probably because it did not make it onto the blocks which were
|
||||
/// finalized.
|
||||
@@ -288,7 +265,7 @@ pub enum TransactionStatus<'client, T: Config, E: Decode, Evs: Decode> {
|
||||
/// blocks, and so the subscription has ended.
|
||||
FinalityTimeout(T::Hash),
|
||||
/// The transaction has been finalized by a finality-gadget, e.g GRANDPA.
|
||||
Finalized(TransactionInBlock<'client, T, E, Evs>),
|
||||
Finalized(TxInBlock<T, C>),
|
||||
/// The transaction has been replaced in the pool by another transaction
|
||||
/// that provides the same tags. (e.g. same (sender, nonce)).
|
||||
Usurped(T::Hash),
|
||||
@@ -298,10 +275,10 @@ pub enum TransactionStatus<'client, T: Config, E: Decode, Evs: Decode> {
|
||||
Invalid,
|
||||
}
|
||||
|
||||
impl<'client, T: Config, E: Decode, Evs: Decode> TransactionStatus<'client, T, E, Evs> {
|
||||
impl<T: Config, C> TxStatus<T, C> {
|
||||
/// A convenience method to return the `Finalized` details. Returns
|
||||
/// [`None`] if the enum variant is not [`TransactionStatus::Finalized`].
|
||||
pub fn as_finalized(&self) -> Option<&TransactionInBlock<'client, T, E, Evs>> {
|
||||
/// [`None`] if the enum variant is not [`TxStatus::Finalized`].
|
||||
pub fn as_finalized(&self) -> Option<&TxInBlock<T, C>> {
|
||||
match self {
|
||||
Self::Finalized(val) => Some(val),
|
||||
_ => None,
|
||||
@@ -309,8 +286,8 @@ impl<'client, T: Config, E: Decode, Evs: Decode> TransactionStatus<'client, T, E
|
||||
}
|
||||
|
||||
/// A convenience method to return the `InBlock` details. Returns
|
||||
/// [`None`] if the enum variant is not [`TransactionStatus::InBlock`].
|
||||
pub fn as_in_block(&self) -> Option<&TransactionInBlock<'client, T, E, Evs>> {
|
||||
/// [`None`] if the enum variant is not [`TxStatus::InBlock`].
|
||||
pub fn as_in_block(&self) -> Option<&TxInBlock<T, C>> {
|
||||
match self {
|
||||
Self::InBlock(val) => Some(val),
|
||||
_ => None,
|
||||
@@ -320,27 +297,19 @@ impl<'client, T: Config, E: Decode, Evs: Decode> TransactionStatus<'client, T, E
|
||||
|
||||
/// This struct represents a transaction that has made it into a block.
|
||||
#[derive(Derivative)]
|
||||
#[derivative(Debug(bound = ""))]
|
||||
pub struct TransactionInBlock<'client, T: Config, E: Decode, Evs: Decode> {
|
||||
#[derivative(Debug(bound = "C: std::fmt::Debug"))]
|
||||
pub struct TxInBlock<T: Config, C> {
|
||||
block_hash: T::Hash,
|
||||
ext_hash: T::Hash,
|
||||
client: &'client Client<T>,
|
||||
_error: PhantomDataSendSync<(E, Evs)>,
|
||||
client: C,
|
||||
}
|
||||
|
||||
impl<'client, T: Config, E: Decode + HasModuleError, Evs: Decode>
|
||||
TransactionInBlock<'client, T, E, Evs>
|
||||
{
|
||||
pub(crate) fn new(
|
||||
block_hash: T::Hash,
|
||||
ext_hash: T::Hash,
|
||||
client: &'client Client<T>,
|
||||
) -> Self {
|
||||
impl<T: Config, C: OnlineClientT<T>> TxInBlock<T, C> {
|
||||
pub(crate) fn new(block_hash: T::Hash, ext_hash: T::Hash, client: C) -> Self {
|
||||
Self {
|
||||
block_hash,
|
||||
ext_hash,
|
||||
client,
|
||||
_error: PhantomDataSendSync::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,34 +331,21 @@ impl<'client, T: Config, E: Decode + HasModuleError, Evs: Decode>
|
||||
/// **Note:** If multiple `ExtrinsicFailed` errors are returned (for instance
|
||||
/// because a pallet chooses to emit one as an event, which is considered
|
||||
/// abnormal behaviour), it is not specified which of the errors is returned here.
|
||||
/// You can use [`TransactionInBlock::fetch_events`] instead if you'd like to
|
||||
/// You can use [`TxInBlock::fetch_events`] instead if you'd like to
|
||||
/// work with multiple "error" events.
|
||||
///
|
||||
/// **Note:** This has to download block details from the node and decode events
|
||||
/// from them.
|
||||
pub async fn wait_for_success(&self) -> Result<TransactionEvents<T, Evs>, Error<E>> {
|
||||
pub async fn wait_for_success(&self) -> Result<TxEvents<T>, Error> {
|
||||
let events = self.fetch_events().await?;
|
||||
|
||||
// Try to find any errors; return the first one we encounter.
|
||||
for ev in events.iter_raw() {
|
||||
for ev in events.iter() {
|
||||
let ev = ev?;
|
||||
if &ev.pallet == "System" && &ev.variant == "ExtrinsicFailed" {
|
||||
let dispatch_error = E::decode(&mut &*ev.bytes)?;
|
||||
if let Some(error_data) = dispatch_error.module_error_data() {
|
||||
// Error index is utilized as the first byte from the error array.
|
||||
let locked_metadata = self.client.metadata();
|
||||
let metadata = locked_metadata.read();
|
||||
let details = metadata
|
||||
.error(error_data.pallet_index, error_data.error_index())?;
|
||||
return Err(Error::Module(ModuleError {
|
||||
pallet: details.pallet().to_string(),
|
||||
error: details.error().to_string(),
|
||||
description: details.description().to_vec(),
|
||||
error_data,
|
||||
}))
|
||||
} else {
|
||||
return Err(Error::Runtime(RuntimeError(dispatch_error)))
|
||||
}
|
||||
if ev.pallet_name() == "System" && ev.variant_name() == "ExtrinsicFailed" {
|
||||
let dispatch_error =
|
||||
DispatchError::decode_from(ev.field_bytes(), &self.client.metadata());
|
||||
return Err(dispatch_error.into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,13 +358,13 @@ impl<'client, T: Config, E: Decode + HasModuleError, Evs: Decode>
|
||||
///
|
||||
/// **Note:** This has to download block details from the node and decode events
|
||||
/// from them.
|
||||
pub async fn fetch_events(&self) -> Result<TransactionEvents<T, Evs>, BasicError> {
|
||||
pub async fn fetch_events(&self) -> Result<TxEvents<T>, Error> {
|
||||
let block = self
|
||||
.client
|
||||
.rpc()
|
||||
.block(Some(self.block_hash))
|
||||
.await?
|
||||
.ok_or(BasicError::Transaction(TransactionError::BlockHashNotFound))?;
|
||||
.ok_or(Error::Transaction(TransactionError::BlockHashNotFound))?;
|
||||
|
||||
let extrinsic_idx = block.block.extrinsics
|
||||
.iter()
|
||||
@@ -418,11 +374,13 @@ impl<'client, T: Config, E: Decode + HasModuleError, Evs: Decode>
|
||||
})
|
||||
// If we successfully obtain the block hash we think contains our
|
||||
// extrinsic, the extrinsic should be in there somewhere..
|
||||
.ok_or(BasicError::Transaction(TransactionError::BlockHashNotFound))?;
|
||||
.ok_or(Error::Transaction(TransactionError::BlockHashNotFound))?;
|
||||
|
||||
let events = events::at::<T, Evs>(self.client, self.block_hash).await?;
|
||||
let events = EventsClient::new(self.client.clone())
|
||||
.at(Some(self.block_hash))
|
||||
.await?;
|
||||
|
||||
Ok(TransactionEvents {
|
||||
Ok(TxEvents {
|
||||
ext_hash: self.ext_hash,
|
||||
ext_idx: extrinsic_idx as u32,
|
||||
events,
|
||||
@@ -434,13 +392,13 @@ impl<'client, T: Config, E: Decode + HasModuleError, Evs: Decode>
|
||||
/// We can iterate over the events, or look for a specific one.
|
||||
#[derive(Derivative)]
|
||||
#[derivative(Debug(bound = ""))]
|
||||
pub struct TransactionEvents<T: Config, Evs: Decode> {
|
||||
pub struct TxEvents<T: Config> {
|
||||
ext_hash: T::Hash,
|
||||
ext_idx: u32,
|
||||
events: Events<T, Evs>,
|
||||
events: Events<T>,
|
||||
}
|
||||
|
||||
impl<T: Config, Evs: Decode> TransactionEvents<T, Evs> {
|
||||
impl<T: Config> TxEvents<T> {
|
||||
/// Return the hash of the block that the transaction has made it into.
|
||||
pub fn block_hash(&self) -> T::Hash {
|
||||
self.events.block_hash()
|
||||
@@ -452,34 +410,18 @@ impl<T: Config, Evs: Decode> TransactionEvents<T, Evs> {
|
||||
}
|
||||
|
||||
/// Return all of the events in the block that the transaction made it into.
|
||||
pub fn all_events_in_block(&self) -> &events::Events<T, Evs> {
|
||||
pub fn all_events_in_block(&self) -> &events::Events<T> {
|
||||
&self.events
|
||||
}
|
||||
|
||||
/// Iterate over the statically decoded events associated with this transaction.
|
||||
///
|
||||
/// This works in the same way that [`events::Events::iter()`] does, with the
|
||||
/// exception that it filters out events not related to the submitted extrinsic.
|
||||
pub fn iter(
|
||||
&self,
|
||||
) -> impl Iterator<Item = Result<EventDetails<Evs>, BasicError>> + '_ {
|
||||
self.events.iter().filter(|ev| {
|
||||
ev.as_ref()
|
||||
.map(|ev| ev.phase == Phase::ApplyExtrinsic(self.ext_idx))
|
||||
.unwrap_or(true) // Keep any errors
|
||||
})
|
||||
}
|
||||
|
||||
/// Iterate over all of the raw events associated with this transaction.
|
||||
///
|
||||
/// This works in the same way that [`events::Events::iter_raw()`] does, with the
|
||||
/// This works in the same way that [`events::Events::iter()`] does, with the
|
||||
/// exception that it filters out events not related to the submitted extrinsic.
|
||||
pub fn iter_raw(
|
||||
&self,
|
||||
) -> impl Iterator<Item = Result<RawEventDetails, BasicError>> + '_ {
|
||||
self.events.iter_raw().filter(|ev| {
|
||||
pub fn iter(&self) -> impl Iterator<Item = Result<EventDetails, Error>> + '_ {
|
||||
self.events.iter().filter(|ev| {
|
||||
ev.as_ref()
|
||||
.map(|ev| ev.phase == Phase::ApplyExtrinsic(self.ext_idx))
|
||||
.map(|ev| ev.phase() == Phase::ApplyExtrinsic(self.ext_idx))
|
||||
.unwrap_or(true) // Keep any errors.
|
||||
})
|
||||
}
|
||||
@@ -488,10 +430,8 @@ impl<T: Config, Evs: Decode> TransactionEvents<T, Evs> {
|
||||
///
|
||||
/// This works in the same way that [`events::Events::find()`] does, with the
|
||||
/// exception that it filters out events not related to the submitted extrinsic.
|
||||
pub fn find<Ev: crate::Event>(
|
||||
&self,
|
||||
) -> impl Iterator<Item = Result<Ev, BasicError>> + '_ {
|
||||
self.iter_raw().filter_map(|ev| {
|
||||
pub fn find<Ev: StaticEvent>(&self) -> impl Iterator<Item = Result<Ev, Error>> + '_ {
|
||||
self.iter().filter_map(|ev| {
|
||||
ev.and_then(|ev| ev.as_event::<Ev>().map_err(Into::into))
|
||||
.transpose()
|
||||
})
|
||||
@@ -502,7 +442,7 @@ impl<T: Config, Evs: Decode> TransactionEvents<T, Evs> {
|
||||
///
|
||||
/// This works in the same way that [`events::Events::find_first()`] does, with the
|
||||
/// exception that it ignores events not related to the submitted extrinsic.
|
||||
pub fn find_first<Ev: crate::Event>(&self) -> Result<Option<Ev>, BasicError> {
|
||||
pub fn find_first<Ev: StaticEvent>(&self) -> Result<Option<Ev>, Error> {
|
||||
self.find::<Ev>().next().transpose()
|
||||
}
|
||||
|
||||
@@ -510,7 +450,7 @@ impl<T: Config, Evs: Decode> TransactionEvents<T, Evs> {
|
||||
///
|
||||
/// This works in the same way that [`events::Events::has()`] does, with the
|
||||
/// exception that it ignores events not related to the submitted extrinsic.
|
||||
pub fn has<Ev: crate::Event>(&self) -> Result<bool, BasicError> {
|
||||
pub fn has<Ev: StaticEvent>(&self) -> Result<bool, Error> {
|
||||
Ok(self.find::<Ev>().next().transpose()?.is_some())
|
||||
}
|
||||
}
|
||||
@@ -1,126 +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.
|
||||
|
||||
//! Perform runtime updates in the background using [UpdateClient].
|
||||
//!
|
||||
//! There are cases when the node would perform a runtime update. As a result, the subxt's metadata
|
||||
//! would be out of sync and the API would not be able to submit valid extrinsics.
|
||||
//! This API keeps the `RuntimeVersion` and `Metadata` of the client synced with the target node.
|
||||
//!
|
||||
//! The runtime update is recommended for long-running clients, or for cases where manually
|
||||
//! restarting subxt would not be feasible. Even with this, extrinsics submitted during a node
|
||||
//! runtime update are at risk or failing, as it will take `subxt` a moment to catch up.
|
||||
//!
|
||||
//! ## Note
|
||||
//!
|
||||
//! Here we use tokio to check for updates in the background, but any runtime can be used.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # use subxt::{ClientBuilder, DefaultConfig};
|
||||
//! #
|
||||
//! # #[tokio::main]
|
||||
//! # async fn main() {
|
||||
//! # let client = ClientBuilder::new()
|
||||
//! # .set_url("wss://rpc.polkadot.io:443")
|
||||
//! # .build::<DefaultConfig>()
|
||||
//! # .await
|
||||
//! # .unwrap();
|
||||
//! #
|
||||
//! let update_client = client.updates();
|
||||
//! // Spawn a new background task to handle runtime updates.
|
||||
//! tokio::spawn(async move {
|
||||
//! let result = update_client.perform_runtime_updates().await;
|
||||
//! println!("Runtime update finished with result={:?}", result);
|
||||
//! });
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
use crate::{
|
||||
rpc::{
|
||||
Rpc,
|
||||
RuntimeVersion,
|
||||
},
|
||||
BasicError,
|
||||
Config,
|
||||
Metadata,
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Client wrapper for performing runtime updates.
|
||||
pub struct UpdateClient<T: Config> {
|
||||
rpc: Rpc<T>,
|
||||
metadata: Arc<RwLock<Metadata>>,
|
||||
runtime_version: Arc<RwLock<RuntimeVersion>>,
|
||||
}
|
||||
|
||||
impl<T: Config> UpdateClient<T> {
|
||||
/// Create a new [`UpdateClient`].
|
||||
pub fn new(
|
||||
rpc: Rpc<T>,
|
||||
metadata: Arc<RwLock<Metadata>>,
|
||||
runtime_version: Arc<RwLock<RuntimeVersion>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
rpc,
|
||||
metadata,
|
||||
runtime_version,
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs runtime updates indefinitely unless encountering an error.
|
||||
///
|
||||
/// *Note:* This should be called from a dedicated background task.
|
||||
pub async fn perform_runtime_updates(&self) -> Result<(), BasicError> {
|
||||
// Obtain an update subscription to further detect changes in the runtime version of the node.
|
||||
let mut update_subscription = self.rpc.subscribe_runtime_version().await?;
|
||||
|
||||
while let Some(update_runtime_version) = update_subscription.next().await {
|
||||
// The Runtime Version obtained via subscription.
|
||||
let update_runtime_version = update_runtime_version?;
|
||||
|
||||
// To ensure there are no races between:
|
||||
// - starting the subxt::Client (fetching runtime version / metadata)
|
||||
// - subscribing to the runtime updates
|
||||
// the node provides its runtime version immediately after subscribing.
|
||||
//
|
||||
// In those cases, set the Runtime Version on the client if and only if
|
||||
// the provided runtime version is different than what the client currently
|
||||
// has stored.
|
||||
{
|
||||
// The Runtime Version of the client, as set during building the client
|
||||
// or during updates.
|
||||
let runtime_version = self.runtime_version.read();
|
||||
if runtime_version.spec_version == update_runtime_version.spec_version {
|
||||
tracing::debug!(
|
||||
"Runtime update not performed for spec_version={}, client has spec_version={}",
|
||||
update_runtime_version.spec_version, runtime_version.spec_version
|
||||
);
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Update the RuntimeVersion first.
|
||||
{
|
||||
let mut runtime_version = self.runtime_version.write();
|
||||
// Update both the `RuntimeVersion` and `Metadata` of the client.
|
||||
tracing::info!(
|
||||
"Performing runtime update from {} to {}",
|
||||
runtime_version.spec_version,
|
||||
update_runtime_version.spec_version,
|
||||
);
|
||||
*runtime_version = update_runtime_version;
|
||||
}
|
||||
|
||||
// Fetch the new metadata of the runtime node.
|
||||
let update_metadata = self.rpc.metadata().await?;
|
||||
tracing::debug!("Performing metadata update");
|
||||
let mut metadata = self.metadata.write();
|
||||
*metadata = update_metadata;
|
||||
tracing::debug!("Runtime update completed");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// 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.
|
||||
|
||||
//! Miscellaneous utility helpers.
|
||||
|
||||
use codec::{
|
||||
Decode,
|
||||
DecodeAll,
|
||||
Encode,
|
||||
};
|
||||
use derivative::Derivative;
|
||||
|
||||
/// Wraps an already encoded byte vector, prevents being encoded as a raw byte vector as part of
|
||||
/// the transaction payload
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Encoded(pub Vec<u8>);
|
||||
|
||||
impl codec::Encode for Encoded {
|
||||
fn encode(&self) -> Vec<u8> {
|
||||
self.0.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper for any type `T` which implement encode/decode in a way compatible with `Vec<u8>`.
|
||||
///
|
||||
/// [`WrapperKeepOpaque`] stores the type only in its opaque format, aka as a `Vec<u8>`. To
|
||||
/// access the real type `T` [`Self::try_decode`] needs to be used.
|
||||
#[derive(Derivative, Encode, Decode)]
|
||||
#[derivative(
|
||||
Debug(bound = ""),
|
||||
Clone(bound = ""),
|
||||
PartialEq(bound = ""),
|
||||
Eq(bound = ""),
|
||||
Default(bound = ""),
|
||||
Hash(bound = "")
|
||||
)]
|
||||
pub struct WrapperKeepOpaque<T> {
|
||||
data: Vec<u8>,
|
||||
_phantom: PhantomDataSendSync<T>,
|
||||
}
|
||||
|
||||
impl<T: Decode> WrapperKeepOpaque<T> {
|
||||
/// Try to decode the wrapped type from the inner `data`.
|
||||
///
|
||||
/// Returns `None` if the decoding failed.
|
||||
pub fn try_decode(&self) -> Option<T> {
|
||||
T::decode_all(&mut &self.data[..]).ok()
|
||||
}
|
||||
|
||||
/// Returns the length of the encoded `T`.
|
||||
pub fn encoded_len(&self) -> usize {
|
||||
self.data.len()
|
||||
}
|
||||
|
||||
/// Returns the encoded data.
|
||||
pub fn encoded(&self) -> &[u8] {
|
||||
&self.data
|
||||
}
|
||||
|
||||
/// Create from the given encoded `data`.
|
||||
pub fn from_encoded(data: Vec<u8>) -> Self {
|
||||
Self {
|
||||
data,
|
||||
_phantom: PhantomDataSendSync::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A version of [`std::marker::PhantomData`] that is also Send and Sync (which is fine
|
||||
/// because regardless of the generic param, it is always possible to Send + Sync this
|
||||
/// 0 size type).
|
||||
#[derive(Derivative, Encode, Decode, scale_info::TypeInfo)]
|
||||
#[derivative(
|
||||
Clone(bound = ""),
|
||||
PartialEq(bound = ""),
|
||||
Debug(bound = ""),
|
||||
Eq(bound = ""),
|
||||
Default(bound = ""),
|
||||
Hash(bound = "")
|
||||
)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
#[doc(hidden)]
|
||||
pub struct PhantomDataSendSync<T>(core::marker::PhantomData<T>);
|
||||
|
||||
impl<T> PhantomDataSendSync<T> {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self(core::marker::PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<T> Send for PhantomDataSendSync<T> {}
|
||||
unsafe impl<T> Sync for PhantomDataSendSync<T> {}
|
||||
|
||||
/// This represents a key-value collection and is SCALE compatible
|
||||
/// with collections like BTreeMap. This has the same type params
|
||||
/// as `BTreeMap` which allows us to easily swap the two during codegen.
|
||||
pub type KeyedVec<K, V> = Vec<(K, V)>;
|
||||
@@ -3,24 +3,29 @@
|
||||
// see LICENSE for license details.
|
||||
|
||||
use crate::{
|
||||
test_node_process,
|
||||
test_node_process_with,
|
||||
utils::node_runtime::system,
|
||||
pair_signer,
|
||||
test_context,
|
||||
test_context_with,
|
||||
utils::{
|
||||
node_runtime,
|
||||
wait_for_blocks,
|
||||
},
|
||||
};
|
||||
|
||||
use sp_core::storage::{
|
||||
well_known_keys,
|
||||
StorageKey,
|
||||
use sp_core::{
|
||||
sr25519::Pair as Sr25519Pair,
|
||||
storage::well_known_keys,
|
||||
Pair,
|
||||
};
|
||||
use sp_keyring::AccountKeyring;
|
||||
use subxt::error::DispatchError;
|
||||
|
||||
#[tokio::test]
|
||||
async fn insert_key() {
|
||||
let test_node_process = test_node_process_with(AccountKeyring::Bob).await;
|
||||
let client = test_node_process.client();
|
||||
let ctx = test_context_with(AccountKeyring::Bob).await;
|
||||
let api = ctx.client();
|
||||
|
||||
let public = AccountKeyring::Alice.public().as_array_ref().to_vec();
|
||||
client
|
||||
.rpc()
|
||||
api.rpc()
|
||||
.insert_key(
|
||||
"aura".to_string(),
|
||||
"//Alice".to_string(),
|
||||
@@ -28,7 +33,7 @@ async fn insert_key() {
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(client
|
||||
assert!(api
|
||||
.rpc()
|
||||
.has_key(public.clone().into(), "aura".to_string())
|
||||
.await
|
||||
@@ -37,29 +42,30 @@ async fn insert_key() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn fetch_block_hash() {
|
||||
let node_process = test_node_process().await;
|
||||
node_process.client().rpc().block_hash(None).await.unwrap();
|
||||
let ctx = test_context().await;
|
||||
ctx.client().rpc().block_hash(None).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fetch_block() {
|
||||
let node_process = test_node_process().await;
|
||||
let client = node_process.client();
|
||||
let block_hash = client.rpc().block_hash(None).await.unwrap();
|
||||
client.rpc().block(block_hash).await.unwrap();
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
let block_hash = api.rpc().block_hash(None).await.unwrap();
|
||||
api.rpc().block(block_hash).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fetch_read_proof() {
|
||||
let node_process = test_node_process().await;
|
||||
let client = node_process.client();
|
||||
let block_hash = client.rpc().block_hash(None).await.unwrap();
|
||||
client
|
||||
.rpc()
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
let block_hash = api.rpc().block_hash(None).await.unwrap();
|
||||
api.rpc()
|
||||
.read_proof(
|
||||
vec![
|
||||
StorageKey(well_known_keys::HEAP_PAGES.to_vec()),
|
||||
StorageKey(well_known_keys::EXTRINSIC_INDEX.to_vec()),
|
||||
well_known_keys::HEAP_PAGES,
|
||||
well_known_keys::EXTRINSIC_INDEX,
|
||||
],
|
||||
block_hash,
|
||||
)
|
||||
@@ -69,27 +75,31 @@ async fn fetch_read_proof() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn chain_subscribe_blocks() {
|
||||
let node_process = test_node_process().await;
|
||||
let client = node_process.client();
|
||||
let mut blocks = client.rpc().subscribe_blocks().await.unwrap();
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
let mut blocks = api.rpc().subscribe_blocks().await.unwrap();
|
||||
blocks.next().await.unwrap().unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn chain_subscribe_finalized_blocks() {
|
||||
let node_process = test_node_process().await;
|
||||
let client = node_process.client();
|
||||
let mut blocks = client.rpc().subscribe_finalized_blocks().await.unwrap();
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
let mut blocks = api.rpc().subscribe_finalized_blocks().await.unwrap();
|
||||
blocks.next().await.unwrap().unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fetch_keys() {
|
||||
let node_process = test_node_process().await;
|
||||
let client = node_process.client();
|
||||
let keys = client
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
let addr = node_runtime::storage().system().account_root();
|
||||
let keys = api
|
||||
.storage()
|
||||
.fetch_keys::<system::storage::Account>(4, None, None)
|
||||
.fetch_keys(&addr.to_root_bytes(), 4, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(keys.len(), 4)
|
||||
@@ -97,13 +107,11 @@ async fn fetch_keys() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_iter() {
|
||||
let node_process = test_node_process().await;
|
||||
let client = node_process.client();
|
||||
let mut iter = client
|
||||
.storage()
|
||||
.iter::<system::storage::Account>(None)
|
||||
.await
|
||||
.unwrap();
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
let addr = node_runtime::storage().system().account_root();
|
||||
let mut iter = api.storage().iter(addr, 10, None).await.unwrap();
|
||||
let mut i = 0;
|
||||
while iter.next().await.unwrap().is_some() {
|
||||
i += 1;
|
||||
@@ -113,9 +121,96 @@ async fn test_iter() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn fetch_system_info() {
|
||||
let node_process = test_node_process().await;
|
||||
let client = node_process.client();
|
||||
assert_eq!(client.rpc().system_chain().await.unwrap(), "Development");
|
||||
assert_eq!(client.rpc().system_name().await.unwrap(), "Substrate Node");
|
||||
assert!(!client.rpc().system_version().await.unwrap().is_empty());
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
assert_eq!(api.rpc().system_chain().await.unwrap(), "Development");
|
||||
assert_eq!(api.rpc().system_name().await.unwrap(), "Substrate Node");
|
||||
assert!(!api.rpc().system_version().await.unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dry_run_passes() {
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
let alice = pair_signer(AccountKeyring::Alice.pair());
|
||||
let bob = pair_signer(AccountKeyring::Bob.pair());
|
||||
|
||||
wait_for_blocks(&api).await;
|
||||
|
||||
let tx = node_runtime::tx()
|
||||
.balances()
|
||||
.transfer(bob.account_id().clone().into(), 10_000);
|
||||
|
||||
let signed_extrinsic = api
|
||||
.tx()
|
||||
.create_signed(&tx, &alice, Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
api.rpc()
|
||||
.dry_run(signed_extrinsic.encoded(), None)
|
||||
.await
|
||||
.expect("dryrunning failed")
|
||||
.expect("expected dryrunning to be successful")
|
||||
.unwrap();
|
||||
|
||||
signed_extrinsic
|
||||
.submit_and_watch()
|
||||
.await
|
||||
.unwrap()
|
||||
.wait_for_finalized_success()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dry_run_fails() {
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
wait_for_blocks(&api).await;
|
||||
|
||||
let alice = pair_signer(AccountKeyring::Alice.pair());
|
||||
let hans = pair_signer(Sr25519Pair::generate().0);
|
||||
|
||||
let tx = node_runtime::tx().balances().transfer(
|
||||
hans.account_id().clone().into(),
|
||||
100_000_000_000_000_000_000_000_000_000_000_000,
|
||||
);
|
||||
|
||||
let signed_extrinsic = api
|
||||
.tx()
|
||||
.create_signed(&tx, &alice, Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let dry_run_res = api
|
||||
.rpc()
|
||||
.dry_run(signed_extrinsic.encoded(), None)
|
||||
.await
|
||||
.expect("dryrunning failed")
|
||||
.expect("expected dryrun transaction to be valid");
|
||||
|
||||
if let Err(sp_runtime::DispatchError::Module(module_error)) = dry_run_res {
|
||||
assert_eq!(module_error.index, 6);
|
||||
assert_eq!(module_error.error, 2);
|
||||
} else {
|
||||
panic!("expected a module error when dryrunning");
|
||||
}
|
||||
|
||||
let res = signed_extrinsic
|
||||
.submit_and_watch()
|
||||
.await
|
||||
.unwrap()
|
||||
.wait_for_finalized_success()
|
||||
.await;
|
||||
|
||||
if let Err(subxt::error::Error::Runtime(DispatchError::Module(err))) = res {
|
||||
assert_eq!(err.pallet, "Balances");
|
||||
assert_eq!(err.error, "InsufficientBalance");
|
||||
} else {
|
||||
panic!("expected a runtime module error");
|
||||
}
|
||||
}
|
||||
|
||||
+18248
-31374
File diff suppressed because it is too large
Load Diff
@@ -4,28 +4,30 @@
|
||||
|
||||
use crate::{
|
||||
node_runtime::{
|
||||
self,
|
||||
balances,
|
||||
system,
|
||||
},
|
||||
pair_signer,
|
||||
test_context,
|
||||
utils::wait_for_blocks,
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use sp_keyring::AccountKeyring;
|
||||
|
||||
// Check that we can subscribe to non-finalized block events.
|
||||
#[tokio::test]
|
||||
async fn non_finalized_block_subscription() -> Result<(), subxt::BasicError> {
|
||||
tracing_subscriber::fmt::try_init().ok();
|
||||
async fn non_finalized_block_subscription() -> Result<(), subxt::Error> {
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
let mut event_sub = ctx.api.events().subscribe().await?;
|
||||
let mut event_sub = api.events().subscribe().await?;
|
||||
|
||||
// Wait for the next set of events, and check that the
|
||||
// associated block hash is not finalized yet.
|
||||
let events = event_sub.next().await.unwrap()?;
|
||||
let event_block_hash = events.block_hash();
|
||||
let current_block_hash = ctx.api.client.rpc().block_hash(None).await?.unwrap();
|
||||
let current_block_hash = api.rpc().block_hash(None).await?.unwrap();
|
||||
|
||||
assert_eq!(event_block_hash, current_block_hash);
|
||||
Ok(())
|
||||
@@ -33,18 +35,18 @@ async fn non_finalized_block_subscription() -> Result<(), subxt::BasicError> {
|
||||
|
||||
// Check that we can subscribe to finalized block events.
|
||||
#[tokio::test]
|
||||
async fn finalized_block_subscription() -> Result<(), subxt::BasicError> {
|
||||
tracing_subscriber::fmt::try_init().ok();
|
||||
async fn finalized_block_subscription() -> Result<(), subxt::Error> {
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
let mut event_sub = ctx.api.events().subscribe_finalized().await?;
|
||||
let mut event_sub = api.events().subscribe_finalized().await?;
|
||||
|
||||
// Wait for the next set of events, and check that the
|
||||
// associated block hash is the one we just finalized.
|
||||
// (this can be a bit slow as we have to wait for finalization)
|
||||
let events = event_sub.next().await.unwrap()?;
|
||||
let event_block_hash = events.block_hash();
|
||||
let finalized_hash = ctx.api.client.rpc().finalized_head().await?;
|
||||
let finalized_hash = api.rpc().finalized_head().await?;
|
||||
|
||||
assert_eq!(event_block_hash, finalized_hash);
|
||||
Ok(())
|
||||
@@ -53,24 +55,25 @@ async fn finalized_block_subscription() -> Result<(), subxt::BasicError> {
|
||||
// Check that our subscription actually keeps producing events for
|
||||
// a few blocks.
|
||||
#[tokio::test]
|
||||
async fn subscription_produces_events_each_block() -> Result<(), subxt::BasicError> {
|
||||
tracing_subscriber::fmt::try_init().ok();
|
||||
async fn subscription_produces_events_each_block() -> Result<(), subxt::Error> {
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
let mut event_sub = ctx.api.events().subscribe().await?;
|
||||
wait_for_blocks(&api).await;
|
||||
|
||||
let mut event_sub = api.events().subscribe().await?;
|
||||
|
||||
for i in 0..3 {
|
||||
let events = event_sub
|
||||
.next()
|
||||
.await
|
||||
.expect("events expected each block")?;
|
||||
|
||||
let success_event = events
|
||||
.find_first::<system::events::ExtrinsicSuccess>()
|
||||
.expect("decode error");
|
||||
// Every now and then I get no bytes back for the first block events;
|
||||
// I assume that this might be the case for the genesis block, so don't
|
||||
// worry if no event found (but we should have no decode errors etc either way).
|
||||
if i > 0 && success_event.is_none() {
|
||||
|
||||
if success_event.is_none() {
|
||||
let n = events.len();
|
||||
panic!("Expected an extrinsic success event on iteration {i} (saw {n} other events)")
|
||||
}
|
||||
@@ -82,13 +85,12 @@ async fn subscription_produces_events_each_block() -> Result<(), subxt::BasicErr
|
||||
// Check that our subscription receives events, and we can filter them based on
|
||||
// it's Stream impl, and ultimately see the event we expect.
|
||||
#[tokio::test]
|
||||
async fn balance_transfer_subscription() -> Result<(), subxt::BasicError> {
|
||||
tracing_subscriber::fmt::try_init().ok();
|
||||
async fn balance_transfer_subscription() -> Result<(), subxt::Error> {
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
// Subscribe to balance transfer events, ignoring all else.
|
||||
let event_sub = ctx
|
||||
.api
|
||||
let event_sub = api
|
||||
.events()
|
||||
.subscribe()
|
||||
.await?
|
||||
@@ -101,11 +103,12 @@ async fn balance_transfer_subscription() -> Result<(), subxt::BasicError> {
|
||||
// Make a transfer:
|
||||
let alice = pair_signer(AccountKeyring::Alice.pair());
|
||||
let bob = AccountKeyring::Bob.to_account_id();
|
||||
ctx.api
|
||||
.tx()
|
||||
let transfer_tx = node_runtime::tx()
|
||||
.balances()
|
||||
.transfer(bob.clone().into(), 10_000)?
|
||||
.sign_and_submit_then_watch_default(&alice)
|
||||
.transfer(bob.clone().into(), 10_000);
|
||||
|
||||
api.tx()
|
||||
.sign_and_submit_then_watch_default(&transfer_tx, &alice)
|
||||
.await?;
|
||||
|
||||
// Wait for the next balance transfer event in our subscription stream
|
||||
@@ -124,21 +127,20 @@ async fn balance_transfer_subscription() -> Result<(), subxt::BasicError> {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn missing_block_headers_will_be_filled_in() -> Result<(), subxt::BasicError> {
|
||||
async fn missing_block_headers_will_be_filled_in() -> Result<(), subxt::Error> {
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
// This function is not publically available to use, but contains
|
||||
// the key logic for filling in missing blocks, so we want to test it.
|
||||
// This is used in `subscribe_finalized` to ensure no block headers are
|
||||
// missed.
|
||||
use subxt::events::subscribe_to_block_headers_filling_in_gaps;
|
||||
|
||||
let ctx = test_context().await;
|
||||
|
||||
// Manually subscribe to the next 6 finalized block headers, but deliberately
|
||||
// filter out some in the middle so we get back b _ _ b _ b. This guarantees
|
||||
// that there will be some gaps, even if there aren't any from the subscription.
|
||||
let some_finalized_blocks = ctx
|
||||
.api
|
||||
.client
|
||||
let some_finalized_blocks = api
|
||||
.rpc()
|
||||
.subscribe_finalized_blocks()
|
||||
.await?
|
||||
@@ -152,7 +154,7 @@ async fn missing_block_headers_will_be_filled_in() -> Result<(), subxt::BasicErr
|
||||
|
||||
// This should spot any gaps in the middle and fill them back in.
|
||||
let all_finalized_blocks = subscribe_to_block_headers_filling_in_gaps(
|
||||
&ctx.api.client,
|
||||
ctx.client(),
|
||||
None,
|
||||
some_finalized_blocks,
|
||||
);
|
||||
@@ -185,7 +187,7 @@ async fn check_events_are_sendable() {
|
||||
tokio::task::spawn(async {
|
||||
let ctx = test_context().await;
|
||||
|
||||
let mut event_sub = ctx.api.events().subscribe().await?;
|
||||
let mut event_sub = ctx.client().events().subscribe().await?;
|
||||
|
||||
while let Some(ev) = event_sub.next().await {
|
||||
// if `event_sub` doesn't implement Send, we can't hold
|
||||
@@ -193,7 +195,7 @@ async fn check_events_are_sendable() {
|
||||
// requires Send. This will lead to a compile error.
|
||||
}
|
||||
|
||||
Ok::<_, subxt::BasicError>(())
|
||||
Ok::<_, subxt::Error>(())
|
||||
});
|
||||
|
||||
// Check that FilterEvents can be used across await points.
|
||||
@@ -201,7 +203,7 @@ async fn check_events_are_sendable() {
|
||||
let ctx = test_context().await;
|
||||
|
||||
let mut event_sub = ctx
|
||||
.api
|
||||
.client()
|
||||
.events()
|
||||
.subscribe()
|
||||
.await?
|
||||
@@ -213,6 +215,6 @@ async fn check_events_are_sendable() {
|
||||
// requires Send; This will lead to a compile error.
|
||||
}
|
||||
|
||||
Ok::<_, subxt::BasicError>(())
|
||||
Ok::<_, subxt::Error>(())
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
|
||||
use crate::{
|
||||
node_runtime::{
|
||||
self,
|
||||
balances,
|
||||
runtime_types,
|
||||
system,
|
||||
DispatchError,
|
||||
},
|
||||
pair_signer,
|
||||
test_context,
|
||||
@@ -22,32 +22,36 @@ use sp_runtime::{
|
||||
AccountId32,
|
||||
MultiAddress,
|
||||
};
|
||||
use subxt::Error;
|
||||
use subxt::error::{
|
||||
DispatchError,
|
||||
Error,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn tx_basic_transfer() -> Result<(), subxt::Error<DispatchError>> {
|
||||
async fn tx_basic_transfer() -> Result<(), subxt::Error> {
|
||||
let alice = pair_signer(AccountKeyring::Alice.pair());
|
||||
let bob = pair_signer(AccountKeyring::Bob.pair());
|
||||
let bob_address = bob.account_id().clone().into();
|
||||
let cxt = test_context().await;
|
||||
let api = &cxt.api;
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
let alice_account_addr = node_runtime::storage().system().account(alice.account_id());
|
||||
let bob_account_addr = node_runtime::storage().system().account(bob.account_id());
|
||||
|
||||
let alice_pre = api
|
||||
.storage()
|
||||
.system()
|
||||
.account(alice.account_id(), None)
|
||||
.fetch_or_default(&alice_account_addr, None)
|
||||
.await?;
|
||||
let bob_pre = api
|
||||
.storage()
|
||||
.system()
|
||||
.account(bob.account_id(), None)
|
||||
.fetch_or_default(&bob_account_addr, None)
|
||||
.await?;
|
||||
|
||||
let tx = node_runtime::tx().balances().transfer(bob_address, 10_000);
|
||||
|
||||
let events = api
|
||||
.tx()
|
||||
.balances()
|
||||
.transfer(bob_address, 10_000)?
|
||||
.sign_and_submit_then_watch_default(&alice)
|
||||
.sign_and_submit_then_watch_default(&tx, &alice)
|
||||
.await?
|
||||
.wait_for_finalized_success()
|
||||
.await?;
|
||||
@@ -69,13 +73,11 @@ async fn tx_basic_transfer() -> Result<(), subxt::Error<DispatchError>> {
|
||||
|
||||
let alice_post = api
|
||||
.storage()
|
||||
.system()
|
||||
.account(alice.account_id(), None)
|
||||
.fetch_or_default(&alice_account_addr, None)
|
||||
.await?;
|
||||
let bob_post = api
|
||||
.storage()
|
||||
.system()
|
||||
.account(bob.account_id(), None)
|
||||
.fetch_or_default(&bob_account_addr, None)
|
||||
.await?;
|
||||
|
||||
assert!(alice_pre.data.free - 10_000 >= alice_post.data.free);
|
||||
@@ -84,26 +86,118 @@ async fn tx_basic_transfer() -> Result<(), subxt::Error<DispatchError>> {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multiple_transfers_work_nonce_incremented(
|
||||
) -> Result<(), subxt::Error<DispatchError>> {
|
||||
async fn tx_dynamic_transfer() -> Result<(), subxt::Error> {
|
||||
use subxt::ext::scale_value::{
|
||||
At,
|
||||
Composite,
|
||||
Value,
|
||||
};
|
||||
|
||||
let alice = pair_signer(AccountKeyring::Alice.pair());
|
||||
let bob = pair_signer(AccountKeyring::Bob.pair());
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
let alice_account_addr = subxt::dynamic::storage(
|
||||
"System",
|
||||
"Account",
|
||||
vec![Value::from_bytes(&alice.account_id())],
|
||||
);
|
||||
let bob_account_addr = subxt::dynamic::storage(
|
||||
"System",
|
||||
"Account",
|
||||
vec![Value::from_bytes(&bob.account_id())],
|
||||
);
|
||||
|
||||
let alice_pre = api
|
||||
.storage()
|
||||
.fetch_or_default(&alice_account_addr, None)
|
||||
.await?;
|
||||
let bob_pre = api
|
||||
.storage()
|
||||
.fetch_or_default(&bob_account_addr, None)
|
||||
.await?;
|
||||
|
||||
let tx = subxt::dynamic::tx(
|
||||
"Balances",
|
||||
"transfer",
|
||||
vec![
|
||||
Value::unnamed_variant("Id", vec![Value::from_bytes(&bob.account_id())]),
|
||||
Value::u128(10_000u128),
|
||||
],
|
||||
);
|
||||
|
||||
let events = api
|
||||
.tx()
|
||||
.sign_and_submit_then_watch_default(&tx, &alice)
|
||||
.await?
|
||||
.wait_for_finalized_success()
|
||||
.await?;
|
||||
|
||||
let event_fields = events
|
||||
.iter()
|
||||
.filter_map(|ev| ev.ok())
|
||||
.find(|ev| ev.pallet_name() == "Balances" && ev.variant_name() == "Transfer")
|
||||
.expect("Failed to find Transfer event")
|
||||
.field_values()
|
||||
.map_context(|_| ());
|
||||
|
||||
let expected_fields = Composite::Named(vec![
|
||||
(
|
||||
"from".into(),
|
||||
Value::unnamed_composite(vec![Value::from_bytes(&alice.account_id())]),
|
||||
),
|
||||
(
|
||||
"to".into(),
|
||||
Value::unnamed_composite(vec![Value::from_bytes(&bob.account_id())]),
|
||||
),
|
||||
("amount".into(), Value::u128(10_000)),
|
||||
]);
|
||||
assert_eq!(event_fields, expected_fields);
|
||||
|
||||
let alice_post = api
|
||||
.storage()
|
||||
.fetch_or_default(&alice_account_addr, None)
|
||||
.await?;
|
||||
let bob_post = api
|
||||
.storage()
|
||||
.fetch_or_default(&bob_account_addr, None)
|
||||
.await?;
|
||||
|
||||
let alice_pre_free = alice_pre.at("data").at("free").unwrap().as_u128().unwrap();
|
||||
let alice_post_free = alice_post.at("data").at("free").unwrap().as_u128().unwrap();
|
||||
|
||||
let bob_pre_free = bob_pre.at("data").at("free").unwrap().as_u128().unwrap();
|
||||
let bob_post_free = bob_post.at("data").at("free").unwrap().as_u128().unwrap();
|
||||
|
||||
assert!(alice_pre_free - 10_000 >= alice_post_free);
|
||||
assert_eq!(bob_pre_free + 10_000, bob_post_free);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multiple_transfers_work_nonce_incremented() -> Result<(), subxt::Error> {
|
||||
let alice = pair_signer(AccountKeyring::Alice.pair());
|
||||
let bob = pair_signer(AccountKeyring::Bob.pair());
|
||||
let bob_address: MultiAddress<AccountId32, u32> = bob.account_id().clone().into();
|
||||
let cxt = test_context().await;
|
||||
let api = &cxt.api;
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
let bob_account_addr = node_runtime::storage().system().account(bob.account_id());
|
||||
|
||||
let bob_pre = api
|
||||
.storage()
|
||||
.system()
|
||||
.account(bob.account_id(), None)
|
||||
.fetch_or_default(&bob_account_addr, None)
|
||||
.await?;
|
||||
|
||||
let tx = node_runtime::tx()
|
||||
.balances()
|
||||
.transfer(bob_address.clone(), 10_000);
|
||||
for _ in 0..3 {
|
||||
api
|
||||
.tx()
|
||||
.balances()
|
||||
.transfer(bob_address.clone(), 10_000)?
|
||||
.sign_and_submit_then_watch_default(&alice)
|
||||
.sign_and_submit_then_watch_default(&tx, &alice)
|
||||
.await?
|
||||
.wait_for_in_block() // Don't need to wait for finalization; this is quicker.
|
||||
.await?
|
||||
@@ -113,8 +207,7 @@ async fn multiple_transfers_work_nonce_incremented(
|
||||
|
||||
let bob_post = api
|
||||
.storage()
|
||||
.system()
|
||||
.account(bob.account_id(), None)
|
||||
.fetch_or_default(&bob_account_addr, None)
|
||||
.await?;
|
||||
|
||||
assert_eq!(bob_pre.data.free + 30_000, bob_post.data.free);
|
||||
@@ -123,45 +216,40 @@ async fn multiple_transfers_work_nonce_incremented(
|
||||
|
||||
#[tokio::test]
|
||||
async fn storage_total_issuance() {
|
||||
let cxt = test_context().await;
|
||||
let total_issuance = cxt
|
||||
.api
|
||||
.storage()
|
||||
.balances()
|
||||
.total_issuance(None)
|
||||
.await
|
||||
.unwrap();
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
let addr = node_runtime::storage().balances().total_issuance();
|
||||
let total_issuance = api.storage().fetch_or_default(&addr, None).await.unwrap();
|
||||
assert_ne!(total_issuance, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn storage_balance_lock() -> Result<(), subxt::Error<DispatchError>> {
|
||||
async fn storage_balance_lock() -> Result<(), subxt::Error> {
|
||||
let bob = pair_signer(AccountKeyring::Bob.pair());
|
||||
let charlie = AccountKeyring::Charlie.to_account_id();
|
||||
let cxt = test_context().await;
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
cxt.api
|
||||
.tx()
|
||||
.staking()
|
||||
.bond(
|
||||
charlie.into(),
|
||||
100_000_000_000_000,
|
||||
runtime_types::pallet_staking::RewardDestination::Stash,
|
||||
)?
|
||||
.sign_and_submit_then_watch_default(&bob)
|
||||
let tx = node_runtime::tx().staking().bond(
|
||||
charlie.into(),
|
||||
100_000_000_000_000,
|
||||
runtime_types::pallet_staking::RewardDestination::Stash,
|
||||
);
|
||||
|
||||
api.tx()
|
||||
.sign_and_submit_then_watch_default(&tx, &bob)
|
||||
.await?
|
||||
.wait_for_finalized_success()
|
||||
.await?
|
||||
.find_first::<system::events::ExtrinsicSuccess>()?
|
||||
.expect("No ExtrinsicSuccess Event found");
|
||||
|
||||
let locked_account = AccountKeyring::Bob.to_account_id();
|
||||
let locks = cxt
|
||||
.api
|
||||
.storage()
|
||||
let locks_addr = node_runtime::storage()
|
||||
.balances()
|
||||
.locks(&locked_account, None)
|
||||
.await?;
|
||||
.locks(&AccountKeyring::Bob.to_account_id());
|
||||
|
||||
let locks = api.storage().fetch_or_default(&locks_addr, None).await?;
|
||||
|
||||
assert_eq!(
|
||||
locks.0,
|
||||
@@ -177,38 +265,37 @@ async fn storage_balance_lock() -> Result<(), subxt::Error<DispatchError>> {
|
||||
|
||||
#[tokio::test]
|
||||
async fn transfer_error() {
|
||||
tracing_subscriber::fmt::try_init().ok();
|
||||
let alice = pair_signer(AccountKeyring::Alice.pair());
|
||||
let alice_addr = alice.account_id().clone().into();
|
||||
let hans = pair_signer(Pair::generate().0);
|
||||
let hans_address = hans.account_id().clone().into();
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
ctx.api
|
||||
.tx()
|
||||
let to_hans_tx = node_runtime::tx()
|
||||
.balances()
|
||||
.transfer(hans_address, 100_000_000_000_000_000)
|
||||
.unwrap()
|
||||
.sign_and_submit_then_watch_default(&alice)
|
||||
.transfer(hans_address, 100_000_000_000_000_000);
|
||||
let to_alice_tx = node_runtime::tx()
|
||||
.balances()
|
||||
.transfer(alice_addr, 100_000_000_000_000_000);
|
||||
|
||||
api.tx()
|
||||
.sign_and_submit_then_watch_default(&to_hans_tx, &alice)
|
||||
.await
|
||||
.unwrap()
|
||||
.wait_for_finalized_success()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let res = ctx
|
||||
.api
|
||||
let res = api
|
||||
.tx()
|
||||
.balances()
|
||||
.transfer(alice_addr, 100_000_000_000_000_000)
|
||||
.unwrap()
|
||||
.sign_and_submit_then_watch_default(&hans)
|
||||
.sign_and_submit_then_watch_default(&to_alice_tx, &hans)
|
||||
.await
|
||||
.unwrap()
|
||||
.wait_for_finalized_success()
|
||||
.await;
|
||||
|
||||
if let Err(Error::Module(err)) = res {
|
||||
if let Err(Error::Runtime(DispatchError::Module(err))) = res {
|
||||
assert_eq!(err.pallet, "Balances");
|
||||
assert_eq!(err.error, "InsufficientBalance");
|
||||
} else {
|
||||
@@ -218,19 +305,17 @@ async fn transfer_error() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn transfer_implicit_subscription() {
|
||||
tracing_subscriber::fmt::try_init().ok();
|
||||
let alice = pair_signer(AccountKeyring::Alice.pair());
|
||||
let bob = AccountKeyring::Bob.to_account_id();
|
||||
let bob_addr = bob.clone().into();
|
||||
let cxt = test_context().await;
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
let event = cxt
|
||||
.api
|
||||
let to_bob_tx = node_runtime::tx().balances().transfer(bob_addr, 10_000);
|
||||
|
||||
let event = api
|
||||
.tx()
|
||||
.balances()
|
||||
.transfer(bob_addr, 10_000)
|
||||
.unwrap()
|
||||
.sign_and_submit_then_watch_default(&alice)
|
||||
.sign_and_submit_then_watch_default(&to_bob_tx, &alice)
|
||||
.await
|
||||
.unwrap()
|
||||
.wait_for_finalized_success()
|
||||
@@ -252,19 +337,19 @@ async fn transfer_implicit_subscription() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn constant_existential_deposit() {
|
||||
let cxt = test_context().await;
|
||||
let locked_metadata = cxt.client().metadata();
|
||||
let metadata = locked_metadata.read();
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
// get and decode constant manually via metadata:
|
||||
let metadata = api.metadata();
|
||||
let balances_metadata = metadata.pallet("Balances").unwrap();
|
||||
let constant_metadata = balances_metadata.constant("ExistentialDeposit").unwrap();
|
||||
let existential_deposit = u128::decode(&mut &constant_metadata.value[..]).unwrap();
|
||||
assert_eq!(existential_deposit, 100_000_000_000_000);
|
||||
assert_eq!(
|
||||
existential_deposit,
|
||||
cxt.api
|
||||
.constants()
|
||||
.balances()
|
||||
.existential_deposit()
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
// constant address for API access:
|
||||
let addr = node_runtime::constants().balances().existential_deposit();
|
||||
|
||||
// Make sure thetwo are identical:
|
||||
assert_eq!(existential_deposit, api.constants().at(&addr).unwrap());
|
||||
}
|
||||
|
||||
@@ -7,57 +7,46 @@ use sp_keyring::AccountKeyring;
|
||||
use crate::{
|
||||
node_runtime::{
|
||||
self,
|
||||
contracts::{
|
||||
calls::TransactionApi,
|
||||
events,
|
||||
storage,
|
||||
},
|
||||
contracts::events,
|
||||
system,
|
||||
DispatchError,
|
||||
},
|
||||
test_context,
|
||||
NodeRuntimeParams,
|
||||
TestContext,
|
||||
};
|
||||
use sp_core::sr25519::Pair;
|
||||
use sp_runtime::MultiAddress;
|
||||
use subxt::{
|
||||
Client,
|
||||
tx::{
|
||||
PairSigner,
|
||||
TxProgress,
|
||||
},
|
||||
Config,
|
||||
DefaultConfig,
|
||||
Error,
|
||||
PairSigner,
|
||||
TransactionProgress,
|
||||
OnlineClient,
|
||||
SubstrateConfig,
|
||||
};
|
||||
|
||||
struct ContractsTestContext {
|
||||
cxt: TestContext,
|
||||
signer: PairSigner<DefaultConfig, Pair>,
|
||||
signer: PairSigner<SubstrateConfig, Pair>,
|
||||
}
|
||||
|
||||
type Hash = <DefaultConfig as Config>::Hash;
|
||||
type AccountId = <DefaultConfig as Config>::AccountId;
|
||||
type Hash = <SubstrateConfig as Config>::Hash;
|
||||
type AccountId = <SubstrateConfig as Config>::AccountId;
|
||||
|
||||
impl ContractsTestContext {
|
||||
async fn init() -> Self {
|
||||
tracing_subscriber::fmt::try_init().ok();
|
||||
let cxt = test_context().await;
|
||||
let signer = PairSigner::new(AccountKeyring::Alice.pair());
|
||||
|
||||
Self { cxt, signer }
|
||||
}
|
||||
|
||||
fn client(&self) -> &Client<DefaultConfig> {
|
||||
fn client(&self) -> OnlineClient<SubstrateConfig> {
|
||||
self.cxt.client()
|
||||
}
|
||||
|
||||
fn contracts_tx(&self) -> TransactionApi<DefaultConfig, NodeRuntimeParams> {
|
||||
self.cxt.api.tx().contracts()
|
||||
}
|
||||
|
||||
async fn instantiate_with_code(
|
||||
&self,
|
||||
) -> Result<(Hash, AccountId), Error<DispatchError>> {
|
||||
async fn instantiate_with_code(&self) -> Result<(Hash, AccountId), Error> {
|
||||
tracing::info!("instantiate_with_code:");
|
||||
const CONTRACT: &str = r#"
|
||||
(module
|
||||
@@ -67,20 +56,19 @@ impl ContractsTestContext {
|
||||
"#;
|
||||
let code = wabt::wat2wasm(CONTRACT).expect("invalid wabt");
|
||||
|
||||
let instantiate_tx = node_runtime::tx().contracts().instantiate_with_code(
|
||||
100_000_000_000_000_000, // endowment
|
||||
500_000_000_000, // gas_limit
|
||||
None, // storage_deposit_limit
|
||||
code,
|
||||
vec![], // data
|
||||
vec![], // salt
|
||||
);
|
||||
|
||||
let events = self
|
||||
.cxt
|
||||
.api
|
||||
.client()
|
||||
.tx()
|
||||
.contracts()
|
||||
.instantiate_with_code(
|
||||
100_000_000_000_000_000, // endowment
|
||||
500_000_000_000, // gas_limit
|
||||
None, // storage_deposit_limit
|
||||
code,
|
||||
vec![], // data
|
||||
vec![], // salt
|
||||
)?
|
||||
.sign_and_submit_then_watch_default(&self.signer)
|
||||
.sign_and_submit_then_watch_default(&instantiate_tx, &self.signer)
|
||||
.await?
|
||||
.wait_for_finalized_success()
|
||||
.await?;
|
||||
@@ -108,19 +96,21 @@ impl ContractsTestContext {
|
||||
code_hash: Hash,
|
||||
data: Vec<u8>,
|
||||
salt: Vec<u8>,
|
||||
) -> Result<AccountId, Error<DispatchError>> {
|
||||
) -> Result<AccountId, Error> {
|
||||
// call instantiate extrinsic
|
||||
let instantiate_tx = node_runtime::tx().contracts().instantiate(
|
||||
100_000_000_000_000_000, // endowment
|
||||
500_000_000_000, // gas_limit
|
||||
None, // storage_deposit_limit
|
||||
code_hash,
|
||||
data,
|
||||
salt,
|
||||
);
|
||||
|
||||
let result = self
|
||||
.contracts_tx()
|
||||
.instantiate(
|
||||
100_000_000_000_000_000, // endowment
|
||||
500_000_000_000, // gas_limit
|
||||
None, // storage_deposit_limit
|
||||
code_hash,
|
||||
data,
|
||||
salt,
|
||||
)?
|
||||
.sign_and_submit_then_watch_default(&self.signer)
|
||||
.client()
|
||||
.tx()
|
||||
.sign_and_submit_then_watch_default(&instantiate_tx, &self.signer)
|
||||
.await?
|
||||
.wait_for_finalized_success()
|
||||
.await?;
|
||||
@@ -137,21 +127,20 @@ impl ContractsTestContext {
|
||||
&self,
|
||||
contract: AccountId,
|
||||
input_data: Vec<u8>,
|
||||
) -> Result<
|
||||
TransactionProgress<'_, DefaultConfig, DispatchError, node_runtime::Event>,
|
||||
Error<DispatchError>,
|
||||
> {
|
||||
) -> Result<TxProgress<SubstrateConfig, OnlineClient<SubstrateConfig>>, Error> {
|
||||
tracing::info!("call: {:?}", contract);
|
||||
let call_tx = node_runtime::tx().contracts().call(
|
||||
MultiAddress::Id(contract),
|
||||
0, // value
|
||||
500_000_000, // gas_limit
|
||||
None, // storage_deposit_limit
|
||||
input_data,
|
||||
);
|
||||
|
||||
let result = self
|
||||
.contracts_tx()
|
||||
.call(
|
||||
MultiAddress::Id(contract),
|
||||
0, // value
|
||||
500_000_000, // gas_limit
|
||||
None, // storage_deposit_limit
|
||||
input_data,
|
||||
)?
|
||||
.sign_and_submit_then_watch_default(&self.signer)
|
||||
.client()
|
||||
.tx()
|
||||
.sign_and_submit_then_watch_default(&call_tx, &self.signer)
|
||||
.await?;
|
||||
|
||||
tracing::info!("Call result: {:?}", result);
|
||||
@@ -190,19 +179,17 @@ async fn tx_call() {
|
||||
let cxt = ContractsTestContext::init().await;
|
||||
let (_, contract) = cxt.instantiate_with_code().await.unwrap();
|
||||
|
||||
let contract_info = cxt
|
||||
.cxt
|
||||
.api
|
||||
.storage()
|
||||
let info_addr = node_runtime::storage()
|
||||
.contracts()
|
||||
.contract_info_of(&contract, None)
|
||||
.await;
|
||||
.contract_info_of(&contract);
|
||||
|
||||
let contract_info = cxt.client().storage().fetch(&info_addr, None).await;
|
||||
assert!(contract_info.is_ok());
|
||||
|
||||
let keys = cxt
|
||||
.client()
|
||||
.storage()
|
||||
.fetch_keys::<storage::ContractInfoOf>(5, None, None)
|
||||
.fetch_keys(&info_addr.to_bytes(), 10, None, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.iter()
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
|
||||
use crate::{
|
||||
node_runtime::{
|
||||
self,
|
||||
runtime_types::pallet_staking::{
|
||||
RewardDestination,
|
||||
ValidatorPrefs,
|
||||
},
|
||||
staking,
|
||||
DispatchError,
|
||||
},
|
||||
pair_signer,
|
||||
test_context,
|
||||
@@ -20,7 +20,10 @@ use sp_core::{
|
||||
Pair,
|
||||
};
|
||||
use sp_keyring::AccountKeyring;
|
||||
use subxt::Error;
|
||||
use subxt::error::{
|
||||
DispatchError,
|
||||
Error,
|
||||
};
|
||||
|
||||
/// Helper function to generate a crypto pair from seed
|
||||
fn get_from_seed(seed: &str) -> sr25519::Pair {
|
||||
@@ -37,14 +40,17 @@ fn default_validator_prefs() -> ValidatorPrefs {
|
||||
|
||||
#[tokio::test]
|
||||
async fn validate_with_controller_account() {
|
||||
let alice = pair_signer(AccountKeyring::Alice.pair());
|
||||
let ctx = test_context().await;
|
||||
ctx.api
|
||||
.tx()
|
||||
let api = ctx.client();
|
||||
|
||||
let alice = pair_signer(AccountKeyring::Alice.pair());
|
||||
|
||||
let tx = node_runtime::tx()
|
||||
.staking()
|
||||
.validate(default_validator_prefs())
|
||||
.unwrap()
|
||||
.sign_and_submit_then_watch_default(&alice)
|
||||
.validate(default_validator_prefs());
|
||||
|
||||
api.tx()
|
||||
.sign_and_submit_then_watch_default(&tx, &alice)
|
||||
.await
|
||||
.unwrap()
|
||||
.wait_for_finalized_success()
|
||||
@@ -53,19 +59,23 @@ async fn validate_with_controller_account() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn validate_not_possible_for_stash_account() -> Result<(), Error<DispatchError>> {
|
||||
let alice_stash = pair_signer(get_from_seed("Alice//stash"));
|
||||
async fn validate_not_possible_for_stash_account() -> Result<(), Error> {
|
||||
let ctx = test_context().await;
|
||||
let announce_validator = ctx
|
||||
.api
|
||||
.tx()
|
||||
let api = ctx.client();
|
||||
|
||||
let alice_stash = pair_signer(get_from_seed("Alice//stash"));
|
||||
|
||||
let tx = node_runtime::tx()
|
||||
.staking()
|
||||
.validate(default_validator_prefs())?
|
||||
.sign_and_submit_then_watch_default(&alice_stash)
|
||||
.validate(default_validator_prefs());
|
||||
|
||||
let announce_validator = api
|
||||
.tx()
|
||||
.sign_and_submit_then_watch_default(&tx, &alice_stash)
|
||||
.await?
|
||||
.wait_for_finalized_success()
|
||||
.await;
|
||||
assert_matches!(announce_validator, Err(Error::Module(err)) => {
|
||||
assert_matches!(announce_validator, Err(Error::Runtime(DispatchError::Module(err))) => {
|
||||
assert_eq!(err.pallet, "Staking");
|
||||
assert_eq!(err.error, "NotController");
|
||||
});
|
||||
@@ -74,16 +84,18 @@ async fn validate_not_possible_for_stash_account() -> Result<(), Error<DispatchE
|
||||
|
||||
#[tokio::test]
|
||||
async fn nominate_with_controller_account() {
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
let alice = pair_signer(AccountKeyring::Alice.pair());
|
||||
let bob = pair_signer(AccountKeyring::Bob.pair());
|
||||
let ctx = test_context().await;
|
||||
|
||||
ctx.api
|
||||
.tx()
|
||||
let tx = node_runtime::tx()
|
||||
.staking()
|
||||
.nominate(vec![bob.account_id().clone().into()])
|
||||
.unwrap()
|
||||
.sign_and_submit_then_watch_default(&alice)
|
||||
.nominate(vec![bob.account_id().clone().into()]);
|
||||
|
||||
api.tx()
|
||||
.sign_and_submit_then_watch_default(&tx, &alice)
|
||||
.await
|
||||
.unwrap()
|
||||
.wait_for_finalized_success()
|
||||
@@ -92,22 +104,26 @@ async fn nominate_with_controller_account() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn nominate_not_possible_for_stash_account() -> Result<(), Error<DispatchError>> {
|
||||
async fn nominate_not_possible_for_stash_account() -> Result<(), Error> {
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
let alice_stash = pair_signer(get_from_seed("Alice//stash"));
|
||||
let bob = pair_signer(AccountKeyring::Bob.pair());
|
||||
let ctx = test_context().await;
|
||||
|
||||
let nomination = ctx
|
||||
.api
|
||||
.tx()
|
||||
let tx = node_runtime::tx()
|
||||
.staking()
|
||||
.nominate(vec![bob.account_id().clone().into()])?
|
||||
.sign_and_submit_then_watch_default(&alice_stash)
|
||||
.await?
|
||||
.nominate(vec![bob.account_id().clone().into()]);
|
||||
|
||||
let nomination = api
|
||||
.tx()
|
||||
.sign_and_submit_then_watch_default(&tx, &alice_stash)
|
||||
.await
|
||||
.unwrap()
|
||||
.wait_for_finalized_success()
|
||||
.await;
|
||||
|
||||
assert_matches!(nomination, Err(Error::Module(err)) => {
|
||||
assert_matches!(nomination, Err(Error::Runtime(DispatchError::Module(err))) => {
|
||||
assert_eq!(err.pallet, "Staking");
|
||||
assert_eq!(err.error, "NotController");
|
||||
});
|
||||
@@ -115,52 +131,45 @@ async fn nominate_not_possible_for_stash_account() -> Result<(), Error<DispatchE
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn chill_works_for_controller_only() -> Result<(), Error<DispatchError>> {
|
||||
async fn chill_works_for_controller_only() -> Result<(), Error> {
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
let alice_stash = pair_signer(get_from_seed("Alice//stash"));
|
||||
let bob_stash = pair_signer(get_from_seed("Bob//stash"));
|
||||
let alice = pair_signer(AccountKeyring::Alice.pair());
|
||||
let ctx = test_context().await;
|
||||
|
||||
// this will fail the second time, which is why this is one test, not two
|
||||
ctx.api
|
||||
.tx()
|
||||
let nominate_tx = node_runtime::tx()
|
||||
.staking()
|
||||
.nominate(vec![bob_stash.account_id().clone().into()])?
|
||||
.sign_and_submit_then_watch_default(&alice)
|
||||
.nominate(vec![bob_stash.account_id().clone().into()]);
|
||||
api.tx()
|
||||
.sign_and_submit_then_watch_default(&nominate_tx, &alice)
|
||||
.await?
|
||||
.wait_for_finalized_success()
|
||||
.await?;
|
||||
|
||||
let ledger = ctx
|
||||
.api
|
||||
.storage()
|
||||
.staking()
|
||||
.ledger(alice.account_id(), None)
|
||||
.await?
|
||||
.unwrap();
|
||||
let ledger_addr = node_runtime::storage().staking().ledger(alice.account_id());
|
||||
let ledger = api.storage().fetch(&ledger_addr, None).await?.unwrap();
|
||||
assert_eq!(alice_stash.account_id(), &ledger.stash);
|
||||
|
||||
let chill = ctx
|
||||
.api
|
||||
let chill_tx = node_runtime::tx().staking().chill();
|
||||
|
||||
let chill = api
|
||||
.tx()
|
||||
.staking()
|
||||
.chill()?
|
||||
.sign_and_submit_then_watch_default(&alice_stash)
|
||||
.sign_and_submit_then_watch_default(&chill_tx, &alice_stash)
|
||||
.await?
|
||||
.wait_for_finalized_success()
|
||||
.await;
|
||||
|
||||
assert_matches!(chill, Err(Error::Module(err)) => {
|
||||
assert_matches!(chill, Err(Error::Runtime(DispatchError::Module(err))) => {
|
||||
assert_eq!(err.pallet, "Staking");
|
||||
assert_eq!(err.error, "NotController");
|
||||
});
|
||||
|
||||
let is_chilled = ctx
|
||||
.api
|
||||
let is_chilled = api
|
||||
.tx()
|
||||
.staking()
|
||||
.chill()?
|
||||
.sign_and_submit_then_watch_default(&alice)
|
||||
.sign_and_submit_then_watch_default(&chill_tx, &alice)
|
||||
.await?
|
||||
.wait_for_finalized_success()
|
||||
.await?
|
||||
@@ -171,43 +180,35 @@ async fn chill_works_for_controller_only() -> Result<(), Error<DispatchError>> {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tx_bond() -> Result<(), Error<DispatchError>> {
|
||||
let alice = pair_signer(AccountKeyring::Alice.pair());
|
||||
async fn tx_bond() -> Result<(), Error> {
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
let bond = ctx
|
||||
.api
|
||||
let alice = pair_signer(AccountKeyring::Alice.pair());
|
||||
|
||||
let bond_tx = node_runtime::tx().staking().bond(
|
||||
AccountKeyring::Bob.to_account_id().into(),
|
||||
100_000_000_000_000,
|
||||
RewardDestination::Stash,
|
||||
);
|
||||
|
||||
let bond = api
|
||||
.tx()
|
||||
.staking()
|
||||
.bond(
|
||||
AccountKeyring::Bob.to_account_id().into(),
|
||||
100_000_000_000_000,
|
||||
RewardDestination::Stash,
|
||||
)
|
||||
.unwrap()
|
||||
.sign_and_submit_then_watch_default(&alice)
|
||||
.sign_and_submit_then_watch_default(&bond_tx, &alice)
|
||||
.await?
|
||||
.wait_for_finalized_success()
|
||||
.await;
|
||||
|
||||
assert!(bond.is_ok());
|
||||
|
||||
let bond_again = ctx
|
||||
.api
|
||||
let bond_again = api
|
||||
.tx()
|
||||
.staking()
|
||||
.bond(
|
||||
AccountKeyring::Bob.to_account_id().into(),
|
||||
100_000_000_000_000,
|
||||
RewardDestination::Stash,
|
||||
)
|
||||
.unwrap()
|
||||
.sign_and_submit_then_watch_default(&alice)
|
||||
.sign_and_submit_then_watch_default(&bond_tx, &alice)
|
||||
.await?
|
||||
.wait_for_finalized_success()
|
||||
.await;
|
||||
|
||||
assert_matches!(bond_again, Err(Error::Module(err)) => {
|
||||
assert_matches!(bond_again, Err(Error::Runtime(DispatchError::Module(err))) => {
|
||||
assert_eq!(err.pallet, "Staking");
|
||||
assert_eq!(err.error, "AlreadyBonded");
|
||||
});
|
||||
@@ -215,35 +216,37 @@ async fn tx_bond() -> Result<(), Error<DispatchError>> {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn storage_history_depth() -> Result<(), Error<DispatchError>> {
|
||||
async fn storage_history_depth() -> Result<(), Error> {
|
||||
let ctx = test_context().await;
|
||||
let history_depth = ctx.api.storage().staking().history_depth(None).await?;
|
||||
let api = ctx.client();
|
||||
let history_depth_addr = node_runtime::storage().staking().history_depth();
|
||||
let history_depth = api
|
||||
.storage()
|
||||
.fetch_or_default(&history_depth_addr, None)
|
||||
.await?;
|
||||
assert_eq!(history_depth, 84);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn storage_current_era() -> Result<(), Error<DispatchError>> {
|
||||
async fn storage_current_era() -> Result<(), Error> {
|
||||
let ctx = test_context().await;
|
||||
let _current_era = ctx
|
||||
.api
|
||||
let api = ctx.client();
|
||||
let current_era_addr = node_runtime::storage().staking().current_era();
|
||||
let _current_era = api
|
||||
.storage()
|
||||
.staking()
|
||||
.current_era(None)
|
||||
.fetch(¤t_era_addr, None)
|
||||
.await?
|
||||
.expect("current era always exists");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn storage_era_reward_points() -> Result<(), Error<DispatchError>> {
|
||||
let cxt = test_context().await;
|
||||
let current_era_result = cxt
|
||||
.api
|
||||
.storage()
|
||||
.staking()
|
||||
.eras_reward_points(&0, None)
|
||||
.await;
|
||||
async fn storage_era_reward_points() -> Result<(), Error> {
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
let reward_points_addr = node_runtime::storage().staking().eras_reward_points(&0);
|
||||
let current_era_result = api.storage().fetch(&reward_points_addr, None).await;
|
||||
assert!(current_era_result.is_ok());
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -4,35 +4,35 @@
|
||||
|
||||
use crate::{
|
||||
node_runtime::{
|
||||
self,
|
||||
runtime_types,
|
||||
sudo,
|
||||
DispatchError,
|
||||
},
|
||||
pair_signer,
|
||||
test_context,
|
||||
};
|
||||
use sp_keyring::AccountKeyring;
|
||||
|
||||
type Call = runtime_types::node_runtime::Call;
|
||||
type Call = runtime_types::kitchensink_runtime::Call;
|
||||
type BalancesCall = runtime_types::pallet_balances::pallet::Call;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sudo() -> Result<(), subxt::Error<DispatchError>> {
|
||||
async fn test_sudo() -> Result<(), subxt::Error> {
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
let alice = pair_signer(AccountKeyring::Alice.pair());
|
||||
let bob = AccountKeyring::Bob.to_account_id().into();
|
||||
let cxt = test_context().await;
|
||||
|
||||
let call = Call::Balances(BalancesCall::transfer {
|
||||
dest: bob,
|
||||
value: 10_000,
|
||||
});
|
||||
let tx = node_runtime::tx().sudo().sudo(call);
|
||||
|
||||
let found_event = cxt
|
||||
.api
|
||||
let found_event = api
|
||||
.tx()
|
||||
.sudo()
|
||||
.sudo(call)?
|
||||
.sign_and_submit_then_watch_default(&alice)
|
||||
.sign_and_submit_then_watch_default(&tx, &alice)
|
||||
.await?
|
||||
.wait_for_finalized_success()
|
||||
.await?
|
||||
@@ -43,22 +43,22 @@ async fn test_sudo() -> Result<(), subxt::Error<DispatchError>> {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sudo_unchecked_weight() -> Result<(), subxt::Error<DispatchError>> {
|
||||
async fn test_sudo_unchecked_weight() -> Result<(), subxt::Error> {
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
let alice = pair_signer(AccountKeyring::Alice.pair());
|
||||
let bob = AccountKeyring::Bob.to_account_id().into();
|
||||
let cxt = test_context().await;
|
||||
|
||||
let call = Call::Balances(BalancesCall::transfer {
|
||||
dest: bob,
|
||||
value: 10_000,
|
||||
});
|
||||
let tx = node_runtime::tx().sudo().sudo_unchecked_weight(call, 0);
|
||||
|
||||
let found_event = cxt
|
||||
.api
|
||||
let found_event = api
|
||||
.tx()
|
||||
.sudo()
|
||||
.sudo_unchecked_weight(call, 0)?
|
||||
.sign_and_submit_then_watch_default(&alice)
|
||||
.sign_and_submit_then_watch_default(&tx, &alice)
|
||||
.await?
|
||||
.wait_for_finalized_success()
|
||||
.await?
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
use crate::{
|
||||
node_runtime::{
|
||||
self,
|
||||
system,
|
||||
DispatchError,
|
||||
},
|
||||
pair_signer,
|
||||
test_context,
|
||||
@@ -14,15 +14,17 @@ use assert_matches::assert_matches;
|
||||
use sp_keyring::AccountKeyring;
|
||||
|
||||
#[tokio::test]
|
||||
async fn storage_account() -> Result<(), subxt::Error<DispatchError>> {
|
||||
async fn storage_account() -> Result<(), subxt::Error> {
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
let alice = pair_signer(AccountKeyring::Alice.pair());
|
||||
|
||||
let cxt = test_context().await;
|
||||
let account_info = cxt
|
||||
.api
|
||||
let account_info_addr = node_runtime::storage().system().account(alice.account_id());
|
||||
|
||||
let account_info = api
|
||||
.storage()
|
||||
.system()
|
||||
.account(alice.account_id(), None)
|
||||
.fetch_or_default(&account_info_addr, None)
|
||||
.await;
|
||||
|
||||
assert_matches!(account_info, Ok(_));
|
||||
@@ -30,16 +32,19 @@ async fn storage_account() -> Result<(), subxt::Error<DispatchError>> {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tx_remark_with_event() -> Result<(), subxt::Error<DispatchError>> {
|
||||
let alice = pair_signer(AccountKeyring::Alice.pair());
|
||||
let cxt = test_context().await;
|
||||
async fn tx_remark_with_event() -> Result<(), subxt::Error> {
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
let found_event = cxt
|
||||
.api
|
||||
.tx()
|
||||
let alice = pair_signer(AccountKeyring::Alice.pair());
|
||||
|
||||
let tx = node_runtime::tx()
|
||||
.system()
|
||||
.remark_with_event(b"remarkable".to_vec())?
|
||||
.sign_and_submit_then_watch_default(&alice)
|
||||
.remark_with_event(b"remarkable".to_vec());
|
||||
|
||||
let found_event = api
|
||||
.tx()
|
||||
.sign_and_submit_then_watch_default(&tx, &alice)
|
||||
.await?
|
||||
.wait_for_finalized_success()
|
||||
.await?
|
||||
|
||||
@@ -2,13 +2,20 @@
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use crate::test_context;
|
||||
use crate::{
|
||||
node_runtime,
|
||||
test_context,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn storage_get_current_timestamp() {
|
||||
let cxt = test_context().await;
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
let timestamp = cxt.api.storage().timestamp().now(None).await;
|
||||
let timestamp = api
|
||||
.storage()
|
||||
.fetch(&node_runtime::storage().timestamp().now(), None)
|
||||
.await;
|
||||
|
||||
assert!(timestamp.is_ok())
|
||||
}
|
||||
|
||||
@@ -24,3 +24,11 @@ mod storage;
|
||||
use test_runtime::node_runtime;
|
||||
#[cfg(test)]
|
||||
use utils::*;
|
||||
|
||||
// We don't use this dependency, but it's here so that we
|
||||
// can enable logging easily if need be. Add this to a test
|
||||
// to enable tracing for it:
|
||||
//
|
||||
// tracing_subscriber::fmt::init();
|
||||
#[cfg(test)]
|
||||
use tracing_subscriber as _;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// see LICENSE for license details.
|
||||
|
||||
use crate::{
|
||||
node_runtime,
|
||||
test_context,
|
||||
TestContext,
|
||||
};
|
||||
@@ -28,72 +29,57 @@ use scale_info::{
|
||||
TypeInfo,
|
||||
};
|
||||
use subxt::{
|
||||
ClientBuilder,
|
||||
DefaultConfig,
|
||||
Metadata,
|
||||
SubstrateExtrinsicParams,
|
||||
OfflineClient,
|
||||
SubstrateConfig,
|
||||
};
|
||||
|
||||
use crate::utils::node_runtime;
|
||||
|
||||
type RuntimeApi =
|
||||
node_runtime::RuntimeApi<DefaultConfig, SubstrateExtrinsicParams<DefaultConfig>>;
|
||||
|
||||
async fn metadata_to_api(metadata: RuntimeMetadataV14, cxt: &TestContext) -> RuntimeApi {
|
||||
async fn metadata_to_api(
|
||||
metadata: RuntimeMetadataV14,
|
||||
ctx: &TestContext,
|
||||
) -> OfflineClient<SubstrateConfig> {
|
||||
let prefixed = RuntimeMetadataPrefixed::from(metadata);
|
||||
let metadata = Metadata::try_from(prefixed).unwrap();
|
||||
|
||||
ClientBuilder::new()
|
||||
.set_url(cxt.node_proc.ws_url().to_string())
|
||||
.set_metadata(metadata)
|
||||
.build()
|
||||
.await
|
||||
.unwrap()
|
||||
.to_runtime_api::<node_runtime::RuntimeApi<
|
||||
DefaultConfig,
|
||||
SubstrateExtrinsicParams<DefaultConfig>,
|
||||
>>()
|
||||
OfflineClient::new(
|
||||
ctx.client().genesis_hash(),
|
||||
ctx.client().runtime_version(),
|
||||
metadata,
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn full_metadata_check() {
|
||||
let cxt = test_context().await;
|
||||
let api = &cxt.api;
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
// Runtime metadata is identical to the metadata used during API generation.
|
||||
assert!(api.validate_metadata().is_ok());
|
||||
assert!(node_runtime::validate_codegen(&api).is_ok());
|
||||
|
||||
// Modify the metadata.
|
||||
let mut metadata: RuntimeMetadataV14 = {
|
||||
let locked_client_metadata = api.client.metadata();
|
||||
let client_metadata = locked_client_metadata.read();
|
||||
client_metadata.runtime_metadata().clone()
|
||||
};
|
||||
let mut metadata: RuntimeMetadataV14 = api.metadata().runtime_metadata().clone();
|
||||
metadata.pallets[0].name = "NewPallet".to_string();
|
||||
|
||||
let new_api = metadata_to_api(metadata, &cxt).await;
|
||||
let api = metadata_to_api(metadata, &ctx).await;
|
||||
assert_eq!(
|
||||
new_api
|
||||
.validate_metadata()
|
||||
node_runtime::validate_codegen(&api)
|
||||
.expect_err("Validation should fail for incompatible metadata"),
|
||||
::subxt::MetadataError::IncompatibleMetadata
|
||||
::subxt::error::MetadataError::IncompatibleMetadata
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn constant_values_are_not_validated() {
|
||||
let cxt = test_context().await;
|
||||
let api = &cxt.api;
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
// Ensure that `ExistentialDeposit` is compatible before altering the metadata.
|
||||
assert!(cxt.api.constants().balances().existential_deposit().is_ok());
|
||||
let deposit_addr = node_runtime::constants().balances().existential_deposit();
|
||||
|
||||
// Retrieve existential deposit to validate it and confirm that it's OK.
|
||||
assert!(api.constants().at(&deposit_addr).is_ok());
|
||||
|
||||
// Modify the metadata.
|
||||
let mut metadata: RuntimeMetadataV14 = {
|
||||
let locked_client_metadata = api.client.metadata();
|
||||
let client_metadata = locked_client_metadata.read();
|
||||
client_metadata.runtime_metadata().clone()
|
||||
};
|
||||
let mut metadata: RuntimeMetadataV14 = api.metadata().runtime_metadata().clone();
|
||||
|
||||
let mut existential = metadata
|
||||
.pallets
|
||||
@@ -108,13 +94,10 @@ async fn constant_values_are_not_validated() {
|
||||
// Modifying a constant value should not lead to an error:
|
||||
existential.value = vec![0u8; 32];
|
||||
|
||||
let new_api = metadata_to_api(metadata, &cxt).await;
|
||||
let api = metadata_to_api(metadata, &ctx).await;
|
||||
|
||||
assert!(new_api.validate_metadata().is_ok());
|
||||
assert!(new_api.constants().balances().existential_deposit().is_ok());
|
||||
|
||||
// Other constant validation should not be impacted.
|
||||
assert!(new_api.constants().balances().max_locks().is_ok());
|
||||
assert!(node_runtime::validate_codegen(&api).is_ok());
|
||||
assert!(api.constants().at(&deposit_addr).is_ok());
|
||||
}
|
||||
|
||||
fn default_pallet() -> PalletMetadata {
|
||||
@@ -143,11 +126,15 @@ fn pallets_to_metadata(pallets: Vec<PalletMetadata>) -> RuntimeMetadataV14 {
|
||||
|
||||
#[tokio::test]
|
||||
async fn calls_check() {
|
||||
let cxt = test_context().await;
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
let unbond_tx = node_runtime::tx().staking().unbond(123_456_789_012_345);
|
||||
let withdraw_unbonded_addr = node_runtime::tx().staking().withdraw_unbonded(10);
|
||||
|
||||
// Ensure that `Unbond` and `WinthdrawUnbonded` calls are compatible before altering the metadata.
|
||||
assert!(cxt.api.tx().staking().unbond(123_456_789_012_345).is_ok());
|
||||
assert!(cxt.api.tx().staking().withdraw_unbonded(10).is_ok());
|
||||
assert!(api.tx().validate(&unbond_tx).is_ok());
|
||||
assert!(api.tx().validate(&withdraw_unbonded_addr).is_ok());
|
||||
|
||||
// Reconstruct the `Staking` call as is.
|
||||
struct CallRec;
|
||||
@@ -181,9 +168,11 @@ async fn calls_check() {
|
||||
..default_pallet()
|
||||
};
|
||||
let metadata = pallets_to_metadata(vec![pallet]);
|
||||
let new_api = metadata_to_api(metadata, &cxt).await;
|
||||
assert!(new_api.tx().staking().unbond(123_456_789_012_345).is_ok());
|
||||
assert!(new_api.tx().staking().withdraw_unbonded(10).is_ok());
|
||||
let api = metadata_to_api(metadata, &ctx).await;
|
||||
|
||||
// The calls should still be valid with this new type info:
|
||||
assert!(api.tx().validate(&unbond_tx).is_ok());
|
||||
assert!(api.tx().validate(&withdraw_unbonded_addr).is_ok());
|
||||
|
||||
// Change `Unbond` call but leave the rest as is.
|
||||
struct CallRecSecond;
|
||||
@@ -216,31 +205,24 @@ async fn calls_check() {
|
||||
..default_pallet()
|
||||
};
|
||||
let metadata = pallets_to_metadata(vec![pallet]);
|
||||
let new_api = metadata_to_api(metadata, &cxt).await;
|
||||
let api = metadata_to_api(metadata, &ctx).await;
|
||||
|
||||
// Unbond call should fail, while withdraw_unbonded remains compatible.
|
||||
assert!(new_api.tx().staking().unbond(123_456_789_012_345).is_err());
|
||||
assert!(new_api.tx().staking().withdraw_unbonded(10).is_ok());
|
||||
assert!(api.tx().validate(&unbond_tx).is_err());
|
||||
assert!(api.tx().validate(&withdraw_unbonded_addr).is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn storage_check() {
|
||||
let cxt = test_context().await;
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
let tx_count_addr = node_runtime::storage().system().extrinsic_count();
|
||||
let tx_len_addr = node_runtime::storage().system().all_extrinsics_len();
|
||||
|
||||
// Ensure that `ExtrinsicCount` and `EventCount` storages are compatible before altering the metadata.
|
||||
assert!(cxt
|
||||
.api
|
||||
.storage()
|
||||
.system()
|
||||
.extrinsic_count(None)
|
||||
.await
|
||||
.is_ok());
|
||||
assert!(cxt
|
||||
.api
|
||||
.storage()
|
||||
.system()
|
||||
.all_extrinsics_len(None)
|
||||
.await
|
||||
.is_ok());
|
||||
assert!(api.storage().validate(&tx_count_addr).is_ok());
|
||||
assert!(api.storage().validate(&tx_len_addr).is_ok());
|
||||
|
||||
// Reconstruct the storage.
|
||||
let storage = PalletStorageMetadata {
|
||||
@@ -268,19 +250,11 @@ async fn storage_check() {
|
||||
..default_pallet()
|
||||
};
|
||||
let metadata = pallets_to_metadata(vec![pallet]);
|
||||
let new_api = metadata_to_api(metadata, &cxt).await;
|
||||
assert!(new_api
|
||||
.storage()
|
||||
.system()
|
||||
.extrinsic_count(None)
|
||||
.await
|
||||
.is_ok());
|
||||
assert!(new_api
|
||||
.storage()
|
||||
.system()
|
||||
.all_extrinsics_len(None)
|
||||
.await
|
||||
.is_ok());
|
||||
let api = metadata_to_api(metadata, &ctx).await;
|
||||
|
||||
// The addresses should still validate:
|
||||
assert!(api.storage().validate(&tx_count_addr).is_ok());
|
||||
assert!(api.storage().validate(&tx_len_addr).is_ok());
|
||||
|
||||
// Reconstruct the storage while modifying ExtrinsicCount.
|
||||
let storage = PalletStorageMetadata {
|
||||
@@ -309,17 +283,9 @@ async fn storage_check() {
|
||||
..default_pallet()
|
||||
};
|
||||
let metadata = pallets_to_metadata(vec![pallet]);
|
||||
let new_api = metadata_to_api(metadata, &cxt).await;
|
||||
assert!(new_api
|
||||
.storage()
|
||||
.system()
|
||||
.extrinsic_count(None)
|
||||
.await
|
||||
.is_err());
|
||||
assert!(new_api
|
||||
.storage()
|
||||
.system()
|
||||
.all_extrinsics_len(None)
|
||||
.await
|
||||
.is_ok());
|
||||
let api = metadata_to_api(metadata, &ctx).await;
|
||||
|
||||
// The count route should fail now; the other will be ok still.
|
||||
assert!(api.storage().validate(&tx_count_addr).is_err());
|
||||
assert!(api.storage().validate(&tx_len_addr).is_ok());
|
||||
}
|
||||
|
||||
@@ -3,47 +3,48 @@
|
||||
// see LICENSE for license details.
|
||||
|
||||
use crate::{
|
||||
node_runtime::{
|
||||
self,
|
||||
DispatchError,
|
||||
},
|
||||
node_runtime,
|
||||
pair_signer,
|
||||
test_context,
|
||||
utils::wait_for_blocks,
|
||||
};
|
||||
use sp_keyring::AccountKeyring;
|
||||
|
||||
#[tokio::test]
|
||||
async fn storage_plain_lookup() -> Result<(), subxt::Error<DispatchError>> {
|
||||
async fn storage_plain_lookup() -> Result<(), subxt::Error> {
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
// Look up a plain value. Wait long enough that we don't get the genesis block data,
|
||||
// because it may have no storage associated with it.
|
||||
tokio::time::sleep(std::time::Duration::from_secs(6)).await;
|
||||
let entry = ctx.api.storage().timestamp().now(None).await?;
|
||||
wait_for_blocks(&api).await;
|
||||
|
||||
let addr = node_runtime::storage().timestamp().now();
|
||||
let entry = api.storage().fetch_or_default(&addr, None).await?;
|
||||
assert!(entry > 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn storage_map_lookup() -> Result<(), subxt::Error<DispatchError>> {
|
||||
async fn storage_map_lookup() -> Result<(), subxt::Error> {
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
let signer = pair_signer(AccountKeyring::Alice.pair());
|
||||
let alice = AccountKeyring::Alice.to_account_id();
|
||||
|
||||
// Do some transaction to bump the Alice nonce to 1:
|
||||
ctx.api
|
||||
.tx()
|
||||
.system()
|
||||
.remark(vec![1, 2, 3, 4, 5])?
|
||||
.sign_and_submit_then_watch_default(&signer)
|
||||
let remark_tx = node_runtime::tx().system().remark(vec![1, 2, 3, 4, 5]);
|
||||
api.tx()
|
||||
.sign_and_submit_then_watch_default(&remark_tx, &signer)
|
||||
.await?
|
||||
.wait_for_finalized_success()
|
||||
.await?;
|
||||
|
||||
// Look up the nonce for the user (we expect it to be 1).
|
||||
let entry = ctx.api.storage().system().account(&alice, None).await?;
|
||||
let nonce_addr = node_runtime::storage().system().account(&alice);
|
||||
let entry = api.storage().fetch_or_default(&nonce_addr, None).await?;
|
||||
assert_eq!(entry.nonce, 1);
|
||||
|
||||
Ok(())
|
||||
@@ -54,23 +55,15 @@ async fn storage_map_lookup() -> Result<(), subxt::Error<DispatchError>> {
|
||||
// treated as a StorageKey (ie we should hash both values together with one hasher, rather
|
||||
// than hash both values separately, or ignore the second value).
|
||||
#[tokio::test]
|
||||
async fn storage_n_mapish_key_is_properly_created(
|
||||
) -> Result<(), subxt::Error<DispatchError>> {
|
||||
async fn storage_n_mapish_key_is_properly_created() -> Result<(), subxt::Error> {
|
||||
use codec::Encode;
|
||||
use node_runtime::{
|
||||
runtime_types::sp_core::crypto::KeyTypeId,
|
||||
session::storage::KeyOwner,
|
||||
};
|
||||
use subxt::{
|
||||
storage::StorageKeyPrefix,
|
||||
StorageEntry,
|
||||
};
|
||||
use node_runtime::runtime_types::sp_core::crypto::KeyTypeId;
|
||||
|
||||
// This is what the generated code hashes a `session().key_owner(..)` key into:
|
||||
let actual_key_bytes = KeyOwner(&KeyTypeId([1, 2, 3, 4]), &[5u8, 6, 7, 8])
|
||||
.key()
|
||||
.final_key(StorageKeyPrefix::new::<KeyOwner>())
|
||||
.0;
|
||||
let actual_key_bytes = node_runtime::storage()
|
||||
.session()
|
||||
.key_owner(KeyTypeId([1, 2, 3, 4]), [5u8, 6, 7, 8])
|
||||
.to_bytes();
|
||||
|
||||
// Let's manually hash to what we assume it should be and compare:
|
||||
let expected_key_bytes = {
|
||||
@@ -89,8 +82,9 @@ async fn storage_n_mapish_key_is_properly_created(
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn storage_n_map_storage_lookup() -> Result<(), subxt::Error<DispatchError>> {
|
||||
async fn storage_n_map_storage_lookup() -> Result<(), subxt::Error> {
|
||||
let ctx = test_context().await;
|
||||
let api = ctx.client();
|
||||
|
||||
// Boilerplate; we create a new asset class with ID 99, and then
|
||||
// we "approveTransfer" of some of this asset class. This gives us an
|
||||
@@ -98,30 +92,27 @@ async fn storage_n_map_storage_lookup() -> Result<(), subxt::Error<DispatchError
|
||||
let signer = pair_signer(AccountKeyring::Alice.pair());
|
||||
let alice = AccountKeyring::Alice.to_account_id();
|
||||
let bob = AccountKeyring::Bob.to_account_id();
|
||||
ctx.api
|
||||
.tx()
|
||||
|
||||
let tx1 = node_runtime::tx()
|
||||
.assets()
|
||||
.create(99, alice.clone().into(), 1)?
|
||||
.sign_and_submit_then_watch_default(&signer)
|
||||
.create(99, alice.clone().into(), 1);
|
||||
let tx2 = node_runtime::tx()
|
||||
.assets()
|
||||
.approve_transfer(99, bob.clone().into(), 123);
|
||||
api.tx()
|
||||
.sign_and_submit_then_watch_default(&tx1, &signer)
|
||||
.await?
|
||||
.wait_for_finalized_success()
|
||||
.await?;
|
||||
ctx.api
|
||||
.tx()
|
||||
.assets()
|
||||
.approve_transfer(99, bob.clone().into(), 123)?
|
||||
.sign_and_submit_then_watch_default(&signer)
|
||||
api.tx()
|
||||
.sign_and_submit_then_watch_default(&tx2, &signer)
|
||||
.await?
|
||||
.wait_for_finalized_success()
|
||||
.await?;
|
||||
|
||||
// The actual test; look up this approval in storage:
|
||||
let entry = ctx
|
||||
.api
|
||||
.storage()
|
||||
.assets()
|
||||
.approvals(&99, &alice, &bob, None)
|
||||
.await?;
|
||||
let addr = node_runtime::storage().assets().approvals(99, &alice, &bob);
|
||||
let entry = api.storage().fetch(&addr, None).await?;
|
||||
assert_eq!(entry.map(|a| a.amount), Some(123));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -10,20 +10,14 @@ pub(crate) use crate::{
|
||||
use sp_core::sr25519::Pair;
|
||||
use sp_keyring::AccountKeyring;
|
||||
use subxt::{
|
||||
Client,
|
||||
DefaultConfig,
|
||||
PairSigner,
|
||||
SubstrateExtrinsicParams,
|
||||
tx::PairSigner,
|
||||
SubstrateConfig,
|
||||
};
|
||||
|
||||
/// substrate node should be installed on the $PATH
|
||||
const SUBSTRATE_NODE_PATH: &str = "substrate";
|
||||
|
||||
pub type NodeRuntimeParams = SubstrateExtrinsicParams<DefaultConfig>;
|
||||
|
||||
pub async fn test_node_process_with(
|
||||
key: AccountKeyring,
|
||||
) -> TestNodeProcess<DefaultConfig> {
|
||||
pub async fn test_context_with(key: AccountKeyring) -> TestContext {
|
||||
let path = std::env::var("SUBSTRATE_NODE_PATH").unwrap_or_else(|_| {
|
||||
if which::which(SUBSTRATE_NODE_PATH).is_err() {
|
||||
panic!("A substrate binary should be installed on your path for integration tests. \
|
||||
@@ -32,35 +26,19 @@ pub async fn test_node_process_with(
|
||||
SUBSTRATE_NODE_PATH.to_string()
|
||||
});
|
||||
|
||||
let proc = TestNodeProcess::<DefaultConfig>::build(path.as_str())
|
||||
let proc = TestContext::build(path.as_str())
|
||||
.with_authority(key)
|
||||
.spawn::<DefaultConfig>()
|
||||
.spawn::<SubstrateConfig>()
|
||||
.await;
|
||||
proc.unwrap()
|
||||
}
|
||||
|
||||
pub async fn test_node_process() -> TestNodeProcess<DefaultConfig> {
|
||||
test_node_process_with(AccountKeyring::Alice).await
|
||||
}
|
||||
|
||||
pub struct TestContext {
|
||||
pub node_proc: TestNodeProcess<DefaultConfig>,
|
||||
pub api: node_runtime::RuntimeApi<DefaultConfig, NodeRuntimeParams>,
|
||||
}
|
||||
|
||||
impl TestContext {
|
||||
pub fn client(&self) -> &Client<DefaultConfig> {
|
||||
&self.api.client
|
||||
}
|
||||
}
|
||||
pub type TestContext = TestNodeProcess<SubstrateConfig>;
|
||||
|
||||
pub async fn test_context() -> TestContext {
|
||||
tracing_subscriber::fmt::try_init().ok();
|
||||
let node_proc = test_node_process_with(AccountKeyring::Alice).await;
|
||||
let api = node_proc.client().clone().to_runtime_api();
|
||||
TestContext { node_proc, api }
|
||||
test_context_with(AccountKeyring::Alice).await
|
||||
}
|
||||
|
||||
pub fn pair_signer(pair: Pair) -> PairSigner<DefaultConfig, Pair> {
|
||||
pub fn pair_signer(pair: Pair) -> PairSigner<SubstrateConfig, Pair> {
|
||||
PairSigner::new(pair)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
mod context;
|
||||
mod node_proc;
|
||||
mod wait_for_blocks;
|
||||
|
||||
pub use context::*;
|
||||
pub use node_proc::TestNodeProcess;
|
||||
pub use wait_for_blocks::wait_for_blocks;
|
||||
|
||||
@@ -16,16 +16,14 @@ use std::{
|
||||
process,
|
||||
};
|
||||
use subxt::{
|
||||
Client,
|
||||
ClientBuilder,
|
||||
Config,
|
||||
OnlineClient,
|
||||
};
|
||||
|
||||
/// Spawn a local substrate node for testing subxt.
|
||||
pub struct TestNodeProcess<R: Config> {
|
||||
proc: process::Child,
|
||||
client: Client<R>,
|
||||
ws_url: String,
|
||||
client: OnlineClient<R>,
|
||||
}
|
||||
|
||||
impl<R> Drop for TestNodeProcess<R>
|
||||
@@ -61,13 +59,8 @@ where
|
||||
}
|
||||
|
||||
/// Returns the subxt client connected to the running node.
|
||||
pub fn client(&self) -> &Client<R> {
|
||||
&self.client
|
||||
}
|
||||
|
||||
/// Returns the address to which the client is connected.
|
||||
pub fn ws_url(&self) -> &str {
|
||||
&self.ws_url
|
||||
pub fn client(&self) -> OnlineClient<R> {
|
||||
self.client.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,15 +122,9 @@ impl TestNodeProcessBuilder {
|
||||
let ws_url = format!("ws://127.0.0.1:{}", ws_port);
|
||||
|
||||
// Connect to the node with a subxt client:
|
||||
let client = ClientBuilder::new().set_url(ws_url.clone()).build().await;
|
||||
let client = OnlineClient::from_url(ws_url.clone()).await;
|
||||
match client {
|
||||
Ok(client) => {
|
||||
Ok(TestNodeProcess {
|
||||
proc,
|
||||
client,
|
||||
ws_url,
|
||||
})
|
||||
}
|
||||
Ok(client) => Ok(TestNodeProcess { proc, client }),
|
||||
Err(err) => {
|
||||
let err = format!("Failed to connect to node rpc at {}: {}", ws_url, err);
|
||||
tracing::error!("{}", err);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// 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 subxt::{
|
||||
client::OnlineClientT,
|
||||
Config,
|
||||
};
|
||||
|
||||
/// Wait for blocks to be produced before running tests. Waiting for two blocks
|
||||
/// (the genesis block and another one) seems to be enough to allow tests
|
||||
/// like `dry_run_passes` to work properly.
|
||||
pub async fn wait_for_blocks<C: Config>(api: &impl OnlineClientT<C>) {
|
||||
let mut sub = api.rpc().subscribe_blocks().await.unwrap();
|
||||
sub.next().await;
|
||||
sub.next().await;
|
||||
}
|
||||
Reference in New Issue
Block a user