ethers-rs/crates/ethers-contract/ethers-contract-abigen/src/source.rs

268 lines
9.1 KiB
Rust
Raw Normal View History

2020-05-26 09:37:31 +00:00
//! Module implements reading of contract artifacts from various sources.
2020-05-26 18:57:59 +00:00
use super::util;
use ethers_types::Address;
2020-05-26 09:37:31 +00:00
use anyhow::{anyhow, Context, Error, Result};
2020-05-26 18:57:59 +00:00
use std::{
borrow::Cow,
env, fs,
path::{Path, PathBuf},
str::FromStr,
};
2020-05-26 09:37:31 +00:00
use url::Url;
/// A source of a Truffle artifact JSON.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Source {
2020-05-26 18:57:59 +00:00
/// A raw ABI string
String(String),
/// An ABI located on the local file system.
2020-05-26 09:37:31 +00:00
Local(PathBuf),
2020-05-26 18:57:59 +00:00
/// An ABI to be retrieved over HTTP(S).
2020-05-26 09:37:31 +00:00
Http(Url),
2020-05-26 18:57:59 +00:00
2020-05-26 09:37:31 +00:00
/// An address of a mainnet contract that has been verified on Etherscan.io.
Etherscan(Address),
2020-05-26 18:57:59 +00:00
2020-05-26 09:37:31 +00:00
/// 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 {
2020-05-26 18:57:59 +00:00
/// Parses an ABI from a source
2020-05-26 09:37:31 +00:00
///
2020-05-26 18:57:59 +00:00
/// Contract ABIs can be retrieved from the local filesystem or online
/// from `etherscan.io`, this method parses ABI source URLs and accepts
2020-05-26 09:37:31 +00:00
/// the following:
2020-05-26 18:57:59 +00:00
/// - `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`.
2020-05-26 09:37:31 +00:00
/// - `/absolute/path/to/Contract.json` or
/// `file:///absolute/path/to/Contract.json`: an absolute path or file URL
2020-05-26 18:57:59 +00:00
/// to an ABI JSON file.
/// - `http(s)://...` an HTTP url to a contract ABI.
2020-05-26 09:37:31 +00:00
/// - `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
2020-05-26 18:57:59 +00:00
/// `index.js`). The contract ABI will be retrieved through
2020-05-26 09:37:31 +00:00
/// `unpkg.io`.
pub fn parse<S>(source: S) -> Result<Self>
where
S: AsRef<str>,
{
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<P, S>(root: P, source: S) -> Result<Self>
where
P: AsRef<Path>,
S: AsRef<str>,
{
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<P>(path: P) -> Self
where
P: AsRef<Path>,
{
Source::Local(path.as_ref().into())
}
/// Creates an HTTP source from a URL.
pub fn http<S>(url: S) -> Result<Self>
where
S: AsRef<str>,
{
Ok(Source::Http(Url::parse(url.as_ref())?))
}
/// Creates an Etherscan source from an address string.
pub fn etherscan<S>(address: S) -> Result<Self>
where
S: AsRef<str>,
{
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<S>(package_path: S) -> Self
where
S: Into<String>,
{
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.
2020-05-26 18:57:59 +00:00
pub fn get(&self) -> Result<String> {
2020-05-26 09:37:31 +00:00
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),
2020-05-26 18:57:59 +00:00
Source::String(abi) => Ok(abi.clone()),
2020-05-26 09:37:31 +00:00
}
}
}
impl FromStr for Source {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
Source::parse(s)
}
}
/// Reads a Truffle artifact JSON file from the local filesystem.
fn get_local_contract(path: &Path) -> Result<String> {
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<String> {
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<String> {
// 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<String> {
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);
}
}
}