From 7b10b76e2011df0a3eb2c9b754f330def090d994 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Mon, 15 Mar 2021 12:59:52 +0100 Subject: [PATCH] feat: add EthEvent proc macro derive support (#227) * refactor: extract error module and use error macros * feat: add solidity struct parser * refactor: add AbiParse and support struct parsing * test: add more struct parsing tests * feat: add EthAbiType proc macro derive for deriving Tokenizable trait * test: add EthAbiType derive tests * refactor: extract tokenizeable implementation in separate method * chore(test): use EthAbiType derive instead implementing Detokenizeable * feat: introduce EthEvent trait * feat: add EthEvent proc macro derive support * test: add proc macro derive tests * chore: rustfmt Co-authored-by: Georgios Konstantopoulos --- Cargo.lock | 3 + .../ethers-contract-derive/Cargo.toml | 2 +- .../ethers-contract-derive/src/lib.rs | 524 +++++++++++++++++- ethers-contract/src/event.rs | 16 + ethers-contract/src/lib.rs | 3 +- ethers-contract/tests/common/derive.rs | 183 ++++++ ethers-contract/tests/common/mod.rs | 26 +- ethers-core/src/abi/human_readable.rs | 6 +- 8 files changed, 733 insertions(+), 30 deletions(-) create mode 100644 ethers-contract/tests/common/derive.rs diff --git a/Cargo.lock b/Cargo.lock index 5d6d7771..e6f13ecb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "Inflector" version = "0.11.4" @@ -672,6 +674,7 @@ version = "0.2.2" dependencies = [ "ethers-contract-abigen", "ethers-core", + "hex", "proc-macro2", "quote", "serde_json", diff --git a/ethers-contract/ethers-contract-derive/Cargo.toml b/ethers-contract/ethers-contract-derive/Cargo.toml index b0caecbe..1a6c14ac 100644 --- a/ethers-contract/ethers-contract-derive/Cargo.toml +++ b/ethers-contract/ethers-contract-derive/Cargo.toml @@ -17,7 +17,7 @@ ethers-core = { version = "0.2.2", path = "../../ethers-core" } ethers-contract-abigen = { version = "0.2.2", path = "../ethers-contract-abigen" } serde_json = "1.0.53" - +hex = { version = "0.4.3", default-features = false, features = ["std"] } proc-macro2 = "1.0" quote = "1.0" syn = "1.0.12" diff --git a/ethers-contract/ethers-contract-derive/src/lib.rs b/ethers-contract/ethers-contract-derive/src/lib.rs index c5c2922b..37aa94cb 100644 --- a/ethers-contract/ethers-contract-derive/src/lib.rs +++ b/ethers-contract/ethers-contract-derive/src/lib.rs @@ -2,14 +2,23 @@ //! ethereum smart contract. #![deny(missing_docs, unsafe_code)] -mod spanned; +use ethers_contract_abigen::Source; +use proc_macro::TokenStream; +use proc_macro2::{Literal, Span}; +use quote::{quote, quote_spanned}; +use syn::spanned::Spanned as _; +use syn::{ + parse::Error, parse_macro_input, AttrStyle, Data, DeriveInput, Expr, Fields, GenericArgument, + Lit, Meta, NestedMeta, PathArguments, Type, +}; + +use abigen::{expand, ContractArgs}; +use ethers_core::abi::{AbiParser, Event, EventExt, EventParam, ParamType}; +use hex::FromHex; use spanned::Spanned; mod abigen; -use abigen::{expand, ContractArgs}; - -use proc_macro::TokenStream; -use syn::{parse::Error, parse_macro_input}; +mod spanned; /// Proc macro to generate type-safe bindings to a contract. This macro accepts /// an Ethereum contract ABI or a path. Note that this path is rooted in @@ -65,3 +74,508 @@ pub fn abigen(input: TokenStream) -> TokenStream { .unwrap_or_else(|e| Error::new(span, format!("{:?}", e)).to_compile_error()) .into() } + +/// Derives the `EthEvent` and `Tokenizeable` trait for the labeled type. +/// +/// Additional arguments can be specified using the `#[ethevent(...)]` attribute: +/// +/// - `name`, `name = "..."`: Overrides the generated `EthEvent` name, default is the struct's name. +/// - `signature`, `signature = "..."`: The signature as hex string to override the event's signature. +/// - `abi`, `abi = "..."`: The ABI signature for the event this event's data corresponds to. +#[proc_macro_derive(EthEvent, attributes(ethevent))] +pub fn derive_abi_event(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + let attributes = match parse_attributes(&input) { + Ok(attributes) => attributes, + Err(errors) => return TokenStream::from(errors), + }; + + let event_name = attributes + .name + .map(|(n, _)| n) + .unwrap_or_else(|| input.ident.to_string()); + + let (abi, hash) = if let Some((src, span)) = attributes.abi { + if let Ok(mut event) = parse_event(&src) { + event.name = event_name.clone(); + (event.abi_signature(), event.signature()) + } else { + match src.parse::().and_then(|s| s.get()) { + Ok(abi) => { + // try to derive the signature from the abi from the parsed abi + // TODO(mattsse): this will fail for events that contain other non elementary types in their abi + // because the parser doesn't know how to substitute the types + // this could be mitigated by getting the ABI of each non elementary type at runtime + // and computing the the signature as `static Lazy::...` + match parse_event(&abi) { + Ok(mut event) => { + event.name = event_name.clone(); + (event.abi_signature(), event.signature()) + } + Err(err) => { + return TokenStream::from(Error::new(span, err).to_compile_error()) + } + } + } + Err(err) => return TokenStream::from(Error::new(span, err).to_compile_error()), + } + } + } else { + // try to determine the abi from the fields + match derive_abi_event_from_fields(&input) { + Ok(mut event) => { + event.name = event_name.clone(); + (event.abi_signature(), event.signature()) + } + Err(err) => return TokenStream::from(err.to_compile_error()), + } + }; + + let signature = if let Some((hash, _)) = attributes.signature_hash { + signature(&hash) + } else { + signature(hash.as_bytes()) + }; + + let ethevent_impl = quote! { + impl ethers_contract::EthEvent for #name { + + fn name(&self) -> ::std::borrow::Cow<'static, str> { + #event_name.into() + } + + fn signature() -> ethers_core::types::H256 { + #signature + } + + fn abi_signature() -> ::std::borrow::Cow<'static, str> { + #abi.into() + } + } + }; + + let tokenize_impl = derive_tokenizeable_impl(&input); + + // parse attributes abi into source + TokenStream::from(quote! { + #tokenize_impl + #ethevent_impl + }) +} + +fn derive_abi_event_from_fields(input: &DeriveInput) -> Result { + let types: Vec<_> = match input.data { + Data::Struct(ref data) => match data.fields { + Fields::Named(ref fields) => fields.named.iter().map(|f| &f.ty).collect(), + Fields::Unnamed(ref fields) => fields.unnamed.iter().map(|f| &f.ty).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 = types + .iter() + .map(|ty| find_parameter_type(ty)) + .collect::, _>>()?; + + let event = Event { + name: "".to_string(), + inputs: inputs + .into_iter() + .map(|kind| EventParam { + name: "".to_string(), + kind, + indexed: false, + }) + .collect(), + anonymous: false, + }; + Ok(event) +} + +fn find_parameter_type(ty: &Type) -> Result { + match ty { + Type::Array(ty) => { + let param = find_parameter_type(ty.elem.as_ref())?; + if let Expr::Lit(ref expr) = ty.len { + if let Lit::Int(ref len) = expr.lit { + if let Ok(size) = len.base10_parse::() { + return Ok(ParamType::FixedArray(Box::new(param), size)); + } + } + } + Err(Error::new( + ty.span(), + "Failed to derive proper ABI from array field", + )) + } + Type::Path(ty) => { + if let Some(ident) = ty.path.get_ident() { + return match ident.to_string().to_lowercase().as_str() { + "address" => Ok(ParamType::Address), + "string" => Ok(ParamType::String), + "bool" => Ok(ParamType::Bool), + "int" | "uint" => Ok(ParamType::Uint(256)), + "h160" => Ok(ParamType::FixedBytes(20)), + "h256" | "secret" | "hash" => Ok(ParamType::FixedBytes(32)), + "h512" | "public" => Ok(ParamType::FixedBytes(64)), + s => parse_int_param_type(s).ok_or_else(|| { + Error::new(ty.span(), "Failed to derive proper ABI from fields") + }), + }; + } + // check for `Vec` + if ty.path.segments.len() == 1 && ty.path.segments[0].ident == "Vec" { + if let PathArguments::AngleBracketed(ref args) = ty.path.segments[0].arguments { + if args.args.len() == 1 { + if let GenericArgument::Type(ref ty) = args.args.iter().next().unwrap() { + let kind = find_parameter_type(ty)?; + return Ok(ParamType::Array(Box::new(kind))); + } + } + } + } + + Err(Error::new( + ty.span(), + "Failed to derive proper ABI from fields", + )) + } + Type::Tuple(ty) => { + let params = ty + .elems + .iter() + .map(|t| find_parameter_type(t)) + .collect::, _>>()?; + Ok(ParamType::Tuple(params)) + } + _ => { + eprintln!("Found other types"); + Err(Error::new( + ty.span(), + "Failed to derive proper ABI from fields", + )) + } + } +} + +fn parse_int_param_type(s: &str) -> Option { + let size = s + .chars() + .skip(1) + .collect::() + .parse::() + .ok()?; + if s.starts_with('u') { + Some(ParamType::Uint(size)) + } else if s.starts_with('i') { + Some(ParamType::Int(size)) + } else { + None + } +} + +fn signature(hash: &[u8]) -> proc_macro2::TokenStream { + let bytes = hash.iter().copied().map(Literal::u8_unsuffixed); + quote! {ethers_core::types::H256([#( #bytes ),*])} +} + +fn parse_event(abi: &str) -> Result { + let abi = if !abi.trim_start().starts_with("event ") { + format!("event {}", abi) + } else { + abi.to_string() + }; + AbiParser::default() + .parse_event(&abi) + .map_err(|err| format!("Failed to parse the event ABI: {:?}", err)) +} + +/// Derives the `Tokenizable` trait for the labeled type. +/// +/// This derive macro automatically adds a type bound `field: Tokenizable` for each +/// field type. +#[proc_macro_derive(EthAbiType)] +pub fn derive_abi_type(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + TokenStream::from(derive_tokenizeable_impl(&input)) +} + +fn derive_tokenizeable_impl(input: &DeriveInput) -> proc_macro2::TokenStream { + let name = &input.ident; + let generic_params = input.generics.params.iter().map(|p| quote! { #p }); + let generic_params = quote! { #(#generic_params,)* }; + + let generic_args = input.generics.type_params().map(|p| { + let name = &p.ident; + quote_spanned! { p.ident.span() => #name } + }); + + let generic_args = quote! { #(#generic_args,)* }; + + let generic_predicates = match input.generics.where_clause { + Some(ref clause) => { + let predicates = clause.predicates.iter().map(|p| quote! { #p }); + quote! { #(#predicates,)* } + } + None => quote! {}, + }; + + let (tokenize_predicates, params_len, init_struct_impl, into_token_impl) = match input.data { + Data::Struct(ref data) => match data.fields { + Fields::Named(ref fields) => { + let tokenize_predicates = fields.named.iter().map(|f| { + let ty = &f.ty; + quote_spanned! { f.span() => #ty: ethers_core::abi::Tokenize } + }); + let tokenize_predicates = quote! { #(#tokenize_predicates,)* }; + + let assignments = fields.named.iter().map(|f| { + let name = f.ident.as_ref().expect("Named fields have names"); + quote_spanned! { f.span() => #name: ethers_core::abi::Tokenizable::from_token(iter.next().expect("tokens size is sufficient qed").into_token())? } + }); + let init_struct_impl = quote! { Self { #(#assignments,)* } }; + + let into_token = fields.named.iter().map(|f| { + let name = f.ident.as_ref().expect("Named fields have names"); + quote_spanned! { f.span() => self.#name.into_token() } + }); + let into_token_impl = quote! { #(#into_token,)* }; + + ( + tokenize_predicates, + fields.named.len(), + init_struct_impl, + into_token_impl, + ) + } + Fields::Unnamed(ref fields) => { + let tokenize_predicates = fields.unnamed.iter().map(|f| { + let ty = &f.ty; + quote_spanned! { f.span() => #ty: ethers_core::abi::Tokenize } + }); + let tokenize_predicates = quote! { #(#tokenize_predicates,)* }; + + let assignments = fields.unnamed.iter().map(|f| { + quote_spanned! { f.span() => ethers_core::abi::Tokenizable::from_token(iter.next().expect("tokens size is sufficient qed").into_token())? } + }); + let init_struct_impl = quote! { Self(#(#assignments,)* ) }; + + let into_token = fields.unnamed.iter().enumerate().map(|(i, f)| { + let idx = syn::Index::from(i); + quote_spanned! { f.span() => self.#idx.into_token() } + }); + let into_token_impl = quote! { #(#into_token,)* }; + + ( + tokenize_predicates, + fields.unnamed.len(), + init_struct_impl, + into_token_impl, + ) + } + Fields::Unit => { + return Error::new( + input.span(), + "EthAbiType cannot be derived for empty structs and unit", + ) + .to_compile_error(); + } + }, + Data::Enum(_) => { + return Error::new(input.span(), "EthAbiType cannot be derived for enums") + .to_compile_error(); + } + Data::Union(_) => { + return Error::new(input.span(), "EthAbiType cannot be derived for unions") + .to_compile_error(); + } + }; + + quote! { + impl<#generic_params> ethers_core::abi::Tokenizable for #name<#generic_args> + where + #generic_predicates + #tokenize_predicates + { + + fn from_token(token: ethers_core::abi::Token) -> Result where + Self: Sized { + if let ethers_core::abi::Token::Tuple(tokens) = token { + if tokens.len() != #params_len { + return Err(ethers_core::abi::InvalidOutputType(format!( + "Expected {} tokens, got {}: {:?}", + #params_len, + tokens.len(), + tokens + ))); + } + + let mut iter = tokens.into_iter(); + + Ok(#init_struct_impl) + } else { + Err(ethers_core::abi::InvalidOutputType(format!( + "Expected Tuple, got {:?}", + token + ))) + } + } + + fn into_token(self) -> ethers_core::abi::Token { + ethers_core::abi::Token::Tuple( + vec![ + #into_token_impl + ] + ) + } + } + } +} + +struct Attributes { + name: Option<(String, Span)>, + abi: Option<(String, Span)>, + signature_hash: Option<(Vec, Span)>, +} + +impl Default for Attributes { + fn default() -> Self { + Self { + name: None, + abi: None, + signature_hash: None, + } + } +} + +fn parse_attributes(input: &DeriveInput) -> Result { + let mut result = Attributes::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("ethevent") { + for n in meta.nested.iter() { + if let NestedMeta::Meta(meta) = n { + match meta { + Meta::Path(path) => { + return Err(Error::new( + path.span(), + "unrecognized ethevent parameter", + ) + .to_compile_error()); + } + Meta::List(meta) => { + // TODO support raw list + return Err(Error::new( + meta.path.span(), + "unrecognized ethevent 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 if meta.path.is_ident("signature") { + if let Lit::Str(ref lit_str) = meta.lit { + if result.signature_hash.is_none() { + match Vec::from_hex(lit_str.value()) { + Ok(sig) => { + result.signature_hash = + Some((sig, lit_str.span())) + } + Err(err) => { + return Err(Error::new( + meta.span(), + format!( + "Expected hex signature: {:?}", + err + ), + ) + .to_compile_error()); + } + } + } else { + return Err(Error::new( + meta.span(), + "signature already specified", + ) + .to_compile_error()); + } + } else { + return Err(Error::new( + meta.span(), + "signature must be a hex string", + ) + .to_compile_error()); + } + } else { + return Err(Error::new( + meta.span(), + "unrecognized ethevent parameter", + ) + .to_compile_error()); + } + } + } + } + } + } + } + } + } + Ok(result) +} diff --git a/ethers-contract/src/event.rs b/ethers-contract/src/event.rs index 15a72116..73493fd7 100644 --- a/ethers-contract/src/event.rs +++ b/ethers-contract/src/event.rs @@ -5,8 +5,24 @@ use ethers_core::{ types::{BlockNumber, Filter, Log, TxHash, ValueOrArray, H256, U64}, }; use ethers_providers::{FilterWatcher, Middleware, PubsubClient, SubscriptionStream}; +use std::borrow::Cow; 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>; + + /// Retrieves the signature for the event this data corresponds to. + /// This signature is the Keccak-256 hash of the ABI signature of + /// this event. + fn signature() -> H256; + + /// Retrieves the ABI signature for the event this data corresponds + /// to. + fn abi_signature() -> Cow<'static, str>; +} + /// Helper for managing the event filter before querying or streaming its logs #[derive(Debug)] #[must_use = "event filters do nothing unless you `query` or `stream` them"] diff --git a/ethers-contract/src/lib.rs b/ethers-contract/src/lib.rs index fe2f0fa8..84a8c87d 100644 --- a/ethers-contract/src/lib.rs +++ b/ethers-contract/src/lib.rs @@ -26,6 +26,7 @@ mod factory; pub use factory::ContractFactory; mod event; +pub use event::EthEvent; mod stream; @@ -46,7 +47,7 @@ pub use ethers_contract_abigen::Abigen; #[cfg(feature = "abigen")] #[cfg_attr(docsrs, doc(cfg(feature = "abigen")))] -pub use ethers_contract_derive::abigen; +pub use ethers_contract_derive::{abigen, EthAbiType, EthEvent}; // Hide the Lazy re-export, it's just for convenience #[doc(hidden)] diff --git a/ethers-contract/tests/common/derive.rs b/ethers-contract/tests/common/derive.rs new file mode 100644 index 00000000..4f36b505 --- /dev/null +++ b/ethers-contract/tests/common/derive.rs @@ -0,0 +1,183 @@ +use ethers::core::types::{H160, H256, I256, U128, U256}; +use ethers_contract::{EthAbiType, EthEvent}; +use ethers_core::abi::Tokenizable; +use ethers_core::types::Address; + +#[derive(Debug, Clone, PartialEq, EthAbiType)] +struct ValueChanged { + old_author: Address, + new_author: Address, + old_value: String, + new_value: String, +} + +#[derive(Debug, Clone, PartialEq, EthAbiType)] +struct ValueChangedWrapper { + inner: ValueChanged, + msg: String, +} + +#[derive(Debug, Clone, PartialEq, EthAbiType)] +struct ValueChangedTuple(Address, Address, String, String); + +#[derive(Debug, Clone, PartialEq, EthAbiType)] +struct ValueChangedTupleWrapper(ValueChangedTuple, String); + +#[test] +fn can_detokenize_struct() { + let value = ValueChanged { + old_author: "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap(), + new_author: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".parse().unwrap(), + old_value: "50".to_string(), + new_value: "100".to_string(), + }; + + let token = value.clone().into_token(); + assert_eq!(value, ValueChanged::from_token(token).unwrap()); +} + +#[test] +fn can_detokenize_nested_structs() { + let value = ValueChangedWrapper { + inner: ValueChanged { + old_author: "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap(), + new_author: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".parse().unwrap(), + old_value: "50".to_string(), + new_value: "100".to_string(), + }, + msg: "hello world".to_string(), + }; + + let token = value.clone().into_token(); + assert_eq!(value, ValueChangedWrapper::from_token(token).unwrap()); +} + +#[test] +fn can_detokenize_tuple_struct() { + let value = ValueChangedTuple( + "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap(), + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".parse().unwrap(), + "50".to_string(), + "100".to_string(), + ); + + let token = value.clone().into_token(); + assert_eq!(value, ValueChangedTuple::from_token(token).unwrap()); +} + +#[test] +fn can_detokenize_nested_tuple_struct() { + let value = ValueChangedTupleWrapper( + ValueChangedTuple( + "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap(), + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".parse().unwrap(), + "50".to_string(), + "100".to_string(), + ), + "hello world".to_string(), + ); + + let token = value.clone().into_token(); + assert_eq!(value, ValueChangedTupleWrapper::from_token(token).unwrap()); +} + +#[test] +fn can_derive_eth_event() { + #[derive(Debug, Clone, PartialEq, EthEvent)] + pub struct ValueChangedEvent { + old_author: Address, + new_author: Address, + old_value: String, + 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!("ValueChangedEvent", value.name()); + assert_eq!( + "ValueChangedEvent(address,address,string,string)", + ValueChangedEvent::abi_signature() + ); + + let token = value.clone().into_token(); + assert_eq!(value, ValueChangedEvent::from_token(token).unwrap()); +} + +#[test] +fn can_set_eth_event_name_attribute() { + #[derive(Debug, PartialEq, EthEvent)] + #[ethevent(name = "MyEvent")] + pub struct ValueChangedEvent { + old_author: Address, + new_author: Address, + old_value: String, + 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(address,address,string,string)", + ValueChangedEvent::abi_signature() + ); +} + +#[test] +fn can_detect_various_event_abi_types() { + #[derive(Debug, PartialEq, EthEvent)] + struct ValueChangedEvent { + old_author: Address, + s: String, + h1: H256, + i256: I256, + u256: U256, + b: bool, + v: Vec
, + bs: Vec, + h160: H160, + u128: U128, + int8: i8, + int16: i16, + int32: i32, + int64: i64, + int128: i128, + uint8: u8, + uint16: u16, + uint32: u32, + uint64: u64, + uint128: u128, + } + + assert_eq!( + "ValueChangedEvent(address,string,bytes32,int256,uint256,bool,address[],bool[],bytes20,uint128,int8,int16,int32,int64,int128,uint8,uint16,uint32,uint64,uint128)", + ValueChangedEvent::abi_signature() + ); +} + +// #[test] +// fn can_set_eth_abi_attribute() { +// #[derive(Debug, Clone, PartialEq, EthAbiType)] +// struct SomeType { +// inner: Address, +// msg: String, +// } +// +// #[derive(Debug, PartialEq, EthEvent)] +// #[ethevent(abi = "ValueChangedEvent(address,(address,string),string)")] +// pub struct ValueChangedEvent { +// old_author: Address, +// inner: SomeType, +// new_value: String, +// } +// } diff --git a/ethers-contract/tests/common/mod.rs b/ethers-contract/tests/common/mod.rs index 065a2482..fe3f8773 100644 --- a/ethers-contract/tests/common/mod.rs +++ b/ethers-contract/tests/common/mod.rs @@ -1,17 +1,19 @@ +mod derive; use ethers_core::{ - abi::{Abi, Detokenize, InvalidOutputType, Token}, + abi::Abi, types::{Address, Bytes}, }; -use ethers_contract::{Contract, ContractFactory}; +use ethers_contract::{Contract, ContractFactory, EthAbiType}; use ethers_core::utils::{GanacheInstance, Solc}; use ethers_middleware::signer::SignerMiddleware; use ethers_providers::{Http, Middleware, Provider}; use ethers_signers::LocalWallet; use std::{convert::TryFrom, sync::Arc, time::Duration}; -// Note: We also provide the `abigen` macro for generating these bindings automatically -#[derive(Clone, Debug)] +// Note: The `EthAbiType` derive macro implements the necessary conversion between `Tokens` and +// the struct +#[derive(Clone, Debug, EthAbiType)] pub struct ValueChanged { pub old_author: Address, pub new_author: Address, @@ -19,22 +21,6 @@ pub struct ValueChanged { pub new_value: String, } -impl Detokenize for ValueChanged { - fn from_tokens(tokens: Vec) -> Result { - let old_author: Address = tokens[1].clone().into_address().unwrap(); - let new_author: Address = tokens[1].clone().into_address().unwrap(); - let old_value = tokens[2].clone().into_string().unwrap(); - let new_value = tokens[3].clone().into_string().unwrap(); - - Ok(Self { - old_author, - new_author, - old_value, - new_value, - }) - } -} - /// compiles the given contract and returns the ABI and Bytecode pub fn compile_contract(name: &str, filename: &str) -> (Abi, Bytes) { let compiled = Solc::new(&format!("./tests/solidity-contracts/{}", filename)) diff --git a/ethers-core/src/abi/human_readable.rs b/ethers-core/src/abi/human_readable.rs index 98ec7142..19195c6d 100644 --- a/ethers-core/src/abi/human_readable.rs +++ b/ethers-core/src/abi/human_readable.rs @@ -152,7 +152,7 @@ impl AbiParser { } /// Parses a solidity event declaration from `event (args*) anonymous?` - fn parse_event(&self, s: &str) -> Result { + pub fn parse_event(&self, s: &str) -> Result { let mut event = s.trim(); if !event.starts_with("event ") { bail!("Not an event `{}`", s) @@ -237,7 +237,7 @@ impl AbiParser { }) } - fn parse_function(&mut self, s: &str) -> Result { + pub fn parse_function(&mut self, s: &str) -> Result { let mut input = s.trim(); if !input.starts_with("function ") { bail!("Not a function `{}`", input) @@ -328,7 +328,7 @@ impl AbiParser { } } - fn parse_constructor(&self, s: &str) -> Result { + pub fn parse_constructor(&self, s: &str) -> Result { let mut input = s.trim(); if !input.starts_with("constructor") { bail!("Not a constructor `{}`", input)