Skip to content

Commit

Permalink
Obtain DispatchError::Module info dynamically (#453)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jsdw authored Feb 17, 2022
1 parent eeb8b4b commit e866d74
Show file tree
Hide file tree
Showing 10 changed files with 1,670 additions and 1,610 deletions.
168 changes: 21 additions & 147 deletions codegen/src/api/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
}
}
}
Expand Down
13 changes: 4 additions & 9 deletions codegen/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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

Expand Down
7 changes: 5 additions & 2 deletions subxt/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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`].
Expand Down
25 changes: 25 additions & 0 deletions subxt/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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)),
Expand Down Expand Up @@ -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 changes: 2 additions & 0 deletions subxt/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ pub use crate::{
BasicError,
Error,
GenericError,
HasModuleError,
RuntimeError,
TransactionError,
},
Expand All @@ -98,6 +99,7 @@ pub use crate::{
UncheckedExtrinsic,
},
metadata::{
ErrorMetadata,
Metadata,
MetadataError,
PalletMetadata,
Expand Down
Loading

0 comments on commit e866d74

Please sign in to comment.