refactor(abigen): source (#2016)
* rm parse_address * refactor: sources * add comments, support <chain>:<address> * fix doc * chore: clippy * fmt * fix Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
This commit is contained in:
parent
0841e9b53e
commit
c2d7b8321f
|
@ -1362,6 +1362,7 @@ dependencies = [
|
|||
"cfg-if 1.0.0",
|
||||
"dunce",
|
||||
"ethers-core",
|
||||
"ethers-etherscan",
|
||||
"ethers-solc",
|
||||
"eyre",
|
||||
"getrandom 0.2.8",
|
||||
|
@ -1375,6 +1376,7 @@ dependencies = [
|
|||
"serde_json",
|
||||
"syn",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"toml",
|
||||
"url",
|
||||
"walkdir",
|
||||
|
|
|
@ -15,6 +15,7 @@ keywords = ["ethereum", "web3", "celo", "ethers"]
|
|||
|
||||
[dependencies]
|
||||
ethers-core = { version = "^1.0.0", path = "../../ethers-core", features = ["macros"] }
|
||||
ethers-etherscan = { path = "../../ethers-etherscan", default-features = false, optional = true }
|
||||
|
||||
proc-macro2 = "1.0"
|
||||
quote = "1.0"
|
||||
|
@ -22,11 +23,9 @@ syn = { version = "1.0.12", default-features = false, features = ["full"] }
|
|||
prettyplease = "0.1.23"
|
||||
|
||||
Inflector = "0.11"
|
||||
url = "2.1"
|
||||
serde_json = "1.0.61"
|
||||
serde = { version = "1.0.124", features = ["derive"] }
|
||||
hex = { version = "0.4.2", default-features = false, features = ["std"] }
|
||||
reqwest = { version = "0.11.3", default-features = false, features = ["blocking"], optional = true }
|
||||
cfg-if = "1.0.0"
|
||||
dunce = "1.0.2"
|
||||
walkdir = "2.3.2"
|
||||
|
@ -34,6 +33,13 @@ eyre = "0.6"
|
|||
regex = "1.6.0"
|
||||
toml = "0.5.9"
|
||||
|
||||
reqwest = { version = "0.11.3", default-features = false, features = ["blocking"], optional = true }
|
||||
tokio = { version = "1.0", default-features = false, features = [
|
||||
"rt-multi-thread",
|
||||
"sync",
|
||||
], optional = true }
|
||||
url = { version = "2.3.1", default-features = false, optional = true }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
# NOTE: this enables wasm compatibility for getrandom indirectly
|
||||
getrandom = { version = "0.2", features = ["js"] }
|
||||
|
@ -43,9 +49,9 @@ all-features = true
|
|||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[features]
|
||||
default = ["reqwest", "rustls"]
|
||||
openssl = ["reqwest/native-tls"]
|
||||
rustls = ["reqwest/rustls-tls"]
|
||||
online = ["reqwest", "ethers-etherscan", "url", "tokio"]
|
||||
openssl = ["online", "reqwest/native-tls", "ethers-etherscan/openssl"]
|
||||
rustls = ["online", "reqwest/rustls-tls", "ethers-etherscan/rustls"]
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.2.0"
|
||||
|
|
|
@ -27,9 +27,6 @@ pub use multi::MultiAbigen;
|
|||
mod source;
|
||||
pub use source::Source;
|
||||
|
||||
mod util;
|
||||
pub use util::parse_address;
|
||||
|
||||
pub use ethers_core::types::Address;
|
||||
|
||||
use contract::{Context, ExpandedContract};
|
||||
|
|
|
@ -1,400 +0,0 @@
|
|||
//! Module implements reading of contract artifacts from various sources.
|
||||
use super::util;
|
||||
use ethers_core::types::Address;
|
||||
|
||||
use crate::util::resolve_path;
|
||||
use cfg_if::cfg_if;
|
||||
use eyre::{eyre, Context, Error, Result};
|
||||
use std::{env, fs, path::Path, 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(String),
|
||||
|
||||
/// An ABI to be retrieved over HTTP(S).
|
||||
Http(Url),
|
||||
|
||||
/// An address of a mainnet contract that has been verified on Bscscan.com.
|
||||
Bscscan(Address),
|
||||
|
||||
/// An address of a mainnet contract that has been verified on Etherscan.io.
|
||||
Etherscan(Address),
|
||||
|
||||
/// An address of a mainnet contract that has been verified on Polygonscan.com.
|
||||
Polygonscan(Address),
|
||||
|
||||
/// An address of a mainnet contract that has been verified on snowtrace.io.
|
||||
Snowtrace(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`. They can also be provided in-line. This method parses
|
||||
/// ABI source URLs and accepts the following:
|
||||
///
|
||||
/// - raw ABI JSON
|
||||
///
|
||||
/// - `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.
|
||||
/// - `bscscan:0xXX..XX` or `https://bscscan.io/address/0xXX..XX`: a address or URL of a
|
||||
/// verified contract on Bscscan.
|
||||
/// - `polygonscan:0xXX..XX` or `https://polygonscan.io/address/0xXX..XX`: a address or URL of a
|
||||
/// verified contract on Polygonscan.
|
||||
/// - `snowtrace:0xXX..XX` or `https://snowtrace.io/address/0xXX..XX`: a address or URL of a
|
||||
/// verified contract on Snowtrace.
|
||||
/// - `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<S>(source: S) -> Result<Self>
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
let source = source.as_ref();
|
||||
if matches!(source.chars().next(), Some('[' | '{')) {
|
||||
return Ok(Source::String(source.to_owned()))
|
||||
}
|
||||
let root = env::var("CARGO_MANIFEST_DIR")?;
|
||||
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.
|
||||
fn with_root<P, S>(root: P, source: S) -> Result<Self>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
S: AsRef<str>,
|
||||
{
|
||||
let source = source.as_ref();
|
||||
let root = root.as_ref();
|
||||
cfg_if! {
|
||||
if #[cfg(target_arch = "wasm32")] {
|
||||
let root = if root.starts_with("/") {
|
||||
format!("file:://{}", root.display())
|
||||
} else {
|
||||
format!("{}", root.display())
|
||||
};
|
||||
let base = Url::parse(&root)
|
||||
.map_err(|_| eyre!("root path '{}' is not absolute", root))?;
|
||||
} else {
|
||||
let base = Url::from_directory_path(root)
|
||||
.map_err(|_| eyre!("root path '{}' is not absolute", root.display()))?;
|
||||
}
|
||||
}
|
||||
let url = base.join(source)?;
|
||||
|
||||
match url.scheme() {
|
||||
"file" => Ok(Source::local(source)),
|
||||
"http" | "https" => match url.host_str() {
|
||||
Some("bscscan.com") => Source::etherscan(
|
||||
url.path()
|
||||
.rsplit('/')
|
||||
.next()
|
||||
.ok_or_else(|| eyre!("HTTP URL does not have a path"))?,
|
||||
),
|
||||
Some("etherscan.io") => Source::etherscan(
|
||||
url.path()
|
||||
.rsplit('/')
|
||||
.next()
|
||||
.ok_or_else(|| eyre!("HTTP URL does not have a path"))?,
|
||||
),
|
||||
Some("polygonscan.com") => Source::polygonscan(
|
||||
url.path()
|
||||
.rsplit('/')
|
||||
.next()
|
||||
.ok_or_else(|| eyre!("HTTP URL does not have a path"))?,
|
||||
),
|
||||
Some("snowtrace.io") => Source::snowtrace(
|
||||
url.path()
|
||||
.rsplit('/')
|
||||
.next()
|
||||
.ok_or_else(|| eyre!("HTTP URL does not have a path"))?,
|
||||
),
|
||||
_ => Ok(Source::Http(url)),
|
||||
},
|
||||
"bscscan" => Source::bscscan(url.path()),
|
||||
"etherscan" => Source::etherscan(url.path()),
|
||||
"polygonscan" => Source::polygonscan(url.path()),
|
||||
"snowtrace" => Source::snowtrace(url.path()),
|
||||
"npm" => Ok(Source::npm(url.path())),
|
||||
_ => Err(eyre!("unsupported URL '{}'", url)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a local filesystem source from a path string.
|
||||
pub fn local(path: impl Into<String>) -> Self {
|
||||
Source::Local(path.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 Bscscan source from an address string.
|
||||
pub fn bscscan<S>(address: S) -> Result<Self>
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
let address =
|
||||
util::parse_address(address).context("failed to parse address for Bscscan source")?;
|
||||
Ok(Source::Bscscan(address))
|
||||
}
|
||||
|
||||
/// 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 Polygonscan source from an address string.
|
||||
pub fn polygonscan<S>(address: S) -> Result<Self>
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
let address = util::parse_address(address)
|
||||
.context("failed to parse address for Polygonscan source")?;
|
||||
Ok(Source::Polygonscan(address))
|
||||
}
|
||||
|
||||
/// Creates an Snowtrace source from an address string.
|
||||
pub fn snowtrace<S>(address: S) -> Result<Self>
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
let address =
|
||||
util::parse_address(address).context("failed to parse address for Snowtrace source")?;
|
||||
Ok(Source::Snowtrace(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
|
||||
/// depending on the source type.
|
||||
pub fn get(&self) -> Result<String> {
|
||||
cfg_if! {
|
||||
if #[cfg(target_arch = "wasm32")] {
|
||||
match self {
|
||||
Source::Local(path) => get_local_contract(path),
|
||||
Source::Http(_) => panic!("Http abi location are not supported for wasm"),
|
||||
Source::Bscscan(_) => panic!("Bscscan abi location are not supported for wasm"),
|
||||
Source::Etherscan(_) => panic!("Etherscan abi location are not supported for wasm"),
|
||||
Source::Polygonscan(_) => panic!("Polygonscan abi location are not supported for wasm"),
|
||||
Source::Snowtrace(_) => panic!("Snowtrace abi location are not supported for wasm"),
|
||||
Source::Npm(_) => panic!("npm abi location are not supported for wasm"),
|
||||
Source::String(abi) => Ok(abi.clone()),
|
||||
}
|
||||
} else {
|
||||
match self {
|
||||
Source::Local(path) => get_local_contract(path),
|
||||
Source::Http(url) => get_http_contract(url),
|
||||
Source::Bscscan(address) => get_etherscan_contract(*address, "bscscan.com"),
|
||||
Source::Etherscan(address) => get_etherscan_contract(*address, "etherscan.io"),
|
||||
Source::Polygonscan(address) => get_etherscan_contract(*address, "polygonscan.com"),
|
||||
Source::Snowtrace(address) => get_etherscan_contract(*address, "snowtrace.io"),
|
||||
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<Self> {
|
||||
Source::parse(s)
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads an artifact JSON file from the local filesystem.
|
||||
///
|
||||
/// The given path can be relative or absolute and can contain env vars like
|
||||
/// `"$CARGO_MANIFEST_DIR/contracts/a.json"`
|
||||
/// If the path is relative after all env vars have been resolved then we assume the root is either
|
||||
/// `CARGO_MANIFEST_DIR` or the current working directory.
|
||||
fn get_local_contract(path: impl AsRef<str>) -> Result<String> {
|
||||
let path = resolve_path(path.as_ref())?;
|
||||
let path = if path.is_relative() {
|
||||
let manifest_path = env::var("CARGO_MANIFEST_DIR")?;
|
||||
let root = Path::new(&manifest_path);
|
||||
let mut contract_path = root.join(&path);
|
||||
if !contract_path.exists() {
|
||||
contract_path = dunce::canonicalize(&path)?;
|
||||
}
|
||||
if !contract_path.exists() {
|
||||
eyre::bail!("Unable to find local contract \"{}\"", path.display())
|
||||
}
|
||||
contract_path
|
||||
} else {
|
||||
path
|
||||
};
|
||||
|
||||
let json = fs::read_to_string(&path)
|
||||
.context(format!("failed to read artifact JSON file with path {}", &path.display()))?;
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
/// Retrieves a Truffle artifact or ABI from an HTTP URL.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
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(json)
|
||||
}
|
||||
|
||||
/// Retrieves a contract ABI from the Etherscan HTTP API and wraps it in an
|
||||
/// artifact JSON for compatibility with the code generation facilities.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn get_etherscan_contract(address: Address, domain: &str) -> 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 = {
|
||||
let key_res = match domain {
|
||||
"bscscan.com" => env::var("BSCSCAN_API_KEY").ok(),
|
||||
"etherscan.io" => env::var("ETHERSCAN_API_KEY").ok(),
|
||||
"polygonscan.com" => env::var("POLYGONSCAN_API_KEY").ok(),
|
||||
"snowtrace.io" => env::var("SNOWTRACE_API_KEY").ok(),
|
||||
_ => None,
|
||||
};
|
||||
key_res.map(|key| format!("&apikey={key}")).unwrap_or_default()
|
||||
};
|
||||
|
||||
let abi_url = format!(
|
||||
"http://api.{domain}/api?module=contract&action=getabi&address={address:?}&format=raw{api_key}",
|
||||
);
|
||||
let abi = util::http_get(&abi_url).context(format!("failed to retrieve ABI from {domain}"))?;
|
||||
|
||||
if abi.starts_with("Contract source code not verified") {
|
||||
eyre::bail!("Contract source code not verified: {:?}", address);
|
||||
}
|
||||
if abi.starts_with('{') && abi.contains("Max rate limit reached") {
|
||||
eyre::bail!(
|
||||
"Max rate limit reached, please use etherscan API Key for higher rate limit: {:?}",
|
||||
address
|
||||
);
|
||||
}
|
||||
|
||||
Ok(abi)
|
||||
}
|
||||
|
||||
/// Retrieves a Truffle artifact or ABI from an npm package through `unpkg.io`.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
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(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(),
|
||||
),
|
||||
(
|
||||
"bscscan:0x0001020304050607080910111213141516171819",
|
||||
Source::bscscan("0x0001020304050607080910111213141516171819").unwrap(),
|
||||
),
|
||||
(
|
||||
"etherscan:0x0001020304050607080910111213141516171819",
|
||||
Source::etherscan("0x0001020304050607080910111213141516171819").unwrap(),
|
||||
),
|
||||
(
|
||||
"polygonscan:0x0001020304050607080910111213141516171819",
|
||||
Source::polygonscan("0x0001020304050607080910111213141516171819").unwrap(),
|
||||
),
|
||||
(
|
||||
"snowtrace:0x0001020304050607080910111213141516171819",
|
||||
Source::snowtrace("0x0001020304050607080910111213141516171819").unwrap(),
|
||||
),
|
||||
(
|
||||
"https://bscscan.io/address/0x0001020304050607080910111213141516171819",
|
||||
Source::bscscan("0x0001020304050607080910111213141516171819").unwrap(),
|
||||
),
|
||||
(
|
||||
"https://etherscan.io/address/0x0001020304050607080910111213141516171819",
|
||||
Source::etherscan("0x0001020304050607080910111213141516171819").unwrap(),
|
||||
),
|
||||
(
|
||||
"https://polygonscan.com/address/0x0001020304050607080910111213141516171819",
|
||||
Source::polygonscan("0x0001020304050607080910111213141516171819").unwrap(),
|
||||
),
|
||||
(
|
||||
"https://snowtrace.io/address/0x0001020304050607080910111213141516171819",
|
||||
Source::snowtrace("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);
|
||||
}
|
||||
|
||||
let src = r#"[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"name","type":"string"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"symbol","type":"string"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"decimals","type":"uint8"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"spender","type":"address"},{"name":"value","type":"uint256"}],"name":"approve","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"totalSupply","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"from","type":"address"},{"name":"to","type":"address"},{"name":"value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"who","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"to","type":"address"},{"name":"value","type":"uint256"}],"name":"transfer","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"owner","type":"address"},{"name":"spender","type":"address"}],"name":"allowance","outputs":[{"name":"remaining","type":"uint256"}],"payable":false,"type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"}]"#;
|
||||
let parsed = Source::parse(src).unwrap();
|
||||
assert_eq!(parsed, Source::String(src.to_owned()));
|
||||
|
||||
let hardhat_src = format!(
|
||||
r#"{{"_format": "hh-sol-artifact-1", "contractName": "Verifier", "sourceName": "contracts/verifier.sol", "abi": {src}, "bytecode": "0x", "deployedBytecode": "0x", "linkReferences": {{}}, "deployedLinkReferences": {{}}}}"#,
|
||||
);
|
||||
let hardhat_parsed = Source::parse(&hardhat_src).unwrap();
|
||||
assert_eq!(hardhat_parsed, Source::String(hardhat_src));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn get_etherscan_contract() {
|
||||
let source = Source::etherscan("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
|
||||
let _dai = source.get().unwrap();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,168 @@
|
|||
//! Parse ABI artifacts from different sources.
|
||||
|
||||
// TODO: Support `online` for WASM
|
||||
|
||||
#[cfg(all(feature = "online", not(target_arch = "wasm32")))]
|
||||
mod online;
|
||||
#[cfg(all(feature = "online", not(target_arch = "wasm32")))]
|
||||
pub use online::Explorer;
|
||||
|
||||
use crate::util;
|
||||
use eyre::{Error, Result};
|
||||
use std::{env, fs, path::PathBuf, str::FromStr};
|
||||
|
||||
/// A source of an Ethereum smart contract's ABI.
|
||||
///
|
||||
/// See [`parse`][#method.parse] for more information.
|
||||
#[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 address of a smart contract address verified at a supported blockchain explorer.
|
||||
#[cfg(feature = "online")]
|
||||
Explorer(Explorer, ethers_core::types::Address),
|
||||
|
||||
/// The package identifier of an npm package with a path to a Truffle artifact or ABI to be
|
||||
/// retrieved from `unpkg.io`.
|
||||
#[cfg(feature = "online")]
|
||||
Npm(String),
|
||||
|
||||
/// An ABI to be retrieved over HTTP(S).
|
||||
#[cfg(feature = "online")]
|
||||
Http(url::Url),
|
||||
}
|
||||
|
||||
impl FromStr for Source {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
Source::parse(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl Source {
|
||||
/// Parses an ABI from a source.
|
||||
///
|
||||
/// This method accepts the following:
|
||||
///
|
||||
/// - `{ ... }` or `[ ... ]`: A raw or human-readable ABI object or array of objects.
|
||||
///
|
||||
/// - `relative/path/to/Contract.json`: a relative path to an ABI JSON file. This relative path
|
||||
/// is rooted in the current working directory.
|
||||
///
|
||||
/// - `/absolute/path/to/Contract.json` or `file:///absolute/path/to/Contract.json`: an absolute
|
||||
/// path or file URL to an ABI JSON file.
|
||||
///
|
||||
/// If the `online` feature is enabled:
|
||||
///
|
||||
/// - `npm:@org/package@1.0.0/path/to/contract.json`: A npmjs package with an optional version
|
||||
/// and path (defaulting to the latest version and `index.js`), retrieved through `unpkg.io`.
|
||||
///
|
||||
/// - `http://...`: an HTTP URL to a contract ABI. <br> Note: either the `rustls` or `openssl`
|
||||
/// feature must be enabled to support *HTTPS* URLs.
|
||||
///
|
||||
/// - `<name>:<address>`, `<chain>:<address>` or `<url>/.../<address>`: an address or URL of a
|
||||
/// verified contract on a blockchain explorer. <br> Supported explorers and their respective
|
||||
/// chain:
|
||||
/// - `etherscan` -> `mainnet`
|
||||
/// - `bscscan` -> `bsc`
|
||||
/// - `polygonscan` -> `polygon`
|
||||
/// - `snowtrace` -> `avalanche`
|
||||
pub fn parse(source: impl AsRef<str>) -> Result<Self> {
|
||||
let source = source.as_ref().trim();
|
||||
match source.chars().next() {
|
||||
Some('[' | '{') => Ok(Self::String(source.to_string())),
|
||||
|
||||
#[cfg(not(feature = "online"))]
|
||||
_ => Ok(Self::local(source)?),
|
||||
|
||||
#[cfg(feature = "online")]
|
||||
Some('/') => Self::local(source),
|
||||
#[cfg(feature = "online")]
|
||||
_ => Self::parse_online(source),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a local filesystem source from a path string.
|
||||
pub fn local(path: impl AsRef<str>) -> Result<Self> {
|
||||
// resolve env vars
|
||||
let path = path.as_ref().trim_start_matches("file://");
|
||||
let mut resolved = util::resolve_path(path)?;
|
||||
|
||||
if resolved.is_relative() {
|
||||
// set root at manifest dir, if the path exists
|
||||
if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") {
|
||||
let new = PathBuf::from(manifest_dir).join(&resolved);
|
||||
if new.exists() {
|
||||
resolved = new;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// canonicalize
|
||||
if let Ok(canonicalized) = dunce::canonicalize(&resolved) {
|
||||
resolved = canonicalized;
|
||||
} else {
|
||||
return Err(eyre::eyre!("File does not exist: {}", resolved.display()))
|
||||
}
|
||||
|
||||
Ok(Source::Local(resolved))
|
||||
}
|
||||
|
||||
/// 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 depending on the source type.
|
||||
pub fn get(&self) -> Result<String> {
|
||||
match self {
|
||||
Self::Local(path) => Ok(fs::read_to_string(path)?),
|
||||
Self::String(abi) => Ok(abi.clone()),
|
||||
|
||||
#[cfg(feature = "online")]
|
||||
_ => {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(target_arch = "wasm32")] {
|
||||
Err(eyre::eyre!("Online ABI locations are currently unsupported for WASM builds."))
|
||||
} else {
|
||||
self.get_online()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn parse_source() {
|
||||
let rel = "../tests/solidity-contracts/console.json";
|
||||
let abs = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/solidity-contracts/console.json");
|
||||
let abs_url = concat!(
|
||||
"file://",
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../tests/solidity-contracts/console.json"
|
||||
);
|
||||
let exp = Source::Local(Path::new(rel).canonicalize().unwrap());
|
||||
assert_eq!(Source::parse(rel).unwrap(), exp);
|
||||
assert_eq!(Source::parse(abs).unwrap(), exp);
|
||||
assert_eq!(Source::parse(abs_url).unwrap(), exp);
|
||||
|
||||
// ABI
|
||||
let source = r#"[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"name","type":"string"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"symbol","type":"string"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"decimals","type":"uint8"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"spender","type":"address"},{"name":"value","type":"uint256"}],"name":"approve","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"totalSupply","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"from","type":"address"},{"name":"to","type":"address"},{"name":"value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"who","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"to","type":"address"},{"name":"value","type":"uint256"}],"name":"transfer","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"owner","type":"address"},{"name":"spender","type":"address"}],"name":"allowance","outputs":[{"name":"remaining","type":"uint256"}],"payable":false,"type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"}]"#;
|
||||
let parsed = Source::parse(source).unwrap();
|
||||
assert_eq!(parsed, Source::String(source.to_owned()));
|
||||
|
||||
// Hardhat-like artifact
|
||||
let source = format!(
|
||||
r#"{{"_format": "hh-sol-artifact-1", "contractName": "Verifier", "sourceName": "contracts/verifier.sol", "abi": {source}, "bytecode": "0x", "deployedBytecode": "0x", "linkReferences": {{}}, "deployedLinkReferences": {{}}}}"#,
|
||||
);
|
||||
let parsed = Source::parse(&source).unwrap();
|
||||
assert_eq!(parsed, Source::String(source));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,209 @@
|
|||
use super::Source;
|
||||
use crate::util;
|
||||
use ethers_core::types::{Address, Chain};
|
||||
use ethers_etherscan::Client;
|
||||
use eyre::{Context, Result};
|
||||
use std::{fmt, str::FromStr};
|
||||
use url::Url;
|
||||
|
||||
/// An [etherscan](https://etherscan.io)-like blockchain explorer.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum Explorer {
|
||||
#[default]
|
||||
Etherscan,
|
||||
Bscscan,
|
||||
Polygonscan,
|
||||
Snowtrace,
|
||||
}
|
||||
|
||||
impl FromStr for Explorer {
|
||||
type Err = eyre::Report;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"etherscan" | "etherscan.io" => Ok(Self::Etherscan),
|
||||
"bscscan" | "bscscan.com" => Ok(Self::Bscscan),
|
||||
"polygonscan" | "polygonscan.com" => Ok(Self::Polygonscan),
|
||||
"snowtrace" | "snowtrace.io" => Ok(Self::Snowtrace),
|
||||
_ => Err(eyre::eyre!("Invalid or unsupported blockchain explorer: {s}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Explorer {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt::Debug::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Explorer {
|
||||
/// Returns the chain's Explorer, if it is known.
|
||||
pub fn from_chain(chain: Chain) -> Result<Self> {
|
||||
match chain {
|
||||
Chain::Mainnet => Ok(Self::Etherscan),
|
||||
Chain::BinanceSmartChain => Ok(Self::Bscscan),
|
||||
Chain::Polygon => Ok(Self::Polygonscan),
|
||||
Chain::Avalanche => Ok(Self::Snowtrace),
|
||||
_ => Err(eyre::eyre!("Provided chain has no known blockchain explorer")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the Explorer's chain. If it has multiple, the main one is returned.
|
||||
pub const fn chain(&self) -> Chain {
|
||||
match self {
|
||||
Self::Etherscan => Chain::Mainnet,
|
||||
Self::Bscscan => Chain::BinanceSmartChain,
|
||||
Self::Polygonscan => Chain::Polygon,
|
||||
Self::Snowtrace => Chain::Avalanche,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an `ethers-etherscan` client using this Explorer's settings.
|
||||
pub fn client(self, api_key: Option<String>) -> Result<Client> {
|
||||
let chain = self.chain();
|
||||
let client = match api_key {
|
||||
Some(api_key) => Client::new(chain, api_key),
|
||||
None => Client::new_from_env(chain),
|
||||
}?;
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
/// Retrieves a contract ABI from the Etherscan HTTP API and wraps it in an artifact JSON for
|
||||
/// compatibility with the code generation facilities.
|
||||
pub fn get(self, address: Address) -> Result<String> {
|
||||
// TODO: Improve this
|
||||
let client = self.client(None)?;
|
||||
let future = client.contract_abi(address);
|
||||
let abi = match tokio::runtime::Handle::try_current() {
|
||||
Ok(handle) => handle.block_on(future),
|
||||
_ => tokio::runtime::Runtime::new().expect("Could not start runtime").block_on(future),
|
||||
}?;
|
||||
Ok(serde_json::to_string(&abi)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Source {
|
||||
#[inline]
|
||||
pub(super) fn parse_online(source: &str) -> Result<Self> {
|
||||
if let Ok(url) = Url::parse(source) {
|
||||
match url.scheme() {
|
||||
// file://<path>
|
||||
"file" => Self::local(source),
|
||||
|
||||
// npm:<npm package>
|
||||
"npm" => Ok(Self::npm(url.path())),
|
||||
|
||||
// try first: <explorer url>/.../<address>
|
||||
// then: any http url
|
||||
"http" | "https" => Ok(url
|
||||
.host_str()
|
||||
.and_then(|host| Self::from_explorer(host, &url).ok())
|
||||
.unwrap_or(Self::Http(url))),
|
||||
|
||||
// custom scheme: <explorer or chain>:<address>
|
||||
// fallback: local fs path
|
||||
scheme => Self::from_explorer(scheme, &url)
|
||||
.or_else(|_| Self::local(source))
|
||||
.wrap_err("Invalid path or URL"),
|
||||
}
|
||||
} else {
|
||||
// not a valid URL so fallback to path
|
||||
Self::local(source)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse `s` as an explorer ("etherscan"), explorer domain ("etherscan.io") or a chain that has
|
||||
/// an explorer ("mainnet").
|
||||
///
|
||||
/// The URL can be either <explorer>:<address> or <explorer_url>/.../<address>
|
||||
fn from_explorer(s: &str, url: &Url) -> Result<Self> {
|
||||
let explorer: Explorer = s.parse().or_else(|_| Explorer::from_chain(s.parse()?))?;
|
||||
let address = last_segment_address(url).ok_or_else(|| eyre::eyre!("Invalid URL: {url}"))?;
|
||||
Ok(Self::Explorer(explorer, address))
|
||||
}
|
||||
|
||||
/// Creates an HTTP source from a URL.
|
||||
pub fn http(url: impl AsRef<str>) -> Result<Self> {
|
||||
Ok(Self::Http(Url::parse(url.as_ref())?))
|
||||
}
|
||||
|
||||
/// Creates an Etherscan source from an address string.
|
||||
pub fn explorer(chain: Chain, address: Address) -> Result<Self> {
|
||||
let explorer = Explorer::from_chain(chain)?;
|
||||
Ok(Self::Explorer(explorer, address))
|
||||
}
|
||||
|
||||
/// Creates an Etherscan source from an address string.
|
||||
pub fn npm(package_path: impl Into<String>) -> Self {
|
||||
Self::Npm(package_path.into())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) fn get_online(&self) -> Result<String> {
|
||||
match self {
|
||||
Self::Http(url) => {
|
||||
util::http_get(url.clone()).wrap_err("Failed to retrieve ABI from URL")
|
||||
}
|
||||
Self::Explorer(explorer, address) => explorer.get(*address),
|
||||
Self::Npm(package) => {
|
||||
// TODO: const?
|
||||
let unpkg = Url::parse("https://unpkg.io/").unwrap();
|
||||
let url = unpkg.join(package).wrap_err("Invalid NPM package")?;
|
||||
util::http_get(url).wrap_err("Failed to retrieve ABI from NPM package")
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn last_segment_address(url: &Url) -> Option<Address> {
|
||||
url.path().rsplit('/').next()?.parse().ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_online_source() {
|
||||
assert_eq!(
|
||||
Source::parse("https://my.domain.eth/path/to/Contract.json").unwrap(),
|
||||
Source::http("https://my.domain.eth/path/to/Contract.json").unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Source::parse("npm:@openzeppelin/contracts@2.5.0/build/contracts/IERC20.json").unwrap(),
|
||||
Source::npm("@openzeppelin/contracts@2.5.0/build/contracts/IERC20.json")
|
||||
);
|
||||
|
||||
let explorers = &[
|
||||
("mainnet:", "etherscan:", "https://etherscan.io/address/", Chain::Mainnet),
|
||||
("bsc:", "bscscan:", "https://bscscan.com/address/", Chain::BinanceSmartChain),
|
||||
("polygon:", "polygonscan:", "https://polygonscan.com/address/", Chain::Polygon),
|
||||
("avalanche:", "snowtrace:", "https://snowtrace.io/address/", Chain::Avalanche),
|
||||
];
|
||||
|
||||
let address: Address = "0x0102030405060708091011121314151617181920".parse().unwrap();
|
||||
for &(chain_s, scan_s, url_s, chain) in explorers {
|
||||
let expected = Source::explorer(chain, address).unwrap();
|
||||
|
||||
let tests2 = [chain_s, scan_s, url_s].map(|s| s.to_string() + &format!("{address:?}"));
|
||||
let tests2 = tests2.map(Source::parse).into_iter().chain(Some(Ok(expected.clone())));
|
||||
let tests2 = tests2.collect::<Result<Vec<_>>>().unwrap();
|
||||
|
||||
for slice in tests2.windows(2) {
|
||||
let (a, b) = (&slice[0], &slice[1]);
|
||||
if a != b {
|
||||
panic!("Expected: {expected:?}; Got: {a:?} | {b:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_mainnet_contract() {
|
||||
let source = Source::parse("mainnet:0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
|
||||
let abi = source.get().unwrap();
|
||||
assert!(!abi.is_empty());
|
||||
}
|
||||
}
|
|
@ -1,7 +1,4 @@
|
|||
use ethers_core::{
|
||||
abi::{Param, ParamType},
|
||||
types::Address,
|
||||
};
|
||||
use ethers_core::abi::{Param, ParamType};
|
||||
use eyre::Result;
|
||||
use inflector::Inflector;
|
||||
use proc_macro2::{Ident, Literal, Span, TokenStream};
|
||||
|
@ -96,26 +93,10 @@ pub fn expand_derives(derives: &[Path]) -> TokenStream {
|
|||
quote! {#(#derives),*}
|
||||
}
|
||||
|
||||
/// Parses the given address string
|
||||
pub fn parse_address<S>(address_str: S) -> Result<Address>
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
let address_str = address_str.as_ref();
|
||||
eyre::ensure!(address_str.starts_with("0x"), "address must start with '0x'");
|
||||
Ok(address_str[2..].parse()?)
|
||||
}
|
||||
|
||||
/// Perform an HTTP GET request and return the contents of the response.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn http_get(_url: &str) -> Result<String> {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "reqwest")]{
|
||||
Ok(reqwest::blocking::get(_url)?.text()?)
|
||||
} else {
|
||||
eyre::bail!("HTTP is unsupported")
|
||||
}
|
||||
}
|
||||
/// Perform a blocking HTTP GET request and return the contents of the response as a String.
|
||||
#[cfg(all(feature = "online", not(target_arch = "wasm32")))]
|
||||
pub fn http_get(url: impl reqwest::IntoUrl) -> Result<String> {
|
||||
Ok(reqwest::blocking::get(url)?.text()?)
|
||||
}
|
||||
|
||||
/// Replaces any occurrences of env vars in the `raw` str with their value
|
||||
|
@ -245,23 +226,6 @@ mod tests {
|
|||
assert_quote!(expand_input_name(0, "CamelCase1"), { camel_case_1 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_address_missing_prefix() {
|
||||
let _ = parse_address("0000000000000000000000000000000000000000").unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_address_address_too_short() {
|
||||
let _ = parse_address("0x00000000000000").unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_address_ok() {
|
||||
let expected =
|
||||
Address::from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]);
|
||||
assert_eq!(parse_address("0x000102030405060708090a0b0c0d0e0f10111213").unwrap(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_safe_module_name() {
|
||||
assert_eq!(safe_module_name("Valid"), "valid");
|
||||
|
|
Loading…
Reference in New Issue