Skip to content

Commit

Permalink
Handle sp_runtime::ModuleError substrate updates (#492)
Browse files Browse the repository at this point in the history
* codegen: Handle new errors of type [u8; 4] with backwards compatibility

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Add comments about the compatibility of `DispatchError::Module`

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Handle legacy cases appropriately

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Introduce `ModuleErrorType` for compatibility versioning

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Implement `module_error_type` helper

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Implement `quote::ToTokens` for `ModuleErrorType`

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Rename new error to ArrayError

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Add strict checks for ModuleError

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Fix cargo fmt

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen, subxt: Expose the error's raw bytes to user

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* subxt: Update polkadot with ModuleErrorRaw

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* subxt: Rename `ModuleErrorRaw` to `ModuleErrorData`

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* subxt: Expose method to obtain error index

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Rename `ModuleErrorRaw` to `ModuleErrorData`

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>
  • Loading branch information
lexnv committed Mar 31, 2022
1 parent 3d669f9 commit 9318f62
Show file tree
Hide file tree
Showing 5 changed files with 570 additions and 262 deletions.
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

0 comments on commit 9318f62

Please sign in to comment.