codegen for root level error (#930)

* code gen for root error

* cargo fmt

* polkadot.rs regenerated

* use pallet name and decode with metadata

* remove pallet by name fn

* test that we can decode a ModuleError via as_root_error

* nits

---------

Co-authored-by: James Wilson <james@jsdw.me>
This commit is contained in:
Tadeo Hepperle
2023-05-02 17:33:09 +02:00
committed by GitHub
parent a3b3d262ed
commit 265f16fdec
6 changed files with 1572 additions and 6 deletions
+34
View File
@@ -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;
})
}
+52 -1
View File
@@ -6,6 +6,7 @@
mod calls;
mod constants;
mod errors;
mod events;
mod storage;
@@ -320,10 +321,13 @@ impl RuntimeGenerator {
should_gen_docs,
)?;
let errors = errors::generate_error_type_alias(&type_gen, pallet, should_gen_docs)?;
Ok(quote! {
pub mod #mod_name {
use super::root_mod;
use super::#types_mod_ident;
#errors
#calls
#event
#storage_mod
@@ -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 )*
}
};
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()
@@ -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
}
@@ -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
+11 -1
View File
@@ -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]
@@ -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();
@@ -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.
+13 -1
View File
@@ -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;
@@ -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>;
}
@@ -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;
File diff suppressed because it is too large Load Diff