ethers_etherscan/
stats.rs

1use crate::{Client, EtherscanError, Response, Result};
2use chrono::{DateTime, NaiveDate, NaiveTime, TimeZone, Utc};
3use ethers_core::{types::U256, utils::parse_units};
4use serde::{Deserialize, Deserializer};
5use std::str::FromStr;
6
7#[derive(Deserialize, Clone, Debug)]
8#[serde(rename_all = "PascalCase")]
9pub struct EthSupply2 {
10    /// The current amount of ETH in circulation
11    #[serde(deserialize_with = "deserialize_number_from_string")]
12    #[serde(rename = "EthSupply")]
13    pub eth_supply: u128,
14    /// The current amount of ETH2 Staking rewards
15    #[serde(deserialize_with = "deserialize_number_from_string")]
16    #[serde(rename = "Eth2Staking")]
17    pub eth2_staking: u128,
18    /// The current amount of EIP1559 burnt fees
19    #[serde(deserialize_with = "deser_wei_amount")]
20    #[serde(rename = "BurntFees")]
21    pub burnt_fees: U256,
22    /// Total withdrawn ETH from the beacon chain
23    #[serde(deserialize_with = "deserialize_number_from_string")]
24    #[serde(rename = "WithdrawnTotal")]
25    pub withdrawn_total: u128,
26}
27
28#[derive(Deserialize, Clone, Debug)]
29pub struct EthPrice {
30    /// ETH-to-BTC exchange rate
31    #[serde(deserialize_with = "deserialize_number_from_string")]
32    pub ethbtc: f64,
33    /// Last updated timestamp for the ETH-to-BTC exchange rate
34    #[serde(deserialize_with = "deserialize_datetime_from_string")]
35    pub ethbtc_timestamp: DateTime<Utc>,
36    /// ETH-to-USD exchange rate
37    #[serde(deserialize_with = "deserialize_number_from_string")]
38    pub ethusd: f64,
39    /// Last updated timestamp for the ETH-to-USD exchange rate
40    #[serde(deserialize_with = "deserialize_datetime_from_string")]
41    pub ethusd_timestamp: DateTime<Utc>,
42}
43
44#[derive(Deserialize, Clone, Debug)]
45pub struct NodeCount {
46    /// Last updated date for the total number of discoverable Ethereum nodes
47    #[serde(rename = "UTCDate")]
48    #[serde(deserialize_with = "deserialize_utc_date_from_string")]
49    pub utc_date: DateTime<Utc>,
50    /// The total number of discoverable Ethereum nodes
51    #[serde(rename = "TotalNodeCount")]
52    #[serde(deserialize_with = "deserialize_number_from_string")]
53    pub total_node_count: usize,
54}
55
56// This function is used to deserialize a string or number into a U256 with an
57// amount of wei. If the contents is a number, deserialize it. If the contents
58// is a string, attempt to deser as first a decimal f64 then a decimal U256.
59fn deser_wei_amount<'de, D>(deserializer: D) -> Result<U256, D::Error>
60where
61    D: Deserializer<'de>,
62{
63    #[derive(Deserialize)]
64    #[serde(untagged)]
65    enum StringOrInt {
66        Number(u64),
67        String(String),
68    }
69
70    match StringOrInt::deserialize(deserializer)? {
71        StringOrInt::Number(i) => Ok(U256::from(i)),
72        StringOrInt::String(s) => {
73            parse_units(s, "wei").map(Into::into).map_err(serde::de::Error::custom)
74        }
75    }
76}
77
78fn deserialize_number_from_string<'de, T, D>(deserializer: D) -> Result<T, D::Error>
79where
80    D: Deserializer<'de>,
81    T: FromStr + serde::Deserialize<'de>,
82    <T as FromStr>::Err: std::fmt::Display,
83{
84    #[derive(Deserialize)]
85    #[serde(untagged)]
86    enum StringOrInt<T> {
87        String(String),
88        Number(T),
89    }
90
91    match StringOrInt::<T>::deserialize(deserializer)? {
92        StringOrInt::String(s) => s.parse::<T>().map_err(serde::de::Error::custom),
93        StringOrInt::Number(i) => Ok(i),
94    }
95}
96
97fn deserialize_utc_date_from_string<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
98where
99    D: Deserializer<'de>,
100{
101    let s: String = Deserialize::deserialize(deserializer)?;
102
103    let naive_date = NaiveDate::parse_from_str(&s, "%Y-%m-%d").expect("Invalid date format");
104    let naive_time = NaiveTime::from_hms_opt(0, 0, 0).unwrap();
105
106    Ok(DateTime::<Utc>::from_naive_utc_and_offset(naive_date.and_time(naive_time), Utc))
107}
108
109fn deserialize_datetime_from_string<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
110where
111    D: Deserializer<'de>,
112{
113    #[derive(Deserialize)]
114    #[serde(untagged)]
115    enum StringOrInt {
116        String(String),
117        Number(i64),
118    }
119
120    match StringOrInt::deserialize(deserializer)? {
121        StringOrInt::String(s) => {
122            let i = s.parse::<i64>().unwrap();
123            Ok(Utc.timestamp_opt(i, 0).unwrap())
124        }
125        StringOrInt::Number(i) => Ok(Utc.timestamp_opt(i, 0).unwrap()),
126    }
127}
128
129impl Client {
130    /// Returns the current amount of Ether in circulation excluding ETH2 Staking rewards
131    /// and EIP1559 burnt fees.
132    pub async fn eth_supply(&self) -> Result<u128> {
133        let query = self.create_query("stats", "ethsupply", serde_json::Value::Null);
134        let response: Response<String> = self.get_json(&query).await?;
135
136        if response.status == "1" {
137            Ok(u128::from_str(&response.result).map_err(|_| EtherscanError::EthSupplyFailed)?)
138        } else {
139            Err(EtherscanError::EthSupplyFailed)
140        }
141    }
142
143    /// Returns the current amount of Ether in circulation, ETH2 Staking rewards,
144    /// EIP1559 burnt fees, and total withdrawn ETH from the beacon chain.
145    pub async fn eth_supply2(&self) -> Result<EthSupply2> {
146        let query = self.create_query("stats", "ethsupply2", serde_json::Value::Null);
147        let response: Response<EthSupply2> = self.get_json(&query).await?;
148
149        Ok(response.result)
150    }
151
152    /// Returns the latest price of 1 ETH.
153    pub async fn eth_price(&self) -> Result<EthPrice> {
154        let query = self.create_query("stats", "ethprice", serde_json::Value::Null);
155        let response: Response<EthPrice> = self.get_json(&query).await?;
156
157        Ok(response.result)
158    }
159
160    /// Returns the total number of discoverable Ethereum nodes.
161    pub async fn node_count(&self) -> Result<NodeCount> {
162        let query = self.create_query("stats", "nodecount", serde_json::Value::Null);
163        let response: Response<NodeCount> = self.get_json(&query).await?;
164
165        Ok(response.result)
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn response_works() {
175        // Sample Response from the etherscan documentation
176        // https://docs.etherscan.io/api-endpoints/stats-1#get-total-supply-of-ether-2
177        let v = r#"{
178                    "status":"1",
179                    "message":"OK",
180                    "result":{
181                        "EthSupply":"122373866217800000000000000",
182                        "Eth2Staking":"1157529105115885000000000",
183                        "BurntFees":"3102505506455601519229842",
184                        "WithdrawnTotal":"1170200333006131000000000"
185                    }
186                }"#;
187        let eth_supply2: Response<EthSupply2> = serde_json::from_str(v).unwrap();
188        assert_eq!(eth_supply2.message, "OK");
189        assert_eq!(eth_supply2.result.eth_supply, 122373866217800000000000000);
190        assert_eq!(eth_supply2.result.eth2_staking, 1157529105115885000000000);
191        assert_eq!(
192            eth_supply2.result.burnt_fees,
193            parse_units("3102505506455601519229842", "wei").map(Into::into).unwrap()
194        );
195        assert_eq!(eth_supply2.result.withdrawn_total, 1170200333006131000000000);
196
197        // Sample Response from the etherscan documentation
198        // https://docs.etherscan.io/api-endpoints/stats-1#get-ether-last-price
199        let v = r#"{
200                    "status":"1",
201                    "message":"OK",
202                    "result":{
203                        "ethbtc":"0.06116",
204                        "ethbtc_timestamp":"1624961308",
205                        "ethusd":"2149.18",
206                        "ethusd_timestamp":"1624961308"
207                    }
208                }"#;
209        let eth_price: Response<EthPrice> = serde_json::from_str(v).unwrap();
210        assert_eq!(eth_price.message, "OK");
211        assert_eq!(eth_price.result.ethbtc, 0.06116);
212        assert_eq!(eth_price.result.ethusd, 2149.18);
213
214        // Sample Response from the etherscan documentation
215        // https://docs.etherscan.io/api-endpoints/stats-1#get-total-nodes-count
216        let v = r#"{
217                    "status":"1",
218                    "message":"OK",
219                    "result":{
220                        "UTCDate":"2021-06-29",
221                        "TotalNodeCount":"6413"
222                    }
223                }"#;
224        let node_count: Response<NodeCount> = serde_json::from_str(v).unwrap();
225        assert_eq!(node_count.message, "OK");
226        assert_eq!(node_count.result.total_node_count, 6413);
227    }
228}