Repository: gakonst/ethers-rs
Pull request #478 (2021-09-29 19:29:34)

feat(contract, abigen): support Hardhat ABI format
## Motivation

Resolves https://github.com/gakonst/ethers-rs/issues/476

On compilation, Hardhat emits not just JSON description of the contract's ABI, but also some artifact metadata (see https://hardhat.org/guides/compile-contracts.html#artifacts)

## Solution

Support Hardhat format deserialization


Patch:
diff --git a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs
index 608778e1..fc481eff 100644
--- a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs
+++ b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs
@@ -205,6 +205,7 @@ mod tests {
         let params = vec![Param {
             name: "arg_a".to_string(),
             kind: ParamType::Address,
+            internal_type: None,
         }];
         let token_stream = expand_inputs_call_arg(&params);
         assert_eq!(token_stream.to_string(), "arg_a");
@@ -214,10 +215,12 @@ mod tests {
             Param {
                 name: "arg_a".to_string(),
                 kind: ParamType::Address,
+                internal_type: None,
             },
             Param {
                 name: "arg_b".to_string(),
                 kind: ParamType::Uint(256usize),
+                internal_type: None,
             },
         ];
         let token_stream = expand_inputs_call_arg(&params);
@@ -228,14 +231,17 @@ mod tests {
             Param {
                 name: "arg_a".to_string(),
                 kind: ParamType::Address,
+                internal_type: None,
             },
             Param {
                 name: "arg_b".to_string(),
                 kind: ParamType::Uint(128usize),
+                internal_type: None,
             },
             Param {
                 name: "arg_c".to_string(),
                 kind: ParamType::Bool,
+                internal_type: None,
             },
         ];
         let token_stream = expand_inputs_call_arg(&params);
@@ -256,10 +262,12 @@ mod tests {
                     Param {
                         name: "a".to_string(),
                         kind: ParamType::Bool,
+                        internal_type: None,
                     },
                     Param {
                         name: "b".to_string(),
                         kind: ParamType::Address,
+                        internal_type: None,
                     },
                 ],
             )
@@ -279,6 +287,7 @@ mod tests {
             expand_fn_outputs(&[Param {
                 name: "a".to_string(),
                 kind: ParamType::Bool,
+                internal_type: None,
             }])
             .unwrap(),
             { bool },
@@ -292,10 +301,12 @@ mod tests {
                 Param {
                     name: "a".to_string(),
                     kind: ParamType::Bool,
+                    internal_type: None,
                 },
                 Param {
                     name: "b".to_string(),
                     kind: ParamType::Address,
+                    internal_type: None,
                 },
             ],)
             .unwrap(),
diff --git a/ethers-contract/ethers-contract-abigen/src/rawabi.rs b/ethers-contract/ethers-contract-abigen/src/rawabi.rs
index fc04f49d..3904fd68 100644
--- a/ethers-contract/ethers-contract-abigen/src/rawabi.rs
+++ b/ethers-contract/ethers-contract-abigen/src/rawabi.rs
@@ -1,10 +1,72 @@
 //! This is a basic representation of a contract ABI that does no post processing but contains the raw content of the ABI.
 
 #![allow(missing_docs)]
-use serde::{Deserialize, Serialize};
+use serde::{
+    de::{EnumAccess, Error, MapAccess, SeqAccess, Visitor},
+    Deserialize, Deserializer, Serialize,
+};
 
 /// Contract ABI as a list of items where each item can be a function, constructor or event
-pub type RawAbi = Vec<Item>;
+pub struct RawAbi(Vec<Item>);
+
+impl IntoIterator for RawAbi {
+    type Item = Item;
+    type IntoIter = std::vec::IntoIter<Self::Item>;
+
+    #[inline]
+    fn into_iter(self) -> Self::IntoIter {
+        self.0.into_iter()
+    }
+}
+
+struct RawAbiVisitor;
+
+impl<'de> Visitor<'de> for RawAbiVisitor {
+    type Value = RawAbi;
+
+    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
+        formatter.write_str("a sequence or map with `abi` key")
+    }
+
+    fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
+    where
+        A: SeqAccess<'de>,
+    {
+        let mut vec = Vec::new();
+
+        while let Some(element) = seq.next_element()? {
+            vec.push(element);
+        }
+
+        Ok(RawAbi(vec))
+    }
+
+    fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
+    where
+        A: MapAccess<'de>,
+    {
+        let mut vec = None;
+
+        while let Some(key) = map.next_key::<String>()? {
+            if key == "abi" {
+                vec = Some(RawAbi(map.next_value::<Vec<Item>>()?));
+            } else {
+                map.next_value::<serde::de::IgnoredAny>()?;
+            }
+        }
+
+        vec.ok_or_else(|| serde::de::Error::missing_field("abi"))
+    }
+}
+
+impl<'de> Deserialize<'de> for RawAbi {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        deserializer.deserialize_any(RawAbiVisitor)
+    }
+}
 
 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
 #[serde(rename_all = "camelCase")]
@@ -40,9 +102,17 @@ pub struct Component {
 #[cfg(test)]
 mod tests {
     use super::*;
+
     #[test]
     fn can_parse_raw_abi() {
         const VERIFIER_ABI: &str = include_str!("../../tests/solidity-contracts/verifier_abi.json");
         let _ = serde_json::from_str::<RawAbi>(VERIFIER_ABI).unwrap();
     }
+
+    #[test]
+    fn can_parse_hardhat_raw_abi() {
+        const VERIFIER_ABI: &str =
+            include_str!("../../tests/solidity-contracts/verifier_abi_hardhat.json");
+        let _ = serde_json::from_str::<RawAbi>(VERIFIER_ABI).unwrap();
+    }
 }
diff --git a/ethers-contract/ethers-contract-abigen/src/source.rs b/ethers-contract/ethers-contract-abigen/src/source.rs
index 32f1ad56..71c08745 100644
--- a/ethers-contract/ethers-contract-abigen/src/source.rs
+++ b/ethers-contract/ethers-contract-abigen/src/source.rs
@@ -63,7 +63,7 @@ impl Source {
         S: AsRef<str>,
     {
         let source = source.as_ref();
-        if source.starts_with('[') {
+        if matches!(source.chars().next(), Some('[' | '{')) {
             return Ok(Source::String(source.to_owned()));
         }
         let root = env::current_dir()?.canonicalize()?;
@@ -283,5 +283,12 @@ mod tests {
         let src = r#"[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"name","type":"string"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"symbol","type":"string"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"decimals","type":"uint8"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"spender","type":"address"},{"name":"value","type":"uint256"}],"name":"approve","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"totalSupply","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"from","type":"address"},{"name":"to","type":"address"},{"name":"value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"who","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"to","type":"address"},{"name":"value","type":"uint256"}],"name":"transfer","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"owner","type":"address"},{"name":"spender","type":"address"}],"name":"allowance","outputs":[{"name":"remaining","type":"uint256"}],"payable":false,"type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"}]"#;
         let parsed = Source::parse(src).unwrap();
         assert_eq!(parsed, Source::String(src.to_owned()));
+
+        let hardhat_src = format!(
+            r#"{{"_format": "hh-sol-artifact-1", "contractName": "Verifier", "sourceName": "contracts/verifier.sol", "abi": {}, "bytecode": "0x", "deployedBytecode": "0x", "linkReferences": {{}}, "deployedLinkReferences": {{}}}}"#,
+            src,
+        );
+        let hardhat_parsed = Source::parse(&hardhat_src).unwrap();
+        assert_eq!(hardhat_parsed, Source::String(hardhat_src));
     }
 }


Test patch:
diff --git a/ethers-contract/tests/solidity-contracts/verifier_abi_hardhat.json b/ethers-contract/tests/solidity-contracts/verifier_abi_hardhat.json
new file mode 100644
index 00000000..cfc92371
--- /dev/null
+++ b/ethers-contract/tests/solidity-contracts/verifier_abi_hardhat.json
@@ -0,0 +1,185 @@
+{
+  "_format": "hh-sol-artifact-1",
+  "contractName": "Verifier",
+  "sourceName": "contracts/verifier.sol",
+  "abi": [
+    {
+      "inputs": [],
+      "stateMutability": "nonpayable",
+      "type": "constructor"
+    },
+    {
+      "inputs": [
+        {
+          "internalType": "uint256[]",
+          "name": "input",
+          "type": "uint256[]"
+        },
+        {
+          "components": [
+            {
+              "components": [
+                {
+                  "internalType": "uint256",
+                  "name": "X",
+                  "type": "uint256"
+                },
+                {
+                  "internalType": "uint256",
+                  "name": "Y",
+                  "type": "uint256"
+                }
+              ],
+              "internalType": "struct Pairing.G1Point",
+              "name": "A",
+              "type": "tuple"
+            },
+            {
+              "components": [
+                {
+                  "internalType": "uint256[2]",
+                  "name": "X",
+                  "type": "uint256[2]"
+                },
+                {
+                  "internalType": "uint256[2]",
+                  "name": "Y",
+                  "type": "uint256[2]"
+                }
+              ],
+              "internalType": "struct Pairing.G2Point",
+              "name": "B",
+              "type": "tuple"
+            },
+            {
+              "components": [
+                {
+                  "internalType": "uint256",
+                  "name": "X",
+                  "type": "uint256"
+                },
+                {
+                  "internalType": "uint256",
+                  "name": "Y",
+                  "type": "uint256"
+                }
+              ],
+              "internalType": "struct Pairing.G1Point",
+              "name": "C",
+              "type": "tuple"
+            }
+          ],
+          "internalType": "struct Verifier.Proof",
+          "name": "proof",
+          "type": "tuple"
+        },
+        {
+          "components": [
+            {
+              "components": [
+                {
+                  "internalType": "uint256",
+                  "name": "X",
+                  "type": "uint256"
+                },
+                {
+                  "internalType": "uint256",
+                  "name": "Y",
+                  "type": "uint256"
+                }
+              ],
+              "internalType": "struct Pairing.G1Point",
+              "name": "alfa1",
+              "type": "tuple"
+            },
+            {
+              "components": [
+                {
+                  "internalType": "uint256[2]",
+                  "name": "X",
+                  "type": "uint256[2]"
+                },
+                {
+                  "internalType": "uint256[2]",
+                  "name": "Y",
+                  "type": "uint256[2]"
+                }
+              ],
+              "internalType": "struct Pairing.G2Point",
+              "name": "beta2",
+              "type": "tuple"
+            },
+            {
+              "components": [
+                {
+                  "internalType": "uint256[2]",
+                  "name": "X",
+                  "type": "uint256[2]"
+                },
+                {
+                  "internalType": "uint256[2]",
+                  "name": "Y",
+                  "type": "uint256[2]"
+                }
+              ],
+              "internalType": "struct Pairing.G2Point",
+              "name": "gamma2",
+              "type": "tuple"
+            },
+            {
+              "components": [
+                {
+                  "internalType": "uint256[2]",
+                  "name": "X",
+                  "type": "uint256[2]"
+                },
+                {
+                  "internalType": "uint256[2]",
+                  "name": "Y",
+                  "type": "uint256[2]"
+                }
+              ],
+              "internalType": "struct Pairing.G2Point",
+              "name": "delta2",
+              "type": "tuple"
+            },
+            {
+              "components": [
+                {
+                  "internalType": "uint256",
+                  "name": "X",
+                  "type": "uint256"
+                },
+                {
+                  "internalType": "uint256",
+                  "name": "Y",
+                  "type": "uint256"
+                }
+              ],
+              "internalType": "struct Pairing.G1Point[]",
+              "name": "IC",
+              "type": "tuple[]"
+            }
+          ],
+          "internalType": "struct Verifier.VerifyingKey",
+          "name": "vk",
+          "type": "tuple"
+        }
+      ],
+      "name": "verify",
+      "outputs": [
+        {
+          "internalType": "bool",
+          "name": "",
+          "type": "bool"
+        }
+      ],
+      "stateMutability": "view",
+      "type": "function"
+    }
+  ],
+  "bytecode": "0x",
+  "deployedBytecode": "0x",
+  "linkReferences": {},
+  "deployedLinkReferences": {}
+}