Repository: gakonst/ethers-rs
Pull request #1433 (2022-06-30 05:17:29)

feat(ethers-solc): configurable build-info output dir
<!--
Thank you for your Pull Request. Please provide a description above and review
the requirements below.

Bug fixes and new features should include tests.

Contributors guide: https://github.com/gakonst/ethers-rs/blob/master/CONTRIBUTING.md

The contributors guide includes instructions for running rustfmt and building the
documentation.
-->

## Motivation
I would like to be able to configure the output directory of the hh style bulidinfo files so that tooling that expects the build info files to be in a specific directory can just work.

<!--
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

Allows for the build info output directory to be configured. It should be backwards compatible by using the same directory that it currently uses by default. Will require a follow up PR in foundry to pass through the config value

<!--
Summarize the solution and provide any necessary context needed to understand
the code change.
-->

## PR Checklist

- [x] Added Tests
- [ ] Added Documentation
- [x] Updated the changelog


tynes: > this is great!
> 
> we can follow up with a corresponding config value in foundry.toml
> 
> the way that works is, the `Config` struct contains all settings variables: https://github.com/foundry-rs/foundry/blob/master/config/src/lib.rs#L90-L108
> 
> which are picked up by foundry.toml, env, cli args ...
> 
> and are then used to create the ethers-solc::Project here
> 
> https://github.com/foundry-rs/foundry/blob/master/config/src/lib.rs#L528

Thanks for the tips! Will open PRs in foundry and the hh plugin once this version of ethers is pulled into foundry

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 78458657..d23b282b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
 
 ### Unreleased
 
+- Allow configuration of the output directory of the generated `BuildInfo` [#1433](https://github.com/gakonst/ethers-rs/pull/1433)
 - capture unknown fields in `Block` and `Transaction` type via new `OtherFields` type [#1423](https://github.com/gakonst/ethers-rs/pull/1423)
 - Methods like `set_to()` from `TypedTransaction` can be chained
 - Use H64 for Block Nonce [#1396](https://github.com/gakonst/ethers-rs/pull/1396)
diff --git a/ethers-core/src/types/transaction/response.rs b/ethers-core/src/types/transaction/response.rs
index 90d532de..54fe85b3 100644
--- a/ethers-core/src/types/transaction/response.rs
+++ b/ethers-core/src/types/transaction/response.rs
@@ -9,6 +9,7 @@ use crate::{
 };
 use rlp::{Decodable, DecoderError, RlpStream};
 use serde::{Deserialize, Serialize};
+use std::cmp::Ordering;
 
 /// Details of a signed transaction
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
@@ -434,6 +435,28 @@ impl rlp::Encodable for TransactionReceipt {
     }
 }
 
+// Compares the transaction receipt against another receipt by checking the blocks first and then
+// the transaction index in the block
+impl Ord for TransactionReceipt {
+    fn cmp(&self, other: &Self) -> Ordering {
+        match (self.block_number, other.block_number) {
+            (Some(number), Some(other_number)) => match number.cmp(&other_number) {
+                Ordering::Equal => self.transaction_index.cmp(&other.transaction_index),
+                ord => ord,
+            },
+            (Some(_), None) => Ordering::Less,
+            (None, Some(_)) => Ordering::Greater,
+            (None, None) => self.transaction_index.cmp(&other.transaction_index),
+        }
+    }
+}
+
+impl PartialOrd<Self> for TransactionReceipt {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
 #[cfg(test)]
 #[cfg(not(feature = "celo"))]
 mod tests {
@@ -886,4 +909,17 @@ mod tests {
         let receipt = serde_json::to_value(receipt).unwrap();
         assert_eq!(v, receipt);
     }
+
+    #[test]
+    fn can_sort_receipts() {
+        let mut a = TransactionReceipt { block_number: Some(0u64.into()), ..Default::default() };
+        let b = TransactionReceipt { block_number: Some(1u64.into()), ..Default::default() };
+        assert!(a < b);
+
+        a = b.clone();
+        assert_eq!(a.cmp(&b), Ordering::Equal);
+
+        a.transaction_index = 1u64.into();
+        assert!(a > b);
+    }
 }
diff --git a/ethers-solc/src/artifact_output/configurable.rs b/ethers-solc/src/artifact_output/configurable.rs
index a964b20e..7958a211 100644
--- a/ethers-solc/src/artifact_output/configurable.rs
+++ b/ethers-solc/src/artifact_output/configurable.rs
@@ -1,4 +1,12 @@
 //! A configurable artifacts handler implementation
+//!
+//! Configuring artifacts requires two pieces: the `ConfigurableArtifacts` handler, which contains
+//! the configuration of how to construct the `ConfigurableArtifact` type based on a `Contract`. The
+//! `ConfigurableArtifacts` populates a single `Artifact`, the `ConfigurableArtifact`, by default
+//! with essential entries only, such as `abi`, `bytecode`,..., but may include additional values
+//! based on its `ExtraOutputValues` that maps to various objects in the solc contract output, see
+//! also: [`OutputSelection`](crate::artifacts::OutputSelection). In addition to that some output
+//! values can also be emitted as standalone files.
 
 use crate::{
     artifacts::{
@@ -34,6 +42,8 @@ pub struct ConfigurableContractArtifact {
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub assembly: Option<String>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub opcodes: Option<String>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
     pub method_identifiers: Option<BTreeMap<String, String>>,
     #[serde(default, skip_serializing_if = "Vec::is_empty")]
     pub generated_sources: Vec<GeneratedSource>,
@@ -248,6 +258,7 @@ impl ArtifactOutput for ConfigurableArtifacts {
         let mut artifact_assembly = None;
         let mut artifact_storage_layout = None;
         let mut generated_sources = None;
+        let mut opcodes = None;
 
         let Contract {
             abi,
@@ -298,13 +309,17 @@ impl ArtifactOutput for ConfigurableArtifacts {
 
             if self.additional_values.function_debug_data {
                 artifact_function_debug_data =
-                    bytecode.as_ref().map(|b| b.function_debug_data.clone());
+                    bytecode.as_mut().map(|code| std::mem::take(&mut code.function_debug_data));
             }
             if self.additional_values.generated_sources {
                 generated_sources =
                     bytecode.as_mut().map(|code| std::mem::take(&mut code.generated_sources));
             }
 
+            if self.additional_values.opcodes {
+                opcodes = bytecode.as_mut().and_then(|code| code.opcodes.take())
+            }
+
             artifact_bytecode = bytecode.map(Into::into);
             artifact_deployed_bytecode = deployed_bytecode.map(Into::into);
             artifact_method_identifiers = Some(method_identifiers);
@@ -322,6 +337,7 @@ impl ArtifactOutput for ConfigurableArtifacts {
             bytecode: artifact_bytecode,
             deployed_bytecode: artifact_deployed_bytecode,
             assembly: artifact_assembly,
+            opcodes,
             function_debug_data: artifact_function_debug_data,
             method_identifiers: artifact_method_identifiers,
             gas_estimates: artifact_gas_estimates,
@@ -373,6 +389,7 @@ pub struct ExtraOutputValues {
     pub function_debug_data: bool,
     pub generated_sources: bool,
     pub source_map: bool,
+    pub opcodes: bool,
 
     /// PRIVATE: This structure may grow, As such, constructing this structure should
     /// _always_ be done using a public constructor or update syntax:
@@ -408,6 +425,7 @@ impl ExtraOutputValues {
             function_debug_data: true,
             generated_sources: true,
             source_map: true,
+            opcodes: true,
             __non_exhaustive: (),
         }
     }
@@ -444,6 +462,7 @@ impl ExtraOutputValues {
                         config.method_identifiers = true;
                         config.generated_sources = true;
                         config.source_map = true;
+                        config.opcodes = true;
                     }
                     EvmOutputSelection::Assembly => {
                         config.assembly = true;
@@ -457,6 +476,9 @@ impl ExtraOutputValues {
                     EvmOutputSelection::ByteCode(BytecodeOutputSelection::FunctionDebugData) => {
                         config.function_debug_data = true;
                     }
+                    EvmOutputSelection::ByteCode(BytecodeOutputSelection::Opcodes) => {
+                        config.opcodes = true;
+                    }
                     EvmOutputSelection::ByteCode(BytecodeOutputSelection::GeneratedSources) => {
                         config.generated_sources = true;
                     }
diff --git a/ethers-solc/src/artifact_output/mod.rs b/ethers-solc/src/artifact_output/mod.rs
index db5be8f9..616e6cdb 100644
--- a/ethers-solc/src/artifact_output/mod.rs
+++ b/ethers-solc/src/artifact_output/mod.rs
@@ -1,8 +1,16 @@
 //! Output artifact handling
 
 use crate::{
-    artifacts::FileToContractsMap, error::Result, utils, HardhatArtifact, ProjectPathsConfig,
-    SolcError,
+    artifacts::{
+        contract::{CompactContract, CompactContractBytecode, Contract},
+        BytecodeObject, CompactBytecode, CompactContractBytecodeCow, CompactDeployedBytecode,
+        FileToContractsMap, SourceFile,
+    },
+    compile::output::{contracts::VersionedContracts, sources::VersionedSourceFiles},
+    error::Result,
+    sourcemap::{SourceMap, SyntaxError},
+    sources::VersionedSourceFile,
+    utils, HardhatArtifact, ProjectPathsConfig, SolcError,
 };
 use ethers_core::{abi::Abi, types::Bytes};
 use semver::Version;
@@ -13,18 +21,7 @@ use std::{
     fmt, fs, io,
     path::{Path, PathBuf},
 };
-
 mod configurable;
-use crate::{
-    artifacts::{
-        contract::{CompactContract, CompactContractBytecode, Contract},
-        BytecodeObject, CompactBytecode, CompactContractBytecodeCow, CompactDeployedBytecode,
-        SourceFile,
-    },
-    compile::output::{contracts::VersionedContracts, sources::VersionedSourceFiles},
-    sourcemap::{SourceMap, SyntaxError},
-    sources::VersionedSourceFile,
-};
 pub use configurable::*;
 
 /// Represents unique artifact metadata for identifying artifacts on output
diff --git a/ethers-solc/src/artifacts/mod.rs b/ethers-solc/src/artifacts/mod.rs
index eed1593f..c08b64b2 100644
--- a/ethers-solc/src/artifacts/mod.rs
+++ b/ethers-solc/src/artifacts/mod.rs
@@ -422,6 +422,24 @@ impl Settings {
                 })
                 .collect(),
         );
+
+        if let Some(mut model_checker) = self.model_checker.take() {
+            model_checker.contracts = model_checker
+                .contracts
+                .into_iter()
+                .map(|(path, contracts)| {
+                    (
+                        Path::new(&path)
+                            .strip_prefix(base)
+                            .map(|p| format!("{}", p.display()))
+                            .unwrap_or(path),
+                        contracts,
+                    )
+                })
+                .collect();
+            self.model_checker = Some(model_checker);
+        }
+
         self
     }
 }
diff --git a/ethers-solc/src/compile/output/contracts.rs b/ethers-solc/src/compile/output/contracts.rs
index 6a2f0873..f8de71b2 100644
--- a/ethers-solc/src/compile/output/contracts.rs
+++ b/ethers-solc/src/compile/output/contracts.rs
@@ -166,6 +166,28 @@ impl VersionedContracts {
         })
     }
 
+    /// Returns an iterator over (`file`, `name`, `Contract`)
+    pub fn into_contracts_with_files(self) -> impl Iterator<Item = (String, String, Contract)> {
+        self.0.into_iter().flat_map(|(file, contracts)| {
+            contracts.into_iter().flat_map(move |(name, c)| {
+                let file = file.clone();
+                c.into_iter().map(move |c| (file.clone(), name.clone(), c.contract))
+            })
+        })
+    }
+
+    /// Returns an iterator over (`file`, `name`, `Contract`, `Version`)
+    pub fn into_contracts_with_files_and_version(
+        self,
+    ) -> impl Iterator<Item = (String, String, Contract, Version)> {
+        self.0.into_iter().flat_map(|(file, contracts)| {
+            contracts.into_iter().flat_map(move |(name, c)| {
+                let file = file.clone();
+                c.into_iter().map(move |c| (file.clone(), name.clone(), c.contract, c.version))
+            })
+        })
+    }
+
     /// Sets the contract's file paths to `root` adjoined to `self.file`.
     pub fn join_all(&mut self, root: impl AsRef<Path>) -> &mut Self {
         let root = root.as_ref();
diff --git a/ethers-solc/src/compile/output/info.rs b/ethers-solc/src/compile/output/info.rs
index 8e118cea..3094bd9a 100644
--- a/ethers-solc/src/compile/output/info.rs
+++ b/ethers-solc/src/compile/output/info.rs
@@ -1,5 +1,5 @@
 //! Commonly used identifiers for contracts in the compiled output
-use std::{convert::TryFrom, fmt, str::FromStr};
+use std::{borrow::Cow, convert::TryFrom, fmt, str::FromStr};
 
 #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
 #[error("{0}")]
@@ -77,6 +77,39 @@ impl From<FullContractInfo> for ContractInfo {
     }
 }
 
+/// The reference type for `ContractInfo`
+#[derive(Clone, Debug, Eq, PartialEq, Hash)]
+pub struct ContractInfoRef<'a> {
+    pub path: Option<Cow<'a, str>>,
+    pub name: Cow<'a, str>,
+}
+
+impl<'a> From<ContractInfo> for ContractInfoRef<'a> {
+    fn from(info: ContractInfo) -> Self {
+        ContractInfoRef { path: info.path.map(Into::into), name: info.name.into() }
+    }
+}
+
+impl<'a> From<&'a ContractInfo> for ContractInfoRef<'a> {
+    fn from(info: &'a ContractInfo) -> Self {
+        ContractInfoRef {
+            path: info.path.as_deref().map(Into::into),
+            name: info.name.as_str().into(),
+        }
+    }
+}
+impl<'a> From<FullContractInfo> for ContractInfoRef<'a> {
+    fn from(info: FullContractInfo) -> Self {
+        ContractInfoRef { path: Some(info.path.into()), name: info.name.into() }
+    }
+}
+
+impl<'a> From<&'a FullContractInfo> for ContractInfoRef<'a> {
+    fn from(info: &'a FullContractInfo) -> Self {
+        ContractInfoRef { path: Some(info.path.as_str().into()), name: info.name.as_str().into() }
+    }
+}
+
 /// Represents the common contract argument pattern `<path>:<contractname>`
 #[derive(Clone, Debug, Eq, PartialEq, Hash)]
 pub struct FullContractInfo {
diff --git a/ethers-solc/src/compile/output/mod.rs b/ethers-solc/src/compile/output/mod.rs
index c7e10779..bcb5e1e8 100644
--- a/ethers-solc/src/compile/output/mod.rs
+++ b/ethers-solc/src/compile/output/mod.rs
@@ -6,6 +6,7 @@ use crate::{
         Error,
     },
     buildinfo::RawBuildInfo,
+    info::ContractInfoRef,
     sources::{VersionedSourceFile, VersionedSourceFiles},
     ArtifactId, ArtifactOutput, Artifacts, CompilerOutput, ConfigurableArtifacts, SolcIoError,
 };
@@ -378,6 +379,38 @@ impl AggregatedCompilerOutput {
         self.contracts.remove(path, contract)
     }
 
+    /// 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().output();
+    /// 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<Contract> {
+        let ContractInfoRef { path, name } = info.into();
+        if let Some(path) = path {
+            self.remove(path, name)
+        } else {
+            self.remove_first(name)
+        }
+    }
+
     /// Iterate over all contracts and their names
     pub fn contracts_iter(&self) -> impl Iterator<Item = (&String, &Contract)> {
         self.contracts.contracts()
@@ -388,6 +421,32 @@ impl AggregatedCompilerOutput {
         self.contracts.into_contracts()
     }
 
+    /// Returns an iterator over (`file`, `name`, `Contract`)
+    pub fn contracts_with_files_iter(&self) -> impl Iterator<Item = (&String, &String, &Contract)> {
+        self.contracts.contracts_with_files()
+    }
+
+    /// Returns an iterator over (`file`, `name`, `Contract`)
+    pub fn contracts_with_files_into_iter(
+        self,
+    ) -> impl Iterator<Item = (String, String, Contract)> {
+        self.contracts.into_contracts_with_files()
+    }
+
+    /// Returns an iterator over (`file`, `name`, `Contract`, `Version`)
+    pub fn contracts_with_files_and_version_iter(
+        &self,
+    ) -> impl Iterator<Item = (&String, &String, &Contract, &Version)> {
+        self.contracts.contracts_with_files_and_version()
+    }
+
+    /// Returns an iterator over (`file`, `name`, `Contract`, `Version`)
+    pub fn contracts_with_files_and_version_into_iter(
+        self,
+    ) -> impl Iterator<Item = (String, String, Contract, Version)> {
+        self.contracts.into_contracts_with_files_and_version()
+    }
+
     /// Given the contract file's path and the contract's name, tries to return the contract's
     /// bytecode, runtime bytecode, and abi
     /// # Example
diff --git a/ethers-solc/src/config.rs b/ethers-solc/src/config.rs
index ed1bfbba..50621940 100644
--- a/ethers-solc/src/config.rs
+++ b/ethers-solc/src/config.rs
@@ -25,6 +25,8 @@ pub struct ProjectPathsConfig {
     pub cache: PathBuf,
     /// Where to store build artifacts
     pub artifacts: PathBuf,
+    /// Where to store the build info files
+    pub build_infos: PathBuf,
     /// Where to find sources
     pub sources: PathBuf,
     /// Where to find tests
@@ -67,6 +69,7 @@ impl ProjectPathsConfig {
     pub fn paths(&self) -> ProjectPaths {
         ProjectPaths {
             artifacts: self.artifacts.clone(),
+            build_infos: self.build_infos.clone(),
             sources: self.sources.clone(),
             tests: self.tests.clone(),
             scripts: self.scripts.clone(),
@@ -387,6 +390,7 @@ impl fmt::Display for ProjectPathsConfig {
 #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
 pub struct ProjectPaths {
     pub artifacts: PathBuf,
+    pub build_infos: PathBuf,
     pub sources: PathBuf,
     pub tests: PathBuf,
     pub scripts: PathBuf,
@@ -398,6 +402,7 @@ impl ProjectPaths {
     pub fn join_all(&mut self, root: impl AsRef<Path>) -> &mut Self {
         let root = root.as_ref();
         self.artifacts = root.join(&self.artifacts);
+        self.build_infos = root.join(&self.build_infos);
         self.sources = root.join(&self.sources);
         self.tests = root.join(&self.tests);
         self.scripts = root.join(&self.scripts);
@@ -413,6 +418,9 @@ impl ProjectPaths {
         if let Ok(prefix) = self.artifacts.strip_prefix(base) {
             self.artifacts = prefix.to_path_buf();
         }
+        if let Ok(prefix) = self.build_infos.strip_prefix(base) {
+            self.build_infos = prefix.to_path_buf();
+        }
         if let Ok(prefix) = self.sources.strip_prefix(base) {
             self.sources = prefix.to_path_buf();
         }
@@ -436,6 +444,7 @@ impl Default for ProjectPaths {
     fn default() -> Self {
         Self {
             artifacts: "out".into(),
+            build_infos: ["out", "build-info"].iter().collect::<PathBuf>(),
             sources: "src".into(),
             tests: "test".into(),
             scripts: "script".into(),
@@ -460,6 +469,7 @@ impl PathStyle {
             PathStyle::Dapptools => ProjectPathsConfig::builder()
                 .sources(root.join("src"))
                 .artifacts(root.join("out"))
+                .build_infos(root.join("out").join("build-info"))
                 .lib(root.join("lib"))
                 .remappings(Remapping::find_many(&root.join("lib")))
                 .root(root)
@@ -467,6 +477,7 @@ impl PathStyle {
             PathStyle::HardHat => ProjectPathsConfig::builder()
                 .sources(root.join("contracts"))
                 .artifacts(root.join("artifacts"))
+                .build_infos(root.join("artifacts").join("build-info"))
                 .lib(root.join("node_modules"))
                 .root(root)
                 .build()?,
@@ -479,6 +490,7 @@ pub struct ProjectPathsConfigBuilder {
     root: Option<PathBuf>,
     cache: Option<PathBuf>,
     artifacts: Option<PathBuf>,
+    build_infos: Option<PathBuf>,
     sources: Option<PathBuf>,
     tests: Option<PathBuf>,
     scripts: Option<PathBuf>,
@@ -502,6 +514,11 @@ impl ProjectPathsConfigBuilder {
         self
     }
 
+    pub fn build_infos(mut self, build_infos: impl Into<PathBuf>) -> Self {
+        self.build_infos = Some(utils::canonicalized(build_infos));
+        self
+    }
+
     pub fn sources(mut self, sources: impl Into<PathBuf>) -> Self {
         self.sources = Some(utils::canonicalized(sources));
         self
@@ -561,6 +578,9 @@ impl ProjectPathsConfigBuilder {
             artifacts: self
                 .artifacts
                 .unwrap_or_else(|| ProjectPathsConfig::find_artifacts_dir(&root)),
+            build_infos: self.build_infos.unwrap_or_else(|| {
+                ProjectPathsConfig::find_artifacts_dir(&root).join("build-info")
+            }),
             sources: self.sources.unwrap_or_else(|| ProjectPathsConfig::find_source_dir(&root)),
             tests: self.tests.unwrap_or_else(|| root.join("test")),
             scripts: self.scripts.unwrap_or_else(|| root.join("script")),
@@ -702,6 +722,7 @@ mod tests {
         let root = crate::utils::tempdir("root").unwrap();
         let out = root.path().join("out");
         let artifacts = root.path().join("artifacts");
+        let build_infos = artifacts.join("build-info");
         let contracts = root.path().join("contracts");
         let src = root.path().join("src");
         let lib = root.path().join("lib");
@@ -729,6 +750,11 @@ mod tests {
             ProjectPathsConfig::builder().build_with_root(&root).artifacts,
             utils::canonicalized(artifacts),
         );
+        assert_eq!(
+            ProjectPathsConfig::builder().build_with_root(&root).build_infos,
+            utils::canonicalized(build_infos)
+        );
+
         std::fs::File::create(&out).unwrap();
         assert_eq!(ProjectPathsConfig::find_artifacts_dir(root), out,);
         assert_eq!(
diff --git a/ethers-solc/src/lib.rs b/ethers-solc/src/lib.rs
index e51cb441..ccf9f63a 100644
--- a/ethers-solc/src/lib.rs
+++ b/ethers-solc/src/lib.rs
@@ -125,8 +125,8 @@ impl<T: ArtifactOutput> Project<T> {
     }
 
     /// Returns the path to the `build-info` directory nested in the artifacts dir
-    pub fn build_info_path(&self) -> PathBuf {
-        self.paths.artifacts.join("build-info")
+    pub fn build_info_path(&self) -> &PathBuf {
+        &self.paths.build_infos
     }
 
     /// Returns the root directory of the project


Test patch:
diff --git a/ethers-solc/tests/project.rs b/ethers-solc/tests/project.rs
index f02ef995..f126f5c3 100644
--- a/ethers-solc/tests/project.rs
+++ b/ethers-solc/tests/project.rs
@@ -15,6 +15,7 @@ use ethers_solc::{
     },
     buildinfo::BuildInfo,
     cache::{SolFilesCache, SOLIDITY_FILES_CACHE_FILENAME},
+    info::ContractInfo,
     project_util::*,
     remappings::Remapping,
     CompilerInput, ConfigurableArtifacts, ExtraOutputValues, Graph, Project, ProjectCompileOutput,
@@ -139,6 +140,7 @@ fn can_compile_configured() {
             metadata: true,
             ir: true,
             ir_optimized: true,
+            opcodes: true,
             ..Default::default()
         },
         ..Default::default()
@@ -152,6 +154,7 @@ fn can_compile_configured() {
     assert!(artifact.raw_metadata.is_some());
     assert!(artifact.ir.is_some());
     assert!(artifact.ir_optimized.is_some());
+    assert!(artifact.opcodes.is_some());
 }
 
 #[test]
@@ -1546,9 +1549,16 @@ fn can_compile_sparse_with_link_references() {
     let lib = dup.remove_first("MyLib");
     assert!(lib.is_none());
 
-    let lib = output.remove(my_lib_path.to_string_lossy(), "MyLib");
+    dup = output.clone();
+    let lib = dup.remove(my_lib_path.to_string_lossy(), "MyLib");
     assert!(lib.is_some());
-    let lib = output.remove(my_lib_path.to_string_lossy(), "MyLib");
+    let lib = dup.remove(my_lib_path.to_string_lossy(), "MyLib");
+    assert!(lib.is_none());
+
+    let info = ContractInfo::new(format!("{}:{}", my_lib_path.to_string_lossy(), "MyLib"));
+    let lib = output.remove_contract(&info);
+    assert!(lib.is_some());
+    let lib = output.remove_contract(&info);
     assert!(lib.is_none());
 }