Obtain DispatchError::Module info dynamically (#453)

* Add error information back into metadata to roll back removal in #394

* Go back to obtaining runtime error info

* re-do codegen too to check that it's all gravy

* Convert DispatchError module errors into a module variant to make them easier to work with

* Fix broken doc link
This commit is contained in:
James Wilson
2022-02-17 13:45:57 +00:00
committed by GitHub
parent eeb8b4b741
commit e866d744de
10 changed files with 1670 additions and 1610 deletions
+21 -147
View File
@@ -15,64 +15,18 @@
// along with subxt. If not, see <http://www.gnu.org/licenses/>.
use frame_metadata::v14::RuntimeMetadataV14;
use proc_macro2::{
Span as Span2,
TokenStream as TokenStream2,
};
use proc_macro2::TokenStream as TokenStream2;
use proc_macro_error::abort_call_site;
use quote::quote;
use scale_info::TypeDef;
/// Tokens which allow us to provide static error information in the generated output.
pub struct ErrorDetails {
/// This type definition will be used in the `dispatch_error_impl_fn` and is
/// expected to be generated somewhere in scope for that to be possible.
pub type_def: TokenStream2,
// A function which will live in an impl block for our `DispatchError`,
// to statically return details for known error types:
pub dispatch_error_impl_fn: TokenStream2,
}
impl ErrorDetails {
fn emit_compile_error(err: &str) -> ErrorDetails {
let err_lit_str = syn::LitStr::new(err, Span2::call_site());
ErrorDetails {
type_def: quote!(),
dispatch_error_impl_fn: quote!(compile_error!(#err_lit_str)),
}
}
}
/// The purpose of this is to enumerate all of the possible `(module_index, error_index)` error
/// variants, so that we can convert `u8` error codes inside a generated `DispatchError` into
/// nicer error strings with documentation. To do this, we emit the type we'll return instances of,
/// and a function that returns such an instance for all of the error codes seen in the metadata.
pub fn generate_error_details(metadata: &RuntimeMetadataV14) -> ErrorDetails {
let errors = match pallet_errors(metadata) {
Ok(errors) => errors,
Err(e) => {
let err_string =
format!("Failed to generate error details from metadata: {}", e);
return ErrorDetails::emit_compile_error(&err_string)
}
};
let match_body_items = errors.into_iter().map(|err| {
let docs = err.docs;
let pallet_index = err.pallet_index;
let error_index = err.error_index;
let pallet_name = err.pallet;
let error_name = err.error;
quote! {
(#pallet_index, #error_index) => Some(ErrorDetails {
pallet: #pallet_name,
error: #error_name,
docs: #docs
})
}
});
/// 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_def = metadata
.types
.types()
@@ -111,108 +65,28 @@ pub fn generate_error_details(metadata: &RuntimeMetadataV14) -> ErrorDetails {
false
};
let dispatch_error_impl_fn = if module_variant_is_struct {
let trait_fn_body = if module_variant_is_struct {
quote! {
pub fn details(&self) -> Option<ErrorDetails> {
if let Self::Module { error, index } = self {
match (index, error) {
#( #match_body_items ),*,
_ => None
}
} else {
None
}
if let &Self::Module { index, error } = self {
Some((index, error))
} else {
None
}
}
} else {
quote! {
pub fn details(&self) -> Option<ErrorDetails> {
if let Self::Module (module_error) = self {
match (module_error.index, module_error.error) {
#( #match_body_items ),*,
_ => None
}
} else {
None
}
if let Self::Module (module_error) = self {
Some((module_error.index, module_error.error))
} else {
None
}
}
};
ErrorDetails {
type_def: quote! {
pub struct ErrorDetails {
pub pallet: &'static str,
pub error: &'static str,
pub docs: &'static str,
}
},
dispatch_error_impl_fn,
}
}
fn pallet_errors(
metadata: &RuntimeMetadataV14,
) -> Result<Vec<ErrorMetadata>, InvalidMetadataError> {
let get_type_def_variant = |type_id: u32| {
let ty = metadata
.types
.resolve(type_id)
.ok_or(InvalidMetadataError::MissingType(type_id))?;
if let scale_info::TypeDef::Variant(var) = ty.type_def() {
Ok(var)
} else {
Err(InvalidMetadataError::TypeDefNotVariant(type_id))
}
};
let mut pallet_errors = vec![];
for pallet in &metadata.pallets {
let error = match &pallet.error {
Some(err) => err,
None => continue,
};
let type_def_variant = get_type_def_variant(error.ty.id())?;
for var in type_def_variant.variants().iter() {
pallet_errors.push(ErrorMetadata {
pallet_index: pallet.index,
error_index: var.index(),
pallet: pallet.name.clone(),
error: var.name().clone(),
docs: var.docs().join("\n"),
});
}
}
Ok(pallet_errors)
}
/// Information about each error that we find in the metadata;
/// used to generate the static error information.
#[derive(Clone, Debug)]
struct ErrorMetadata {
pub pallet_index: u8,
pub error_index: u8,
pub pallet: String,
pub error: String,
pub docs: String,
}
#[derive(Debug)]
enum InvalidMetadataError {
MissingType(u32),
TypeDefNotVariant(u32),
}
impl std::fmt::Display for InvalidMetadataError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
InvalidMetadataError::MissingType(n) => {
write!(f, "Type {} missing from type registry", n)
}
InvalidMetadataError::TypeDefNotVariant(n) => {
write!(f, "Type {} was not a variant/enum type", n)
quote! {
impl ::subxt::HasModuleError for #types_mod_ident::sp_runtime::DispatchError {
fn module_error_indices(&self) -> Option<(u8,u8)> {
#trait_fn_body
}
}
}
+4 -9
View File
@@ -265,9 +265,8 @@ impl RuntimeGenerator {
pallet.calls.as_ref().map(|_| pallet_mod_name)
});
let error_details = errors::generate_error_details(&self.metadata);
let error_type = error_details.type_def;
let error_fn = error_details.dispatch_error_impl_fn;
let has_module_error_impl =
errors::generate_has_module_error_impl(&self.metadata, types_mod_ident);
let default_account_data_ident = format_ident!("DefaultAccountData");
let default_account_data_impl = generate_default_account_data_impl(
@@ -291,12 +290,8 @@ impl RuntimeGenerator {
/// The default error type returned when there is a runtime issue.
pub type DispatchError = #types_mod_ident::sp_runtime::DispatchError;
// Statically generate error information so that we don't need runtime metadata for it.
#error_type
impl DispatchError {
#error_fn
}
// Impl HasModuleError on DispatchError so we can pluck out module error details.
#has_module_error_impl
#default_account_data_impl
+5 -2
View File
@@ -19,7 +19,10 @@ use sp_runtime::traits::Hash;
pub use sp_runtime::traits::SignedExtension;
use crate::{
error::BasicError,
error::{
BasicError,
HasModuleError,
},
extrinsic::{
self,
SignedExtra,
@@ -206,7 +209,7 @@ where
X: SignedExtra<T>,
A: AccountData,
C: Call + Send + Sync,
E: Decode,
E: Decode + HasModuleError,
Evs: Decode,
{
/// Create a new [`SubmittableExtrinsic`].
+25
View File
@@ -70,6 +70,9 @@ pub enum GenericError<E> {
/// 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),
/// Other error.
#[error("Other error: {0}")]
Other(String),
@@ -94,6 +97,7 @@ impl<E> GenericError<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)),
@@ -167,3 +171,24 @@ pub enum TransactionError {
#[error("The block containing the transaction can no longer be found (perhaps it was on a non-finalized fork?)")]
BlockHashNotFound,
}
/// Details about a module error that has occurred.
#[derive(Clone, Debug, thiserror::Error)]
#[error("{pallet}: {error}\n\n{}", .description.join("\n"))]
pub struct ModuleError {
/// The name of the pallet that the error came from.
pub pallet: String,
/// The name of the error.
pub error: String,
/// A description of the error.
pub description: Vec<String>,
}
/// 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_indices(&self) -> Option<(u8, u8)>;
}
+2
View File
@@ -81,6 +81,7 @@ pub use crate::{
BasicError,
Error,
GenericError,
HasModuleError,
RuntimeError,
TransactionError,
},
@@ -98,6 +99,7 @@ pub use crate::{
UncheckedExtrinsic,
},
metadata::{
ErrorMetadata,
Metadata,
MetadataError,
PalletMetadata,
+66
View File
@@ -84,6 +84,7 @@ pub struct Metadata {
metadata: RuntimeMetadataLastVersion,
pallets: HashMap<String, PalletMetadata>,
events: HashMap<(u8, u8), EventMetadata>,
errors: HashMap<(u8, u8), ErrorMetadata>,
}
impl Metadata {
@@ -107,6 +108,19 @@ impl Metadata {
Ok(event)
}
/// Returns the metadata for the error at the given pallet and error indices.
pub fn error(
&self,
pallet_index: u8,
error_index: u8,
) -> Result<&ErrorMetadata, MetadataError> {
let error = self
.errors
.get(&(pallet_index, error_index))
.ok_or(MetadataError::ErrorNotFound(pallet_index, error_index))?;
Ok(error)
}
/// Resolve a type definition.
pub fn resolve_type(&self, id: u32) -> Option<&Type<PortableForm>> {
self.metadata.types.resolve(id)
@@ -169,6 +183,7 @@ impl PalletMetadata {
}
}
/// Metadata for specific events.
#[derive(Clone, Debug)]
pub struct EventMetadata {
pallet: String,
@@ -193,6 +208,31 @@ impl EventMetadata {
}
}
/// Metadata for specific errors.
#[derive(Clone, Debug)]
pub struct ErrorMetadata {
pallet: String,
error: String,
variant: Variant<PortableForm>,
}
impl ErrorMetadata {
/// Get the name of the pallet from which the error originates.
pub fn pallet(&self) -> &str {
&self.pallet
}
/// Get the name of the specific pallet error.
pub fn error(&self) -> &str {
&self.error
}
/// Get the description of the specific pallet error.
pub fn description(&self) -> &[String] {
self.variant.docs()
}
}
#[derive(Debug, thiserror::Error)]
pub enum InvalidMetadataError {
#[error("Invalid prefix")]
@@ -293,10 +333,36 @@ impl TryFrom<RuntimeMetadataPrefixed> for Metadata {
})
.collect();
let pallet_errors = metadata
.pallets
.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();
Ok(Self {
metadata,
pallets,
events,
errors,
})
}
}
+23 -9
View File
@@ -27,6 +27,8 @@ use crate::{
error::{
BasicError,
Error,
HasModuleError,
ModuleError,
RuntimeError,
TransactionError,
},
@@ -54,7 +56,7 @@ use jsonrpsee::core::{
/// returned from [`crate::SubmittableExtrinsic::sign_and_submit_then_watch()`].
#[derive(Derivative)]
#[derivative(Debug(bound = ""))]
pub struct TransactionProgress<'client, T: Config, E: Decode, Evs: Decode> {
pub struct TransactionProgress<'client, T: Config, E, Evs> {
sub: Option<RpcSubscription<SubstrateTransactionStatus<T::Hash, T::Hash>>>,
ext_hash: T::Hash,
client: &'client Client<T>,
@@ -64,12 +66,11 @@ pub struct TransactionProgress<'client, T: Config, E: Decode, Evs: Decode> {
// 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: Decode, Evs: Decode> Unpin
for TransactionProgress<'client, T, E, Evs>
{
}
impl<'client, T: Config, E, Evs> Unpin for TransactionProgress<'client, T, E, Evs> {}
impl<'client, T: Config, E: Decode, Evs: Decode> TransactionProgress<'client, T, E, Evs> {
impl<'client, T: Config, E: Decode + HasModuleError, Evs: Decode>
TransactionProgress<'client, T, E, Evs>
{
/// Instantiate a new [`TransactionProgress`] from a custom subscription.
pub fn new(
sub: RpcSubscription<SubstrateTransactionStatus<T::Hash, T::Hash>>,
@@ -171,7 +172,7 @@ impl<'client, T: Config, E: Decode, Evs: Decode> TransactionProgress<'client, T,
}
}
impl<'client, T: Config, E: Decode, Evs: Decode> Stream
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>;
@@ -340,7 +341,9 @@ pub struct TransactionInBlock<'client, T: Config, E: Decode, Evs: Decode> {
_error: PhantomDataSendSync<(E, Evs)>,
}
impl<'client, T: Config, E: Decode, Evs: Decode> TransactionInBlock<'client, T, E, Evs> {
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,
@@ -387,7 +390,18 @@ impl<'client, T: Config, E: Decode, Evs: Decode> TransactionInBlock<'client, T,
let ev = ev?;
if &ev.pallet == "System" && &ev.variant == "ExtrinsicFailed" {
let dispatch_error = E::decode(&mut &*ev.data)?;
return Err(Error::Runtime(RuntimeError(dispatch_error)))
if let Some((pallet_idx, error_idx)) =
dispatch_error.module_error_indices()
{
let details = self.client.metadata().error(pallet_idx, error_idx)?;
return Err(Error::Module(ModuleError {
pallet: details.pallet().to_string(),
error: details.error().to_string(),
description: details.description().to_vec(),
}))
} else {
return Err(Error::Runtime(RuntimeError(dispatch_error)))
}
}
}
File diff suppressed because one or more lines are too long
+6 -7
View File
@@ -154,9 +154,9 @@ async fn transfer_error() {
let alice_addr = alice.account_id().clone().into();
let hans = pair_signer(Pair::generate().0);
let hans_address = hans.account_id().clone().into();
let cxt = test_context().await;
let ctx = test_context().await;
cxt.api
ctx.api
.tx()
.balances()
.transfer(hans_address, 100_000_000_000_000_000)
@@ -167,7 +167,7 @@ async fn transfer_error() {
.await
.unwrap();
let res = cxt
let res = ctx
.api
.tx()
.balances()
@@ -178,10 +178,9 @@ async fn transfer_error() {
.wait_for_finalized_success()
.await;
if let Err(Error::Runtime(err)) = res {
let details = err.inner().details().unwrap();
assert_eq!(details.pallet, "Balances");
assert_eq!(details.error, "InsufficientBalance");
if let Err(Error::Module(err)) = res {
assert_eq!(err.pallet, "Balances");
assert_eq!(err.error, "InsufficientBalance");
} else {
panic!("expected a runtime module error");
}
+34 -38
View File
@@ -53,8 +53,8 @@ fn default_validator_prefs() -> ValidatorPrefs {
#[async_std::test]
async fn validate_with_controller_account() {
let alice = pair_signer(AccountKeyring::Alice.pair());
let cxt = test_context().await;
cxt.api
let ctx = test_context().await;
ctx.api
.tx()
.staking()
.validate(default_validator_prefs())
@@ -69,8 +69,8 @@ async fn validate_with_controller_account() {
#[async_std::test]
async fn validate_not_possible_for_stash_account() -> Result<(), Error<DispatchError>> {
let alice_stash = pair_signer(get_from_seed("Alice//stash"));
let cxt = test_context().await;
let announce_validator = cxt
let ctx = test_context().await;
let announce_validator = ctx
.api
.tx()
.staking()
@@ -79,10 +79,9 @@ async fn validate_not_possible_for_stash_account() -> Result<(), Error<DispatchE
.await?
.wait_for_finalized_success()
.await;
assert_matches!(announce_validator, Err(Error::Runtime(err)) => {
let details = err.inner().details().unwrap();
assert_eq!(details.pallet, "Staking");
assert_eq!(details.error, "NotController");
assert_matches!(announce_validator, Err(Error::Module(err)) => {
assert_eq!(err.pallet, "Staking");
assert_eq!(err.error, "NotController");
});
Ok(())
}
@@ -91,9 +90,9 @@ async fn validate_not_possible_for_stash_account() -> Result<(), Error<DispatchE
async fn nominate_with_controller_account() {
let alice = pair_signer(AccountKeyring::Alice.pair());
let bob = pair_signer(AccountKeyring::Bob.pair());
let cxt = test_context().await;
let ctx = test_context().await;
cxt.api
ctx.api
.tx()
.staking()
.nominate(vec![bob.account_id().clone().into()])
@@ -109,9 +108,9 @@ async fn nominate_with_controller_account() {
async fn nominate_not_possible_for_stash_account() -> Result<(), Error<DispatchError>> {
let alice_stash = pair_signer(get_from_seed("Alice//stash"));
let bob = pair_signer(AccountKeyring::Bob.pair());
let cxt = test_context().await;
let ctx = test_context().await;
let nomination = cxt
let nomination = ctx
.api
.tx()
.staking()
@@ -121,10 +120,9 @@ async fn nominate_not_possible_for_stash_account() -> Result<(), Error<DispatchE
.wait_for_finalized_success()
.await;
assert_matches!(nomination, Err(Error::Runtime(err)) => {
let details = err.inner().details().unwrap();
assert_eq!(details.pallet, "Staking");
assert_eq!(details.error, "NotController");
assert_matches!(nomination, Err(Error::Module(err)) => {
assert_eq!(err.pallet, "Staking");
assert_eq!(err.error, "NotController");
});
Ok(())
}
@@ -134,10 +132,10 @@ async fn chill_works_for_controller_only() -> Result<(), Error<DispatchError>> {
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 cxt = test_context().await;
let ctx = test_context().await;
// this will fail the second time, which is why this is one test, not two
cxt.api
ctx.api
.tx()
.staking()
.nominate(vec![bob_stash.account_id().clone().into()])
@@ -146,7 +144,7 @@ async fn chill_works_for_controller_only() -> Result<(), Error<DispatchError>> {
.wait_for_finalized_success()
.await?;
let ledger = cxt
let ledger = ctx
.api
.storage()
.staking()
@@ -155,7 +153,7 @@ async fn chill_works_for_controller_only() -> Result<(), Error<DispatchError>> {
.unwrap();
assert_eq!(alice_stash.account_id(), &ledger.stash);
let chill = cxt
let chill = ctx
.api
.tx()
.staking()
@@ -165,13 +163,12 @@ async fn chill_works_for_controller_only() -> Result<(), Error<DispatchError>> {
.wait_for_finalized_success()
.await;
assert_matches!(chill, Err(Error::Runtime(err)) => {
let details = err.inner().details().unwrap();
assert_eq!(details.pallet, "Staking");
assert_eq!(details.error, "NotController");
assert_matches!(chill, Err(Error::Module(err)) => {
assert_eq!(err.pallet, "Staking");
assert_eq!(err.error, "NotController");
});
let is_chilled = cxt
let is_chilled = ctx
.api
.tx()
.staking()
@@ -189,9 +186,9 @@ async fn chill_works_for_controller_only() -> Result<(), Error<DispatchError>> {
#[async_std::test]
async fn tx_bond() -> Result<(), Error<DispatchError>> {
let alice = pair_signer(AccountKeyring::Alice.pair());
let cxt = test_context().await;
let ctx = test_context().await;
let bond = cxt
let bond = ctx
.api
.tx()
.staking()
@@ -207,7 +204,7 @@ async fn tx_bond() -> Result<(), Error<DispatchError>> {
assert!(bond.is_ok());
let bond_again = cxt
let bond_again = ctx
.api
.tx()
.staking()
@@ -221,26 +218,25 @@ async fn tx_bond() -> Result<(), Error<DispatchError>> {
.wait_for_finalized_success()
.await;
assert_matches!(bond_again, Err(Error::Runtime(err)) => {
let details = err.inner().details().unwrap();
assert_eq!(details.pallet, "Staking");
assert_eq!(details.error, "AlreadyBonded");
assert_matches!(bond_again, Err(Error::Module(err)) => {
assert_eq!(err.pallet, "Staking");
assert_eq!(err.error, "AlreadyBonded");
});
Ok(())
}
#[async_std::test]
async fn storage_history_depth() -> Result<(), Error<DispatchError>> {
let cxt = test_context().await;
let history_depth = cxt.api.storage().staking().history_depth(None).await?;
let ctx = test_context().await;
let history_depth = ctx.api.storage().staking().history_depth(None).await?;
assert_eq!(history_depth, 84);
Ok(())
}
#[async_std::test]
async fn storage_current_era() -> Result<(), Error<DispatchError>> {
let cxt = test_context().await;
let _current_era = cxt
let ctx = test_context().await;
let _current_era = ctx
.api
.storage()
.staking()
@@ -252,8 +248,8 @@ async fn storage_current_era() -> Result<(), Error<DispatchError>> {
#[async_std::test]
async fn storage_era_reward_points() -> Result<(), Error<DispatchError>> {
let cxt = test_context().await;
let current_era_result = cxt
let ctx = test_context().await;
let current_era_result = ctx
.api
.storage()
.staking()