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

feat: substitute overloaded functions #501

Merged
merged 2 commits into from
Oct 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
Comment on lines +114 to +115
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should prob remove?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, seems like they're unused, clippy --fix must have detected that,
can be removed

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