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 <me@gakonst.com>

* style: reindent macro body

* test: add tuple event test

Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
This commit is contained in:
Matthias Seitz 2021-10-18 12:28:38 +02:00 committed by GitHub
parent 071a41605b
commit fb4d9a9ef1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 950 additions and 134 deletions

View File

@ -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)

View File

@ -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,
})
}

View File

@ -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<TokenStream> {
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::<Vec<_>>();
@ -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<Self, #ethers_core::abi::InvalidOutputType> 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<Self, #ethers_core::abi::Error>
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<Vec<(TokenStream, TokenStream, bool)>> {
/// Expands the name-type pairs for the given inputs
fn expand_event_params(&self, event: &Event) -> Result<Vec<(TokenStream, TokenStream, bool)>> {
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, &params)
};
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::<Vec<_>>();
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, &params);
@ -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, &params);
@ -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, &params);
@ -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, &params);

View File

@ -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<TokenStream> {
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::<Result<Vec<_>>>()?;
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<TokenStream> {
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<String, Ident>) -> Result<TokenStream> {
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<Self, #ethers_contract::AbiError> {
#(
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<Vec<(TokenStream, TokenStream)>> {
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, &param.name);
for (idx, param) in fun.inputs.iter().enumerate() {
let name = util::expand_input_name(idx, &param.name);
let ty = self.expand_input_param(fun, &param.name, &param.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<TokenStream> {
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, &param.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<M, #outputs> };
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::<Vec<_>>();
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::<Vec<_>>();
quote! { struct #name { #( #fields, )* } }
}
#[cfg(test)]
mod tests {
use ethers_core::abi::ParamType;

View File

@ -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! {

View File

@ -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<S>(address_str: S) -> Result<Address>
where

View File

@ -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<Self, #ethers_core::abi::InvalidOutputType> 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<Item = &'a Variant> + 'a,
) -> Result<TokenStream, Error> {
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<Self, #ethers_core::abi::InvalidOutputType> 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 { }
})
}

View File

@ -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<Self, #contract_crate::AbiError> {
#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] != <Self as #contract_crate::EthCall>::selector() {
return Err(#contract_crate::AbiError::WrongSelector);
}
#data_types_init
let data_tokens = #core_crate::abi::decode(&data_types, &bytes[4..])?;
Ok(<Self as #core_crate::abi::Tokenizable>::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<Function, Error> {
#[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<EthCallAttributes, TokenStream> {
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<Function, String> {
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))
}

View File

@ -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<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(),
"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::<Result<Vec<_>, _>>()?;
let event = Event {
name: "".to_string(),
inputs: inputs
inputs: utils::derive_abi_inputs_from_fields(input, "EthEvent")?
.into_iter()
.map(|(name, kind)| EventParam {
name,

View File

@ -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` (`<name>(<args>,...)`) the `<name`>
/// must match either the struct name or the name attribute: `#[ethcall(name ="<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))
}

View File

@ -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<ParamType> {
let size = s
@ -158,3 +166,49 @@ pub fn find_parameter_type(ty: &Type) -> Result<ParamType, Error> {
)),
}
}
/// Attempts to determine the ABI Paramtypes from the type's AST
pub fn derive_abi_inputs_from_fields(
input: &DeriveInput,
trait_name: &str,
) -> Result<Vec<(String, ParamType)>, 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()
}

View File

@ -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<T: EthCall> AbiEncode for T {
fn encode(self) -> Result<Bytes, AbiError> {
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<M: Middleware> {

View File

@ -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<Bytes, AbiError>;
}
/// Trait for ABI decoding
pub trait AbiDecode: Sized {
/// Decodes the ABI encoded data
fn decode(bytes: impl AsRef<[u8]>) -> Result<Self, AbiError>;
}

View File

@ -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)]

View File

@ -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());
}

View File

@ -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<T: Tokenizable>() {}
fn assert_ethcall<T: 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::<Call>();
assert_tokenizeable::<Call2>();
assert_tokenizeable::<Call3>();
}
#[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 = <OneParam as EthLogDecode>::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<u8>,
}
assert_tokenizeable::<MyStruct>();
assert_ethcall::<MyStruct>();
#[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::<MyCall>();
assert_ethcall::<MyCall>();
}
#[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::<FooCall>();
assert_ethcall::<FooCall>();
}

View File

@ -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<Vec<(Param, Option<String>)>> {

View File

@ -59,9 +59,7 @@ fn twos_complement(u: U256) -> U256 {
fn handle_overflow<T>((result, overflow): (T, bool)) -> T {
#[cfg(debug_assertions)]
{
if overflow {
panic!("overflow");
}
assert!(!overflow, "overflow");
}
let _ = overflow;

View File

@ -483,7 +483,7 @@ pub fn find_parameter_type(ty: &Type) -> Result<ParamType, TokenStream> {
let params = ty
.elems
.iter()
.map(|t| find_parameter_type(t))
.map(find_parameter_type)
.collect::<Result<Vec<_>, _>>()?;
Ok(ParamType::Tuple(params))
}