2021-10-04 19:05:11 +00:00
|
|
|
//! Bindings for [etherscan.io web api](https://docs.etherscan.io/)
|
|
|
|
|
2022-06-08 10:29:00 +00:00
|
|
|
use contract::ContractMetadata;
|
|
|
|
use errors::EtherscanError;
|
|
|
|
use ethers_core::{
|
|
|
|
abi::{Abi, Address},
|
|
|
|
types::{Chain, H256},
|
|
|
|
};
|
|
|
|
use reqwest::{header, IntoUrl, Url};
|
|
|
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
2022-04-06 02:01:44 +00:00
|
|
|
use std::{
|
|
|
|
borrow::Cow,
|
|
|
|
io::Write,
|
|
|
|
path::PathBuf,
|
|
|
|
time::{Duration, SystemTime, UNIX_EPOCH},
|
|
|
|
};
|
2022-04-23 08:42:12 +00:00
|
|
|
use tracing::trace;
|
2022-02-26 14:35:43 +00:00
|
|
|
pub mod account;
|
2021-11-27 07:54:20 +00:00
|
|
|
pub mod contract;
|
|
|
|
pub mod errors;
|
|
|
|
pub mod gas;
|
2022-03-06 15:21:19 +00:00
|
|
|
pub mod source_tree;
|
2021-11-27 07:54:20 +00:00
|
|
|
pub mod transaction;
|
2022-05-09 17:44:32 +00:00
|
|
|
pub mod utils;
|
2021-10-17 10:01:20 +00:00
|
|
|
|
2021-12-02 00:54:16 +00:00
|
|
|
pub(crate) type Result<T> = std::result::Result<T, EtherscanError>;
|
2021-10-04 19:05:11 +00:00
|
|
|
|
2021-10-17 10:01:20 +00:00
|
|
|
/// The Etherscan.io API client.
|
2021-10-24 18:41:50 +00:00
|
|
|
#[derive(Clone, Debug)]
|
2021-10-04 19:05:11 +00:00
|
|
|
pub struct Client {
|
2021-10-17 10:01:20 +00:00
|
|
|
/// Client that executes HTTP requests
|
2021-10-04 19:05:11 +00:00
|
|
|
client: reqwest::Client,
|
2021-10-17 10:01:20 +00:00
|
|
|
/// Etherscan API key
|
2021-10-04 19:05:11 +00:00
|
|
|
api_key: String,
|
2022-03-19 17:05:39 +00:00
|
|
|
/// Etherscan API endpoint like <https://api(-chain).etherscan.io/api>
|
2021-10-04 19:05:11 +00:00
|
|
|
etherscan_api_url: Url,
|
2022-03-19 17:05:39 +00:00
|
|
|
/// Etherscan base endpoint like <https://etherscan.io>
|
2021-10-04 19:05:11 +00:00
|
|
|
etherscan_url: Url,
|
2022-04-06 02:01:44 +00:00
|
|
|
/// Path to where ABI files should be cached
|
|
|
|
cache: Option<Cache>,
|
|
|
|
}
|
|
|
|
|
2022-04-29 11:25:52 +00:00
|
|
|
impl Client {
|
|
|
|
/// Creates a `ClientBuilder` to configure a `Client`.
|
|
|
|
/// This is the same as `ClientBuilder::default()`.
|
|
|
|
///
|
|
|
|
/// # Example
|
|
|
|
///
|
|
|
|
/// ```rust
|
|
|
|
/// use ethers_core::types::Chain;
|
|
|
|
/// use ethers_etherscan::Client;
|
|
|
|
/// let client = Client::builder().with_api_key("<API KEY>").chain(Chain::Mainnet).unwrap().build().unwrap();
|
|
|
|
/// ```
|
|
|
|
pub fn builder() -> ClientBuilder {
|
|
|
|
ClientBuilder::default()
|
2022-04-06 02:01:44 +00:00
|
|
|
}
|
2021-10-04 19:05:11 +00:00
|
|
|
|
2022-04-29 11:25:52 +00:00
|
|
|
/// Creates a new instance that caches etherscan requests
|
2022-04-06 02:01:44 +00:00
|
|
|
pub fn new_cached(
|
|
|
|
chain: Chain,
|
|
|
|
api_key: impl Into<String>,
|
|
|
|
cache_root: Option<PathBuf>,
|
|
|
|
cache_ttl: Duration,
|
|
|
|
) -> Result<Self> {
|
|
|
|
let mut this = Self::new(chain, api_key)?;
|
|
|
|
this.cache = cache_root.map(|root| Cache::new(root, cache_ttl));
|
|
|
|
Ok(this)
|
|
|
|
}
|
|
|
|
|
2021-10-17 10:01:20 +00:00
|
|
|
/// Create a new client with the correct endpoints based on the chain and provided API key
|
2021-10-24 18:41:50 +00:00
|
|
|
pub fn new(chain: Chain, api_key: impl Into<String>) -> Result<Self> {
|
2022-04-29 11:25:52 +00:00
|
|
|
Client::builder().with_api_key(api_key).chain(chain)?.build()
|
2021-10-17 10:01:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Create a new client with the correct endpoints based on the chain and API key
|
|
|
|
/// from ETHERSCAN_API_KEY environment variable
|
|
|
|
pub fn new_from_env(chain: Chain) -> Result<Self> {
|
2021-11-22 09:02:28 +00:00
|
|
|
let api_key = match chain {
|
|
|
|
Chain::Avalanche | Chain::AvalancheFuji => std::env::var("SNOWTRACE_API_KEY")?,
|
|
|
|
Chain::Polygon | Chain::PolygonMumbai => std::env::var("POLYGONSCAN_API_KEY")?,
|
2022-01-28 07:06:35 +00:00
|
|
|
Chain::Mainnet |
|
|
|
|
Chain::Ropsten |
|
|
|
|
Chain::Kovan |
|
|
|
|
Chain::Rinkeby |
|
|
|
|
Chain::Goerli |
|
|
|
|
Chain::Optimism |
|
|
|
|
Chain::OptimismKovan |
|
|
|
|
Chain::BinanceSmartChain |
|
2022-02-05 14:36:21 +00:00
|
|
|
Chain::BinanceSmartChainTestnet |
|
|
|
|
Chain::Arbitrum |
|
2022-02-18 14:04:11 +00:00
|
|
|
Chain::ArbitrumTestnet |
|
2022-05-17 17:57:48 +00:00
|
|
|
Chain::Cronos |
|
|
|
|
Chain::CronosTestnet => std::env::var("ETHERSCAN_API_KEY")?,
|
2022-04-23 08:42:12 +00:00
|
|
|
Chain::Fantom | Chain::FantomTestnet => {
|
|
|
|
std::env::var("FTMSCAN_API_KEY").or_else(|_| std::env::var("FANTOMSCAN_API_KEY"))?
|
|
|
|
}
|
2022-06-03 16:16:35 +00:00
|
|
|
Chain::XDai |
|
|
|
|
Chain::Sepolia |
|
|
|
|
Chain::Rsk |
|
|
|
|
Chain::Sokol |
|
|
|
|
Chain::Poa |
|
|
|
|
Chain::Oasis |
|
|
|
|
Chain::Emerald |
|
|
|
|
Chain::EmeraldTestnet => String::default(),
|
2021-12-03 18:05:38 +00:00
|
|
|
Chain::Moonbeam | Chain::MoonbeamDev | Chain::Moonriver => {
|
|
|
|
std::env::var("MOONSCAN_API_KEY")?
|
|
|
|
}
|
2022-06-08 10:29:00 +00:00
|
|
|
Chain::AnvilHardhat | Chain::Dev => {
|
|
|
|
return Err(EtherscanError::LocalNetworksNotSupported)
|
|
|
|
}
|
2021-11-22 09:02:28 +00:00
|
|
|
};
|
|
|
|
Self::new(chain, api_key)
|
2021-10-04 19:05:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn etherscan_api_url(&self) -> &Url {
|
|
|
|
&self.etherscan_api_url
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn etherscan_url(&self) -> &Url {
|
|
|
|
&self.etherscan_url
|
|
|
|
}
|
|
|
|
|
2021-10-17 10:01:20 +00:00
|
|
|
/// Return the URL for the given block number
|
|
|
|
pub fn block_url(&self, block: u64) -> String {
|
2022-03-31 23:42:31 +00:00
|
|
|
format!("{}block/{}", self.etherscan_url, block)
|
2021-10-17 10:01:20 +00:00
|
|
|
}
|
|
|
|
|
2021-10-04 19:05:11 +00:00
|
|
|
/// Return the URL for the given address
|
|
|
|
pub fn address_url(&self, address: Address) -> String {
|
2022-04-10 16:04:43 +00:00
|
|
|
format!("{}address/{:?}", self.etherscan_url, address)
|
2021-10-17 10:01:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Return the URL for the given transaction hash
|
2022-04-10 16:04:43 +00:00
|
|
|
pub fn transaction_url(&self, tx_hash: H256) -> String {
|
|
|
|
format!("{}tx/{:?}", self.etherscan_url, tx_hash)
|
2021-10-04 19:05:11 +00:00
|
|
|
}
|
|
|
|
|
2021-10-17 10:01:20 +00:00
|
|
|
/// Return the URL for the given token hash
|
2022-04-10 16:04:43 +00:00
|
|
|
pub fn token_url(&self, token_hash: Address) -> String {
|
|
|
|
format!("{}token/{:?}", self.etherscan_url, token_hash)
|
2021-10-17 10:01:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Execute an API POST request with a form
|
2021-10-04 19:05:11 +00:00
|
|
|
async fn post_form<T: DeserializeOwned, Form: Serialize>(
|
|
|
|
&self,
|
|
|
|
form: &Form,
|
2021-10-17 10:01:20 +00:00
|
|
|
) -> Result<Response<T>> {
|
2022-04-23 08:42:12 +00:00
|
|
|
trace!(target: "etherscan", "POST FORM {}", self.etherscan_api_url);
|
2021-10-04 19:05:11 +00:00
|
|
|
Ok(self
|
|
|
|
.client
|
|
|
|
.post(self.etherscan_api_url.clone())
|
|
|
|
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
|
|
|
|
.form(form)
|
|
|
|
.send()
|
|
|
|
.await?
|
|
|
|
.json()
|
|
|
|
.await?)
|
|
|
|
}
|
|
|
|
|
2021-10-17 10:01:20 +00:00
|
|
|
/// Execute an API GET request with parameters
|
|
|
|
async fn get_json<T: DeserializeOwned, Q: Serialize>(&self, query: &Q) -> Result<Response<T>> {
|
2022-04-23 08:42:12 +00:00
|
|
|
trace!(target: "etherscan", "GET JSON {}", self.etherscan_api_url);
|
2022-04-06 02:01:44 +00:00
|
|
|
let res: ResponseData<T> = self
|
2021-10-04 19:05:11 +00:00
|
|
|
.client
|
|
|
|
.get(self.etherscan_api_url.clone())
|
|
|
|
.header(header::ACCEPT, "application/json")
|
|
|
|
.query(query)
|
|
|
|
.send()
|
|
|
|
.await?
|
|
|
|
.json()
|
2022-04-06 02:01:44 +00:00
|
|
|
.await?;
|
|
|
|
|
|
|
|
match res {
|
|
|
|
ResponseData::Error { result, .. } => {
|
|
|
|
if result.starts_with("Max rate limit reached") {
|
|
|
|
Err(EtherscanError::RateLimitExceeded)
|
|
|
|
} else {
|
|
|
|
Err(EtherscanError::Unknown(result))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ResponseData::Success(res) => Ok(res),
|
|
|
|
}
|
2021-10-04 19:05:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn create_query<T: Serialize>(
|
|
|
|
&self,
|
|
|
|
module: &'static str,
|
|
|
|
action: &'static str,
|
|
|
|
other: T,
|
|
|
|
) -> Query<T> {
|
|
|
|
Query {
|
|
|
|
apikey: Cow::Borrowed(&self.api_key),
|
|
|
|
module: Cow::Borrowed(module),
|
|
|
|
action: Cow::Borrowed(action),
|
|
|
|
other,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-29 11:25:52 +00:00
|
|
|
#[derive(Clone, Debug, Default)]
|
|
|
|
pub struct ClientBuilder {
|
|
|
|
/// Client that executes HTTP requests
|
|
|
|
client: Option<reqwest::Client>,
|
|
|
|
/// Etherscan API key
|
|
|
|
api_key: Option<String>,
|
|
|
|
/// Etherscan API endpoint like <https://api(-chain).etherscan.io/api>
|
|
|
|
etherscan_api_url: Option<Url>,
|
|
|
|
/// Etherscan base endpoint like <https://etherscan.io>
|
|
|
|
etherscan_url: Option<Url>,
|
|
|
|
/// Path to where ABI files should be cached
|
|
|
|
cache: Option<Cache>,
|
|
|
|
}
|
|
|
|
|
|
|
|
// === impl ClientBuilder ===
|
|
|
|
|
|
|
|
impl ClientBuilder {
|
|
|
|
/// Configures the etherscan url and api url for the given chain
|
|
|
|
///
|
|
|
|
/// # Errors
|
|
|
|
///
|
|
|
|
/// Fails if the chain is not supported by etherscan
|
|
|
|
pub fn chain(self, chain: Chain) -> Result<Self> {
|
|
|
|
fn urls(
|
|
|
|
api: impl IntoUrl,
|
|
|
|
url: impl IntoUrl,
|
|
|
|
) -> (reqwest::Result<Url>, reqwest::Result<Url>) {
|
|
|
|
(api.into_url(), url.into_url())
|
|
|
|
}
|
|
|
|
|
|
|
|
let (etherscan_api_url, etherscan_url) = match chain {
|
|
|
|
Chain::Mainnet => urls("https://api.etherscan.io/api", "https://etherscan.io"),
|
|
|
|
Chain::Ropsten | Chain::Kovan | Chain::Rinkeby | Chain::Goerli => {
|
|
|
|
let chain_name = chain.to_string().to_lowercase();
|
|
|
|
urls(
|
|
|
|
format!("https://api-{}.etherscan.io/api", chain_name),
|
|
|
|
format!("https://{}.etherscan.io", chain_name),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
Chain::Polygon => urls("https://api.polygonscan.com/api", "https://polygonscan.com"),
|
|
|
|
Chain::PolygonMumbai => {
|
|
|
|
urls("https://api-testnet.polygonscan.com/api", "https://mumbai.polygonscan.com")
|
|
|
|
}
|
|
|
|
Chain::Avalanche => urls("https://api.snowtrace.io/api", "https://snowtrace.io"),
|
|
|
|
Chain::AvalancheFuji => {
|
|
|
|
urls("https://api-testnet.snowtrace.io/api", "https://testnet.snowtrace.io")
|
|
|
|
}
|
|
|
|
Chain::Optimism => {
|
|
|
|
urls("https://api-optimistic.etherscan.io/api", "https://optimistic.etherscan.io")
|
|
|
|
}
|
|
|
|
Chain::OptimismKovan => urls(
|
|
|
|
"https://api-kovan-optimistic.etherscan.io/api",
|
|
|
|
"https://kovan-optimistic.etherscan.io",
|
|
|
|
),
|
|
|
|
Chain::Fantom => urls("https://api.ftmscan.com/api", "https://ftmscan.com"),
|
|
|
|
Chain::FantomTestnet => {
|
|
|
|
urls("https://api-testnet.ftmscan.com/api", "https://testnet.ftmscan.com")
|
|
|
|
}
|
|
|
|
Chain::BinanceSmartChain => urls("https://api.bscscan.com/api", "https://bscscan.com"),
|
|
|
|
Chain::BinanceSmartChainTestnet => {
|
|
|
|
urls("https://api-testnet.bscscan.com/api", "https://testnet.bscscan.com")
|
|
|
|
}
|
|
|
|
Chain::Arbitrum => urls("https://api.arbiscan.io/api", "https://arbiscan.io"),
|
|
|
|
Chain::ArbitrumTestnet => {
|
|
|
|
urls("https://api-testnet.arbiscan.io/api", "https://testnet.arbiscan.io")
|
|
|
|
}
|
|
|
|
Chain::Cronos => urls("https://api.cronoscan.com/api", "https://cronoscan.com"),
|
2022-05-17 17:57:48 +00:00
|
|
|
Chain::CronosTestnet => {
|
|
|
|
urls("https://api-testnet.cronoscan.com/api", "https://testnet.cronoscan.com")
|
|
|
|
}
|
2022-05-06 18:34:09 +00:00
|
|
|
Chain::Moonbeam => {
|
|
|
|
urls("https://api-moonbeam.moonscan.io/api", "https://moonbeam.moonscan.io/")
|
|
|
|
}
|
|
|
|
Chain::Moonriver => {
|
|
|
|
urls("https://api-moonriver.moonscan.io/api", "https://moonriver.moonscan.io")
|
|
|
|
}
|
2022-06-03 16:16:35 +00:00
|
|
|
// blockscout API is etherscan compatible
|
|
|
|
Chain::XDai => urls(
|
|
|
|
"https://blockscout.com/xdai/mainnet/api",
|
|
|
|
"https://blockscout.com/xdai/mainnet",
|
|
|
|
),
|
|
|
|
Chain::Sokol => {
|
|
|
|
urls("https://blockscout.com/poa/sokol/api", "https://blockscout.com/poa/sokol")
|
|
|
|
}
|
|
|
|
Chain::Poa => {
|
|
|
|
urls("https://blockscout.com/poa/core/api", "https://blockscout.com/poa/core")
|
|
|
|
}
|
|
|
|
Chain::Rsk => {
|
|
|
|
urls("https://blockscout.com/rsk/mainnet/api", "https://blockscout.com/rsk/mainnet")
|
|
|
|
}
|
|
|
|
Chain::Oasis => urls("https://scan.oasischain.io/api", "https://scan.oasischain.io/"),
|
|
|
|
Chain::Emerald => urls(
|
|
|
|
"https://explorer.emerald.oasis.dev/api",
|
|
|
|
"https://explorer.emerald.oasis.dev/",
|
|
|
|
),
|
|
|
|
Chain::EmeraldTestnet => urls(
|
|
|
|
"https://testnet.explorer.emerald.oasis.dev/api",
|
|
|
|
"https://testnet.explorer.emerald.oasis.dev/",
|
|
|
|
),
|
2022-06-08 10:29:00 +00:00
|
|
|
Chain::AnvilHardhat | Chain::Dev => {
|
|
|
|
return Err(EtherscanError::LocalNetworksNotSupported)
|
|
|
|
}
|
2022-04-29 11:25:52 +00:00
|
|
|
chain => return Err(EtherscanError::ChainNotSupported(chain)),
|
|
|
|
};
|
|
|
|
self.with_api_url(etherscan_api_url?)?.with_url(etherscan_url?)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Configures the etherscan url
|
|
|
|
///
|
|
|
|
/// # Errors
|
|
|
|
///
|
|
|
|
/// Fails if the `etherscan_url` is not a valid `Url`
|
|
|
|
pub fn with_url(mut self, etherscan_url: impl IntoUrl) -> Result<Self> {
|
|
|
|
self.etherscan_url = Some(etherscan_url.into_url()?);
|
|
|
|
Ok(self)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Configures the `reqwest::Client`
|
|
|
|
pub fn with_client(mut self, client: reqwest::Client) -> Self {
|
|
|
|
self.client = Some(client);
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Configures the etherscan api url
|
|
|
|
///
|
|
|
|
/// # Errors
|
|
|
|
///
|
|
|
|
/// Fails if the `etherscan_api_url` is not a valid `Url`
|
|
|
|
pub fn with_api_url(mut self, etherscan_api_url: impl IntoUrl) -> Result<Self> {
|
|
|
|
self.etherscan_api_url = Some(etherscan_api_url.into_url()?);
|
|
|
|
Ok(self)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Configures the etherscan api key
|
|
|
|
pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
|
|
|
|
self.api_key = Some(api_key.into());
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Configures cache for etherscan request
|
|
|
|
pub fn with_cache(mut self, cache_root: Option<PathBuf>, cache_ttl: Duration) -> Self {
|
|
|
|
self.cache = cache_root.map(|root| Cache::new(root, cache_ttl));
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns a Client that uses this ClientBuilder configuration.
|
|
|
|
///
|
|
|
|
/// # Errors
|
|
|
|
/// if required fields are missing:
|
|
|
|
/// - `api_key`
|
|
|
|
/// - `etherscan_api_url`
|
|
|
|
/// - `etherscan_url`
|
|
|
|
pub fn build(self) -> Result<Client> {
|
|
|
|
let ClientBuilder { client, api_key, etherscan_api_url, etherscan_url, cache } = self;
|
|
|
|
|
|
|
|
let client = Client {
|
|
|
|
client: client.unwrap_or_default(),
|
|
|
|
api_key: api_key
|
|
|
|
.ok_or_else(|| EtherscanError::Builder("etherscan api key".to_string()))?,
|
|
|
|
etherscan_api_url: etherscan_api_url
|
|
|
|
.ok_or_else(|| EtherscanError::Builder("etherscan api url".to_string()))?,
|
|
|
|
etherscan_url: etherscan_url
|
|
|
|
.ok_or_else(|| EtherscanError::Builder("etherscan url".to_string()))?,
|
|
|
|
cache,
|
|
|
|
};
|
|
|
|
Ok(client)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// A wrapper around an Etherscan cache object with an expiry
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
|
|
struct CacheEnvelope<T> {
|
|
|
|
expiry: u64,
|
|
|
|
data: T,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Simple cache for etherscan requests
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
struct Cache {
|
|
|
|
root: PathBuf,
|
|
|
|
ttl: Duration,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Cache {
|
|
|
|
fn new(root: PathBuf, ttl: Duration) -> Self {
|
|
|
|
Self { root, ttl }
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_abi(&self, address: Address) -> Option<Option<ethers_core::abi::Abi>> {
|
|
|
|
self.get("abi", address)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn set_abi(&self, address: Address, abi: Option<&Abi>) {
|
|
|
|
self.set("abi", address, abi)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_source(&self, address: Address) -> Option<Option<ContractMetadata>> {
|
|
|
|
self.get("sources", address)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn set_source(&self, address: Address, source: Option<&ContractMetadata>) {
|
|
|
|
self.set("sources", address, source)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn set<T: Serialize>(&self, prefix: &str, address: Address, item: T) {
|
|
|
|
let path = self.root.join(prefix).join(format!("{:?}.json", address));
|
|
|
|
let writer = std::fs::File::create(path).ok().map(std::io::BufWriter::new);
|
|
|
|
if let Some(mut writer) = writer {
|
|
|
|
let _ = serde_json::to_writer(
|
|
|
|
&mut writer,
|
|
|
|
&CacheEnvelope {
|
|
|
|
expiry: SystemTime::now()
|
|
|
|
.checked_add(self.ttl)
|
|
|
|
.expect("cache ttl overflowed")
|
|
|
|
.duration_since(UNIX_EPOCH)
|
|
|
|
.expect("system time is before unix epoch")
|
|
|
|
.as_secs(),
|
|
|
|
data: item,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
let _ = writer.flush();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get<T: DeserializeOwned>(&self, prefix: &str, address: Address) -> Option<T> {
|
|
|
|
let path = self.root.join(prefix).join(format!("{:?}.json", address));
|
|
|
|
let reader = std::io::BufReader::new(std::fs::File::open(path).ok()?);
|
|
|
|
if let Ok(inner) = serde_json::from_reader::<_, CacheEnvelope<T>>(reader) {
|
|
|
|
// If this does not return None then we have passed the expiry
|
|
|
|
if SystemTime::now()
|
|
|
|
.duration_since(UNIX_EPOCH)
|
|
|
|
.expect("system time is before unix epoch")
|
|
|
|
.checked_sub(Duration::from_secs(inner.expiry))
|
|
|
|
.is_some()
|
|
|
|
{
|
|
|
|
return None
|
|
|
|
}
|
|
|
|
|
|
|
|
return Some(inner.data)
|
|
|
|
}
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-04 19:05:11 +00:00
|
|
|
/// The API response type
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
|
|
pub struct Response<T> {
|
|
|
|
pub status: String,
|
|
|
|
pub message: String,
|
|
|
|
pub result: T,
|
|
|
|
}
|
|
|
|
|
2022-04-06 02:01:44 +00:00
|
|
|
#[derive(Deserialize, Debug, Clone)]
|
|
|
|
#[serde(untagged)]
|
|
|
|
pub enum ResponseData<T> {
|
|
|
|
Success(Response<T>),
|
|
|
|
Error { status: String, message: String, result: String },
|
|
|
|
}
|
|
|
|
|
2021-10-04 19:05:11 +00:00
|
|
|
/// The type that gets serialized as query
|
|
|
|
#[derive(Debug, Serialize)]
|
|
|
|
struct Query<'a, T: Serialize> {
|
|
|
|
apikey: Cow<'a, str>,
|
|
|
|
module: Cow<'a, str>,
|
|
|
|
action: Cow<'a, str>,
|
|
|
|
#[serde(flatten)]
|
|
|
|
other: T,
|
|
|
|
}
|
2021-10-24 18:41:50 +00:00
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
2022-04-23 08:42:12 +00:00
|
|
|
use crate::{Client, EtherscanError};
|
|
|
|
use ethers_core::types::{Address, Chain, H256};
|
2021-11-27 07:54:20 +00:00
|
|
|
use std::{
|
|
|
|
future::Future,
|
|
|
|
time::{Duration, SystemTime},
|
|
|
|
};
|
|
|
|
|
2021-10-24 18:41:50 +00:00
|
|
|
#[test]
|
|
|
|
fn chain_not_supported() {
|
2022-06-03 16:16:35 +00:00
|
|
|
let err = Client::new_from_env(Chain::Sepolia).unwrap_err();
|
2021-10-24 18:41:50 +00:00
|
|
|
|
|
|
|
assert!(matches!(err, EtherscanError::ChainNotSupported(_)));
|
2022-06-03 16:16:35 +00:00
|
|
|
assert_eq!(err.to_string(), "Chain sepolia not supported");
|
2021-10-24 18:41:50 +00:00
|
|
|
}
|
2021-11-27 07:54:20 +00:00
|
|
|
|
2022-03-31 23:42:31 +00:00
|
|
|
#[test]
|
|
|
|
fn stringifies_block_url() {
|
|
|
|
let etherscan = Client::new_from_env(Chain::Mainnet).unwrap();
|
|
|
|
let block: u64 = 1;
|
|
|
|
let block_url: String = etherscan.block_url(block);
|
|
|
|
assert_eq!(block_url, format!("https://etherscan.io/block/{}", block));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn stringifies_address_url() {
|
|
|
|
let etherscan = Client::new_from_env(Chain::Mainnet).unwrap();
|
|
|
|
let addr: Address = Address::zero();
|
|
|
|
let address_url: String = etherscan.address_url(addr);
|
2022-04-10 16:04:43 +00:00
|
|
|
assert_eq!(address_url, format!("https://etherscan.io/address/{:?}", addr));
|
2022-03-31 23:42:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn stringifies_transaction_url() {
|
|
|
|
let etherscan = Client::new_from_env(Chain::Mainnet).unwrap();
|
2022-04-10 16:04:43 +00:00
|
|
|
let tx_hash = H256::zero();
|
2022-03-31 23:42:31 +00:00
|
|
|
let tx_url: String = etherscan.transaction_url(tx_hash);
|
2022-04-10 16:04:43 +00:00
|
|
|
assert_eq!(tx_url, format!("https://etherscan.io/tx/{:?}", tx_hash));
|
2022-03-31 23:42:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn stringifies_token_url() {
|
|
|
|
let etherscan = Client::new_from_env(Chain::Mainnet).unwrap();
|
2022-04-10 16:04:43 +00:00
|
|
|
let token_hash = Address::zero();
|
2022-03-31 23:42:31 +00:00
|
|
|
let token_url: String = etherscan.token_url(token_hash);
|
2022-04-10 16:04:43 +00:00
|
|
|
assert_eq!(token_url, format!("https://etherscan.io/token/{:?}", token_hash));
|
2022-03-31 23:42:31 +00:00
|
|
|
}
|
|
|
|
|
2022-03-30 18:20:23 +00:00
|
|
|
#[test]
|
|
|
|
fn local_networks_not_supported() {
|
|
|
|
let err = Client::new_from_env(Chain::Dev).unwrap_err();
|
|
|
|
assert!(matches!(err, EtherscanError::LocalNetworksNotSupported));
|
|
|
|
}
|
|
|
|
|
2021-11-27 07:54:20 +00:00
|
|
|
pub async fn run_at_least_duration(duration: Duration, block: impl Future) {
|
|
|
|
let start = SystemTime::now();
|
|
|
|
block.await;
|
|
|
|
if let Some(sleep) = duration.checked_sub(start.elapsed().unwrap()) {
|
|
|
|
tokio::time::sleep(sleep).await;
|
|
|
|
}
|
|
|
|
}
|
2021-10-24 18:41:50 +00:00
|
|
|
}
|