Generate structs for the return data of abigen-erated contracts (#1440)

* convert some helper functions

* use said helpers

* more

* don't derive EthCall on return structs

* move return structs to separate function

* remove unused

* remove duplicate code

* reduce code duplication

also use an iterator instead of a presized vector

* comments

* stuck

* fix wrong field

* rename

* don't generate structs for no-output functions

* cosmetic changes

* test: decode and verify result

* more testing

unnamed output (tuple struct)
no output (doesn't exist) -> can't verify this in code though

* Update ethers-contract/ethers-contract-abigen/src/contract/methods.rs

Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>

* remove dbg print

oops :(

Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
This commit is contained in:
jole 2022-07-04 20:54:02 +02:00 committed by GitHub
parent 3c1de64240
commit b287fcca4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 160 additions and 15 deletions

View File

@ -37,9 +37,15 @@ impl Context {
.collect::<Result<Vec<_>>>()?;
let function_impls = quote! { #( #functions )* };
let call_structs = self.expand_call_structs(aliases)?;
let call_structs = self.expand_call_structs(aliases.clone())?;
let return_structs = self.expand_return_structs(aliases)?;
Ok((function_impls, call_structs))
let all_structs = quote! {
#call_structs
#return_structs
};
Ok((function_impls, all_structs))
}
/// Returns all deploy (constructor) implementations
@ -135,7 +141,47 @@ impl Context {
})
}
/// Expands all structs
/// Expands to the corresponding struct type based on the inputs of the given function
fn expand_return_struct(
&self,
function: &Function,
alias: Option<&MethodAlias>,
) -> Result<TokenStream> {
let struct_name = expand_return_struct_name(function, alias);
let fields = self.expand_output_params(function)?;
// no point in having structs when there is no data returned
if function.outputs.is_empty() {
return Ok(TokenStream::new())
}
// expand as a tuple if all fields are anonymous
let all_anonymous_fields = function.outputs.iter().all(|output| output.name.is_empty());
let return_type_definition = if all_anonymous_fields {
// expand to a tuple struct
expand_data_tuple(&struct_name, &fields)
} else {
// expand to a struct
expand_data_struct(&struct_name, &fields)
};
let abi_signature = function.abi_signature();
let doc = format!(
"Container type for all return fields from the `{}` function with signature `{}` and selector `{:?}`",
function.name,
abi_signature,
function.selector()
);
let abi_signature_doc = util::expand_doc(&doc);
let ethers_contract = ethers_contract_crate();
// use the same derives as for events
let derives = util::expand_derives(&self.event_derives);
Ok(quote! {
#abi_signature_doc
#[derive(Clone, Debug, Default, Eq, PartialEq, #ethers_contract::EthAbiType, #ethers_contract::EthAbiCodec, #derives)]
pub #return_type_definition
})
}
/// Expands all call structs
fn expand_call_structs(&self, aliases: BTreeMap<String, MethodAlias>) -> Result<TokenStream> {
let mut struct_defs = Vec::new();
let mut struct_names = Vec::new();
@ -214,20 +260,52 @@ impl Context {
})
}
/// Expands all return structs
fn expand_return_structs(&self, aliases: BTreeMap<String, MethodAlias>) -> Result<TokenStream> {
let mut struct_defs = Vec::new();
for function in self.abi.functions.values().flatten() {
let signature = function.abi_signature();
let alias = aliases.get(&signature);
struct_defs.push(self.expand_return_struct(function, alias)?);
}
let struct_def_tokens = quote! {
#(#struct_defs)*
};
Ok(struct_def_tokens)
}
/// The name ident of the calls enum
fn expand_calls_enum_name(&self) -> Ident {
util::ident(&format!("{}Calls", self.contract_ident))
}
/// Expands to the `name : type` pairs of the function's inputs
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() {
/// Expands to the `name : type` pairs of the function's parameters
fn expand_params(
&self,
fun: &Function,
params: &[Param],
) -> Result<Vec<(TokenStream, TokenStream)>> {
params
.iter()
.enumerate()
.map(|(idx, param)| {
let name = util::expand_input_name(idx, &param.name);
let ty = self.expand_input_param_type(fun, &param.name, &param.kind)?;
args.push((name, ty));
Ok((name, ty))
})
.collect()
}
Ok(args)
/// Expands to the `name : type` pairs of the function's inputs
fn expand_input_params(&self, fun: &Function) -> Result<Vec<(TokenStream, TokenStream)>> {
self.expand_params(fun, &fun.inputs)
}
/// Expands to the `name : type` pairs of the function's outputs
fn expand_output_params(&self, fun: &Function) -> Result<Vec<(TokenStream, TokenStream)>> {
self.expand_params(fun, &fun.outputs)
}
/// Expands to the return type of a function
@ -607,16 +685,30 @@ fn expand_function_name(function: &Function, alias: Option<&MethodAlias>) -> Ide
}
}
/// Expands to the name of the call struct
fn expand_call_struct_name(function: &Function, alias: Option<&MethodAlias>) -> Ident {
/// Expands the name of a struct by a postfix
fn expand_struct_name_postfix(
function: &Function,
alias: Option<&MethodAlias>,
postfix: &str,
) -> Ident {
let name = if let Some(alias) = alias {
format!("{}Call", alias.struct_name)
format!("{}{}", alias.struct_name, postfix)
} else {
format!("{}Call", util::safe_pascal_case(&function.name))
format!("{}{}", util::safe_pascal_case(&function.name), postfix)
};
util::ident(&name)
}
/// Expands to the name of the call struct
fn expand_call_struct_name(function: &Function, alias: Option<&MethodAlias>) -> Ident {
expand_struct_name_postfix(function, alias, "Call")
}
/// Expands to the name of the return struct
fn expand_return_struct_name(function: &Function, alias: Option<&MethodAlias>) -> Ident {
expand_struct_name_postfix(function, alias, "Return")
}
/// Expands to the name of the call struct
fn expand_call_struct_variant_name(function: &Function, alias: Option<&MethodAlias>) -> Ident {
if let Some(alias) = alias {

View File

@ -1,7 +1,7 @@
#![cfg(feature = "abigen")]
#![allow(unused)]
//! Test cases to validate the `abigen!` macro
use ethers_contract::{abigen, EthCall, EthEvent};
use ethers_contract::{abigen, Abigen, EthCall, EthEvent};
use ethers_core::{
abi::{AbiDecode, AbiEncode, Address, Tokenizable},
types::{transaction::eip2718::TypedTransaction, Eip1559TransactionRequest, U256},
@ -167,6 +167,34 @@ fn can_generate_internal_structs_multiple() {
let _ = contract.verify(vec![], proof, vk);
}
#[test]
fn can_gen_return_struct() {
abigen!(MultiInputOutput, "ethers-contract/tests/solidity-contracts/MultiInputOutput.json");
fn verify<T: AbiEncode + AbiDecode + Clone + std::fmt::Debug + std::cmp::PartialEq>(
binding: T,
) {
let encoded = binding.clone().encode();
let decoded = T::decode(&encoded).unwrap();
assert_eq!(binding, decoded);
}
// just make sure they are accessible and work
let dupe = DupeIntReturn { out_one: 5.into(), out_two: 1234.into() };
verify(dupe);
let array =
ArrayRelayerReturn { outputs: vec![4.into(), 9.into(), 2.into()], some_number: 42.into() };
verify(array);
let single = SingleUnnamedReturn { 0: 4321.into() };
verify(single);
// doesnt exist:
// let nonexistant = CallWithoutReturnDataReturn;
}
#[test]
fn can_gen_human_readable_with_structs() {
abigen!(

View File

@ -0,0 +1 @@
{"abi":[{"inputs":[{"internalType":"uint256[]","name":"inputs","type":"uint256[]"}],"name":"arrayRelayer","outputs":[{"internalType":"uint256[]","name":"outputs","type":"uint256[]"},{"internalType":"uint256","name":"someNumber","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"input","type":"uint256"}],"name":"callWithoutReturnData","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"input","type":"uint256"}],"name":"dupeInt","outputs":[{"internalType":"uint256","name":"outOne","type":"uint256"},{"internalType":"uint256","name":"outTwo","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"singleUnnamed","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"}]}

View File

@ -0,0 +1,24 @@
pragma solidity >=0.6.0;
pragma experimental ABIEncoderV2;
contract MultiInputOutput {
function dupeInt(uint256 input) public pure returns (uint256 outOne, uint256 outTwo) {
return (input, input);
}
function arrayRelayer(uint256[] memory inputs) public pure returns (uint256[] memory outputs, uint someNumber) {
outputs = new uint[](inputs.length);
for(uint256 i = 0; i < inputs.length; i++) {
outputs[i] = inputs[i];
}
someNumber = 42;
}
function singleUnnamed() public pure returns (uint) {
return 0x45;
}
function callWithoutReturnData(uint256 input) public pure {
// silence unused errors
uint nothing = input;
input = nothing;
return;
}
}