From 19d2fd115529ec18652f2ce544a9e52d70047883 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Thu, 17 Feb 2022 16:31:35 +0100 Subject: [PATCH] feat(solc): add configurable Artifact type (#907) * refactor: make artifacts a sub mod * feat: more options for output selection * feat(solc): make ArtifactOutput struct functions * fix: migrate all features * feat: add configurable artifacts type * refactor: move configurable to separate file * feat: impl ArtifactOutput * refactor: write extras * simplify write extra * feat: more helper functions * feat: implement delegate * fix: failing doc test * fix: rustfmt * chore: update CHANGELOG * feat: add helper functions * refactor: remove flatten * feat: add link function * feat: replace default type * fix: doc tests * feat: more utility functions * fix: failing tests * chore: rename types * chore: bump ethers-solc 0.3.0 * fix: set metadata file extension properly --- CHANGELOG.md | 2 + Cargo.lock | 2 +- Cargo.toml | 2 +- ethers-contract/Cargo.toml | 2 +- ethers-etherscan/Cargo.toml | 2 +- ethers-etherscan/src/contract.rs | 2 +- ethers-middleware/Cargo.toml | 2 +- ethers-solc/Cargo.toml | 2 +- .../src/artifact_output/configurable.rs | 486 ++++++++++++++++++ .../mod.rs} | 78 ++- ethers-solc/src/artifacts/mod.rs | 79 ++- ethers-solc/src/artifacts/output_selection.rs | 67 ++- ethers-solc/src/compile/output.rs | 38 +- ethers-solc/src/compile/project.rs | 8 +- ethers-solc/src/config.rs | 36 +- ethers-solc/src/hh.rs | 8 +- ethers-solc/src/lib.rs | 157 ++++-- ethers-solc/src/project_util.rs | 66 ++- ethers-solc/tests/project.rs | 46 +- examples/ethers-wasm/Cargo.toml | 2 +- 20 files changed, 935 insertions(+), 152 deletions(-) create mode 100644 ethers-solc/src/artifact_output/configurable.rs rename ethers-solc/src/{artifact_output.rs => artifact_output/mod.rs} (89%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26cbba18..df428694 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,8 @@ ### Unreleased +- Let `Project` take ownership of `ArtifactOutput` and change trait interface + [#907](https://github.com/gakonst/ethers-rs/pull/907) - Total revamp of the `Project::compile` pipeline [#802](https://github.com/gakonst/ethers-rs/pull/802) - Support multiple versions of compiled contracts diff --git a/Cargo.lock b/Cargo.lock index 025b652f..647ba046 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1375,7 +1375,7 @@ dependencies = [ [[package]] name = "ethers-solc" -version = "0.2.0" +version = "0.3.0" dependencies = [ "colored", "criterion", diff --git a/Cargo.toml b/Cargo.toml index dfcdecec..e9916114 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,7 +88,7 @@ ethers-core = { version = "^0.6.0", default-features = false, path = "./ethers-c ethers-providers = { version = "^0.6.0", default-features = false, path = "./ethers-providers" } ethers-signers = { version = "^0.6.0", default-features = false, path = "./ethers-signers" } ethers-middleware = { version = "^0.6.0", default-features = false, path = "./ethers-middleware" } -ethers-solc = { version = "^0.2.0", default-features = false, path = "./ethers-solc" } +ethers-solc = { version = "^0.3.0", default-features = false, path = "./ethers-solc" } ethers-etherscan = { version = "^0.2.0", default-features = false, path = "./ethers-etherscan" } [dev-dependencies] diff --git a/ethers-contract/Cargo.toml b/ethers-contract/Cargo.toml index d3ac7f09..17b1eef0 100644 --- a/ethers-contract/Cargo.toml +++ b/ethers-contract/Cargo.toml @@ -32,7 +32,7 @@ ethers-contract-abigen = { version = "^0.6.0", path = "ethers-contract-abigen" } ethers-contract-derive = { version = "^0.6.0", path = "ethers-contract-derive" } ethers-core = { version = "^0.6.0", path = "../ethers-core", default-features = false, features = ["eip712"]} ethers-derive-eip712 = { version = "^0.2.0", path = "../ethers-core/ethers-derive-eip712"} -ethers-solc = { version = "^0.2.0", path = "../ethers-solc", default-features = false } +ethers-solc = { version = "^0.3.0", path = "../ethers-solc", default-features = false } [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] tokio = { version = "1.5", default-features = false, features = ["macros"] } diff --git a/ethers-etherscan/Cargo.toml b/ethers-etherscan/Cargo.toml index 8445b995..33b05f99 100644 --- a/ethers-etherscan/Cargo.toml +++ b/ethers-etherscan/Cargo.toml @@ -15,7 +15,7 @@ keywords = ["ethereum", "web3", "etherscan", "ethers"] [dependencies] ethers-core = { version = "^0.6.0", path = "../ethers-core", default-features = false } -ethers-solc = { version = "^0.2.0", path = "../ethers-solc", default-features = false } +ethers-solc = { version = "^0.3.0", path = "../ethers-solc", default-features = false } reqwest = { version = "0.11.9", default-features = false, features = ["json"] } serde = { version = "1.0.124", default-features = false, features = ["derive"] } serde_json = { version = "1.0.64", default-features = false } diff --git a/ethers-etherscan/src/contract.rs b/ethers-etherscan/src/contract.rs index 9ee33bf4..a3524b9b 100644 --- a/ethers-etherscan/src/contract.rs +++ b/ethers-etherscan/src/contract.rs @@ -311,7 +311,7 @@ mod tests { .sources(&root) .build() .expect("failed to resolve project paths"); - let project = Project::::builder() + let project = Project::builder() .paths(paths) .build() .expect("failed to build the project"); diff --git a/ethers-middleware/Cargo.toml b/ethers-middleware/Cargo.toml index 4c77dec9..9748dc3f 100644 --- a/ethers-middleware/Cargo.toml +++ b/ethers-middleware/Cargo.toml @@ -42,7 +42,7 @@ hex = { version = "0.4.3", default-features = false, features = ["std"] } rand = { version = "0.8.5", default-features = false } ethers-providers = { version = "^0.6.0", path = "../ethers-providers", default-features = false, features = ["ws", "rustls"] } once_cell = "1.8.0" -ethers-solc = { version = "^0.2.0", path = "../ethers-solc", default-features = false } +ethers-solc = { version = "^0.3.0", path = "../ethers-solc", default-features = false } serial_test = "0.5.1" [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] diff --git a/ethers-solc/Cargo.toml b/ethers-solc/Cargo.toml index 0892d28b..0be7463a 100644 --- a/ethers-solc/Cargo.toml +++ b/ethers-solc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ethers-solc" -version = "0.2.0" +version = "0.3.0" authors = ["Matthias Seitz ", "Georgios Konstantopoulos "] license = "MIT OR Apache-2.0" edition = "2018" diff --git a/ethers-solc/src/artifact_output/configurable.rs b/ethers-solc/src/artifact_output/configurable.rs new file mode 100644 index 00000000..ae6b4162 --- /dev/null +++ b/ethers-solc/src/artifact_output/configurable.rs @@ -0,0 +1,486 @@ +//! A configurable artifacts handler implementation + +use crate::{ + artifacts::{ + output_selection::{ContractOutputSelection, EvmOutputSelection, EwasmOutputSelection}, + CompactBytecode, CompactContract, CompactContractBytecode, CompactDeployedBytecode, + CompactEvm, DevDoc, Ewasm, GasEstimates, Metadata, Offsets, Settings, StorageLayout, + UserDoc, + }, + ArtifactOutput, Contract, SolcConfig, SolcError, +}; +use ethers_core::abi::Abi; +use serde::{Deserialize, Serialize}; +use std::{collections::BTreeMap, fs, path::Path}; + +/// Represents the `Artifact` that `ConfigurableArtifacts` emits. +/// +/// This is essentially a superset of [`CompactContractBytecode`]. +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ConfigurableContractArtifact { + /// The Ethereum Contract ABI. If empty, it is represented as an empty + /// array. See https://docs.soliditylang.org/en/develop/abi-spec.html + pub abi: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub bytecode: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub deployed_bytecode: Option, + + #[serde(default, skip_serializing_if = "Option::is_none")] + pub assembly: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub method_identifiers: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub gas_estimates: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub metadata: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub storage_layout: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub userdoc: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub devdoc: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub ir: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub ir_optimized: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub ewasm: Option, +} + +impl ConfigurableContractArtifact { + /// Returns the inner element that contains the core bytecode related information + pub fn into_contract_bytecode(self) -> CompactContractBytecode { + self.into() + } + + /// Looks for all link references in deployment and runtime bytecodes + pub fn all_link_references(&self) -> BTreeMap>> { + let mut links = BTreeMap::new(); + if let Some(bcode) = &self.bytecode { + links.extend(bcode.link_references.clone()); + } + + if let Some(d_bcode) = &self.deployed_bytecode { + if let Some(bcode) = &d_bcode.bytecode { + links.extend(bcode.link_references.clone()); + } + } + links + } +} + +impl From for CompactContractBytecode { + fn from(artifact: ConfigurableContractArtifact) -> Self { + CompactContractBytecode { + abi: artifact.abi, + bytecode: artifact.bytecode, + deployed_bytecode: artifact.deployed_bytecode, + } + } +} + +impl From for CompactContract { + fn from(artifact: ConfigurableContractArtifact) -> Self { + CompactContractBytecode::from(artifact).into() + } +} + +/// An `Artifact` implementation that can be configured to include additional content and emit +/// additional files +/// +/// Creates a single json artifact with +/// ```json +/// { +/// "abi": [], +/// "bytecode": {...}, +/// "deployedBytecode": {...} +/// // additional values +/// } +/// ``` +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] +pub struct ConfigurableArtifacts { + /// A set of additional values to include in the contract's artifact file + pub additional_values: ExtraOutputValues, + + /// A set of values that should be written to a separate file + pub additional_files: ExtraOutputFiles, + + /// PRIVATE: This structure may grow, As such, constructing this structure should + /// _always_ be done using a public constructor or update syntax: + /// + /// ```rust + /// + /// use ethers_solc::{ExtraOutputFiles, ConfigurableArtifacts}; + /// let config = ConfigurableArtifacts { + /// additional_files: ExtraOutputFiles { metadata: true, ..Default::default() }, + /// ..Default::default() + /// }; + /// ``` + #[doc(hidden)] + pub __non_exhaustive: (), +} + +impl ConfigurableArtifacts { + pub fn new( + extra_values: impl IntoIterator, + extra_files: impl IntoIterator, + ) -> Self { + Self { + additional_values: ExtraOutputValues::from_output_selection(extra_values), + additional_files: ExtraOutputFiles::from_output_selection(extra_files), + ..Default::default() + } + } + + /// Returns the `Settings` this configuration corresponds to + pub fn settings(&self) -> Settings { + SolcConfig::builder().additional_outputs(self.output_selection()).build().into() + } + + /// Returns the output selection corresponding to this configuration + pub fn output_selection(&self) -> Vec { + let mut selection = ContractOutputSelection::basic(); + if self.additional_values.ir { + selection.push(ContractOutputSelection::Ir); + } + if self.additional_values.ir_optimized || self.additional_files.ir_optimized { + selection.push(ContractOutputSelection::IrOptimized); + } + if self.additional_values.metadata || self.additional_files.metadata { + selection.push(ContractOutputSelection::Metadata); + } + if self.additional_values.storage_layout { + selection.push(ContractOutputSelection::StorageLayout); + } + if self.additional_values.devdoc { + selection.push(ContractOutputSelection::DevDoc); + } + if self.additional_values.userdoc { + selection.push(ContractOutputSelection::UserDoc); + } + if self.additional_values.gas_estimates { + selection.push(EvmOutputSelection::GasEstimates.into()); + } + if self.additional_values.assembly || self.additional_files.assembly { + selection.push(EvmOutputSelection::Assembly.into()); + } + if self.additional_values.ewasm || self.additional_files.ewasm { + selection.push(EwasmOutputSelection::All.into()); + } + selection + } +} + +impl ArtifactOutput for ConfigurableArtifacts { + type Artifact = ConfigurableContractArtifact; + + fn write_contract_extras(&self, contract: &Contract, file: &Path) -> Result<(), SolcError> { + self.additional_files.write_extras(contract, file) + } + + fn contract_to_artifact(&self, _file: &str, _name: &str, contract: Contract) -> Self::Artifact { + let mut artifact_userdoc = None; + let mut artifact_devdoc = None; + let mut artifact_metadata = None; + let mut artifact_ir = None; + let mut artifact_ir_optimized = None; + let mut artifact_ewasm = None; + let mut artifact_bytecode = None; + let mut artifact_deployed_bytecode = None; + let mut artifact_gas_estimates = None; + let mut artifact_method_identifiers = None; + let mut artifact_assembly = None; + let mut artifact_storage_layout = None; + + let Contract { + abi, + metadata, + userdoc, + devdoc, + ir, + storage_layout, + evm, + ewasm, + ir_optimized, + } = contract; + + if self.additional_values.metadata { + artifact_metadata = metadata; + } + if self.additional_values.userdoc { + artifact_userdoc = Some(userdoc); + } + if self.additional_values.devdoc { + artifact_devdoc = Some(devdoc); + } + if self.additional_values.ewasm { + artifact_ewasm = ewasm; + } + if self.additional_values.ir { + artifact_ir = ir; + } + if self.additional_values.ir_optimized { + artifact_ir_optimized = ir_optimized; + } + if self.additional_values.storage_layout { + artifact_storage_layout = Some(storage_layout); + } + + if let Some(evm) = evm { + let CompactEvm { + assembly, + bytecode, + deployed_bytecode, + method_identifiers, + gas_estimates, + .. + } = evm.into_compact(); + + artifact_bytecode = bytecode; + artifact_deployed_bytecode = deployed_bytecode; + + if self.additional_values.gas_estimates { + artifact_gas_estimates = gas_estimates; + } + if self.additional_values.method_identifiers { + artifact_method_identifiers = Some(method_identifiers); + } + if self.additional_values.assembly { + artifact_assembly = assembly; + } + } + + ConfigurableContractArtifact { + abi, + bytecode: artifact_bytecode, + deployed_bytecode: artifact_deployed_bytecode, + assembly: artifact_assembly, + method_identifiers: artifact_method_identifiers, + gas_estimates: artifact_gas_estimates, + metadata: artifact_metadata, + storage_layout: artifact_storage_layout, + userdoc: artifact_userdoc, + devdoc: artifact_devdoc, + ir: artifact_ir, + ir_optimized: artifact_ir_optimized, + ewasm: artifact_ewasm, + } + } +} + +/// Determines the additional values to include in the contract's artifact file +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] +pub struct ExtraOutputValues { + pub ast: bool, + pub userdoc: bool, + pub devdoc: bool, + pub method_identifiers: bool, + pub storage_layout: bool, + pub assembly: bool, + pub gas_estimates: bool, + pub compact_format: bool, + pub metadata: bool, + pub ir: bool, + pub ir_optimized: bool, + pub ewasm: bool, + + /// PRIVATE: This structure may grow, As such, constructing this structure should + /// _always_ be done using a public constructor or update syntax: + /// + /// ```rust + /// + /// use ethers_solc::ExtraOutputValues; + /// let config = ExtraOutputValues { + /// ir: true, + /// ..Default::default() + /// }; + /// ``` + #[doc(hidden)] + pub __non_exhaustive: (), +} + +impl ExtraOutputValues { + /// Returns an instance where all values are set to `true` + pub fn all() -> Self { + Self { + ast: true, + userdoc: true, + devdoc: true, + method_identifiers: true, + storage_layout: true, + assembly: true, + gas_estimates: true, + compact_format: true, + metadata: true, + ir: true, + ir_optimized: true, + ewasm: true, + __non_exhaustive: (), + } + } + + /// Sets the values based on a set of `ContractOutputSelection` + pub fn from_output_selection( + settings: impl IntoIterator, + ) -> Self { + let mut config = Self::default(); + for value in settings.into_iter() { + match value { + ContractOutputSelection::DevDoc => { + config.devdoc = true; + } + ContractOutputSelection::UserDoc => { + config.userdoc = true; + } + ContractOutputSelection::Metadata => { + config.metadata = true; + } + ContractOutputSelection::Ir => { + config.ir = true; + } + ContractOutputSelection::IrOptimized => { + config.ir_optimized = true; + } + ContractOutputSelection::StorageLayout => { + config.storage_layout = true; + } + ContractOutputSelection::Evm(evm) => match evm { + EvmOutputSelection::All => { + config.assembly = true; + config.gas_estimates = true; + config.method_identifiers = true; + } + EvmOutputSelection::Assembly => { + config.assembly = true; + } + EvmOutputSelection::MethodIdentifiers => { + config.method_identifiers = true; + } + EvmOutputSelection::GasEstimates => { + config.gas_estimates = true; + } + _ => {} + }, + ContractOutputSelection::Ewasm(_) => { + config.ewasm = true; + } + _ => {} + } + } + + config + } +} + +/// Determines what to emit as additional file +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] +pub struct ExtraOutputFiles { + pub metadata: bool, + pub ir_optimized: bool, + pub ewasm: bool, + pub assembly: bool, + + /// PRIVATE: This structure may grow, As such, constructing this structure should + /// _always_ be done using a public constructor or update syntax: + /// + /// ```rust + /// + /// use ethers_solc::ExtraOutputFiles; + /// let config = ExtraOutputFiles { + /// metadata: true, + /// ..Default::default() + /// }; + /// ``` + #[doc(hidden)] + pub __non_exhaustive: (), +} + +impl ExtraOutputFiles { + /// Returns an instance where all values are set to `true` + pub fn all() -> Self { + Self { + metadata: true, + ir_optimized: true, + ewasm: true, + assembly: true, + __non_exhaustive: (), + } + } + + /// Sets the values based on a set of `ContractOutputSelection` + pub fn from_output_selection( + settings: impl IntoIterator, + ) -> Self { + let mut config = Self::default(); + for value in settings.into_iter() { + match value { + ContractOutputSelection::Metadata => { + config.metadata = true; + } + ContractOutputSelection::IrOptimized => { + config.ir_optimized = true; + } + ContractOutputSelection::Evm(evm) => match evm { + EvmOutputSelection::All => { + config.assembly = true; + } + EvmOutputSelection::Assembly => { + config.assembly = true; + } + _ => {} + }, + ContractOutputSelection::Ewasm(_) => { + config.ewasm = true; + } + _ => {} + } + } + config + } + + /// Write the set values as separate files + pub fn write_extras(&self, contract: &Contract, file: &Path) -> Result<(), SolcError> { + if self.metadata { + if let Some(ref metadata) = contract.metadata { + let file = file.with_extension("metadata.json"); + fs::write(&file, serde_json::to_string_pretty(metadata)?) + .map_err(|err| SolcError::io(err, file))? + } + } + + if self.ir_optimized { + if let Some(ref iropt) = contract.ir_optimized { + let file = file.with_extension("iropt"); + fs::write(&file, iropt).map_err(|err| SolcError::io(err, file))? + } + } + + if self.ewasm { + if let Some(ref ir) = contract.ir { + let file = file.with_extension("ir"); + fs::write(&file, ir).map_err(|err| SolcError::io(err, file))? + } + } + + if self.ewasm { + if let Some(ref ewasm) = contract.ewasm { + let file = file.with_extension("ewasm"); + fs::write(&file, serde_json::to_vec_pretty(ewasm)?) + .map_err(|err| SolcError::io(err, file))?; + } + } + + if self.assembly { + if let Some(ref evm) = contract.evm { + if let Some(ref asm) = evm.assembly { + let file = file.with_extension("asm"); + fs::write(&file, asm).map_err(|err| SolcError::io(err, file))? + } + } + } + + Ok(()) + } +} diff --git a/ethers-solc/src/artifact_output.rs b/ethers-solc/src/artifact_output/mod.rs similarity index 89% rename from ethers-solc/src/artifact_output.rs rename to ethers-solc/src/artifact_output/mod.rs index d307050d..1240e557 100644 --- a/ethers-solc/src/artifact_output.rs +++ b/ethers-solc/src/artifact_output/mod.rs @@ -15,6 +15,9 @@ use std::{ path::{Path, PathBuf}, }; +mod configurable; +pub use configurable::*; + /// Represents an artifact file representing a [`crate::Contract`] #[derive(Debug, Clone, PartialEq)] pub struct ArtifactFile { @@ -314,18 +317,24 @@ pub trait ArtifactOutput { /// This will be invoked with all aggregated contracts from (multiple) solc `CompilerOutput`. /// See [`crate::AggregatedCompilerOutput`] fn on_output( + &self, contracts: &VersionedContracts, layout: &ProjectPathsConfig, ) -> Result> { - let mut artifacts = Self::output_to_artifacts(contracts); + let mut artifacts = self.output_to_artifacts(contracts); artifacts.join_all(&layout.artifacts); artifacts.write_all()?; - Self::write_extras(contracts, layout)?; + self.write_extras(contracts, layout)?; Ok(artifacts) } + /// Write additional files for the contract + fn write_contract_extras(&self, contract: &Contract, file: &Path) -> Result<()> { + ExtraOutputFiles::all().write_extras(contract, file) + } + /// Writes additional files for the contracts if the included in the `Contract`, such as `ir`, /// `ewasm`, `iropt`. /// @@ -334,7 +343,11 @@ pub trait ArtifactOutput { /// [`Contract`] will `None`. If they'll be manually added to the `output_selection`, then /// we're also creating individual files for this output, such as `Greeter.iropt`, /// `Gretter.ewasm` - fn write_extras(contracts: &VersionedContracts, layout: &ProjectPathsConfig) -> Result<()> { + fn write_extras( + &self, + contracts: &VersionedContracts, + layout: &ProjectPathsConfig, + ) -> Result<()> { for (file, contracts) in contracts.as_ref().iter() { for (name, versioned_contracts) in contracts { for c in versioned_contracts { @@ -347,30 +360,7 @@ pub trait ArtifactOutput { let file = layout.artifacts.join(artifact_path); utils::create_parent_dir_all(&file)?; - if let Some(iropt) = &c.contract.ir_optimized { - fs::write(&file.with_extension("iropt"), iropt) - .map_err(|err| SolcError::io(err, file.with_extension("iropt")))? - } - - if let Some(ir) = &c.contract.ir { - fs::write(&file.with_extension("ir"), ir) - .map_err(|err| SolcError::io(err, file.with_extension("ir")))? - } - - if let Some(ewasm) = &c.contract.ewasm { - fs::write( - &file.with_extension("ewasm"), - serde_json::to_vec_pretty(&ewasm)?, - ) - .map_err(|err| SolcError::io(err, file.with_extension("ewasm")))?; - } - - if let Some(evm) = &c.contract.evm { - if let Some(asm) = &evm.assembly { - fs::write(&file.with_extension("asm"), asm) - .map_err(|err| SolcError::io(err, file.with_extension("asm")))? - } - } + self.write_contract_extras(&c.contract, &file)?; } } } @@ -474,13 +464,13 @@ pub trait ArtifactOutput { /// /// This is the core conversion function that takes care of converting a `Contract` into the /// associated `Artifact` type - fn contract_to_artifact(_file: &str, _name: &str, contract: Contract) -> Self::Artifact; + fn contract_to_artifact(&self, _file: &str, _name: &str, contract: Contract) -> Self::Artifact; /// Convert the compiler output into a set of artifacts /// /// **Note:** This does only convert, but _NOT_ write the artifacts to disk, See /// [`Self::on_output()`] - fn output_to_artifacts(contracts: &VersionedContracts) -> Artifacts { + fn output_to_artifacts(&self, contracts: &VersionedContracts) -> Artifacts { let mut artifacts = ArtifactsMap::new(); for (file, contracts) in contracts.as_ref().iter() { let mut entries = BTreeMap::new(); @@ -493,8 +483,7 @@ pub trait ArtifactOutput { } else { Self::output_file(file, name) }; - let artifact = - Self::contract_to_artifact(file, name, contract.contract.clone()); + let artifact = self.contract_to_artifact(file, name, contract.contract.clone()); contracts.push(ArtifactFile { artifact, @@ -511,40 +500,45 @@ pub trait ArtifactOutput { } } -/// An Artifacts implementation that uses a compact representation +/// An `Artifact` implementation that uses a compact representation /// /// Creates a single json artifact with /// ```json /// { /// "abi": [], -/// "bin": "...", -/// "runtime-bin": "..." +/// "bytecode": {...}, +/// "deployedBytecode": {...} /// } /// ``` -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub struct MinimalCombinedArtifacts; +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] +pub struct MinimalCombinedArtifacts { + _priv: (), +} impl ArtifactOutput for MinimalCombinedArtifacts { type Artifact = CompactContractBytecode; - fn contract_to_artifact(_file: &str, _name: &str, contract: Contract) -> Self::Artifact { + fn contract_to_artifact(&self, _file: &str, _name: &str, contract: Contract) -> Self::Artifact { Self::Artifact::from(contract) } } /// An Artifacts handler implementation that works the same as `MinimalCombinedArtifacts` but also /// supports reading hardhat artifacts if an initial attempt to deserialize an artifact failed -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub struct MinimalCombinedArtifactsHardhatFallback; +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] +pub struct MinimalCombinedArtifactsHardhatFallback { + _priv: (), +} impl ArtifactOutput for MinimalCombinedArtifactsHardhatFallback { type Artifact = CompactContractBytecode; fn on_output( + &self, output: &VersionedContracts, layout: &ProjectPathsConfig, ) -> Result> { - MinimalCombinedArtifacts::on_output(output, layout) + MinimalCombinedArtifacts::default().on_output(output, layout) } fn read_cached_artifact(path: impl AsRef) -> Result { @@ -561,8 +555,8 @@ impl ArtifactOutput for MinimalCombinedArtifactsHardhatFallback { } } - fn contract_to_artifact(file: &str, name: &str, contract: Contract) -> Self::Artifact { - MinimalCombinedArtifacts::contract_to_artifact(file, name, contract) + fn contract_to_artifact(&self, file: &str, name: &str, contract: Contract) -> Self::Artifact { + MinimalCombinedArtifacts::default().contract_to_artifact(file, name, contract) } } diff --git a/ethers-solc/src/artifacts/mod.rs b/ethers-solc/src/artifacts/mod.rs index de43bc17..9ad98b5e 100644 --- a/ethers-solc/src/artifacts/mod.rs +++ b/ethers-solc/src/artifacts/mod.rs @@ -24,6 +24,7 @@ use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer}; pub mod output_selection; pub mod serde_helpers; +use crate::artifacts::output_selection::ContractOutputSelection; pub use serde_helpers::{deserialize_bytes, deserialize_opt_bytes}; /// Solidity files are made up of multiple `source units`, a solidity contract is such a `source @@ -229,6 +230,25 @@ impl Settings { )]) } + /// Inserts a set of `ContractOutputSelection` + pub fn push_all(&mut self, settings: impl IntoIterator) { + for value in settings { + self.push_output_selection(value) + } + } + + /// Inserts a set of `ContractOutputSelection` + #[must_use] + pub fn with_extra_output( + mut self, + settings: impl IntoIterator, + ) -> Self { + for value in settings { + self.push_output_selection(value) + } + self + } + /// Inserts the value for all files and contracts /// /// ``` @@ -922,6 +942,7 @@ impl From for ContractBytecode { /// Unlike `CompactContractSome` which contains the `BytecodeObject`, this holds the whole /// `Bytecode` object. #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] pub struct CompactContractBytecode { /// The Ethereum Contract ABI. If empty, it is represented as an empty /// array. See https://docs.soliditylang.org/en/develop/abi-spec.html @@ -952,13 +973,8 @@ impl CompactContractBytecode { impl From for CompactContractBytecode { fn from(c: Contract) -> Self { let (bytecode, deployed_bytecode) = if let Some(evm) = c.evm { - let (maybe_bcode, maybe_runtime) = match (evm.bytecode, evm.deployed_bytecode) { - (Some(bcode), Some(dbcode)) => (Some(bcode.into()), Some(dbcode.into())), - (None, Some(dbcode)) => (None, Some(dbcode.into())), - (Some(bcode), None) => (Some(bcode.into()), None), - (None, None) => (None, None), - }; - (maybe_bcode, maybe_runtime) + let evm = evm.into_compact(); + (evm.bytecode, evm.deployed_bytecode) } else { (None, None) }; @@ -1336,6 +1352,55 @@ pub struct Evm { pub gas_estimates: Option, } +impl Evm { + /// Crate internal helper do transform the underlying bytecode artifacts into a more convenient + /// structure + pub(crate) fn into_compact(self) -> CompactEvm { + let Evm { + assembly, + legacy_assembly, + bytecode, + deployed_bytecode, + method_identifiers, + gas_estimates, + } = self; + + let (bytecode, deployed_bytecode) = match (bytecode, deployed_bytecode) { + (Some(bcode), Some(dbcode)) => (Some(bcode.into()), Some(dbcode.into())), + (None, Some(dbcode)) => (None, Some(dbcode.into())), + (Some(bcode), None) => (Some(bcode.into()), None), + (None, None) => (None, None), + }; + + CompactEvm { + assembly, + legacy_assembly, + bytecode, + deployed_bytecode, + method_identifiers, + gas_estimates, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct CompactEvm { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub assembly: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub legacy_assembly: Option, + pub bytecode: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub deployed_bytecode: Option, + /// The list of function hashes + #[serde(default, skip_serializing_if = "::std::collections::BTreeMap::is_empty")] + pub method_identifiers: BTreeMap, + /// Function gas estimates + #[serde(default, skip_serializing_if = "Option::is_none")] + pub gas_estimates: Option, +} + #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Bytecode { diff --git a/ethers-solc/src/artifacts/output_selection.rs b/ethers-solc/src/artifacts/output_selection.rs index 4742fab9..375e485f 100644 --- a/ethers-solc/src/artifacts/output_selection.rs +++ b/ethers-solc/src/artifacts/output_selection.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::{fmt, str::FromStr}; /// Contract level output selection -#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum ContractOutputSelection { Abi, DevDoc, @@ -17,6 +17,23 @@ pub enum ContractOutputSelection { Ewasm(EwasmOutputSelection), } +impl ContractOutputSelection { + /// Returns the basic set of contract level settings that should be included in the `Contract` + /// that solc emits: + /// - "abi" + /// - "evm.bytecode" + /// - "evm.deployedBytecode" + /// - "evm.methodIdentifiers" + pub fn basic() -> Vec { + vec![ + ContractOutputSelection::Abi, + BytecodeOutputSelection::All.into(), + DeployedBytecodeOutputSelection::All.into(), + EvmOutputSelection::MethodIdentifiers.into(), + ] + } +} + impl Serialize for ContractOutputSelection { fn serialize(&self, serializer: S) -> Result where @@ -61,8 +78,12 @@ impl FromStr for ContractOutputSelection { "userdoc" => Ok(ContractOutputSelection::UserDoc), "metadata" => Ok(ContractOutputSelection::Metadata), "ir" => Ok(ContractOutputSelection::Ir), - "irOptimized" => Ok(ContractOutputSelection::IrOptimized), - "storageLayout" => Ok(ContractOutputSelection::StorageLayout), + "ir-optimized" | "irOptimized" | "iroptimized" => { + Ok(ContractOutputSelection::IrOptimized) + } + "storage-layout" | "storagelayout" | "storageLayout" => { + Ok(ContractOutputSelection::StorageLayout) + } s => EvmOutputSelection::from_str(s) .map(ContractOutputSelection::Evm) .or_else(|_| EwasmOutputSelection::from_str(s).map(ContractOutputSelection::Ewasm)) @@ -71,8 +92,20 @@ impl FromStr for ContractOutputSelection { } } +impl> From for ContractOutputSelection { + fn from(evm: T) -> Self { + ContractOutputSelection::Evm(evm.into()) + } +} + +impl From for ContractOutputSelection { + fn from(ewasm: EwasmOutputSelection) -> Self { + ContractOutputSelection::Ewasm(ewasm) + } +} + /// Contract level output selection for `evm` -#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum EvmOutputSelection { All, Assembly, @@ -83,6 +116,18 @@ pub enum EvmOutputSelection { DeployedByteCode(DeployedBytecodeOutputSelection), } +impl From for EvmOutputSelection { + fn from(b: BytecodeOutputSelection) -> Self { + EvmOutputSelection::ByteCode(b) + } +} + +impl From for EvmOutputSelection { + fn from(b: DeployedBytecodeOutputSelection) -> Self { + EvmOutputSelection::DeployedByteCode(b) + } +} + impl Serialize for EvmOutputSelection { fn serialize(&self, serializer: S) -> Result where @@ -121,10 +166,12 @@ impl FromStr for EvmOutputSelection { fn from_str(s: &str) -> Result { match s { "evm" => Ok(EvmOutputSelection::All), - "evm.assembly" => Ok(EvmOutputSelection::Assembly), + "asm" | "evm.assembly" => Ok(EvmOutputSelection::Assembly), "evm.legacyAssembly" => Ok(EvmOutputSelection::LegacyAssembly), - "evm.methodIdentifiers" => Ok(EvmOutputSelection::MethodIdentifiers), - "evm.gasEstimates" => Ok(EvmOutputSelection::GasEstimates), + "methodidentifiers" | "evm.methodIdentifiers" | "evm.methodidentifiers" => { + Ok(EvmOutputSelection::MethodIdentifiers) + } + "gas" | "evm.gasEstimates" | "evm.gasestimates" => Ok(EvmOutputSelection::GasEstimates), s => BytecodeOutputSelection::from_str(s) .map(EvmOutputSelection::ByteCode) .or_else(|_| { @@ -137,7 +184,7 @@ impl FromStr for EvmOutputSelection { } /// Contract level output selection for `evm.bytecode` -#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum BytecodeOutputSelection { All, FunctionDebugData, @@ -202,7 +249,7 @@ impl FromStr for BytecodeOutputSelection { } /// Contract level output selection for `evm.deployedBytecode` -#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum DeployedBytecodeOutputSelection { All, FunctionDebugData, @@ -277,7 +324,7 @@ impl FromStr for DeployedBytecodeOutputSelection { } /// Contract level output selection for `evm.ewasm` -#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum EwasmOutputSelection { All, Wast, diff --git a/ethers-solc/src/compile/output.rs b/ethers-solc/src/compile/output.rs index c3bed6dd..2acd1079 100644 --- a/ethers-solc/src/compile/output.rs +++ b/ethers-solc/src/compile/output.rs @@ -1,9 +1,11 @@ //! The output of a compiled project use crate::{ - artifacts::{CompactContractRef, Contract, Error, SourceFile, SourceFiles}, + artifacts::{ + CompactContractBytecode, CompactContractRef, Contract, Error, SourceFile, SourceFiles, + }, contracts::{VersionedContract, VersionedContracts}, - ArtifactOutput, Artifacts, CompilerOutput, + ArtifactOutput, Artifacts, CompilerOutput, ConfigurableArtifacts, }; use semver::Version; use std::{collections::BTreeMap, fmt, path::Path}; @@ -11,7 +13,7 @@ use std::{collections::BTreeMap, fmt, path::Path}; /// Contains a mixture of already compiled/cached artifacts and the input set of sources that still /// need to be compiled. #[derive(Debug, Clone, PartialEq, Default)] -pub struct ProjectCompileOutput { +pub struct ProjectCompileOutput { /// contains the aggregated `CompilerOutput` /// /// See [`CompilerSources::compile`] @@ -33,11 +35,11 @@ impl ProjectCompileOutput { /// /// ```no_run /// use std::collections::btree_map::BTreeMap; - /// use ethers_solc::artifacts::CompactContractBytecode; + /// use ethers_solc::ConfigurableContractArtifact; /// use ethers_solc::Project; /// /// let project = Project::builder().build().unwrap(); - /// let contracts: BTreeMap = project.compile().unwrap().into_artifacts().collect(); + /// let contracts: BTreeMap = project.compile().unwrap().into_artifacts().collect(); /// ``` pub fn into_artifacts(self) -> impl Iterator { let Self { cached_artifacts, compiled_artifacts, .. } = self; @@ -53,11 +55,10 @@ impl ProjectCompileOutput { /// /// ```no_run /// use std::collections::btree_map::BTreeMap; - /// use ethers_solc::artifacts::CompactContractBytecode; - /// use ethers_solc::Project; + /// use ethers_solc::{ConfigurableContractArtifact, Project}; /// /// let project = Project::builder().build().unwrap(); - /// let contracts: Vec<(String, String, CompactContractBytecode)> = project.compile().unwrap().into_artifacts_with_files().collect(); + /// let contracts: Vec<(String, String, ConfigurableContractArtifact)> = project.compile().unwrap().into_artifacts_with_files().collect(); /// ``` /// /// **NOTE** the `file` will be returned as is, see also [`Self::with_stripped_file_prefixes()`] @@ -173,6 +174,27 @@ where } } +impl ProjectCompileOutput { + /// A helper functions that extracts the underlying [`CompactContractBytecode`] from the + /// [`ConfigurableContractArtifact`] + /// + /// # Example + /// + /// ```no_run + /// use std::collections::btree_map::BTreeMap; + /// use ethers_solc::artifacts::CompactContractBytecode; + /// use ethers_solc::Project; + /// + /// let project = Project::builder().build().unwrap(); + /// let contracts: BTreeMap = project.compile().unwrap().into_contract_bytecodes().collect(); + /// ``` + pub fn into_contract_bytecodes( + self, + ) -> impl Iterator { + self.into_artifacts().map(|(name, artifact)| (name, artifact.into_contract_bytecode())) + } +} + impl fmt::Display for ProjectCompileOutput { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.compiler_output.is_unchanged() { diff --git a/ethers-solc/src/compile/project.rs b/ethers-solc/src/compile/project.rs index e03f44dc..6e1a9ac7 100644 --- a/ethers-solc/src/compile/project.rs +++ b/ethers-solc/src/compile/project.rs @@ -219,11 +219,15 @@ impl<'a, T: ArtifactOutput> CompiledState<'a, T> { /// Writes all output contracts to disk if enabled in the `Project` fn write_artifacts(self) -> Result> { let CompiledState { output, cache } = self; + // write all artifacts let compiled_artifacts = if !cache.project().no_artifacts { - T::on_output(&output.contracts, &cache.project().paths)? + cache + .project() + .artifacts_handler() + .on_output(&output.contracts, &cache.project().paths)? } else { - T::output_to_artifacts(&output.contracts) + cache.project().artifacts_handler().output_to_artifacts(&output.contracts) }; Ok(ArtifactsState { output, cache, compiled_artifacts }) diff --git a/ethers-solc/src/config.rs b/ethers-solc/src/config.rs index 26e29e68..9a7077b5 100644 --- a/ethers-solc/src/config.rs +++ b/ethers-solc/src/config.rs @@ -7,6 +7,7 @@ use crate::{ utils, Source, Sources, }; +use crate::artifacts::output_selection::ContractOutputSelection; use serde::{Deserialize, Serialize}; use std::{ fmt::{self, Formatter}, @@ -465,9 +466,18 @@ impl SolcConfig { } } +impl From for Settings { + fn from(config: SolcConfig) -> Self { + config.settings + } +} + #[derive(Default)] pub struct SolcConfigBuilder { settings: Option, + + /// additionally selected outputs that should be included in the `Contract` that `solcĀ“ creates + output_selection: Vec, } impl SolcConfigBuilder { @@ -476,12 +486,34 @@ impl SolcConfigBuilder { self } + /// Adds another `ContractOutputSelection` to the set + #[must_use] + pub fn additional_output(mut self, output: impl Into) -> Self { + self.output_selection.push(output.into()); + self + } + + /// Adds multiple `ContractOutputSelection` to the set + #[must_use] + pub fn additional_outputs(mut self, outputs: I) -> Self + where + I: IntoIterator, + S: Into, + { + for out in outputs { + self = self.additional_output(out); + } + self + } + /// Creates the solc config /// /// If no solc version is configured then it will be determined by calling `solc --version`. pub fn build(self) -> SolcConfig { - let Self { settings } = self; - SolcConfig { settings: settings.unwrap_or_default() } + let Self { settings, output_selection } = self; + let mut settings = settings.unwrap_or_default(); + settings.push_all(output_selection); + SolcConfig { settings } } } diff --git a/ethers-solc/src/hh.rs b/ethers-solc/src/hh.rs index d6edb664..722316dd 100644 --- a/ethers-solc/src/hh.rs +++ b/ethers-solc/src/hh.rs @@ -78,13 +78,15 @@ impl From for CompactContractBytecode { } /// Hardhat style artifacts handler -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub struct HardhatArtifacts; +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] +pub struct HardhatArtifacts { + _priv: (), +} impl ArtifactOutput for HardhatArtifacts { type Artifact = HardhatArtifact; - fn contract_to_artifact(file: &str, name: &str, contract: Contract) -> Self::Artifact { + fn contract_to_artifact(&self, file: &str, name: &str, contract: Contract) -> Self::Artifact { let (bytecode, link_references, deployed_bytecode, deployed_link_references) = if let Some(evm) = contract.evm { let (deployed_bytecode, deployed_link_references) = diff --git a/ethers-solc/src/lib.rs b/ethers-solc/src/lib.rs index c230ac06..a74d455a 100644 --- a/ethers-solc/src/lib.rs +++ b/ethers-solc/src/lib.rs @@ -2,6 +2,7 @@ pub mod artifacts; pub mod sourcemap; pub use artifacts::{CompilerInput, CompilerOutput, EvmVersion}; +use std::collections::BTreeMap; mod artifact_output; pub mod cache; @@ -30,13 +31,12 @@ pub mod utils; use crate::{ artifacts::{Contract, Sources}, + contracts::VersionedContracts, error::{SolcError, SolcIoError}, }; use error::Result; -use std::{ - marker::PhantomData, - path::{Path, PathBuf}, -}; +use semver::Version; +use std::path::{Path, PathBuf}; /// Utilities for creating, mocking and testing of (temporary) projects #[cfg(feature = "project-util")] @@ -44,7 +44,7 @@ pub mod project_util; /// Represents a project workspace and handles `solc` compiling of all contracts in that workspace. #[derive(Debug)] -pub struct Project { +pub struct Project { /// The layout of the pub paths: ProjectPathsConfig, /// Where to find solc @@ -57,8 +57,8 @@ pub struct Project { pub no_artifacts: bool, /// Whether writing artifacts to disk is enabled pub auto_detect: bool, - /// How to handle compiler output - pub artifacts: PhantomData, + /// Handles all artifacts related tasks, reading and writing from the artifact dir. + pub artifacts: T, /// Errors/Warnings which match these error codes are not going to be logged pub ignored_error_codes: Vec, /// The paths which will be allowed for library inclusion @@ -74,7 +74,7 @@ impl Project { /// /// # Example /// - /// Configure with `MinimalCombinedArtifacts` artifacts output + /// Configure with `ConfigurableArtifacts` artifacts output /// /// ```rust /// use ethers_solc::Project; @@ -91,15 +91,15 @@ impl Project { /// or use the builder directly /// /// ```rust - /// use ethers_solc::{MinimalCombinedArtifacts, ProjectBuilder}; - /// let config = ProjectBuilder::::default().build().unwrap(); + /// use ethers_solc::{ConfigurableArtifacts, ProjectBuilder}; + /// let config = ProjectBuilder::::default().build().unwrap(); /// ``` pub fn builder() -> ProjectBuilder { ProjectBuilder::default() } } -impl Project { +impl Project { /// Returns the path to the artifacts directory pub fn artifacts_path(&self) -> &PathBuf { &self.paths.artifacts @@ -120,6 +120,11 @@ impl Project { &self.paths.root } + /// Returns the handler that takes care of processing all artifacts + pub fn artifacts_handler(&self) -> &T { + &self.artifacts + } + /// Applies the configured settings to the given `Solc` fn configure_solc(&self, mut solc: Solc) -> Solc { if self.allowed_lib_paths.0.is_empty() { @@ -187,7 +192,7 @@ impl Project { /// # } /// ``` #[tracing::instrument(skip_all, name = "compile")] - pub fn compile(&self) -> Result> { + pub fn compile(&self) -> Result> { let sources = self.paths.read_input_files()?; tracing::trace!("found {} sources to compile: {:?}", sources.len(), sources.keys()); @@ -226,7 +231,7 @@ impl Project { /// # } /// ``` #[cfg(all(feature = "svm", feature = "async"))] - pub fn svm_compile(&self, sources: Sources) -> Result> { + pub fn svm_compile(&self, sources: Sources) -> Result> { project::ProjectCompiler::with_sources(self, sources)?.compile() } @@ -257,7 +262,7 @@ impl Project { &self, solc: &Solc, sources: Sources, - ) -> Result> { + ) -> Result> { project::ProjectCompiler::with_sources_and_solc( self, sources, @@ -325,7 +330,7 @@ impl Project { } } -pub struct ProjectBuilder { +pub struct ProjectBuilder { /// The layout of the paths: Option, /// Where to find solc @@ -340,7 +345,8 @@ pub struct ProjectBuilder auto_detect: bool, /// Use offline mode offline: bool, - artifacts: PhantomData, + /// handles all artifacts related tasks + artifacts: T, /// Which error codes to ignore pub ignored_error_codes: Vec, /// All allowed paths @@ -348,7 +354,24 @@ pub struct ProjectBuilder solc_jobs: Option, } -impl ProjectBuilder { +impl ProjectBuilder { + /// Create a new builder with the given artifacts handler + pub fn new(artifacts: T) -> Self { + Self { + paths: None, + solc: None, + solc_config: None, + cached: true, + no_artifacts: false, + auto_detect: true, + offline: false, + artifacts, + ignored_error_codes: Vec::new(), + allowed_paths: vec![], + solc_jobs: None, + } + } + #[must_use] pub fn paths(mut self, paths: ProjectPathsConfig) -> Self { self.paths = Some(paths); @@ -454,7 +477,7 @@ impl ProjectBuilder { } /// Set arbitrary `ArtifactOutputHandler` - pub fn artifacts(self) -> ProjectBuilder { + pub fn artifacts(self, artifacts: A) -> ProjectBuilder { let ProjectBuilder { paths, solc, @@ -476,7 +499,7 @@ impl ProjectBuilder { no_artifacts, auto_detect, offline, - artifacts: PhantomData::default(), + artifacts, ignored_error_codes, allowed_paths, solc_jobs, @@ -485,7 +508,7 @@ impl ProjectBuilder { /// Adds an allowed-path to the solc executable #[must_use] - pub fn allowed_path>(mut self, path: T) -> Self { + pub fn allowed_path>(mut self, path: P) -> Self { self.allowed_paths.push(path.into()); self } @@ -503,7 +526,7 @@ impl ProjectBuilder { self } - pub fn build(self) -> Result> { + pub fn build(self) -> Result> { let Self { paths, solc, @@ -544,29 +567,85 @@ impl ProjectBuilder { } } -impl Default for ProjectBuilder { +impl Default for ProjectBuilder { fn default() -> Self { - Self { - paths: None, - solc: None, - solc_config: None, - cached: true, - no_artifacts: false, - auto_detect: true, - offline: false, - artifacts: PhantomData::default(), - ignored_error_codes: Vec::new(), - allowed_paths: vec![], - solc_jobs: None, - } + Self::new(T::default()) } } -impl ArtifactOutput for Project { - type Artifact = Artifacts::Artifact; +impl ArtifactOutput for Project { + type Artifact = T::Artifact; - fn contract_to_artifact(file: &str, name: &str, contract: Contract) -> Self::Artifact { - Artifacts::contract_to_artifact(file, name, contract) + fn on_output( + &self, + contracts: &VersionedContracts, + layout: &ProjectPathsConfig, + ) -> Result> { + self.artifacts_handler().on_output(contracts, layout) + } + + fn write_contract_extras(&self, contract: &Contract, file: &Path) -> Result<()> { + self.artifacts_handler().write_contract_extras(contract, file) + } + + fn write_extras( + &self, + contracts: &VersionedContracts, + layout: &ProjectPathsConfig, + ) -> Result<()> { + self.artifacts_handler().write_extras(contracts, layout) + } + + fn output_file_name(name: impl AsRef) -> PathBuf { + T::output_file_name(name) + } + + fn output_file_name_versioned(name: impl AsRef, version: &Version) -> PathBuf { + T::output_file_name_versioned(name, version) + } + + fn output_file(contract_file: impl AsRef, name: impl AsRef) -> PathBuf { + T::output_file(contract_file, name) + } + + fn output_file_versioned( + contract_file: impl AsRef, + name: impl AsRef, + version: &Version, + ) -> PathBuf { + T::output_file_versioned(contract_file, name, version) + } + + fn contract_name(file: impl AsRef) -> Option { + T::contract_name(file) + } + + fn output_exists( + contract_file: impl AsRef, + name: impl AsRef, + root: impl AsRef, + ) -> bool { + T::output_exists(contract_file, name, root) + } + + fn read_cached_artifact(path: impl AsRef) -> Result { + T::read_cached_artifact(path) + } + + fn read_cached_artifacts(files: I) -> Result> + where + I: IntoIterator, + P: Into, + { + T::read_cached_artifacts(files) + } + + fn contract_to_artifact(&self, file: &str, name: &str, contract: Contract) -> Self::Artifact { + self.artifacts_handler().contract_to_artifact(file, name, contract) + } + + fn output_to_artifacts(&self, contracts: &VersionedContracts) -> Artifacts { + self.artifacts_handler().output_to_artifacts(contracts) } } diff --git a/ethers-solc/src/project_util.rs b/ethers-solc/src/project_util.rs index 5e77434a..e5b36f5f 100644 --- a/ethers-solc/src/project_util.rs +++ b/ethers-solc/src/project_util.rs @@ -1,10 +1,11 @@ //! Utilities for mocking project workspaces use crate::{ + artifacts::Settings, config::ProjectPathsConfigBuilder, error::{Result, SolcError}, hh::HardhatArtifacts, utils::tempdir, - ArtifactOutput, MinimalCombinedArtifacts, PathStyle, Project, ProjectCompileOutput, + ArtifactOutput, ConfigurableArtifacts, PathStyle, Project, ProjectCompileOutput, ProjectPathsConfig, SolcIoError, }; use fs_extra::{dir, file}; @@ -17,7 +18,7 @@ use tempfile::TempDir; /// A [`Project`] wrapper that lives in a new temporary directory /// /// Once `TempProject` is dropped, the temp dir is automatically removed, see [`TempDir::drop()`] -pub struct TempProject { +pub struct TempProject { /// temporary workspace root _root: TempDir, /// actual project workspace with the `root` tempdir as its root @@ -32,26 +33,29 @@ impl TempProject { Ok(project) } - /// Creates a new temp project inside a tempdir with a prefixed directory - pub fn prefixed(prefix: &str, paths: ProjectPathsConfigBuilder) -> Result { + /// Creates a new temp project using the provided paths and artifacts handler. + /// sets the project root to a temp dir + pub fn with_artifacts(paths: ProjectPathsConfigBuilder, artifacts: T) -> Result { + Self::prefixed_with_artifacts("temp-project", paths, artifacts) + } + + /// Creates a new temp project inside a tempdir with a prefixed directory and the given + /// artifacts handler + pub fn prefixed_with_artifacts( + prefix: &str, + paths: ProjectPathsConfigBuilder, + artifacts: T, + ) -> Result { let tmp_dir = tempdir(prefix)?; let paths = paths.build_with_root(tmp_dir.path()); - let inner = Project::builder().artifacts().paths(paths).build()?; + let inner = Project::builder().artifacts(artifacts).paths(paths).build()?; Ok(Self::create_new(tmp_dir, inner)?) } - /// Creates a new temp project for the given `PathStyle` - pub fn with_style(prefix: &str, style: PathStyle) -> Result { - let tmp_dir = tempdir(prefix)?; - let paths = style.paths(tmp_dir.path())?; - let inner = Project::builder().artifacts().paths(paths).build()?; - Ok(Self::create_new(tmp_dir, inner)?) - } - - /// Creates a new temp project using the provided paths and setting the project root to a temp - /// dir - pub fn new(paths: ProjectPathsConfigBuilder) -> Result { - Self::prefixed("temp-project", paths) + /// Overwrites the settings to pass to `solc` + pub fn with_settings(mut self, settings: impl Into) -> Self { + self.inner.solc_config.settings = settings.into(); + self } pub fn project(&self) -> &Project { @@ -158,6 +162,27 @@ impl TempProject { } } +impl TempProject { + /// Creates a new temp project inside a tempdir with a prefixed directory + pub fn prefixed(prefix: &str, paths: ProjectPathsConfigBuilder) -> Result { + Self::prefixed_with_artifacts(prefix, paths, T::default()) + } + + /// Creates a new temp project for the given `PathStyle` + pub fn with_style(prefix: &str, style: PathStyle) -> Result { + let tmp_dir = tempdir(prefix)?; + let paths = style.paths(tmp_dir.path())?; + let inner = Project::builder().artifacts(T::default()).paths(paths).build()?; + Ok(Self::create_new(tmp_dir, inner)?) + } + + /// Creates a new temp project using the provided paths and setting the project root to a temp + /// dir + pub fn new(paths: ProjectPathsConfigBuilder) -> Result { + Self::prefixed("temp-project", paths) + } +} + impl fmt::Debug for TempProject { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("TempProject").field("paths", self.paths()).finish() @@ -189,18 +214,19 @@ impl TempProject { let paths = ProjectPathsConfig::hardhat(tmp_dir.path())?; - let inner = Project::builder().artifacts().paths(paths).build()?; + let inner = + Project::builder().artifacts(HardhatArtifacts::default()).paths(paths).build()?; Ok(Self::create_new(tmp_dir, inner)?) } } -impl TempProject { +impl TempProject { /// Creates an empty new dapptools style workspace in a new temporary dir pub fn dapptools() -> Result { let tmp_dir = tempdir("tmp_dapp")?; let paths = ProjectPathsConfig::dapptools(tmp_dir.path())?; - let inner = Project::builder().artifacts().paths(paths).build()?; + let inner = Project::builder().paths(paths).build()?; Ok(Self::create_new(tmp_dir, inner)?) } } diff --git a/ethers-solc/tests/project.rs b/ethers-solc/tests/project.rs index 137e45d1..882a8975 100644 --- a/ethers-solc/tests/project.rs +++ b/ethers-solc/tests/project.rs @@ -11,7 +11,8 @@ use ethers_solc::{ cache::{SolFilesCache, SOLIDITY_FILES_CACHE_FILENAME}, project_util::*, remappings::Remapping, - Graph, MinimalCombinedArtifacts, Project, ProjectCompileOutput, ProjectPathsConfig, + ConfigurableArtifacts, ExtraOutputValues, Graph, Project, ProjectCompileOutput, + ProjectPathsConfig, }; use pretty_assertions::assert_eq; @@ -28,7 +29,7 @@ fn can_compile_hardhat_sample() { let paths = ProjectPathsConfig::builder() .sources(root.join("contracts")) .lib(root.join("node_modules")); - let project = TempProject::::new(paths).unwrap(); + let project = TempProject::::new(paths).unwrap(); let compiled = project.compile().unwrap(); assert!(compiled.find("Greeter").is_some()); @@ -53,7 +54,7 @@ fn can_compile_hardhat_sample() { fn can_compile_dapp_sample() { let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/dapp-sample"); let paths = ProjectPathsConfig::builder().sources(root.join("src")).lib(root.join("lib")); - let project = TempProject::::new(paths).unwrap(); + let project = TempProject::::new(paths).unwrap(); let compiled = project.compile().unwrap(); assert!(compiled.find("Dapp").is_some()); @@ -76,9 +77,33 @@ fn can_compile_dapp_sample() { assert_eq!(cache, updated_cache); } +#[test] +fn can_compile_configured() { + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/dapp-sample"); + let paths = ProjectPathsConfig::builder().sources(root.join("src")).lib(root.join("lib")); + + let handler = ConfigurableArtifacts { + additional_values: ExtraOutputValues { + metadata: true, + ir: true, + ir_optimized: true, + ..Default::default() + }, + ..Default::default() + }; + + 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(); + assert!(artifact.metadata.is_some()); + assert!(artifact.ir.is_some()); + assert!(artifact.ir_optimized.is_some()); +} + #[test] fn can_compile_dapp_detect_changes_in_libs() { - let mut project = TempProject::::dapptools().unwrap(); + let mut project = TempProject::::dapptools().unwrap(); let remapping = project.paths().libraries[0].join("remapping"); project @@ -151,8 +176,7 @@ fn can_compile_dapp_detect_changes_in_libs() { #[test] fn can_compile_dapp_detect_changes_in_sources() { - init_tracing(); - let project = TempProject::::dapptools().unwrap(); + let project = TempProject::::dapptools().unwrap(); let src = project .add_source( @@ -330,7 +354,7 @@ fn can_flatten_file() { .sources(root.join("src")) .lib(root.join("lib1")) .lib(root.join("lib2")); - let project = TempProject::::new(paths).unwrap(); + let project = TempProject::::new(paths).unwrap(); let result = project.flatten(&target); assert!(result.is_ok()); @@ -346,7 +370,7 @@ fn can_flatten_file_with_external_lib() { let paths = ProjectPathsConfig::builder() .sources(root.join("contracts")) .lib(root.join("node_modules")); - let project = TempProject::::new(paths).unwrap(); + let project = TempProject::::new(paths).unwrap(); let target = root.join("contracts").join("Greeter.sol"); @@ -362,7 +386,7 @@ fn can_flatten_file_with_external_lib() { fn can_flatten_file_in_dapp_sample() { let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/dapp-sample"); let paths = ProjectPathsConfig::builder().sources(root.join("src")).lib(root.join("lib")); - let project = TempProject::::new(paths).unwrap(); + let project = TempProject::::new(paths).unwrap(); let target = root.join("src/Dapp.t.sol"); @@ -379,7 +403,7 @@ fn can_flatten_file_in_dapp_sample() { fn can_flatten_file_with_duplicates() { let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/flatten-sample"); let paths = ProjectPathsConfig::builder().sources(root.join("contracts")); - let project = TempProject::::new(paths).unwrap(); + let project = TempProject::::new(paths).unwrap(); let target = root.join("contracts/FooBar.sol"); @@ -395,7 +419,7 @@ fn can_flatten_file_with_duplicates() { #[test] fn can_detect_type_error() { - let project = TempProject::::dapptools().unwrap(); + let project = TempProject::::dapptools().unwrap(); project .add_source( diff --git a/examples/ethers-wasm/Cargo.toml b/examples/ethers-wasm/Cargo.toml index 8914eba2..118490a4 100644 --- a/examples/ethers-wasm/Cargo.toml +++ b/examples/ethers-wasm/Cargo.toml @@ -43,4 +43,4 @@ hex = "0.4.3" web-sys = "0.3.56" [dev-dependencies] -wasm-bindgen-test = "0.3.29" \ No newline at end of file +wasm-bindgen-test = "0.3.29"