1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
use std::{collections::HashMap, str::FromStr};

use serde::{de, Deserialize};
use serde_aux::prelude::*;

use ethers_core::types::U256;

use crate::{Client, EtherscanError, Response, Result};

#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct GasOracle {
    #[serde(deserialize_with = "deserialize_number_from_string")]
    pub safe_gas_price: u64,
    #[serde(deserialize_with = "deserialize_number_from_string")]
    pub propose_gas_price: u64,
    #[serde(deserialize_with = "deserialize_number_from_string")]
    pub fast_gas_price: u64,
    #[serde(deserialize_with = "deserialize_number_from_string")]
    pub last_block: u64,
    #[serde(deserialize_with = "deserialize_number_from_string")]
    #[serde(rename = "suggestBaseFee")]
    pub suggested_base_fee: f64,
    #[serde(deserialize_with = "deserialize_f64_vec")]
    #[serde(rename = "gasUsedRatio")]
    pub gas_used_ratio: Vec<f64>,
}

fn deserialize_f64_vec<'de, D>(deserializer: D) -> core::result::Result<Vec<f64>, D::Error>
where
    D: de::Deserializer<'de>,
{
    let str_sequence = String::deserialize(deserializer)?;
    str_sequence
        .split(',')
        .map(|item| f64::from_str(item).map_err(|err| de::Error::custom(err.to_string())))
        .collect()
}

impl Client {
    /// Returns the estimated time, in seconds, for a transaction to be confirmed on the blockchain
    /// for the specified gas price
    pub async fn gas_estimate(&self, gas_price: U256) -> Result<u32> {
        let query = self.create_query(
            "gastracker",
            "gasestimate",
            HashMap::from([("gasprice", gas_price.to_string())]),
        );
        let response: Response<String> = self.get_json(&query).await?;

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

    /// Returns the current Safe, Proposed and Fast gas prices
    /// Post EIP-1559 changes:
    /// - Safe/Proposed/Fast gas price recommendations are now modeled as Priority Fees.
    /// - New field `suggestBaseFee`, the baseFee of the next pending block
    /// - New field `gasUsedRatio`, to estimate how busy the network is
    pub async fn gas_oracle(&self) -> Result<GasOracle> {
        let query = self.create_query("gastracker", "gasoracle", serde_json::Value::Null);
        let response: Response<GasOracle> = self.get_json(&query).await?;

        Ok(response.result)
    }
}

#[cfg(test)]
mod tests {
    use std::time::Duration;

    use serial_test::serial;

    use ethers_core::types::Chain;

    use crate::tests::run_at_least_duration;

    use super::*;

    #[tokio::test]
    #[serial]
    async fn gas_estimate_success() {
        run_at_least_duration(Duration::from_millis(250), async {
            let client = Client::new_from_env(Chain::Mainnet).unwrap();

            let result = client.gas_estimate(2000000000u32.into()).await;

            assert!(result.is_ok());
        })
        .await
    }

    #[tokio::test]
    #[serial]
    async fn gas_estimate_error() {
        run_at_least_duration(Duration::from_millis(250), async {
            let client = Client::new_from_env(Chain::Mainnet).unwrap();

            let err = client.gas_estimate(7123189371829732819379218u128.into()).await.unwrap_err();

            assert!(matches!(err, EtherscanError::GasEstimationFailed));
        })
        .await
    }

    #[tokio::test]
    #[serial]
    async fn gas_oracle_success() {
        run_at_least_duration(Duration::from_millis(250), async {
            let client = Client::new_from_env(Chain::Mainnet).unwrap();

            let result = client.gas_oracle().await;

            assert!(result.is_ok());

            let oracle = result.unwrap();

            assert!(oracle.safe_gas_price > 0);
            assert!(oracle.propose_gas_price > 0);
            assert!(oracle.fast_gas_price > 0);
            assert!(oracle.last_block > 0);
            assert!(oracle.suggested_base_fee > 0.0);
            assert!(oracle.gas_used_ratio.len() > 0);
        })
        .await
    }
}