From 44bb02f857fb95426aa34136ce6c81613872fc4b Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Wed, 13 Oct 2021 11:53:43 +0200 Subject: [PATCH] feat: substitute overloaded functions (#501) * feat: substitute overloaded functions * chore: update changelog --- CHANGELOG.md | 1 + .../src/contract/methods.rs | 115 +++++++++++++++--- ethers-contract/tests/abigen.rs | 20 +++ 3 files changed, 120 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd991759..40308609 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Unreleased +- `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) - Use rust types as contract function inputs for human readable abi [#482](https://github.com/gakonst/ethers-rs/pull/482) - Add EIP-712 `sign_typed_data` signer method; add ethers-core type `Eip712` trait and derive macro in ethers-derive-eip712 [#481](https://github.com/gakonst/ethers-rs/pull/481) diff --git a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs index f06c4017..4dceb244 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs @@ -1,21 +1,25 @@ -use super::{types, util, Context}; +use std::collections::BTreeMap; + use anyhow::{Context as _, Result}; +use inflector::Inflector; +use proc_macro2::{Literal, TokenStream}; +use quote::quote; +use syn::Ident; + use ethers_core::abi::ParamType; use ethers_core::{ abi::{Function, FunctionExt, Param}, types::Selector, }; -use inflector::Inflector; -use proc_macro2::{Literal, TokenStream}; -use quote::quote; -use std::collections::BTreeMap; -use syn::Ident; + +use super::{types, util, Context}; /// Expands a context into a method struct containing all the generated bindings /// to the Solidity contract methods. impl Context { + /// Expands all method implementations pub(crate) fn methods(&self) -> Result { - let mut aliases = self.method_aliases.clone(); + let mut aliases = self.get_method_aliases()?; let sorted_functions: BTreeMap<_, _> = self.abi.functions.clone().into_iter().collect(); let functions = sorted_functions .values() @@ -43,8 +47,10 @@ impl Context { 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,))` - // this is currently necessary because internally `flatten_tokens` is called which removes the outermost `tuple` level - // and since `((#name))` is not a rust tuple it doesn't get wrapped into another tuple that will be peeled off by `flatten_tokens` + // this is currently necessary because internally `flatten_tokens` is called which + // removes the outermost `tuple` level and since `((#name))` is not + // a rust tuple it doesn't get wrapped into another tuple that will be peeled off by + // `flatten_tokens` ParamType::Tuple(_) if fun.inputs.len() == 1 => { // make sure the tuple gets converted to `Token::Tuple` quote! {(#name,)} @@ -97,7 +103,7 @@ impl Context { } } - #[allow(unused)] + /// Expands a single function with the given alias fn expand_function(&self, function: &Function, alias: Option) -> Result { let name = alias.unwrap_or_else(|| util::safe_ident(&function.name.to_snake_case())); let selector = expand_selector(function.selector()); @@ -105,8 +111,8 @@ 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_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 }; @@ -127,6 +133,79 @@ impl Context { } }) } + + /// Returns the method aliases, either configured by the user or determined + /// based on overloaded functions. + /// + /// In case of overloaded functions we would follow rust's general + /// convention of suffixing the function name with _with + // The first function or the function with the least amount of arguments should + // be named as in the ABI, the following functions suffixed with _with_ + + // additional_params[0].name + (_and_(additional_params[1+i].name))* + fn get_method_aliases(&self) -> Result> { + let mut aliases = self.method_aliases.clone(); + // find all duplicates, where no aliases where provided + for functions in self.abi.functions.values() { + if functions + .iter() + .filter(|f| !aliases.contains_key(&f.abi_signature())) + .count() + <= 1 + { + // no conflicts + continue; + } + + // sort functions by number of inputs asc + let mut functions = functions.iter().collect::>(); + functions.sort_by(|f1, f2| f1.inputs.len().cmp(&f2.inputs.len())); + let prev = functions[0]; + for duplicate in functions.into_iter().skip(1) { + // attempt to find diff in the input arguments + let diff = duplicate + .inputs + .iter() + .filter(|i1| prev.inputs.iter().all(|i2| *i1 != i2)) + .collect::>(); + + let alias = match diff.len() { + 0 => { + // this should not happen since functions with same name and input are + // illegal + anyhow::bail!( + "Function with same name and parameter types defined twice: {}", + duplicate.name + ); + } + 1 => { + // single additional input params + format!( + "{}_with_{}", + duplicate.name.to_snake_case(), + diff[0].name.to_snake_case() + ) + } + _ => { + // 1 + n additional input params + let and = diff + .iter() + .skip(1) + .map(|i| i.name.to_snake_case()) + .collect::>() + .join("_and_"); + format!( + "{}_with_{}_and_{}", + duplicate.name.to_snake_case(), + diff[0].name.to_snake_case(), + and + ) + } + }; + aliases.insert(duplicate.abi_signature(), util::safe_ident(&alias)); + } + } + Ok(aliases) + } } fn expand_fn_outputs(outputs: &[Param]) -> Result { @@ -150,9 +229,10 @@ fn expand_selector(selector: Selector) -> TokenStream { #[cfg(test)] mod tests { - use super::*; use ethers_core::abi::ParamType; + use super::*; + // packs the argument in a tuple to be used for the contract call fn expand_inputs_call_arg(inputs: &[Param]) -> TokenStream { let names = inputs @@ -162,9 +242,12 @@ mod tests { let name = util::expand_input_name(i, ¶m.name); 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,))` - // this is currently necessary because internally `flatten_tokens` is called which removes the outermost `tuple` level - // and since `((#name))` is not a rust tuple it doesn't get wrapped into another tuple that will be peeled off by `flatten_tokens` + // we need to force this argument into a tuple so it gets expanded to + // `((#name,))` this is currently necessary because + // internally `flatten_tokens` is called which removes the outermost `tuple` + // level and since `((#name))` is not a rust tuple it + // doesn't get wrapped into another tuple that will be peeled off by + // `flatten_tokens` ParamType::Tuple(_) if inputs.len() == 1 => { // make sure the tuple gets converted to `Token::Tuple` quote! {(#name,)} diff --git a/ethers-contract/tests/abigen.rs b/ethers-contract/tests/abigen.rs index c41f0f09..4bc51a13 100644 --- a/ethers-contract/tests/abigen.rs +++ b/ethers-contract/tests/abigen.rs @@ -173,3 +173,23 @@ fn can_gen_human_readable_with_structs() { let f = Foo { x: 100u64.into() }; let _ = contract.foo(f); } + +#[test] +fn can_handle_overloaded_functions() { + abigen!( + SimpleContract, + r#"[ + getValue() (uint256) + getValue(uint256 otherValue) (uint256) + getValue(uint256 otherValue, address addr) (uint256) + ]"# + ); + + let (provider, _) = Provider::mocked(); + let client = Arc::new(provider); + let contract = SimpleContract::new(Address::zero(), client); + // ensure both functions are callable + 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()); +}