From 27a184db458692219d937e7d7e9d37f866fdc6f7 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Tue, 2 Aug 2022 20:03:52 +0200 Subject: [PATCH] feat: add EthError trait and derive (#1549) * feat: add EthError trait and derive * update changelog --- CHANGELOG.md | 1 + .../ethers-contract-abigen/src/contract.rs | 96 +++++++-- .../src/contract/common.rs | 97 ++++++++- .../src/contract/errors.rs | 169 ++++++++++++++++ .../src/contract/methods.rs | 91 ++------- .../src/contract/structs.rs | 5 + .../ethers-contract-abigen/src/lib.rs | 15 ++ .../ethers-contract-derive/src/call.rs | 191 +++--------------- .../ethers-contract-derive/src/calllike.rs | 174 ++++++++++++++++ .../ethers-contract-derive/src/error.rs | 116 +++++++++++ .../ethers-contract-derive/src/lib.rs | 39 ++++ .../ethers-contract-derive/src/utils.rs | 6 +- ethers-contract/src/error.rs | 20 ++ ethers-contract/src/lib.rs | 7 +- ethers-contract/tests/it/abigen.rs | 12 ++ ethers-contract/tests/it/common/derive.rs | 31 ++- ethers-core/src/abi/human_readable/lexer.rs | 51 ++++- ethers-core/src/abi/human_readable/mod.rs | 14 +- ethers-core/src/abi/mod.rs | 23 +++ 19 files changed, 892 insertions(+), 266 deletions(-) create mode 100644 ethers-contract/ethers-contract-abigen/src/contract/errors.rs create mode 100644 ethers-contract/ethers-contract-derive/src/calllike.rs create mode 100644 ethers-contract/ethers-contract-derive/src/error.rs create mode 100644 ethers-contract/src/error.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index bd3c3941..383fe5ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,6 +87,7 @@ ### Unreleased +- generate error bindings for custom errors [#1549](https://github.com/gakonst/ethers-rs/pull/1549) - Support overloaded events [#1233](https://github.com/gakonst/ethers-rs/pull/1233) - Relax Clone requirements when Arc is used diff --git a/ethers-contract/ethers-contract-abigen/src/contract.rs b/ethers-contract/ethers-contract-abigen/src/contract.rs index df31a3ec..7ce03b73 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract.rs @@ -1,22 +1,22 @@ #![deny(missing_docs)] mod common; +mod errors; mod events; mod methods; mod structs; mod types; use super::{util, Abigen}; -use crate::contract::structs::InternalStructs; +use crate::{ + contract::{methods::MethodAlias, structs::InternalStructs}, + rawabi::JsonAbi, +}; use ethers_core::{ - abi::{Abi, AbiParser}, + abi::{Abi, AbiParser, ErrorExt, EventExt}, macros::{ethers_contract_crate, ethers_core_crate, ethers_providers_crate}, + types::Bytes, }; use eyre::{eyre, Context as _, Result}; - -use crate::contract::methods::MethodAlias; - -use crate::rawabi::JsonAbi; -use ethers_core::{abi::EventExt, types::Bytes}; use proc_macro2::{Ident, Literal, TokenStream}; use quote::quote; use serde::Deserialize; @@ -34,6 +34,8 @@ pub struct ExpandedContract { pub contract: TokenStream, /// All event impls of the contract pub events: TokenStream, + /// All error impls of the contract + pub errors: TokenStream, /// All contract call struct related types pub call_structs: TokenStream, /// The contract's internal structs @@ -43,8 +45,15 @@ pub struct ExpandedContract { impl ExpandedContract { /// Merges everything into a single module pub fn into_tokens(self) -> TokenStream { - let ExpandedContract { module, imports, contract, events, call_structs, abi_structs } = - self; + let ExpandedContract { + module, + imports, + contract, + events, + call_structs, + abi_structs, + errors, + } = self; quote! { // export all the created data types pub use #module::*; @@ -53,6 +62,7 @@ impl ExpandedContract { pub mod #module { #imports #contract + #errors #events #call_structs #abi_structs @@ -87,6 +97,9 @@ pub struct Context { /// Manually specified method aliases. method_aliases: BTreeMap, + /// Manually specified method aliases. + error_aliases: BTreeMap, + /// Derives added to event structs and enums. event_derives: Vec, @@ -125,6 +138,9 @@ impl Context { // 6. Declare the structs parsed from the human readable abi let abi_structs_decl = self.abi_structs()?; + // 7. declare all error types + let errors_decl = self.errors()?; + let ethers_core = ethers_core_crate(); let ethers_contract = ethers_contract_crate(); let ethers_providers = ethers_providers_crate(); @@ -145,6 +161,7 @@ impl Context { #contract_methods #contract_events + } impl From<#ethers_contract::Contract> for #name { @@ -159,6 +176,7 @@ impl Context { imports, contract, events: events_decl, + errors: errors_decl, call_structs, abi_structs: abi_structs_decl, }) @@ -226,20 +244,30 @@ impl Context { event_aliases.insert(signature, alias); } - // also check for overloaded functions not covered by aliases, in which case we simply + // also check for overloaded events not covered by aliases, in which case we simply // numerate them for events in abi.events.values() { - let not_aliased = - events.iter().filter(|ev| !event_aliases.contains_key(&ev.abi_signature())); - if not_aliased.clone().count() > 1 { - let mut aliases = Vec::new(); - // overloaded events - for (idx, event) in not_aliased.enumerate() { - let event_name = format!("{}{}", event.name, idx + 1); - aliases.push((event.abi_signature(), events::event_struct_alias(&event_name))); - } - event_aliases.extend(aliases); - } + insert_alias_names( + &mut event_aliases, + events.iter().map(|e| (e.abi_signature(), e.name.as_str())), + events::event_struct_alias, + ); + } + + let mut error_aliases = BTreeMap::new(); + for (signature, alias) in args.error_aliases.into_iter() { + let alias = syn::parse_str(&alias)?; + error_aliases.insert(signature, alias); + } + + // also check for overloaded errors not covered by aliases, in which case we simply + // numerate them + for errors in abi.errors.values() { + insert_alias_names( + &mut error_aliases, + errors.iter().map(|e| (e.abi_signature(), e.name.as_str())), + errors::error_struct_alias, + ); } let event_derives = args @@ -259,6 +287,7 @@ impl Context { contract_name: args.contract_name, contract_bytecode, method_aliases, + error_aliases: Default::default(), event_derives, event_aliases, }) @@ -290,6 +319,31 @@ impl Context { } } +/// Solidity supports overloading as long as the signature of an event, error, function is unique, +/// which results in a mapping `(name -> Vec)` +/// +/// +/// This will populate the alias map for the value in the mapping (`Vec`) via `abi +/// signature -> name` using the given aliases and merge it with all names not yet aliased. +/// +/// If the iterator yields more than one element, this will simply numerate them +fn insert_alias_names<'a, I, F>(aliases: &mut BTreeMap, elements: I, get_ident: F) +where + I: IntoIterator, + F: Fn(&str) -> Ident, +{ + let not_aliased = + elements.into_iter().filter(|(sig, _name)| !aliases.contains_key(sig)).collect::>(); + if not_aliased.len() > 1 { + let mut overloaded_aliases = Vec::new(); + for (idx, (sig, name)) in not_aliased.into_iter().enumerate() { + let unique_name = format!("{}{}", name, idx + 1); + overloaded_aliases.push((sig, get_ident(&unique_name))); + } + aliases.extend(overloaded_aliases); + } +} + /// Parse the abi via `Source::parse` and return if the abi defined as human readable fn parse_abi(abi_str: &str) -> Result<(Abi, bool, AbiParser)> { let mut abi_parser = AbiParser::default(); diff --git a/ethers-contract/ethers-contract-abigen/src/contract/common.rs b/ethers-contract/ethers-contract-abigen/src/contract/common.rs index 0a7335c3..2235fe8d 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract/common.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract/common.rs @@ -1,9 +1,67 @@ use super::{util, Context}; -use proc_macro2::TokenStream; +use crate::contract::types; +use ethers_core::{ + abi::{Param, ParamType}, + macros::{ethers_contract_crate, ethers_core_crate, ethers_providers_crate}, +}; +use proc_macro2::{Ident, TokenStream}; use quote::quote; -use ethers_core::macros::{ethers_contract_crate, ethers_core_crate, ethers_providers_crate}; +/// Expands to the `name : type` pairs for the params +pub(crate) fn expand_params<'a, F>( + params: &[Param], + resolve_tuple: F, +) -> eyre::Result> +where + F: Fn(&str) -> Option<&'a str>, +{ + params + .iter() + .enumerate() + .map(|(idx, param)| { + let name = util::expand_input_name(idx, ¶m.name); + let ty = expand_param_type(param, ¶m.kind, |s| resolve_tuple(s))?; + Ok((name, ty)) + }) + .collect() +} + +/// returns the Tokenstream for the corresponding rust type +pub(crate) fn expand_param_type<'a, F>( + param: &Param, + kind: &ParamType, + resolve_tuple: F, +) -> eyre::Result +where + F: Fn(&str) -> Option<&'a str>, +{ + match kind { + ParamType::Array(ty) => { + let ty = expand_param_type(param, ty, resolve_tuple)?; + Ok(quote! { + ::std::vec::Vec<#ty> + }) + } + ParamType::FixedArray(ty, size) => { + let ty = expand_param_type(param, ty, resolve_tuple)?; + let size = *size; + Ok(quote! {[#ty; #size]}) + } + ParamType::Tuple(_) => { + let ty = if let Some(rust_struct_name) = + param.internal_type.as_ref().and_then(|s| resolve_tuple(s.as_str())) + { + let ident = util::ident(rust_struct_name); + quote! {#ident} + } else { + types::expand(kind)? + }; + Ok(ty) + } + _ => types::expand(kind), + } +} pub(crate) fn imports(name: &str) -> TokenStream { let doc = util::expand_doc(&format!("{} was auto-generated with ethers-rs Abigen. More information at: https://github.com/gakonst/ethers-rs", name)); @@ -95,3 +153,38 @@ pub(crate) fn struct_declaration(cx: &Context) -> TokenStream { } } } + +/// Expands to the tuple struct definition +pub(crate) fn expand_data_tuple( + name: &Ident, + params: &[(TokenStream, TokenStream)], +) -> TokenStream { + let fields = params + .iter() + .map(|(_, ty)| { + quote! { + pub #ty } + }) + .collect::>(); + + if fields.is_empty() { + quote! { struct #name; } + } else { + quote! { struct #name( #( #fields ),* ); } + } +} + +/// Expands to a struct definition with named fields +pub(crate) fn expand_data_struct( + name: &Ident, + params: &[(TokenStream, TokenStream)], +) -> TokenStream { + let fields = params + .iter() + .map(|(name, ty)| { + quote! { pub #name: #ty } + }) + .collect::>(); + + quote! { struct #name { #( #fields, )* } } +} diff --git a/ethers-contract/ethers-contract-abigen/src/contract/errors.rs b/ethers-contract/ethers-contract-abigen/src/contract/errors.rs new file mode 100644 index 00000000..fa3384d2 --- /dev/null +++ b/ethers-contract/ethers-contract-abigen/src/contract/errors.rs @@ -0,0 +1,169 @@ +//! derive error bindings + +use super::{util, Context}; +use crate::contract::common::{expand_data_struct, expand_data_tuple, expand_params}; +use ethers_core::{ + abi::{ethabi::AbiError, ErrorExt}, + macros::{ethers_contract_crate, ethers_core_crate}, +}; +use eyre::Result; +use inflector::Inflector; +use proc_macro2::{Ident, TokenStream}; +use quote::quote; + +impl Context { + /// Returns all error declarations + pub(crate) fn errors(&self) -> Result { + let data_types = self + .abi + .errors + .values() + .flatten() + .map(|event| self.expand_error(event)) + .collect::>>()?; + + // only expand an enum when multiple errors are present + let errors_enum_decl = if self.abi.errors.values().flatten().count() > 1 { + self.expand_errors_enum() + } else { + quote! {} + }; + + Ok(quote! { + // HERE + #( #data_types )* + + #errors_enum_decl + + // HERE end + }) + } + + /// Expands an ABI error into a single error data type. This can expand either + /// into a structure or a tuple in the case where all error parameters are anonymous. + fn expand_error(&self, error: &AbiError) -> Result { + let sig = self.error_aliases.get(&error.abi_signature()).cloned(); + let abi_signature = error.abi_signature(); + + let error_name = error_struct_name(&error.name, sig); + + let fields = self.expand_error_params(error)?; + + // expand as a tuple if all fields are anonymous + let all_anonymous_fields = error.inputs.iter().all(|input| input.name.is_empty()); + let data_type_definition = if all_anonymous_fields { + // expand to a tuple struct + expand_data_tuple(&error_name, &fields) + } else { + // expand to a struct + expand_data_struct(&error_name, &fields) + }; + + let doc = format!( + "Custom Error type `{}` with signature `{}` and selector `{:?}`", + error.name, + abi_signature, + error.selector() + ); + let abi_signature_doc = util::expand_doc(&doc); + let ethers_contract = ethers_contract_crate(); + // use the same derives as for events + let derives = util::expand_derives(&self.event_derives); + + let error_name = &error.name; + + Ok(quote! { + #abi_signature_doc + #[derive(Clone, Debug, Default, Eq, PartialEq, #ethers_contract::EthError, #ethers_contract::EthDisplay, #derives)] + #[etherror( name = #error_name, abi = #abi_signature )] + pub #data_type_definition + }) + } + + /// Expands to the `name : type` pairs of the function's outputs + fn expand_error_params(&self, error: &AbiError) -> Result> { + expand_params(&error.inputs, |s| self.internal_structs.get_struct_type(s)) + } + + /// The name ident of the errors enum + fn expand_error_enum_name(&self) -> Ident { + util::ident(&format!("{}Errors", self.contract_ident)) + } + + /// Generate an enum with a variant for each event + fn expand_errors_enum(&self) -> TokenStream { + let variants = self + .abi + .errors + .values() + .flatten() + .map(|err| { + error_struct_name(&err.name, self.error_aliases.get(&err.abi_signature()).cloned()) + }) + .collect::>(); + + let ethers_core = ethers_core_crate(); + let ethers_contract = ethers_contract_crate(); + + // use the same derives as for events + let derives = util::expand_derives(&self.event_derives); + let enum_name = self.expand_error_enum_name(); + + quote! { + #[derive(Debug, Clone, PartialEq, Eq, #ethers_contract::EthAbiType, #derives)] + pub enum #enum_name { + #(#variants(#variants)),* + } + + impl #ethers_core::abi::AbiDecode for #enum_name { + fn decode(data: impl AsRef<[u8]>) -> ::std::result::Result { + #( + if let Ok(decoded) = <#variants as #ethers_core::abi::AbiDecode>::decode(data.as_ref()) { + return Ok(#enum_name::#variants(decoded)) + } + )* + Err(#ethers_core::abi::Error::InvalidData.into()) + } + } + + impl #ethers_core::abi::AbiEncode for #enum_name { + fn encode(self) -> Vec { + match self { + #( + #enum_name::#variants(element) => element.encode() + ),* + } + } + } + + impl ::std::fmt::Display for #enum_name { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match self { + #( + #enum_name::#variants(element) => element.fmt(f) + ),* + } + } + } + + #( + impl ::std::convert::From<#variants> for #enum_name { + fn from(var: #variants) -> Self { + #enum_name::#variants(var) + } + } + )* + + } + } +} + +/// Expands an ABI error into an identifier for its event data type. +fn error_struct_name(error_name: &str, alias: Option) -> Ident { + alias.unwrap_or_else(|| util::ident(error_name)) +} + +/// Returns the alias name for an error +pub(crate) fn error_struct_alias(name: &str) -> Ident { + util::ident(&name.to_pascal_case()) +} diff --git a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs index 11b1641a..314d03df 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs @@ -1,18 +1,19 @@ use std::collections::{btree_map::Entry, BTreeMap, HashMap}; -use eyre::{Context as _, Result}; -use inflector::Inflector; -use proc_macro2::{Literal, TokenStream}; -use quote::quote; -use syn::Ident; - +use super::{types, util, Context}; +use crate::contract::common::{ + expand_data_struct, expand_data_tuple, expand_param_type, expand_params, +}; use ethers_core::{ abi::{Function, FunctionExt, Param, ParamType}, macros::{ethers_contract_crate, ethers_core_crate}, types::Selector, }; - -use super::{types, util, Context}; +use eyre::{Context as _, Result}; +use inflector::Inflector; +use proc_macro2::{Literal, TokenStream}; +use quote::quote; +use syn::Ident; /// The maximum amount of overloaded functions that are attempted to auto aliased with their param /// name. If there is a function that with `NAME_ALIASING_OVERLOADED_FUNCTIONS_CAP` overloads then @@ -142,7 +143,7 @@ impl Context { } /// Expands to the corresponding struct type based on the inputs of the given function - fn expand_return_struct( + pub fn expand_return_struct( &self, function: &Function, alias: Option<&MethodAlias>, @@ -296,15 +297,9 @@ impl Context { /// Expands to the `name : type` pairs of the function's outputs fn expand_output_params(&self, fun: &Function) -> Result> { - fun.outputs - .iter() - .enumerate() - .map(|(idx, param)| { - let name = util::expand_input_name(idx, ¶m.name); - let ty = self.expand_output_param_type(fun, param, ¶m.kind)?; - Ok((name, ty)) - }) - .collect() + expand_params(&fun.outputs, |s| { + self.internal_structs.get_function_output_struct_type(&fun.name, s) + }) } /// Expands to the return type of a function @@ -388,39 +383,16 @@ impl Context { } } - /// returns the Tokenstream for the corresponding rust type of the output param + /// returns the TokenStream for the corresponding rust type of the output param fn expand_output_param_type( &self, fun: &Function, param: &Param, kind: &ParamType, ) -> Result { - match kind { - ParamType::Array(ty) => { - let ty = self.expand_output_param_type(fun, param, ty)?; - Ok(quote! { - ::std::vec::Vec<#ty> - }) - } - ParamType::FixedArray(ty, size) => { - let ty = self.expand_output_param_type(fun, param, ty)?; - let size = *size; - Ok(quote! {[#ty; #size]}) - } - ParamType::Tuple(_) => { - let ty = if let Some(rust_struct_name) = - param.internal_type.as_ref().and_then(|s| { - self.internal_structs.get_function_output_struct_type(&fun.name, s) - }) { - let ident = util::ident(rust_struct_name); - quote! {#ident} - } else { - types::expand(kind)? - }; - Ok(ty) - } - _ => types::expand(kind), - } + expand_param_type(param, kind, |s| { + self.internal_structs.get_function_output_struct_type(&fun.name, s) + }) } /// Expands a single function with the given alias @@ -717,35 +689,6 @@ fn expand_call_struct_variant_name(function: &Function, alias: Option<&MethodAli } } -/// Expands to the tuple struct definition -fn expand_data_tuple(name: &Ident, params: &[(TokenStream, TokenStream)]) -> TokenStream { - let fields = params - .iter() - .map(|(_, ty)| { - quote! { - pub #ty } - }) - .collect::>(); - - if fields.is_empty() { - quote! { struct #name; } - } else { - quote! { struct #name( #( #fields ),* ); } - } -} - -/// Expands to the struct definition of a call struct -fn expand_data_struct(name: &Ident, params: &[(TokenStream, TokenStream)]) -> TokenStream { - let fields = params - .iter() - .map(|(name, ty)| { - quote! { pub #name: #ty } - }) - .collect::>(); - - quote! { struct #name { #( #fields, )* } } -} - #[cfg(test)] mod tests { use ethers_core::abi::ParamType; diff --git a/ethers-contract/ethers-contract-abigen/src/contract/structs.rs b/ethers-contract/ethers-contract-abigen/src/contract/structs.rs index 947604b9..fc82f34f 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract/structs.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract/structs.rs @@ -334,6 +334,11 @@ impl InternalStructs { .map(String::as_str) } + /// Returns the name of the rust type for the type + pub fn get_struct_type(&self, internal_type: &str) -> Option<&str> { + self.rust_type_names.get(struct_type_identifier(internal_type)).map(String::as_str) + } + /// Returns the mapping table of abi `internal type identifier -> rust type` pub fn rust_type_names(&self) -> &HashMap { &self.rust_type_names diff --git a/ethers-contract/ethers-contract-abigen/src/lib.rs b/ethers-contract/ethers-contract-abigen/src/lib.rs index a538df71..b9482b52 100644 --- a/ethers-contract/ethers-contract-abigen/src/lib.rs +++ b/ethers-contract/ethers-contract-abigen/src/lib.rs @@ -72,6 +72,9 @@ pub struct Abigen { /// Manually specified event name aliases. event_aliases: HashMap, + + /// Manually specified error name aliases. + error_aliases: HashMap, } impl Abigen { @@ -85,6 +88,7 @@ impl Abigen { event_derives: Vec::new(), event_aliases: HashMap::new(), rustfmt: true, + error_aliases: Default::default(), }) } @@ -126,6 +130,17 @@ impl Abigen { self } + /// Manually adds a solidity error alias to specify what the error struct will be in Rust. + #[must_use] + pub fn add_error_alias(mut self, signature: S1, alias: S2) -> Self + where + S1: Into, + S2: Into, + { + self.error_aliases.insert(signature.into(), alias.into()); + self + } + /// Specify whether or not to format the code using a locally installed copy /// of `rustfmt`. /// diff --git a/ethers-contract/ethers-contract-derive/src/call.rs b/ethers-contract/ethers-contract-derive/src/call.rs index f86163ed..cf8c244e 100644 --- a/ethers-contract/ethers-contract-derive/src/call.rs +++ b/ethers-contract/ethers-contract-derive/src/call.rs @@ -1,17 +1,17 @@ //! Helper functions for deriving `EthCall` -use crate::{abi_ty, utils}; +use crate::{calllike::*, utils, utils::ident}; use ethers_core::{ - abi::{Function, FunctionExt, HumanReadableParser}, + abi::{FunctionExt, HumanReadableParser}, macros::{ethers_contract_crate, ethers_core_crate}, }; -use proc_macro2::{Span, TokenStream}; +use proc_macro2::TokenStream; use quote::quote; -use syn::{parse::Error, spanned::Spanned as _, AttrStyle, DeriveInput, Lit, Meta, NestedMeta}; +use syn::{parse::Error, DeriveInput}; /// Generates the `ethcall` trait support pub(crate) fn derive_eth_call_impl(input: DeriveInput) -> TokenStream { - let attributes = match parse_call_attributes(&input) { + let attributes = match parse_calllike_attributes(&input, "ethcall") { Ok(attributes) => attributes, Err(errors) => return errors, }; @@ -48,7 +48,7 @@ pub(crate) fn derive_eth_call_impl(input: DeriveInput) -> TokenStream { function.name = function_call_name.clone(); let abi = function.abi_signature(); let selector = utils::selector(function.selector()); - let decode_impl = derive_decode_impl_from_function(&function); + let decode_impl = derive_decode_impl_from_params(&function.inputs, ident("EthCall")); derive_trait_impls( &input, @@ -59,6 +59,25 @@ pub(crate) fn derive_eth_call_impl(input: DeriveInput) -> TokenStream { ) } +/// Use the `AbiType` trait to determine the correct `ParamType` and signature at runtime +fn derive_trait_impls_with_abi_type( + input: &DeriveInput, + function_call_name: &str, + abi_signature: Option<&str>, +) -> Result { + let abi_signature = if let Some(abi) = abi_signature { + quote! {#abi} + } else { + utils::derive_abi_signature_with_abi_type(input, function_call_name, "EthCall")? + }; + + let abi_signature = quote! { + #abi_signature.into() + }; + let decode_impl = derive_decode_impl_with_abi_type(input, ident("EthCall"))?; + Ok(derive_trait_impls(input, function_call_name, abi_signature, None, decode_impl)) +} + /// Generates the EthCall implementation pub fn derive_trait_impls( input: &DeriveInput, @@ -93,167 +112,11 @@ pub fn derive_trait_impls( #abi_signature } } - - impl #core_crate::abi::AbiDecode for #struct_name { - fn decode(bytes: impl AsRef<[u8]>) -> ::std::result::Result { - #decode_impl - } - } - - impl #core_crate::abi::AbiEncode for #struct_name { - fn encode(self) -> ::std::vec::Vec { - let tokens = #core_crate::abi::Tokenize::into_tokens(self); - let selector = ::selector(); - let encoded = #core_crate::abi::encode(&tokens); - selector - .iter() - .copied() - .chain(encoded.into_iter()) - .collect() - } - } - }; - let tokenize_impl = abi_ty::derive_tokenizeable_impl(input); + let codec_impl = derive_codec_impls(input, decode_impl, ident("EthCall")); quote! { - #tokenize_impl #ethcall_impl + #codec_impl } } - -/// Generates the decode implementation based on the function's input types -fn derive_decode_impl_from_function(function: &Function) -> TokenStream { - let datatypes = function.inputs.iter().map(|input| utils::param_type_quote(&input.kind)); - let datatypes_array = quote! {[#( #datatypes ),*]}; - derive_decode_impl(datatypes_array) -} - -/// Generates the decode implementation based on the function's runtime `AbiType` impl -fn derive_decode_impl_with_abi_type(input: &DeriveInput) -> Result { - let datatypes_array = utils::derive_abi_parameters_array(input, "EthCall")?; - Ok(derive_decode_impl(datatypes_array)) -} - -fn derive_decode_impl(datatypes_array: TokenStream) -> TokenStream { - let core_crate = ethers_core_crate(); - let contract_crate = ethers_contract_crate(); - let data_types_init = quote! {let data_types = #datatypes_array;}; - - quote! { - let bytes = bytes.as_ref(); - if bytes.len() < 4 || bytes[..4] != ::selector() { - return Err(#contract_crate::AbiError::WrongSelector); - } - #data_types_init - let data_tokens = #core_crate::abi::decode(&data_types, &bytes[4..])?; - Ok(::from_token( #core_crate::abi::Token::Tuple(data_tokens))?) - } -} - -/// Use the `AbiType` trait to determine the correct `ParamType` and signature at runtime -fn derive_trait_impls_with_abi_type( - input: &DeriveInput, - function_call_name: &str, - abi_signature: Option<&str>, -) -> Result { - let abi_signature = if let Some(abi) = abi_signature { - quote! {#abi} - } else { - utils::derive_abi_signature_with_abi_type(input, function_call_name, "EthCall")? - }; - - let abi_signature = quote! { - #abi_signature.into() - }; - let decode_impl = derive_decode_impl_with_abi_type(input)?; - Ok(derive_trait_impls(input, function_call_name, abi_signature, None, decode_impl)) -} - -/// All the attributes the `EthCall` macro supports -#[derive(Default)] -struct EthCallAttributes { - name: Option<(String, Span)>, - abi: Option<(String, Span)>, -} - -/// extracts the attributes from the struct annotated with `EthCall` -fn parse_call_attributes(input: &DeriveInput) -> Result { - let mut result = EthCallAttributes::default(); - for a in input.attrs.iter() { - if let AttrStyle::Outer = a.style { - if let Ok(Meta::List(meta)) = a.parse_meta() { - if meta.path.is_ident("ethcall") { - for n in meta.nested.iter() { - if let NestedMeta::Meta(meta) = n { - match meta { - Meta::Path(path) => { - return Err(Error::new( - path.span(), - "unrecognized ethcall parameter", - ) - .to_compile_error()) - } - Meta::List(meta) => { - return Err(Error::new( - meta.path.span(), - "unrecognized ethcall parameter", - ) - .to_compile_error()) - } - Meta::NameValue(meta) => { - if meta.path.is_ident("name") { - if let Lit::Str(ref lit_str) = meta.lit { - if result.name.is_none() { - result.name = - Some((lit_str.value(), lit_str.span())); - } else { - return Err(Error::new( - meta.span(), - "name already specified", - ) - .to_compile_error()) - } - } else { - return Err(Error::new( - meta.span(), - "name must be a string", - ) - .to_compile_error()) - } - } else if meta.path.is_ident("abi") { - if let Lit::Str(ref lit_str) = meta.lit { - if result.abi.is_none() { - result.abi = - Some((lit_str.value(), lit_str.span())); - } else { - return Err(Error::new( - meta.span(), - "abi already specified", - ) - .to_compile_error()) - } - } else { - return Err(Error::new( - meta.span(), - "abi must be a string", - ) - .to_compile_error()) - } - } else { - return Err(Error::new( - meta.span(), - "unrecognized ethcall parameter", - ) - .to_compile_error()) - } - } - } - } - } - } - } - } - } - Ok(result) -} diff --git a/ethers-contract/ethers-contract-derive/src/calllike.rs b/ethers-contract/ethers-contract-derive/src/calllike.rs new file mode 100644 index 00000000..21743758 --- /dev/null +++ b/ethers-contract/ethers-contract-derive/src/calllike.rs @@ -0,0 +1,174 @@ +//! Code used by both `call` and `error` + +use crate::{abi_ty, utils}; +use ethers_core::{ + abi::Param, + macros::{ethers_contract_crate, ethers_core_crate}, +}; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::quote; +use syn::{parse::Error, spanned::Spanned as _, AttrStyle, DeriveInput, Lit, Meta, NestedMeta}; + +/// All the attributes the `EthCall`/`EthError` macro supports +#[derive(Default)] +pub struct EthCalllikeAttributes { + pub name: Option<(String, Span)>, + pub abi: Option<(String, Span)>, +} + +/// extracts the attributes from the struct annotated with the given attribute +pub fn parse_calllike_attributes( + input: &DeriveInput, + attr_name: &str, +) -> Result { + let mut result = EthCalllikeAttributes::default(); + for a in input.attrs.iter() { + if let AttrStyle::Outer = a.style { + if let Ok(Meta::List(meta)) = a.parse_meta() { + if meta.path.is_ident(attr_name) { + for n in meta.nested.iter() { + if let NestedMeta::Meta(meta) = n { + match meta { + Meta::Path(path) => { + return Err(Error::new( + path.span(), + format!("unrecognized {} parameter", attr_name), + ) + .to_compile_error()) + } + Meta::List(meta) => { + return Err(Error::new( + meta.path.span(), + format!("unrecognized {} parameter", attr_name), + ) + .to_compile_error()) + } + Meta::NameValue(meta) => { + if meta.path.is_ident("name") { + if let Lit::Str(ref lit_str) = meta.lit { + if result.name.is_none() { + result.name = + Some((lit_str.value(), lit_str.span())); + } else { + return Err(Error::new( + meta.span(), + "name already specified", + ) + .to_compile_error()) + } + } else { + return Err(Error::new( + meta.span(), + "name must be a string", + ) + .to_compile_error()) + } + } else if meta.path.is_ident("abi") { + if let Lit::Str(ref lit_str) = meta.lit { + if result.abi.is_none() { + result.abi = + Some((lit_str.value(), lit_str.span())); + } else { + return Err(Error::new( + meta.span(), + "abi already specified", + ) + .to_compile_error()) + } + } else { + return Err(Error::new( + meta.span(), + "abi must be a string", + ) + .to_compile_error()) + } + } else { + return Err(Error::new( + meta.span(), + format!("unrecognized {} parameter", attr_name), + ) + .to_compile_error()) + } + } + } + } + } + } + } + } + } + Ok(result) +} + +/// Generates the decode implementation based on the type's runtime `AbiType` impl +pub fn derive_decode_impl_with_abi_type( + input: &DeriveInput, + trait_ident: Ident, +) -> Result { + let datatypes_array = utils::derive_abi_parameters_array(input, &trait_ident.to_string())?; + Ok(derive_decode_impl(datatypes_array, trait_ident)) +} + +/// Generates the decode implementation based on the params +pub fn derive_decode_impl_from_params(params: &[Param], trait_ident: Ident) -> TokenStream { + let datatypes = params.iter().map(|input| utils::param_type_quote(&input.kind)); + let datatypes_array = quote! {[#( #datatypes ),*]}; + derive_decode_impl(datatypes_array, trait_ident) +} + +pub fn derive_decode_impl(datatypes_array: TokenStream, trait_ident: Ident) -> TokenStream { + let core_crate = ethers_core_crate(); + let contract_crate = ethers_contract_crate(); + let data_types_init = quote! {let data_types = #datatypes_array;}; + + quote! { + let bytes = bytes.as_ref(); + if bytes.len() < 4 || bytes[..4] != ::selector() { + return Err(#contract_crate::AbiError::WrongSelector); + } + #data_types_init + let data_tokens = #core_crate::abi::decode(&data_types, &bytes[4..])?; + Ok(::from_token( #core_crate::abi::Token::Tuple(data_tokens))?) + } +} + +/// Generates the Codec implementation +pub fn derive_codec_impls( + input: &DeriveInput, + decode_impl: TokenStream, + trait_ident: Ident, +) -> TokenStream { + // the ethers crates to use + let core_crate = ethers_core_crate(); + let contract_crate = ethers_contract_crate(); + let struct_name = &input.ident; + + let codec_impl = quote! { + + impl #core_crate::abi::AbiDecode for #struct_name { + fn decode(bytes: impl AsRef<[u8]>) -> ::std::result::Result { + #decode_impl + } + } + + impl #core_crate::abi::AbiEncode for #struct_name { + fn encode(self) -> ::std::vec::Vec { + let tokens = #core_crate::abi::Tokenize::into_tokens(self); + let selector = ::selector(); + let encoded = #core_crate::abi::encode(&tokens); + selector + .iter() + .copied() + .chain(encoded.into_iter()) + .collect() + } + } + + }; + let tokenize_impl = abi_ty::derive_tokenizeable_impl(input); + + quote! { + #tokenize_impl + #codec_impl + } +} diff --git a/ethers-contract/ethers-contract-derive/src/error.rs b/ethers-contract/ethers-contract-derive/src/error.rs new file mode 100644 index 00000000..c0914dc1 --- /dev/null +++ b/ethers-contract/ethers-contract-derive/src/error.rs @@ -0,0 +1,116 @@ +//! Helper functions for deriving `EthError` + +use crate::{calllike::*, utils, utils::ident}; +use ethers_core::{ + abi::{ErrorExt, HumanReadableParser}, + macros::{ethers_contract_crate, ethers_core_crate}, +}; +use proc_macro2::TokenStream; +use quote::quote; +use syn::{parse::Error, DeriveInput}; + +/// Generates the `EthError` trait support +pub(crate) fn derive_eth_error_impl(input: DeriveInput) -> TokenStream { + let attributes = match parse_calllike_attributes(&input, "etherror") { + Ok(attributes) => attributes, + Err(errors) => return errors, + }; + + let error_name = attributes.name.map(|(s, _)| s).unwrap_or_else(|| input.ident.to_string()); + + let mut error = if let Some((src, span)) = attributes.abi { + let raw_function_sig = src.trim_start_matches("error ").trim_start(); + // try to parse as solidity error + if let Ok(fun) = HumanReadableParser::parse_error(&src) { + fun + } else { + // try to determine the abi by using its fields at runtime + return match derive_trait_impls_with_abi_type( + &input, + &error_name, + Some(raw_function_sig), + ) { + Ok(derived) => derived, + Err(err) => { + Error::new(span, format!("Unable to determine ABI for `{}` : {}", src, err)) + .to_compile_error() + } + } + } + } else { + // try to determine the abi by using its fields at runtime + return match derive_trait_impls_with_abi_type(&input, &error_name, None) { + Ok(derived) => derived, + Err(err) => err.to_compile_error(), + } + }; + error.name = error_name.clone(); + let abi = error.abi_signature(); + let selector = utils::selector(error.selector()); + let decode_impl = derive_decode_impl_from_params(&error.inputs, ident("EthError")); + + derive_trait_impls(&input, &error_name, quote! {#abi.into()}, Some(selector), decode_impl) +} + +/// Use the `AbiType` trait to determine the correct `ParamType` and signature at runtime +fn derive_trait_impls_with_abi_type( + input: &DeriveInput, + function_call_name: &str, + abi_signature: Option<&str>, +) -> Result { + let abi_signature = if let Some(abi) = abi_signature { + quote! {#abi} + } else { + utils::derive_abi_signature_with_abi_type(input, function_call_name, "EthError")? + }; + + let abi_signature = quote! { + #abi_signature.into() + }; + let decode_impl = derive_decode_impl_with_abi_type(input, ident("EthError"))?; + Ok(derive_trait_impls(input, function_call_name, abi_signature, None, decode_impl)) +} + +/// Generates the EthError implementation +pub fn derive_trait_impls( + input: &DeriveInput, + function_call_name: &str, + abi_signature: TokenStream, + selector: Option, + decode_impl: TokenStream, +) -> TokenStream { + // the ethers crates to use + let core_crate = ethers_core_crate(); + let contract_crate = ethers_contract_crate(); + let struct_name = &input.ident; + + let selector = selector.unwrap_or_else(|| { + quote! { + #core_crate::utils::id(Self::abi_signature()) + } + }); + + let etherror_impl = quote! { + impl #contract_crate::EthError for #struct_name { + + fn error_name() -> ::std::borrow::Cow<'static, str> { + #function_call_name.into() + } + + fn selector() -> #core_crate::types::Selector { + #selector + } + + fn abi_signature() -> ::std::borrow::Cow<'static, str> { + #abi_signature + } + } + + }; + let codec_impl = derive_codec_impls(input, decode_impl, ident("EthError")); + + quote! { + #etherror_impl + #codec_impl + } +} diff --git a/ethers-contract/ethers-contract-derive/src/lib.rs b/ethers-contract/ethers-contract-derive/src/lib.rs index b505d19e..81f3c773 100644 --- a/ethers-contract/ethers-contract-derive/src/lib.rs +++ b/ethers-contract/ethers-contract-derive/src/lib.rs @@ -11,8 +11,10 @@ use abigen::Contracts; pub(crate) mod abi_ty; mod abigen; mod call; +pub(crate) mod calllike; mod codec; mod display; +mod error; mod event; mod spanned; pub(crate) mod utils; @@ -281,3 +283,40 @@ pub fn derive_abi_call(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); TokenStream::from(call::derive_eth_call_impl(input)) } + +/// Derives the `EthError` and `Tokenizeable` trait for the labeled type. +/// +/// Additional arguments can be specified using the `#[etherror(...)]` +/// attribute: +/// +/// For the struct: +/// +/// - `name`, `name = "..."`: Overrides the generated `EthCall` function name, default is the +/// struct's name. +/// - `abi`, `abi = "..."`: The ABI signature for the function this call's data corresponds to. +/// +/// NOTE: in order to successfully parse the `abi` (`(,...)`) the ` +/// must match either the struct name or the name attribute: `#[ethcall(name =""]` +/// +/// # Example +/// +/// ```ignore +/// use ethers_contract::EthError; +/// +/// #[derive(Debug, Clone, EthError)] +/// #[etherror(name ="my_error")] +/// struct MyError { +/// addr: Address, +/// old_value: String, +/// new_value: String, +/// } +/// assert_eq!( +/// MyCall::abi_signature().as_ref(), +/// "my_error(address,string,string)" +/// ); +/// ``` +#[proc_macro_derive(EthError, attributes(etherror))] +pub fn derive_abi_error(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + TokenStream::from(error::derive_eth_error_impl(input)) +} diff --git a/ethers-contract/ethers-contract-derive/src/utils.rs b/ethers-contract/ethers-contract-derive/src/utils.rs index 65319f06..b8691552 100644 --- a/ethers-contract/ethers-contract-derive/src/utils.rs +++ b/ethers-contract/ethers-contract-derive/src/utils.rs @@ -1,11 +1,15 @@ use ethers_core::{abi::ParamType, macros::ethers_core_crate, types::Selector}; -use proc_macro2::Literal; +use proc_macro2::{Ident, Literal, Span}; use quote::{quote, quote_spanned}; use syn::{ parse::Error, spanned::Spanned as _, Data, DeriveInput, Expr, Fields, GenericArgument, Lit, PathArguments, Type, }; +pub fn ident(name: &str) -> Ident { + Ident::new(name, Span::call_site()) +} + pub fn signature(hash: &[u8]) -> proc_macro2::TokenStream { let core_crate = ethers_core_crate(); let bytes = hash.iter().copied().map(Literal::u8_unsuffixed); diff --git a/ethers-contract/src/error.rs b/ethers-contract/src/error.rs new file mode 100644 index 00000000..cfac0651 --- /dev/null +++ b/ethers-contract/src/error.rs @@ -0,0 +1,20 @@ +use ethers_core::{ + abi::{AbiDecode, AbiEncode, Tokenizable}, + types::Selector, + utils::id, +}; +use std::borrow::Cow; + +/// A helper trait for types that represents a custom error type +pub trait EthError: Tokenizable + AbiDecode + AbiEncode + Send + Sync { + /// The name of the error + fn error_name() -> Cow<'static, str>; + + /// Retrieves the ABI signature for the error + fn abi_signature() -> Cow<'static, str>; + + /// The selector of the error + fn selector() -> Selector { + id(Self::abi_signature()) + } +} diff --git a/ethers-contract/src/lib.rs b/ethers-contract/src/lib.rs index 25bcf8d5..6b8ba377 100644 --- a/ethers-contract/src/lib.rs +++ b/ethers-contract/src/lib.rs @@ -11,6 +11,9 @@ pub use base::{decode_function_data, encode_function_data, AbiError, BaseContrac mod call; pub use call::{ContractError, EthCall}; +mod error; +pub use error::EthError; + mod factory; pub use factory::{ContractDeployer, ContractFactory}; @@ -42,7 +45,9 @@ pub use ethers_contract_abigen::{Abigen, MultiAbigen}; #[cfg(any(test, feature = "abigen"))] #[cfg_attr(docsrs, doc(cfg(feature = "abigen")))] -pub use ethers_contract_derive::{abigen, EthAbiCodec, EthAbiType, EthCall, EthDisplay, EthEvent}; +pub use ethers_contract_derive::{ + abigen, EthAbiCodec, EthAbiType, EthCall, EthDisplay, EthError, EthEvent, +}; // Hide the Lazy re-export, it's just for convenience #[doc(hidden)] diff --git a/ethers-contract/tests/it/abigen.rs b/ethers-contract/tests/it/abigen.rs index 9ca7e9a9..5dd25efc 100644 --- a/ethers-contract/tests/it/abigen.rs +++ b/ethers-contract/tests/it/abigen.rs @@ -629,6 +629,18 @@ fn can_gen_seaport() { "fulfillAdvancedOrder(((address,address,(uint8,address,uint256,uint256,uint256)[],(uint8,address,uint256,uint256,uint256,address)[],uint8,uint256,uint256,bytes32,uint256,bytes32,uint256),uint120,uint120,bytes,bytes),(uint256,uint8,uint256,uint256,bytes32[])[],bytes32,address)" ); assert_eq!(hex::encode(FulfillAdvancedOrderCall::selector()), "e7acab24"); + + assert_codec::(); + let err = SeaportErrors::BadContractSignature(BadContractSignature::default()); + + let encoded = err.clone().encode(); + assert_eq!(err, SeaportErrors::decode(encoded).unwrap()); + + let err = SeaportErrors::ConsiderationNotMet(ConsiderationNotMet { + order_index: U256::zero(), + consideration_index: U256::zero(), + shortfall_amount: U256::zero(), + }); } #[test] diff --git a/ethers-contract/tests/it/common/derive.rs b/ethers-contract/tests/it/common/derive.rs index 3198cc9e..6aad8e1b 100644 --- a/ethers-contract/tests/it/common/derive.rs +++ b/ethers-contract/tests/it/common/derive.rs @@ -1,5 +1,5 @@ use ethers_contract::{ - abigen, EthAbiCodec, EthAbiType, EthCall, EthDisplay, EthEvent, EthLogDecode, + abigen, EthAbiCodec, EthAbiType, EthCall, EthDisplay, EthError, EthEvent, EthLogDecode, }; use ethers_core::{ abi::{AbiDecode, AbiEncode, RawLog, Tokenizable}, @@ -8,6 +8,7 @@ use ethers_core::{ fn assert_tokenizeable() {} fn assert_ethcall() {} +fn assert_etherror() {} #[derive(Debug, Clone, PartialEq, Eq, EthAbiType)] struct ValueChanged { @@ -618,3 +619,31 @@ fn can_use_result_name() { let _call = ResultCall { result: Result { result: U256::zero() } }; } + +#[test] +fn can_derive_etherror() { + #[derive(Debug, PartialEq, Eq, EthError)] + #[etherror(name = "MyError", abi = "MyError(address,address,string)")] + struct MyError { + old_author: Address, + addr: Address, + new_value: String, + } + + assert_eq!(MyError::abi_signature().as_ref(), "MyError(address,address,string)"); + + assert_tokenizeable::(); + assert_etherror::(); +} + +#[test] +fn can_use_human_readable_error() { + abigen!( + ErrContract, + r#"[ + error MyError(address,address,string) + ]"#, + ); + + assert_etherror::(); +} diff --git a/ethers-core/src/abi/human_readable/lexer.rs b/ethers-core/src/abi/human_readable/lexer.rs index 3c9c6f24..4c7fea58 100644 --- a/ethers-core/src/abi/human_readable/lexer.rs +++ b/ethers-core/src/abi/human_readable/lexer.rs @@ -1,4 +1,6 @@ -use ethabi::{Constructor, Event, EventParam, Function, Param, ParamType, StateMutability}; +use ethabi::{ + AbiError, Constructor, Event, EventParam, Function, Param, ParamType, StateMutability, +}; use std::{fmt, iter::Peekable, str::CharIndices}; use unicode_xid::UnicodeXID; @@ -320,6 +322,18 @@ impl<'input> HumanReadableParser<'input> { Self::new(input).take_function() } + /// Parses a [Function] from a human readable form + /// + /// # Example + /// + /// ``` + /// use ethers_core::abi::HumanReadableParser; + /// let err = HumanReadableParser::parse_error("error MyError(address author, string oldValue, string newValue)").unwrap(); + /// ``` + pub fn parse_error(input: &'input str) -> Result { + Self::new(input).take_error() + } + /// Parses a [Constructor] from a human readable form /// /// # Example @@ -344,6 +358,15 @@ impl<'input> HumanReadableParser<'input> { Self::new(input).take_event() } + /// Returns the next `Error` and consumes the underlying tokens + pub fn take_error(&mut self) -> Result { + let name = self.take_identifier(Token::Error)?; + self.take_open_parenthesis()?; + let inputs = self.take_function_params()?; + self.take_close_parenthesis()?; + Ok(AbiError { name: name.to_string(), inputs }) + } + /// Returns the next `Constructor` and consumes the underlying tokens pub fn take_constructor(&mut self) -> Result { self.take_next_exact(Token::Constructor)?; @@ -352,6 +375,7 @@ impl<'input> HumanReadableParser<'input> { self.take_close_parenthesis()?; Ok(Constructor { inputs }) } + /// Returns the next `Function` and consumes the underlying tokens pub fn take_function(&mut self) -> Result { let name = self.take_identifier(Token::Function)?; @@ -833,6 +857,31 @@ pub enum DataLocation { mod tests { use super::*; + #[test] + fn parse_error() { + let f = AbiError { + name: "MyError".to_string(), + inputs: vec![ + Param { name: "author".to_string(), kind: ParamType::Address, internal_type: None }, + Param { + name: "oldValue".to_string(), + kind: ParamType::String, + internal_type: None, + }, + Param { + name: "newValue".to_string(), + kind: ParamType::String, + internal_type: None, + }, + ], + }; + let parsed = HumanReadableParser::parse_error( + "error MyError(address author, string oldValue, string newValue)", + ) + .unwrap(); + assert_eq!(f, parsed); + } + #[test] fn parse_constructor() { let f = Constructor { diff --git a/ethers-core/src/abi/human_readable/mod.rs b/ethers-core/src/abi/human_readable/mod.rs index 763798d7..ca832e8f 100644 --- a/ethers-core/src/abi/human_readable/mod.rs +++ b/ethers-core/src/abi/human_readable/mod.rs @@ -1,3 +1,4 @@ +use ethabi::AbiError; use std::collections::{BTreeMap, HashMap, VecDeque}; use crate::abi::{ @@ -82,6 +83,17 @@ impl AbiParser { if line.starts_with("event") { let event = self.parse_event(line)?; abi.events.entry(event.name.clone()).or_default().push(event); + } else if let Some(err) = line.strip_prefix("error") { + // an error is essentially a function without outputs, so we parse as function here + let function = match self.parse_function(err) { + Ok(function) => function, + Err(_) => bail!("Illegal abi `{}`, expected error", line), + }; + if !function.outputs.is_empty() { + bail!("Illegal abi `{}`, expected error", line); + } + let error = AbiError { name: function.name, inputs: function.inputs }; + abi.errors.entry(error.name.clone()).or_default().push(error); } else if line.starts_with("constructor") { let inputs = self .constructor_inputs(line)? @@ -103,7 +115,7 @@ impl AbiParser { // function may have shorthand declaration, so it won't start with "function" let function = match self.parse_function(line) { Ok(function) => function, - Err(_) => bail!("Illegal abi `{}`", line), + Err(_) => bail!("Illegal abi `{}`, expected function", line), }; abi.functions.entry(function.name.clone()).or_default().push(function); } diff --git a/ethers-core/src/abi/mod.rs b/ethers-core/src/abi/mod.rs index 88c57f88..fdb6eb08 100644 --- a/ethers-core/src/abi/mod.rs +++ b/ethers-core/src/abi/mod.rs @@ -70,6 +70,29 @@ impl EventExt for Event { } } +/// Extension trait for `ethabi::AbiError`. +pub trait ErrorExt { + /// Compute the method signature in the standard ABI format. + fn abi_signature(&self) -> String; + + /// Compute the Keccak256 error selector used by contract ABIs. + fn selector(&self) -> Selector; +} + +impl ErrorExt for ethabi::AbiError { + fn abi_signature(&self) -> String { + if self.inputs.is_empty() { + return format!("{}()", self.name) + } + let inputs = self.inputs.iter().map(|p| p.kind.to_string()).collect::>().join(","); + format!("{}({})", self.name, inputs) + } + + fn selector(&self) -> Selector { + id(self.abi_signature()) + } +} + /// A trait for types that can be represented in the ethereum ABI. pub trait AbiType { /// The native ABI type this type represents.