diff --git a/ethcontract-common/src/artifact.rs b/ethcontract-common/src/artifact.rs new file mode 100644 index 00000000..a38ff82f --- /dev/null +++ b/ethcontract-common/src/artifact.rs @@ -0,0 +1,101 @@ +//! Tools for loading artifacts that contain compiled contracts. +//! +//! Artifacts come in various shapes and sizes, but usually they +//! are JSON files containing one or multiple compiled contracts +//! as well as their deployment information. +//! +//! This module provides trait [`Artifact`] that encapsulates different +//! artifact models. It also provides tools to load artifacts from different +//! sources, and parse them using different formats. + +use crate::Contract; +use std::collections::HashMap; + +pub mod truffle; + +/// An entity that contains compiled contracts. +pub struct Artifact { + origin: String, + contracts: HashMap, +} + +impl Artifact { + /// Create a new empty artifact. + pub fn new() -> Self { + Artifact { + origin: "".to_string(), + contracts: HashMap::new(), + } + } + + /// Create a new artifact with an origin information. + pub fn with_origin(origin: String) -> Self { + Artifact { + origin, + contracts: HashMap::new(), + } + } + + /// Describe where this artifact comes from. + /// + /// This function is used when a human-readable reference to the artifact + /// is required. It could be anything: path to a json file, url, etc. + pub fn origin(&self) -> &str { + &self.origin + } + + /// Set new origin for the artifact. + /// + /// Artifact loaders will set origin to something meaningful in most cases, + /// so this function should not be used often. There are cases when + /// it is required, though. + pub fn set_origin(&mut self, origin: impl Into) { + self.origin = origin.into(); + } + + /// Check whether this artifact has a contract with the given name. + pub fn contains(&self, name: &str) -> bool { + self.contracts.contains_key(name) + } + + /// Get contract by name. + /// + /// Some artifact formats allow exporting a single unnamed contract. + /// In this case, the contract will have an empty string as its name. + pub fn get(&self, name: &str) -> Option<&Contract> { + self.contracts.get(name) + } + + /// Insert a new contract to the artifact. + /// + /// If contract with this name already exists, replace it + /// and return an old contract. + pub fn insert(&mut self, contract: Contract) -> Option { + self.contracts.insert(contract.name.clone(), contract) + } + + /// Remove contract from the artifact. + /// + /// Returns removed contract or [`None`] if contract with the given name + /// wasn't found. + pub fn remove(&mut self, name: &str) -> Option { + self.contracts.remove(name) + } + + /// Create an iterator that yields the artifact's contracts. + pub fn iter(&self) -> impl Iterator + '_ { + self.contracts.values() + } + + /// Take all contracts from the artifact, leaving it empty, + /// and iterate over them. + pub fn drain(&mut self) -> impl Iterator + '_ { + self.contracts.drain().map(|(_, contract)| contract) + } +} + +impl Default for Artifact { + fn default() -> Self { + Artifact::new() + } +} diff --git a/ethcontract-common/src/artifact/truffle.rs b/ethcontract-common/src/artifact/truffle.rs new file mode 100644 index 00000000..009b4436 --- /dev/null +++ b/ethcontract-common/src/artifact/truffle.rs @@ -0,0 +1,133 @@ +//! Implements the most common artifact format used in Truffle, Waffle +//! and some other libraries. +//! +//! This artifact is represented as a JSON file containing information about +//! a single contract. We parse the following fields: +//! +//! - `contractName`: name of the contract (optional); +//! - `abi`: information about contract's interface; +//! - `bytecode`: contract's compiled bytecode (optional); +//! - `networks`: info about known contract deployments (optional); +//! - `devdoc`, `userdoc`: additional documentation for contract's methods. + +use crate::artifact::Artifact; +use crate::errors::ArtifactError; +use crate::Contract; +use serde_json::Value; +use std::fs::File; +use std::path::Path; + +/// Loads truffle artifacts. +pub struct TruffleLoader { + /// Override for artifact's origin. + /// + /// If empty, origin will be derived automatically. + pub origin: Option, + + /// Override for contract's name. + /// + /// Truffle artifacts contain a single contract which may + pub name: Option, +} + +impl TruffleLoader { + /// Create a new truffle loader. + pub fn new() -> Self { + TruffleLoader { + origin: None, + name: None, + } + } + + /// Create a new truffle loader and set an override for artifact's origins. + pub fn with_origin(origin: String) -> Self { + TruffleLoader { + origin: Some(origin), + name: None, + } + } + + /// Set new override for artifact's origin. See [`origin`] for more info. + /// + /// [`origin`]: #structfield.origin + #[inline] + pub fn origin(mut self, origin: String) -> Self { + self.origin = Some(origin); + self + } + + /// Set new override for artifact's name. See [`name`] for more info. + /// + /// [`name`]: #structfield.name + #[inline] + pub fn name(mut self, name: String) -> Self { + self.name = Some(name); + self + } + + /// Parse a truffle artifact from JSON string. + pub fn load_from_string(&self, json: &str) -> Result { + let origin = self + .origin + .clone() + .unwrap_or_else(|| "".to_string()); + let mut artifact = Artifact::with_origin(origin); + artifact.insert(self.load_contract_from_string(json)?); + Ok(artifact) + } + + /// Parse a contract from JSON string. + pub fn load_contract_from_string(&self, json: &str) -> Result { + let mut contract: Contract = serde_json::from_str(json)?; + if let Some(name) = &self.name { + contract.name = name.clone(); + } + Ok(contract) + } + + /// Loads a truffle artifact from JSON value. + pub fn load_from_json(&self, value: Value) -> Result { + let origin = self + .origin + .clone() + .unwrap_or_else(|| "".to_string()); + let mut artifact = Artifact::with_origin(origin); + artifact.insert(self.load_contract_from_json(value)?); + Ok(artifact) + } + + /// Loads a contract from JSON value. + pub fn load_contract_from_json(&self, value: Value) -> Result { + let mut contract: Contract = serde_json::from_value(value)?; + if let Some(name) = &self.name { + contract.name = name.clone(); + } + Ok(contract) + } + + /// Loads a truffle artifact from disk. + pub fn load_from_file(&self, path: &Path) -> Result { + let origin = self + .origin + .clone() + .unwrap_or_else(|| path.display().to_string()); + let mut artifact = Artifact::with_origin(origin); + artifact.insert(self.load_contract_from_file(path)?); + Ok(artifact) + } + + /// Loads a contract from disk. + pub fn load_contract_from_file(&self, path: &Path) -> Result { + let mut contract: Contract = serde_json::from_reader(File::open(path)?)?; + if let Some(name) = &self.name { + contract.name = name.clone(); + } + Ok(contract) + } +} + +impl Default for TruffleLoader { + fn default() -> Self { + TruffleLoader::new() + } +} diff --git a/ethcontract-common/src/contract.rs b/ethcontract-common/src/contract.rs index c95d0d98..6a6c79a7 100644 --- a/ethcontract-common/src/contract.rs +++ b/ethcontract-common/src/contract.rs @@ -13,9 +13,9 @@ use web3::types::Address; #[derive(Clone, Debug, Deserialize)] #[serde(default = "Contract::empty")] pub struct Contract { - /// The contract name + /// The contract name. Unnamed contracts have an empty string as their name. #[serde(rename = "contractName")] - pub contract_name: String, + pub name: String, /// The contract ABI pub abi: Abi, /// The contract deployment bytecode. @@ -32,7 +32,7 @@ impl Contract { /// Creates an empty contract instance. pub fn empty() -> Self { Contract { - contract_name: String::new(), + name: String::new(), abi: Abi { constructor: None, functions: HashMap::new(), diff --git a/ethcontract-common/src/lib.rs b/ethcontract-common/src/lib.rs index 8ca95566..d0e41ee6 100644 --- a/ethcontract-common/src/lib.rs +++ b/ethcontract-common/src/lib.rs @@ -4,6 +4,7 @@ //! the `ethcontract-derive` crate. pub mod abiext; +pub mod artifact; pub mod bytecode; pub mod contract; pub mod errors; diff --git a/ethcontract-generate/src/contract.rs b/ethcontract-generate/src/contract.rs index d79234b5..8e35f4b9 100644 --- a/ethcontract-generate/src/contract.rs +++ b/ethcontract-generate/src/contract.rs @@ -74,8 +74,8 @@ impl Context { let raw_contract_name = if let Some(name) = args.contract_name_override.as_ref() { name - } else if !contract.contract_name.is_empty() { - &contract.contract_name + } else if !contract.name.is_empty() { + &contract.name } else { return Err(anyhow!( "contract artifact is missing a name, this can happen when \