2021-10-04 19:05:11 +00:00
|
|
|
//! Bindings for [etherscan.io web api](https://docs.etherscan.io/)
|
|
|
|
|
2021-10-17 10:01:20 +00:00
|
|
|
mod contract;
|
|
|
|
mod errors;
|
|
|
|
mod transaction;
|
|
|
|
|
|
|
|
use errors::EtherscanError;
|
2021-10-24 18:41:50 +00:00
|
|
|
use ethers_core::{abi::Address, types::Chain};
|
2021-10-04 19:05:11 +00:00
|
|
|
use reqwest::{header, Url};
|
|
|
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
2021-10-24 18:41:50 +00:00
|
|
|
use std::borrow::Cow;
|
2021-10-17 10:01:20 +00:00
|
|
|
|
|
|
|
pub 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,
|
2021-10-17 10:01:20 +00:00
|
|
|
/// Etherscan API endpoint like https://api(-chain).etherscan.io/api
|
2021-10-04 19:05:11 +00:00
|
|
|
etherscan_api_url: Url,
|
2021-10-17 10:01:20 +00:00
|
|
|
/// Etherscan base endpoint like https://etherscan.io
|
2021-10-04 19:05:11 +00:00
|
|
|
etherscan_url: Url,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Client {
|
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> {
|
2021-10-04 19:05:11 +00:00
|
|
|
let (etherscan_api_url, etherscan_url) = match chain {
|
2021-10-17 10:01:20 +00:00
|
|
|
Chain::Mainnet => (
|
|
|
|
Url::parse("https://api.etherscan.io/api"),
|
|
|
|
Url::parse("https://etherscan.io"),
|
|
|
|
),
|
2021-10-24 18:41:50 +00:00
|
|
|
Chain::Ropsten | Chain::Kovan | Chain::Rinkeby | Chain::Goerli => {
|
|
|
|
let chain_name = chain.to_string().to_lowercase();
|
|
|
|
|
|
|
|
(
|
|
|
|
Url::parse(&format!("https://api-{}.etherscan.io/api", chain_name)),
|
|
|
|
Url::parse(&format!("https://{}.etherscan.io", chain_name)),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
chain => return Err(EtherscanError::ChainNotSupported(chain)),
|
2021-10-04 19:05:11 +00:00
|
|
|
};
|
|
|
|
|
2021-10-24 18:41:50 +00:00
|
|
|
Ok(Self {
|
2021-10-04 19:05:11 +00:00
|
|
|
client: Default::default(),
|
|
|
|
api_key: api_key.into(),
|
|
|
|
etherscan_api_url: etherscan_api_url.expect("is valid http"),
|
|
|
|
etherscan_url: etherscan_url.expect("is valid http"),
|
2021-10-24 18:41:50 +00:00
|
|
|
})
|
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-10-24 18:41:50 +00:00
|
|
|
Self::new(chain, std::env::var("ETHERSCAN_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 {
|
|
|
|
format!("{}/block/{}", self.etherscan_url, block)
|
|
|
|
}
|
|
|
|
|
2021-10-04 19:05:11 +00:00
|
|
|
/// Return the URL for the given address
|
|
|
|
pub fn address_url(&self, address: Address) -> String {
|
2021-10-17 10:01:20 +00:00
|
|
|
format!("{}/address/{}", self.etherscan_url, address)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Return the URL for the given transaction hash
|
|
|
|
pub fn transaction_url(&self, tx_hash: impl AsRef<str>) -> String {
|
|
|
|
format!("{}/tx/{}", self.etherscan_url, tx_hash.as_ref())
|
2021-10-04 19:05:11 +00:00
|
|
|
}
|
|
|
|
|
2021-10-17 10:01:20 +00:00
|
|
|
/// Return the URL for the given token hash
|
|
|
|
pub fn token_url(&self, token_hash: impl AsRef<str>) -> String {
|
|
|
|
format!("{}/token/{}", self.etherscan_url, token_hash.as_ref())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 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>> {
|
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>> {
|
2021-10-04 19:05:11 +00:00
|
|
|
Ok(self
|
|
|
|
.client
|
|
|
|
.get(self.etherscan_api_url.clone())
|
|
|
|
.header(header::ACCEPT, "application/json")
|
|
|
|
.query(query)
|
|
|
|
.send()
|
|
|
|
.await?
|
|
|
|
.json()
|
|
|
|
.await?)
|
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// The API response type
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
|
|
pub struct Response<T> {
|
|
|
|
pub status: String,
|
|
|
|
pub message: String,
|
|
|
|
pub result: T,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 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 {
|
|
|
|
use crate::{Client, EtherscanError};
|
|
|
|
use ethers_core::types::Chain;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn chain_not_supported() {
|
|
|
|
let err = Client::new_from_env(Chain::XDai).unwrap_err();
|
|
|
|
|
|
|
|
assert!(matches!(err, EtherscanError::ChainNotSupported(_)));
|
|
|
|
assert_eq!(err.to_string(), "chain XDai not supported");
|
|
|
|
}
|
|
|
|
}
|