Repository: gakonst/ethers-rs
Pull request #1440 (2022-06-30 23:16:17)

Generate structs for the return data of abigen-erated contracts
<!--
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
I explain the usecase in #1405, the short version is that you cannot easily decode the return of calls without an instatiated contract (`::new()` with a provider). Since abigen already generates structs representing calls, which AbiEncode into bytes, it only makes sense it should also have structs that AbiDecode the return data into a usable response representation.
<!--
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
I hope the code is mostly self explanitory. Since I am new to contributing here, I'd love some feedback on the stuff i did so far.

I'm wondering if you'd want there to be an enum for return structs when there is more than one, like with the calls? I can put it back in but i initially removed it.
<!--
Summarize the solution and provide any necessary context needed to understand
the code change.
-->

## PR Checklist

- [ ] Added Tests
- [ ] Added Documentation
- [ ] Updated the changelog

gakonst: like the direction - could you also add some tests for this?
joleeee: I made a test now, let me know if it is satisfactory or what else should be tested. Note that this will not generate return bindings for functions that do not return anything.
joleeee: That is correct, this only adds additional structs, it does not change the call-bindings in any way.

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 35678881..9795a618 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -601,9 +601,9 @@ dependencies = [
 
 [[package]]
 name = "coins-ledger"
-version = "0.6.0"
+version = "0.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "498841803f751d49bb08fe4a88b514087c52e5df885575ed4757f14ab151e239"
+checksum = "bbfa8a730d02735d8d53888a95d8f33aaa9dda9979862de113202421db939b2a"
 dependencies = [
  "async-trait",
  "blake2b_simd",
@@ -1279,6 +1279,7 @@ dependencies = [
  "convert_case",
  "elliptic-curve",
  "ethabi",
+ "fastrlp",
  "generic-array 0.14.5",
  "hex",
  "hex-literal",
@@ -1512,6 +1513,31 @@ dependencies = [
  "instant",
 ]
 
+[[package]]
+name = "fastrlp"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6c60b758dc5bf92743e1b863ac88b84a4750bd096b19c2f524359659dc1f1ac"
+dependencies = [
+ "arrayvec 0.7.2",
+ "auto_impl",
+ "bytes",
+ "ethereum-types",
+ "fastrlp-derive",
+]
+
+[[package]]
+name = "fastrlp-derive"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9499f20a2fa1a744422de24d1b4d1ec58f240147de1d0a3ceacadf2e240b4fc2"
+dependencies = [
+ "bytes",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "ff"
 version = "0.11.0"
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::<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 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)?;
+                Ok((name, ty))
+            })
+            .collect()
+    }
+
     /// 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() {
-            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(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<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 {
diff --git a/ethers-contract/src/event.rs b/ethers-contract/src/event.rs
index 600855a0..bc393e70 100644
--- a/ethers-contract/src/event.rs
+++ b/ethers-contract/src/event.rs
@@ -115,7 +115,44 @@ where
     M: Middleware,
     D: EthLogDecode,
 {
-    /// Returns a stream for the event
+    /// Turns this event filter into `Stream` that yields decoded events.
+    ///
+    /// This will first install a new logs filter via [`eth_newFilter`](https://docs.alchemy.com/alchemy/apis/ethereum/eth-newfilter) using the configured `filter` object. See also [`FilterWatcher`](ethers_providers::FilterWatcher).
+    ///
+    /// Once the filter is created, this will periodically call [`eth_getFilterChanges`](https://docs.alchemy.com/alchemy/apis/ethereum/eth-getfilterchanges) to get the newest logs and decode them
+    ///
+    /// **Note:** Compared to [`Self::subscribe`], which is only available on `PubsubClient`s, such
+    /// as Websocket, this is a poll-based subscription, as the node does not notify us when a new
+    /// matching log is available, instead we have to actively ask for new logs using additional RPC
+    /// requests, and this is done on an interval basis.
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// # async fn test<M:ethers_providers::Middleware>(contract: ethers_contract::Contract<M>) {
+    /// # use ethers_core::types::*;
+    /// # use futures_util::stream::StreamExt;
+    /// # use ethers_contract::{Contract, EthEvent};
+    ///
+    /// // The event we want to get
+    /// #[derive(Clone, Debug, EthEvent)]
+    /// pub struct Approval {
+    ///     #[ethevent(indexed)]
+    ///     pub token_owner: Address,
+    ///     #[ethevent(indexed)]
+    ///     pub spender: Address,
+    ///     pub tokens: U256,
+    /// }
+    ///
+    /// let ev = contract.event::<Approval>().from_block(1337).to_block(2000);
+    /// let mut event_stream = ev.stream().await.unwrap();
+    ///
+    ///  while let Some(Ok(approval)) = event_stream.next().await {
+    ///      let Approval{token_owner,spender,tokens} = approval;
+    /// }
+    ///
+    /// # }
+    /// ```
     pub async fn stream(
         &'a self,
     ) -> Result<
@@ -136,6 +173,8 @@ where
     D: EthLogDecode,
 {
     /// Returns a subscription for the event
+    ///
+    /// See also [Self::stream()].
     pub async fn subscribe(
         &'a self,
     ) -> Result<
diff --git a/ethers-core/Cargo.toml b/ethers-core/Cargo.toml
index 394d228b..e5f3dd2b 100644
--- a/ethers-core/Cargo.toml
+++ b/ethers-core/Cargo.toml
@@ -10,6 +10,7 @@ repository = "https://github.com/gakonst/ethers-rs"
 keywords = ["ethereum", "web3", "celo", "ethers"]
 
 [dependencies]
+fastrlp = { version = "0.1.2", features = ["std", "derive", "ethereum-types"] }
 rlp = { version = "0.5.0", default-features = false, features = ["std"] }
 ethabi = { version = "17.1.0", default-features = false, features = ["full-serde", "rlp"] }
 arrayvec = { version = "0.7.2", default-features = false }
diff --git a/ethers-core/src/types/bytes.rs b/ethers-core/src/types/bytes.rs
index 35c0e28d..a23f0521 100644
--- a/ethers-core/src/types/bytes.rs
+++ b/ethers-core/src/types/bytes.rs
@@ -1,6 +1,5 @@
+use fastrlp::{Decodable, Encodable};
 use serde::{Deserialize, Deserializer, Serialize, Serializer};
-use thiserror::Error;
-
 use std::{
     borrow::Borrow,
     clone::Clone,
@@ -8,6 +7,7 @@ use std::{
     ops::Deref,
     str::FromStr,
 };
+use thiserror::Error;
 
 /// Wrapper type around Bytes to deserialize/serialize "0x" prefixed ethereum hex strings
 #[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize, Ord, PartialOrd)]
@@ -131,6 +131,21 @@ impl PartialEq<bytes::Bytes> for Bytes {
     }
 }
 
+impl Encodable for Bytes {
+    fn length(&self) -> usize {
+        self.0.length()
+    }
+    fn encode(&self, out: &mut dyn bytes::BufMut) {
+        self.0.encode(out)
+    }
+}
+
+impl Decodable for Bytes {
+    fn decode(buf: &mut &[u8]) -> Result<Self, fastrlp::DecodeError> {
+        Ok(Self(bytes::Bytes::decode(buf)?))
+    }
+}
+
 #[derive(Debug, Clone, Error)]
 #[error("Failed to parse bytes: {0}")]
 pub struct ParseBytesError(String);
diff --git a/ethers-core/src/types/signature.rs b/ethers-core/src/types/signature.rs
index 5cddf4e7..95c3f324 100644
--- a/ethers-core/src/types/signature.rs
+++ b/ethers-core/src/types/signature.rs
@@ -4,6 +4,7 @@ use crate::{
     utils::hash_message,
 };
 
+use fastrlp::Decodable;
 use serde::{Deserialize, Serialize};
 use std::{convert::TryFrom, fmt, str::FromStr};
 
@@ -53,7 +54,7 @@ pub enum RecoveryMessage {
     Hash(H256),
 }
 
-#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Copy)]
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Copy, Hash)]
 /// An ECDSA signature
 pub struct Signature {
     /// R value
@@ -141,6 +142,29 @@ impl Signature {
     pub fn to_vec(&self) -> Vec<u8> {
         self.into()
     }
+
+    /// Decodes a signature from RLP bytes, assuming no RLP header
+    pub(crate) fn decode_signature(buf: &mut &[u8]) -> Result<Self, fastrlp::DecodeError> {
+        let v = u64::decode(buf)?;
+        Ok(Self { r: U256::decode(buf)?, s: U256::decode(buf)?, v })
+    }
+}
+
+impl fastrlp::Decodable for Signature {
+    fn decode(buf: &mut &[u8]) -> Result<Self, fastrlp::DecodeError> {
+        Self::decode_signature(buf)
+    }
+}
+
+impl fastrlp::Encodable for Signature {
+    fn length(&self) -> usize {
+        self.r.length() + self.s.length() + self.v.length()
+    }
+    fn encode(&self, out: &mut dyn bytes::BufMut) {
+        self.v.encode(out);
+        self.r.encode(out);
+        self.s.encode(out);
+    }
 }
 
 fn normalize_recovery_id(v: u64) -> u8 {
diff --git a/ethers-signers/Cargo.toml b/ethers-signers/Cargo.toml
index 71a934f5..19de8701 100644
--- a/ethers-signers/Cargo.toml
+++ b/ethers-signers/Cargo.toml
@@ -18,7 +18,7 @@ ethers-core = { version = "^0.13.0", path = "../ethers-core", features = ["eip71
 thiserror = { version = "1.0.31", default-features = false }
 coins-bip32 = "0.6.0"
 coins-bip39 = "0.6.0"
-coins-ledger = { version = "0.6.0", default-features = false, optional = true }
+coins-ledger = { version = "0.6.1", default-features = false, optional = true }
 hex = { version = "0.4.3", default-features = false, features = ["std"] }
 async-trait = { version = "0.1.50", default-features = false }
 elliptic-curve = { version = "0.11.12", default-features = false }
diff --git a/ethers-solc/src/artifact_output/mod.rs b/ethers-solc/src/artifact_output/mod.rs
index 616e6cdb..05e56922 100644
--- a/ethers-solc/src/artifact_output/mod.rs
+++ b/ethers-solc/src/artifact_output/mod.rs
@@ -19,8 +19,10 @@ use std::{
     borrow::Cow,
     collections::{btree_map::BTreeMap, HashSet},
     fmt, fs, io,
+    ops::Deref,
     path::{Path, PathBuf},
 };
+
 mod configurable;
 pub use configurable::*;
 
@@ -64,7 +66,7 @@ impl ArtifactId {
 }
 
 /// Represents an artifact file representing a [`crate::Contract`]
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub struct ArtifactFile<T> {
     /// The Artifact that was written
     pub artifact: T,
@@ -102,7 +104,7 @@ impl<T> ArtifactFile<T> {
 pub(crate) type ArtifactsMap<T> = FileToContractsMap<Vec<ArtifactFile<T>>>;
 
 /// Represents a set of Artifacts
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub struct Artifacts<T>(pub ArtifactsMap<T>);
 
 impl<T> From<ArtifactsMap<T>> for Artifacts<T> {
@@ -149,6 +151,14 @@ impl<T> AsMut<ArtifactsMap<T>> for Artifacts<T> {
     }
 }
 
+impl<T> Deref for Artifacts<T> {
+    type Target = ArtifactsMap<T>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
 impl<T: Serialize> Artifacts<T> {
     /// Writes all artifacts into the given `artifacts_root` folder
     pub fn write_all(&self) -> Result<()> {
@@ -264,18 +274,49 @@ impl<T> Artifacts<T> {
     }
 
     /// Finds the first artifact `T` with a matching contract name
-    pub fn find(&self, contract_name: impl AsRef<str>) -> Option<&T> {
+    pub fn find_first(&self, contract_name: impl AsRef<str>) -> Option<&T> {
         let contract_name = contract_name.as_ref();
         self.0.iter().find_map(|(_file, contracts)| {
             contracts.get(contract_name).and_then(|c| c.get(0).map(|a| &a.artifact))
         })
     }
 
+    ///  Finds the artifact with matching path and name
+    pub fn find(&self, path: impl AsRef<str>, contract: impl AsRef<str>) -> Option<&T> {
+        let contract_path = path.as_ref();
+        let contract_name = contract.as_ref();
+        self.0.iter().filter(|(path, _)| path.as_str() == contract_path).find_map(
+            |(_file, contracts)| {
+                contracts.get(contract_name).and_then(|c| c.get(0).map(|a| &a.artifact))
+            },
+        )
+    }
+
+    /// Removes the artifact with matching file and name
+    pub fn remove(&mut self, path: impl AsRef<str>, contract: impl AsRef<str>) -> Option<T> {
+        let contract_path = path.as_ref();
+        let contract_name = contract.as_ref();
+        self.0.iter_mut().filter(|(path, _)| path.as_str() == contract_path).find_map(
+            |(_file, contracts)| {
+                let mut artifact = None;
+                if let Some((c, mut artifacts)) = contracts.remove_entry(contract_name) {
+                    if !artifacts.is_empty() {
+                        artifact = Some(artifacts.remove(0).artifact);
+                    }
+                    if !artifacts.is_empty() {
+                        contracts.insert(c, artifacts);
+                    }
+                }
+                artifact
+            },
+        )
+    }
+
     /// Removes the first artifact `T` with a matching contract name
     ///
     /// *Note:* if there are multiple artifacts (contract compiled with different solc) then this
     /// returns the first artifact in that set
-    pub fn remove(&mut self, contract_name: impl AsRef<str>) -> Option<T> {
+    pub fn remove_first(&mut self, contract_name: impl AsRef<str>) -> Option<T> {
         let contract_name = contract_name.as_ref();
         self.0.iter_mut().find_map(|(_file, contracts)| {
             let mut artifact = None;
diff --git a/ethers-solc/src/artifacts/contract.rs b/ethers-solc/src/artifacts/contract.rs
index bd4e6a5d..d0a51fe2 100644
--- a/ethers-solc/src/artifacts/contract.rs
+++ b/ethers-solc/src/artifacts/contract.rs
@@ -498,7 +498,7 @@ impl<'a> CompactContractRef<'a> {
     /// use ethers_solc::artifacts::*;
     /// # fn demo(project: Project) {
     /// let output = project.compile().unwrap().output();
-    /// let contract = output.find("Greeter").unwrap();
+    /// let contract = output.find_first("Greeter").unwrap();
     /// let contract = contract.unwrap();
     /// # }
     /// ```
diff --git a/ethers-solc/src/compile/output/contracts.rs b/ethers-solc/src/compile/output/contracts.rs
index f8de71b2..e81cd3a2 100644
--- a/ethers-solc/src/compile/output/contracts.rs
+++ b/ethers-solc/src/compile/output/contracts.rs
@@ -4,7 +4,7 @@ use crate::artifacts::{
 };
 use semver::Version;
 use serde::{Deserialize, Serialize};
-use std::{collections::BTreeMap, path::Path};
+use std::{collections::BTreeMap, ops::Deref, path::Path};
 
 /// file -> [(contract name  -> Contract + solc version)]
 #[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
@@ -34,16 +34,41 @@ impl VersionedContracts {
     /// use ethers_solc::artifacts::*;
     /// # fn demo(project: Project) {
     /// let output = project.compile().unwrap().output();
-    /// let contract = output.find("Greeter").unwrap();
+    /// let contract = output.find_first("Greeter").unwrap();
     /// # }
     /// ```
-    pub fn find(&self, contract: impl AsRef<str>) -> Option<CompactContractRef> {
+    pub fn find_first(&self, contract: impl AsRef<str>) -> Option<CompactContractRef> {
         let contract_name = contract.as_ref();
         self.contracts().find_map(|(name, contract)| {
             (name == contract_name).then(|| CompactContractRef::from(contract))
         })
     }
 
+    /// Finds the contract with matching path and name
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use ethers_solc::Project;
+    /// use ethers_solc::artifacts::*;
+    /// # fn demo(project: Project) {
+    /// let output = project.compile().unwrap().output();
+    /// let contract = output.contracts.find("src/Greeter.sol", "Greeter").unwrap();
+    /// # }
+    /// ```
+    pub fn find(
+        &self,
+        path: impl AsRef<str>,
+        contract: impl AsRef<str>,
+    ) -> Option<CompactContractRef> {
+        let contract_path = path.as_ref();
+        let contract_name = contract.as_ref();
+        self.contracts_with_files().find_map(|(path, name, contract)| {
+            (path == contract_path && name == contract_name)
+                .then(|| CompactContractRef::from(contract))
+        })
+    }
+
     /// Removes the _first_ contract with the given name from the set
     ///
     /// # Example
@@ -231,6 +256,14 @@ impl AsMut<FileToContractsMap<Vec<VersionedContract>>> for VersionedContracts {
     }
 }
 
+impl Deref for VersionedContracts {
+    type Target = FileToContractsMap<Vec<VersionedContract>>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
 impl IntoIterator for VersionedContracts {
     type Item = (String, BTreeMap<String, Vec<VersionedContract>>);
     type IntoIter =
diff --git a/ethers-solc/src/compile/output/mod.rs b/ethers-solc/src/compile/output/mod.rs
index bcb5e1e8..298a0680 100644
--- a/ethers-solc/src/compile/output/mod.rs
+++ b/ethers-solc/src/compile/output/mod.rs
@@ -53,6 +53,28 @@ impl<T: ArtifactOutput> ProjectCompileOutput<T> {
         cached_artifacts.into_artifacts::<T>().chain(compiled_artifacts.into_artifacts::<T>())
     }
 
+    /// This returns a chained iterator of both cached and recompiled contract artifacts that yields
+    /// the contract name and the corresponding artifact
+    ///
+    /// # Example
+    ///
+    /// ```no_run
+    /// use std::collections::btree_map::BTreeMap;
+    /// use ethers_solc::ConfigurableContractArtifact;
+    /// use ethers_solc::Project;
+    ///
+    /// let project = Project::builder().build().unwrap();
+    /// let artifacts: BTreeMap<String, &ConfigurableContractArtifact> = project.compile().unwrap().artifacts().collect();
+    /// ```
+    pub fn artifacts(&self) -> impl Iterator<Item = (String, &T::Artifact)> {
+        self.cached_artifacts
+            .artifact_files()
+            .chain(self.compiled_artifacts.artifact_files())
+            .filter_map(|artifact| {
+                T::contract_name(&artifact.file).map(|name| (name, &artifact.artifact))
+            })
+    }
+
     /// All artifacts together with their contract file and name as tuple `(file, contract
     /// name, artifact)`
     ///
@@ -151,15 +173,6 @@ impl<T: ArtifactOutput> ProjectCompileOutput<T> {
         self.compiler_output.has_warning(&self.ignored_error_codes)
     }
 
-    /// Finds the first contract with the given name and removes it from the set
-    pub fn remove(&mut self, contract_name: impl AsRef<str>) -> Option<T::Artifact> {
-        let contract_name = contract_name.as_ref();
-        if let artifact @ Some(_) = self.compiled_artifacts.remove(contract_name) {
-            return artifact
-        }
-        self.cached_artifacts.remove(contract_name)
-    }
-
     /// Returns the set of `Artifacts` that were cached and got reused during
     /// [`crate::Project::compile()`]
     pub fn cached_artifacts(&self) -> &Artifacts<T::Artifact> {
@@ -188,19 +201,140 @@ impl<T: ArtifactOutput> ProjectCompileOutput<T> {
         }
         contracts
     }
-}
 
-impl<T: ArtifactOutput> ProjectCompileOutput<T>
-where
-    T::Artifact: Clone,
-{
+    /// Removes the contract with matching path and name using the `<path>:<contractname>` pattern
+    /// where `path` is optional.
+    ///
+    /// If the `path` segment is `None`, then the first matching `Contract` is returned, see
+    /// [Self::remove_first]
+    ///
+    /// # Example
+    ///
+    ///
+    /// ```
+    /// use ethers_solc::Project;
+    /// use ethers_solc::artifacts::*;
+    /// use ethers_solc::info::ContractInfo;
+    ///
+    /// # fn demo(project: Project) {
+    /// let  output = project.compile().unwrap();
+    /// let info = ContractInfo::new("src/Greeter.sol:Greeter");
+    /// let contract = output.find_contract(&info).unwrap();
+    /// # }
+    /// ```
+    pub fn find_contract<'a>(&self, info: impl Into<ContractInfoRef<'a>>) -> Option<&T::Artifact> {
+        let ContractInfoRef { path, name } = info.into();
+        if let Some(path) = path {
+            self.find(path, name)
+        } else {
+            self.find_first(name)
+        }
+    }
+
+    /// Finds the artifact with matching path and name
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use ethers_solc::Project;
+    /// use ethers_solc::artifacts::*;
+    /// # fn demo(project: Project) {
+    /// let output = project.compile().unwrap();
+    /// let contract = output.find("src/Greeter.sol", "Greeter").unwrap();
+    /// # }
+    /// ```
+    pub fn find(&self, path: impl AsRef<str>, contract: impl AsRef<str>) -> Option<&T::Artifact> {
+        let contract_path = path.as_ref();
+        let contract_name = contract.as_ref();
+        if let artifact @ Some(_) = self.compiled_artifacts.find(contract_path, contract_name) {
+            return artifact
+        }
+        self.cached_artifacts.find(contract_path, contract_name)
+    }
+
     /// Finds the first contract with the given name
-    pub fn find(&self, contract_name: impl AsRef<str>) -> Option<&T::Artifact> {
+    pub fn find_first(&self, contract_name: impl AsRef<str>) -> Option<&T::Artifact> {
+        let contract_name = contract_name.as_ref();
+        if let artifact @ Some(_) = self.compiled_artifacts.find_first(contract_name) {
+            return artifact
+        }
+        self.cached_artifacts.find_first(contract_name)
+    }
+
+    /// Finds the artifact with matching path and name
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use ethers_solc::Project;
+    /// use ethers_solc::artifacts::*;
+    /// # fn demo(project: Project) {
+    /// let output = project.compile().unwrap();
+    /// let contract = output.find("src/Greeter.sol", "Greeter").unwrap();
+    /// # }
+    /// ```
+    pub fn remove(
+        &mut self,
+        path: impl AsRef<str>,
+        contract: impl AsRef<str>,
+    ) -> Option<T::Artifact> {
+        let contract_path = path.as_ref();
+        let contract_name = contract.as_ref();
+        if let artifact @ Some(_) = self.compiled_artifacts.remove(contract_path, contract_name) {
+            return artifact
+        }
+        self.cached_artifacts.remove(contract_path, contract_name)
+    }
+
+    /// Removes the _first_ contract with the given name from the set
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use ethers_solc::Project;
+    /// use ethers_solc::artifacts::*;
+    /// # fn demo(project: Project) {
+    /// let mut output = project.compile().unwrap();
+    /// let contract = output.remove_first("Greeter").unwrap();
+    /// # }
+    /// ```
+    pub fn remove_first(&mut self, contract_name: impl AsRef<str>) -> Option<T::Artifact> {
         let contract_name = contract_name.as_ref();
-        if let artifact @ Some(_) = self.compiled_artifacts.find(contract_name) {
+        if let artifact @ Some(_) = self.compiled_artifacts.remove_first(contract_name) {
             return artifact
         }
-        self.cached_artifacts.find(contract_name)
+        self.cached_artifacts.remove_first(contract_name)
+    }
+
+    /// Removes the contract with matching path and name using the `<path>:<contractname>` pattern
+    /// where `path` is optional.
+    ///
+    /// If the `path` segment is `None`, then the first matching `Contract` is returned, see
+    /// [Self::remove_first]
+    ///
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use ethers_solc::Project;
+    /// use ethers_solc::artifacts::*;
+    /// use ethers_solc::info::ContractInfo;
+    /// # fn demo(project: Project) {
+    /// let mut output = project.compile().unwrap();
+    /// let info = ContractInfo::new("src/Greeter.sol:Greeter");
+    /// let contract = output.remove_contract(&info).unwrap();
+    /// # }
+    /// ```
+    pub fn remove_contract<'a>(
+        &mut self,
+        info: impl Into<ContractInfoRef<'a>>,
+    ) -> Option<T::Artifact> {
+        let ContractInfoRef { path, name } = info.into();
+        if let Some(path) = path {
+            self.remove(path, name)
+        } else {
+            self.remove_first(name)
+        }
     }
 }
 
@@ -340,11 +474,11 @@ impl AggregatedCompilerOutput {
     /// use ethers_solc::artifacts::*;
     /// # fn demo(project: Project) {
     /// let output = project.compile().unwrap().output();
-    /// let contract = output.find("Greeter").unwrap();
+    /// let contract = output.find_first("Greeter").unwrap();
     /// # }
     /// ```
-    pub fn find(&self, contract: impl AsRef<str>) -> Option<CompactContractRef> {
-        self.contracts.find(contract)
+    pub fn find_first(&self, contract: impl AsRef<str>) -> Option<CompactContractRef> {
+        self.contracts.find_first(contract)
     }
 
     /// Removes the _first_ contract with the given name from the set
@@ -548,7 +682,7 @@ impl<'a> OutputDiagnostics<'a> {
             return true
         }
 
-        self.compiler_output.find(&contract_path).map_or(false, |contract| {
+        self.compiler_output.find_first(&contract_path).map_or(false, |contract| {
             contract.abi.map_or(false, |abi| abi.functions.contains_key("IS_TEST"))
         })
     }
diff --git a/examples/contract_human_readable.rs b/examples/contract_human_readable.rs
index a1de69f2..33ddc2d2 100644
--- a/examples/contract_human_readable.rs
+++ b/examples/contract_human_readable.rs
@@ -30,7 +30,7 @@ async fn main() -> Result<()> {
     let project = Project::builder().paths(paths).ephemeral().no_artifacts().build().unwrap();
     // compile the project and get the artifacts
     let output = project.compile().unwrap();
-    let contract = output.find("SimpleStorage").expect("could not find contract").clone();
+    let contract = output.find_first("SimpleStorage").expect("could not find contract").clone();
     let (abi, bytecode, _) = contract.into_parts();
 
     // 2. instantiate our wallet & anvil


Test patch:
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<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!(
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,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;
+	}
+}
diff --git a/ethers-solc/tests/project.rs b/ethers-solc/tests/project.rs
index f126f5c3..1093036b 100644
--- a/ethers-solc/tests/project.rs
+++ b/ethers-solc/tests/project.rs
@@ -55,21 +55,21 @@ fn can_compile_hardhat_sample() {
     let project = TempProject::<ConfigurableArtifacts>::new(paths).unwrap();
 
     let compiled = project.compile().unwrap();
-    assert!(compiled.find("Greeter").is_some());
-    assert!(compiled.find("console").is_some());
+    assert!(compiled.find_first("Greeter").is_some());
+    assert!(compiled.find_first("console").is_some());
     assert!(!compiled.has_compiler_errors());
 
     // nothing to compile
     let compiled = project.compile().unwrap();
-    assert!(compiled.find("Greeter").is_some());
-    assert!(compiled.find("console").is_some());
+    assert!(compiled.find_first("Greeter").is_some());
+    assert!(compiled.find_first("console").is_some());
     assert!(compiled.is_unchanged());
 
     // delete artifacts
     std::fs::remove_dir_all(&project.paths().artifacts).unwrap();
     let compiled = project.compile().unwrap();
-    assert!(compiled.find("Greeter").is_some());
-    assert!(compiled.find("console").is_some());
+    assert!(compiled.find_first("Greeter").is_some());
+    assert!(compiled.find_first("console").is_some());
     assert!(!compiled.is_unchanged());
 }
 
@@ -80,12 +80,12 @@ fn can_compile_dapp_sample() {
     let project = TempProject::<ConfigurableArtifacts>::new(paths).unwrap();
 
     let compiled = project.compile().unwrap();
-    assert!(compiled.find("Dapp").is_some());
+    assert!(compiled.find_first("Dapp").is_some());
     assert!(!compiled.has_compiler_errors());
 
     // nothing to compile
     let compiled = project.compile().unwrap();
-    assert!(compiled.find("Dapp").is_some());
+    assert!(compiled.find_first("Dapp").is_some());
     assert!(compiled.is_unchanged());
 
     let cache = SolFilesCache::read(project.cache_path()).unwrap();
@@ -93,7 +93,7 @@ fn can_compile_dapp_sample() {
     // delete artifacts
     std::fs::remove_dir_all(&project.paths().artifacts).unwrap();
     let compiled = project.compile().unwrap();
-    assert!(compiled.find("Dapp").is_some());
+    assert!(compiled.find_first("Dapp").is_some());
     assert!(!compiled.is_unchanged());
 
     let updated_cache = SolFilesCache::read(project.cache_path()).unwrap();
@@ -107,14 +107,14 @@ fn can_compile_yul_sample() {
     let project = TempProject::<ConfigurableArtifacts>::new(paths).unwrap();
 
     let compiled = project.compile().unwrap();
-    assert!(compiled.find("Dapp").is_some());
-    assert!(compiled.find("SimpleStore").is_some());
+    assert!(compiled.find_first("Dapp").is_some());
+    assert!(compiled.find_first("SimpleStore").is_some());
     assert!(!compiled.has_compiler_errors());
 
     // nothing to compile
     let compiled = project.compile().unwrap();
-    assert!(compiled.find("Dapp").is_some());
-    assert!(compiled.find("SimpleStore").is_some());
+    assert!(compiled.find_first("Dapp").is_some());
+    assert!(compiled.find_first("SimpleStore").is_some());
     assert!(compiled.is_unchanged());
 
     let cache = SolFilesCache::read(project.cache_path()).unwrap();
@@ -122,8 +122,8 @@ fn can_compile_yul_sample() {
     // delete artifacts
     std::fs::remove_dir_all(&project.paths().artifacts).unwrap();
     let compiled = project.compile().unwrap();
-    assert!(compiled.find("Dapp").is_some());
-    assert!(compiled.find("SimpleStore").is_some());
+    assert!(compiled.find_first("Dapp").is_some());
+    assert!(compiled.find_first("SimpleStore").is_some());
     assert!(!compiled.is_unchanged());
 
     let updated_cache = SolFilesCache::read(project.cache_path()).unwrap();
@@ -149,7 +149,7 @@ fn can_compile_configured() {
     let settings = handler.settings();
     let project = TempProject::with_artifacts(paths, handler).unwrap().with_settings(settings);
     let compiled = project.compile().unwrap();
-    let artifact = compiled.find("Dapp").unwrap();
+    let artifact = compiled.find_first("Dapp").unwrap();
     assert!(artifact.metadata.is_some());
     assert!(artifact.raw_metadata.is_some());
     assert!(artifact.ir.is_some());
@@ -195,13 +195,13 @@ fn can_compile_dapp_detect_changes_in_libs() {
     assert_eq!(graph.files().clone(), HashMap::from([(src, 0), (lib, 1),]));
 
     let compiled = project.compile().unwrap();
-    assert!(compiled.find("Foo").is_some());
-    assert!(compiled.find("Bar").is_some());
+    assert!(compiled.find_first("Foo").is_some());
+    assert!(compiled.find_first("Bar").is_some());
     assert!(!compiled.has_compiler_errors());
 
     // nothing to compile
     let compiled = project.compile().unwrap();
-    assert!(compiled.find("Foo").is_some());
+    assert!(compiled.find_first("Foo").is_some());
     assert!(compiled.is_unchanged());
 
     let cache = SolFilesCache::read(&project.paths().cache).unwrap();
@@ -224,8 +224,8 @@ fn can_compile_dapp_detect_changes_in_libs() {
     assert_eq!(graph.files().len(), 2);
 
     let compiled = project.compile().unwrap();
-    assert!(compiled.find("Foo").is_some());
-    assert!(compiled.find("Bar").is_some());
+    assert!(compiled.find_first("Foo").is_some());
+    assert!(compiled.find_first("Bar").is_some());
     // ensure change is detected
     assert!(!compiled.is_unchanged());
 }
@@ -269,14 +269,14 @@ fn can_compile_dapp_detect_changes_in_sources() {
 
     let compiled = project.compile().unwrap();
     assert!(!compiled.has_compiler_errors());
-    assert!(compiled.find("DssSpellTest").is_some());
-    assert!(compiled.find("DssSpellTestBase").is_some());
+    assert!(compiled.find_first("DssSpellTest").is_some());
+    assert!(compiled.find_first("DssSpellTestBase").is_some());
 
     // nothing to compile
     let compiled = project.compile().unwrap();
     assert!(compiled.is_unchanged());
-    assert!(compiled.find("DssSpellTest").is_some());
-    assert!(compiled.find("DssSpellTestBase").is_some());
+    assert!(compiled.find_first("DssSpellTest").is_some());
+    assert!(compiled.find_first("DssSpellTestBase").is_some());
 
     let cache = SolFilesCache::read(&project.paths().cache).unwrap();
     assert_eq!(cache.files.len(), 2);
@@ -303,8 +303,8 @@ fn can_compile_dapp_detect_changes_in_sources() {
     assert_eq!(graph.files().len(), 2);
 
     let compiled = project.compile().unwrap();
-    assert!(compiled.find("DssSpellTest").is_some());
-    assert!(compiled.find("DssSpellTestBase").is_some());
+    assert!(compiled.find_first("DssSpellTest").is_some());
+    assert!(compiled.find_first("DssSpellTestBase").is_some());
     // ensure change is detected
     assert!(!compiled.is_unchanged());
 
@@ -377,26 +377,26 @@ fn can_compile_dapp_sample_with_cache() {
     // first compile
     let project = Project::builder().paths(paths).build().unwrap();
     let compiled = project.compile().unwrap();
-    assert!(compiled.find("Dapp").is_some());
+    assert!(compiled.find_first("Dapp").is_some());
     assert!(!compiled.has_compiler_errors());
 
     // cache is used when nothing to compile
     let compiled = project.compile().unwrap();
-    assert!(compiled.find("Dapp").is_some());
+    assert!(compiled.find_first("Dapp").is_some());
     assert!(compiled.is_unchanged());
 
     // deleted artifacts cause recompile even with cache
     std::fs::remove_dir_all(&project.artifacts_path()).unwrap();
     let compiled = project.compile().unwrap();
-    assert!(compiled.find("Dapp").is_some());
+    assert!(compiled.find_first("Dapp").is_some());
     assert!(!compiled.is_unchanged());
 
     // new file is compiled even with partial cache
     std::fs::copy(cache_testdata_dir.join("NewContract.sol"), root.join("src/NewContract.sol"))
         .unwrap();
     let compiled = project.compile().unwrap();
-    assert!(compiled.find("Dapp").is_some());
-    assert!(compiled.find("NewContract").is_some());
+    assert!(compiled.find_first("Dapp").is_some());
+    assert!(compiled.find_first("NewContract").is_some());
     assert!(!compiled.is_unchanged());
     assert_eq!(
         compiled.into_artifacts().map(|(artifact_id, _)| artifact_id.name).collect::<HashSet<_>>(),
@@ -424,7 +424,7 @@ fn can_compile_dapp_sample_with_cache() {
     // deleted artifact is not taken from the cache
     std::fs::remove_file(&project.paths.sources.join("Dapp.sol")).unwrap();
     let compiled: ProjectCompileOutput<_> = project.compile().unwrap();
-    assert!(compiled.find("Dapp").is_none());
+    assert!(compiled.find_first("Dapp").is_none());
 }
 
 fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> {
@@ -1022,7 +1022,7 @@ fn can_compile_single_files() {
 
     let compiled = tmp.project().compile_file(f.clone()).unwrap();
     assert!(!compiled.has_compiler_errors());
-    assert!(compiled.find("Foo").is_some());
+    assert!(compiled.find_first("Foo").is_some());
 
     let bar = tmp
         .add_contract(
@@ -1037,8 +1037,8 @@ fn can_compile_single_files() {
 
     let compiled = tmp.project().compile_files(vec![f, bar]).unwrap();
     assert!(!compiled.has_compiler_errors());
-    assert!(compiled.find("Foo").is_some());
-    assert!(compiled.find("Bar").is_some());
+    assert!(compiled.find_first("Foo").is_some());
+    assert!(compiled.find_first("Bar").is_some());
 }
 
 #[test]
@@ -1066,7 +1066,7 @@ contract LinkTest {
     let compiled = tmp.compile().unwrap();
     assert!(!compiled.has_compiler_errors());
 
-    let contract = compiled.find("LinkTest").unwrap();
+    let contract = compiled.find_first("LinkTest").unwrap();
     let bytecode = &contract.bytecode.as_ref().unwrap().object;
     assert!(bytecode.is_unlinked());
     let s = bytecode.as_str().unwrap();
@@ -1111,8 +1111,8 @@ library MyLib {
     let compiled = tmp.compile().unwrap();
     assert!(!compiled.has_compiler_errors());
 
-    assert!(compiled.find("MyLib").is_some());
-    let contract = compiled.find("LinkTest").unwrap();
+    assert!(compiled.find_first("MyLib").is_some());
+    let contract = compiled.find_first("LinkTest").unwrap();
     let bytecode = &contract.bytecode.as_ref().unwrap().object;
     assert!(bytecode.is_unlinked());
 
@@ -1126,8 +1126,8 @@ library MyLib {
     let compiled = tmp.compile().unwrap();
     assert!(!compiled.has_compiler_errors());
 
-    assert!(compiled.find("MyLib").is_some());
-    let contract = compiled.find("LinkTest").unwrap();
+    assert!(compiled.find_first("MyLib").is_some());
+    let contract = compiled.find_first("LinkTest").unwrap();
     let bytecode = &contract.bytecode.as_ref().unwrap().object;
     assert!(!bytecode.is_unlinked());
 
@@ -1138,8 +1138,8 @@ library MyLib {
     let compiled = tmp.compile().unwrap();
     assert!(!compiled.has_compiler_errors());
 
-    assert!(compiled.find("MyLib").is_some());
-    let contract = compiled.find("LinkTest").unwrap();
+    assert!(compiled.find_first("MyLib").is_some());
+    let contract = compiled.find_first("LinkTest").unwrap();
     let bytecode = &contract.bytecode.as_ref().unwrap().object;
     assert!(!bytecode.is_unlinked());
 }
@@ -1183,8 +1183,8 @@ library MyLib {
     let compiled = tmp.compile().unwrap();
     assert!(!compiled.has_compiler_errors());
 
-    assert!(compiled.find("MyLib").is_some());
-    let contract = compiled.find("LinkTest").unwrap();
+    assert!(compiled.find_first("MyLib").is_some());
+    let contract = compiled.find_first("LinkTest").unwrap();
     let bytecode = &contract.bytecode.as_ref().unwrap().object;
     assert!(bytecode.is_unlinked());
 
@@ -1195,8 +1195,8 @@ library MyLib {
     let compiled = tmp.compile().unwrap();
     assert!(!compiled.has_compiler_errors());
 
-    assert!(compiled.find("MyLib").is_some());
-    let contract = compiled.find("LinkTest").unwrap();
+    assert!(compiled.find_first("MyLib").is_some());
+    let contract = compiled.find_first("LinkTest").unwrap();
     let bytecode = &contract.bytecode.as_ref().unwrap().object;
     assert!(!bytecode.is_unlinked());
 }
@@ -1223,12 +1223,12 @@ fn can_recompile_with_changes() {
 
     let compiled = tmp.compile().unwrap();
     assert!(!compiled.has_compiler_errors());
-    assert!(compiled.find("A").is_some());
-    assert!(compiled.find("B").is_some());
+    assert!(compiled.find_first("A").is_some());
+    assert!(compiled.find_first("B").is_some());
 
     let compiled = tmp.compile().unwrap();
-    assert!(compiled.find("A").is_some());
-    assert!(compiled.find("B").is_some());
+    assert!(compiled.find_first("A").is_some());
+    assert!(compiled.find_first("B").is_some());
     assert!(compiled.is_unchanged());
 
     // modify A.sol
@@ -1236,8 +1236,8 @@ fn can_recompile_with_changes() {
     let compiled = tmp.compile().unwrap();
     assert!(!compiled.has_compiler_errors());
     assert!(!compiled.is_unchanged());
-    assert!(compiled.find("A").is_some());
-    assert!(compiled.find("B").is_some());
+    assert!(compiled.find_first("A").is_some());
+    assert!(compiled.find_first("B").is_some());
 }
 
 #[test]
@@ -1272,18 +1272,18 @@ fn can_recompile_with_lowercase_names() {
 
     let compiled = tmp.compile().unwrap();
     assert!(!compiled.has_compiler_errors());
-    assert!(compiled.find("DeployProxy").is_some());
-    assert!(compiled.find("UpgradeProxy").is_some());
-    assert!(compiled.find("ProxyAdmin").is_some());
+    assert!(compiled.find_first("DeployProxy").is_some());
+    assert!(compiled.find_first("UpgradeProxy").is_some());
+    assert!(compiled.find_first("ProxyAdmin").is_some());
 
     let artifacts = tmp.artifacts_snapshot().unwrap();
     assert_eq!(artifacts.artifacts.as_ref().len(), 3);
     artifacts.assert_artifacts_essentials_present();
 
     let compiled = tmp.compile().unwrap();
-    assert!(compiled.find("DeployProxy").is_some());
-    assert!(compiled.find("UpgradeProxy").is_some());
-    assert!(compiled.find("ProxyAdmin").is_some());
+    assert!(compiled.find_first("DeployProxy").is_some());
+    assert!(compiled.find_first("UpgradeProxy").is_some());
+    assert!(compiled.find_first("ProxyAdmin").is_some());
     assert!(compiled.is_unchanged());
 
     // modify upgradeProxy.sol
@@ -1291,9 +1291,9 @@ fn can_recompile_with_lowercase_names() {
     let compiled = tmp.compile().unwrap();
     assert!(!compiled.has_compiler_errors());
     assert!(!compiled.is_unchanged());
-    assert!(compiled.find("DeployProxy").is_some());
-    assert!(compiled.find("UpgradeProxy").is_some());
-    assert!(compiled.find("ProxyAdmin").is_some());
+    assert!(compiled.find_first("DeployProxy").is_some());
+    assert!(compiled.find_first("UpgradeProxy").is_some());
+    assert!(compiled.find_first("ProxyAdmin").is_some());
 
     let artifacts = tmp.artifacts_snapshot().unwrap();
     assert_eq!(artifacts.artifacts.as_ref().len(), 3);
@@ -1331,12 +1331,12 @@ fn can_recompile_unchanged_with_empty_files() {
 
     let compiled = tmp.compile().unwrap();
     assert!(!compiled.has_compiler_errors());
-    assert!(compiled.find("A").is_some());
-    assert!(compiled.find("C").is_some());
+    assert!(compiled.find_first("A").is_some());
+    assert!(compiled.find_first("C").is_some());
 
     let compiled = tmp.compile().unwrap();
-    assert!(compiled.find("A").is_some());
-    assert!(compiled.find("C").is_some());
+    assert!(compiled.find_first("A").is_some());
+    assert!(compiled.find_first("C").is_some());
     assert!(compiled.is_unchanged());
 
     // modify C.sol
@@ -1344,8 +1344,8 @@ fn can_recompile_unchanged_with_empty_files() {
     let compiled = tmp.compile().unwrap();
     assert!(!compiled.has_compiler_errors());
     assert!(!compiled.is_unchanged());
-    assert!(compiled.find("A").is_some());
-    assert!(compiled.find("C").is_some());
+    assert!(compiled.find_first("A").is_some());
+    assert!(compiled.find_first("C").is_some());
 }
 
 #[test]
@@ -1380,8 +1380,8 @@ contract Contract {
 
     let compiled = tmp.compile().unwrap();
     assert!(!compiled.has_compiler_errors());
-    assert!(compiled.find("Contract").is_some());
-    assert!(compiled.find("top_level").is_some());
+    assert!(compiled.find_first("Contract").is_some());
+    assert!(compiled.find_first("top_level").is_some());
     let mut artifacts = tmp.artifacts_snapshot().unwrap();
 
     assert_eq!(artifacts.artifacts.as_ref().len(), 2);
@@ -1536,11 +1536,11 @@ fn can_compile_sparse_with_link_references() {
 
     let mut output = compiled.clone().output();
 
-    assert!(compiled.find("ATest").is_some());
-    assert!(compiled.find("MyLib").is_some());
-    let lib = compiled.remove("MyLib").unwrap();
+    assert!(compiled.find_first("ATest").is_some());
+    assert!(compiled.find_first("MyLib").is_some());
+    let lib = compiled.remove_first("MyLib").unwrap();
     assert!(lib.bytecode.is_some());
-    let lib = compiled.remove("MyLib");
+    let lib = compiled.remove_first("MyLib");
     assert!(lib.is_none());
 
     let mut dup = output.clone();
@@ -1578,7 +1578,7 @@ fn can_sanitize_bytecode_hash() {
 
     let compiled = tmp.compile().unwrap();
     assert!(!compiled.has_compiler_errors());
-    assert!(compiled.find("A").is_some());
+    assert!(compiled.find_first("A").is_some());
 }
 
 #[test]
@@ -1614,7 +1614,7 @@ fn can_compile_model_checker_sample() {
     });
     let compiled = project.compile().unwrap();
 
-    assert!(compiled.find("Assert").is_some());
+    assert!(compiled.find_first("Assert").is_some());
     assert!(!compiled.has_compiler_errors());
     assert!(compiled.has_compiler_warnings());
 }
@@ -1735,8 +1735,8 @@ fn can_parse_notice() {
     let mut compiled = project.compile().unwrap();
     assert!(!compiled.has_compiler_errors());
     assert!(!compiled.is_unchanged());
-    assert!(compiled.find("Contract").is_some());
-    let userdoc = compiled.remove("Contract").unwrap().userdoc;
+    assert!(compiled.find_first("Contract").is_some());
+    let userdoc = compiled.remove_first("Contract").unwrap().userdoc;
 
     assert_eq!(
         userdoc,
@@ -1759,8 +1759,8 @@ fn can_parse_notice() {
     let mut compiled = project.compile().unwrap();
     assert!(!compiled.has_compiler_errors());
     assert!(!compiled.is_unchanged());
-    assert!(compiled.find("Contract").is_some());
-    let userdoc = compiled.remove("Contract").unwrap().userdoc;
+    assert!(compiled.find_first("Contract").is_some());
+    let userdoc = compiled.remove_first("Contract").unwrap().userdoc;
 
     assert_eq!(
         userdoc,
@@ -1830,8 +1830,8 @@ contract NotERC20 is INotERC20 {
     assert!(!compiled.has_compiler_errors());
     assert!(!compiled.is_unchanged());
 
-    assert!(compiled.find("INotERC20").is_some());
-    let contract = compiled.remove("INotERC20").unwrap();
+    assert!(compiled.find_first("INotERC20").is_some());
+    let contract = compiled.remove_first("INotERC20").unwrap();
     assert_eq!(
         contract.userdoc,
         Some(UserDoc {
@@ -1901,8 +1901,8 @@ contract NotERC20 is INotERC20 {
         })
     );
 
-    assert!(compiled.find("NotERC20").is_some());
-    let contract = compiled.remove("NotERC20").unwrap();
+    assert!(compiled.find_first("NotERC20").is_some());
+    let contract = compiled.remove_first("NotERC20").unwrap();
     assert_eq!(
         contract.userdoc,
         Some(UserDoc {