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

219 lines
7.7 KiB
Rust

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 {
/// <https://etherscan.io>
#[default]
Etherscan,
/// <https://bscscan.com>
Bscscan,
/// <https://polygonscan.com>
Polygonscan,
/// <https://snowtrace.io>
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_opt_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() {
// Skip if ETHERSCAN_API_KEY is not set
if std::env::var("ETHERSCAN_API_KEY").is_err() {
return
}
let source = Source::parse("mainnet:0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let abi = source.get().unwrap();
assert!(!abi.is_empty());
}
}