diff --git a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs index dee0da35..912444c7 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs @@ -37,9 +37,15 @@ impl Context { .collect::>>()?; 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 { + 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) -> Result { 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) -> Result { + 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 parameters + fn expand_params( + &self, + fun: &Function, + params: &[Param], + ) -> Result> { + params + .iter() + .enumerate() + .map(|(idx, param)| { + let name = util::expand_input_name(idx, ¶m.name); + let ty = self.expand_input_param_type(fun, ¶m.name, ¶m.kind)?; + Ok((name, ty)) + }) + .collect() + } + /// Expands to the `name : type` pairs of the function's inputs fn expand_input_params(&self, fun: &Function) -> Result> { - let mut args = Vec::with_capacity(fun.inputs.len()); - for (idx, param) in fun.inputs.iter().enumerate() { - let name = util::expand_input_name(idx, ¶m.name); - let ty = self.expand_input_param_type(fun, ¶m.name, ¶m.kind)?; - args.push((name, ty)); - } - Ok(args) + 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> { + 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 { diff --git a/ethers-contract/tests/abigen.rs b/ethers-contract/tests/abigen.rs index 63a55ce0..3271c551 100644 --- a/ethers-contract/tests/abigen.rs +++ b/ethers-contract/tests/abigen.rs @@ -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( + 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!( diff --git a/ethers-contract/tests/solidity-contracts/MultiInputOutput.json b/ethers-contract/tests/solidity-contracts/MultiInputOutput.json new file mode 100644 index 00000000..30a7bd21 --- /dev/null +++ b/ethers-contract/tests/solidity-contracts/MultiInputOutput.json @@ -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"}]} diff --git a/ethers-contract/tests/solidity-contracts/MultiInputOutput.sol b/ethers-contract/tests/solidity-contracts/MultiInputOutput.sol new file mode 100644 index 00000000..990e0ed7 --- /dev/null +++ b/ethers-contract/tests/solidity-contracts/MultiInputOutput.sol @@ -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; + } +}