Repository: gakonst/ethers-rs
Pull request #1449 (2022-07-04 12:32:58)

refactor(solc): unify find/remove api
<!--
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
unify API to get/remove a contract/artifact for all types that contain mappings for `file -> name -> contract`
<!--
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
* `{find,remove}_first(name)`
* `{find,remove}(file, name)`
* `{find,remove}_contract(ContractInfo{path?, name})`

<!--
Summarize the solution and provide any necessary context needed to understand
the code change.
-->

## PR Checklist

- [ ] Added Tests
- [x] Added Documentation
- [ ] Updated the changelog



Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 67248d75..9795a618 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"
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/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-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 {