From 57010c1c606439e172758c5156f6914b4f1b2acf Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Tue, 16 Mar 2021 20:37:19 +0100 Subject: [PATCH] feat(abigen): include ethevent proc macro in abigen code gen workflow (#232) * fix: make EthEvent name method a trait method * refactor: make expand methods members of Context * fix: make AbiParser parsing non consumeable * feat: add struct expanding * feat: use derive(EthEvent) in abigen workflow * test: check EthEvent in abigen macro * test: make test compile again * refactor: simplify and optimize abi parsing from single str * test: add human readable abigen tests --- .../ethers-contract-abigen/src/contract.rs | 25 +- .../src/contract/common.rs | 15 +- .../src/contract/events.rs | 367 ++++++++---------- .../src/contract/structs.rs | 109 ++++++ .../ethers-contract-derive/src/lib.rs | 2 +- ethers-contract/src/event.rs | 2 +- ethers-contract/tests/abigen.rs | 66 ++++ ethers-contract/tests/common/derive.rs | 11 +- ethers-core/src/abi/human_readable.rs | 62 ++- ethers-core/src/abi/mod.rs | 2 +- 10 files changed, 412 insertions(+), 249 deletions(-) create mode 100644 ethers-contract/ethers-contract-abigen/src/contract/structs.rs create mode 100644 ethers-contract/tests/abigen.rs diff --git a/ethers-contract/ethers-contract-abigen/src/contract.rs b/ethers-contract/ethers-contract-abigen/src/contract.rs index 02b623a7..02a3a582 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract.rs @@ -2,11 +2,13 @@ mod common; mod events; mod methods; +mod structs; mod types; use super::util; use super::Abigen; use anyhow::{anyhow, Context as _, Result}; +use ethers_core::abi::AbiParser; use ethers_core::{ abi::{parse_abi, Abi}, types::Address, @@ -25,6 +27,9 @@ pub(crate) struct Context { /// The parsed ABI. abi: Abi, + /// The parser used for human readable format + abi_parser: AbiParser, + /// Was the ABI in human readable format? human_readable: bool, @@ -59,11 +64,14 @@ impl Context { let events_decl = cx.events_declaration()?; // 3. impl block for the event functions - let contract_events = cx.events()?; + let contract_events = cx.event_methods()?; // 4. impl block for the contract methods let contract_methods = cx.methods()?; + // 5. Declare the structs parsed from the human readable abi + let abi_structs_decl = cx.abi_structs()?; + Ok(quote! { // export all the created data types pub use #name_mod::*; @@ -91,6 +99,8 @@ impl Context { } #events_decl + + #abi_structs_decl } }) } @@ -99,22 +109,14 @@ impl Context { fn from_abigen(args: Abigen) -> Result { // get the actual ABI string let abi_str = args.abi_source.get().context("failed to get ABI JSON")?; + let mut abi_parser = AbiParser::default(); // parse it let (abi, human_readable): (Abi, _) = if let Ok(abi) = serde_json::from_str(&abi_str) { // normal abi format (abi, false) } else { // heuristic for parsing the human readable format - - // replace bad chars - let abi_str = abi_str.replace('[', "").replace(']', ""); - // split lines and get only the non-empty things - let split: Vec<&str> = abi_str - .split('\n') - .map(|x| x.trim()) - .filter(|x| !x.is_empty()) - .collect(); - (parse_abi(&split)?, true) + (abi_parser.parse_str(&abi_str)?, true) }; let contract_name = util::ident(&args.contract_name); @@ -144,6 +146,7 @@ impl Context { abi, human_readable, abi_str: Literal::string(&abi_str), + abi_parser, contract_name, method_aliases, event_derives, diff --git a/ethers-contract/ethers-contract-abigen/src/contract/common.rs b/ethers-contract/ethers-contract-abigen/src/contract/common.rs index a67954a1..49da608f 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract/common.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract/common.rs @@ -15,7 +15,7 @@ pub(crate) fn imports(name: &str) -> TokenStream { use std::sync::Arc; use ethers::{ core::{ - abi::{Abi, Token, Detokenize, InvalidOutputType, Tokenizable, parse_abi}, + abi::{Abi, Token, Detokenize, InvalidOutputType, Tokenizable}, types::*, // import all the types so that we can codegen for everything }, contract::{Contract, builders::{ContractCall, Event}, Lazy}, @@ -24,6 +24,7 @@ pub(crate) fn imports(name: &str) -> TokenStream { } } +/// Generates the static `Abi` constants and the contract struct pub(crate) fn struct_declaration(cx: &Context, abi_name: &proc_macro2::Ident) -> TokenStream { let name = &cx.contract_name; let abi = &cx.abi_str; @@ -35,16 +36,8 @@ pub(crate) fn struct_declaration(cx: &Context, abi_name: &proc_macro2::Ident) -> } } else { quote! { - pub static #abi_name: Lazy = Lazy::new(|| { - let abi_str = #abi.replace('[', "").replace(']', ""); - // split lines and get only the non-empty things - let split: Vec<&str> = abi_str - .split("\n") - .map(|x| x.trim()) - .filter(|x| !x.is_empty()) - .collect(); - parse_abi(&split).expect("invalid abi") - }); + pub static #abi_name: Lazy = Lazy::new(|| ethers::core::abi::parse_abi_str(#abi) + .expect("invalid abi")); } }; diff --git a/ethers-contract/ethers-contract-abigen/src/contract/events.rs b/ethers-contract/ethers-contract-abigen/src/contract/events.rs index 9063d5f3..41a2fa39 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract/events.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract/events.rs @@ -1,6 +1,6 @@ use super::{types, util, Context}; use anyhow::Result; -use ethers_core::abi::{Event, EventExt, EventParam, Hash, ParamType}; +use ethers_core::abi::{Event, EventExt, EventParam, Hash, ParamType, SolStruct}; use inflector::Inflector; use proc_macro2::{Literal, TokenStream}; use quote::quote; @@ -14,7 +14,7 @@ impl Context { let data_types = sorted_events .values() .flatten() - .map(|event| expand_event(event, &self.event_derives)) + .map(|event| self.expand_event(event)) .collect::>>()?; if data_types.is_empty() { @@ -26,12 +26,13 @@ impl Context { }) } - pub fn events(&self) -> Result { + /// 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 data_types = sorted_events .values() .flatten() - .map(|event| expand_filter(event)) + .map(|event| self.expand_filter(event)) .collect::>(); if data_types.is_empty() { @@ -42,214 +43,201 @@ impl Context { #( #data_types )* }) } -} -/// Expands into a single method for contracting an event stream. -fn expand_filter(event: &Event) -> TokenStream { - // append `filter` to disambiguate with potentially conflicting - // function names - let name = util::safe_ident(&format!("{}_filter", event.name.to_snake_case())); - // let result = util::ident(&event.name.to_pascal_case()); - let result = expand_struct_name(event); + /// 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 + /// indexed. + /// If a complex types matches with a struct previously parsed by the AbiParser, + /// we can replace it + fn expand_input_type(&self, input: &EventParam) -> Result { + Ok(match (&input.kind, input.indexed) { + (ParamType::Array(ty), true) => { + if let ParamType::Tuple(..) = **ty { + // represents an array of a struct + if let Some(ty) = self + .abi_parser + .structs + .get(&input.name) + .map(SolStruct::name) + .map(util::ident) + { + return Ok(quote! {::std::vec::Vec<#ty>}); + } + } + quote! { H256 } + } + (ParamType::FixedArray(ty, size), true) => { + if let ParamType::Tuple(..) = **ty { + // represents a fixed array of a struct + if let Some(ty) = self + .abi_parser + .structs + .get(&input.name) + .map(SolStruct::name) + .map(util::ident) + { + let size = Literal::usize_unsuffixed(*size); + return Ok(quote! {[#ty; #size]}); + } + } + quote! { H256 } + } + (ParamType::Tuple(..), true) => { + // represents an struct + if let Some(ty) = self + .abi_parser + .structs + .get(&input.name) + .map(SolStruct::name) + .map(util::ident) + { + quote! {#ty} + } else { + quote! { H256 } + } + } + (ParamType::Bytes, true) | (ParamType::String, true) => { + quote! { H256 } + } + (kind, _) => types::expand(kind)?, + }) + } - let ev_name = Literal::string(&event.name); + /// Expands an ABI event into name-type pairs for each of its parameters. + fn expand_params(&self, event: &Event) -> Result> { + event + .inputs + .iter() + .enumerate() + .map(|(i, input)| { + // NOTE: Events can contain nameless values. + let name = util::expand_input_name(i, &input.name); + let ty = self.expand_input_type(&input)?; - let doc = util::expand_doc(&format!("Gets the contract's `{}` event", event.name)); - quote! { + Ok((name, ty)) + }) + .collect() + } - #doc - pub fn #name(&self) -> Event { - self.0.event(#ev_name).expect("event not found (this should never happen)") + /// Expands into a single method for contracting an event stream. + fn expand_filter(&self, event: &Event) -> TokenStream { + // append `filter` to disambiguate with potentially conflicting + // function names + let name = util::safe_ident(&format!("{}_filter", event.name.to_snake_case())); + // let result = util::ident(&event.name.to_pascal_case()); + let result = expand_struct_name(event); + let ev_name = Literal::string(&event.name); + + let doc = util::expand_doc(&format!("Gets the contract's `{}` event", event.name)); + quote! { + #doc + pub fn #name(&self) -> Event { + self.0.event(#ev_name).expect("event not found (this should never happen)") + } } } -} -/// Expands an ABI event into a single event data type. This can expand either -/// into a structure or a tuple in the case where all event parameters (topics -/// and data) are anonymous. -fn expand_event(event: &Event, event_derives: &[Path]) -> Result { - let event_name = expand_struct_name(event); + /// Expands an ABI event into a single event data type. This can expand either + /// into a structure or a tuple in the case where all event parameters (topics + /// and data) are anonymous. + fn expand_event(&self, event: &Event) -> Result { + let event_name = expand_struct_name(event); - let signature = expand_hash(event.signature()); + let params = self.expand_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 { + expand_data_tuple(&event_name, ¶ms) + } else { + expand_data_struct(&event_name, ¶ms) + }; - let abi_signature = event.abi_signature(); - let abi_signature_lit = Literal::string(&abi_signature); - let abi_signature_doc = util::expand_doc(&format!("`{}`", abi_signature)); + let derives = expand_derives(&self.event_derives); + let abi_signature = event.abi_signature(); + let event_abi_name = &event.name; - let params = expand_params(event)?; + Ok(quote! { + #[derive(Clone, Debug, Default, Eq, PartialEq, ethers::contract::EthEvent, #derives)] + #[ethevent( name = #event_abi_name, abi = #abi_signature )] + pub #data_type_definition + }) + } - // 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, data_type_construction) = if all_anonymous_fields { - expand_data_tuple(&event_name, ¶ms) - } else { - expand_data_struct(&event_name, ¶ms) - }; + /// Expands a event parameter into an event builder filter method for the + /// specified topic index. + fn expand_builder_topic_filter( + &self, + topic_index: usize, + param: &EventParam, + ) -> Result { + let doc = util::expand_doc(&format!( + "Adds a filter for the `{}` event parameter.", + param.name, + )); + let topic = util::ident(&format!("topic{}", topic_index)); + let name = if param.name.is_empty() { + topic.clone() + } else { + util::safe_ident(¶m.name.to_snake_case()) + }; + let ty = self.expand_input_type(¶m)?; - // read each token parameter as the required data type - let params_len = Literal::usize_unsuffixed(params.len()); - let read_param_token = params - .iter() - .map(|(name, _)| { - quote! { - let #name = Tokenizable::from_token(tokens.next().expect("this should never happen"))?; + Ok(quote! { + #doc + pub fn #name(mut self, topic: Topic<#ty>) -> Self { + self.0 = (self.0).#topic(topic); + self } }) - .collect::>(); + } - let derives = expand_derives(event_derives); + /// Expands an ABI event into filter methods for its indexed parameters. + fn expand_builder_topic_filters(&self, event: &Event) -> Result { + let topic_filters = event + .inputs + .iter() + .filter(|input| input.indexed) + .enumerate() + .map(|(topic_index, input)| self.expand_builder_topic_filter(topic_index, input)) + .collect::>>()?; - Ok(quote! { - #[derive(Clone, Debug, Default, Eq, PartialEq, #derives)] - pub #data_type_definition - - impl #event_name { - /// Retrieves the signature for the event this data corresponds to. - /// This signature is the Keccak-256 hash of the ABI signature of - /// this event. - pub const fn signature() -> H256 { - #signature - } - - /// Retrieves the ABI signature for the event this data corresponds - /// to. For this event the value should always be: - /// - #abi_signature_doc - pub const fn abi_signature() -> &'static str { - #abi_signature_lit - } - } - - impl Detokenize for #event_name { - fn from_tokens( - tokens: Vec, - ) -> Result { - if tokens.len() != #params_len { - return Err(InvalidOutputType(format!( - "Expected {} tokens, got {}: {:?}", - #params_len, - tokens.len(), - tokens - ))); - } - - #[allow(unused_mut)] - let mut tokens = tokens.into_iter(); - #( #read_param_token )* - - Ok(#data_type_construction) - } - } - }) + Ok(quote! { + #( #topic_filters )* + }) + } } /// Expands an ABI event into an identifier for its event data type. fn expand_struct_name(event: &Event) -> TokenStream { + // TODO: get rid of `Filter` suffix? let name = format!("{}Filter", event.name.to_pascal_case()); let event_name = util::ident(&name); quote! { #event_name } } -/// Expands an ABI event into name-type pairs for each of its parameters. -fn expand_params(event: &Event) -> Result> { - event - .inputs - .iter() - .enumerate() - .map(|(i, input)| { - // NOTE: Events can contain nameless values. - let name = util::expand_input_name(i, &input.name); - let ty = expand_input_type(&input)?; - - Ok((name, ty)) - }) - .collect() -} - /// Expands an event data structure from its name-type parameter pairs. Returns /// a tuple with the type definition (i.e. the struct declaration) and /// construction (i.e. code for creating an instance of the event data). -fn expand_data_struct( - name: &TokenStream, - params: &[(TokenStream, TokenStream)], -) -> (TokenStream, TokenStream) { +fn expand_data_struct(name: &TokenStream, params: &[(TokenStream, TokenStream)]) -> TokenStream { let fields = params .iter() .map(|(name, ty)| quote! { pub #name: #ty }) .collect::>(); - let param_names = params - .iter() - .map(|(name, _)| name) - .cloned() - .collect::>(); - - let definition = quote! { struct #name { #( #fields, )* } }; - let construction = quote! { #name { #( #param_names ),* } }; - - (definition, construction) + quote! { struct #name { #( #fields, )* } } } /// Expands an event data named tuple from its name-type parameter pairs. /// Returns a tuple with the type definition and construction. -fn expand_data_tuple( - name: &TokenStream, - params: &[(TokenStream, TokenStream)], -) -> (TokenStream, TokenStream) { +fn expand_data_tuple(name: &TokenStream, params: &[(TokenStream, TokenStream)]) -> TokenStream { let fields = params .iter() .map(|(_, ty)| quote! { pub #ty }) .collect::>(); - let param_names = params - .iter() - .map(|(name, _)| name) - .cloned() - .collect::>(); - - let definition = quote! { struct #name( #( #fields ),* ); }; - let construction = quote! { #name( #( #param_names ),* ) }; - - (definition, construction) -} - -/// Expands an ABI event into filter methods for its indexed parameters. -fn expand_builder_topic_filters(event: &Event) -> Result { - let topic_filters = event - .inputs - .iter() - .filter(|input| input.indexed) - .enumerate() - .map(|(topic_index, input)| expand_builder_topic_filter(topic_index, input)) - .collect::>>()?; - - Ok(quote! { - #( #topic_filters )* - }) -} - -/// Expands a event parameter into an event builder filter method for the -/// specified topic index. -fn expand_builder_topic_filter(topic_index: usize, param: &EventParam) -> Result { - let doc = util::expand_doc(&format!( - "Adds a filter for the `{}` event parameter.", - param.name, - )); - let topic = util::ident(&format!("topic{}", topic_index)); - let name = if param.name.is_empty() { - topic.clone() - } else { - util::safe_ident(¶m.name.to_snake_case()) - }; - let ty = expand_input_type(¶m)?; - - Ok(quote! { - #doc - pub fn #name(mut self, topic: Topic<#ty>) -> Self { - self.0 = (self.0).#topic(topic); - self - } - }) + quote! { struct #name( #( #fields ),* ); } } /// Expands an ABI event into an identifier for its event data type. @@ -262,24 +250,6 @@ fn expand_derives(derives: &[Path]) -> TokenStream { quote! {#(#derives),*} } -/// 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 -/// indexed. -fn expand_input_type(input: &EventParam) -> Result { - Ok(match (&input.kind, input.indexed) { - (ParamType::Array(..), true) - | (ParamType::Bytes, true) - | (ParamType::FixedArray(..), true) - | (ParamType::String, true) - | (ParamType::Tuple(..), true) => { - quote! { H256 } - } - (kind, _) => types::expand(kind)?, - }) -} - /// Expands a 256-bit `Hash` into a literal representation that can be used with /// quasi-quoting for code generation. We do this to avoid allocating at runtime fn expand_hash(hash: Hash) -> TokenStream { @@ -293,8 +263,13 @@ fn expand_hash(hash: Hash) -> TokenStream { #[cfg(test)] mod tests { use super::*; + use crate::Abigen; use ethers_core::abi::{EventParam, ParamType}; + fn test_context() -> Context { + Context::from_abigen(Abigen::new("TestToken", "[]").unwrap()).unwrap() + } + #[test] fn expand_transfer_filter() { let event = Event { @@ -318,8 +293,8 @@ mod tests { ], anonymous: false, }; - - assert_quote!(expand_filter(&event), { + let cx = test_context(); + assert_quote!(cx.expand_filter(&event), { #[doc = "Gets the contract's `Transfer` event"] pub fn transfer_filter(&self) -> Event { self.0 @@ -348,9 +323,10 @@ mod tests { anonymous: false, }; + let cx = test_context(); + let params = cx.expand_params(&event).unwrap(); let name = expand_struct_name(&event); - let params = expand_params(&event).unwrap(); - let (definition, construction) = expand_data_struct(&name, ¶ms); + let definition = expand_data_struct(&name, ¶ms); assert_quote!(definition, { struct FooFilter { @@ -358,7 +334,6 @@ mod tests { pub p1: Address, } }); - assert_quote!(construction, { FooFilter { a, p1 } }); } #[test] @@ -380,14 +355,14 @@ mod tests { anonymous: false, }; + let cx = test_context(); + let params = cx.expand_params(&event).unwrap(); let name = expand_struct_name(&event); - let params = expand_params(&event).unwrap(); - let (definition, construction) = expand_data_tuple(&name, ¶ms); + let definition = expand_data_tuple(&name, ¶ms); assert_quote!(definition, { struct FooFilter(pub bool, pub Address); }); - assert_quote!(construction, { FooFilter(p0, p1) }); } #[test] diff --git a/ethers-contract/ethers-contract-abigen/src/contract/structs.rs b/ethers-contract/ethers-contract-abigen/src/contract/structs.rs new file mode 100644 index 00000000..cacb8e16 --- /dev/null +++ b/ethers-contract/ethers-contract-abigen/src/contract/structs.rs @@ -0,0 +1,109 @@ +//! Methods for expanding structs +use anyhow::{Context as _, Result}; +use inflector::Inflector; +use proc_macro2::{Literal, TokenStream}; +use quote::quote; + +use ethers_core::abi::{struct_def::FieldType, ParamType}; + +use crate::contract::{types, Context}; +use crate::util; + +impl Context { + /// Generate corresponding types for structs parsed from a human readable ABI + /// + /// NOTE: This assumes that all structs that are potentially used as type for variable are + /// in fact present in the `AbiParser`, this is sound because `AbiParser::parse` would have + /// failed already + pub fn abi_structs(&self) -> Result { + let mut structs = Vec::with_capacity(self.abi_parser.structs.len()); + for (name, sol_struct) in &self.abi_parser.structs { + let mut fields = Vec::with_capacity(sol_struct.fields().len()); + let mut param_types = Vec::with_capacity(sol_struct.fields().len()); + for field in sol_struct.fields() { + let field_name = util::ident(&field.name().to_snake_case()); + match field.r#type() { + FieldType::Elementary(ty) => { + param_types.push(ty.clone()); + let ty = types::expand(ty)?; + fields.push(quote! { pub #field_name: #ty }); + } + FieldType::Struct(struct_ty) => { + let ty = util::ident(struct_ty.name()); + fields.push(quote! { pub #field_name: #ty }); + + let tuple = self + .abi_parser + .struct_tuples + .get(struct_ty.name()) + .context(format!("No types found for {}", struct_ty.name()))? + .clone(); + param_types.push(ParamType::Tuple(tuple)); + } + FieldType::StructArray(struct_ty) => { + let ty = util::ident(struct_ty.name()); + fields.push(quote! { pub #field_name: ::std::vec::Vec<#ty> }); + + let tuple = self + .abi_parser + .struct_tuples + .get(struct_ty.name()) + .context(format!("No types found for {}", struct_ty.name()))? + .clone(); + param_types.push(ParamType::Array(Box::new(ParamType::Tuple(tuple)))); + } + FieldType::FixedStructArray(struct_ty, len) => { + let ty = util::ident(struct_ty.name()); + let size = Literal::usize_unsuffixed(*len); + fields.push(quote! { pub #field_name: [#ty; #size] }); + + let tuple = self + .abi_parser + .struct_tuples + .get(struct_ty.name()) + .context(format!("No types found for {}", struct_ty.name()))? + .clone(); + param_types.push(ParamType::FixedArray( + Box::new(ParamType::Tuple(tuple)), + *len, + )); + } + FieldType::Mapping(_) => { + return Err(anyhow::anyhow!( + "Mapping types in struct `{}` are not supported {:?}", + name, + field + )); + } + } + } + + let abi_signature = format!( + "{}({})", + name, + param_types + .iter() + .map(|kind| kind.to_string()) + .collect::>() + .join(","), + ); + + let abi_signature_doc = util::expand_doc(&format!("`{}`", abi_signature)); + + let name = util::ident(name); + + // use the same derives as for events + let derives = &self.event_derives; + let derives = quote! {#(#derives),*}; + + structs.push(quote! { + #abi_signature_doc + #[derive(Clone, Debug, Default, Eq, PartialEq, ethers::contract::EthAbiType, #derives)] + pub struct #name { + #( #fields ),* + } + }); + } + Ok(quote! {#( #structs )*}) + } +} diff --git a/ethers-contract/ethers-contract-derive/src/lib.rs b/ethers-contract/ethers-contract-derive/src/lib.rs index ccaa8b56..03f34ce2 100644 --- a/ethers-contract/ethers-contract-derive/src/lib.rs +++ b/ethers-contract/ethers-contract-derive/src/lib.rs @@ -190,7 +190,7 @@ pub fn derive_abi_event(input: TokenStream) -> TokenStream { let ethevent_impl = quote! { impl ethers_contract::EthEvent for #name { - fn name(&self) -> ::std::borrow::Cow<'static, str> { + fn name() -> ::std::borrow::Cow<'static, str> { #event_name.into() } diff --git a/ethers-contract/src/event.rs b/ethers-contract/src/event.rs index 73493fd7..f74d1be7 100644 --- a/ethers-contract/src/event.rs +++ b/ethers-contract/src/event.rs @@ -11,7 +11,7 @@ use std::marker::PhantomData; /// A trait for implementing event bindings pub trait EthEvent: Detokenize { /// The name of the event this type represents - fn name(&self) -> Cow<'static, str>; + fn name() -> Cow<'static, str>; /// Retrieves the signature for the event this data corresponds to. /// This signature is the Keccak-256 hash of the ABI signature of diff --git a/ethers-contract/tests/abigen.rs b/ethers-contract/tests/abigen.rs new file mode 100644 index 00000000..5e601724 --- /dev/null +++ b/ethers-contract/tests/abigen.rs @@ -0,0 +1,66 @@ +//! Test cases to validate the `abigen!` macro +use ethers_contract::{abigen, EthEvent}; +use ethers_core::abi::Tokenizable; + +#[test] +fn can_gen_human_readable() { + abigen!( + SimpleContract, + r#"[ + event ValueChanged(address indexed author, string oldValue, string newValue) + ]"#, + event_derives(serde::Deserialize, serde::Serialize) + ); + assert_eq!("ValueChanged", ValueChangedFilter::name()); + assert_eq!( + "ValueChanged(address,string,string)", + ValueChangedFilter::abi_signature() + ); +} + +#[test] +fn can_gen_structs_readable() { + abigen!( + SimpleContract, + r#"[ + struct Value {address addr; string value;} + struct Addresses {address[] addr; string s;} + event ValueChanged(Value indexed old, Value newValue, Addresses _a) + ]"#, + event_derives(serde::Deserialize, serde::Serialize) + ); + let value = Addresses { + addr: vec!["eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap()], + s: "hello".to_string(), + }; + let token = value.clone().into_token(); + assert_eq!(value, Addresses::from_token(token).unwrap()); + + assert_eq!("ValueChanged", ValueChangedFilter::name()); + assert_eq!( + "ValueChanged((address,string),(address,string),(address[],string))", + ValueChangedFilter::abi_signature() + ); +} + +// NOTE(mattsse): There is currently a limitation with the `ethabi` crate's `Reader` +// that doesn't support arrays of tuples; https://github.com/gakonst/ethabi/pull/1 should fix this +// See also https://github.com/rust-ethereum/ethabi/issues/178 and +// https://github.com/rust-ethereum/ethabi/pull/186 + +// #[test] +// fn can_gen_structs_with_arrays_readable() { +// abigen!( +// SimpleContract, +// r#"[ +// struct Value {address addr; string value;} +// struct Addresses {address[] addr; string s;} +// event ValueChanged(Value indexed old, Value newValue, Addresses[] _a) +// ]"#, +// event_derives(serde::Deserialize, serde::Serialize) +// ); +// assert_eq!( +// "ValueChanged((address,string),(address,string),(address[],string)[])", +// ValueChangedFilter::abi_signature() +// ); +// } diff --git a/ethers-contract/tests/common/derive.rs b/ethers-contract/tests/common/derive.rs index 93534b3f..2757178f 100644 --- a/ethers-contract/tests/common/derive.rs +++ b/ethers-contract/tests/common/derive.rs @@ -98,7 +98,7 @@ fn can_derive_eth_event() { new_value: "100".to_string(), }; - assert_eq!("ValueChangedEvent", value.name()); + assert_eq!("ValueChangedEvent", ValueChangedEvent::name()); assert_eq!( "ValueChangedEvent(address,address,string,string)", ValueChangedEvent::abi_signature() @@ -119,14 +119,7 @@ fn can_set_eth_event_name_attribute() { new_value: String, } - let value = ValueChangedEvent { - old_author: "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap(), - new_author: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".parse().unwrap(), - old_value: "50".to_string(), - new_value: "100".to_string(), - }; - - assert_eq!("MyEvent", value.name()); + assert_eq!("MyEvent", ValueChangedEvent::name()); assert_eq!( "MyEvent(address,address,string,string)", ValueChangedEvent::abi_signature() diff --git a/ethers-core/src/abi/human_readable.rs b/ethers-core/src/abi/human_readable.rs index 19195c6d..e87b6480 100644 --- a/ethers-core/src/abi/human_readable.rs +++ b/ethers-core/src/abi/human_readable.rs @@ -9,17 +9,33 @@ use crate::abi::{ /// A parser that turns a "human readable abi" into a `Abi` pub struct AbiParser { - abi: Abi, /// solidity structs - structs: HashMap, + pub structs: HashMap, /// solidity structs as tuples - struct_tuples: HashMap>, + pub struct_tuples: HashMap>, } impl AbiParser { /// Parses a "human readable abi" string - pub fn parse_str(self, s: &str) -> Result { - self.parse(&s.lines().collect::>()) + /// + /// # Example + /// + /// ``` + /// # use ethers::abi::AbiParser; + /// let abi = AbiParser::default().parse_str(r#"[ + /// function setValue(string) + /// function getValue() external view returns (string) + /// event ValueChanged(address indexed author, string oldValue, string newValue) + /// ]"#).unwrap(); + /// ``` + pub fn parse_str(&mut self, s: &str) -> Result { + self.parse( + &s.trim() + .trim_start_matches('[') + .trim_end_matches(']') + .lines() + .collect::>(), + ) } /// Parses a "human readable abi" string vector @@ -32,11 +48,21 @@ impl AbiParser { /// "function x() external view returns (uint256)", /// ]).unwrap(); /// ``` - pub fn parse(mut self, input: &[&str]) -> Result { + pub fn parse(&mut self, input: &[&str]) -> Result { // parse struct first + let mut abi = Abi { + constructor: None, + functions: HashMap::new(), + events: HashMap::new(), + receive: false, + fallback: false, + }; + let (structs, types): (Vec<_>, Vec<_>) = input .iter() .map(|s| escape_quotes(s)) + .map(str::trim) + .filter(|s| !s.is_empty()) .partition(|s| s.starts_with("struct")); for sol in structs { @@ -52,25 +78,23 @@ impl AbiParser { line = line.trim_start(); if line.starts_with("function") { let function = self.parse_function(&line)?; - self.abi - .functions + abi.functions .entry(function.name.clone()) .or_default() .push(function); } else if line.starts_with("event") { let event = self.parse_event(line)?; - self.abi - .events + abi.events .entry(event.name.clone()) .or_default() .push(event); } else if line.starts_with("constructor") { - self.abi.constructor = Some(self.parse_constructor(line)?); + abi.constructor = Some(self.parse_constructor(line)?); } else { bail!("Illegal abi `{}`", line) } } - Ok(self.abi) + Ok(abi) } /// Substitutes any other struct references within structs with tuples @@ -136,13 +160,6 @@ impl AbiParser { /// Link additional structs for parsing pub fn with_structs(structs: Vec) -> Self { Self { - abi: Abi { - constructor: None, - functions: HashMap::new(), - events: HashMap::new(), - receive: false, - fallback: false, - }, structs: structs .into_iter() .map(|s| (s.name().to_string(), s)) @@ -392,6 +409,13 @@ pub fn parse(input: &[&str]) -> Result { AbiParser::default().parse(input) } +/// Parses a "human readable abi" string +/// +/// See also `AbiParser::parse_str` +pub fn parse_str(input: &str) -> Result { + AbiParser::default().parse_str(input) +} + /// Parses an identifier like event or function name pub(crate) fn parse_identifier(input: &mut &str) -> Result { let mut chars = input.trim_start().chars(); diff --git a/ethers-core/src/abi/mod.rs b/ethers-core/src/abi/mod.rs index b09c91be..057fe156 100644 --- a/ethers-core/src/abi/mod.rs +++ b/ethers-core/src/abi/mod.rs @@ -15,7 +15,7 @@ mod error; pub use error::ParseError; mod human_readable; -pub use human_readable::{parse as parse_abi, AbiParser}; +pub use human_readable::{parse as parse_abi, parse_str as parse_abi_str, AbiParser}; /// Extension trait for `ethabi::Function`. pub trait FunctionExt {