Repository: gakonst/ethers-rs
Pull request #482 (2021-10-01 19:35:28)

feat: support human readable struct inputs
<!--
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
Support custom structs as contract params in human readable abi
<!--
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
* keep track of the function's types (input, output) in the abi parser, this is added on top the existing parser and adds a bit more verbosity in to the param parse functions...
* reuse the existing `InternalStructs` when determine function inputs
<!--
Summarize the solution and provide any necessary context needed to understand
the code change.
-->

## PR Checklist

- [x] Added Tests
- [x] Added Documentation
- [x] Updated the changelog



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c21c63d7..987c9958 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,8 @@
 
 ### Unreleased
 
+* Use rust types as contract function inputs for human readable abi [#482](https://github.com/gakonst/ethers-rs/pull/482)
+* 
 ### 0.5.3
 
 * Allow configuring the optimizer & passing arbitrary arguments to solc [#427](https://github.com/gakonst/ethers-rs/pull/427)
@@ -44,4 +46,4 @@
 
 ### 0.5.3
 
-* Added Time Lagged middleware [#457](https://github.com/gakonst/ethers-rs/pull/457)
+* Added Time Lagged middleware [#457](https://github.com/gakonst/ethers-rs/pull/457)
\ No newline at end of file
diff --git a/ethers-contract/ethers-contract-abigen/src/contract.rs b/ethers-contract/ethers-contract-abigen/src/contract.rs
index c3c0b617..6b29b7a5 100644
--- a/ethers-contract/ethers-contract-abigen/src/contract.rs
+++ b/ethers-contract/ethers-contract-abigen/src/contract.rs
@@ -131,12 +131,26 @@ impl Context {
         };
 
         // try to extract all the solidity structs from the normal JSON ABI
-        // we need to parse the json abi again because we need the internalType fields which are omitted by ethabi.
-        let internal_structs = (!human_readable)
-            .then(|| serde_json::from_str::<RawAbi>(&abi_str).ok())
-            .flatten()
-            .map(InternalStructs::new)
-            .unwrap_or_default();
+        // we need to parse the json abi again because we need the internalType fields which are omitted by ethabi. If the ABI was defined as human readable we use the `internal_structs` from the Abi Parser
+        let internal_structs = if human_readable {
+            let mut internal_structs = InternalStructs::default();
+            // the types in the abi_parser are already valid rust types so simply clone them to make it consistent with the `RawAbi` variant
+            internal_structs.rust_type_names.extend(
+                abi_parser
+                    .function_params
+                    .values()
+                    .map(|ty| (ty.clone(), ty.clone())),
+            );
+            internal_structs.function_params = abi_parser.function_params.clone();
+            internal_structs.outputs = abi_parser.outputs.clone();
+
+            internal_structs
+        } else {
+            serde_json::from_str::<RawAbi>(&abi_str)
+                .ok()
+                .map(InternalStructs::new)
+                .unwrap_or_default()
+        };
 
         let contract_name = util::ident(&args.contract_name);
 
diff --git a/ethers-contract/ethers-contract-abigen/src/contract/structs.rs b/ethers-contract/ethers-contract-abigen/src/contract/structs.rs
index 8a1b4a1c..88c683b3 100644
--- a/ethers-contract/ethers-contract-abigen/src/contract/structs.rs
+++ b/ethers-contract/ethers-contract-abigen/src/contract/structs.rs
@@ -194,22 +194,22 @@ impl Context {
 #[derive(Debug, Clone, Default)]
 pub struct InternalStructs {
     /// All unique internal types that are function inputs or outputs
-    top_level_internal_types: HashMap<String, Component>,
+    pub(crate) top_level_internal_types: HashMap<String, Component>,
 
     /// (function name, param name) -> struct which are the identifying properties we get the name from ethabi.
-    function_params: HashMap<(String, String), String>,
+    pub(crate) function_params: HashMap<(String, String), String>,
 
     /// (function name) -> Vec<structs> all structs the function returns
-    outputs: HashMap<String, Vec<String>>,
+    pub(crate) outputs: HashMap<String, Vec<String>>,
 
     /// All the structs extracted from the abi with their identifier as key
-    structs: HashMap<String, SolStruct>,
+    pub(crate) structs: HashMap<String, SolStruct>,
 
     /// solidity structs as tuples
-    struct_tuples: HashMap<String, ParamType>,
+    pub(crate) struct_tuples: HashMap<String, ParamType>,
 
     /// Contains the names for the rust types (id -> rust type name)
-    rust_type_names: HashMap<String, String>,
+    pub(crate) rust_type_names: HashMap<String, String>,
 }
 
 impl InternalStructs {
diff --git a/ethers-core/src/abi/human_readable.rs b/ethers-core/src/abi/human_readable.rs
index 90fa735d..9c013c74 100644
--- a/ethers-core/src/abi/human_readable.rs
+++ b/ethers-core/src/abi/human_readable.rs
@@ -13,6 +13,10 @@ pub struct AbiParser {
     pub structs: HashMap<String, SolStruct>,
     /// solidity structs as tuples
     pub struct_tuples: HashMap<String, Vec<ParamType>>,
+    /// (function name, param name) -> struct which are the identifying properties we get the name from ethabi.
+    pub function_params: HashMap<(String, String), String>,
+    /// (function name) -> Vec<structs> all structs the function returns
+    pub outputs: HashMap<String, Vec<String>>,
 }
 
 impl AbiParser {
@@ -83,7 +87,22 @@ impl AbiParser {
                     .or_default()
                     .push(event);
             } else if line.starts_with("constructor") {
-                abi.constructor = Some(self.parse_constructor(line)?);
+                let inputs = self
+                    .constructor_inputs(line)?
+                    .into_iter()
+                    .map(|(input, struct_name)| {
+                        if let Some(struct_name) = struct_name {
+                            // keep track of the user defined struct of that param
+                            self.function_params.insert(
+                                ("constructor".to_string(), input.name.clone()),
+                                struct_name,
+                            );
+                        }
+                        input
+                    })
+                    .collect();
+
+                abi.constructor = Some(Constructor { inputs });
             } else {
                 // function may have shorthand declaration, so it won't start with "function"
                 let function = match self.parse_function(line) {
@@ -148,6 +167,8 @@ impl AbiParser {
                 .map(|s| (s.name().to_string(), s))
                 .collect(),
             struct_tuples: HashMap::new(),
+            function_params: Default::default(),
+            outputs: Default::default(),
         }
     }
 
@@ -233,7 +254,7 @@ impl AbiParser {
         Ok(EventParam {
             name: name.to_string(),
             indexed,
-            kind: self.parse_type(type_str)?,
+            kind: self.parse_type(type_str)?.0,
         })
     }
 
@@ -278,6 +299,16 @@ impl AbiParser {
 
         let inputs = if let Some(params) = input_args {
             self.parse_params(params)?
+                .into_iter()
+                .map(|(input, struct_name)| {
+                    if let Some(struct_name) = struct_name {
+                        // keep track of the user defined struct of that param
+                        self.function_params
+                            .insert((name.clone(), input.name.clone()), struct_name);
+                    }
+                    input
+                })
+                .collect()
         } else {
             Vec::new()
         };
@@ -287,7 +318,19 @@ impl AbiParser {
                 .trim()
                 .strip_suffix(')')
                 .ok_or_else(|| format_err!("Expected output args parentheses at `{}`", s))?;
-            self.parse_params(params)?
+            let output_params = self.parse_params(params)?;
+            let mut outputs = Vec::with_capacity(output_params.len());
+            let mut output_types = Vec::new();
+
+            for (output, struct_name) in output_params {
+                if let Some(struct_name) = struct_name {
+                    // keep track of the user defined struct of that param
+                    output_types.push(struct_name);
+                }
+                outputs.push(output);
+            }
+            self.outputs.insert(name.clone(), output_types);
+            outputs
         } else {
             Vec::new()
         };
@@ -304,31 +347,33 @@ impl AbiParser {
         })
     }
 
-    fn parse_params(&self, s: &str) -> Result<Vec<Param>> {
+    fn parse_params(&self, s: &str) -> Result<Vec<(Param, Option<String>)>> {
         s.split(',')
             .filter(|s| !s.is_empty())
             .map(|s| self.parse_param(s))
             .collect::<Result<Vec<_>, _>>()
     }
 
-    fn parse_type(&self, type_str: &str) -> Result<ParamType> {
+    /// Returns the `ethabi` `ParamType` for the function parameter and the aliased struct type, if it is a user defined struct
+    fn parse_type(&self, type_str: &str) -> Result<(ParamType, Option<String>)> {
         if let Ok(kind) = Reader::read(type_str) {
-            Ok(kind)
+            Ok((kind, None))
         } else {
             // try struct instead
             if let Ok(field) = StructFieldType::parse(type_str) {
                 let struct_ty = field
                     .as_struct()
                     .ok_or_else(|| format_err!("Expected struct type `{}`", type_str))?;
+                let name = struct_ty.name();
                 let tuple = self
                     .struct_tuples
-                    .get(struct_ty.name())
+                    .get(name)
                     .cloned()
                     .map(ParamType::Tuple)
                     .ok_or_else(|| format_err!("Unknown struct `{}`", struct_ty.name()))?;
 
                 if let Some(field) = field.as_struct() {
-                    Ok(field.as_param(tuple))
+                    Ok((field.as_param(tuple), Some(name.to_string())))
                 } else {
                     bail!("Expected struct type")
                 }
@@ -339,6 +384,15 @@ impl AbiParser {
     }
 
     pub fn parse_constructor(&self, s: &str) -> Result<Constructor> {
+        let inputs = self
+            .constructor_inputs(s)?
+            .into_iter()
+            .map(|s| s.0)
+            .collect();
+        Ok(Constructor { inputs })
+    }
+
+    fn constructor_inputs(&self, s: &str) -> Result<Vec<(Param, Option<String>)>> {
         let mut input = s.trim();
         if !input.starts_with("constructor") {
             bail!("Not a constructor `{}`", input)
@@ -353,12 +407,10 @@ impl AbiParser {
             .last()
             .ok_or_else(|| format_err!("Expected closing `)` in `{}`", s))?;
 
-        let inputs = self.parse_params(params)?;
-
-        Ok(Constructor { inputs })
+        self.parse_params(params)
     }
 
-    fn parse_param(&self, param: &str) -> Result<Param> {
+    fn parse_param(&self, param: &str) -> Result<(Param, Option<String>)> {
         let mut iter = param.trim().rsplitn(3, is_whitespace);
 
         let mut name = iter
@@ -375,12 +427,15 @@ impl AbiParser {
             type_str = name;
             name = "";
         }
-
-        Ok(Param {
-            name: name.to_string(),
-            kind: self.parse_type(type_str)?,
-            internal_type: None,
-        })
+        let (kind, user_struct) = self.parse_type(type_str)?;
+        Ok((
+            Param {
+                name: name.to_string(),
+                kind,
+                internal_type: None,
+            },
+            user_struct,
+        ))
     }
 }
 
diff --git a/ethers-core/src/utils/solc.rs b/ethers-core/src/utils/solc.rs
index af796fcd..5beecaa9 100644
--- a/ethers-core/src/utils/solc.rs
+++ b/ethers-core/src/utils/solc.rs
@@ -97,6 +97,10 @@ pub struct Solc {
     /// Paths for importing other libraries
     pub allowed_paths: Vec<PathBuf>,
 
+    /// Output a single json document containing the specified information.
+    /// Default is `abi,bin,bin-runtime`
+    pub combined_json: Option<String>,
+
     /// Additional arguments to pass to solc
     pub args: Vec<String>,
 }
@@ -121,18 +125,21 @@ impl Solc {
             optimizer: Some(200), // default optimizer runs = 200
             evm_version: EvmVersion::Istanbul,
             allowed_paths: Vec::new(),
+            combined_json: Some("abi,bin,bin-runtime".to_string()),
             args: Vec::new(),
         }
     }
 
-    /// Gets the ABI for the contracts
-    pub fn build_raw(self) -> Result<HashMap<String, CompiledContractStr>> {
+    /// Gets the complete solc output as json object
+    pub fn exec(self) -> Result<serde_json::Value> {
         let path = self.solc_path.unwrap_or_else(|| PathBuf::from(SOLC));
 
         let mut command = Command::new(&path);
         let version = Solc::version(Some(path));
 
-        command.arg("--combined-json").arg("abi,bin,bin-runtime");
+        if let Some(combined_json) = self.combined_json {
+            command.arg("--combined-json").arg(combined_json);
+        }
 
         if let Some(evm_version) = normalize_evm_version(&version, self.evm_version) {
             command.arg("--evm-version").arg(evm_version.to_string());
@@ -160,7 +167,12 @@ impl Solc {
         }
 
         // Deserialize the output
-        let mut output: serde_json::Value = serde_json::from_slice(&command.stdout)?;
+        Ok(serde_json::from_slice(&command.stdout)?)
+    }
+
+    /// Gets the ABI for the contracts
+    pub fn build_raw(self) -> Result<HashMap<String, CompiledContractStr>> {
+        let mut output = self.exec()?;
         let contract_values = output["contracts"].as_object_mut().ok_or_else(|| {
             SolcError::SolcError("no contracts found in `solc` output".to_string())
         })?;
@@ -281,6 +293,13 @@ impl Solc {
         self
     }
 
+    /// Sets the `combined-json` option, by default this is set to `abi,bin,bin-runtime`
+    /// NOTE: In order to get the `CompiledContract` from `Self::build`, this _must_ contain `abi,bin`.
+    pub fn combined_json(mut self, combined_json: impl Into<String>) -> Self {
+        self.combined_json = Some(combined_json.into());
+        self
+    }
+
     /// Sets the optimizer runs (default = 200). None indicates no optimization
     ///
     /// ```rust,no_run


Test patch:
diff --git a/ethers-contract/tests/abigen.rs b/ethers-contract/tests/abigen.rs
index cb577ce4..d79660aa 100644
--- a/ethers-contract/tests/abigen.rs
+++ b/ethers-contract/tests/abigen.rs
@@ -1,7 +1,9 @@
 #![cfg(feature = "abigen")]
 //! Test cases to validate the `abigen!` macro
 use ethers_contract::{abigen, EthEvent};
-use ethers_core::abi::Tokenizable;
+use ethers_core::abi::{Address, Tokenizable};
+use ethers_providers::Provider;
+use std::sync::Arc;
 
 #[test]
 fn can_gen_human_readable() {
@@ -74,3 +76,21 @@ fn can_generate_internal_structs() {
     assert_tokenizeable::<G1Point>();
     assert_tokenizeable::<G2Point>();
 }
+
+#[test]
+fn can_gen_human_readable_with_structs() {
+    abigen!(
+        SimpleContract,
+        r#"[
+        struct Foo { uint256 x; }
+        function foo(Foo memory x)
+    ]"#,
+        event_derives(serde::Deserialize, serde::Serialize)
+    );
+    assert_tokenizeable::<Foo>();
+
+    let (client, _mock) = Provider::mocked();
+    let contract = SimpleContract::new(Address::default(), Arc::new(client));
+    let foo = Foo { x: 100u64.into() };
+    let _ = contract.foo(foo);
+}