use crate::artifact::Artifact;
use crate::contract::Network;
use crate::errors::ArtifactError;
use crate::{Address, Contract, DeploymentInformation, TransactionHash};
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;
#[must_use = "hardhat loaders do nothing unless you load them"]
pub struct HardHatLoader {
pub origin: Option<String>,
pub networks_allow_list: Vec<NetworkEntry>,
pub networks_deny_list: Vec<NetworkEntry>,
pub contracts_allow_list: Vec<String>,
pub contracts_deny_list: Vec<String>,
}
impl HardHatLoader {
pub fn new() -> Self {
HardHatLoader {
origin: None,
networks_deny_list: Vec::new(),
networks_allow_list: Vec::new(),
contracts_allow_list: Vec::new(),
contracts_deny_list: Vec::new(),
}
}
pub fn with_origin(origin: impl Into<String>) -> Self {
HardHatLoader {
origin: Some(origin.into()),
networks_deny_list: Vec::new(),
networks_allow_list: Vec::new(),
contracts_allow_list: Vec::new(),
contracts_deny_list: Vec::new(),
}
}
pub fn origin(mut self, origin: impl Into<String>) -> Self {
self.origin = Some(origin.into());
self
}
pub fn allow_network_by_chain_id(mut self, network: impl Into<String>) -> Self {
self.networks_allow_list
.push(NetworkEntry::ByChainId(network.into()));
self
}
pub fn allow_network_by_name(mut self, network: impl Into<String>) -> Self {
self.networks_allow_list
.push(NetworkEntry::ByName(network.into()));
self
}
pub fn deny_network_by_chain_id(mut self, network: impl Into<String>) -> Self {
self.networks_deny_list
.push(NetworkEntry::ByChainId(network.into()));
self
}
pub fn deny_network_by_name(mut self, network: impl Into<String>) -> Self {
self.networks_deny_list
.push(NetworkEntry::ByName(network.into()));
self
}
pub fn allow_contract(mut self, contract: impl Into<String>) -> Self {
self.contracts_allow_list.push(contract.into());
self
}
pub fn deny_contract(mut self, contract: impl Into<String>) -> Self {
self.contracts_deny_list.push(contract.into());
self
}
pub fn load_from_reader(&self, f: Format, v: impl Read) -> Result<Artifact, ArtifactError> {
self.load_artifact(f, "<unknown>", v, from_reader, from_reader)
}
pub fn load_from_slice(&self, f: Format, v: &[u8]) -> Result<Artifact, ArtifactError> {
self.load_artifact(f, "<unknown>", v, from_slice, from_slice)
}
pub fn load_from_str(&self, f: Format, v: &str) -> Result<Artifact, ArtifactError> {
self.load_artifact(f, "<unknown>", v, from_str, from_str)
}
pub fn load_from_value(&self, f: Format, v: Value) -> Result<Artifact, ArtifactError> {
self.load_artifact(f, "<unknown>", v, from_value, from_value)
}
pub fn load_from_file(
&self,
f: Format,
p: impl AsRef<Path>,
) -> Result<Artifact, ArtifactError> {
let path = p.as_ref();
let file = File::open(path)?;
let reader = BufReader::new(file);
self.load_artifact(f, path.display(), reader, from_reader, from_reader)
}
pub fn load_from_directory(&self, p: impl AsRef<Path>) -> Result<Artifact, ArtifactError> {
self._load_from_directory(p.as_ref())
}
fn _load_from_directory(&self, p: &Path) -> Result<Artifact, ArtifactError> {
let mut artifact = Artifact::with_origin(p.display().to_string());
let mut chain_id_buf = String::new();
for chain_entry in p.read_dir()? {
let chain_entry = chain_entry?;
let chain_path = chain_entry.path();
if !chain_path.is_dir() {
continue;
}
let chain_id_file = chain_path.join(".chainId");
if !chain_id_file.exists() {
continue;
}
chain_id_buf.clear();
File::open(chain_id_file)?.read_to_string(&mut chain_id_buf)?;
let chain_id = chain_id_buf.trim().to_string();
let chain_name = chain_path
.file_name()
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::Other,
format!("unable to get directory name for path {:?}", chain_path),
)
})?
.to_string_lossy();
if !self.network_allowed(&chain_id, &chain_name) {
continue;
}
for contract_entry in chain_path.read_dir()? {
let contract_entry = contract_entry?;
let contract_path = contract_entry.path();
if !contract_path.is_file() {
continue;
}
let mut contract_name = contract_path
.file_name()
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::Other,
format!("unable to get file name for path {:?}", contract_path),
)
})?
.to_string_lossy()
.into_owned();
if !contract_name.ends_with(".json") {
continue;
}
contract_name.truncate(contract_name.len() - ".json".len());
if !self.contract_allowed(&contract_name) {
continue;
}
let HardHatContract {
address,
transaction_hash,
mut contract,
} = {
let file = File::open(contract_path)?;
let reader = BufReader::new(file);
from_reader(reader)?
};
contract.name = contract_name;
self.add_contract_to_artifact(
&mut artifact,
contract,
chain_id.clone(),
address,
transaction_hash,
)?;
}
}
Ok(artifact)
}
fn load_artifact<T>(
&self,
format: Format,
origin: impl ToString,
source: T,
single_loader: impl FnOnce(T) -> serde_json::Result<HardHatExport>,
multi_loader: impl FnOnce(T) -> serde_json::Result<HardHatMultiExport>,
) -> Result<Artifact, ArtifactError> {
let origin = self.origin.clone().unwrap_or_else(|| origin.to_string());
let mut artifact = Artifact::with_origin(origin);
match 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.network_allowed(&export.chain_id, &export.chain_name) {
for (name, contract) in export.contracts {
let HardHatContract {
address,
transaction_hash,
mut contract,
} = contract;
if !self.contract_allowed(&name) {
continue;
}
contract.name = name;
self.add_contract_to_artifact(
artifact,
contract,
export.chain_id.clone(),
address,
transaction_hash,
)?;
}
}
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 add_contract_to_artifact(
&self,
artifact: &mut Artifact,
contract: Contract,
chain_id: String,
address: Address,
transaction_hash: Option<TransactionHash>,
) -> Result<(), ArtifactError> {
let contract_guard = artifact.get_mut(&contract.name);
let mut contract = if let Some(existing_contract) = contract_guard {
if existing_contract.interface != contract.interface {
return Err(ArtifactError::AbiMismatch(contract.name));
}
existing_contract
} else {
drop(contract_guard);
artifact.insert(contract).inserted_contract
};
let deployment_information = transaction_hash.map(DeploymentInformation::TransactionHash);
if contract.networks.contains_key(&chain_id) {
Err(ArtifactError::DuplicateChain(chain_id))
} else {
contract.networks_mut().insert(
chain_id,
Network {
address,
deployment_information,
},
);
Ok(())
}
}
fn contract_allowed(&self, name: &str) -> bool {
!self.contract_explicitly_denied(name)
&& (self.contracts_allow_list.is_empty() || self.contract_explicitly_allowed(name))
}
fn contract_explicitly_allowed(&self, name: &str) -> bool {
self.contracts_allow_list.iter().any(|x| x == name)
}
fn contract_explicitly_denied(&self, name: &str) -> bool {
self.contracts_deny_list.iter().any(|x| x == name)
}
fn network_allowed(&self, chain_id: &str, chain_name: &str) -> bool {
!self.network_explicitly_denied(chain_id, chain_name)
&& (self.networks_allow_list.is_empty()
|| self.network_explicitly_allowed(chain_id, chain_name))
}
fn network_explicitly_allowed(&self, chain_id: &str, chain_name: &str) -> bool {
self.networks_allow_list
.iter()
.any(|x| x.matches(chain_id, chain_name))
}
fn network_explicitly_denied(&self, chain_id: &str, chain_name: &str) -> bool {
self.networks_deny_list
.iter()
.any(|x| x.matches(chain_id, chain_name))
}
}
impl Default for HardHatLoader {
fn default() -> Self {
HardHatLoader::new()
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Format {
SingleExport,
MultiExport,
}
#[derive(Clone, Debug)]
pub enum NetworkEntry {
ByChainId(String),
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<String, HashMap<String, HardHatExport>>,
}
#[derive(Deserialize)]
struct HardHatExport {
#[serde(rename = "name")]
chain_name: String,
#[serde(rename = "chainId")]
chain_id: String,
contracts: HashMap<String, HardHatContract>,
}
#[derive(Deserialize)]
struct HardHatContract {
address: Address,
#[serde(rename = "transactionHash")]
transaction_hash: Option<TransactionHash>,
#[serde(flatten)]
contract: Contract,
}
#[cfg(test)]
mod test {
use super::*;
use std::path::PathBuf;
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()
.load_from_str(Format::SingleExport, 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));
}
static MULTI_EXPORT: &str = r#"
{
"1": {
"mainnet": {
"name": "mainnet",
"chainId": "1",
"contracts": {
"A": {
"address": "0x000000000000000000000000000000000000000A"
},
"B": {
"address": "0x000000000000000000000000000000000000000B"
}
}
}
},
"4": {
"rinkeby": {
"name": "rinkeby",
"chainId": "4",
"contracts": {
"A": {
"address": "0x00000000000000000000000000000000000000AA"
}
}
}
}
}
"#;
#[test]
fn load_multi() {
let artifact = HardHatLoader::new()
.load_from_str(Format::MultiExport, MULTI_EXPORT)
.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()
.load_from_str(Format::MultiExport, 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().load_from_str(Format::MultiExport, 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().load_from_str(Format::MultiExport, 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()
.allow_network_by_name("mainnet")
.allow_network_by_name("rinkeby")
.load_from_str(Format::MultiExport, 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()
.allow_network_by_chain_id("4")
.load_from_str(Format::MultiExport, 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()
.deny_network_by_name("mainnet_beta")
.load_from_str(Format::MultiExport, 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()
.deny_network_by_chain_id("1")
.load_from_str(Format::MultiExport, 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_allow_contract_name() {
let artifact = HardHatLoader::new()
.allow_contract("A")
.load_from_str(Format::MultiExport, MULTI_EXPORT)
.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(0xAA));
let artifact = HardHatLoader::new()
.allow_contract("X")
.load_from_str(Format::MultiExport, MULTI_EXPORT)
.unwrap();
assert_eq!(artifact.len(), 0);
}
#[test]
fn load_multi_deny_contract_name() {
let artifact = HardHatLoader::new()
.deny_contract("A")
.load_from_str(Format::MultiExport, MULTI_EXPORT)
.unwrap();
assert_eq!(artifact.len(), 1);
let a = artifact.get("B").unwrap();
assert_eq!(a.name, "B");
assert_eq!(a.networks.len(), 1);
assert_eq!(a.networks["1"].address, address(0xB));
let artifact = HardHatLoader::new()
.deny_contract("X")
.load_from_str(Format::MultiExport, MULTI_EXPORT)
.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));
}
fn hardhat_dir() -> PathBuf {
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("../examples/hardhat/deployments");
path
}
#[test]
fn load_from_directory() {
let artifact = HardHatLoader::new()
.load_from_directory(hardhat_dir())
.unwrap();
assert_eq!(artifact.len(), 1);
let a = artifact.get("DeployedContract").unwrap();
assert_eq!(a.name, "DeployedContract");
assert_eq!(a.networks.len(), 2);
assert_eq!(
a.networks["4"].address,
"0x4E29B76eC7d20c58A6B156CB464594a4ae39FdEd"
.parse()
.unwrap()
);
assert_eq!(
a.networks["4"].deployment_information,
Some(DeploymentInformation::TransactionHash(
"0x0122d15a8d394b8f9e45c15b7d3e5365bbf7122a15952246676e2fe7eb858f35"
.parse()
.unwrap()
))
);
assert_eq!(
a.networks["1337"].address,
"0x29BE0588389993e7064C21f00761303eb51373F5"
.parse()
.unwrap()
);
assert_eq!(
a.networks["1337"].deployment_information,
Some(DeploymentInformation::TransactionHash(
"0xe0631d7f749fe73f94e59f6e25ff9b925980e8e29ed67b8f862ec76a783ea06e"
.parse()
.unwrap()
))
);
}
}