Skip to content
34 changes: 34 additions & 0 deletions codegen/src/api/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright 2019-2023 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::v15::PalletMetadata;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use scale_info::form::PortableForm;

use crate::types::TypeGenerator;

use super::CodegenError;

/// Generate error type alias from the provided pallet metadata.
pub fn generate_error_type_alias(
type_gen: &TypeGenerator,
pallet: &PalletMetadata<PortableForm>,
should_gen_docs: bool,
) -> Result<TokenStream2, CodegenError> {
let Some(error) = &pallet.error else {
return Ok(quote!());
};

let error_type = type_gen.resolve_type_path(error.ty.id);
let error_ty = type_gen.resolve_type(error.ty.id);
let docs = &error_ty.docs;
let docs = should_gen_docs
.then_some(quote! { #( #[doc = #docs ] )* })
.unwrap_or_default();
Ok(quote! {
#docs
pub type Error = #error_type;
})
}
53 changes: 52 additions & 1 deletion codegen/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

mod calls;
mod constants;
mod errors;
mod events;
mod storage;

Expand Down Expand Up @@ -320,10 +321,13 @@ impl RuntimeGenerator {
should_gen_docs,
)?;

let errors = errors::generate_error_type_alias(&type_gen, pallet, should_gen_docs)?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: We could name this something like error_alias to be a bit more explicit, however you prefer here


Ok(quote! {
pub mod #mod_name {
use super::root_mod;
use super::#types_mod_ident;
#errors
#calls
#event
#storage_mod
Expand Down Expand Up @@ -371,6 +375,43 @@ impl RuntimeGenerator {
})
});

let outer_error_variants = self.metadata.pallets.iter().filter_map(|p| {
let variant_name = format_ident!("{}", p.name);
let mod_name = format_ident!("{}", p.name.to_string().to_snake_case());
let index = proc_macro2::Literal::u8_unsuffixed(p.index);

p.error.as_ref().map(|_| {
quote! {
#[codec(index = #index)]
#variant_name(#mod_name::Error),
}
})
});

let outer_error = quote! {
#default_derives
pub enum Error {
#( #outer_error_variants )*
}
};
Comment on lines +378 to +396
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving here my thoughts, not necessarily to be done for this PR:

This part of the code seems highly similar to the generation of the outer Event enum.
There might be a few things we could do here to reduce code duplicate, improve reliability and maintenance:

  • add a function generate_outer_enum
  • take in the name of the enum (either Event / Error or other names we might add in the future)
#[derive(ToTokens)]
enum OuterEnumName {
   Error,
   Event,
}


let root_error_if_arms = self.metadata.pallets.iter().filter_map(|p| {
let variant_name_str = &p.name;
let variant_name = format_ident!("{}", variant_name_str);
let mod_name = format_ident!("{}", variant_name_str.to_string().to_snake_case());
p.error.as_ref().map(|err|
{
let type_id = err.ty.id;
quote! {
if pallet_name == #variant_name_str {
let variant_error = #mod_name::Error::decode_with_metadata(cursor, #type_id, metadata)?;
return Ok(Error::#variant_name(variant_error));
}
}
}
)
});

let mod_ident = &item_mod_ir.ident;
let pallets_with_constants: Vec<_> = pallets_with_mod_names
.iter()
Expand Down Expand Up @@ -424,6 +465,16 @@ impl RuntimeGenerator {
}
}

#outer_error
impl #crate_path::error::RootError for Error {
fn root_error(pallet_bytes: &[u8], pallet_name: &str, metadata: &#crate_path::Metadata) -> Result<Self, #crate_path::Error> {
use #crate_path::metadata::DecodeWithMetadata;
let cursor = &mut &pallet_bytes[..];
#( #root_error_if_arms )*
Err(#crate_path::ext::scale_decode::Error::custom(format!("Pallet name '{}' not found in root Error enum", pallet_name)).into())
}
}

pub fn constants() -> ConstantsApi {
ConstantsApi
}
Expand Down Expand Up @@ -495,7 +546,7 @@ where
let ty = type_gen.resolve_type(type_id);

let scale_info::TypeDef::Variant(variant) = &ty.type_def else {
return Err(CodegenError::InvalidType(error_message_type_name.into()))
return Err(CodegenError::InvalidType(error_message_type_name.into()));
};

variant
Expand Down
12 changes: 11 additions & 1 deletion subxt/src/error/dispatch_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ use core::fmt::Debug;
use scale_decode::visitor::DecodeAsTypeResult;
use std::borrow::Cow;

use super::Error;
use crate::error::RootError;

/// An error dispatching a transaction.
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
#[non_exhaustive]
Expand Down Expand Up @@ -133,12 +136,13 @@ impl PartialEq for ModuleError {
self.raw == other.raw
}
}

impl Eq for ModuleError {}

impl std::fmt::Display for ModuleError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Ok(details) = self.details() else {
return f.write_str("Unknown pallet error (pallet and error details cannot be retrieved)")
return f.write_str("Unknown pallet error (pallet and error details cannot be retrieved)");
};

let pallet = details.pallet();
Expand All @@ -159,6 +163,12 @@ impl ModuleError {
pub fn raw(&self) -> RawModuleError {
self.raw
}

/// Attempts to decode the ModuleError into a value implementing the trait `RootError`
/// where the actual type of value is the generated top level enum `Error`.
pub fn as_root_error<E: RootError>(&self) -> Result<E, Error> {
E::root_error(&self.raw.error, self.details()?.pallet(), &self.metadata)
}
}

/// The error details about a module error that has occurred.
Expand Down
14 changes: 13 additions & 1 deletion subxt/src/error/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pub use dispatch_error::{
};

// Re-expose the errors we use from other crates here:
pub use crate::metadata::{InvalidMetadataError, MetadataError};
pub use crate::metadata::{InvalidMetadataError, Metadata, MetadataError};
pub use scale_decode::Error as DecodeError;
pub use scale_encode::Error as EncodeError;

Expand Down Expand Up @@ -162,3 +162,15 @@ pub enum StorageAddressError {
fields: usize,
},
}

/// This trait is implemented on the statically generated root ModuleError type
#[doc(hidden)]
pub trait RootError: Sized {
/// Given details of the pallet error we want to decode
fn root_error(
// typically a [u8; 4] encodes the error of a pallet
pallet_bytes: &[u8],
pallet_name: &str,
metadata: &Metadata,
) -> Result<Self, Error>;
}
37 changes: 37 additions & 0 deletions testing/integration-tests/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,43 @@ async fn submit_large_extrinsic() {
.unwrap();
}

#[tokio::test]
async fn decode_a_module_error() {
use node_runtime::runtime_types::pallet_assets::pallet as assets;

let ctx = test_context().await;
let api = ctx.client();

let alice = pair_signer(AccountKeyring::Alice.pair());
let alice_addr = alice.account_id().clone().into();

// Trying to work with an asset ID 1 which doesn't exist should return an
// "unknown" module error from the assets pallet.
let freeze_unknown_asset = node_runtime::tx().assets().freeze(1, alice_addr);

let err = api
.tx()
.sign_and_submit_then_watch_default(&freeze_unknown_asset, &alice)
.await
.unwrap()
.wait_for_finalized_success()
.await
.expect_err("an 'unknown asset' error");

let Error::Runtime(DispatchError::Module(module_err)) = err else {
panic!("Expected a ModuleError, got {err:?}");
};

// Decode the error into our generated Error type.
let decoded_err = module_err.as_root_error::<node_runtime::Error>().unwrap();

// Decoding should result in an Assets.Unknown error:
assert_eq!(
decoded_err,
node_runtime::Error::Assets(assets::Error::Unknown)
);
}

#[tokio::test]
async fn unsigned_extrinsic_is_same_shape_as_polkadotjs() {
let ctx = test_context().await;
Expand Down
Loading