From dd2c5891022e16f41607294e2b4e12782deffb65 Mon Sep 17 00:00:00 2001 From: Alexey Shekhirin Date: Sat, 27 Nov 2021 10:54:20 +0300 Subject: [PATCH] feat(etherscan, middleware): implement gas endpoints and use in oracle middleware (#621) * feat(etherscan, middleware): implement gas-related endpoints and use them in gas oracle * cleanup deps * better comment * revertme: add debug print * Revert "revertme: add debug print" This reverts commit 63993642c481b4a04cca5bb54ea8647ffca46f94. * make etherscan tests serial * run each etherscan test at least 200ms * 200ms -> 250ms * Revert "200ms -> 250ms" This reverts commit 29c7f6ed9aada7d9346a138437f2f4cfe5ea4290. * add separate etherscan key for celo tests * 200ms -> 250ms * add Chain::Sepolia to match --- .github/workflows/ci.yml | 5 +- Cargo.lock | 27 +++- ethers-core/src/types/chain.rs | 2 + ethers-etherscan/Cargo.toml | 4 +- ethers-etherscan/src/contract.rs | 73 ++++++---- ethers-etherscan/src/errors.rs | 2 + ethers-etherscan/src/gas.rs | 130 ++++++++++++++++++ ethers-etherscan/src/lib.rs | 33 +++-- ethers-etherscan/src/transaction.rs | 88 +++++++----- ethers-middleware/Cargo.toml | 3 +- ethers-middleware/src/gas_oracle/etherscan.rs | 80 ++--------- ethers-middleware/src/gas_oracle/mod.rs | 5 + ethers-middleware/tests/gas_oracle.rs | 11 +- 13 files changed, 313 insertions(+), 150 deletions(-) create mode 100644 ethers-etherscan/src/gas.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 40f32b02..3e0c2324 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: @@ -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: @@ -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: diff --git a/Cargo.lock b/Cargo.lock index 3963bbab..6187ada9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1073,7 +1073,9 @@ dependencies = [ "ethers-core", "reqwest", "serde", + "serde-aux", "serde_json", + "serial_test", "thiserror", "tokio", ] @@ -1085,6 +1087,7 @@ dependencies = [ "async-trait", "ethers-contract", "ethers-core", + "ethers-etherscan", "ethers-providers", "ethers-signers", "ethers-solc", @@ -1095,8 +1098,8 @@ dependencies = [ "rand 0.8.4", "reqwest", "serde", - "serde-aux", "serde_json", + "serial_test", "thiserror", "tokio", "tracing", @@ -2759,6 +2762,28 @@ dependencies = [ "serde", ] +[[package]] +name = "serial_test" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0bccbcf40c8938196944a3da0e133e031a33f4d6b72db3bda3cc556e361905d" +dependencies = [ + "lazy_static", + "parking_lot", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2acd6defeddb41eb60bb468f8825d0cfd0c2a76bc03bfd235b6a1dc4f6a1ad5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha-1" version = "0.9.8" diff --git a/ethers-core/src/types/chain.rs b/ethers-core/src/types/chain.rs index 77353c20..28ad23c3 100644 --- a/ethers-core/src/types/chain.rs +++ b/ethers-core/src/types/chain.rs @@ -14,6 +14,7 @@ pub enum Chain { PolygonMumbai, Avalanche, AvalancheFuji, + Sepolia, } impl fmt::Display for Chain { @@ -35,6 +36,7 @@ impl From for u32 { Chain::PolygonMumbai => 80001, Chain::Avalanche => 43114, Chain::AvalancheFuji => 43113, + Chain::Sepolia => 11155111, } } } diff --git a/ethers-etherscan/Cargo.toml b/ethers-etherscan/Cargo.toml index f7c327d9..dae64a8f 100644 --- a/ethers-etherscan/Cargo.toml +++ b/ethers-etherscan/Cargo.toml @@ -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 diff --git a/ethers-etherscan/src/contract.rs b/ethers-etherscan/src/contract.rs index faf50661..87ea0b03 100644 --- a/ethers-etherscan/src/contract.rs +++ b/ethers-etherscan/src/contract.rs @@ -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 { @@ -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(); + 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(); + 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(); + 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(); + 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 } } diff --git a/ethers-etherscan/src/errors.rs b/ethers-etherscan/src/errors.rs index 59db8874..8a53ca7a 100644 --- a/ethers-etherscan/src/errors.rs +++ b/ethers-etherscan/src/errors.rs @@ -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)] diff --git a/ethers-etherscan/src/gas.rs b/ethers-etherscan/src/gas.rs new file mode 100644 index 00000000..bdbbc640 --- /dev/null +++ b/ethers-etherscan/src/gas.rs @@ -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, +} + +fn deserialize_f64_vec<'de, D>(deserializer: D) -> core::result::Result, 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 { + let query = self.create_query( + "gastracker", + "gasestimate", + HashMap::from([("gasprice", gas_price.to_string())]), + ); + let response: Response = 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 { + let query = self.create_query("gastracker", "gasoracle", serde_json::Value::Null); + let response: Response = 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 + } +} diff --git a/ethers-etherscan/src/lib.rs b/ethers-etherscan/src/lib.rs index b7985573..402fa35c 100644 --- a/ethers-etherscan/src/lib.rs +++ b/ethers-etherscan/src/lib.rs @@ -1,14 +1,17 @@ //! Bindings for [etherscan.io web api](https://docs.etherscan.io/) -pub mod contract; -pub mod errors; -mod transaction; +use std::borrow::Cow; + +use reqwest::{header, Url}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; use errors::EtherscanError; use ethers_core::{abi::Address, types::Chain}; -use reqwest::{header, Url}; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use std::borrow::Cow; + +pub mod contract; +pub mod errors; +pub mod gas; +pub mod transaction; pub type Result = std::result::Result; @@ -75,7 +78,7 @@ impl Client { Chain::Mainnet | Chain::Ropsten | Chain::Kovan | Chain::Rinkeby | Chain::Goerli => { std::env::var("ETHERSCAN_API_KEY")? } - Chain::XDai => String::default(), + Chain::XDai | Chain::Sepolia => String::default(), }; Self::new(chain, api_key) } @@ -172,9 +175,15 @@ struct Query<'a, T: Serialize> { #[cfg(test)] mod tests { - use crate::{Client, EtherscanError}; + use std::{ + future::Future, + time::{Duration, SystemTime}, + }; + use ethers_core::types::Chain; + use crate::{Client, EtherscanError}; + #[test] fn chain_not_supported() { let err = Client::new_from_env(Chain::XDai).unwrap_err(); @@ -182,4 +191,12 @@ mod tests { assert!(matches!(err, EtherscanError::ChainNotSupported(_))); assert_eq!(err.to_string(), "chain XDai not supported"); } + + pub async fn run_at_least_duration(duration: Duration, block: impl Future) { + let start = SystemTime::now(); + block.await; + if let Some(sleep) = duration.checked_sub(start.elapsed().unwrap()) { + tokio::time::sleep(sleep).await; + } + } } diff --git a/ethers-etherscan/src/transaction.rs b/ethers-etherscan/src/transaction.rs index ee0a70ea..694aaebd 100644 --- a/ethers-etherscan/src/transaction.rs +++ b/ethers-etherscan/src/transaction.rs @@ -19,9 +19,6 @@ struct TransactionReceiptStatus { impl Client { /// Returns the status of a contract execution pub async fn check_contract_execution_status(&self, tx_hash: impl AsRef) -> Result<()> { - let mut map = HashMap::new(); - map.insert("txhash", tx_hash.as_ref()); - let query = self.create_query( "transaction", "getstatus", @@ -55,61 +52,82 @@ impl Client { #[cfg(test)] mod tests { + use std::time::Duration; + + use serial_test::serial; + + use crate::{tests::run_at_least_duration, Chain}; + use super::*; - use crate::Chain; #[tokio::test] + #[serial] async fn check_contract_execution_status_success() { - let client = Client::new_from_env(Chain::Mainnet).unwrap(); + run_at_least_duration(Duration::from_millis(250), async { + let client = Client::new_from_env(Chain::Mainnet).unwrap(); - let status = client - .check_contract_execution_status( - "0x16197e2a0eacc44c1ebdfddcfcfcafb3538de557c759a66e0ba95263b23d9007", - ) - .await; + let status = client + .check_contract_execution_status( + "0x16197e2a0eacc44c1ebdfddcfcfcafb3538de557c759a66e0ba95263b23d9007", + ) + .await; - assert!(status.is_ok()); + assert!(status.is_ok()); + }) + .await } #[tokio::test] + #[serial] async fn check_contract_execution_status_error() { - let client = Client::new_from_env(Chain::Mainnet).unwrap(); + run_at_least_duration(Duration::from_millis(250), async { + let client = Client::new_from_env(Chain::Mainnet).unwrap(); - let err = client - .check_contract_execution_status( - "0x15f8e5ea1079d9a0bb04a4c58ae5fe7654b5b2b4463375ff7ffb490aa0032f3a", - ) - .await - .unwrap_err(); + let err = client + .check_contract_execution_status( + "0x15f8e5ea1079d9a0bb04a4c58ae5fe7654b5b2b4463375ff7ffb490aa0032f3a", + ) + .await + .unwrap_err(); - assert!(matches!(err, EtherscanError::ExecutionFailed(_))); - assert_eq!(err.to_string(), "contract execution call failed: Bad jump destination"); + assert!(matches!(err, EtherscanError::ExecutionFailed(_))); + assert_eq!(err.to_string(), "contract execution call failed: Bad jump destination"); + }) + .await } #[tokio::test] + #[serial] async fn check_transaction_receipt_status_success() { - let client = Client::new_from_env(Chain::Mainnet).unwrap(); + run_at_least_duration(Duration::from_millis(250), async { + let client = Client::new_from_env(Chain::Mainnet).unwrap(); - let success = client - .check_transaction_receipt_status( - "0x513c1ba0bebf66436b5fed86ab668452b7805593c05073eb2d51d3a52f480a76", - ) - .await; + let success = client + .check_transaction_receipt_status( + "0x513c1ba0bebf66436b5fed86ab668452b7805593c05073eb2d51d3a52f480a76", + ) + .await; - assert!(success.is_ok()); + assert!(success.is_ok()); + }) + .await } #[tokio::test] + #[serial] async fn check_transaction_receipt_status_failed() { - let client = Client::new_from_env(Chain::Mainnet).unwrap(); + run_at_least_duration(Duration::from_millis(250), async { + let client = Client::new_from_env(Chain::Mainnet).unwrap(); - let err = client - .check_transaction_receipt_status( - "0x21a29a497cb5d4bf514c0cca8d9235844bd0215c8fab8607217546a892fd0758", - ) - .await - .unwrap_err(); + let err = client + .check_transaction_receipt_status( + "0x21a29a497cb5d4bf514c0cca8d9235844bd0215c8fab8607217546a892fd0758", + ) + .await + .unwrap_err(); - assert!(matches!(err, EtherscanError::TransactionReceiptFailed)); + assert!(matches!(err, EtherscanError::TransactionReceiptFailed)); + }) + .await } } diff --git a/ethers-middleware/Cargo.toml b/ethers-middleware/Cargo.toml index 3facf35a..64e20d3e 100644 --- a/ethers-middleware/Cargo.toml +++ b/ethers-middleware/Cargo.toml @@ -16,6 +16,7 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] ethers-contract = { version = "^0.6.0", path = "../ethers-contract", default-features = false, features = ["abigen"] } ethers-core = { version = "^0.6.0", path = "../ethers-core", default-features = false } +ethers-etherscan = { version = "^0.2.0", path = "../ethers-etherscan", default-features = false } ethers-providers = { version = "^0.6.0", path = "../ethers-providers", default-features = false } ethers-signers = { version = "^0.6.0", path = "../ethers-signers", default-features = false } @@ -27,7 +28,6 @@ tracing = { version = "0.1.29", default-features = false } tracing-futures = { version = "0.2.5", default-features = false } # for gas oracles -serde-aux = { version = "3.0.1", default-features = false } reqwest = { version = "0.11.6", default-features = false, features = ["json", "rustls-tls"] } url = { version = "2.2.2", default-features = false } @@ -43,6 +43,7 @@ rand = { version = "0.8.4", default-features = false } ethers-providers = { version = "^0.6.0", path = "../ethers-providers", default-features = false, features = ["ws", "rustls"] } once_cell = "1.8.0" ethers-solc = { version = "^0.1.0", path = "../ethers-solc", default-features = false } +serial_test = "0.5.1" [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] tokio = { version = "1.5", default-features = false, features = ["rt", "macros", "time"] } diff --git a/ethers-middleware/src/gas_oracle/etherscan.rs b/ethers-middleware/src/gas_oracle/etherscan.rs index de606a18..828f5f92 100644 --- a/ethers-middleware/src/gas_oracle/etherscan.rs +++ b/ethers-middleware/src/gas_oracle/etherscan.rs @@ -1,73 +1,22 @@ -use ethers_core::types::U256; - use async_trait::async_trait; -use reqwest::Client; -use serde::Deserialize; -use serde_aux::prelude::*; -use url::Url; + +use ethers_core::types::U256; +use ethers_etherscan::Client; use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError, GWEI_TO_WEI}; -const ETHERSCAN_URL_PREFIX: &str = - "https://api.etherscan.io/api?module=gastracker&action=gasoracle"; - /// A client over HTTP for the [Etherscan](https://api.etherscan.io/api?module=gastracker&action=gasoracle) gas tracker API /// that implements the `GasOracle` trait #[derive(Clone, Debug)] pub struct Etherscan { client: Client, - url: Url, gas_category: GasCategory, } -#[derive(Deserialize)] -struct EtherscanResponseWrapper { - result: EtherscanResponse, -} - -#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd)] -#[serde(rename_all = "PascalCase")] -pub struct EtherscanResponse { - #[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, -} - -use serde::de; -use std::str::FromStr; -fn deserialize_f64_vec<'de, D>(deserializer: D) -> Result, 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 Etherscan { /// Creates a new [Etherscan](https://etherscan.io/gastracker) gas price oracle. - pub fn new(api_key: Option<&str>) -> Self { - let url = match api_key { - Some(key) => format!("{}&apikey={}", ETHERSCAN_URL_PREFIX, key), - None => ETHERSCAN_URL_PREFIX.to_string(), - }; - - let url = Url::parse(&url).expect("invalid url"); - - Etherscan { client: Client::new(), url, gas_category: GasCategory::Standard } + pub fn new(client: Client) -> Self { + Etherscan { client, gas_category: GasCategory::Standard } } /// Sets the gas price category to be used when fetching the gas price. @@ -75,17 +24,6 @@ impl Etherscan { self.gas_category = gas_category; self } - - pub async fn query(&self) -> Result { - let res = self - .client - .get(self.url.as_ref()) - .send() - .await? - .json::() - .await?; - Ok(res.result) - } } #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] @@ -96,12 +34,12 @@ impl GasOracle for Etherscan { return Err(GasOracleError::GasCategoryNotSupported) } - let res = self.query().await?; + let result = self.client.gas_oracle().await?; match self.gas_category { - GasCategory::SafeLow => Ok(U256::from(res.safe_gas_price * GWEI_TO_WEI)), - GasCategory::Standard => Ok(U256::from(res.propose_gas_price * GWEI_TO_WEI)), - GasCategory::Fast => Ok(U256::from(res.fast_gas_price * GWEI_TO_WEI)), + GasCategory::SafeLow => Ok(U256::from(result.safe_gas_price * GWEI_TO_WEI)), + GasCategory::Standard => Ok(U256::from(result.propose_gas_price * GWEI_TO_WEI)), + GasCategory::Fast => Ok(U256::from(result.fast_gas_price * GWEI_TO_WEI)), _ => Err(GasOracleError::GasCategoryNotSupported), } } diff --git a/ethers-middleware/src/gas_oracle/mod.rs b/ethers-middleware/src/gas_oracle/mod.rs index 6bb0ddd9..517de01b 100644 --- a/ethers-middleware/src/gas_oracle/mod.rs +++ b/ethers-middleware/src/gas_oracle/mod.rs @@ -35,6 +35,11 @@ pub enum GasOracleError { #[error(transparent)] HttpClientError(#[from] ReqwestError), + /// An internal error in the Etherscan client request made from the underlying + /// gas oracle + #[error(transparent)] + EtherscanError(#[from] ethers_etherscan::errors::EtherscanError), + /// An internal error thrown when the required gas category is not /// supported by the gas oracle API #[error("gas category not supported")] diff --git a/ethers-middleware/tests/gas_oracle.rs b/ethers-middleware/tests/gas_oracle.rs index 618484dd..49560319 100644 --- a/ethers-middleware/tests/gas_oracle.rs +++ b/ethers-middleware/tests/gas_oracle.rs @@ -10,6 +10,7 @@ use ethers_middleware::gas_oracle::{ GasOracleMiddleware, }; use ethers_providers::{Http, Middleware, Provider}; +use serial_test::serial; #[derive(Debug)] struct FakeGasOracle { @@ -60,20 +61,20 @@ async fn eth_gas_station() { } #[tokio::test] +#[serial] async fn etherscan() { - let api_key = std::env::var("ETHERSCAN_API_KEY").unwrap(); - let api_key = Some(api_key.as_str()); + let etherscan_client = ethers_etherscan::Client::new_from_env(Chain::Mainnet).unwrap(); // initialize and fetch gas estimates from Etherscan // since etherscan does not support `fastest` category, we expect an error - let etherscan_oracle = Etherscan::new(api_key).category(GasCategory::Fastest); + let etherscan_oracle = Etherscan::new(etherscan_client.clone()).category(GasCategory::Fastest); let data = etherscan_oracle.fetch().await; assert!(data.is_err()); // but fetching the `standard` gas price should work fine - let etherscan_oracle_2 = Etherscan::new(api_key).category(GasCategory::SafeLow); + let etherscan_oracle = Etherscan::new(etherscan_client).category(GasCategory::SafeLow); - let data = etherscan_oracle_2.fetch().await; + let data = etherscan_oracle.fetch().await; assert!(data.is_ok()); }