From fb4d9a9ef1cc9df340023d61a6885693a6a6fcf5 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Mon, 18 Oct 2021 12:28:38 +0200 Subject: [PATCH] feat: function call enums EthCall macro and more (#517) * fix: do not sort event variants * style: use deref over clone * style: refactor some stuff * feat: add ethcall trait * feat: include in abigen * feat: add bare bones eth call derive * feat: impl EthCall derive * feat: support enums * feat: use abigen enum derive * fix: concrete abi and map errors * test: first call test * rustfmt * chore: use correct trait name on error * feat: derive display for call structs * feat: add from conversion * test: add convert test * chore: docs and test * chore: update changelog * cargo fix * feat: add unit type derive support and more test * chore: patch ethabi * chore: rm ethabi patch * feat: add encode/decode trait impls * style: use AsRef<[u8]> * Update ethers-contract/ethers-contract-abigen/src/contract/methods.rs Co-authored-by: Georgios Konstantopoulos * style: reindent macro body * test: add tuple event test Co-authored-by: Georgios Konstantopoulos --- CHANGELOG.md | 1 + .../ethers-contract-abigen/src/contract.rs | 9 +- .../src/contract/events.rs | 68 ++--- .../src/contract/methods.rs | 219 +++++++++++++-- .../src/contract/structs.rs | 3 +- .../ethers-contract-abigen/src/util.rs | 4 + .../ethers-contract-derive/src/abi_ty.rs | 102 ++++++- .../ethers-contract-derive/src/call.rs | 254 ++++++++++++++++++ .../ethers-contract-derive/src/event.rs | 41 +-- .../ethers-contract-derive/src/lib.rs | 74 ++++- .../ethers-contract-derive/src/utils.rs | 56 +++- ethers-contract/src/call.rs | 33 +++ ethers-contract/src/codec.rs | 14 + ethers-contract/src/lib.rs | 7 +- ethers-contract/tests/abigen.rs | 75 +++++- ethers-contract/tests/common/derive.rs | 100 ++++++- ethers-core/src/abi/human_readable.rs | 18 +- ethers-core/src/types/i256.rs | 4 +- ethers-core/src/types/transaction/eip712.rs | 2 +- 19 files changed, 950 insertions(+), 134 deletions(-) create mode 100644 ethers-contract/ethers-contract-derive/src/call.rs create mode 100644 ethers-contract/src/codec.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 696e6d38..fdf0b046 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Unreleased +- add `EthCall` trait and derive macro which generates matching structs for contract calls [#517](https://github.com/gakonst/ethers-rs/pull/517) - `abigen!` now generates `Display` for all events using the new `EthDisplay` macro [#513](https://github.com/gakonst/ethers-rs/pull/513) - `abigen!` now supports overloaded functions natively [#501](https://github.com/gakonst/ethers-rs/pull/501) - `abigen!` now supports multiple contracts [#498](https://github.com/gakonst/ethers-rs/pull/498) diff --git a/ethers-contract/ethers-contract-abigen/src/contract.rs b/ethers-contract/ethers-contract-abigen/src/contract.rs index b226d2f6..8f70ba19 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract.rs @@ -29,6 +29,8 @@ pub struct ExpandedContract { pub contract: TokenStream, /// All event impls of the contract pub events: TokenStream, + /// All contract call struct related types + pub call_structs: TokenStream, /// The contract's internal structs pub abi_structs: TokenStream, } @@ -41,6 +43,7 @@ impl ExpandedContract { imports, contract, events, + call_structs, abi_structs, } = self; quote! { @@ -52,6 +55,7 @@ impl ExpandedContract { #imports #contract #events + #call_structs #abi_structs } } @@ -111,8 +115,8 @@ impl Context { // 3. impl block for the event functions let contract_events = self.event_methods()?; - // 4. impl block for the contract methods - let contract_methods = self.methods()?; + // 4. impl block for the contract methods and their corresponding types + let (contract_methods, call_structs) = self.methods_and_call_structs()?; // 5. Declare the structs parsed from the human readable abi let abi_structs_decl = self.abi_structs()?; @@ -146,6 +150,7 @@ impl Context { imports, contract, events: events_decl, + call_structs, abi_structs: abi_structs_decl, }) } diff --git a/ethers-contract/ethers-contract-abigen/src/contract/events.rs b/ethers-contract/ethers-contract-abigen/src/contract/events.rs index 06492e74..317df833 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract/events.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract/events.rs @@ -5,7 +5,6 @@ use inflector::Inflector; use proc_macro2::{Ident, Literal, TokenStream}; use quote::quote; use std::collections::BTreeMap; -use syn::Path; impl Context { /// Expands each event to a struct + its impl Detokenize block @@ -33,9 +32,10 @@ impl Context { /// Generate the event filter methods for the contract pub fn event_methods(&self) -> Result { - let sorted_events: BTreeMap<_, _> = self.abi.events.clone().into_iter().collect(); + let sorted_events: BTreeMap<_, _> = self.abi.events.iter().collect(); let filter_methods = sorted_events .values() + .map(std::ops::Deref::deref) .flatten() .map(|event| self.expand_filter(event)) .collect::>(); @@ -51,9 +51,9 @@ impl Context { /// Generate an enum with a variant for each event fn expand_events_enum(&self) -> TokenStream { - let sorted_events: BTreeMap<_, _> = self.abi.events.clone().into_iter().collect(); - - let variants = sorted_events + let variants = self + .abi + .events .values() .flatten() .map(|e| expand_struct_name(e, self.event_aliases.get(&e.abi_signature()).cloned())) @@ -65,33 +65,11 @@ impl Context { let ethers_contract = util::ethers_contract_crate(); quote! { - #[derive(Debug, Clone, PartialEq, Eq)] + #[derive(Debug, Clone, PartialEq, Eq, #ethers_contract::EthAbiType)] pub enum #enum_name { #(#variants(#variants)),* } - impl #ethers_core::abi::Tokenizable for #enum_name { - - fn from_token(token: #ethers_core::abi::Token) -> Result where - Self: Sized { - #( - if let Ok(decoded) = #variants::from_token(token.clone()) { - return Ok(#enum_name::#variants(decoded)) - } - )* - Err(#ethers_core::abi::InvalidOutputType("Failed to decode all event variants".to_string())) - } - - fn into_token(self) -> #ethers_core::abi::Token { - match self { - #( - #enum_name::#variants(element) => element.into_token() - ),* - } - } - } - impl #ethers_core::abi::TokenizableItem for #enum_name { } - impl #ethers_contract::EthLogDecode for #enum_name { fn decode_log(log: &#ethers_core::abi::RawLog) -> Result where @@ -153,8 +131,8 @@ impl Context { /// Expands an event property type. /// - /// Note that this is slightly different than an expanding a Solidity type as - /// complex types like arrays and strings get emited as hashes when they are + /// Note that this is slightly different from expanding a Solidity type as + /// complex types like arrays and strings get emitted as hashes when they are /// indexed. /// If a complex types matches with a struct previously parsed by the AbiParser, /// we can replace it @@ -213,8 +191,8 @@ impl Context { }) } - /// Expands an ABI event into name-type pairs for each of its parameters. - fn expand_params(&self, event: &Event) -> Result> { + /// Expands the name-type pairs for the given inputs + fn expand_event_params(&self, event: &Event) -> Result> { event .inputs .iter() @@ -264,7 +242,7 @@ impl Context { let event_name = expand_struct_name(event, sig); - let params = self.expand_params(event)?; + let params = self.expand_event_params(event)?; // expand as a tuple if all fields are anonymous let all_anonymous_fields = event.inputs.iter().all(|input| input.name.is_empty()); let data_type_definition = if all_anonymous_fields { @@ -273,7 +251,7 @@ impl Context { expand_data_struct(&event_name, ¶ms) }; - let derives = expand_derives(&self.event_derives); + let derives = util::expand_derives(&self.event_derives); let ethers_contract = util::ethers_contract_crate(); @@ -323,16 +301,20 @@ fn expand_data_struct(name: &Ident, params: &[(TokenStream, TokenStream, bool)]) fn expand_data_tuple(name: &Ident, params: &[(TokenStream, TokenStream, bool)]) -> TokenStream { let fields = params .iter() - .map(|(_, ty, _)| quote! { pub #ty }) + .map(|(_, ty, indexed)| { + if *indexed { + quote! { + #[ethevent(indexed)] pub #ty } + } else { + quote! { + pub #ty } + } + }) .collect::>(); quote! { struct #name( #( #fields ),* ); } } -fn expand_derives(derives: &[Path]) -> TokenStream { - quote! {#(#derives),*} -} - #[cfg(test)] mod tests { use super::*; @@ -450,7 +432,7 @@ mod tests { }; let cx = test_context(); - let params = cx.expand_params(&event).unwrap(); + let params = cx.expand_event_params(&event).unwrap(); let name = expand_struct_name(&event, None); let definition = expand_data_struct(&name, ¶ms); @@ -482,7 +464,7 @@ mod tests { }; let cx = test_context_with_alias("Foo(bool,address)", "FooAliased"); - let params = cx.expand_params(&event).unwrap(); + let params = cx.expand_event_params(&event).unwrap(); let alias = Some(util::ident("FooAliased")); let name = expand_struct_name(&event, alias); let definition = expand_data_struct(&name, ¶ms); @@ -515,7 +497,7 @@ mod tests { }; let cx = test_context(); - let params = cx.expand_params(&event).unwrap(); + let params = cx.expand_event_params(&event).unwrap(); let name = expand_struct_name(&event, None); let definition = expand_data_tuple(&name, ¶ms); @@ -544,7 +526,7 @@ mod tests { }; let cx = test_context_with_alias("Foo(bool,address)", "FooAliased"); - let params = cx.expand_params(&event).unwrap(); + let params = cx.expand_event_params(&event).unwrap(); let alias = Some(util::ident("FooAliased")); let name = expand_struct_name(&event, alias); let definition = expand_data_tuple(&name, ¶ms); diff --git a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs index 4dceb244..f747ba87 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs @@ -18,32 +18,162 @@ use super::{types, util, Context}; /// to the Solidity contract methods. impl Context { /// Expands all method implementations - pub(crate) fn methods(&self) -> Result { - let mut aliases = self.get_method_aliases()?; - let sorted_functions: BTreeMap<_, _> = self.abi.functions.clone().into_iter().collect(); + pub(crate) fn methods_and_call_structs(&self) -> Result<(TokenStream, TokenStream)> { + let aliases = self.get_method_aliases()?; + let sorted_functions: BTreeMap<_, _> = self.abi.functions.iter().collect(); let functions = sorted_functions .values() + .map(std::ops::Deref::deref) .flatten() .map(|function| { let signature = function.abi_signature(); - self.expand_function(function, aliases.remove(&signature)) + self.expand_function(function, aliases.get(&signature).cloned()) .with_context(|| format!("error expanding function '{}'", signature)) }) .collect::>>()?; - Ok(quote! { #( #functions )* }) + let function_impls = quote! { #( #functions )* }; + let call_structs = self.expand_call_structs(aliases)?; + + Ok((function_impls, call_structs)) } - fn expand_inputs_call_arg_with_structs( + /// Expands to the corresponding struct type based on the inputs of the given function + fn expand_call_struct( &self, - fun: &Function, - ) -> Result<(TokenStream, TokenStream)> { + function: &Function, + alias: Option<&Ident>, + ) -> Result { + let call_name = expand_call_struct_name(function, alias); + let fields = self.expand_input_pairs(function)?; + // expand as a tuple if all fields are anonymous + let all_anonymous_fields = function.inputs.iter().all(|input| input.name.is_empty()); + let call_type_definition = if all_anonymous_fields { + // expand to a tuple struct + expand_data_tuple(&call_name, &fields) + } else { + // expand to a struct + expand_data_struct(&call_name, &fields) + }; + let function_name = &function.name; + let abi_signature = function.abi_signature(); + let doc = format!( + "Container type for all input parameters for the `{}`function with signature `{}` and selector `{:?}`", + function.name, + abi_signature, + function.selector() + ); + let abi_signature_doc = util::expand_doc(&doc); + let ethers_contract = util::ethers_contract_crate(); + // use the same derives as for events + let derives = util::expand_derives(&self.event_derives); + + Ok(quote! { + #abi_signature_doc + #[derive(Clone, Debug, Default, Eq, PartialEq, #ethers_contract::EthCall, #ethers_contract::EthDisplay, #derives)] + #[ethcall( name = #function_name, abi = #abi_signature )] + pub #call_type_definition + }) + } + + /// Expands all structs + fn expand_call_structs(&self, aliases: BTreeMap) -> Result { + let mut struct_defs = Vec::new(); + let mut struct_names = Vec::new(); + let mut variant_names = Vec::new(); + + for function in self.abi.functions.values().flatten() { + let signature = function.abi_signature(); + let alias = aliases.get(&signature); + struct_defs.push(self.expand_call_struct(function, alias)?); + struct_names.push(expand_call_struct_name(function, alias)); + variant_names.push(expand_call_struct_variant_name(function, alias)); + } + + let struct_def_tokens = quote! { + #(#struct_defs)* + }; + + if struct_defs.len() <= 1 { + // no need for an enum + return Ok(struct_def_tokens); + } + + let ethers_core = util::ethers_core_crate(); + let ethers_contract = util::ethers_contract_crate(); + + let enum_name = self.expand_calls_enum_name(); + Ok(quote! { + #struct_def_tokens + + #[derive(Debug, Clone, PartialEq, Eq, #ethers_contract::EthAbiType)] + pub enum #enum_name { + #(#variant_names(#struct_names)),* + } + + impl #ethers_contract::AbiDecode for #enum_name { + fn decode(data: impl AsRef<[u8]>) -> Result { + #( + if let Ok(decoded) = <#struct_names as #ethers_contract::AbiDecode>::decode(data.as_ref()) { + return Ok(#enum_name::#variant_names(decoded)) + } + )* + Err(#ethers_core::abi::Error::InvalidData.into()) + } + } + + impl #ethers_contract::AbiEncode for #enum_name { + fn encode(self) -> Result<#ethers_core::types::Bytes, #ethers_contract::AbiError> { + match self { + #( + #enum_name::#variant_names(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::#variant_names(element) => element.fmt(f) + ),* + } + } + } + + #( + impl ::std::convert::From<#struct_names> for #enum_name { + fn from(var: #struct_names) -> Self { + #enum_name::#variant_names(var) + } + } + )* + + }) + } + + /// The name ident of the calls enum + fn expand_calls_enum_name(&self) -> Ident { + util::ident(&format!("{}Calls", self.contract_name.to_string())) + } + + /// Expands to the `name : type` pairs of the function's inputs + fn expand_input_pairs(&self, fun: &Function) -> Result> { let mut args = Vec::with_capacity(fun.inputs.len()); - let mut call_args = Vec::with_capacity(fun.inputs.len()); - for (i, param) in fun.inputs.iter().enumerate() { - let name = util::expand_input_name(i, ¶m.name); + for (idx, param) in fun.inputs.iter().enumerate() { + let name = util::expand_input_name(idx, ¶m.name); let ty = self.expand_input_param(fun, ¶m.name, ¶m.kind)?; - args.push(quote! { #name: #ty }); + args.push((name, ty)); + } + Ok(args) + } + + /// Expands the arguments for the call that eventually calls the contract + fn expand_contract_call_args(&self, fun: &Function) -> Result { + let mut call_args = Vec::with_capacity(fun.inputs.len()); + for (idx, param) in fun.inputs.iter().enumerate() { + let name = util::expand_input_name(idx, ¶m.name); let call_arg = match param.kind { // this is awkward edge case where the function inputs are a single struct // we need to force this argument into a tuple so it gets expanded to `((#name,))` @@ -59,14 +189,13 @@ impl Context { }; call_args.push(call_arg); } - let args = quote! { #( , #args )* }; let call_args = match call_args.len() { 0 => quote! { () }, 1 => quote! { #( #call_args )* }, _ => quote! { ( #(#call_args, )* ) }, }; - Ok((args, call_args)) + Ok(call_args) } fn expand_input_param( @@ -111,13 +240,16 @@ impl Context { // TODO use structs let outputs = expand_fn_outputs(&function.outputs)?; - let _ethers_core = util::ethers_core_crate(); - let _ethers_providers = util::ethers_providers_crate(); let ethers_contract = util::ethers_contract_crate(); let result = quote! { #ethers_contract::builders::ContractCall }; - let (input, arg) = self.expand_inputs_call_arg_with_structs(function)?; + let contract_args = self.expand_contract_call_args(function)?; + let function_params = self + .expand_input_pairs(function)? + .into_iter() + .map(|(name, ty)| quote! { #name: #ty }); + let function_params = quote! { #( , #function_params )* }; let doc = util::expand_doc(&format!( "Calls the contract's `{}` (0x{}) function", @@ -127,8 +259,8 @@ impl Context { Ok(quote! { #doc - pub fn #name(&self #input) -> #result { - self.0.method_hash(#selector, #arg) + pub fn #name(&self #function_params) -> #result { + self.0.method_hash(#selector, #contract_args) .expect("method not found (this should never happen)") } }) @@ -227,6 +359,55 @@ fn expand_selector(selector: Selector) -> TokenStream { quote! { [#( #bytes ),*] } } +/// Expands to the name of the call struct +fn expand_call_struct_name(function: &Function, alias: Option<&Ident>) -> Ident { + let name = if let Some(id) = alias { + format!("{}Call", id.to_string().to_pascal_case()) + } else { + format!("{}Call", function.name.to_pascal_case()) + }; + util::ident(&name) +} + +/// Expands to the name of the call struct +fn expand_call_struct_variant_name(function: &Function, alias: Option<&Ident>) -> Ident { + let name = if let Some(id) = alias { + id.to_string().to_pascal_case() + } else { + function.name.to_pascal_case() + }; + util::ident(&name) +} + +/// 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 c48974ce..56902fef 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract/structs.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract/structs.rs @@ -127,8 +127,7 @@ impl Context { let name = util::ident(name); // use the same derives as for events - let derives = &self.event_derives; - let derives = quote! {#(#derives),*}; + let derives = util::expand_derives(&self.event_derives); let ethers_contract = util::ethers_contract_crate(); Ok(quote! { diff --git a/ethers-contract/ethers-contract-abigen/src/util.rs b/ethers-contract/ethers-contract-abigen/src/util.rs index 87946c41..e1687d4b 100644 --- a/ethers-contract/ethers-contract-abigen/src/util.rs +++ b/ethers-contract/ethers-contract-abigen/src/util.rs @@ -117,6 +117,10 @@ pub fn expand_doc(s: &str) -> TokenStream { } } +pub fn expand_derives(derives: &[Path]) -> TokenStream { + quote! {#(#derives),*} +} + /// Parses the given address string pub fn parse_address(address_str: S) -> Result
where diff --git a/ethers-contract/ethers-contract-derive/src/abi_ty.rs b/ethers-contract/ethers-contract-derive/src/abi_ty.rs index 69818dad..06e38257 100644 --- a/ethers-contract/ethers-contract-derive/src/abi_ty.rs +++ b/ethers-contract/ethers-contract-derive/src/abi_ty.rs @@ -1,9 +1,10 @@ //! Helper functions for deriving `EthAbiType` use ethers_contract_abigen::ethers_core_crate; +use proc_macro2::{Ident, TokenStream}; use quote::{quote, quote_spanned}; use syn::spanned::Spanned as _; -use syn::{parse::Error, Data, DeriveInput, Fields}; +use syn::{parse::Error, Data, DeriveInput, Fields, Variant}; /// Generates the tokenize implementation pub fn derive_tokenizeable_impl(input: &DeriveInput) -> proc_macro2::TokenStream { @@ -80,17 +81,13 @@ pub fn derive_tokenizeable_impl(input: &DeriveInput) -> proc_macro2::TokenStream into_token_impl, ) } - Fields::Unit => { - return Error::new( - input.span(), - "EthAbiType cannot be derived for empty structs and unit", - ) - .to_compile_error(); - } + Fields::Unit => return tokenize_unit_type(&input.ident), }, - Data::Enum(_) => { - return Error::new(input.span(), "EthAbiType cannot be derived for enums") - .to_compile_error(); + Data::Enum(ref data) => { + return match tokenize_enum(name, data.variants.iter()) { + Ok(tokens) => tokens, + Err(err) => err.to_compile_error(), + } } Data::Union(_) => { return Error::new(input.span(), "EthAbiType cannot be derived for unions") @@ -169,3 +166,86 @@ pub fn derive_tokenizeable_impl(input: &DeriveInput) -> proc_macro2::TokenStream { } } } + +fn tokenize_unit_type(name: &Ident) -> TokenStream { + let ethers_core = ethers_core_crate(); + quote! { + impl #ethers_core::abi::Tokenizable for #name { + fn from_token(token: #ethers_core::abi::Token) -> Result where + Self: Sized { + if let #ethers_core::abi::Token::Tuple(tokens) = token { + if !tokens.is_empty() { + Err(#ethers_core::abi::InvalidOutputType(::std::format!( + "Expected empty tuple, got {:?}", + tokens + ))) + } else { + Ok(#name{}) + } + } else { + Err(#ethers_core::abi::InvalidOutputType(::std::format!( + "Expected Tuple, got {:?}", + token + ))) + } + } + + fn into_token(self) -> #ethers_core::abi::Token { + #ethers_core::abi::Token::Tuple(::std::vec::Vec::new()) + } + } + impl #ethers_core::abi::TokenizableItem for #name { } + } +} + +fn tokenize_enum<'a>( + enum_name: &Ident, + variants: impl Iterator + 'a, +) -> Result { + let ethers_core = ethers_core_crate(); + + let mut into_tokens = TokenStream::new(); + let mut from_tokens = TokenStream::new(); + for variant in variants { + if variant.fields.len() > 1 { + return Err(Error::new( + variant.span(), + "EthAbiType cannot be derived for enum variants with multiple fields", + )); + } + let var_ident = &variant.ident; + if let Some(field) = variant.fields.iter().next() { + let ty = &field.ty; + from_tokens.extend(quote! { + if let Ok(decoded) = #ty::from_token(token.clone()) { + return Ok(#enum_name::#var_ident(decoded)) + } + }); + into_tokens.extend(quote! { + #enum_name::#var_ident(element) => element.into_token(), + }); + } else { + into_tokens.extend(quote! { + #enum_name::#var_ident(element) => # ethers_core::abi::Token::Tuple(::std::vec::Vec::new()), + }); + } + } + + Ok(quote! { + impl #ethers_core::abi::Tokenizable for #enum_name { + + fn from_token(token: #ethers_core::abi::Token) -> Result where + Self: Sized { + #from_tokens + Err(#ethers_core::abi::InvalidOutputType("Failed to decode all type variants".to_string())) + } + + fn into_token(self) -> #ethers_core::abi::Token { + match self { + #into_tokens + } + } + } + impl #ethers_core::abi::TokenizableItem for #enum_name { } + }) +} diff --git a/ethers-contract/ethers-contract-derive/src/call.rs b/ethers-contract/ethers-contract-derive/src/call.rs new file mode 100644 index 00000000..d41a080a --- /dev/null +++ b/ethers-contract/ethers-contract-derive/src/call.rs @@ -0,0 +1,254 @@ +//! Helper functions for deriving `EthCall` + +use ethers_contract_abigen::{ethers_contract_crate, ethers_core_crate}; +use proc_macro2::{Span, TokenStream}; +use quote::quote; +use syn::spanned::Spanned as _; +use syn::{parse::Error, AttrStyle, DeriveInput, Lit, Meta, NestedMeta}; + +use ethers_core::abi::{param_type::Reader, AbiParser, Function, FunctionExt, Param, ParamType}; + +use crate::abi_ty; +use crate::utils; + +/// Generates the `ethcall` trait support +pub(crate) fn derive_eth_call_impl(input: DeriveInput) -> TokenStream { + // the ethers crates to use + let core_crate = ethers_core_crate(); + let contract_crate = ethers_contract_crate(); + + let name = &input.ident; + let attributes = match parse_call_attributes(&input) { + Ok(attributes) => attributes, + Err(errors) => return errors, + }; + + let function_call_name = attributes + .name + .map(|(s, _)| s) + .unwrap_or_else(|| input.ident.to_string()); + + let mut function = if let Some((src, span)) = attributes.abi { + // try to parse as solidity function + if let Ok(fun) = parse_function(&src) { + fun + } else { + // try as tuple + if let Some(inputs) = Reader::read( + src.trim_start_matches("function ") + .trim_start() + .trim_start_matches(&function_call_name), + ) + .ok() + .and_then(|param| match param { + ParamType::Tuple(params) => Some( + params + .into_iter() + .map(|kind| Param { + name: "".to_string(), + kind, + internal_type: None, + }) + .collect(), + ), + _ => None, + }) { + #[allow(deprecated)] + Function { + name: function_call_name.clone(), + inputs, + outputs: vec![], + constant: false, + state_mutability: Default::default(), + } + } else { + return Error::new(span, format!("Unable to determine ABI: {}", src)) + .to_compile_error(); + } + } + } else { + // // try to determine the abi from the fields + match derive_abi_function_from_fields(&input) { + Ok(event) => event, + Err(err) => return err.to_compile_error(), + } + }; + + function.name = function_call_name.clone(); + + let abi = function.abi_signature(); + let selector = utils::selector(function.selector()); + + let decode_impl = derive_decode_impl(&function); + + let ethcall_impl = quote! { + impl #contract_crate::EthCall for #name { + + fn function_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.into() + } + + } + + impl #contract_crate::AbiDecode for #name { + fn decode(bytes: impl AsRef<[u8]>) -> Result { + #decode_impl + } + } + }; + let tokenize_impl = abi_ty::derive_tokenizeable_impl(&input); + + quote! { + #tokenize_impl + #ethcall_impl + } +} + +fn derive_decode_impl(function: &Function) -> TokenStream { + let core_crate = ethers_core_crate(); + let contract_crate = ethers_contract_crate(); + let data_types = function + .inputs + .iter() + .map(|input| utils::param_type_quote(&input.kind)); + + let data_types_init = quote! {let data_types = [#( #data_types ),*];}; + + 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))?) + } +} + +/// Determine the function's ABI by parsing the AST +fn derive_abi_function_from_fields(input: &DeriveInput) -> Result { + #[allow(deprecated)] + let function = Function { + name: "".to_string(), + inputs: utils::derive_abi_inputs_from_fields(input, "EthCall")? + .into_iter() + .map(|(name, kind)| Param { + name, + kind, + internal_type: None, + }) + .collect(), + outputs: vec![], + constant: false, + state_mutability: Default::default(), + }; + Ok(function) +} + +/// 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) +} + +fn parse_function(abi: &str) -> Result { + let abi = if !abi.trim_start().starts_with("function ") { + format!("function {}", abi) + } else { + abi.to_string() + }; + + AbiParser::default() + .parse_function(&abi) + .map_err(|err| format!("Failed to parse the function ABI: {:?}", err)) +} diff --git a/ethers-contract/ethers-contract-derive/src/event.rs b/ethers-contract/ethers-contract-derive/src/event.rs index 78b8b5a8..63c9fcdf 100644 --- a/ethers-contract/ethers-contract-derive/src/event.rs +++ b/ethers-contract/ethers-contract-derive/src/event.rs @@ -133,7 +133,6 @@ pub(crate) fn derive_eth_event_impl(input: DeriveInput) -> TokenStream { let tokenize_impl = abi_ty::derive_tokenizeable_impl(&input); - // parse attributes abi into source quote! { #tokenize_impl #ethevent_impl @@ -322,47 +321,11 @@ fn derive_decode_from_log_impl( }) } +/// Determine the event's ABI by parsing the AST fn derive_abi_event_from_fields(input: &DeriveInput) -> Result { - let fields: Vec<_> = match input.data { - Data::Struct(ref data) => match data.fields { - Fields::Named(ref fields) => fields.named.iter().collect(), - Fields::Unnamed(ref fields) => fields.unnamed.iter().collect(), - Fields::Unit => { - return Err(Error::new( - input.span(), - "EthEvent cannot be derived for empty structs and unit", - )) - } - }, - Data::Enum(_) => { - return Err(Error::new( - input.span(), - "EthEvent cannot be derived for enums", - )); - } - Data::Union(_) => { - return Err(Error::new( - input.span(), - "EthEvent cannot be derived for unions", - )); - } - }; - - let inputs = fields - .iter() - .map(|f| { - let name = f - .ident - .as_ref() - .map(|name| name.to_string()) - .unwrap_or_else(|| "".to_string()); - utils::find_parameter_type(&f.ty).map(|ty| (name, ty)) - }) - .collect::, _>>()?; - let event = Event { name: "".to_string(), - inputs: inputs + inputs: utils::derive_abi_inputs_from_fields(input, "EthEvent")? .into_iter() .map(|(name, kind)| EventParam { name, diff --git a/ethers-contract/ethers-contract-derive/src/lib.rs b/ethers-contract/ethers-contract-derive/src/lib.rs index bf36f392..e30efb9b 100644 --- a/ethers-contract/ethers-contract-derive/src/lib.rs +++ b/ethers-contract/ethers-contract-derive/src/lib.rs @@ -9,6 +9,7 @@ use abigen::Contracts; pub(crate) mod abi_ty; mod abigen; +mod call; mod display; mod event; mod spanned; @@ -111,6 +112,9 @@ pub fn derive_abi_type(input: TokenStream) -> TokenStream { /// # Example /// /// ```ignore +/// use ethers_contract::EthDisplay; +/// use ethers_core::types::*; +/// /// #[derive(Debug, Clone, EthAbiType, EthDisplay)] /// struct MyStruct { /// addr: Address, @@ -161,7 +165,8 @@ pub fn derive_eth_display(input: TokenStream) -> TokenStream { /// /// # Example /// ```ignore -/// # use ethers_core::types::Address; +/// use ethers_contract::EthCall; +/// use ethers_core::types::Address; /// /// #[derive(Debug, EthAbiType)] /// struct Inner { @@ -183,3 +188,70 @@ pub fn derive_abi_event(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); TokenStream::from(event::derive_eth_event_impl(input)) } + +/// Derives the `EthCall` and `Tokenizeable` trait for the labeled type. +/// +/// Additional arguments can be specified using the `#[ethcall(...)]` +/// 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::EthCall; +/// +/// #[derive(Debug, Clone, EthCall)] +/// #[ethcall(name ="my_call")] +/// struct MyCall { +/// addr: Address, +/// old_value: String, +/// new_value: String, +/// } +/// assert_eq!( +/// MyCall::abi_signature().as_ref(), +/// "my_call(address,string,string)" +/// ); +/// ``` +/// +/// # Example +/// +/// Call with struct inputs +/// +/// ```ignore +/// use ethers_core::abi::Address; +/// +/// #[derive(Debug, Clone, PartialEq, EthAbiType)] +/// struct SomeType { +/// inner: Address, +/// msg: String, +/// } +/// +/// #[derive(Debug, PartialEq, EthCall)] +/// #[ethcall(name = "foo", abi = "foo(address,(address,string),string)")] +/// struct FooCall { +/// old_author: Address, +/// inner: SomeType, +/// new_value: String, +/// } +/// +/// assert_eq!( +/// FooCall::abi_signature().as_ref(), +/// "foo(address,(address,string),string)" +/// ); +/// ``` +/// +#[proc_macro_derive(EthCall, attributes(ethcall))] +pub fn derive_abi_call(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + TokenStream::from(call::derive_eth_call_impl(input)) +} diff --git a/ethers-contract/ethers-contract-derive/src/utils.rs b/ethers-contract/ethers-contract-derive/src/utils.rs index 29e4d849..49da2b9e 100644 --- a/ethers-contract/ethers-contract-derive/src/utils.rs +++ b/ethers-contract/ethers-contract-derive/src/utils.rs @@ -1,9 +1,12 @@ use ethers_contract_abigen::ethers_core_crate; use ethers_core::abi::ParamType; +use ethers_core::types::Selector; use proc_macro2::Literal; use quote::quote; use syn::spanned::Spanned as _; -use syn::{parse::Error, Expr, GenericArgument, Lit, PathArguments, Type}; +use syn::{ + parse::Error, Data, DeriveInput, Expr, Fields, GenericArgument, Lit, PathArguments, Type, +}; pub fn signature(hash: &[u8]) -> proc_macro2::TokenStream { let core_crate = ethers_core_crate(); @@ -11,6 +14,11 @@ pub fn signature(hash: &[u8]) -> proc_macro2::TokenStream { quote! {#core_crate::types::H256([#( #bytes ),*])} } +pub fn selector(selector: Selector) -> proc_macro2::TokenStream { + let bytes = selector.iter().copied().map(Literal::u8_unsuffixed); + quote! {[#( #bytes ),*]} +} + /// Parses an int type from its string representation pub fn parse_int_param_type(s: &str) -> Option { let size = s @@ -158,3 +166,49 @@ pub fn find_parameter_type(ty: &Type) -> Result { )), } } + +/// Attempts to determine the ABI Paramtypes from the type's AST +pub fn derive_abi_inputs_from_fields( + input: &DeriveInput, + trait_name: &str, +) -> Result, Error> { + let fields: Vec<_> = match input.data { + Data::Struct(ref data) => match data.fields { + Fields::Named(ref fields) => fields.named.iter().collect(), + Fields::Unnamed(ref fields) => fields.unnamed.iter().collect(), + Fields::Unit => { + return Err(Error::new( + input.span(), + format!( + "{} cannot be derived for empty structs and unit", + trait_name + ), + )) + } + }, + Data::Enum(_) => { + return Err(Error::new( + input.span(), + format!("{} cannot be derived for enums", trait_name), + )); + } + Data::Union(_) => { + return Err(Error::new( + input.span(), + format!("{} cannot be derived for unions", trait_name), + )); + } + }; + + fields + .iter() + .map(|f| { + let name = f + .ident + .as_ref() + .map(|name| name.to_string()) + .unwrap_or_else(|| "".to_string()); + find_parameter_type(&f.ty).map(|ty| (name, ty)) + }) + .collect() +} diff --git a/ethers-contract/src/call.rs b/ethers-contract/src/call.rs index b6f7edda..e58e727c 100644 --- a/ethers-contract/src/call.rs +++ b/ethers-contract/src/call.rs @@ -7,10 +7,43 @@ use ethers_core::{ }; use ethers_providers::{Middleware, PendingTransaction, ProviderError}; +use std::borrow::Cow; use std::{fmt::Debug, marker::PhantomData, sync::Arc}; +use crate::{AbiDecode, AbiEncode}; +use ethers_core::abi::{Tokenizable, Tokenize}; +use ethers_core::types::Selector; +use ethers_core::utils::id; use thiserror::Error as ThisError; +/// A helper trait for types that represent all call input parameters of a specific function +pub trait EthCall: Tokenizable + AbiDecode + Send + Sync { + /// The name of the function + fn function_name() -> Cow<'static, str>; + + /// Retrieves the ABI signature for the call + fn abi_signature() -> Cow<'static, str>; + + /// The selector of the function + fn selector() -> Selector { + id(Self::abi_signature()) + } +} + +impl AbiEncode for T { + fn encode(self) -> Result { + let tokens = self.into_tokens(); + let selector = Self::selector(); + let encoded = ethers_core::abi::encode(&tokens); + let encoded: Vec<_> = selector + .iter() + .copied() + .chain(encoded.into_iter()) + .collect(); + Ok(encoded.into()) + } +} + #[derive(ThisError, Debug)] /// An Error which is thrown when interacting with a smart contract pub enum ContractError { diff --git a/ethers-contract/src/codec.rs b/ethers-contract/src/codec.rs new file mode 100644 index 00000000..19060bac --- /dev/null +++ b/ethers-contract/src/codec.rs @@ -0,0 +1,14 @@ +use crate::AbiError; +use ethers_core::types::Bytes; + +/// Trait for ABI encoding +pub trait AbiEncode { + /// ABI encode the type + fn encode(self) -> Result; +} + +/// Trait for ABI decoding +pub trait AbiDecode: Sized { + /// Decodes the ABI encoded data + fn decode(bytes: impl AsRef<[u8]>) -> Result; +} diff --git a/ethers-contract/src/lib.rs b/ethers-contract/src/lib.rs index db3d2a22..e46fc305 100644 --- a/ethers-contract/src/lib.rs +++ b/ethers-contract/src/lib.rs @@ -20,7 +20,7 @@ mod base; pub use base::{decode_function_data, encode_function_data, AbiError, BaseContract}; mod call; -pub use call::ContractError; +pub use call::{ContractError, EthCall}; mod factory; pub use factory::ContractFactory; @@ -31,6 +31,9 @@ pub use event::EthEvent; mod log; pub use log::{decode_logs, EthLogDecode, LogMeta}; +mod codec; +pub use codec::{AbiDecode, AbiEncode}; + mod stream; mod multicall; @@ -50,7 +53,7 @@ pub use ethers_contract_abigen::Abigen; #[cfg(any(test, feature = "abigen"))] #[cfg_attr(docsrs, doc(cfg(feature = "abigen")))] -pub use ethers_contract_derive::{abigen, EthAbiType, EthDisplay, EthEvent}; +pub use ethers_contract_derive::{abigen, EthAbiType, EthCall, EthDisplay, EthEvent}; // Hide the Lazy re-export, it's just for convenience #[doc(hidden)] diff --git a/ethers-contract/tests/abigen.rs b/ethers-contract/tests/abigen.rs index 4bc51a13..801df914 100644 --- a/ethers-contract/tests/abigen.rs +++ b/ethers-contract/tests/abigen.rs @@ -1,6 +1,6 @@ #![cfg(feature = "abigen")] //! Test cases to validate the `abigen!` macro -use ethers_contract::{abigen, EthEvent}; +use ethers_contract::{abigen, AbiDecode, AbiEncode, EthEvent}; use ethers_core::abi::{Address, Tokenizable}; use ethers_core::types::U256; use ethers_providers::Provider; @@ -163,6 +163,8 @@ fn can_gen_human_readable_with_structs() { r#"[ struct Foo { uint256 x; } function foo(Foo memory x) + function bar(uint256 x, uint256 y, address addr) + yeet(uint256,uint256,address) ]"#, event_derives(serde::Deserialize, serde::Serialize) ); @@ -172,6 +174,33 @@ fn can_gen_human_readable_with_structs() { let contract = SimpleContract::new(Address::default(), Arc::new(client)); let f = Foo { x: 100u64.into() }; let _ = contract.foo(f); + + let call = BarCall { + x: 1u64.into(), + y: 0u64.into(), + addr: Address::random(), + }; + let encoded_call = contract.encode("bar", (call.x, call.y, call.addr)).unwrap(); + assert_eq!(encoded_call, call.clone().encode().unwrap()); + let decoded_call = BarCall::decode(encoded_call.as_ref()).unwrap(); + assert_eq!(call, decoded_call); + + let contract_call = SimpleContractCalls::Bar(call); + let decoded_enum = SimpleContractCalls::decode(encoded_call.as_ref()).unwrap(); + assert_eq!(contract_call, decoded_enum); + assert_eq!(encoded_call, contract_call.encode().unwrap()); + + let call = YeetCall(1u64.into(), 0u64.into(), Address::zero()); + let encoded_call = contract.encode("yeet", (call.0, call.1, call.2)).unwrap(); + assert_eq!(encoded_call, call.clone().encode().unwrap()); + let decoded_call = YeetCall::decode(encoded_call.as_ref()).unwrap(); + assert_eq!(call, decoded_call); + + let contract_call = SimpleContractCalls::Yeet(call.clone()); + let decoded_enum = SimpleContractCalls::decode(encoded_call.as_ref()).unwrap(); + assert_eq!(contract_call, decoded_enum); + assert_eq!(contract_call, call.into()); + assert_eq!(encoded_call, contract_call.encode().unwrap()); } #[test] @@ -192,4 +221,48 @@ fn can_handle_overloaded_functions() { let _ = contract.get_value(); let _ = contract.get_value_with_other_value(1337u64.into()); let _ = contract.get_value_with_other_value_and_addr(1337u64.into(), Address::zero()); + + let call = GetValueCall; + + let encoded_call = contract.encode("getValue", ()).unwrap(); + assert_eq!(encoded_call, call.clone().encode().unwrap()); + let decoded_call = GetValueCall::decode(encoded_call.as_ref()).unwrap(); + assert_eq!(call, decoded_call); + + let contract_call = SimpleContractCalls::GetValue(call); + let decoded_enum = SimpleContractCalls::decode(encoded_call.as_ref()).unwrap(); + assert_eq!(contract_call, decoded_enum); + assert_eq!(encoded_call, contract_call.encode().unwrap()); + + let call = GetValueWithOtherValueCall { + other_value: 420u64.into(), + }; + + let encoded_call = contract + .encode_with_selector([15, 244, 201, 22], call.other_value) + .unwrap(); + assert_eq!(encoded_call, call.clone().encode().unwrap()); + let decoded_call = GetValueWithOtherValueCall::decode(encoded_call.as_ref()).unwrap(); + assert_eq!(call, decoded_call); + + let contract_call = SimpleContractCalls::GetValueWithOtherValue(call); + let decoded_enum = SimpleContractCalls::decode(encoded_call.as_ref()).unwrap(); + assert_eq!(contract_call, decoded_enum); + assert_eq!(encoded_call, contract_call.encode().unwrap()); + + let call = GetValueWithOtherValueAndAddrCall { + other_value: 420u64.into(), + addr: Address::random(), + }; + + let encoded_call = contract + .encode_with_selector([14, 97, 29, 56], (call.other_value, call.addr)) + .unwrap(); + let decoded_call = GetValueWithOtherValueAndAddrCall::decode(encoded_call.as_ref()).unwrap(); + assert_eq!(call, decoded_call); + + let contract_call = SimpleContractCalls::GetValueWithOtherValueAndAddr(call); + let decoded_enum = SimpleContractCalls::decode(encoded_call.as_ref()).unwrap(); + assert_eq!(contract_call, decoded_enum); + assert_eq!(encoded_call, contract_call.encode().unwrap()); } diff --git a/ethers-contract/tests/common/derive.rs b/ethers-contract/tests/common/derive.rs index 6d471522..19aecfc3 100644 --- a/ethers-contract/tests/common/derive.rs +++ b/ethers-contract/tests/common/derive.rs @@ -1,9 +1,12 @@ use ethers_contract::EthLogDecode; -use ethers_contract::{abigen, EthAbiType, EthDisplay, EthEvent}; +use ethers_contract::{abigen, AbiDecode, EthAbiType, EthCall, EthDisplay, EthEvent}; use ethers_core::abi::{RawLog, Tokenizable}; use ethers_core::types::Address; use ethers_core::types::{H160, H256, I256, U128, U256}; +fn assert_tokenizeable() {} +fn assert_ethcall() {} + #[derive(Debug, Clone, PartialEq, EthAbiType)] struct ValueChanged { old_author: Address, @@ -42,6 +45,22 @@ fn can_detokenize_struct() { assert_eq!(value, ValueChanged::from_token(token).unwrap()); } +#[test] +fn can_derive_abi_type_empty_struct() { + #[derive(Debug, Clone, PartialEq, EthAbiType)] + struct Call(); + + #[derive(Debug, Clone, PartialEq, EthAbiType)] + struct Call2 {}; + + #[derive(Debug, Clone, PartialEq, EthAbiType)] + struct Call3; + + assert_tokenizeable::(); + assert_tokenizeable::(); + assert_tokenizeable::(); +} + #[test] fn can_detokenize_nested_structs() { let value = ValueChangedWrapper { @@ -319,6 +338,27 @@ fn can_decode_event_single_param() { assert_eq!(event.param1, 123u64.into()); } +#[test] +fn can_decode_event_tuple_single_param() { + #[derive(Debug, PartialEq, EthEvent)] + struct OneParam(#[ethevent(indexed)] U256); + + let log = RawLog { + topics: vec![ + "bd9bb67345a2fcc8ef3b0857e7e2901f5a0dcfc7fe5e3c10dc984f02842fb7ba" + .parse() + .unwrap(), + "000000000000000000000000000000000000000000000000000000000000007b" + .parse() + .unwrap(), + ], + data: vec![], + }; + + let event = ::decode_log(&log).unwrap(); + assert_eq!(event.0, 123u64.into()); +} + #[test] fn can_decode_event_with_no_params() { #[derive(Debug, PartialEq, EthEvent)] @@ -389,3 +429,61 @@ fn eth_display_works_for_human_readable() { }; assert_eq!("abc".to_string(), format!("{}", log)); } + +#[test] +fn can_derive_ethcall() { + #[derive(Debug, Clone, EthCall, EthDisplay)] + struct MyStruct { + addr: Address, + old_value: String, + new_value: String, + h: H256, + i: I256, + arr_u8: [u8; 32], + arr_u16: [u16; 32], + v: Vec, + } + + assert_tokenizeable::(); + assert_ethcall::(); + + #[derive(Debug, Clone, EthCall, EthDisplay)] + #[ethcall(name = "my_call")] + struct MyCall { + addr: Address, + old_value: String, + new_value: String, + } + assert_eq!( + MyCall::abi_signature().as_ref(), + "my_call(address,string,string)" + ); + + assert_tokenizeable::(); + assert_ethcall::(); +} + +#[test] +fn can_derive_ethcall_with_nested_structs() { + #[derive(Debug, Clone, PartialEq, EthAbiType)] + struct SomeType { + inner: Address, + msg: String, + } + + #[derive(Debug, PartialEq, EthCall)] + #[ethcall(name = "foo", abi = "foo(address,(address,string),string)")] + struct FooCall { + old_author: Address, + inner: SomeType, + new_value: String, + } + + assert_eq!( + FooCall::abi_signature().as_ref(), + "foo(address,(address,string),string)" + ); + + assert_tokenizeable::(); + assert_ethcall::(); +} diff --git a/ethers-core/src/abi/human_readable.rs b/ethers-core/src/abi/human_readable.rs index 9c013c74..c1317b41 100644 --- a/ethers-core/src/abi/human_readable.rs +++ b/ethers-core/src/abi/human_readable.rs @@ -337,14 +337,16 @@ impl AbiParser { let state_mutability = modifiers.map(detect_state_mutability).unwrap_or_default(); - #[allow(deprecated)] - Ok(Function { - name, - inputs, - outputs, - state_mutability, - constant: false, - }) + Ok( + #[allow(deprecated)] + Function { + name, + inputs, + outputs, + state_mutability, + constant: false, + }, + ) } fn parse_params(&self, s: &str) -> Result)>> { diff --git a/ethers-core/src/types/i256.rs b/ethers-core/src/types/i256.rs index 940f82f6..bbf6c27d 100644 --- a/ethers-core/src/types/i256.rs +++ b/ethers-core/src/types/i256.rs @@ -59,9 +59,7 @@ fn twos_complement(u: U256) -> U256 { fn handle_overflow((result, overflow): (T, bool)) -> T { #[cfg(debug_assertions)] { - if overflow { - panic!("overflow"); - } + assert!(!overflow, "overflow"); } let _ = overflow; diff --git a/ethers-core/src/types/transaction/eip712.rs b/ethers-core/src/types/transaction/eip712.rs index 57ab7e0e..dc15bbdc 100644 --- a/ethers-core/src/types/transaction/eip712.rs +++ b/ethers-core/src/types/transaction/eip712.rs @@ -483,7 +483,7 @@ pub fn find_parameter_type(ty: &Type) -> Result { let params = ty .elems .iter() - .map(|t| find_parameter_type(t)) + .map(find_parameter_type) .collect::, _>>()?; Ok(ParamType::Tuple(params)) }