Skip to content
This repository has been archived by the owner on Oct 19, 2024. It is now read-only.

Commit

Permalink
feat(etherscan): Implemented Some APIs for stats
Browse files Browse the repository at this point in the history
The following APIs have been implemented.
- https://docs.etherscan.io/api-endpoints/stats-1
    - Get Total Supply of Ether (`ethsupply`)
    - Get Total Supply of Ether 2 (`ethsupply2`)
    - Get Ether Last Price (`ethprice`)
    - Get Total Node Count (`nodecount`)
  • Loading branch information
woxjro committed Oct 27, 2023
1 parent 17c008e commit b716a3f
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 0 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ auto_impl = "1.1"

# misc
bytes = "1.4"
chrono = { version = "0.4", default-features = false }
criterion = "0.5"
dunce = "1.0"
eyre = "0.6"
Expand Down
1 change: 1 addition & 0 deletions ethers-etherscan/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ serde_json.workspace = true
thiserror.workspace = true
tracing.workspace = true
semver.workspace = true
chrono.workspace = true

[dev-dependencies]
ethers-solc.workspace = true
Expand Down
2 changes: 2 additions & 0 deletions ethers-etherscan/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ pub enum EtherscanError {
TransactionReceiptFailed,
#[error("Gas estimation failed")]
GasEstimationFailed,
#[error("Eth supply failed")]
EthSupplyFailed,
#[error("Bad status code: {0}")]
BadStatusCode(String),
#[error(transparent)]
Expand Down
1 change: 1 addition & 0 deletions ethers-etherscan/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub mod contract;
pub mod errors;
pub mod gas;
pub mod source_tree;
pub mod stats;
mod transaction;
pub mod utils;
pub mod verify;
Expand Down
161 changes: 161 additions & 0 deletions ethers-etherscan/src/stats.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
use crate::{Client, EtherscanError, Response, Result};
use chrono::{DateTime, NaiveDate, NaiveTime, TimeZone, Utc};
use ethers_core::{types::U256, utils::parse_units};
use serde::{Deserialize, Deserializer};
use std::str::FromStr;

#[derive(Deserialize, Clone, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct EthSupply2 {
/// The current amount of ETH in circulation
#[serde(deserialize_with = "deserialize_number_from_string")]
#[serde(rename = "EthSupply")]
pub eth_supply: u128,
/// The current amount of ETH2 Staking rewards
#[serde(deserialize_with = "deserialize_number_from_string")]
#[serde(rename = "Eth2Staking")]
pub eth2_staking: u128,
/// The current amount of EIP1559 burnt fees
#[serde(deserialize_with = "deser_wei_amount")]
#[serde(rename = "BurntFees")]
pub burnt_fees: U256,
/// Total withdrawn ETH from the beacon chain
#[serde(deserialize_with = "deserialize_number_from_string")]
#[serde(rename = "WithdrawnTotal")]
pub withdrawn_total: u128,
}

#[derive(Deserialize, Clone, Debug)]
pub struct EthPrice {
/// ETH-to-BTC exchange rate
#[serde(deserialize_with = "deserialize_number_from_string")]
pub ethbtc: f64,
/// Last updated timestamp for the ETH-to-BTC exchange rate
#[serde(deserialize_with = "deserialize_datetime_from_string")]
pub ethbtc_timestamp: DateTime<Utc>,
/// ETH-to-USD exchange rate
#[serde(deserialize_with = "deserialize_number_from_string")]
pub ethusd: f64,
/// Last updated timestamp for the ETH-to-USD exchange rate
#[serde(deserialize_with = "deserialize_datetime_from_string")]
pub ethusd_timestamp: DateTime<Utc>,
}

#[derive(Deserialize, Clone, Debug)]
pub struct NodeCount {
/// Last updated date for the total number of discoverable Ethereum nodes
#[serde(rename = "UTCDate")]
#[serde(deserialize_with = "deserialize_utc_date_from_string")]
pub utc_date: DateTime<Utc>,
/// The total number of discoverable Ethereum nodes
#[serde(rename = "TotalNodeCount")]
#[serde(deserialize_with = "deserialize_number_from_string")]
pub total_node_count: usize,
}

// This function is used to deserialize a string or number into a U256 with an
// amount of wei. If the contents is a number, deserialize it. If the contents
// is a string, attempt to deser as first a decimal f64 then a decimal U256.
fn deser_wei_amount<'de, D>(deserializer: D) -> Result<U256, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum StringOrInt {
Number(u64),
String(String),
}

match StringOrInt::deserialize(deserializer)? {
StringOrInt::Number(i) => Ok(U256::from(i)),
StringOrInt::String(s) => {
parse_units(s, "wei").map(Into::into).map_err(serde::de::Error::custom)
}
}
}

fn deserialize_number_from_string<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: FromStr + serde::Deserialize<'de>,
<T as FromStr>::Err: std::fmt::Display,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum StringOrInt<T> {
String(String),
Number(T),
}

match StringOrInt::<T>::deserialize(deserializer)? {
StringOrInt::String(s) => s.parse::<T>().map_err(serde::de::Error::custom),
StringOrInt::Number(i) => Ok(i),
}
}

fn deserialize_utc_date_from_string<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
where
D: Deserializer<'de>,
{
let s: String = Deserialize::deserialize(deserializer)?;

let naive_date = NaiveDate::parse_from_str(&s, "%Y-%m-%d").expect("Invalid date format");
let naive_time = NaiveTime::from_hms_opt(0, 0, 0).unwrap();

Ok(DateTime::<Utc>::from_naive_utc_and_offset(naive_date.and_time(naive_time), Utc))
}

fn deserialize_datetime_from_string<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum StringOrInt {
String(String),
Number(i64),
}

match StringOrInt::deserialize(deserializer)? {
StringOrInt::String(s) => {
let i = s.parse::<i64>().unwrap();
Ok(Utc.timestamp_opt(i, 0).unwrap())
}
StringOrInt::Number(i) => Ok(Utc.timestamp_opt(i, 0).unwrap()),
}
}

impl Client {
pub async fn eth_supply(&self) -> Result<u128> {
let query = self.create_query("stats", "ethsupply", serde_json::Value::Null);
let response: Response<String> = self.get_json(&query).await?;

if response.status == "1" {
Ok(u128::from_str(&response.result).map_err(|_| EtherscanError::EthSupplyFailed)?)
} else {
Err(EtherscanError::EthSupplyFailed)
}
}

pub async fn eth_supply2(&self) -> Result<EthSupply2> {
let query = self.create_query("stats", "ethsupply2", serde_json::Value::Null);
let response: Response<EthSupply2> = self.get_json(&query).await?;

Ok(response.result)
}

pub async fn eth_price(&self) -> Result<EthPrice> {
let query = self.create_query("stats", "ethprice", serde_json::Value::Null);
let response: Response<EthPrice> = self.get_json(&query).await?;

Ok(response.result)
}

pub async fn node_count(&self) -> Result<NodeCount> {
let query = self.create_query("stats", "nodecount", serde_json::Value::Null);
let response: Response<NodeCount> = self.get_json(&query).await?;

Ok(response.result)
}
}
1 change: 1 addition & 0 deletions ethers-etherscan/tests/it/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ mod account;
mod blocks;
mod contract;
mod gas;
mod stats;
mod transaction;
mod verify;
mod version;
Expand Down
47 changes: 47 additions & 0 deletions ethers-etherscan/tests/it/stats.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use crate::*;
use ethers_core::types::Chain;
use serial_test::serial;

#[tokio::test]
#[serial]
async fn eth_supply_success() {
run_with_client(Chain::Mainnet, |client| async move {
let result = client.eth_supply().await;

result.unwrap();
})
.await
}

#[tokio::test]
#[serial]
async fn eth_supply2_success() {
run_with_client(Chain::Mainnet, |client| async move {
let result = client.eth_supply2().await;

result.unwrap();
})
.await
}

#[tokio::test]
#[serial]
async fn eth_price_success() {
run_with_client(Chain::Mainnet, |client| async move {
let result = client.eth_price().await;

result.unwrap();
})
.await
}

#[tokio::test]
#[serial]
async fn node_count_success() {
run_with_client(Chain::Mainnet, |client| async move {
let result = client.node_count().await;

result.unwrap();
})
.await
}

0 comments on commit b716a3f

Please sign in to comment.