//! Module implements reading of contract artifacts from various sources. use super::util; use ethers_types::Address; use anyhow::{anyhow, Context, Error, Result}; use std::{ borrow::Cow, env, fs, path::{Path, PathBuf}, str::FromStr, }; use url::Url; /// A source of a Truffle artifact JSON. #[derive(Clone, Debug, Eq, PartialEq)] pub enum Source { /// A raw ABI string String(String), /// An ABI located on the local file system. Local(PathBuf), /// An ABI to be retrieved over HTTP(S). Http(Url), /// An address of a mainnet contract that has been verified on Etherscan.io. Etherscan(Address), /// The package identifier of an npm package with a path to a Truffle /// artifact or ABI to be retrieved from `unpkg.io`. Npm(String), } impl Source { /// Parses an ABI from a source /// /// Contract ABIs can be retrieved from the local filesystem or online /// from `etherscan.io`, this method parses ABI source URLs and accepts /// the following: /// - `relative/path/to/Contract.json`: a relative path to an ABI JSON file. /// This relative path is rooted in the current working directory. /// To specify the root for relative paths, use `Source::with_root`. /// - `/absolute/path/to/Contract.json` or /// `file:///absolute/path/to/Contract.json`: an absolute path or file URL /// to an ABI JSON file. /// - `http(s)://...` an HTTP url to a contract ABI. /// - `etherscan:0xXX..XX` or `https://etherscan.io/address/0xXX..XX`: a /// address or URL of a verified contract on Etherscan. /// - `npm:@org/package@1.0.0/path/to/contract.json` an npmjs package with /// an optional version and path (defaulting to the latest version and /// `index.js`). The contract ABI will be retrieved through /// `unpkg.io`. pub fn parse(source: S) -> Result where S: AsRef, { let root = env::current_dir()?.canonicalize()?; Source::with_root(root, source) } /// Parses an artifact source from a string and a specified root directory /// for resolving relative paths. See `Source::with_root` for more details /// on supported source strings. pub fn with_root(root: P, source: S) -> Result where P: AsRef, S: AsRef, { let base = Url::from_directory_path(root) .map_err(|_| anyhow!("root path '{}' is not absolute"))?; let url = base.join(source.as_ref())?; match url.scheme() { "file" => Ok(Source::local(url.path())), "http" | "https" => match url.host_str() { Some("etherscan.io") => Source::etherscan( url.path() .rsplit('/') .next() .ok_or_else(|| anyhow!("HTTP URL does not have a path"))?, ), _ => Ok(Source::Http(url)), }, "etherscan" => Source::etherscan(url.path()), "npm" => Ok(Source::npm(url.path())), _ => Err(anyhow!("unsupported URL '{}'", url)), } } /// Creates a local filesystem source from a path string. pub fn local

(path: P) -> Self where P: AsRef, { Source::Local(path.as_ref().into()) } /// Creates an HTTP source from a URL. pub fn http(url: S) -> Result where S: AsRef, { Ok(Source::Http(Url::parse(url.as_ref())?)) } /// Creates an Etherscan source from an address string. pub fn etherscan(address: S) -> Result where S: AsRef, { let address = util::parse_address(address).context("failed to parse address for Etherscan source")?; Ok(Source::Etherscan(address)) } /// Creates an Etherscan source from an address string. pub fn npm(package_path: S) -> Self where S: Into, { Source::Npm(package_path.into()) } /// Retrieves the source JSON of the artifact this will either read the JSON /// from the file system or retrieve a contract ABI from the network /// dependending on the source type. pub fn get(&self) -> Result { match self { Source::Local(path) => get_local_contract(path), Source::Http(url) => get_http_contract(url), Source::Etherscan(address) => get_etherscan_contract(*address), Source::Npm(package) => get_npm_contract(package), Source::String(abi) => Ok(abi.clone()), } } } impl FromStr for Source { type Err = Error; fn from_str(s: &str) -> Result { Source::parse(s) } } /// Reads a Truffle artifact JSON file from the local filesystem. fn get_local_contract(path: &Path) -> Result { let path = if path.is_relative() { let absolute_path = path.canonicalize().with_context(|| { format!( "unable to canonicalize file from working dir {} with path {}", env::current_dir() .map(|cwd| cwd.display().to_string()) .unwrap_or_else(|err| format!("??? ({})", err)), path.display(), ) })?; Cow::Owned(absolute_path) } else { Cow::Borrowed(path) }; let json = fs::read_to_string(path).context("failed to read artifact JSON file")?; Ok(abi_or_artifact(json)) } /// Retrieves a Truffle artifact or ABI from an HTTP URL. fn get_http_contract(url: &Url) -> Result { let json = util::http_get(url.as_str()) .with_context(|| format!("failed to retrieve JSON from {}", url))?; Ok(abi_or_artifact(json)) } /// Retrieves a contract ABI from the Etherscan HTTP API and wraps it in an /// artifact JSON for compatibility with the code generation facilities. fn get_etherscan_contract(address: Address) -> Result { // NOTE: We do not retrieve the bytecode since deploying contracts with the // same bytecode is unreliable as the libraries have already linked and // probably don't reference anything when deploying on other networks. let api_key = env::var("ETHERSCAN_API_KEY") .map(|key| format!("&apikey={}", key)) .unwrap_or_default(); let abi_url = format!( "http://api.etherscan.io/api\ ?module=contract&action=getabi&address={:?}&format=raw{}", address, api_key, ); let abi = util::http_get(&abi_url).context("failed to retrieve ABI from Etherscan.io")?; // NOTE: Wrap the retrieved ABI in an empty contract, this is because // currently, the code generation infrastructure depends on having an // `Artifact` instance. let json = format!( r#"{{"abi":{},"networks":{{"1":{{"address":"{:?}"}}}}}}"#, abi, address, ); Ok(json) } /// Retrieves a Truffle artifact or ABI from an npm package through `unpkg.io`. fn get_npm_contract(package: &str) -> Result { let unpkg_url = format!("https://unpkg.io/{}", package); let json = util::http_get(&unpkg_url) .with_context(|| format!("failed to retrieve JSON from for npm package {}", package))?; Ok(abi_or_artifact(json)) } /// A best-effort coersion of an ABI or Truffle artifact JSON document into a /// Truffle artifact JSON document. /// /// This method uses the fact that ABIs are arrays and Truffle artifacts are /// objects to guess at what type of document this is. Note that no parsing or /// validation is done at this point as the document gets parsed and validated /// at generation time. /// /// This needs to be done as currently the contract generation infrastructure /// depends on having a Truffle artifact. fn abi_or_artifact(json: String) -> String { if json.trim().starts_with('[') { format!(r#"{{"abi":{}}}"#, json.trim()) } else { json } } #[cfg(test)] mod tests { use super::*; #[test] fn parse_source() { let root = "/rooted"; for (url, expected) in &[ ( "relative/Contract.json", Source::local("/rooted/relative/Contract.json"), ), ( "/absolute/Contract.json", Source::local("/absolute/Contract.json"), ), ( "https://my.domain.eth/path/to/Contract.json", Source::http("https://my.domain.eth/path/to/Contract.json").unwrap(), ), ( "etherscan:0x0001020304050607080910111213141516171819", Source::etherscan("0x0001020304050607080910111213141516171819").unwrap(), ), ( "https://etherscan.io/address/0x0001020304050607080910111213141516171819", Source::etherscan("0x0001020304050607080910111213141516171819").unwrap(), ), ( "npm:@openzeppelin/contracts@2.5.0/build/contracts/IERC20.json", Source::npm("@openzeppelin/contracts@2.5.0/build/contracts/IERC20.json"), ), ] { let source = Source::with_root(root, url).unwrap(); assert_eq!(source, *expected); } } }