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

feat(etherscan, middleware): implement gas endpoints and use in oracle middleware #621

Merged
merged 12 commits into from
Nov 27, 2021
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ name: Tests
# so that we do not get rate limited by Etherscan (and it's free to generate as
# many as you want)
env:
ETHERSCAN_API_KEY: I5BXNZYP5GEDWFINGVEZKYIVU2695NPQZB
ETHERSCAN_API_KEY_ETHEREUM: I5BXNZYP5GEDWFINGVEZKYIVU2695NPQZB
ETHERSCAN_API_KEY_CELO: B13XSMUT6Q3Q4WZ5DNQR8RXDBA2KNTMT4M
RINKEBY_PRIVATE_KEY: "a046a5b763923d437855a6fe64962569c9a378efba5c84920212c4b6ae270df5"

jobs:
Expand Down Expand Up @@ -56,6 +57,7 @@ jobs:
- name: cargo test
run: |
export PATH=$HOME/bin:$PATH
export ETHERSCAN_API_KEY=$ETHERSCAN_API_KEY_ETHEREUM
cargo test

feature-tests:
Expand Down Expand Up @@ -103,6 +105,7 @@ jobs:
- name: cargo test (Celo)
run: |
export PATH=$HOME/bin:$PATH
export ETHERSCAN_API_KEY=$ETHERSCAN_API_KEY_CELO
cargo test --all-features

lint:
Expand Down
27 changes: 26 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions ethers-core/src/types/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub enum Chain {
PolygonMumbai,
Avalanche,
AvalancheFuji,
Sepolia,
}

impl fmt::Display for Chain {
Expand All @@ -35,6 +36,7 @@ impl From<Chain> for u32 {
Chain::PolygonMumbai => 80001,
Chain::Avalanche => 43114,
Chain::AvalancheFuji => 43113,
Chain::Sepolia => 11155111,
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion ethers-etherscan/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ ethers-core = { version = "^0.6.0", path = "../ethers-core", default-features =
reqwest = { version = "0.11.6", features = ["json"] }
serde = { version = "1.0.124", default-features = false, features = ["derive"] }
serde_json = { version = "1.0.64", default-features = false }
serde-aux = { version = "3.0.1", default-features = false }
thiserror = "1.0.29"

[dev-dependencies]
tokio = { version = "1.5", features = ["macros", "rt-multi-thread"] }
tokio = { version = "1.5", features = ["macros", "rt-multi-thread", "time"] }
serial_test = "0.5.1"

[package.metadata.docs.rs]
all-features = true
Expand Down
77 changes: 48 additions & 29 deletions ethers-etherscan/src/contract.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use crate::{Client, Response, Result};
use ethers_core::abi::{Abi, Address};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

use serde::{Deserialize, Serialize};

use ethers_core::abi::{Abi, Address};

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

/// Arguments for verifying contracts
#[derive(Debug, Clone, Serialize)]
pub struct VerifyContract {
Expand Down Expand Up @@ -251,50 +254,66 @@ impl Client {

#[cfg(test)]
mod tests {
use crate::{contract::VerifyContract, Client};
use std::time::Duration;

use serial_test::serial;

use ethers_core::types::Chain;

use crate::{contract::VerifyContract, tests::run_at_least_duration, Client};

#[tokio::test]
#[serial]
#[ignore]
async fn can_fetch_contract_abi() {
let client = Client::new_from_env(Chain::Mainnet).unwrap();

let _abi = client
.contract_abi("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413".parse().unwrap())
.await
.unwrap();
run_at_least_duration(Duration::from_millis(250), async {
let client = Client::new_from_env(Chain::Mainnet).unwrap();

let _abi = client
.contract_abi("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413".parse().unwrap())
.await
.unwrap();
})
.await;
}

#[tokio::test]
#[serial]
#[ignore]
async fn can_fetch_contract_source_code() {
let client = Client::new_from_env(Chain::Mainnet).unwrap();

let _meta = client
.contract_source_code("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413".parse().unwrap())
.await
.unwrap();
run_at_least_duration(Duration::from_millis(250), async {
let client = Client::new_from_env(Chain::Mainnet).unwrap();

let _meta = client
.contract_source_code("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413".parse().unwrap())
.await
.unwrap();
})
.await
}

#[tokio::test]
#[serial]
#[ignore]
async fn can_verify_contract() {
// TODO this needs further investigation
run_at_least_duration(Duration::from_millis(250), async {
// TODO this needs further investigation

// https://etherscan.io/address/0x9e744c9115b74834c0f33f4097f40c02a9ac5c33#code
let contract = include_str!("../resources/UniswapExchange.sol");
let address = "0x9e744c9115b74834c0f33f4097f40c02a9ac5c33".parse().unwrap();
let compiler_version = "v0.5.17+commit.d19bba13";
let constructor_args = "0x000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000000000007596179537761700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000035941590000000000000000000000000000000000000000000000000000000000";
// https://etherscan.io/address/0x9e744c9115b74834c0f33f4097f40c02a9ac5c33#code
let contract = include_str!("../resources/UniswapExchange.sol");
let address = "0x9e744c9115b74834c0f33f4097f40c02a9ac5c33".parse().unwrap();
let compiler_version = "v0.5.17+commit.d19bba13";
let constructor_args = "0x000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000000000007596179537761700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000035941590000000000000000000000000000000000000000000000000000000000";

let client = Client::new_from_env(Chain::Mainnet).unwrap();
let client = Client::new_from_env(Chain::Mainnet).unwrap();

let contract =
VerifyContract::new(address, contract.to_string(), compiler_version.to_string())
.constructor_arguments(Some(constructor_args))
.optimization(true)
.runs(200);
let contract =
VerifyContract::new(address, contract.to_string(), compiler_version.to_string())
.constructor_arguments(Some(constructor_args))
.optimization(true)
.runs(200);

let _resp = client.submit_contract_verification(&contract).await;
let _resp = client.submit_contract_verification(&contract).await;
}).await
}
}
2 changes: 2 additions & 0 deletions ethers-etherscan/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ pub enum EtherscanError {
ExecutionFailed(String),
#[error("tx receipt failed")]
TransactionReceiptFailed,
#[error("gas estimation failed")]
GasEstimationFailed,
#[error("bad status code {0}")]
BadStatusCode(String),
#[error(transparent)]
Expand Down
130 changes: 130 additions & 0 deletions ethers-etherscan/src/gas.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,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
}
}
Loading