feat: extend EthEvent with decode_log method and support indexed proc
macro attributes
This commit is contained in:
parent
021f79cb77
commit
245c57a06e
|
@ -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::<Source>().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<Self, ethers::abi::Error> 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<Event, Error> {
|
||||
let types: Vec<_> = match input.data {
|
||||
struct EventField {
|
||||
topic_name: Option<String>,
|
||||
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<proc_macro2::TokenStream, Error> {
|
||||
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::<Vec<u8>>();
|
||||
},
|
||||
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::<Vec<u8>>();
|
||||
},
|
||||
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<Event, 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(),
|
||||
|
@ -239,17 +505,24 @@ fn derive_abi_event_from_fields(input: &DeriveInput) -> Result<Event, Error> {
|
|||
}
|
||||
};
|
||||
|
||||
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::<Result<Vec<_>, _>>()?;
|
||||
|
||||
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<Event, Error> {
|
|||
Ok(event)
|
||||
}
|
||||
|
||||
fn parse_field_attributes(field: &Field) -> Result<(Option<String>, 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<ParamType, Error> {
|
||||
match ty {
|
||||
Type::Array(ty) => {
|
||||
|
@ -315,13 +637,10 @@ fn find_parameter_type(ty: &Type) -> Result<ParamType, Error> {
|
|||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(ParamType::Tuple(params))
|
||||
}
|
||||
_ => {
|
||||
eprintln!("Found other types");
|
||||
Err(Error::new(
|
||||
_ => 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<u8>, 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<Attributes, proc_macro2::TokenStream> {
|
||||
|
@ -525,6 +842,20 @@ fn parse_attributes(input: &DeriveInput) -> Result<Attributes, proc_macro2::Toke
|
|||
if let NestedMeta::Meta(meta) = n {
|
||||
match meta {
|
||||
Meta::Path(path) => {
|
||||
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<Attributes, proc_macro2::Toke
|
|||
.to_compile_error());
|
||||
}
|
||||
Meta::List(meta) => {
|
||||
// TODO support raw list
|
||||
return Err(Error::new(
|
||||
meta.path.span(),
|
||||
"unrecognized ethevent parameter",
|
||||
|
@ -540,7 +870,26 @@ fn parse_attributes(input: &DeriveInput) -> Result<Attributes, proc_macro2::Toke
|
|||
.to_compile_error());
|
||||
}
|
||||
Meta::NameValue(meta) => {
|
||||
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 =
|
||||
|
|
|
@ -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<Self, ethers_core::abi::Error>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
/// Helper for managing the event filter before querying or streaming its logs
|
||||
|
|
Loading…
Reference in New Issue