From 245c57a06ed8da0b819e336446bef6ef57d9c58f Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Wed, 17 Mar 2021 19:33:36 +0100 Subject: [PATCH] feat: extend EthEvent with decode_log method and support indexed proc macro attributes --- .../ethers-contract-derive/src/lib.rs | 439 ++++++++++++++++-- ethers-contract/src/event.rs | 7 +- 2 files changed, 400 insertions(+), 46 deletions(-) diff --git a/ethers-contract/ethers-contract-derive/src/lib.rs b/ethers-contract/ethers-contract-derive/src/lib.rs index 03f34ce2..9430e569 100644 --- a/ethers-contract/ethers-contract-derive/src/lib.rs +++ b/ethers-contract/ethers-contract-derive/src/lib.rs @@ -8,8 +8,8 @@ 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, + parse::Error, parse_macro_input, AttrStyle, Data, DeriveInput, Expr, Field, Fields, + GenericArgument, Lit, Meta, NestedMeta, PathArguments, Type, }; use abigen::{expand, ContractArgs}; @@ -113,14 +113,13 @@ pub fn derive_abi_event(input: TokenStream) -> TokenStream { let event_name = attributes .name - .map(|(n, _)| n) + .map(|(s, _)| s) .unwrap_or_else(|| input.ident.to_string()); - let (abi, hash) = if let Some((src, span)) = attributes.abi { + let mut event = if let Some((src, span)) = attributes.abi { // try to parse as solidity event - if let Ok(mut event) = parse_event(&src) { - event.name = event_name.clone(); - (event.abi_signature(), event.signature()) + if let Ok(event) = parse_event(&src) { + event } else { // try as tuple if let Some(inputs) = Reader::read( @@ -142,12 +141,11 @@ pub fn derive_abi_event(input: TokenStream) -> TokenStream { ), _ => None, }) { - let event = Event { + Event { name: event_name.clone(), inputs, anonymous: false, - }; - (event.abi_signature(), event.signature()) + } } else { match src.parse::().and_then(|s| s.get()) { Ok(abi) => { @@ -157,10 +155,7 @@ pub fn derive_abi_event(input: TokenStream) -> TokenStream { // 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()) - } + Ok(event) => event, Err(err) => { return TokenStream::from(Error::new(span, err).to_compile_error()) } @@ -173,14 +168,23 @@ pub fn derive_abi_event(input: TokenStream) -> TokenStream { } 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()) - } + Ok(event) => event, Err(err) => return TokenStream::from(err.to_compile_error()), } }; + event.name = event_name.clone(); + if let Some((anon, _)) = attributes.anonymous.as_ref() { + event.anonymous = *anon; + } + + let decode_log_impl = match derive_decode_from_log_impl(&input, &event) { + Ok(log) => log, + Err(err) => return TokenStream::from(err.to_compile_error()), + }; + + let (abi, hash) = (event.abi_signature(), event.signature()); + let signature = if let Some((hash, _)) = attributes.signature_hash { signature(&hash) } else { @@ -201,6 +205,11 @@ pub fn derive_abi_event(input: TokenStream) -> TokenStream { fn abi_signature() -> ::std::borrow::Cow<'static, str> { #abi.into() } + + fn decode_log(log: ethers::abi::RawLog) -> Result where Self: Sized { + #decode_log_impl + } + } }; @@ -213,11 +222,268 @@ pub fn derive_abi_event(input: TokenStream) -> TokenStream { }) } -fn derive_abi_event_from_fields(input: &DeriveInput) -> Result { - let types: Vec<_> = match input.data { +struct EventField { + topic_name: Option, + index: usize, + param: EventParam, +} + +impl EventField { + fn is_indexed(&self) -> bool { + self.topic_name.is_some() + } +} + +// Converts param types for indexed parameters to bytes32 where appropriate +// This applies to strings, arrays, structs and bytes to follow the encoding of +// these indexed param types according to +// https://solidity.readthedocs.io/en/develop/abi-spec.html#encoding-of-indexed-event-parameters +fn topic_param_type_quote(kind: &ParamType) -> proc_macro2::TokenStream { + match kind { + ParamType::String + | ParamType::Bytes + | ParamType::Array(_) + | ParamType::FixedArray(_, _) + | ParamType::Tuple(_) => quote! {ethers::abi::ParamType::FixedBytes(32)}, + ty => param_type_quote(ty), + } +} + +fn param_type_quote(kind: &ParamType) -> proc_macro2::TokenStream { + match kind { + ParamType::Address => { + quote! {ethers::abi::ParamType::Address} + } + ParamType::Bytes => { + quote! {ethers::abi::ParamType::Bytes} + } + ParamType::Int(size) => { + let size = Literal::usize_suffixed(*size); + quote! {ethers::abi::ParamType::Int(#size)} + } + ParamType::Uint(size) => { + let size = Literal::usize_suffixed(*size); + quote! {ethers::abi::ParamType::Uint(#size)} + } + ParamType::Bool => { + quote! {ethers::abi::ParamType::Bool} + } + ParamType::String => { + quote! {ethers::abi::ParamType::String} + } + ParamType::Array(ty) => { + let ty = param_type_quote(&*ty); + quote! {ethers::abi::ParamType::Array(Box::new(#ty))} + } + ParamType::FixedBytes(size) => { + let size = Literal::usize_suffixed(*size); + quote! {ethers::abi::ParamType::FixedBytes(#size)} + } + ParamType::FixedArray(ty, size) => { + let ty = param_type_quote(&*ty); + let size = Literal::usize_suffixed(*size); + quote! {ethers::abi::ParamType::FixedArray(Box::new(#ty),#size)} + } + ParamType::Tuple(tuple) => { + let elements = tuple.iter().map(param_type_quote); + quote! { + ethers::abi::ParamType::Tuple( + vec![ + #( #elements ),* + ] + ) + } + } + } +} + +fn derive_decode_from_log_impl( + input: &DeriveInput, + event: &Event, +) -> Result { + let fields: 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::Named(ref fields) => { + if fields.named.len() != event.inputs.len() { + return Err(Error::new( + fields.span(), + format!( + "EthEvent {}'s fields length don't match with signature inputs {}", + event.name, + event.abi_signature() + ), + )); + } + fields.named.iter().collect() + } + Fields::Unnamed(ref fields) => { + if fields.unnamed.len() != event.inputs.len() { + return Err(Error::new( + fields.span(), + format!( + "EthEvent {}'s fields length don't match with signature inputs {}", + event.name, + event.abi_signature() + ), + )); + } + 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 mut event_fields = Vec::with_capacity(fields.len()); + for (index, field) in fields.iter().enumerate() { + let mut param = event.inputs[index].clone(); + + let (topic_name, indexed) = parse_field_attributes(field)?; + if indexed { + param.indexed = true; + } + let topic_name = if param.indexed { + if topic_name.is_none() { + Some(param.name.clone()) + } else { + topic_name + } + } else { + None + }; + + if param.indexed { + if let Some(name) = topic_name.as_ref() { + if name.is_empty() { + return Err(Error::new(field.span(), "EthEvent field requires a name")); + } + } + } + + event_fields.push(EventField { + topic_name, + index, + param, + }); + } + + // convert fields to params list + let topic_types = event_fields + .iter() + .filter(|f| f.is_indexed()) + .map(|f| topic_param_type_quote(&f.param.kind)); + + let topic_types_init = quote! {let topic_types = vec![#( #topic_types ),*];}; + + let data_types = event_fields + .iter() + .filter(|f| !f.is_indexed()) + .map(|f| param_type_quote(&f.param.kind)); + + let data_types_init = quote! {let data_types = vec![#( #data_types ),*];}; + + // decode + let (signature_check, flat_topics_init, topic_tokens_len_check) = if event.anonymous { + ( + quote! {}, + quote! { + let flat_topics = topics.into_iter().flat_map(|t| t.as_ref().to_vec()).collect::>(); + }, + quote! { + if topic_tokens.len() != topics_len { + return Err(ethers::abi::Error::InvalidData); + } + }, + ) + } else { + ( + quote! { + let event_signature = topics.get(0).ok_or(ethers::abi::Error::InvalidData)?; + if event_signature != &Self::signature() { + return Err(ethers::abi::Error::InvalidData); + } + }, + quote! { + let flat_topics = topics.into_iter().skip(1).flat_map(|t| t.as_ref().to_vec()).collect::>(); + }, + quote! { + if topic_tokens.is_empty() || topic_tokens.len() != topics_len - 1 { + return Err(ethers::abi::Error::InvalidData); + } + }, + ) + }; + + // check if indexed are sorted + let tokens_init = if event_fields + .iter() + .filter(|f| f.is_indexed()) + .enumerate() + .all(|(idx, f)| f.index == idx) + { + quote! { + let topic_tokens = ethers::abi::decode(&topic_types, &flat_topics)?; + #topic_tokens_len_check + let data_tokens = ethers::abi::decode(&data_types, &data)?; + let tokens:Vec<_> = topic_tokens.into_iter().chain(data_tokens.into_iter()).collect(); + } + } else { + let swap_tokens = event_fields.iter().map(|field| { + if field.is_indexed() { + quote! { topic_tokens.remove(0) } + } else { + quote! { data_tokens.remove(0) } + } + }); + + quote! { + let mut topic_tokens = ethers::abi::decode(&topic_types, &flat_topics)?; + #topic_tokens_len_check + let mut data_tokens = ethers::abi::decode(&data_types, &data)?; + let mut tokens = Vec::with_capacity(topics_len + data_tokens.len()); + #( tokens.push(#swap_tokens); )* + } + }; + + Ok(quote! { + + let ethers::abi::RawLog {data, topics} = log; + let topics_len = topics.len(); + + #signature_check + + #topic_types_init + #data_types_init + + #flat_topics_init + + #tokens_init + + ethers::abi::Detokenize::from_tokens(tokens).map_err(|_|ethers::abi::Error::InvalidData) + }) +} + +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(), @@ -239,17 +505,24 @@ fn derive_abi_event_from_fields(input: &DeriveInput) -> Result { } }; - let inputs = types + let inputs = fields .iter() - .map(|ty| find_parameter_type(ty)) + .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::, _>>()?; let event = Event { name: "".to_string(), inputs: inputs .into_iter() - .map(|kind| EventParam { - name: "".to_string(), + .map(|(name, kind)| EventParam { + name, kind, indexed: false, }) @@ -259,6 +532,55 @@ fn derive_abi_event_from_fields(input: &DeriveInput) -> Result { Ok(event) } +fn parse_field_attributes(field: &Field) -> Result<(Option, bool), Error> { + let mut indexed = false; + let mut topic_name = None; + for a in field.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) => { + if path.is_ident("indexed") { + indexed = true; + } else { + return Err(Error::new( + path.span(), + "unrecognized ethevent parameter", + )); + } + } + Meta::List(meta) => { + return Err(Error::new( + meta.path.span(), + "unrecognized ethevent parameter", + )); + } + Meta::NameValue(meta) => { + if meta.path.is_ident("name") { + if let Lit::Str(ref lit_str) = meta.lit { + topic_name = Some(lit_str.value()); + } else { + return Err(Error::new( + meta.span(), + "name attribute must be a string", + )); + } + } + } + } + } + } + } + } + } + } + + Ok((topic_name, indexed)) +} + fn find_parameter_type(ty: &Type) -> Result { match ty { Type::Array(ty) => { @@ -315,13 +637,10 @@ fn find_parameter_type(ty: &Type) -> Result { .collect::, _>>()?; Ok(ParamType::Tuple(params)) } - _ => { - eprintln!("Found other types"); - Err(Error::new( - ty.span(), - "Failed to derive proper ABI from fields", - )) - } + _ => Err(Error::new( + ty.span(), + "Failed to derive proper ABI from fields", + )), } } @@ -496,23 +815,21 @@ fn derive_tokenizeable_impl(input: &DeriveInput) -> proc_macro2::TokenStream { ) } } + + impl<#generic_params> ethers_core::abi::TokenizableItem for #name<#generic_args> + where + #generic_predicates + #tokenize_predicates + { } } } +#[derive(Default)] 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, - } - } + anonymous: Option<(bool, Span)>, } fn parse_attributes(input: &DeriveInput) -> Result { @@ -525,6 +842,20 @@ fn parse_attributes(input: &DeriveInput) -> Result { + if let Some(name) = path.get_ident() { + if *name.to_string() == "anonymous" { + if result.anonymous.is_none() { + result.anonymous = Some((true, name.span())); + continue; + } else { + return Err(Error::new( + name.span(), + "anonymous already specified", + ) + .to_compile_error()); + } + } + } return Err(Error::new( path.span(), "unrecognized ethevent parameter", @@ -532,7 +863,6 @@ fn parse_attributes(input: &DeriveInput) -> Result { - // TODO support raw list return Err(Error::new( meta.path.span(), "unrecognized ethevent parameter", @@ -540,7 +870,26 @@ fn parse_attributes(input: &DeriveInput) -> Result { - if meta.path.is_ident("name") { + if meta.path.is_ident("anonymous") { + if let Lit::Bool(ref bool_lit) = meta.lit { + if result.anonymous.is_none() { + result.anonymous = + Some((bool_lit.value, bool_lit.span())); + } else { + return Err(Error::new( + meta.span(), + "anonymous 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("name") { if let Lit::Str(ref lit_str) = meta.lit { if result.name.is_none() { result.name = diff --git a/ethers-contract/src/event.rs b/ethers-contract/src/event.rs index 2c4abf69..f9af1dc1 100644 --- a/ethers-contract/src/event.rs +++ b/ethers-contract/src/event.rs @@ -1,7 +1,7 @@ use crate::{base::decode_event, stream::EventStream, ContractError}; use ethers_core::{ - abi::{Detokenize, Event as AbiEvent}, + abi::{Detokenize, Event as AbiEvent, RawLog}, types::{BlockNumber, Filter, Log, TxHash, ValueOrArray, H256, U64}, }; use ethers_providers::{FilterWatcher, Middleware, PubsubClient, SubscriptionStream}; @@ -21,6 +21,11 @@ pub trait EthEvent: Detokenize { /// Retrieves the ABI signature for the event this data corresponds /// to. fn abi_signature() -> Cow<'static, str>; + + /// Decodes an Ethereum `RawLog` into an instance of the type. + fn decode_log(log: RawLog) -> Result + where + Self: Sized; } /// Helper for managing the event filter before querying or streaming its logs