Repository: gakonst/ethers-rs (rust)

Issue:
Add return struct support from abigen function signatures
**Is your feature request related to a problem? Please describe.**
Currently for a function in contract that returns a struct, its abigen! generated signature would have a tuple as its return types in rust.

```solidity
pragma solidity >=0.8.0;

contract Test {
  struct Person {
    string name;
    uint age;
  }
  function defaultPerson() public pure returns (Person memory) {
    return Person("Alice", 20);
  }
}
```
<details>
<summary>contract's ABI</summary>

```json
[
	{
		"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"
	}
]
```

</details>

and in my Rust code:
```rust
// assume `contract` has been instantiated 
let person = contract.defaultPerson().call().await?;
    // ^^^ this is a tuple of (String, U256), instead of struct Person from abigen!
```

**Describe the solution you'd like**
Ideally we could also have return types matched the solidity function signature instead of ABIEncoderV2's representation.

Thanks a lot! @mattsse 

Pull request:
feat(abigen): use structs for outputs
<!--
Thank you for your Pull Request. Please provide a description above and review
the requirements below.

Bug fixes and new features should include tests.

Contributors guide: https://github.com/gakonst/ethers-rs/blob/master/CONTRIBUTING.md

The contributors guide includes instructions for running rustfmt and building the
documentation.
-->

## Motivation
Closes #661 
<!--
Explain the context and why you're making that change. What is the problem
you're trying to solve? In some cases there is not a problem and this can be
thought of as being the motivation for your change.
-->

## Solution
substitute output tuples that are structs with rust structs, same as inputs
<!--
Summarize the solution and provide any necessary context needed to understand
the code change.
-->

## PR Checklist

- [x] Added Tests
- [ ] Added Documentation
- [x] Updated the changelog


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b7d268b4..f5538665 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/Cargo.lock b/Cargo.lock
index cf084d06..a2dede5b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -287,6 +287,15 @@ dependencies = [
  "generic-array 0.14.4",
 ]
 
+[[package]]
+name = "block-buffer"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1d36a02058e76b040de25a4464ba1c80935655595b661505c8b39b664828b95"
+dependencies = [
+ "generic-array 0.14.4",
+]
+
 [[package]]
 name = "block-modes"
 version = "0.8.1"
@@ -776,6 +785,15 @@ dependencies = [
  "zeroize",
 ]
 
+[[package]]
+name = "crypto-common"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567569e659735adb39ff2d4c20600f7cd78be5471f8c58ab162bce3c03fdbc5f"
+dependencies = [
+ "generic-array 0.14.4",
+]
+
 [[package]]
 name = "crypto-mac"
 version = "0.8.0"
@@ -907,6 +925,17 @@ dependencies = [
  "generic-array 0.14.4",
 ]
 
+[[package]]
+name = "digest"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8549e6bfdecd113b7e221fe60b433087f6957387a20f8118ebca9b12af19143d"
+dependencies = [
+ "block-buffer 0.10.0",
+ "crypto-common",
+ "generic-array 0.14.4",
+]
+
 [[package]]
 name = "dirs-next"
 version = "2.0.0"
@@ -1319,7 +1348,7 @@ dependencies = [
  "glob",
  "hex",
  "home",
- "md-5",
+ "md-5 0.10.0",
  "num_cpus",
  "once_cell",
  "regex",
@@ -1957,6 +1986,15 @@ dependencies = [
  "opaque-debug 0.3.0",
 ]
 
+[[package]]
+name = "md-5"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6a38fc55c8bbc10058782919516f88826e70320db6d206aebc49611d24216ae"
+dependencies = [
+ "digest 0.10.0",
+]
+
 [[package]]
 name = "memchr"
 version = "2.4.1"
@@ -2792,7 +2830,7 @@ dependencies = [
  "http",
  "hyper",
  "log",
- "md-5",
+ "md-5 0.9.1",
  "percent-encoding",
  "pin-project-lite",
  "rusoto_credential",
diff --git a/ethers-contract/ethers-contract-abigen/src/contract/events.rs b/ethers-contract/ethers-contract-abigen/src/contract/events.rs
index f48eabc4..64667cf1 100644
--- a/ethers-contract/ethers-contract-abigen/src/contract/events.rs
+++ b/ethers-contract/ethers-contract-abigen/src/contract/events.rs
@@ -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<_>>();
 
diff --git a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs
index eb40aa8f..7827eba2 100644
--- a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs
+++ b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs
@@ -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 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 name = expand_function_name(function, alias.as_ref());
+        let selector = expand_selector(function.selector());
 
         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
diff --git a/ethers-contract/ethers-contract-abigen/src/contract/structs.rs b/ethers-contract/ethers-contract-abigen/src/contract/structs.rs
index 526a8dcf..64a90bcd 100644
--- a/ethers-contract/ethers-contract-abigen/src/contract/structs.rs
+++ b/ethers-contract/ethers-contract-abigen/src/contract/structs.rs
@@ -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
diff --git a/ethers-contract/src/multicall/mod.rs b/ethers-contract/src/multicall/mod.rs
index 096969d6..ebc4225d 100644
--- a/ethers-contract/src/multicall/mod.rs
+++ b/ethers-contract/src/multicall/mod.rs
@@ -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)) => {
diff --git a/ethers-etherscan/src/gas.rs b/ethers-etherscan/src/gas.rs
index bdbbc640..e9c7e1de 100644
--- a/ethers-etherscan/src/gas.rs
+++ b/ethers-etherscan/src/gas.rs
@@ -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
     }
diff --git a/ethers-signers/src/ledger/app.rs b/ethers-signers/src/ledger/app.rs
index 31e20d06..558d0995 100644
--- a/ethers-signers/src/ledger/app.rs
+++ b/ethers-signers/src/ledger/app.rs
@@ -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;
             }
diff --git a/ethers-solc/Cargo.toml b/ethers-solc/Cargo.toml
index c7bb762f..faf7d910 100644
--- a/ethers-solc/Cargo.toml
+++ b/ethers-solc/Cargo.toml
@@ -23,7 +23,7 @@ tokio = { version = "1.12.0", default-features = false, features = ["process", "
 futures-util = { version = "0.3.18", optional = true }
 once_cell = "1.8.0"
 regex = "1.5.4"
-md-5 = "0.9.1"
+md-5 = "0.10.0"
 thiserror = "1.0.30"
 hex = "0.4.3"
 colored = "2.0.0"
diff --git a/ethers-solc/src/compile.rs b/ethers-solc/src/compile.rs
index d52726da..73246444 100644
--- a/ethers-solc/src/compile.rs
+++ b/ethers-solc/src/compile.rs
@@ -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
diff --git a/ethers-solc/src/config.rs b/ethers-solc/src/config.rs
index 4f97dd1e..c88f4395 100644
--- a/ethers-solc/src/config.rs
+++ b/ethers-solc/src/config.rs
@@ -354,6 +354,7 @@ impl ArtifactOutput for MinimalCombinedArtifacts {
 
     fn read_cached_artifact(path: impl AsRef<Path>) -> Result<Self::Artifact> {
         let file = fs::File::open(path.as_ref())?;
+        let file = io::BufReader::new(file);
         Ok(serde_json::from_reader(file)?)
     }
 


Test patch:
diff --git a/ethers-contract/tests/abigen.rs b/ethers-contract/tests/abigen.rs
index b49c83bb..f3e7b4e9 100644
--- a/ethers-contract/tests/abigen.rs
+++ b/ethers-contract/tests/abigen.rs
@@ -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);
+}
diff --git a/ethers-contract/tests/solidity-contracts/Abiencoderv2Test.sol b/ethers-contract/tests/solidity-contracts/Abiencoderv2Test.sol
new file mode 100644
index 00000000..a89fcf02
--- /dev/null
+++ b/ethers-contract/tests/solidity-contracts/Abiencoderv2Test.sol
@@ -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);
+    }
+}
\ No newline at end of file
diff --git a/ethers-contract/tests/solidity-contracts/abiencoderv2test_abi.json b/ethers-contract/tests/solidity-contracts/abiencoderv2test_abi.json
new file mode 100644
index 00000000..13430b68
--- /dev/null
+++ b/ethers-contract/tests/solidity-contracts/abiencoderv2test_abi.json
@@ -0,0 +1,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"}]
\ No newline at end of file