Repository: gakonst/ethers-rs
Pull request #1446 (2022-07-03 14:21:05)

chore(deps): solang 0.1.16
## Motivation

upgrade solang for use in foundry formatter



Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 67248d75..b52c0fb9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
@@ -3647,9 +3673,9 @@ dependencies = [
 
 [[package]]
 name = "solang-parser"
-version = "0.1.14"
+version = "0.1.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4a62cd1bd34217d4ac27aa4fad0e868983583a0c54cacc3a8590ea7029b50c2b"
+checksum = "a1f68b8280e3f354d5646218319bcee4febe42833cef5ebf653cfc49d0a94409"
 dependencies = [
  "itertools",
  "lalrpop",
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-solc/Cargo.toml b/ethers-solc/Cargo.toml
index 4e9c0d6d..d7b257c9 100644
--- a/ethers-solc/Cargo.toml
+++ b/ethers-solc/Cargo.toml
@@ -35,7 +35,7 @@ tempfile = { version = "3.3.0", optional = true }
 fs_extra = { version = "1.2.0", optional = true }
 sha2 = { version = "0.9.8", default-features = false, optional = true }
 dunce = "1.0.2"
-solang-parser = { default-features = false, version = "=0.1.14" }
+solang-parser = { default-features = false, version = "=0.1.16" }
 rayon = "1.5.3"
 rand = { version = "0.8.5", optional = true }
 path-slash = "0.1.5"
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 {