feat: substitute overloaded functions (#501)
* feat: substitute overloaded functions * chore: update changelog
This commit is contained in:
parent
ea8551da4c
commit
44bb02f857
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
@ -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<Ident>) -> Result<TokenStream> {
|
||||
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<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> {
|
||||
|
@ -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,)}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue