Skip to content
This repository has been archived by the owner on Oct 19, 2024. It is now read-only.

Commit

Permalink
feat: substitute overloaded functions (#501)
Browse files Browse the repository at this point in the history
* feat: substitute overloaded functions

* chore: update changelog
  • Loading branch information
mattsse authored Oct 13, 2021
1 parent ea8551d commit 44bb02f
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 16 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
115 changes: 99 additions & 16 deletions ethers-contract/ethers-contract-abigen/src/contract/methods.rs
Original file line number Diff line number Diff line change
@@ -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<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 functions = sorted_functions
.values()
Expand Down Expand Up @@ -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,)}
Expand Down Expand Up @@ -97,16 +103,16 @@ impl Context {
}
}

#[allow(unused)]
/// Expands a single function with the given alias
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 selector = expand_selector(function.selector());

// 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<M, #outputs> };
Expand All @@ -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> {
Expand All @@ -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
Expand All @@ -162,9 +242,12 @@ mod tests {
let name = util::expand_input_name(i, &param.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,)}
Expand Down
20 changes: 20 additions & 0 deletions ethers-contract/tests/abigen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

0 comments on commit 44bb02f

Please sign in to comment.