feat: substitute overloaded functions (#501)

* feat: substitute overloaded functions

* chore: update changelog
This commit is contained in:
Matthias Seitz 2021-10-13 11:53:43 +02:00 committed by GitHub
parent ea8551da4c
commit 44bb02f857
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 120 additions and 16 deletions

View File

@ -4,6 +4,7 @@
### Unreleased ### 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) - `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) - 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) - 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)

View File

@ -1,21 +1,25 @@
use super::{types, util, Context}; use std::collections::BTreeMap;
use anyhow::{Context as _, Result}; 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::ParamType;
use ethers_core::{ use ethers_core::{
abi::{Function, FunctionExt, Param}, abi::{Function, FunctionExt, Param},
types::Selector, types::Selector,
}; };
use inflector::Inflector;
use proc_macro2::{Literal, TokenStream}; use super::{types, util, Context};
use quote::quote;
use std::collections::BTreeMap;
use syn::Ident;
/// Expands a context into a method struct containing all the generated bindings /// Expands a context into a method struct containing all the generated bindings
/// to the Solidity contract methods. /// to the Solidity contract methods.
impl Context { impl Context {
/// Expands all method implementations
pub(crate) fn methods(&self) -> Result<TokenStream> { pub(crate) fn methods(&self) -> Result<TokenStream> {
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 sorted_functions: BTreeMap<_, _> = self.abi.functions.clone().into_iter().collect();
let functions = sorted_functions let functions = sorted_functions
.values() .values()
@ -43,8 +47,10 @@ impl Context {
let call_arg = match param.kind { let call_arg = match param.kind {
// this is awkward edge case where the function inputs are a single struct // 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,))` // 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 // this is currently necessary because internally `flatten_tokens` is called which
// and since `((#name))` is not a rust tuple it doesn't get wrapped into another tuple that will be peeled off by `flatten_tokens` // 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 => { ParamType::Tuple(_) if fun.inputs.len() == 1 => {
// make sure the tuple gets converted to `Token::Tuple` // make sure the tuple gets converted to `Token::Tuple`
quote! {(#name,)} 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<Ident>) -> Result<TokenStream> { fn expand_function(&self, function: &Function, alias: Option<Ident>) -> Result<TokenStream> {
let name = alias.unwrap_or_else(|| util::safe_ident(&function.name.to_snake_case())); let name = alias.unwrap_or_else(|| util::safe_ident(&function.name.to_snake_case()));
let selector = expand_selector(function.selector()); let selector = expand_selector(function.selector());
@ -105,8 +111,8 @@ impl Context {
// TODO use structs // TODO use structs
let outputs = expand_fn_outputs(&function.outputs)?; let outputs = expand_fn_outputs(&function.outputs)?;
let ethers_core = util::ethers_core_crate(); let _ethers_core = util::ethers_core_crate();
let ethers_providers = util::ethers_providers_crate(); let _ethers_providers = util::ethers_providers_crate();
let ethers_contract = util::ethers_contract_crate(); let ethers_contract = util::ethers_contract_crate();
let result = quote! { #ethers_contract::builders::ContractCall<M, #outputs> }; let result = quote! { #ethers_contract::builders::ContractCall<M, #outputs> };
@ -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<BTreeMap<String, Ident>> {
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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>()
.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<TokenStream> { fn expand_fn_outputs(outputs: &[Param]) -> Result<TokenStream> {
@ -150,9 +229,10 @@ fn expand_selector(selector: Selector) -> TokenStream {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use ethers_core::abi::ParamType; use ethers_core::abi::ParamType;
use super::*;
// packs the argument in a tuple to be used for the contract call // packs the argument in a tuple to be used for the contract call
fn expand_inputs_call_arg(inputs: &[Param]) -> TokenStream { fn expand_inputs_call_arg(inputs: &[Param]) -> TokenStream {
let names = inputs let names = inputs
@ -162,9 +242,12 @@ mod tests {
let name = util::expand_input_name(i, &param.name); let name = util::expand_input_name(i, &param.name);
match param.kind { match param.kind {
// this is awkward edge case where the function inputs are a single struct // 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,))` // we need to force this argument into a tuple so it gets expanded to
// this is currently necessary because internally `flatten_tokens` is called which removes the outermost `tuple` level // `((#name,))` this is currently necessary because
// and since `((#name))` is not a rust tuple it doesn't get wrapped into another tuple that will be peeled off by `flatten_tokens` // 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 => { ParamType::Tuple(_) if inputs.len() == 1 => {
// make sure the tuple gets converted to `Token::Tuple` // make sure the tuple gets converted to `Token::Tuple`
quote! {(#name,)} quote! {(#name,)}

View File

@ -173,3 +173,23 @@ fn can_gen_human_readable_with_structs() {
let f = Foo { x: 100u64.into() }; let f = Foo { x: 100u64.into() };
let _ = contract.foo(f); 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());
}