diff --git a/ethcontract-common/src/artifact.rs b/ethcontract-common/src/artifact.rs index a38ff82f..cdf3731d 100644 --- a/ethcontract-common/src/artifact.rs +++ b/ethcontract-common/src/artifact.rs @@ -8,9 +8,13 @@ //! artifact models. It also provides tools to load artifacts from different //! sources, and parse them using different formats. -use crate::Contract; +use crate::contract::{Documentation, Network}; +use crate::{Abi, Bytecode, Contract}; +use std::collections::hash_map::Entry; use std::collections::HashMap; +use std::ops::Deref; +pub mod hardhat; pub mod truffle; /// An entity that contains compiled contracts. @@ -29,9 +33,9 @@ impl Artifact { } /// Create a new artifact with an origin information. - pub fn with_origin(origin: String) -> Self { + pub fn with_origin(origin: impl Into) -> Self { Artifact { - origin, + origin: origin.into(), contracts: HashMap::new(), } } @@ -53,6 +57,16 @@ impl Artifact { self.origin = origin.into(); } + /// Get number of contracts contained in this artifact. + pub fn len(&self) -> usize { + self.contracts.len() + } + + /// Check if this artifact contains no contracts. + pub fn is_empty(&self) -> bool { + self.contracts.is_empty() + } + /// Check whether this artifact has a contract with the given name. pub fn contains(&self, name: &str) -> bool { self.contracts.contains_key(name) @@ -66,12 +80,33 @@ impl Artifact { self.contracts.get(name) } + /// Get contract by name. + /// + /// Returns a handle that allows mutating the contract. It does not allow + /// renaming contract though. For that, you'll need to remove + /// it and add again. + pub fn get_mut(&mut self, name: &str) -> Option { + self.contracts.get_mut(name).map(ContractMut) + } + /// 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) + /// and return the old contract. + pub fn insert(&mut self, contract: Contract) -> InsertResult { + match self.contracts.entry(contract.name.clone()) { + Entry::Occupied(mut o) => { + let old_contract = o.insert(contract); + InsertResult { + inserted_contract: ContractMut(o.into_mut()), + old_contract: Some(old_contract), + } + } + Entry::Vacant(v) => InsertResult { + inserted_contract: ContractMut(v.insert(contract)), + old_contract: None, + }, + } } /// Remove contract from the artifact. @@ -99,3 +134,120 @@ impl Default for Artifact { Artifact::new() } } + +/// Result of inserting a nre contract into an artifact. +pub struct InsertResult<'a> { + /// Reference to the newly inserted contract. + pub inserted_contract: ContractMut<'a>, + + /// If insert operation replaced an old contract, it will appear here. + pub old_contract: Option, +} + +/// A wrapper that allows mutating contract +/// but doesn't allow changing its name. +pub struct ContractMut<'a>(&'a mut Contract); + +impl<'a> ContractMut<'a> { + /// Get mutable access to abi. + pub fn abi_mut(&mut self) -> &mut Abi { + &mut self.0.abi + } + + /// Get mutable access to bytecode. + pub fn bytecode_mut(&mut self) -> &mut Bytecode { + &mut self.0.bytecode + } + + /// Get mutable access to networks. + pub fn networks_mut(&mut self) -> &mut HashMap { + &mut self.0.networks + } + + /// Get mutable access to devdoc. + pub fn devdoc_mut(&mut self) -> &mut Documentation { + &mut self.0.devdoc + } + + /// Get mutable access to userdoc. + pub fn userdoc_mut(&mut self) -> &mut Documentation { + &mut self.0.userdoc + } +} + +impl Deref for ContractMut<'_> { + type Target = Contract; + + fn deref(&self) -> &Self::Target { + self.0 + } +} + +#[cfg(test)] +mod test { + use super::*; + + fn make_contract(name: &str) -> Contract { + let mut contract = Contract::empty(); + contract.name = name.to_string(); + contract + } + + #[test] + fn insert() { + let mut artifact = Artifact::new(); + + assert_eq!(artifact.len(), 0); + + let insert_res = artifact.insert(make_contract("C1")); + + assert_eq!(insert_res.inserted_contract.name, "C1"); + assert!(insert_res.old_contract.is_none()); + + assert_eq!(artifact.len(), 1); + assert!(artifact.contains("C1")); + + let insert_res = artifact.insert(make_contract("C2")); + + assert_eq!(insert_res.inserted_contract.name, "C2"); + assert!(insert_res.old_contract.is_none()); + + assert_eq!(artifact.len(), 2); + assert!(artifact.contains("C2")); + + let insert_res = artifact.insert(make_contract("C1")); + + assert_eq!(insert_res.inserted_contract.name, "C1"); + assert!(insert_res.old_contract.is_some()); + + assert_eq!(artifact.len(), 2); + } + + #[test] + fn remove() { + let mut artifact = Artifact::new(); + + artifact.insert(make_contract("C1")); + artifact.insert(make_contract("C2")); + + assert_eq!(artifact.len(), 2); + assert!(artifact.contains("C1")); + assert!(artifact.contains("C2")); + + let c0 = artifact.remove("C0"); + assert!(c0.is_none()); + + assert_eq!(artifact.len(), 2); + assert!(artifact.contains("C1")); + assert!(artifact.contains("C2")); + + let c1 = artifact.remove("C1"); + + assert!(c1.is_some()); + assert_eq!(c1.unwrap().name, "C1"); + + assert_eq!(artifact.len(), 1); + assert!(!artifact.contains("C1")); + assert!(artifact.contains("C2")); + } +} diff --git a/ethcontract-common/src/artifact/hardhat.rs b/ethcontract-common/src/artifact/hardhat.rs new file mode 100644 index 00000000..51420292 --- /dev/null +++ b/ethcontract-common/src/artifact/hardhat.rs @@ -0,0 +1,670 @@ +//! Implements artifact format generated by [hardhat-deploy] plugin. +//! +//! There are three distinct artifact formats. +//! +//! First is called "hardhat export", it is a JSON file that contains +//! information about a single network and all contracts deployed on it. +//! It can be generated with `hardhat export` command. +//! +//! Second is called "hardhat multi-export", it contains information about +//! multiple networks, for each network it contains information about +//! all contracts deployed on it. It can be generated with +//! `hardhat export --export-all` command. +//! +//! Both formats are supported by [`HardHatLoader`], see its documentation +//! for info and limitations. +//! +//! [hardhat-deploy]: https://github.com/wighawag/hardhat-deploy + +use crate::artifact::Artifact; +use crate::contract::Network; +use crate::errors::ArtifactError; +use crate::{Address, Contract}; +use serde::Deserialize; +use serde_json::{from_reader, from_slice, from_str, from_value, Value}; +use std::collections::HashMap; +use std::fs::File; +use std::io::{BufReader, Read}; +use std::path::Path; + +/// Loads hardhat artifacts generated via `--export` and `--export-all`. +/// +/// # Limitations +/// +/// In hardhat, a contract could have different ABIs on different networks. +/// This could happen when deploying test versions of contracts. +/// Ethcontract does not support this. Parsing such artifact will result +/// in an error. You'll have to rename contracts, or filter out networks +/// with [`networks_allow_list`]. +/// +/// Another limitation is that hardhat allows having multiple networks +/// with the same chain ID. For example, you can have `rinkeby` +/// and `rinkeby-testing`. Both have chain ID of `4`, but contract addresses +/// and ABIs can be different. Ethcontract does not support this, so you'll +/// have to filter such networks. See [#545] for more info. +/// +/// [#545]: https://github.com/gnosis/ethcontract-rs/issues/545. +pub struct HardHatLoader { + /// Override for artifact's origin. If `None`, origin + /// will be derived automatically. + pub origin: Option, + + /// Artifact format. + pub format: Format, + + /// List of allowed network names and chain IDs. + /// + /// When loading a contract, networks with names that aren't found + /// in this list will be completely ignored. Contracts from these networks + /// will not be loaded. You can use this mechanism to bypass + /// the requirement that a contract must have the same ABI on all networks. + /// + /// Empty list means that all networks are allowed. + pub networks_allow_list: Vec, + + /// List of denied network names and chain IDs. + /// + /// When loading a contract, networks with names that are found + /// in this list will be completely ignored. + /// + /// Empty list means that no networks are denied. + /// + /// Deny list takes precedence over allow list. That is, if network + /// appears in both, it will be denied. + pub networks_deny_list: Vec, +} + +impl HardHatLoader { + /// Create a new hardhat loader. + pub fn new(format: Format) -> Self { + HardHatLoader { + origin: None, + format, + networks_deny_list: Vec::new(), + networks_allow_list: Vec::new(), + } + } + + /// Create a new hardhat loader and set an override for artifact's origins. + pub fn with_origin(format: Format, origin: impl Into) -> Self { + HardHatLoader { + origin: Some(origin.into()), + format, + networks_deny_list: Vec::new(), + networks_allow_list: Vec::new(), + } + } + + /// Set new override for artifact's origin. See [`origin`] for more info. + /// + /// [`origin`]: #structfield.origin + pub fn origin(mut self, origin: impl Into) -> Self { + self.origin = Some(origin.into()); + self + } + + /// Set new format for artifacts. + pub fn format(mut self, format: Format) -> Self { + self.format = format; + self + } + + /// Add chain id to the list of [`allowed networks`]. + /// + /// [`allowed networks`]: #structfield.networks_allow_list + pub fn allow_by_chain_id(mut self, network: impl Into) -> Self { + self.networks_allow_list + .push(NetworkEntry::ByChainId(network.into())); + self + } + + /// Add network name to the list of [`allowed networks`]. + /// + /// [`allowed networks`]: #structfield.networks_allow_list + pub fn allow_by_name(mut self, network: impl Into) -> Self { + self.networks_allow_list + .push(NetworkEntry::ByName(network.into())); + self + } + + /// Add chain id to the list of [`denyid networks`]. + /// + /// [`denied networks`]: #structfield.networks_deny_list + pub fn deny_by_chain_id(mut self, network: impl Into) -> Self { + self.networks_deny_list + .push(NetworkEntry::ByChainId(network.into())); + self + } + + /// Add network name to the list of [`denied networks`]. + /// + /// [`denied networks`]: #structfield.networks_deny_list + pub fn deny_by_name(mut self, network: impl Into) -> Self { + self.networks_deny_list + .push(NetworkEntry::ByName(network.into())); + self + } + + /// Loads an artifact from a loaded JSON value. + pub fn load_from_reader(&self, v: impl Read) -> Result { + self.load_artifact("", v, from_reader, from_reader) + } + + /// Loads an artifact from bytes of JSON text. + pub fn load_from_slice(&self, v: &[u8]) -> Result { + self.load_artifact("", v, from_slice, from_slice) + } + + /// Loads an artifact from string of JSON text. + pub fn load_from_str(&self, v: &str) -> Result { + self.load_artifact("", v, from_str, from_str) + } + + /// Loads an artifact from a loaded JSON value. + pub fn load_from_value(&self, v: Value) -> Result { + self.load_artifact("", v, from_value, from_value) + } + + /// Loads an artifact from disk. + pub fn load_from_file(&self, p: &Path) -> Result { + let file = File::open(p)?; + let reader = BufReader::new(file); + self.load_artifact(p.display(), reader, from_reader, from_reader) + } + + fn load_artifact( + &self, + origin: impl ToString, + source: T, + single_loader: impl FnOnce(T) -> serde_json::Result, + multi_loader: impl FnOnce(T) -> serde_json::Result, + ) -> Result { + let origin = self.origin.clone().unwrap_or_else(|| origin.to_string()); + + let mut artifact = Artifact::with_origin(origin); + + match self.format { + Format::SingleExport => { + let loaded = single_loader(source)?; + self.fill_artifact(&mut artifact, loaded)? + } + Format::MultiExport => { + let loaded = multi_loader(source)?; + self.fill_artifact_multi(&mut artifact, loaded)? + } + } + + Ok(artifact) + } + + fn fill_artifact( + &self, + artifact: &mut Artifact, + export: HardHatExport, + ) -> Result<(), ArtifactError> { + if self.allowed(&export.chain_id, &export.chain_name) { + for (name, contract_with_address) in export.contracts { + let ContractWithAddress { + address, + mut contract, + } = contract_with_address; + + contract.name = name; + + let mut contract = match artifact.get_mut(&contract.name) { + Some(existing_contract) => { + if existing_contract.abi != contract.abi { + return Err(ArtifactError::AbiMismatch(contract.name)); + } + + existing_contract + } + None => artifact.insert(contract).inserted_contract, + }; + + let existing_network = contract.networks_mut().insert( + export.chain_id.clone(), + Network { + address, + deployment_information: None, + }, + ); + + if existing_network.is_some() { + return Err(ArtifactError::DuplicateChain(export.chain_id)); + } + } + } + + Ok(()) + } + + fn fill_artifact_multi( + &self, + artifact: &mut Artifact, + export: HardHatMultiExport, + ) -> Result<(), ArtifactError> { + for (_, export) in export.networks { + for (_, export) in export { + self.fill_artifact(artifact, export)?; + } + } + + Ok(()) + } + + fn allowed(&self, chain_id: &str, chain_name: &str) -> bool { + !self.explicitly_denied(chain_id, chain_name) + && (self.networks_allow_list.is_empty() + || self.explicitly_allowed(chain_id, chain_name)) + } + + fn explicitly_allowed(&self, chain_id: &str, chain_name: &str) -> bool { + self.networks_allow_list + .iter() + .any(|x| x.matches(chain_id, chain_name)) + } + + fn explicitly_denied(&self, chain_id: &str, chain_name: &str) -> bool { + self.networks_deny_list + .iter() + .any(|x| x.matches(chain_id, chain_name)) + } +} + +/// Artifact format. +#[derive(Copy, Clone, Debug)] +pub enum Format { + /// Contracts for a single network. Generated with `hardhat export`. + SingleExport, + + /// Contracts for all networks. Generated with `hardhat export --export-all`. + MultiExport, +} + +/// Network allow-deny entry. +#[derive(Clone, Debug)] +pub enum NetworkEntry { + /// Network identified by chain ID. + ByChainId(String), + + /// Network identified by its name specified in `hardhat.config.js`. + ByName(String), +} + +impl NetworkEntry { + fn matches(&self, chain_id: &str, chain_name: &str) -> bool { + match self { + NetworkEntry::ByChainId(id) => chain_id == id, + NetworkEntry::ByName(name) => chain_name == name, + } + } +} + +#[derive(Deserialize)] +struct HardHatMultiExport { + #[serde(flatten)] + networks: HashMap>, +} + +#[derive(Deserialize)] +struct HardHatExport { + #[serde(rename = "name")] + chain_name: String, + #[serde(rename = "chainId")] + chain_id: String, + + contracts: HashMap, +} + +#[derive(Deserialize)] +struct ContractWithAddress { + address: Address, + #[serde(flatten)] + contract: Contract, +} + +#[cfg(test)] +mod test { + use super::*; + use web3::ethabi::ethereum_types::BigEndianHash; + use web3::types::{H256, U256}; + + fn address(address: u8) -> Address { + Address::from(H256::from_uint(&U256::from(address))) + } + + #[test] + fn load_single() { + let json = r#" + { + "name": "mainnet", + "chainId": "1", + "contracts": { + "A": { + "address": "0x000000000000000000000000000000000000000A" + }, + "B": { + "address": "0x000000000000000000000000000000000000000B" + } + } + } + "#; + + let artifact = HardHatLoader::new(Format::SingleExport) + .load_from_str(json) + .unwrap(); + + assert_eq!(artifact.len(), 2); + + let a = artifact.get("A").unwrap(); + assert_eq!(a.name, "A"); + assert_eq!(a.networks.len(), 1); + assert_eq!(a.networks["1"].address, address(0xA)); + + let b = artifact.get("B").unwrap(); + assert_eq!(b.name, "B"); + assert_eq!(b.networks.len(), 1); + assert_eq!(b.networks["1"].address, address(0xB)); + } + + #[test] + fn load_multi() { + let json = r#" + { + "1": { + "mainnet": { + "name": "mainnet", + "chainId": "1", + "contracts": { + "A": { + "address": "0x000000000000000000000000000000000000000A" + }, + "B": { + "address": "0x000000000000000000000000000000000000000B" + } + } + } + }, + "4": { + "rinkeby": { + "name": "rinkeby", + "chainId": "4", + "contracts": { + "A": { + "address": "0x00000000000000000000000000000000000000AA" + } + } + } + } + } + "#; + + let artifact = HardHatLoader::new(Format::MultiExport) + .load_from_str(json) + .unwrap(); + + assert_eq!(artifact.len(), 2); + + let a = artifact.get("A").unwrap(); + assert_eq!(a.name, "A"); + assert_eq!(a.networks.len(), 2); + assert_eq!(a.networks["1"].address, address(0xA)); + assert_eq!(a.networks["4"].address, address(0xAA)); + + let b = artifact.get("B").unwrap(); + assert_eq!(b.name, "B"); + assert_eq!(b.networks.len(), 1); + assert_eq!(b.networks["1"].address, address(0xB)); + } + + #[test] + fn load_multi_duplicate_networks_ok() { + let json = r#" + { + "1": { + "mainnet": { + "name": "mainnet", + "chainId": "1", + "contracts": { + "A": { + "address": "0x000000000000000000000000000000000000000A" + } + } + }, + "mainnet_beta": { + "name": "mainnet_beta", + "chainId": "1", + "contracts": { + "B": { + "address": "0x000000000000000000000000000000000000000B" + } + } + } + } + } + "#; + + let artifact = HardHatLoader::new(Format::MultiExport) + .load_from_str(json) + .unwrap(); + + assert_eq!(artifact.len(), 2); + + let a = artifact.get("A").unwrap(); + assert_eq!(a.name, "A"); + assert_eq!(a.networks.len(), 1); + assert_eq!(a.networks["1"].address, address(0xA)); + + let b = artifact.get("B").unwrap(); + assert_eq!(b.name, "B"); + assert_eq!(b.networks.len(), 1); + assert_eq!(b.networks["1"].address, address(0xB)); + } + + #[test] + fn load_multi_duplicate_networks_err() { + let json = r#" + { + "1": { + "mainnet": { + "name": "mainnet", + "chainId": "1", + "contracts": { + "A": { + "address": "0x000000000000000000000000000000000000000A" + } + } + }, + "mainnet_beta": { + "name": "mainnet_beta", + "chainId": "1", + "contracts": { + "A": { + "address": "0x00000000000000000000000000000000000000AA" + } + } + } + } + } + "#; + + let err = HardHatLoader::new(Format::MultiExport).load_from_str(json); + + match err { + Err(ArtifactError::DuplicateChain(chain_id)) => assert_eq!(chain_id, "1"), + Err(unexpected_err) => panic!("unexpected error {:?}", unexpected_err), + _ => panic!("didn't throw an error"), + } + } + + #[test] + fn load_multi_mismatching_abi() { + let json = r#" + { + "1": { + "mainnet": { + "name": "mainnet", + "chainId": "1", + "contracts": { + "A": { + "address": "0x000000000000000000000000000000000000000A", + "abi": [ + { + "constant": false, + "inputs": [], + "name": "foo", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } + ] + } + } + } + }, + "4": { + "rinkeby": { + "name": "rinkeby", + "chainId": "4", + "contracts": { + "A": { + "address": "0x00000000000000000000000000000000000000AA", + "abi": [ + { + "constant": false, + "inputs": [], + "name": "bar", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } + ] + } + } + } + } + } + "#; + + let err = HardHatLoader::new(Format::MultiExport).load_from_str(json); + + match err { + Err(ArtifactError::AbiMismatch(name)) => assert_eq!(name, "A"), + Err(unexpected_err) => panic!("unexpected error {:?}", unexpected_err), + _ => panic!("didn't throw an error"), + } + } + + static NETWORK_CONFLICTS: &str = r#" + { + "1": { + "mainnet": { + "name": "mainnet", + "chainId": "1", + "contracts": { + "A": { + "address": "0x000000000000000000000000000000000000000A" + } + } + }, + "mainnet_beta": { + "name": "mainnet_beta", + "chainId": "1", + "contracts": { + "A": { + "address": "0x00000000000000000000000000000000000000AA", + "abi": [ + { + "constant": false, + "inputs": [], + "name": "test_method", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } + ] + } + } + } + }, + "4": { + "rinkeby": { + "name": "rinkeby", + "chainId": "4", + "contracts": { + "A": { + "address": "0x00000000000000000000000000000000000000BA" + } + } + } + } + } + "#; + + #[test] + fn load_multi_allow_by_name() { + let artifact = HardHatLoader::new(Format::MultiExport) + .allow_by_name("mainnet") + .allow_by_name("rinkeby") + .load_from_str(NETWORK_CONFLICTS) + .unwrap(); + + assert_eq!(artifact.len(), 1); + + let a = artifact.get("A").unwrap(); + assert_eq!(a.name, "A"); + assert_eq!(a.networks.len(), 2); + assert_eq!(a.networks["1"].address, address(0xA)); + assert_eq!(a.networks["4"].address, address(0xBA)); + } + + #[test] + fn load_multi_allow_by_chain_id() { + let artifact = HardHatLoader::new(Format::MultiExport) + .allow_by_chain_id("4") + .load_from_str(NETWORK_CONFLICTS) + .unwrap(); + + assert_eq!(artifact.len(), 1); + + let a = artifact.get("A").unwrap(); + assert_eq!(a.name, "A"); + assert_eq!(a.networks.len(), 1); + assert_eq!(a.networks["4"].address, address(0xBA)); + } + + #[test] + fn load_multi_deny_by_name() { + let artifact = HardHatLoader::new(Format::MultiExport) + .deny_by_name("mainnet_beta") + .load_from_str(NETWORK_CONFLICTS) + .unwrap(); + + assert_eq!(artifact.len(), 1); + + let a = artifact.get("A").unwrap(); + assert_eq!(a.name, "A"); + assert_eq!(a.networks.len(), 2); + assert_eq!(a.networks["1"].address, address(0xA)); + assert_eq!(a.networks["4"].address, address(0xBA)); + } + + #[test] + fn load_multi_deny_by_chain_id() { + let artifact = HardHatLoader::new(Format::MultiExport) + .deny_by_chain_id("1") + .load_from_str(NETWORK_CONFLICTS) + .unwrap(); + + assert_eq!(artifact.len(), 1); + + let a = artifact.get("A").unwrap(); + assert_eq!(a.name, "A"); + assert_eq!(a.networks.len(), 1); + assert_eq!(a.networks["4"].address, address(0xBA)); + } +} diff --git a/ethcontract-common/src/artifact/truffle.rs b/ethcontract-common/src/artifact/truffle.rs index 009b4436..b963ba31 100644 --- a/ethcontract-common/src/artifact/truffle.rs +++ b/ethcontract-common/src/artifact/truffle.rs @@ -13,8 +13,9 @@ use crate::artifact::Artifact; use crate::errors::ArtifactError; use crate::Contract; -use serde_json::Value; +use serde_json::{from_reader, from_slice, from_str, from_value, Value}; use std::fs::File; +use std::io::{BufReader, Read}; use std::path::Path; /// Loads truffle artifacts. @@ -26,7 +27,7 @@ pub struct TruffleLoader { /// Override for contract's name. /// - /// Truffle artifacts contain a single contract which may + /// Truffle artifacts contain a single contract which may be unnamed. pub name: Option, } @@ -40,9 +41,9 @@ impl TruffleLoader { } /// Create a new truffle loader and set an override for artifact's origins. - pub fn with_origin(origin: String) -> Self { + pub fn with_origin(origin: impl Into) -> Self { TruffleLoader { - origin: Some(origin), + origin: Some(origin.into()), name: None, } } @@ -50,78 +51,96 @@ impl TruffleLoader { /// 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); + pub fn origin(mut self, origin: impl Into) -> Self { + self.origin = Some(origin.into()); 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); + pub fn name(mut self, name: impl Into) -> Self { + self.name = Some(name.into()); 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) + /// Loads an artifact from a loaded JSON value. + pub fn load_from_reader(&self, v: impl Read) -> Result { + self.load_artifact("", v, from_reader) } - /// 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 an artifact from bytes of JSON text. + pub fn load_from_slice(&self, v: &[u8]) -> Result { + self.load_artifact("", v, from_slice) } - /// 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 an artifact from string of JSON text. + pub fn load_from_str(&self, v: &str) -> Result { + self.load_artifact("", v, from_str) } - /// 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 an artifact from a loaded JSON value. + pub fn load_from_value(&self, v: Value) -> Result { + self.load_artifact("", v, from_value) + } + + /// Loads an artifact from disk. + pub fn load_from_file(&self, p: &Path) -> Result { + let file = File::open(p)?; + let reader = BufReader::new(file); + self.load_artifact(p.display(), reader, from_reader) } - /// 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()); + /// Loads a contract from a loaded JSON value. + pub fn load_contract_from_reader(&self, v: impl Read) -> Result { + self.load_contract(v, from_reader) + } + + /// Loads a contract from bytes of JSON text. + pub fn load_contract_from_slice(&self, v: &[u8]) -> Result { + self.load_contract(v, from_slice) + } + + /// Loads a contract from string of JSON text. + pub fn load_contract_from_str(&self, v: &str) -> Result { + self.load_contract(v, from_str) + } + + /// Loads a contract from a loaded JSON value. + pub fn load_contract_from_value(&self, v: Value) -> Result { + self.load_contract(v, from_value) + } + + /// Loads a contract from disk. + pub fn load_contract_from_file(&self, p: &Path) -> Result { + let file = File::open(p)?; + let reader = BufReader::new(file); + self.load_contract(reader, from_reader) + } + + fn load_artifact( + &self, + origin: impl ToString, + source: T, + loader: impl FnOnce(T) -> serde_json::Result, + ) -> Result { + let origin = self.origin.clone().unwrap_or_else(|| origin.to_string()); let mut artifact = Artifact::with_origin(origin); - artifact.insert(self.load_contract_from_file(path)?); + artifact.insert(self.load_contract(source, loader)?); 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)?)?; + fn load_contract( + &self, + source: T, + loader: impl FnOnce(T) -> serde_json::Result, + ) -> Result { + let mut contract: Contract = loader(source)?; + if let Some(name) = &self.name { contract.name = name.clone(); } + Ok(contract) } } diff --git a/ethcontract-common/src/errors.rs b/ethcontract-common/src/errors.rs index 275782be..e5034076 100644 --- a/ethcontract-common/src/errors.rs +++ b/ethcontract-common/src/errors.rs @@ -4,7 +4,7 @@ use serde_json::Error as JsonError; use std::io::Error as IoError; use thiserror::Error; -/// An error in loading or parsing a truffle artifact. +/// An error in loading or parsing an artifact. #[derive(Debug, Error)] pub enum ArtifactError { /// An IO error occurred when loading a truffle artifact from disk. @@ -14,6 +14,14 @@ pub enum ArtifactError { /// A JSON error occurred while parsing a truffle artifact. #[error("failed to parse contract artifact JSON: {0}")] Json(#[from] JsonError), + + /// Contract was deployed onto different chains, and ABIs don't match. + #[error("contract {0} has different ABIs on different chains")] + AbiMismatch(String), + + /// Contract have multiple deployment addresses on the same chain. + #[error("chain with id {0} appears several times in the artifact")] + DuplicateChain(String), } /// An error reading bytecode string representation.