From 2a3fcbbb4019482dbd42838e63dda8aa30eca515 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Sat, 4 Dec 2021 05:19:00 +0100 Subject: [PATCH] feat(abigen): use AbiType when parsing Function abi signature fails at compile time (#647) * refactor: improved from token impl * feat: add AbiType support * feat: no need for expect * feat: add missing abiarraytype impl * test: add struct derive test * chore: rustfmt * chore: update changelog * chore: rustfmt --- CHANGELOG.md | 2 + .../ethers-contract-derive/src/abi_ty.rs | 18 ++- .../ethers-contract-derive/src/call.rs | 107 ++++++++++++------ .../ethers-contract-derive/src/utils.rs | 87 +++++++++++++- ethers-contract/tests/abigen.rs | 29 ++++- ethers-core/src/abi/mod.rs | 7 +- 6 files changed, 208 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9884ccb..baf8eee5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -92,6 +92,8 @@ ### Unreleased +- Add AbiType implementation during EthAbiType expansion + [#647](https://github.com/gakonst/ethers-rs/pull/647) - fix Etherscan conditional HTTP support [#632](https://github.com/gakonst/ethers-rs/pull/632) - use `CARGO_MANIFEST_DIR` as root for relative paths in abigen diff --git a/ethers-contract/ethers-contract-derive/src/abi_ty.rs b/ethers-contract/ethers-contract-derive/src/abi_ty.rs index 6e153543..18b43669 100644 --- a/ethers-contract/ethers-contract-derive/src/abi_ty.rs +++ b/ethers-contract/ethers-contract-derive/src/abi_ty.rs @@ -1,5 +1,6 @@ //! Helper functions for deriving `EthAbiType` +use crate::utils; use ethers_core::macros::ethers_core_crate; use proc_macro2::{Ident, Literal, TokenStream}; use quote::{quote, quote_spanned}; @@ -38,7 +39,7 @@ pub fn derive_tokenizeable_impl(input: &DeriveInput) -> proc_macro2::TokenStream let assignments = fields.named.iter().map(|f| { let name = f.ident.as_ref().expect("Named fields have names"); - quote_spanned! { f.span() => #name: #core_crate::abi::Tokenizable::from_token(iter.next().expect("tokens size is sufficient qed").into_token())? } + quote_spanned! { f.span() => #name: #core_crate::abi::Tokenizable::from_token(iter.next().unwrap())? } }); let init_struct_impl = quote! { Self { #(#assignments,)* } }; @@ -58,7 +59,7 @@ pub fn derive_tokenizeable_impl(input: &DeriveInput) -> proc_macro2::TokenStream let tokenize_predicates = quote! { #(#tokenize_predicates,)* }; let assignments = fields.unnamed.iter().map(|f| { - quote_spanned! { f.span() => #core_crate::abi::Tokenizable::from_token(iter.next().expect("tokens size is sufficient qed").into_token())? } + quote_spanned! { f.span() => #core_crate::abi::Tokenizable::from_token(iter.next().unwrap())? } }); let init_struct_impl = quote! { Self(#(#assignments,)* ) }; @@ -131,7 +132,20 @@ pub fn derive_tokenizeable_impl(input: &DeriveInput) -> proc_macro2::TokenStream } }; + let params = match utils::derive_param_type_with_abi_type(input, "EthAbiType") { + Ok(params) => params, + Err(err) => return err.to_compile_error(), + }; quote! { + + impl<#generic_params> #core_crate::abi::AbiType for #name<#generic_args> { + fn param_type() -> #core_crate::abi::ParamType { + #params + } + } + + impl<#generic_params> #core_crate::abi::AbiArrayType for #name<#generic_args> {} + impl<#generic_params> #core_crate::abi::Tokenizable for #name<#generic_args> where #generic_predicates diff --git a/ethers-contract/ethers-contract-derive/src/call.rs b/ethers-contract/ethers-contract-derive/src/call.rs index f64ddaa5..f55b791e 100644 --- a/ethers-contract/ethers-contract-derive/src/call.rs +++ b/ethers-contract/ethers-contract-derive/src/call.rs @@ -13,11 +13,6 @@ use crate::{abi_ty, 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, @@ -56,27 +51,58 @@ pub(crate) fn derive_eth_call_impl(input: DeriveInput) -> TokenStream { state_mutability: Default::default(), } } else { - return Error::new(span, format!("Unable to determine ABI: {}", src)) - .to_compile_error() + // try to determine the abi by using its fields at runtime + return match derive_trait_impls_with_abi_type(&input, &function_call_name) { + Ok(derived) => derived, + Err(err) => { + Error::new(span, format!("Unable to determine ABI for `{}` : {}", src, err)) + .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(), + // try to determine the abi by using its fields at runtime + return match derive_trait_impls_with_abi_type(&input, &function_call_name) { + Ok(derived) => derived, + Err(err) => 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_from_function(&function); - let decode_impl = derive_decode_impl(&function); + derive_trait_impls( + &input, + &function_call_name, + quote! {#abi.into()}, + Some(selector), + decode_impl, + ) +} + +/// Generates the EthCall implementation +pub fn derive_trait_impls( + input: &DeriveInput, + function_call_name: &str, + abi_signature: TokenStream, + selector: Option, + decode_impl: TokenStream, +) -> TokenStream { + // the ethers crates to use + let core_crate = ethers_core_crate(); + let contract_crate = ethers_contract_crate(); + let struct_name = &input.ident; + + let selector = selector.unwrap_or_else(|| { + quote! { + #core_crate::utils::id(Self::abi_signature()) + } + }); let ethcall_impl = quote! { - impl #contract_crate::EthCall for #name { + impl #contract_crate::EthCall for #struct_name { fn function_name() -> ::std::borrow::Cow<'static, str> { #function_call_name.into() @@ -87,17 +113,17 @@ pub(crate) fn derive_eth_call_impl(input: DeriveInput) -> TokenStream { } fn abi_signature() -> ::std::borrow::Cow<'static, str> { - #abi.into() + #abi_signature } } - impl #core_crate::abi::AbiDecode for #name { + impl #core_crate::abi::AbiDecode for #struct_name { fn decode(bytes: impl AsRef<[u8]>) -> Result { #decode_impl } } - impl #core_crate::abi::AbiEncode for #name { + impl #core_crate::abi::AbiEncode for #struct_name { fn encode(self) -> ::std::vec::Vec { let tokens = #core_crate::abi::Tokenize::into_tokens(self); let selector = ::selector(); @@ -111,7 +137,7 @@ pub(crate) fn derive_eth_call_impl(input: DeriveInput) -> TokenStream { } }; - let tokenize_impl = abi_ty::derive_tokenizeable_impl(&input); + let tokenize_impl = abi_ty::derive_tokenizeable_impl(input); quote! { #tokenize_impl @@ -119,12 +145,23 @@ pub(crate) fn derive_eth_call_impl(input: DeriveInput) -> TokenStream { } } -fn derive_decode_impl(function: &Function) -> TokenStream { +/// Generates the decode implementation based on the function's input types +fn derive_decode_impl_from_function(function: &Function) -> TokenStream { + let datatypes = function.inputs.iter().map(|input| utils::param_type_quote(&input.kind)); + let datatypes_array = quote! {[#( #datatypes ),*]}; + derive_decode_impl(datatypes_array) +} + +/// Generates the decode implementation based on the function's runtime `AbiType` impl +fn derive_decode_impl_with_abi_type(input: &DeriveInput) -> Result { + let datatypes_array = utils::derive_abi_parameters_array(input, "EthCall")?; + Ok(derive_decode_impl(datatypes_array)) +} + +fn derive_decode_impl(datatypes_array: TokenStream) -> 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 ),*];}; + let data_types_init = quote! {let data_types = #datatypes_array;}; quote! { let bytes = bytes.as_ref(); @@ -137,20 +174,18 @@ fn derive_decode_impl(function: &Function) -> TokenStream { } } -/// Determine the function's ABI by parsing the AST -fn derive_abi_function_from_fields(input: &DeriveInput) -> Result { - #[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(), +/// Use the `AbiType` trait to determine the correct `ParamType` and signature at runtime +fn derive_trait_impls_with_abi_type( + input: &DeriveInput, + function_call_name: &str, +) -> Result { + let abi_signature = + utils::derive_abi_signature_with_abi_type(input, function_call_name, "EthCall")?; + let abi_signature = quote! { + ::std::borrow::Cow::Owned(#abi_signature) }; - Ok(function) + let decode_impl = derive_decode_impl_with_abi_type(input)?; + Ok(derive_trait_impls(input, function_call_name, abi_signature, None, decode_impl)) } /// All the attributes the `EthCall` macro supports diff --git a/ethers-contract/ethers-contract-derive/src/utils.rs b/ethers-contract/ethers-contract-derive/src/utils.rs index c755bda5..cf23bf80 100644 --- a/ethers-contract/ethers-contract-derive/src/utils.rs +++ b/ethers-contract/ethers-contract-derive/src/utils.rs @@ -1,6 +1,6 @@ use ethers_core::{abi::ParamType, macros::ethers_core_crate, types::Selector}; use proc_macro2::Literal; -use quote::quote; +use quote::{quote, quote_spanned}; use syn::{ parse::Error, spanned::Spanned as _, Data, DeriveInput, Expr, Fields, GenericArgument, Lit, PathArguments, Type, @@ -187,3 +187,88 @@ pub fn derive_abi_inputs_from_fields( }) .collect() } + +/// Use `AbiType::param_type` fo each field to construct the input types own param type +pub fn derive_param_type_with_abi_type( + input: &DeriveInput, + trait_name: &str, +) -> Result { + let core_crate = ethers_core_crate(); + let params = derive_abi_parameters_array(input, trait_name)?; + Ok(quote! { + #core_crate::abi::ParamType::Tuple(::std::vec!#params) + }) +} + +/// Use `AbiType::param_type` fo each field to construct the whole signature `(*)` as +/// `String` +pub fn derive_abi_signature_with_abi_type( + input: &DeriveInput, + function_name: &str, + trait_name: &str, +) -> Result { + let params = derive_abi_parameters_array(input, trait_name)?; + Ok(quote! { + { + let params: String = #params + .iter() + .map(|p| p.to_string()) + .collect::<::std::vec::Vec<_>>() + .join(","); + let function_name = #function_name; + format!("{}({})", function_name, params) + } + }) +} + +/// Use `AbiType::param_type` fo each field to construct the signature's parameters as runtime array +/// `[param1, param2,...]` +pub fn derive_abi_parameters_array( + input: &DeriveInput, + trait_name: &str, +) -> Result { + let core_crate = ethers_core_crate(); + + let param_types: Vec<_> = match input.data { + Data::Struct(ref data) => match data.fields { + Fields::Named(ref fields) => fields + .named + .iter() + .map(|f| { + let ty = &f.ty; + quote_spanned! { f.span() => <#ty as #core_crate::abi::AbiType>::param_type() } + }) + .collect(), + Fields::Unnamed(ref fields) => fields + .unnamed + .iter() + .map(|f| { + let ty = &f.ty; + quote_spanned! { f.span() => <#ty as #core_crate::abi::AbiType>::param_type() } + }) + .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), + )) + } + }; + + Ok(quote! { + [#( #param_types ),*] + }) +} diff --git a/ethers-contract/tests/abigen.rs b/ethers-contract/tests/abigen.rs index 8c5ee141..7358fa82 100644 --- a/ethers-contract/tests/abigen.rs +++ b/ethers-contract/tests/abigen.rs @@ -1,6 +1,6 @@ #![cfg(feature = "abigen")] //! Test cases to validate the `abigen!` macro -use ethers_contract::{abigen, EthEvent}; +use ethers_contract::{abigen, EthCall, EthEvent}; use ethers_core::{ abi::{AbiDecode, AbiEncode, Address, Tokenizable}, types::{transaction::eip2718::TypedTransaction, Eip1559TransactionRequest, U256}, @@ -375,3 +375,30 @@ fn can_handle_duplicates_with_same_name() { fn can_abigen_console_sol() { abigen!(Console, "ethers-contract/tests/solidity-contracts/console.json",); } + +#[test] +fn can_generate_nested_types() { + abigen!( + Test, + r#"[ + struct Outer {Inner inner; uint256[] arr;} + struct Inner {uint256 inner;} + function myfun(Outer calldata a) + ]"#, + ); + + assert_eq!(MyfunCall::abi_signature(), "myfun(((uint256),uint256[]))"); + + let (client, _mock) = Provider::mocked(); + let contract = Test::new(Address::default(), Arc::new(client)); + + let inner = Inner { inner: 100u64.into() }; + let a = Outer { inner, arr: vec![101u64.into()] }; + let _ = contract.myfun(a.clone()); + + let call = MyfunCall { a: a.clone() }; + let encoded_call = contract.encode("myfun", (a,)).unwrap(); + assert_eq!(encoded_call, call.clone().encode().into()); + let decoded_call = MyfunCall::decode(encoded_call.as_ref()).unwrap(); + assert_eq!(call, decoded_call); +} diff --git a/ethers-core/src/abi/mod.rs b/ethers-core/src/abi/mod.rs index cc169e40..925880b8 100644 --- a/ethers-core/src/abi/mod.rs +++ b/ethers-core/src/abi/mod.rs @@ -22,7 +22,7 @@ pub use error::{AbiError, ParseError}; mod human_readable; pub use human_readable::{parse as parse_abi, parse_str as parse_abi_str, AbiParser}; -use crate::types::{H256, H512, U128, U256, U64}; +use crate::types::{H256, H512, I256, U128, U256, U64}; /// Extension trait for `ethabi::Function`. pub trait FunctionExt { @@ -98,6 +98,8 @@ impl AbiType for [T; N] { } } +impl AbiArrayType for [T; N] {} + impl AbiType for [u8; N] { fn param_type() -> ParamType { ParamType::FixedBytes(N) @@ -138,7 +140,8 @@ impl_abi_type!( i16 => Int(16), i32 => Int(32), i64 => Int(64), - i128 => Int(128) + i128 => Int(128), + I256 => Int(256) ); macro_rules! impl_abi_type_tuple {