feat(abigen): use structs for outputs (#664)

* feat(abigen): use structs for outputs

* update CHANGELOG

* make clippy happy

* fix lints
This commit is contained in:
Matthias Seitz 2021-12-10 00:00:59 +01:00 committed by GitHub
parent f0a8f01465
commit 59cf991828
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 147 additions and 36 deletions

View File

@ -94,6 +94,8 @@
### Unreleased
- Substitute output tuples with rust struct types for function calls
[#664](https://github.com/gakonst/ethers-rs/pull/664)
- Add AbiType implementation during EthAbiType expansion
[#647](https://github.com/gakonst/ethers-rs/pull/647)
- fix Etherscan conditional HTTP support

View File

@ -38,8 +38,7 @@ impl Context {
let sorted_events: BTreeMap<_, _> = self.abi.events.iter().collect();
let filter_methods = sorted_events
.values()
.map(std::ops::Deref::deref)
.flatten()
.flat_map(std::ops::Deref::deref)
.map(|event| self.expand_filter(event))
.collect::<Vec<_>>();

View File

@ -28,8 +28,7 @@ impl Context {
let sorted_functions: BTreeMap<_, _> = self.abi.functions.iter().collect();
let functions = sorted_functions
.values()
.map(std::ops::Deref::deref)
.flatten()
.flat_map(std::ops::Deref::deref)
.map(|function| {
let signature = function.abi_signature();
self.expand_function(function, aliases.get(&signature).cloned())
@ -50,7 +49,7 @@ impl Context {
alias: Option<&MethodAlias>,
) -> Result<TokenStream> {
let call_name = expand_call_struct_name(function, alias);
let fields = self.expand_input_pairs(function)?;
let fields = self.expand_input_params(function)?;
// expand as a tuple if all fields are anonymous
let all_anonymous_fields = function.inputs.iter().all(|input| input.name.is_empty());
let call_type_definition = if all_anonymous_fields {
@ -163,16 +162,34 @@ impl Context {
}
/// Expands to the `name : type` pairs of the function's inputs
fn expand_input_pairs(&self, fun: &Function) -> Result<Vec<(TokenStream, TokenStream)>> {
fn expand_input_params(&self, fun: &Function) -> Result<Vec<(TokenStream, TokenStream)>> {
let mut args = Vec::with_capacity(fun.inputs.len());
for (idx, param) in fun.inputs.iter().enumerate() {
let name = util::expand_input_name(idx, &param.name);
let ty = self.expand_input_param(fun, &param.name, &param.kind)?;
let ty = self.expand_input_param_type(fun, &param.name, &param.kind)?;
args.push((name, ty));
}
Ok(args)
}
/// Expands to the return type of a function
fn expand_outputs(&self, fun: &Function) -> Result<TokenStream> {
let mut outputs = Vec::with_capacity(fun.outputs.len());
for param in fun.outputs.iter() {
let ty = self.expand_output_param_type(fun, param, &param.kind)?;
outputs.push(ty);
}
let return_ty = match outputs.len() {
0 => quote! { () },
1 => outputs[0].clone(),
_ => {
quote! { (#( #outputs ),*) }
}
};
Ok(return_ty)
}
/// Expands the arguments for the call that eventually calls the contract
fn expand_contract_call_args(&self, fun: &Function) -> Result<TokenStream> {
let mut call_args = Vec::with_capacity(fun.inputs.len());
@ -202,7 +219,8 @@ impl Context {
Ok(call_args)
}
fn expand_input_param(
/// returns the Tokenstream for the corresponding rust type of the param
fn expand_input_param_type(
&self,
fun: &Function,
param: &str,
@ -210,13 +228,13 @@ impl Context {
) -> Result<TokenStream> {
match kind {
ParamType::Array(ty) => {
let ty = self.expand_input_param(fun, param, ty)?;
let ty = self.expand_input_param_type(fun, param, ty)?;
Ok(quote! {
::std::vec::Vec<#ty>
})
}
ParamType::FixedArray(ty, size) => {
let ty = self.expand_input_param(fun, param, ty)?;
let ty = self.expand_input_param_type(fun, param, ty)?;
let size = *size;
Ok(quote! {[#ty; #size]})
}
@ -235,27 +253,61 @@ impl Context {
}
}
/// returns the Tokenstream for the corresponding rust type of the output param
fn expand_output_param_type(
&self,
fun: &Function,
param: &Param,
kind: &ParamType,
) -> Result<TokenStream> {
match kind {
ParamType::Array(ty) => {
let ty = self.expand_output_param_type(fun, param, ty)?;
Ok(quote! {
::std::vec::Vec<#ty>
})
}
ParamType::FixedArray(ty, size) => {
let ty = self.expand_output_param_type(fun, param, ty)?;
let size = *size;
Ok(quote! {[#ty; #size]})
}
ParamType::Tuple(_) => {
let ty = if let Some(rust_struct_name) =
param.internal_type.as_ref().and_then(|s| {
self.internal_structs.get_function_output_struct_type(&fun.name, s)
}) {
let ident = util::ident(rust_struct_name);
quote! {#ident}
} else {
types::expand(kind)?
};
Ok(ty)
}
_ => types::expand(kind),
}
}
/// Expands a single function with the given alias
fn expand_function(
&self,
function: &Function,
alias: Option<MethodAlias>,
) -> Result<TokenStream> {
let ethers_contract = ethers_contract_crate();
let name = expand_function_name(function, alias.as_ref());
let selector = expand_selector(function.selector());
// TODO use structs
let outputs = expand_fn_outputs(&function.outputs)?;
let ethers_contract = ethers_contract_crate();
let result = quote! { #ethers_contract::builders::ContractCall<M, #outputs> };
let contract_args = self.expand_contract_call_args(function)?;
let function_params =
self.expand_input_pairs(function)?.into_iter().map(|(name, ty)| quote! { #name: #ty });
self.expand_input_params(function)?.into_iter().map(|(name, ty)| quote! { #name: #ty });
let function_params = quote! { #( , #function_params )* };
let outputs = self.expand_outputs(function)?;
let result = quote! { #ethers_contract::builders::ContractCall<M, #outputs> };
let doc = util::expand_doc(&format!(
"Calls the contract's `{}` (0x{}) function",
function.name,
@ -478,20 +530,6 @@ impl Context {
}
}
fn expand_fn_outputs(outputs: &[Param]) -> Result<TokenStream> {
match outputs.len() {
0 => Ok(quote! { () }),
1 => types::expand(&outputs[0].kind),
_ => {
let types = outputs
.iter()
.map(|param| types::expand(&param.kind))
.collect::<Result<Vec<_>>>()?;
Ok(quote! { (#( #types ),*) })
}
}
}
fn expand_selector(selector: Selector) -> TokenStream {
let bytes = selector.iter().copied().map(Literal::u8_unsuffixed);
quote! { [#( #bytes ),*] }
@ -575,6 +613,20 @@ mod tests {
use super::*;
fn expand_fn_outputs(outputs: &[Param]) -> Result<TokenStream> {
match outputs.len() {
0 => Ok(quote! { () }),
1 => types::expand(&outputs[0].kind),
_ => {
let types = outputs
.iter()
.map(|param| types::expand(&param.kind))
.collect::<Result<Vec<_>>>()?;
Ok(quote! { (#( #types ),*) })
}
}
}
// packs the argument in a tuple to be used for the contract call
fn expand_inputs_call_arg(inputs: &[Param]) -> TokenStream {
let names = inputs

View File

@ -312,6 +312,22 @@ impl InternalStructs {
.map(String::as_str)
}
/// Returns the name of the rust type that will be generated if the given output is a struct
/// NOTE: this does not account for arrays or fixed arrays
pub fn get_function_output_struct_type(
&self,
function: &str,
internal_type: &str,
) -> Option<&str> {
self.outputs
.get(function)
.and_then(|outputs| {
outputs.iter().find(|s| s.as_str() == struct_type_identifier(internal_type))
})
.and_then(|id| self.rust_type_names.get(id))
.map(String::as_str)
}
/// Returns the mapping table of abi `internal type identifier -> rust type`
pub fn rust_type_names(&self) -> &HashMap<String, String> {
&self.rust_type_names

View File

@ -191,7 +191,7 @@ impl<M: Middleware> Multicall<M> {
/// If more than the maximum number of supported calls are added. The maximum
/// limits is constrained due to tokenization/detokenization support for tuples
pub fn add_call<D: Detokenize>(&mut self, call: ContractCall<M, D>) -> &mut Self {
assert!(!(self.calls.len() >= 16), "Cannot support more than {} calls", 16);
assert!(self.calls.len() < 16, "Cannot support more than {} calls", 16);
match (call.tx.to(), call.tx.data()) {
(Some(NameOrAddress::Address(target)), Some(data)) => {

View File

@ -419,3 +419,32 @@ fn can_handle_case_sensitive_calls() {
let _ = contract.index();
let _ = contract.INDEX();
}
#[tokio::test]
async fn can_abiencoderv2_output() {
abigen!(AbiEncoderv2Test, "ethers-contract/tests/solidity-contracts/abiencoderv2test_abi.json",);
let ganache = ethers_core::utils::Ganache::new().spawn();
let from = ganache.addresses()[0];
let provider = Provider::try_from(ganache.endpoint())
.unwrap()
.with_sender(from)
.interval(std::time::Duration::from_millis(10));
let client = Arc::new(provider);
let contract = "AbiencoderV2Test";
let path = "./tests/solidity-contracts/Abiencoderv2Test.sol";
let compiled = Solc::default().compile_source(path).unwrap();
let compiled = compiled.get(path, contract).unwrap();
let factory = ethers_contract::ContractFactory::new(
compiled.abi.unwrap().clone(),
compiled.bytecode().unwrap().clone(),
client.clone(),
);
let addr = factory.deploy(()).unwrap().legacy().send().await.unwrap().address();
let contract = AbiEncoderv2Test::new(addr, client.clone());
let person = Person { name: "Alice".to_string(), age: 20u64.into() };
let res = contract.default_person().call().await.unwrap();
assert_eq!(res, person);
}

View File

@ -0,0 +1,12 @@
pragma solidity >=0.6.0;
pragma experimental ABIEncoderV2;
contract AbiencoderV2Test {
struct Person {
string name;
uint age;
}
function defaultPerson() public pure returns (Person memory) {
return Person("Alice", 20);
}
}

View File

@ -0,0 +1 @@
[{"inputs":[],"name":"defaultPerson","outputs":[{"components":[{"internalType":"string","name":"name","type":"string"},{"internalType":"uint256","name":"age","type":"uint256"}],"internalType":"struct Hello.Person","name":"","type":"tuple"}],"stateMutability":"pure","type":"function"}]

View File

@ -123,7 +123,7 @@ mod tests {
assert!(oracle.fast_gas_price > 0);
assert!(oracle.last_block > 0);
assert!(oracle.suggested_base_fee > 0.0);
assert!(oracle.gas_used_ratio.len() > 0);
assert!(!oracle.gas_used_ratio.is_empty());
})
.await
}

View File

@ -206,7 +206,7 @@ impl LedgerEthereum {
let mut bytes = vec![depth as u8];
for derivation_index in elements {
let hardened = derivation_index.contains('\'');
let mut index = derivation_index.replace("'", "").parse::<u32>().unwrap();
let mut index = derivation_index.replace('\'', "").parse::<u32>().unwrap();
if hardened {
index |= 0x80000000;
}

View File

@ -202,7 +202,7 @@ impl Solc {
pub fn version_req(source: &Source) -> Result<VersionReq> {
let version = utils::find_version_pragma(&source.content)
.ok_or(SolcError::PragmaNotFound)?
.replace(" ", ",");
.replace(' ', ",");
// Somehow, Solidity semver without an operator is considered to be "exact",
// but lack of operator automatically marks the operator as Caret, so we need