//! Output artifact handling use crate::{ artifacts::FileToContractsMap, contracts::VersionedContracts, error::Result, utils, HardhatArtifact, ProjectPathsConfig, SolcError, }; use ethers_core::{abi::Abi, types::Bytes}; use semver::Version; use serde::{de::DeserializeOwned, Serialize}; use std::{ borrow::Cow, collections::btree_map::BTreeMap, fmt, fs, io, path::{Path, PathBuf}, }; mod configurable; use crate::artifacts::{ contract::{CompactContract, CompactContractBytecode, Contract}, BytecodeObject, CompactBytecode, CompactContractBytecodeCow, CompactDeployedBytecode, }; pub use configurable::*; /// Represents unique artifact metadata for identifying artifacts on output #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] pub struct ArtifactId { /// `artifact` cache path pub path: PathBuf, pub name: String, /// Original source file path pub source: PathBuf, /// `solc` version that produced this artifact pub version: Version, } impl ArtifactId { /// Returns a : slug that identifies an artifact pub fn slug(&self) -> String { format!("{}.json:{}", self.path.file_stem().unwrap().to_string_lossy(), self.name) } /// Returns a : slug that identifies an artifact pub fn slug_versioned(&self) -> String { format!( "{}.{}.{}.{}.json:{}", self.path.file_stem().unwrap().to_string_lossy(), self.version.major, self.version.minor, self.version.patch, self.name ) } } /// Represents an artifact file representing a [`crate::Contract`] #[derive(Debug, Clone, PartialEq)] pub struct ArtifactFile { /// The Artifact that was written pub artifact: T, /// path to the file where the `artifact` was written to pub file: PathBuf, /// `solc` version that produced this artifact pub version: Version, } impl ArtifactFile { /// Writes the given contract to the `out` path creating all parent directories pub fn write(&self) -> Result<()> { utils::create_parent_dir_all(&self.file)?; fs::write(&self.file, serde_json::to_vec_pretty(&self.artifact)?) .map_err(|err| SolcError::io(err, &self.file))?; Ok(()) } } impl ArtifactFile { /// Sets the file to `root` adjoined to `self.file`. pub fn join(&mut self, root: impl AsRef) { self.file = root.as_ref().join(&self.file); } /// Removes `base` from the artifact's path pub fn strip_prefix(&mut self, base: impl AsRef) { if let Ok(prefix) = self.file.strip_prefix(base) { self.file = prefix.to_path_buf(); } } } /// local helper type alias `file name -> (contract name -> Vec<..>)` pub(crate) type ArtifactsMap = FileToContractsMap>>; /// Represents a set of Artifacts #[derive(Debug, Clone, PartialEq)] pub struct Artifacts(pub ArtifactsMap); impl From> for Artifacts { fn from(m: ArtifactsMap) -> Self { Self(m) } } impl<'a, T> IntoIterator for &'a Artifacts { type Item = (&'a String, &'a BTreeMap>>); type IntoIter = std::collections::btree_map::Iter<'a, String, BTreeMap>>>; fn into_iter(self) -> Self::IntoIter { self.0.iter() } } impl IntoIterator for Artifacts { type Item = (String, BTreeMap>>); type IntoIter = std::collections::btree_map::IntoIter>>>; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } } impl Default for Artifacts { fn default() -> Self { Self(Default::default()) } } impl AsRef> for Artifacts { fn as_ref(&self) -> &ArtifactsMap { &self.0 } } impl AsMut> for Artifacts { fn as_mut(&mut self) -> &mut ArtifactsMap { &mut self.0 } } impl Artifacts { /// Writes all artifacts into the given `artifacts_root` folder pub fn write_all(&self) -> Result<()> { for artifact in self.artifact_files() { artifact.write()?; } Ok(()) } } impl Artifacts { pub fn into_inner(self) -> ArtifactsMap { self.0 } /// Sets the artifact files location to `root` adjoined to `self.file`. pub fn join_all(&mut self, root: impl AsRef) -> &mut Self { let root = root.as_ref(); self.artifact_files_mut().for_each(|artifact| artifact.join(root)); self } /// Removes `base` from all artifacts pub fn strip_prefix_all(&mut self, base: impl AsRef) -> &mut Self { let base = base.as_ref(); self.artifact_files_mut().for_each(|artifact| artifact.strip_prefix(base)); self } /// Returns all `ArtifactFile`s for the contract with the matching name fn get_contract_artifact_files(&self, contract_name: &str) -> Option<&Vec>> { self.0.values().find_map(|all| all.get(contract_name)) } /// Returns true if this type contains an artifact with the given path for the given contract pub fn has_contract_artifact(&self, contract_name: &str, artifact_path: &Path) -> bool { self.get_contract_artifact_files(contract_name) .map(|artifacts| artifacts.iter().any(|artifact| artifact.file == artifact_path)) .unwrap_or_default() } /// Returns true if this type contains an artifact with the given path pub fn has_artifact(&self, artifact_path: &Path) -> bool { self.artifact_files().any(|artifact| artifact.file == artifact_path) } /// Iterate over all artifact files pub fn artifact_files(&self) -> impl Iterator> { self.0.values().flat_map(|c| c.values().flat_map(|artifacts| artifacts.iter())) } /// Iterate over all artifact files pub fn artifact_files_mut(&mut self) -> impl Iterator> { self.0.values_mut().flat_map(|c| c.values_mut().flat_map(|artifacts| artifacts.iter_mut())) } /// Returns an iterator over _all_ artifacts and `` pub fn into_artifacts>( self, ) -> impl Iterator { self.0.into_iter().flat_map(|(file, contract_artifacts)| { contract_artifacts.into_iter().flat_map(move |(_contract_name, artifacts)| { let source = PathBuf::from(file.clone()); artifacts.into_iter().filter_map(move |artifact| { O::contract_name(&artifact.file).map(|name| { ( ArtifactId { path: PathBuf::from(&artifact.file), name, source: source.clone(), version: artifact.version, }, artifact.artifact, ) }) }) }) }) } /// Returns an iterator that yields the tuple `(file, contract name, artifact)` /// /// **NOTE** this returns the path as is pub fn into_artifacts_with_files(self) -> impl Iterator { self.0.into_iter().flat_map(|(f, contract_artifacts)| { contract_artifacts.into_iter().flat_map(move |(name, artifacts)| { let contract_name = name; let file = f.clone(); artifacts .into_iter() .map(move |artifact| (file.clone(), contract_name.clone(), artifact.artifact)) }) }) } /// Strips the given prefix from all artifact file paths to make them relative to the given /// `root` argument pub fn into_stripped_file_prefixes(self, base: impl AsRef) -> Self { let base = base.as_ref(); let artifacts = self .0 .into_iter() .map(|(file, c)| { let file_path = Path::new(&file); if let Ok(p) = file_path.strip_prefix(base) { (p.to_string_lossy().to_string(), c) } else { (file, c) } }) .collect(); Artifacts(artifacts) } /// Finds the first artifact `T` with a matching contract name pub fn find(&self, contract_name: impl AsRef) -> 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)) }) } /// 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) -> Option { let contract_name = contract_name.as_ref(); self.0.iter_mut().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 }) } } /// A trait representation for a [`crate::Contract`] artifact pub trait Artifact { /// Returns the artifact's `Abi` and bytecode fn into_inner(self) -> (Option, Option); /// Turns the artifact into a container type for abi, compact bytecode and deployed bytecode fn into_compact_contract(self) -> CompactContract; /// Turns the artifact into a container type for abi, full bytecode and deployed bytecode fn into_contract_bytecode(self) -> CompactContractBytecode; /// Returns the contents of this type as a single tuple of abi, bytecode and deployed bytecode fn into_parts(self) -> (Option, Option, Option); /// Consumes the type and returns the [Abi] fn into_abi(self) -> Option where Self: Sized, { self.into_parts().0 } /// Consumes the type and returns the `bytecode` fn into_bytecode_bytes(self) -> Option where Self: Sized, { self.into_parts().1 } /// Consumes the type and returns the `deployed bytecode` fn into_deployed_bytecode_bytes(self) -> Option where Self: Sized, { self.into_parts().2 } /// Same as [`Self::into_parts()`] but returns `Err` if an element is `None` fn try_into_parts(self) -> Result<(Abi, Bytes, Bytes)> where Self: Sized, { let (abi, bytecode, deployed_bytecode) = self.into_parts(); Ok(( abi.ok_or_else(|| SolcError::msg("abi missing"))?, bytecode.ok_or_else(|| SolcError::msg("bytecode missing"))?, deployed_bytecode.ok_or_else(|| SolcError::msg("deployed bytecode missing"))?, )) } /// Returns the reference of container type for abi, compact bytecode and deployed bytecode if /// available fn get_contract_bytecode(&self) -> CompactContractBytecodeCow; /// Returns the reference to the `bytecode` fn get_bytecode(&self) -> Option> { self.get_contract_bytecode().bytecode } /// Returns the reference to the `bytecode` object fn get_bytecode_object(&self) -> Option> { let val = match self.get_bytecode()? { Cow::Borrowed(b) => Cow::Borrowed(&b.object), Cow::Owned(b) => Cow::Owned(b.object), }; Some(val) } /// Returns the bytes of the `bytecode` object fn get_bytecode_bytes(&self) -> Option> { let val = match self.get_bytecode_object()? { Cow::Borrowed(b) => Cow::Borrowed(b.as_bytes()?), Cow::Owned(b) => Cow::Owned(b.into_bytes()?), }; Some(val) } /// Returns the reference to the `deployedBytecode` fn get_deployed_bytecode(&self) -> Option> { self.get_contract_bytecode().deployed_bytecode } /// Returns the reference to the `bytecode` object fn get_deployed_bytecode_object(&self) -> Option> { let val = match self.get_deployed_bytecode()? { Cow::Borrowed(b) => Cow::Borrowed(&b.bytecode.as_ref()?.object), Cow::Owned(b) => Cow::Owned(b.bytecode?.object), }; Some(val) } /// Returns the bytes of the `deployed bytecode` object fn get_deployed_bytecode_bytes(&self) -> Option> { let val = match self.get_deployed_bytecode_object()? { Cow::Borrowed(b) => Cow::Borrowed(b.as_bytes()?), Cow::Owned(b) => Cow::Owned(b.into_bytes()?), }; Some(val) } /// Returns the reference to the [Abi] if available fn get_abi(&self) -> Option> { self.get_contract_bytecode().abi } } impl Artifact for T where T: Into + Into, for<'a> &'a T: Into>, { fn into_inner(self) -> (Option, Option) { let artifact = self.into_compact_contract(); (artifact.abi, artifact.bin.and_then(|bin| bin.into_bytes())) } fn into_compact_contract(self) -> CompactContract { self.into() } fn into_contract_bytecode(self) -> CompactContractBytecode { self.into() } fn into_parts(self) -> (Option, Option, Option) { self.into_compact_contract().into_parts() } fn get_contract_bytecode(&self) -> CompactContractBytecodeCow { self.into() } } /// Handler invoked with the output of `solc` /// /// Implementers of this trait are expected to take care of [`crate::Contract`] to /// [`crate::ArtifactOutput::Artifact`] conversion and how that `Artifact` type is stored on disk, /// this includes artifact file location and naming. /// /// Depending on the [`crate::Project`] contracts and their compatible versions, /// The project compiler may invoke different `solc` executables on the same /// solidity file leading to multiple [`crate::CompilerOutput`]s for the same `.sol` file. /// In addition to the `solidity file` to `contract` relationship (1-N*) /// [`crate::VersionedContracts`] also tracks the `contract` to (`artifact` + `solc version`) /// relationship (1-N+). pub trait ArtifactOutput { /// Represents the artifact that will be stored for a `Contract` type Artifact: Artifact + DeserializeOwned + Serialize + fmt::Debug; /// Handle the aggregated set of compiled contracts from the solc [`crate::CompilerOutput`]. /// /// 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); artifacts.join_all(&layout.artifacts); artifacts.write_all()?; 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`. /// /// By default, these fields are _not_ enabled in the [`crate::artifacts::Settings`], see /// [`crate::artifacts::output_selection::OutputSelection::default_output_selection()`], and the /// respective fields of the [`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( &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 { let artifact_path = if versioned_contracts.len() > 1 { Self::output_file_versioned(file, name, &c.version) } else { Self::output_file(file, name) }; let file = layout.artifacts.join(artifact_path); utils::create_parent_dir_all(&file)?; self.write_contract_extras(&c.contract, &file)?; } } } Ok(()) } /// Returns the file name for the contract's artifact /// `Greeter.json` fn output_file_name(name: impl AsRef) -> PathBuf { format!("{}.json", name.as_ref()).into() } /// Returns the file name for the contract's artifact and the given version /// `Greeter.0.8.11.json` fn output_file_name_versioned(name: impl AsRef, version: &Version) -> PathBuf { format!("{}.{}.{}.{}.json", name.as_ref(), version.major, version.minor, version.patch) .into() } /// Returns the path to the contract's artifact location based on the contract's file and name /// /// This returns `contract.sol/contract.json` by default fn output_file(contract_file: impl AsRef, name: impl AsRef) -> PathBuf { let name = name.as_ref(); contract_file .as_ref() .file_name() .map(Path::new) .map(|p| p.join(Self::output_file_name(name))) .unwrap_or_else(|| Self::output_file_name(name)) } /// Returns the path to the contract's artifact location based on the contract's file, name and /// version /// /// This returns `contract.sol/contract.0.8.11.json` by default fn output_file_versioned( contract_file: impl AsRef, name: impl AsRef, version: &Version, ) -> PathBuf { let name = name.as_ref(); contract_file .as_ref() .file_name() .map(Path::new) .map(|p| p.join(Self::output_file_name_versioned(name, version))) .unwrap_or_else(|| Self::output_file_name_versioned(name, version)) } /// The inverse of `contract_file_name` /// /// Expected to return the solidity contract's name derived from the file path /// `sources/Greeter.sol` -> `Greeter` fn contract_name(file: impl AsRef) -> Option { file.as_ref().file_stem().and_then(|s| s.to_str().map(|s| s.to_string())) } /// Whether the corresponding artifact of the given contract file and name exists fn output_exists( contract_file: impl AsRef, name: impl AsRef, root: impl AsRef, ) -> bool { root.as_ref().join(Self::output_file(contract_file, name)).exists() } /// Read the artifact that's stored at the given path /// /// # Errors /// /// Returns an error if /// - The file does not exist /// - The file's content couldn't be deserialized into the `Artifact` type fn read_cached_artifact(path: impl AsRef) -> Result { let path = path.as_ref(); let file = fs::File::open(path).map_err(|err| SolcError::io(err, path))?; let file = io::BufReader::new(file); Ok(serde_json::from_reader(file)?) } /// Read the cached artifacts that are located the paths the iterator yields /// /// See [`Self::read_cached_artifact()`] fn read_cached_artifacts(files: I) -> Result> where I: IntoIterator, T: Into, { let mut artifacts = BTreeMap::default(); for path in files.into_iter() { let path = path.into(); let artifact = Self::read_cached_artifact(&path)?; artifacts.insert(path, artifact); } Ok(artifacts) } /// Convert a contract to the artifact type /// /// This is the core conversion function that takes care of converting a `Contract` into the /// associated `Artifact` type 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(&self, contracts: &VersionedContracts) -> Artifacts { let mut artifacts = ArtifactsMap::new(); for (file, contracts) in contracts.as_ref().iter() { let mut entries = BTreeMap::new(); for (name, versioned_contracts) in contracts { let mut contracts = Vec::with_capacity(versioned_contracts.len()); // check if the same contract compiled with multiple solc versions for contract in versioned_contracts { let artifact_path = if versioned_contracts.len() > 1 { Self::output_file_versioned(file, name, &contract.version) } else { Self::output_file(file, name) }; let artifact = self.contract_to_artifact(file, name, contract.contract.clone()); contracts.push(ArtifactFile { artifact, file: artifact_path, version: contract.version.clone(), }); } entries.insert(name.to_string(), contracts); } artifacts.insert(file.to_string(), entries); } Artifacts(artifacts) } } /// An `Artifact` implementation that uses a compact representation /// /// Creates a single json artifact with /// ```json /// { /// "abi": [], /// "bytecode": {...}, /// "deployedBytecode": {...} /// } /// ``` #[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] pub struct MinimalCombinedArtifacts { _priv: (), } impl ArtifactOutput for MinimalCombinedArtifacts { type Artifact = CompactContractBytecode; 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, Default)] pub struct MinimalCombinedArtifactsHardhatFallback { _priv: (), } impl ArtifactOutput for MinimalCombinedArtifactsHardhatFallback { type Artifact = CompactContractBytecode; fn on_output( &self, output: &VersionedContracts, layout: &ProjectPathsConfig, ) -> Result> { MinimalCombinedArtifacts::default().on_output(output, layout) } fn read_cached_artifact(path: impl AsRef) -> Result { let path = path.as_ref(); let content = fs::read_to_string(path).map_err(|err| SolcError::io(err, path))?; if let Ok(a) = serde_json::from_str(&content) { Ok(a) } else { tracing::error!("Failed to deserialize compact artifact"); tracing::trace!("Fallback to hardhat artifact deserialization"); let artifact = serde_json::from_str::(&content)?; tracing::trace!("successfully deserialized hardhat artifact"); Ok(artifact.into_contract_bytecode()) } } fn contract_to_artifact(&self, file: &str, name: &str, contract: Contract) -> Self::Artifact { MinimalCombinedArtifacts::default().contract_to_artifact(file, name, contract) } } #[cfg(test)] mod tests { use super::*; #[test] fn is_artifact() { fn assert_artifact() {} assert_artifact::(); assert_artifact::(); } }