Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle sp_runtime::ModuleError substrate updates #492

Merged
merged 15 commits into from
Mar 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
200 changes: 154 additions & 46 deletions codegen/src/api/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,140 @@ use frame_metadata::v14::RuntimeMetadataV14;
use proc_macro2::TokenStream as TokenStream2;
use proc_macro_error::abort_call_site;
use quote::quote;
use scale_info::TypeDef;
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,
Expand All @@ -27,7 +160,7 @@ pub fn generate_has_module_error_impl(
metadata: &RuntimeMetadataV14,
types_mod_ident: &syn::Ident,
) -> TokenStream2 {
let dispatch_error_def = metadata
let dispatch_error = metadata
.types
.types()
.iter()
Expand All @@ -38,55 +171,30 @@ pub fn generate_has_module_error_impl(
.ty()
.type_def();

// Slightly older versions of substrate have a `DispatchError::Module { index, error }`
// variant. Newer versions have something like a `DispatchError::Module (Details)` variant.
// We check to see which type of variant we're dealing with based on the metadata, and
// generate the correct code to handle either older or newer substrate versions.
let module_variant_is_struct = if let TypeDef::Variant(details) = dispatch_error_def {
let module_variant = details
.variants()
.iter()
.find(|variant| variant.name() == "Module")
.unwrap_or_else(|| {
abort_call_site!("DispatchError::Module variant expected in metadata")
});
let are_fields_named = module_variant
.fields()
.get(0)
.unwrap_or_else(|| {
abort_call_site!(
"DispatchError::Module expected to contain 1 or more fields"
)
})
.name()
.is_some();
are_fields_named
} else {
false
};

let trait_fn_body = if module_variant_is_struct {
quote! {
if let &Self::Module { index, error } = self {
Some((index, error))
} else {
None
}
}
} else {
quote! {
if let Self::Module (module_error) = self {
Some((module_error.index, module_error.error))
} else {
None
}
// 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_indices(&self) -> Option<(u8,u8)> {
#trait_fn_body
fn module_error_data(&self) -> Option<::subxt::ModuleErrorData> {
#error_type
}
}
}
Expand Down
24 changes: 23 additions & 1 deletion subxt/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,28 @@ pub struct ModuleError {
pub error: String,
/// A description of the error.
pub description: Vec<String>,
/// A byte representation of the error.
pub error_data: ModuleErrorData,
}

/// The error details about a module error that has occurred.
///
/// **Note**: Structure used to obtain the underlying bytes of a ModuleError.
#[derive(Clone, Debug, thiserror::Error)]
#[error("Pallet index {pallet_index}: raw error: {error:?}")]
pub struct ModuleErrorData {
/// Index of the pallet that the error came from.
pub pallet_index: u8,
/// Raw error bytes.
pub error: [u8; 4],
}

impl ModuleErrorData {
/// Obtain the error index from the underlying byte data.
pub fn error_index(&self) -> u8 {
// Error index is utilized as the first byte from the error array.
self.error[0]
}
}

/// This trait is automatically implemented for the generated `DispatchError`,
Expand All @@ -190,5 +212,5 @@ pub struct ModuleError {
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)>;
fn module_error_data(&self) -> Option<ModuleErrorData>;
}
1 change: 1 addition & 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::{
Error,
GenericError,
HasModuleError,
ModuleErrorData,
RuntimeError,
TransactionError,
},
Expand Down
11 changes: 7 additions & 4 deletions subxt/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -389,14 +389,17 @@ impl<'client, T: Config, E: Decode + HasModuleError, Evs: Decode>
let ev = ev?;
if &ev.pallet == "System" && &ev.variant == "ExtrinsicFailed" {
let dispatch_error = E::decode(&mut &*ev.data)?;
if let Some((pallet_idx, error_idx)) =
dispatch_error.module_error_indices()
{
let details = self.client.metadata().error(pallet_idx, error_idx)?;
if let Some(error_data) = dispatch_error.module_error_data() {
// Error index is utilized as the first byte from the error array.
let details = self
.client
.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)))
Expand Down