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
|
### 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)
|
||||||
|
|
|
@ -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, ¶m.name);
|
let name = util::expand_input_name(i, ¶m.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,)}
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue