fix(middleware): oracles, tests (#1944)
* fix(middleware): oracles, tests * fix: use util * fix: make blocknative api key optional * make response fields public * import orders * docs: oracle trait * fix: make response fns public * chore: clippy * fix: doc tests
This commit is contained in:
parent
022f082cee
commit
bb4af1c134
|
@ -23,13 +23,13 @@ use ethers_providers::{Middleware, Provider, Http};
|
|||
use std::sync::Arc;
|
||||
use std::convert::TryFrom;
|
||||
use ethers_signers::{LocalWallet, Signer};
|
||||
use ethers_middleware::{*,gas_oracle::*};
|
||||
use ethers_middleware::{gas_oracle::{GasOracle, GasNow}, MiddlewareBuilder};
|
||||
|
||||
fn builder_example() {
|
||||
let key = "fdb33e2105f08abe41a8ee3b758726a31abdd57b7a443f470f23efce853af169";
|
||||
let signer = key.parse::<LocalWallet>().unwrap();
|
||||
let address = signer.address();
|
||||
let gas_oracle = EthGasStation::new(None);
|
||||
let gas_oracle = GasNow::new();
|
||||
|
||||
let provider = Provider::<Http>::try_from("http://localhost:8545")
|
||||
.unwrap()
|
||||
|
@ -58,7 +58,7 @@ fn builder_example_wrap_into() {
|
|||
.unwrap()
|
||||
.wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock))
|
||||
.wrap_into(|p| SignerMiddleware::new(p, signer))
|
||||
.wrap_into(|p| GasOracleMiddleware::new(p, EthGasStation::new(None)))
|
||||
.wrap_into(|p| GasOracleMiddleware::new(p, GasNow::new()))
|
||||
.wrap_into(|p| NonceManagerMiddleware::new(p, address)); // Outermost layer
|
||||
}
|
||||
```
|
||||
|
@ -72,7 +72,7 @@ use ethers_providers::{Provider, Http};
|
|||
use ethers_signers::{LocalWallet, Signer};
|
||||
use ethers_middleware::{
|
||||
gas_escalator::{GasEscalatorMiddleware, GeometricGasPrice, Frequency},
|
||||
gas_oracle::{GasOracleMiddleware, EthGasStation, GasCategory},
|
||||
gas_oracle::{GasOracleMiddleware, GasCategory, GasNow},
|
||||
signer::SignerMiddleware,
|
||||
nonce_manager::NonceManagerMiddleware,
|
||||
};
|
||||
|
@ -91,8 +91,8 @@ let signer = LocalWallet::new(&mut rand::thread_rng());
|
|||
let address = signer.address();
|
||||
let provider = SignerMiddleware::new(provider, signer);
|
||||
|
||||
// Use EthGasStation as the gas oracle
|
||||
let gas_oracle = EthGasStation::new(None);
|
||||
// Use GasNow as the gas oracle
|
||||
let gas_oracle = GasNow::new();
|
||||
let provider = GasOracleMiddleware::new(provider, gas_oracle);
|
||||
|
||||
// Manage nonces locally
|
||||
|
|
|
@ -16,14 +16,14 @@ use ethers_signers::Signer;
|
|||
/// use std::sync::Arc;
|
||||
/// use std::convert::TryFrom;
|
||||
/// use ethers_signers::{LocalWallet, Signer};
|
||||
/// use ethers_middleware::{*,gas_escalator::*,gas_oracle::*};
|
||||
/// use ethers_middleware::{*, gas_escalator::*, gas_oracle::*};
|
||||
///
|
||||
/// fn builder_example() {
|
||||
/// let key = "fdb33e2105f08abe41a8ee3b758726a31abdd57b7a443f470f23efce853af169";
|
||||
/// let signer = key.parse::<LocalWallet>().unwrap();
|
||||
/// let address = signer.address();
|
||||
/// let escalator = GeometricGasPrice::new(1.125, 60_u64, None::<u64>);
|
||||
/// let gas_oracle = EthGasStation::new(None);
|
||||
/// let gas_oracle = GasNow::new();
|
||||
///
|
||||
/// let provider = Provider::<Http>::try_from("http://localhost:8545")
|
||||
/// .unwrap()
|
||||
|
@ -43,7 +43,7 @@ use ethers_signers::Signer;
|
|||
/// .unwrap()
|
||||
/// .wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock))
|
||||
/// .wrap_into(|p| SignerMiddleware::new(p, signer))
|
||||
/// .wrap_into(|p| GasOracleMiddleware::new(p, EthGasStation::new(None)))
|
||||
/// .wrap_into(|p| GasOracleMiddleware::new(p, GasNow::new()))
|
||||
/// .wrap_into(|p| NonceManagerMiddleware::new(p, address)); // Outermost layer
|
||||
/// }
|
||||
/// ```
|
||||
|
|
|
@ -46,7 +46,7 @@ pub enum Frequency {
|
|||
/// use ethers_providers::{Provider, Http};
|
||||
/// use ethers_middleware::{
|
||||
/// gas_escalator::{GeometricGasPrice, Frequency, GasEscalatorMiddleware},
|
||||
/// gas_oracle::{EthGasStation, GasCategory, GasOracleMiddleware},
|
||||
/// gas_oracle::{GasNow, GasCategory, GasOracleMiddleware},
|
||||
/// };
|
||||
/// use std::{convert::TryFrom, time::Duration, sync::Arc};
|
||||
///
|
||||
|
@ -60,7 +60,7 @@ pub enum Frequency {
|
|||
/// };
|
||||
///
|
||||
/// // ... proceed to wrap it in other middleware
|
||||
/// let gas_oracle = EthGasStation::new(None).category(GasCategory::SafeLow);
|
||||
/// let gas_oracle = GasNow::new().category(GasCategory::SafeLow);
|
||||
/// let provider = GasOracleMiddleware::new(provider, gas_oracle);
|
||||
/// ```
|
||||
pub struct GasEscalatorMiddleware<M, E> {
|
||||
|
|
|
@ -1,13 +1,128 @@
|
|||
use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError, GWEI_TO_WEI};
|
||||
use super::{from_gwei_f64, GasCategory, GasOracle, GasOracleError, Result, GWEI_TO_WEI_U256};
|
||||
use async_trait::async_trait;
|
||||
use ethers_core::types::U256;
|
||||
use reqwest::{header::AUTHORIZATION, Client};
|
||||
use serde::Deserialize;
|
||||
use std::{collections::HashMap, convert::TryInto};
|
||||
use std::collections::HashMap;
|
||||
use url::Url;
|
||||
|
||||
const BLOCKNATIVE_GAS_PRICE_ENDPOINT: &str = "https://api.blocknative.com/gasprices/blockprices";
|
||||
const URL: &str = "https://api.blocknative.com/gasprices/blockprices";
|
||||
|
||||
/// A client over HTTP for the [BlockNative](https://www.blocknative.com/gas-estimator) gas tracker API
|
||||
/// that implements the `GasOracle` trait.
|
||||
#[derive(Clone, Debug)]
|
||||
#[must_use]
|
||||
pub struct BlockNative {
|
||||
client: Client,
|
||||
url: Url,
|
||||
api_key: Option<String>,
|
||||
gas_category: GasCategory,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Response {
|
||||
pub system: String,
|
||||
pub network: String,
|
||||
pub unit: String,
|
||||
pub max_price: u64,
|
||||
pub block_prices: Vec<BlockPrice>,
|
||||
pub estimated_base_fees: Option<Vec<HashMap<String, Vec<BaseFeeEstimate>>>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BlockPrice {
|
||||
pub block_number: u64,
|
||||
pub estimated_transaction_count: u64,
|
||||
pub base_fee_per_gas: f64,
|
||||
pub estimated_prices: Vec<GasEstimate>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GasEstimate {
|
||||
pub confidence: u64,
|
||||
pub price: u64,
|
||||
pub max_priority_fee_per_gas: f64,
|
||||
pub max_fee_per_gas: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BaseFeeEstimate {
|
||||
pub confidence: u64,
|
||||
pub base_fee: f64,
|
||||
}
|
||||
|
||||
impl Response {
|
||||
#[inline]
|
||||
pub fn estimate_from_category(&self, gas_category: &GasCategory) -> Result<GasEstimate> {
|
||||
let confidence = gas_category_to_confidence(gas_category);
|
||||
let price = self
|
||||
.block_prices
|
||||
.first()
|
||||
.ok_or(GasOracleError::InvalidResponse)?
|
||||
.estimated_prices
|
||||
.iter()
|
||||
.find(|p| p.confidence == confidence)
|
||||
.ok_or(GasOracleError::GasCategoryNotSupported)?;
|
||||
Ok(*price)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BlockNative {
|
||||
fn default() -> Self {
|
||||
Self::new(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl GasOracle for BlockNative {
|
||||
async fn fetch(&self) -> Result<U256> {
|
||||
let estimate = self.query().await?.estimate_from_category(&self.gas_category)?;
|
||||
Ok(U256::from(estimate.price) * GWEI_TO_WEI_U256)
|
||||
}
|
||||
|
||||
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
|
||||
let estimate = self.query().await?.estimate_from_category(&self.gas_category)?;
|
||||
let max = from_gwei_f64(estimate.max_fee_per_gas);
|
||||
let prio = from_gwei_f64(estimate.max_priority_fee_per_gas);
|
||||
Ok((max, prio))
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockNative {
|
||||
/// Creates a new [BlockNative](https://www.blocknative.com/gas-estimator) gas oracle.
|
||||
pub fn new(api_key: Option<String>) -> Self {
|
||||
Self::with_client(Client::new(), api_key)
|
||||
}
|
||||
|
||||
/// Same as [`Self::new`] but with a custom [`Client`].
|
||||
pub fn with_client(client: Client, api_key: Option<String>) -> Self {
|
||||
let url = Url::parse(URL).unwrap();
|
||||
Self { client, api_key, url, gas_category: GasCategory::Standard }
|
||||
}
|
||||
|
||||
/// Sets the gas price category to be used when fetching the gas price.
|
||||
pub fn category(mut self, gas_category: GasCategory) -> Self {
|
||||
self.gas_category = gas_category;
|
||||
self
|
||||
}
|
||||
|
||||
/// Perform a request to the gas price API and deserialize the response.
|
||||
pub async fn query(&self) -> Result<Response, GasOracleError> {
|
||||
let mut request = self.client.get(self.url.clone());
|
||||
if let Some(api_key) = self.api_key.as_ref() {
|
||||
request = request.header(AUTHORIZATION, api_key);
|
||||
}
|
||||
let response = request.send().await?.error_for_status()?.json().await?;
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn gas_category_to_confidence(gas_category: &GasCategory) -> u64 {
|
||||
match gas_category {
|
||||
GasCategory::SafeLow => 80,
|
||||
|
@ -16,124 +131,3 @@ fn gas_category_to_confidence(gas_category: &GasCategory) -> u64 {
|
|||
GasCategory::Fastest => 99,
|
||||
}
|
||||
}
|
||||
|
||||
/// A client over HTTP for the [BlockNative](https://www.blocknative.com/gas-estimator) gas tracker API
|
||||
/// that implements the `GasOracle` trait
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BlockNative {
|
||||
client: Client,
|
||||
url: Url,
|
||||
api_key: String,
|
||||
gas_category: GasCategory,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BlockNativeGasResponse {
|
||||
system: Option<String>,
|
||||
network: Option<String>,
|
||||
unit: Option<String>,
|
||||
max_price: Option<u64>,
|
||||
block_prices: Vec<BlockPrice>,
|
||||
estimated_base_fees: Vec<HashMap<String, Vec<BaseFeeEstimate>>>,
|
||||
}
|
||||
|
||||
impl BlockNativeGasResponse {
|
||||
pub fn get_estimation_for(
|
||||
&self,
|
||||
gas_category: &GasCategory,
|
||||
) -> Result<EstimatedPrice, GasOracleError> {
|
||||
let confidence = gas_category_to_confidence(gas_category);
|
||||
Ok(self
|
||||
.block_prices
|
||||
.first()
|
||||
.ok_or(GasOracleError::InvalidResponse)?
|
||||
.estimated_prices
|
||||
.iter()
|
||||
.find(|p| p.confidence == confidence)
|
||||
.ok_or(GasOracleError::GasCategoryNotSupported)?
|
||||
.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BlockPrice {
|
||||
block_number: u64,
|
||||
estimated_transaction_count: u64,
|
||||
base_fee_per_gas: f64,
|
||||
estimated_prices: Vec<EstimatedPrice>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EstimatedPrice {
|
||||
confidence: u64,
|
||||
price: u64,
|
||||
max_priority_fee_per_gas: f64,
|
||||
max_fee_per_gas: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BaseFeeEstimate {
|
||||
confidence: u64,
|
||||
base_fee: f64,
|
||||
}
|
||||
|
||||
impl BlockNative {
|
||||
/// Creates a new [BlockNative](https://www.blocknative.com/gas-estimator) gas oracle.
|
||||
pub fn new(api_key: String) -> Self {
|
||||
Self::with_client(Client::new(), api_key)
|
||||
}
|
||||
|
||||
/// Same as [`Self::new`] but with a custom [`Client`].
|
||||
pub fn with_client(client: Client, api_key: String) -> Self {
|
||||
Self {
|
||||
client,
|
||||
api_key,
|
||||
url: BLOCKNATIVE_GAS_PRICE_ENDPOINT.try_into().unwrap(),
|
||||
gas_category: GasCategory::Standard,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the gas price category to be used when fetching the gas price.
|
||||
#[must_use]
|
||||
pub fn category(mut self, gas_category: GasCategory) -> Self {
|
||||
self.gas_category = gas_category;
|
||||
self
|
||||
}
|
||||
|
||||
/// Perform request to Blocknative, decode response
|
||||
pub async fn request(&self) -> Result<BlockNativeGasResponse, GasOracleError> {
|
||||
self.client
|
||||
.get(self.url.as_ref())
|
||||
.header(AUTHORIZATION, &self.api_key)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await
|
||||
.map_err(GasOracleError::HttpClientError)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl GasOracle for BlockNative {
|
||||
async fn fetch(&self) -> Result<U256, GasOracleError> {
|
||||
let prices = self.request().await?.get_estimation_for(&self.gas_category)?;
|
||||
Ok(U256::from(prices.price * 100_u64) * U256::from(GWEI_TO_WEI) / U256::from(100))
|
||||
}
|
||||
|
||||
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
|
||||
let prices = self.request().await?.get_estimation_for(&self.gas_category)?;
|
||||
let base_fee = U256::from((prices.max_fee_per_gas * 100.0) as u64) *
|
||||
U256::from(GWEI_TO_WEI) /
|
||||
U256::from(100);
|
||||
let prio_fee = U256::from((prices.max_priority_fee_per_gas * 100.0) as u64) *
|
||||
U256::from(GWEI_TO_WEI) /
|
||||
U256::from(100);
|
||||
Ok((base_fee, prio_fee))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::gas_oracle::{GasOracle, GasOracleError};
|
||||
use super::{GasOracle, Result};
|
||||
use async_trait::async_trait;
|
||||
use ethers_core::types::U256;
|
||||
use futures_locks::RwLock;
|
||||
|
@ -19,6 +19,24 @@ pub struct Cache<T: GasOracle> {
|
|||
#[derive(Default, Debug)]
|
||||
struct Cached<T: Clone>(RwLock<Option<(Instant, T)>>);
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl<T: GasOracle> GasOracle for Cache<T> {
|
||||
async fn fetch(&self) -> Result<U256> {
|
||||
self.fee.get(self.validity, || self.inner.fetch()).await
|
||||
}
|
||||
|
||||
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
|
||||
self.eip1559.get(self.validity, || self.inner.estimate_eip1559_fees()).await
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: GasOracle> Cache<T> {
|
||||
pub fn new(validity: Duration, inner: T) -> Self {
|
||||
Self { inner, validity, fee: Cached::default(), eip1559: Cached::default() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> Cached<T> {
|
||||
async fn get<F, E, Fut>(&self, validity: Duration, fetch: F) -> Result<T, E>
|
||||
where
|
||||
|
@ -50,21 +68,3 @@ impl<T: Clone> Cached<T> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: GasOracle> Cache<T> {
|
||||
pub fn new(validity: Duration, inner: T) -> Self {
|
||||
Self { inner, validity, fee: Cached::default(), eip1559: Cached::default() }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl<T: GasOracle> GasOracle for Cache<T> {
|
||||
async fn fetch(&self) -> Result<U256, GasOracleError> {
|
||||
self.fee.get(self.validity, || self.inner.fetch()).await
|
||||
}
|
||||
|
||||
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
|
||||
self.eip1559.get(self.validity, || self.inner.estimate_eip1559_fees()).await
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,88 +1,78 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use ethers_core::types::U256;
|
||||
#![allow(deprecated)]
|
||||
|
||||
use super::{GasCategory, GasOracle, GasOracleError, Result, GWEI_TO_WEI_U256};
|
||||
use async_trait::async_trait;
|
||||
use ethers_core::types::U256;
|
||||
use reqwest::Client;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use url::Url;
|
||||
|
||||
use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError, GWEI_TO_WEI};
|
||||
const URL: &str = "https://ethgasstation.info/api/ethgasAPI.json";
|
||||
|
||||
const ETH_GAS_STATION_URL_PREFIX: &str = "https://ethgasstation.info/api/ethgasAPI.json";
|
||||
|
||||
/// A client over HTTP for the [EthGasStation](https://ethgasstation.info/api/ethgasAPI.json) gas tracker API
|
||||
/// that implements the `GasOracle` trait
|
||||
/// A client over HTTP for the [EthGasStation](https://ethgasstation.info) gas tracker API
|
||||
/// that implements the `GasOracle` trait.
|
||||
#[derive(Clone, Debug)]
|
||||
#[deprecated = "ETHGasStation is shutting down: https://twitter.com/ETHGasStation/status/1597341610777317376"]
|
||||
#[must_use]
|
||||
pub struct EthGasStation {
|
||||
client: Client,
|
||||
url: Url,
|
||||
gas_category: GasCategory,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
/// Eth Gas Station's response for the current recommended fast, standard and
|
||||
/// safe low gas prices on the Ethereum network, along with the current block
|
||||
/// and wait times for each "speed".
|
||||
pub struct EthGasStationResponse {
|
||||
/// Recommended safe(expected to be mined in < 30 minutes) gas price in
|
||||
/// x10 Gwei (divide by 10 to convert it to gwei)
|
||||
pub safe_low: f64,
|
||||
/// Recommended average(expected to be mined in < 5 minutes) gas price in
|
||||
/// x10 Gwei (divide by 10 to convert it to gwei)
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Response {
|
||||
/// Recommended safe (expected to be mined in < 30 minutes).
|
||||
///
|
||||
/// In gwei * 10 (divide by 10 to convert it to gwei).
|
||||
pub safe_low: u64,
|
||||
/// Recommended average (expected to be mined in < 5 minutes).
|
||||
///
|
||||
/// In gwei * 10 (divide by 10 to convert it to gwei).
|
||||
pub average: u64,
|
||||
/// Recommended fast(expected to be mined in < 2 minutes) gas price in
|
||||
/// x10 Gwei (divide by 10 to convert it to gwei)
|
||||
/// Recommended fast (expected to be mined in < 2 minutes).
|
||||
///
|
||||
/// In gwei * 10 (divide by 10 to convert it to gwei).
|
||||
pub fast: u64,
|
||||
/// Recommended fastest(expected to be mined in < 30 seconds) gas price
|
||||
/// in x10 Gwei(divide by 10 to convert it to gwei)
|
||||
/// Recommended fastest (expected to be mined in < 30 seconds).
|
||||
///
|
||||
/// In gwei * 10 (divide by 10 to convert it to gwei).
|
||||
pub fastest: u64,
|
||||
|
||||
// post eip-1559 fields
|
||||
/// Average time (in seconds) to mine a single block.
|
||||
#[serde(rename = "block_time")] // inconsistent json response naming...
|
||||
/// Average time(in seconds) to mine one single block
|
||||
pub block_time: f64,
|
||||
/// The latest block number
|
||||
/// The latest block number.
|
||||
pub block_num: u64,
|
||||
/// Smallest value of (gasUsed / gaslimit) from last 10 blocks
|
||||
/// Smallest value of `gasUsed / gaslimit` from the last 10 blocks.
|
||||
pub speed: f64,
|
||||
/// Waiting time(in minutes) for the `safe_low` gas price
|
||||
/// Waiting time (in minutes) for the `safe_low` gas price.
|
||||
pub safe_low_wait: f64,
|
||||
/// Waiting time(in minutes) for the `average` gas price
|
||||
/// Waiting time (in minutes) for the `average` gas price.
|
||||
pub avg_wait: f64,
|
||||
/// Waiting time(in minutes) for the `fast` gas price
|
||||
/// Waiting time (in minutes) for the `fast` gas price.
|
||||
pub fast_wait: f64,
|
||||
/// Waiting time(in minutes) for the `fastest` gas price
|
||||
/// Waiting time (in minutes) for the `fastest` gas price.
|
||||
pub fastest_wait: f64,
|
||||
// What is this?
|
||||
pub gas_price_range: HashMap<u64, f64>,
|
||||
}
|
||||
|
||||
impl EthGasStation {
|
||||
/// Creates a new [EthGasStation](https://docs.ethgasstation.info/) gas oracle
|
||||
pub fn new(api_key: Option<&str>) -> Self {
|
||||
Self::with_client(Client::new(), api_key)
|
||||
}
|
||||
|
||||
/// Same as [`Self::new`] but with a custom [`Client`].
|
||||
pub fn with_client(client: Client, api_key: Option<&str>) -> Self {
|
||||
let mut url = Url::parse(ETH_GAS_STATION_URL_PREFIX).expect("invalid url");
|
||||
if let Some(key) = api_key {
|
||||
url.query_pairs_mut().append_pair("api-key", key);
|
||||
impl Response {
|
||||
#[inline]
|
||||
pub fn gas_from_category(&self, gas_category: GasCategory) -> u64 {
|
||||
match gas_category {
|
||||
GasCategory::SafeLow => self.safe_low,
|
||||
GasCategory::Standard => self.average,
|
||||
GasCategory::Fast => self.fast,
|
||||
GasCategory::Fastest => self.fastest,
|
||||
}
|
||||
EthGasStation { client, url, gas_category: GasCategory::Standard }
|
||||
}
|
||||
|
||||
/// Sets the gas price category to be used when fetching the gas price.
|
||||
#[must_use]
|
||||
pub fn category(mut self, gas_category: GasCategory) -> Self {
|
||||
self.gas_category = gas_category;
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn query(&self) -> Result<EthGasStationResponse, GasOracleError> {
|
||||
Ok(self.client.get(self.url.as_ref()).send().await?.json::<EthGasStationResponse>().await?)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,19 +85,43 @@ impl Default for EthGasStation {
|
|||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl GasOracle for EthGasStation {
|
||||
async fn fetch(&self) -> Result<U256, GasOracleError> {
|
||||
async fn fetch(&self) -> Result<U256> {
|
||||
let res = self.query().await?;
|
||||
let gas_price = match self.gas_category {
|
||||
GasCategory::SafeLow => U256::from((res.safe_low.ceil() as u64 * GWEI_TO_WEI) / 10),
|
||||
GasCategory::Standard => U256::from((res.average * GWEI_TO_WEI) / 10),
|
||||
GasCategory::Fast => U256::from((res.fast * GWEI_TO_WEI) / 10),
|
||||
GasCategory::Fastest => U256::from((res.fastest * GWEI_TO_WEI) / 10),
|
||||
};
|
||||
|
||||
Ok(gas_price)
|
||||
let gas_price = res.gas_from_category(self.gas_category);
|
||||
// gas_price is in `gwei * 10`
|
||||
Ok(U256::from(gas_price) * GWEI_TO_WEI_U256 / U256::from(10_u64))
|
||||
}
|
||||
|
||||
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
|
||||
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
|
||||
Err(GasOracleError::Eip1559EstimationNotSupported)
|
||||
}
|
||||
}
|
||||
|
||||
impl EthGasStation {
|
||||
/// Creates a new [EthGasStation](https://docs.ethgasstation.info/) gas oracle.
|
||||
pub fn new(api_key: Option<&str>) -> Self {
|
||||
Self::with_client(Client::new(), api_key)
|
||||
}
|
||||
|
||||
/// Same as [`Self::new`] but with a custom [`Client`].
|
||||
pub fn with_client(client: Client, api_key: Option<&str>) -> Self {
|
||||
let mut url = Url::parse(URL).unwrap();
|
||||
if let Some(key) = api_key {
|
||||
url.query_pairs_mut().append_pair("api-key", key);
|
||||
}
|
||||
EthGasStation { client, url, gas_category: GasCategory::Standard }
|
||||
}
|
||||
|
||||
/// Sets the gas price category to be used when fetching the gas price.
|
||||
pub fn category(mut self, gas_category: GasCategory) -> Self {
|
||||
self.gas_category = gas_category;
|
||||
self
|
||||
}
|
||||
|
||||
/// Perform a request to the gas price API and deserialize the response.
|
||||
pub async fn query(&self) -> Result<Response> {
|
||||
let response =
|
||||
self.client.get(self.url.clone()).send().await?.error_for_status()?.json().await?;
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,56 +1,42 @@
|
|||
use ethers_core::types::U256;
|
||||
|
||||
use super::{from_gwei_f64, GasCategory, GasOracle, GasOracleError, Result};
|
||||
use async_trait::async_trait;
|
||||
use ethers_core::types::U256;
|
||||
use reqwest::Client;
|
||||
use serde::Deserialize;
|
||||
use url::Url;
|
||||
|
||||
use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError, GWEI_TO_WEI};
|
||||
|
||||
const ETHERCHAIN_URL: &str = "https://www.etherchain.org/api/gasPriceOracle";
|
||||
const URL: &str = "https://www.etherchain.org/api/gasPriceOracle";
|
||||
|
||||
/// A client over HTTP for the [Etherchain](https://www.etherchain.org/api/gasPriceOracle) gas tracker API
|
||||
/// that implements the `GasOracle` trait
|
||||
/// that implements the `GasOracle` trait.
|
||||
#[derive(Clone, Debug)]
|
||||
#[must_use]
|
||||
pub struct Etherchain {
|
||||
client: Client,
|
||||
url: Url,
|
||||
gas_category: GasCategory,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd)]
|
||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EtherchainResponse {
|
||||
pub safe_low: f32,
|
||||
pub standard: f32,
|
||||
pub fast: f32,
|
||||
pub fastest: f32,
|
||||
pub current_base_fee: f32,
|
||||
pub recommended_base_fee: f32,
|
||||
pub struct Response {
|
||||
pub safe_low: f64,
|
||||
pub standard: f64,
|
||||
pub fast: f64,
|
||||
pub fastest: f64,
|
||||
pub current_base_fee: f64,
|
||||
pub recommended_base_fee: f64,
|
||||
}
|
||||
|
||||
impl Etherchain {
|
||||
/// Creates a new [Etherchain](https://etherchain.org/tools/gasPriceOracle) gas price oracle.
|
||||
pub fn new() -> Self {
|
||||
Self::with_client(Client::new())
|
||||
}
|
||||
|
||||
/// Same as [`Self::new`] but with a custom [`Client`].
|
||||
pub fn with_client(client: Client) -> Self {
|
||||
let url = Url::parse(ETHERCHAIN_URL).expect("invalid url");
|
||||
|
||||
Etherchain { client, url, gas_category: GasCategory::Standard }
|
||||
}
|
||||
|
||||
/// Sets the gas price category to be used when fetching the gas price.
|
||||
#[must_use]
|
||||
pub fn category(mut self, gas_category: GasCategory) -> Self {
|
||||
self.gas_category = gas_category;
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn query(&self) -> Result<EtherchainResponse, GasOracleError> {
|
||||
Ok(self.client.get(self.url.as_ref()).send().await?.json::<EtherchainResponse>().await?)
|
||||
impl Response {
|
||||
#[inline]
|
||||
pub fn gas_from_category(&self, gas_category: GasCategory) -> f64 {
|
||||
match gas_category {
|
||||
GasCategory::SafeLow => self.safe_low,
|
||||
GasCategory::Standard => self.standard,
|
||||
GasCategory::Fast => self.fast,
|
||||
GasCategory::Fastest => self.fastest,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,19 +49,39 @@ impl Default for Etherchain {
|
|||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl GasOracle for Etherchain {
|
||||
async fn fetch(&self) -> Result<U256, GasOracleError> {
|
||||
async fn fetch(&self) -> Result<U256> {
|
||||
let res = self.query().await?;
|
||||
let gas_price = match self.gas_category {
|
||||
GasCategory::SafeLow => U256::from((res.safe_low as u64) * GWEI_TO_WEI),
|
||||
GasCategory::Standard => U256::from((res.standard as u64) * GWEI_TO_WEI),
|
||||
GasCategory::Fast => U256::from((res.fast as u64) * GWEI_TO_WEI),
|
||||
GasCategory::Fastest => U256::from((res.fastest as u64) * GWEI_TO_WEI),
|
||||
};
|
||||
|
||||
Ok(gas_price)
|
||||
let gas_price = res.gas_from_category(self.gas_category);
|
||||
Ok(from_gwei_f64(gas_price))
|
||||
}
|
||||
|
||||
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
|
||||
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
|
||||
Err(GasOracleError::Eip1559EstimationNotSupported)
|
||||
}
|
||||
}
|
||||
|
||||
impl Etherchain {
|
||||
/// Creates a new [Etherchain](https://etherchain.org/tools/gasPriceOracle) gas price oracle.
|
||||
pub fn new() -> Self {
|
||||
Self::with_client(Client::new())
|
||||
}
|
||||
|
||||
/// Same as [`Self::new`] but with a custom [`Client`].
|
||||
pub fn with_client(client: Client) -> Self {
|
||||
let url = Url::parse(URL).unwrap();
|
||||
Etherchain { client, url, gas_category: GasCategory::Standard }
|
||||
}
|
||||
|
||||
/// Sets the gas price category to be used when fetching the gas price.
|
||||
pub fn category(mut self, gas_category: GasCategory) -> Self {
|
||||
self.gas_category = gas_category;
|
||||
self
|
||||
}
|
||||
|
||||
/// Perform a request to the gas price API and deserialize the response.
|
||||
pub async fn query(&self) -> Result<Response> {
|
||||
let response =
|
||||
self.client.get(self.url.clone()).send().await?.error_for_status()?.json().await?;
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,57 @@
|
|||
use super::{GasCategory, GasOracle, GasOracleError, Result, GWEI_TO_WEI_U256};
|
||||
use async_trait::async_trait;
|
||||
|
||||
use ethers_core::types::U256;
|
||||
use ethers_etherscan::Client;
|
||||
|
||||
use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError, GWEI_TO_WEI};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
/// 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)]
|
||||
#[must_use]
|
||||
pub struct Etherscan {
|
||||
client: Client,
|
||||
gas_category: GasCategory,
|
||||
}
|
||||
|
||||
impl Deref for Etherscan {
|
||||
type Target = Client;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.client
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Etherscan {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.client
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl GasOracle for Etherscan {
|
||||
async fn fetch(&self) -> Result<U256> {
|
||||
// handle unsupported gas categories before making the request
|
||||
match self.gas_category {
|
||||
GasCategory::SafeLow | GasCategory::Standard | GasCategory::Fast => {}
|
||||
GasCategory::Fastest => return Err(GasOracleError::GasCategoryNotSupported),
|
||||
}
|
||||
|
||||
let result = self.query().await?;
|
||||
let gas_price = match self.gas_category {
|
||||
GasCategory::SafeLow => result.safe_gas_price,
|
||||
GasCategory::Standard => result.propose_gas_price,
|
||||
GasCategory::Fast => result.fast_gas_price,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
Ok(U256::from(gas_price) * GWEI_TO_WEI_U256)
|
||||
}
|
||||
|
||||
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
|
||||
Err(GasOracleError::Eip1559EstimationNotSupported)
|
||||
}
|
||||
}
|
||||
|
||||
impl Etherscan {
|
||||
/// Creates a new [Etherscan](https://etherscan.io/gastracker) gas price oracle.
|
||||
pub fn new(client: Client) -> Self {
|
||||
|
@ -20,32 +59,13 @@ impl Etherscan {
|
|||
}
|
||||
|
||||
/// Sets the gas price category to be used when fetching the gas price.
|
||||
#[must_use]
|
||||
pub fn category(mut self, gas_category: GasCategory) -> Self {
|
||||
self.gas_category = gas_category;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl GasOracle for Etherscan {
|
||||
async fn fetch(&self) -> Result<U256, GasOracleError> {
|
||||
if matches!(self.gas_category, GasCategory::Fastest) {
|
||||
return Err(GasOracleError::GasCategoryNotSupported)
|
||||
}
|
||||
|
||||
let result = self.client.gas_oracle().await?;
|
||||
|
||||
match self.gas_category {
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
|
||||
Err(GasOracleError::Eip1559EstimationNotSupported)
|
||||
/// Perform a request to the gas price API and deserialize the response.
|
||||
pub async fn query(&self) -> Result<ethers_etherscan::gas::GasOracle> {
|
||||
Ok(self.client.gas_oracle().await?)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,64 +1,54 @@
|
|||
use ethers_core::types::U256;
|
||||
|
||||
use super::{GasCategory, GasOracle, GasOracleError, Result};
|
||||
use async_trait::async_trait;
|
||||
use ethers_core::types::U256;
|
||||
use reqwest::Client;
|
||||
use serde::Deserialize;
|
||||
use url::Url;
|
||||
|
||||
use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError};
|
||||
const URL: &str = "https://beaconcha.in/api/v1/execution/gasnow";
|
||||
|
||||
const GAS_NOW_URL: &str = "https://etherchain.org/api/gasnow";
|
||||
|
||||
/// A client over HTTP for the [Etherchain GasNow](https://etherchain.org/tools/gasnow) gas tracker API
|
||||
/// that implements the `GasOracle` trait
|
||||
/// A client over HTTP for the [beaconcha.in GasNow](https://beaconcha.in/gasnow) gas tracker API
|
||||
/// that implements the `GasOracle` trait.
|
||||
#[derive(Clone, Debug)]
|
||||
#[must_use]
|
||||
pub struct GasNow {
|
||||
client: Client,
|
||||
url: Url,
|
||||
gas_category: GasCategory,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct GasNowResponseWrapper {
|
||||
data: GasNowResponse,
|
||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
|
||||
pub struct Response {
|
||||
pub code: u64,
|
||||
pub data: ResponseData,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct GasNowResponse {
|
||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
|
||||
pub struct ResponseData {
|
||||
pub rapid: u64,
|
||||
pub fast: u64,
|
||||
pub standard: u64,
|
||||
pub slow: u64,
|
||||
pub timestamp: u64,
|
||||
#[serde(rename = "priceUSD")]
|
||||
pub price_usd: f64,
|
||||
}
|
||||
|
||||
impl GasNow {
|
||||
/// Creates a new [Etherchain GasNow](https://etherchain.org/tools/gasnow) gas price oracle.
|
||||
pub fn new() -> Self {
|
||||
Self::with_client(Client::new())
|
||||
impl Response {
|
||||
#[inline]
|
||||
pub fn gas_from_category(&self, gas_category: GasCategory) -> u64 {
|
||||
self.data.gas_from_category(gas_category)
|
||||
}
|
||||
}
|
||||
|
||||
/// Same as [`Self::new`] but with a custom [`Client`].
|
||||
pub fn with_client(client: Client) -> Self {
|
||||
let url = Url::parse(GAS_NOW_URL).expect("invalid url");
|
||||
|
||||
Self { client, url, gas_category: GasCategory::Standard }
|
||||
}
|
||||
|
||||
/// Sets the gas price category to be used when fetching the gas price.
|
||||
pub fn category(mut self, gas_category: GasCategory) -> Self {
|
||||
self.gas_category = gas_category;
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn query(&self) -> Result<GasNowResponse, GasOracleError> {
|
||||
let res = self
|
||||
.client
|
||||
.get(self.url.as_ref())
|
||||
.send()
|
||||
.await?
|
||||
.json::<GasNowResponseWrapper>()
|
||||
.await?;
|
||||
Ok(res.data)
|
||||
impl ResponseData {
|
||||
fn gas_from_category(&self, gas_category: GasCategory) -> u64 {
|
||||
match gas_category {
|
||||
GasCategory::SafeLow => self.slow,
|
||||
GasCategory::Standard => self.standard,
|
||||
GasCategory::Fast => self.fast,
|
||||
GasCategory::Fastest => self.rapid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,19 +61,39 @@ impl Default for GasNow {
|
|||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl GasOracle for GasNow {
|
||||
async fn fetch(&self) -> Result<U256, GasOracleError> {
|
||||
async fn fetch(&self) -> Result<U256> {
|
||||
let res = self.query().await?;
|
||||
let gas_price = match self.gas_category {
|
||||
GasCategory::SafeLow => U256::from(res.slow),
|
||||
GasCategory::Standard => U256::from(res.standard),
|
||||
GasCategory::Fast => U256::from(res.fast),
|
||||
GasCategory::Fastest => U256::from(res.rapid),
|
||||
};
|
||||
|
||||
Ok(gas_price)
|
||||
let gas_price = res.gas_from_category(self.gas_category);
|
||||
Ok(U256::from(gas_price))
|
||||
}
|
||||
|
||||
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
|
||||
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
|
||||
Err(GasOracleError::Eip1559EstimationNotSupported)
|
||||
}
|
||||
}
|
||||
|
||||
impl GasNow {
|
||||
/// Creates a new [beaconcha.in GasNow](https://beaconcha.in/gasnow) gas price oracle.
|
||||
pub fn new() -> Self {
|
||||
Self::with_client(Client::new())
|
||||
}
|
||||
|
||||
/// Same as [`Self::new`] but with a custom [`Client`].
|
||||
pub fn with_client(client: Client) -> Self {
|
||||
let url = Url::parse(URL).unwrap();
|
||||
Self { client, url, gas_category: GasCategory::Standard }
|
||||
}
|
||||
|
||||
/// Sets the gas price category to be used when fetching the gas price.
|
||||
pub fn category(mut self, gas_category: GasCategory) -> Self {
|
||||
self.gas_category = gas_category;
|
||||
self
|
||||
}
|
||||
|
||||
/// Perform a request to the gas price API and deserialize the response.
|
||||
pub async fn query(&self) -> Result<Response> {
|
||||
let response =
|
||||
self.client.get(self.url.as_ref()).send().await?.error_for_status()?.json().await?;
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::gas_oracle::{GasOracle, GasOracleError};
|
||||
use super::{GasOracle, GasOracleError, Result};
|
||||
use async_trait::async_trait;
|
||||
use ethers_core::types::U256;
|
||||
use futures_util::future::join_all;
|
||||
|
@ -28,13 +28,10 @@ impl Median {
|
|||
self.oracles.push((weight, Box::new(oracle)));
|
||||
}
|
||||
|
||||
pub async fn query_all<'a, Fn, Fut, O>(
|
||||
&'a self,
|
||||
mut f: Fn,
|
||||
) -> Result<Vec<(f32, O)>, GasOracleError>
|
||||
pub async fn query_all<'a, Fn, Fut, O>(&'a self, mut f: Fn) -> Result<Vec<(f32, O)>>
|
||||
where
|
||||
Fn: FnMut(&'a dyn GasOracle) -> Fut,
|
||||
Fut: Future<Output = Result<O, GasOracleError>>,
|
||||
Fut: Future<Output = Result<O>>,
|
||||
{
|
||||
// Process the oracles in parallel
|
||||
let futures = self.oracles.iter().map(|(_, oracle)| f(oracle.as_ref()));
|
||||
|
@ -62,13 +59,13 @@ impl Median {
|
|||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl GasOracle for Median {
|
||||
async fn fetch(&self) -> Result<U256, GasOracleError> {
|
||||
async fn fetch(&self) -> Result<U256> {
|
||||
let mut values = self.query_all(|oracle| oracle.fetch()).await?;
|
||||
// `query_all` guarantees `values` is not empty
|
||||
Ok(*weighted_fractile_by_key(0.5, &mut values, |fee| fee).unwrap())
|
||||
}
|
||||
|
||||
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
|
||||
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
|
||||
let mut values = self.query_all(|oracle| oracle.estimate_eip1559_fees()).await?;
|
||||
// `query_all` guarantees `values` is not empty
|
||||
Ok((
|
||||
|
|
|
@ -4,8 +4,8 @@ use ethers_core::types::{transaction::eip2718::TypedTransaction, *};
|
|||
use ethers_providers::{FromErr, Middleware, PendingTransaction};
|
||||
use thiserror::Error;
|
||||
|
||||
/// Middleware used for fetching gas prices over an API instead of `eth_gasPrice`.
|
||||
#[derive(Debug)]
|
||||
/// Middleware used for fetching gas prices over an API instead of `eth_gasPrice`
|
||||
pub struct GasOracleMiddleware<M, G> {
|
||||
inner: M,
|
||||
gas_oracle: G,
|
||||
|
@ -21,7 +21,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum MiddlewareError<M: Middleware> {
|
||||
#[error(transparent)]
|
||||
GasOracleError(#[from] GasOracleError),
|
||||
|
|
|
@ -1,54 +1,58 @@
|
|||
mod blocknative;
|
||||
pub mod blocknative;
|
||||
pub use blocknative::BlockNative;
|
||||
|
||||
mod eth_gas_station;
|
||||
pub mod eth_gas_station;
|
||||
#[allow(deprecated)]
|
||||
pub use eth_gas_station::EthGasStation;
|
||||
|
||||
mod etherchain;
|
||||
pub mod etherchain;
|
||||
pub use etherchain::Etherchain;
|
||||
|
||||
mod etherscan;
|
||||
pub mod etherscan;
|
||||
pub use etherscan::Etherscan;
|
||||
|
||||
mod middleware;
|
||||
pub mod middleware;
|
||||
pub use middleware::{GasOracleMiddleware, MiddlewareError};
|
||||
|
||||
mod median;
|
||||
pub mod median;
|
||||
pub use median::Median;
|
||||
|
||||
mod cache;
|
||||
pub mod cache;
|
||||
pub use cache::Cache;
|
||||
|
||||
mod polygon;
|
||||
pub mod polygon;
|
||||
pub use polygon::Polygon;
|
||||
|
||||
mod gas_now;
|
||||
pub mod gas_now;
|
||||
pub use gas_now::GasNow;
|
||||
|
||||
mod provider_oracle;
|
||||
pub mod provider_oracle;
|
||||
pub use provider_oracle::ProviderOracle;
|
||||
|
||||
use ethers_core::types::U256;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use auto_impl::auto_impl;
|
||||
use ethers_core::types::U256;
|
||||
use reqwest::Error as ReqwestError;
|
||||
use std::error::Error;
|
||||
use std::{error::Error, fmt::Debug};
|
||||
use thiserror::Error;
|
||||
|
||||
const GWEI_TO_WEI: u64 = 1000000000;
|
||||
pub(crate) const GWEI_TO_WEI: u64 = 1_000_000_000;
|
||||
pub(crate) const GWEI_TO_WEI_U256: U256 = U256([0, 0, 0, GWEI_TO_WEI]);
|
||||
|
||||
/// Various gas price categories. Choose one of the available
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||
pub type Result<T, E = GasOracleError> = std::result::Result<T, E>;
|
||||
|
||||
/// Generic [`GasOracle`] gas price categories.
|
||||
#[derive(Clone, Copy, Default, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum GasCategory {
|
||||
SafeLow,
|
||||
#[default]
|
||||
Standard,
|
||||
Fast,
|
||||
Fastest,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
/// Error thrown when fetching data from the `GasOracle`
|
||||
/// Error thrown by a [`GasOracle`].
|
||||
#[derive(Debug, Error)]
|
||||
pub enum GasOracleError {
|
||||
/// An internal error in the HTTP request made from the underlying
|
||||
/// gas oracle
|
||||
|
@ -83,48 +87,69 @@ pub enum GasOracleError {
|
|||
UnsupportedChain,
|
||||
|
||||
/// Error thrown when the provider failed.
|
||||
#[error("Chain is not supported by the oracle")]
|
||||
#[error("Provider error: {0}")]
|
||||
ProviderError(#[from] Box<dyn Error + Send + Sync>),
|
||||
}
|
||||
|
||||
/// `GasOracle` is a trait that an underlying gas oracle needs to implement.
|
||||
/// An Ethereum gas price oracle.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use ethers_middleware::{
|
||||
/// gas_oracle::{EthGasStation, Etherscan, GasCategory, GasOracle},
|
||||
/// };
|
||||
/// use ethers_core::types::U256;
|
||||
/// use ethers_middleware::gas_oracle::{GasCategory, GasNow, GasOracle};
|
||||
///
|
||||
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let eth_gas_station_oracle = EthGasStation::new(Some("my-api-key"));
|
||||
/// let etherscan_oracle = EthGasStation::new(None).category(GasCategory::SafeLow);
|
||||
///
|
||||
/// let data_1 = eth_gas_station_oracle.fetch().await?;
|
||||
/// let data_2 = etherscan_oracle.fetch().await?;
|
||||
/// let oracle = GasNow::default().category(GasCategory::SafeLow);
|
||||
/// let gas_price = oracle.fetch().await?;
|
||||
/// assert!(gas_price > U256::zero());
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[auto_impl(&, Box, Arc)]
|
||||
pub trait GasOracle: Send + Sync + std::fmt::Debug {
|
||||
/// Makes an asynchronous HTTP query to the underlying `GasOracle`
|
||||
pub trait GasOracle: Send + Sync + Debug {
|
||||
/// Makes an asynchronous HTTP query to the underlying [`GasOracle`] to fetch the current gas
|
||||
/// price estimate.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use ethers_middleware::{
|
||||
/// gas_oracle::{Etherchain, GasCategory, GasOracle},
|
||||
/// };
|
||||
/// ```no_run
|
||||
/// use ethers_core::types::U256;
|
||||
/// use ethers_middleware::gas_oracle::{GasCategory, GasNow, GasOracle};
|
||||
///
|
||||
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let etherchain_oracle = Etherchain::new().category(GasCategory::Fastest);
|
||||
/// let data = etherchain_oracle.fetch().await?;
|
||||
/// let oracle = GasNow::default().category(GasCategory::SafeLow);
|
||||
/// let gas_price = oracle.fetch().await?;
|
||||
/// assert!(gas_price > U256::zero());
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
async fn fetch(&self) -> Result<U256, GasOracleError>;
|
||||
async fn fetch(&self) -> Result<U256>;
|
||||
|
||||
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError>;
|
||||
/// Makes an asynchronous HTTP query to the underlying [`GasOracle`] to fetch the current max
|
||||
/// gas fee and priority gas fee estimates.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use ethers_core::types::U256;
|
||||
/// use ethers_middleware::gas_oracle::{GasCategory, GasNow, GasOracle};
|
||||
///
|
||||
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let oracle = GasNow::default().category(GasCategory::SafeLow);
|
||||
/// let (max_fee, priority_fee) = oracle.estimate_eip1559_fees().await?;
|
||||
/// assert!(max_fee > U256::zero());
|
||||
/// assert!(priority_fee > U256::zero());
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)>;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[doc(hidden)]
|
||||
pub(crate) fn from_gwei_f64(gwei: f64) -> U256 {
|
||||
ethers_core::types::u256_from_f64_saturating(gwei) * GWEI_TO_WEI_U256
|
||||
}
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError};
|
||||
use super::{from_gwei_f64, GasCategory, GasOracle, GasOracleError, Result};
|
||||
use async_trait::async_trait;
|
||||
use ethers_core::types::{u256_from_f64_saturating, Chain, U256};
|
||||
use ethers_core::types::{Chain, U256};
|
||||
use reqwest::Client;
|
||||
use serde::Deserialize;
|
||||
use url::Url;
|
||||
|
||||
const GAS_PRICE_ENDPOINT: &str = "https://gasstation-mainnet.matic.network/v2";
|
||||
const MUMBAI_GAS_PRICE_ENDPOINT: &str = "https://gasstation-mumbai.matic.today/v2";
|
||||
const MAINNET_URL: &str = "https://gasstation-mainnet.matic.network/v2";
|
||||
const MUMBAI_URL: &str = "https://gasstation-mumbai.matic.today/v2";
|
||||
|
||||
/// The [Polygon](https://docs.polygon.technology/docs/develop/tools/polygon-gas-station/) gas station API
|
||||
/// Queries over HTTP and implements the `GasOracle` trait
|
||||
/// Queries over HTTP and implements the `GasOracle` trait.
|
||||
#[derive(Clone, Debug)]
|
||||
#[must_use]
|
||||
pub struct Polygon {
|
||||
client: Client,
|
||||
url: Url,
|
||||
|
@ -18,74 +19,87 @@ pub struct Polygon {
|
|||
}
|
||||
|
||||
/// The response from the Polygon gas station API.
|
||||
///
|
||||
/// Gas prices are in Gwei.
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Response {
|
||||
estimated_base_fee: f64,
|
||||
safe_low: GasEstimate,
|
||||
standard: GasEstimate,
|
||||
fast: GasEstimate,
|
||||
pub estimated_base_fee: f64,
|
||||
pub safe_low: GasEstimate,
|
||||
pub standard: GasEstimate,
|
||||
pub fast: GasEstimate,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GasEstimate {
|
||||
max_priority_fee: f64,
|
||||
max_fee: f64,
|
||||
pub max_priority_fee: f64,
|
||||
pub max_fee: f64,
|
||||
}
|
||||
|
||||
impl Polygon {
|
||||
pub fn new(chain: Chain) -> Result<Self, GasOracleError> {
|
||||
Self::with_client(Client::new(), chain)
|
||||
impl Response {
|
||||
#[inline]
|
||||
pub fn estimate_from_category(&self, gas_category: GasCategory) -> GasEstimate {
|
||||
match gas_category {
|
||||
GasCategory::SafeLow => self.safe_low,
|
||||
GasCategory::Standard => self.standard,
|
||||
GasCategory::Fast => self.fast,
|
||||
GasCategory::Fastest => self.fast,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_client(client: Client, chain: Chain) -> Result<Self, GasOracleError> {
|
||||
// TODO: Sniff chain from chain id.
|
||||
let url = match chain {
|
||||
Chain::Polygon => Url::parse(GAS_PRICE_ENDPOINT).unwrap(),
|
||||
Chain::PolygonMumbai => Url::parse(MUMBAI_GAS_PRICE_ENDPOINT).unwrap(),
|
||||
_ => return Err(GasOracleError::UnsupportedChain),
|
||||
};
|
||||
Ok(Self { client, url, gas_category: GasCategory::Standard })
|
||||
}
|
||||
|
||||
/// Sets the gas price category to be used when fetching the gas price.
|
||||
#[must_use]
|
||||
pub fn category(mut self, gas_category: GasCategory) -> Self {
|
||||
self.gas_category = gas_category;
|
||||
self
|
||||
}
|
||||
|
||||
/// Perform request to Blocknative, decode response
|
||||
pub async fn request(&self) -> Result<(f64, GasEstimate), GasOracleError> {
|
||||
let response: Response =
|
||||
self.client.get(self.url.as_ref()).send().await?.error_for_status()?.json().await?;
|
||||
let estimate = match self.gas_category {
|
||||
GasCategory::SafeLow => response.safe_low,
|
||||
GasCategory::Standard => response.standard,
|
||||
GasCategory::Fast => response.fast,
|
||||
GasCategory::Fastest => response.fast,
|
||||
};
|
||||
Ok((response.estimated_base_fee, estimate))
|
||||
impl Default for Polygon {
|
||||
fn default() -> Self {
|
||||
Self::new(Chain::Polygon).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl GasOracle for Polygon {
|
||||
async fn fetch(&self) -> Result<U256, GasOracleError> {
|
||||
let (base_fee, estimate) = self.request().await?;
|
||||
let fee = base_fee + estimate.max_priority_fee;
|
||||
Ok(from_gwei(fee))
|
||||
async fn fetch(&self) -> Result<U256> {
|
||||
let response = self.query().await?;
|
||||
let base = response.estimated_base_fee;
|
||||
let prio = response.estimate_from_category(self.gas_category).max_priority_fee;
|
||||
let fee = base + prio;
|
||||
Ok(from_gwei_f64(fee))
|
||||
}
|
||||
|
||||
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
|
||||
let (_, estimate) = self.request().await?;
|
||||
Ok((from_gwei(estimate.max_fee), from_gwei(estimate.max_priority_fee)))
|
||||
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
|
||||
let response = self.query().await?;
|
||||
let estimate = response.estimate_from_category(self.gas_category);
|
||||
let max = from_gwei_f64(estimate.max_fee);
|
||||
let prio = from_gwei_f64(estimate.max_priority_fee);
|
||||
Ok((max, prio))
|
||||
}
|
||||
}
|
||||
|
||||
fn from_gwei(gwei: f64) -> U256 {
|
||||
u256_from_f64_saturating(gwei * 1.0e9_f64)
|
||||
impl Polygon {
|
||||
pub fn new(chain: Chain) -> Result<Self> {
|
||||
Self::with_client(Client::new(), chain)
|
||||
}
|
||||
|
||||
pub fn with_client(client: Client, chain: Chain) -> Result<Self> {
|
||||
// TODO: Sniff chain from chain id.
|
||||
let url = match chain {
|
||||
Chain::Polygon => MAINNET_URL,
|
||||
Chain::PolygonMumbai => MUMBAI_URL,
|
||||
_ => return Err(GasOracleError::UnsupportedChain),
|
||||
};
|
||||
Ok(Self { client, url: Url::parse(url).unwrap(), gas_category: GasCategory::Standard })
|
||||
}
|
||||
|
||||
/// Sets the gas price category to be used when fetching the gas price.
|
||||
pub fn category(mut self, gas_category: GasCategory) -> Self {
|
||||
self.gas_category = gas_category;
|
||||
self
|
||||
}
|
||||
|
||||
/// Perform a request to the gas price API and deserialize the response.
|
||||
pub async fn query(&self) -> Result<Response> {
|
||||
let response =
|
||||
self.client.get(self.url.clone()).send().await?.error_for_status()?.json().await?;
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::gas_oracle::{GasOracle, GasOracleError};
|
||||
use super::{GasOracle, GasOracleError, Result};
|
||||
use async_trait::async_trait;
|
||||
use ethers_core::types::U256;
|
||||
use ethers_providers::Middleware;
|
||||
|
@ -6,7 +6,8 @@ use std::fmt::Debug;
|
|||
|
||||
/// Gas oracle from a [`Middleware`] implementation such as an
|
||||
/// Ethereum RPC provider.
|
||||
#[derive(Debug)]
|
||||
#[derive(Clone, Debug)]
|
||||
#[must_use]
|
||||
pub struct ProviderOracle<M: Middleware> {
|
||||
provider: M,
|
||||
}
|
||||
|
@ -21,16 +22,16 @@ impl<M: Middleware> ProviderOracle<M> {
|
|||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl<M: Middleware> GasOracle for ProviderOracle<M>
|
||||
where
|
||||
<M as Middleware>::Error: 'static,
|
||||
M::Error: 'static,
|
||||
{
|
||||
async fn fetch(&self) -> Result<U256, GasOracleError> {
|
||||
async fn fetch(&self) -> Result<U256> {
|
||||
self.provider
|
||||
.get_gas_price()
|
||||
.await
|
||||
.map_err(|err| GasOracleError::ProviderError(Box::new(err)))
|
||||
}
|
||||
|
||||
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
|
||||
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
|
||||
// TODO: Allow configuring different estimation functions.
|
||||
self.provider
|
||||
.estimate_eip1559_fees(None)
|
||||
|
|
|
@ -1,22 +1,21 @@
|
|||
use ethers_contract::Lazy;
|
||||
use ethers_core::types::*;
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// A lazily computed hash map with the Ethereum network IDs as keys and the corresponding
|
||||
/// DsProxyFactory contract addresses as values
|
||||
pub static ADDRESS_BOOK: Lazy<HashMap<U256, Address>> = Lazy::new(|| {
|
||||
let mut m = HashMap::new();
|
||||
let mut m = HashMap::with_capacity(1);
|
||||
|
||||
// mainnet
|
||||
let addr =
|
||||
Address::from_str("eefba1e63905ef1d7acba5a8513c70307c1ce441").expect("Decoding failed");
|
||||
m.insert(U256::from(1u8), addr);
|
||||
let addr = "eefba1e63905ef1d7acba5a8513c70307c1ce441".parse().unwrap();
|
||||
m.insert(U256::from(1_u64), addr);
|
||||
|
||||
m
|
||||
});
|
||||
|
||||
/// Generated with abigen:
|
||||
///
|
||||
/// Generated with
|
||||
/// ```ignore
|
||||
/// # use ethers_contract::abigen;
|
||||
/// abigen!(DsProxyFactory,
|
||||
|
@ -26,7 +25,6 @@ pub static ADDRESS_BOOK: Lazy<HashMap<U256, Address>> = Lazy::new(|| {
|
|||
/// }
|
||||
/// );
|
||||
/// ```
|
||||
// Auto-generated type-safe bindings
|
||||
pub use dsproxyfactory_mod::*;
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
mod dsproxyfactory_mod {
|
||||
|
|
|
@ -1,69 +1,67 @@
|
|||
#![cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(not(feature = "celo"))]
|
||||
mod tests {
|
||||
use ethers_core::{rand::thread_rng, types::U64};
|
||||
use ethers_middleware::{
|
||||
builder::MiddlewareBuilder,
|
||||
gas_escalator::{Frequency, GasEscalatorMiddleware, GeometricGasPrice},
|
||||
gas_oracle::{EthGasStation, GasOracleMiddleware},
|
||||
nonce_manager::NonceManagerMiddleware,
|
||||
signer::SignerMiddleware,
|
||||
};
|
||||
use ethers_providers::{Middleware, Provider};
|
||||
use ethers_signers::{LocalWallet, Signer};
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_raw_middleware_stack() {
|
||||
let (provider, mock) = Provider::mocked();
|
||||
use ethers_core::{rand::thread_rng, types::U64};
|
||||
use ethers_middleware::{
|
||||
builder::MiddlewareBuilder,
|
||||
gas_escalator::{Frequency, GasEscalatorMiddleware, GeometricGasPrice},
|
||||
gas_oracle::{GasNow, GasOracleMiddleware},
|
||||
nonce_manager::NonceManagerMiddleware,
|
||||
signer::SignerMiddleware,
|
||||
};
|
||||
use ethers_providers::{Middleware, Provider};
|
||||
use ethers_signers::{LocalWallet, Signer};
|
||||
|
||||
let signer = LocalWallet::new(&mut thread_rng());
|
||||
let address = signer.address();
|
||||
let escalator = GeometricGasPrice::new(1.125, 60u64, None::<u64>);
|
||||
#[tokio::test]
|
||||
async fn build_raw_middleware_stack() {
|
||||
let (provider, mock) = Provider::mocked();
|
||||
|
||||
let provider = provider
|
||||
.wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock))
|
||||
.wrap_into(|p| GasOracleMiddleware::new(p, EthGasStation::new(None)))
|
||||
.wrap_into(|p| SignerMiddleware::new(p, signer))
|
||||
.wrap_into(|p| NonceManagerMiddleware::new(p, address));
|
||||
let signer = LocalWallet::new(&mut thread_rng());
|
||||
let address = signer.address();
|
||||
let escalator = GeometricGasPrice::new(1.125, 60u64, None::<u64>);
|
||||
|
||||
// push a response
|
||||
mock.push(U64::from(12u64)).unwrap();
|
||||
let block: U64 = provider.get_block_number().await.unwrap();
|
||||
assert_eq!(block.as_u64(), 12);
|
||||
let provider = provider
|
||||
.wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock))
|
||||
.wrap_into(|p| GasOracleMiddleware::new(p, GasNow::new()))
|
||||
.wrap_into(|p| SignerMiddleware::new(p, signer))
|
||||
.wrap_into(|p| NonceManagerMiddleware::new(p, address));
|
||||
|
||||
provider.get_block_number().await.unwrap_err();
|
||||
// push a response
|
||||
mock.push(U64::from(12u64)).unwrap();
|
||||
let block: U64 = provider.get_block_number().await.unwrap();
|
||||
assert_eq!(block.as_u64(), 12);
|
||||
|
||||
// 2 calls were made
|
||||
mock.assert_request("eth_blockNumber", ()).unwrap();
|
||||
mock.assert_request("eth_blockNumber", ()).unwrap();
|
||||
mock.assert_request("eth_blockNumber", ()).unwrap_err();
|
||||
}
|
||||
provider.get_block_number().await.unwrap_err();
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_declarative_middleware_stack() {
|
||||
let (provider, mock) = Provider::mocked();
|
||||
|
||||
let signer = LocalWallet::new(&mut thread_rng());
|
||||
let address = signer.address();
|
||||
let escalator = GeometricGasPrice::new(1.125, 60u64, None::<u64>);
|
||||
let gas_oracle = EthGasStation::new(None);
|
||||
|
||||
let provider = provider
|
||||
.wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock))
|
||||
.gas_oracle(gas_oracle)
|
||||
.with_signer(signer)
|
||||
.nonce_manager(address);
|
||||
|
||||
// push a response
|
||||
mock.push(U64::from(12u64)).unwrap();
|
||||
let block: U64 = provider.get_block_number().await.unwrap();
|
||||
assert_eq!(block.as_u64(), 12);
|
||||
|
||||
provider.get_block_number().await.unwrap_err();
|
||||
|
||||
// 2 calls were made
|
||||
mock.assert_request("eth_blockNumber", ()).unwrap();
|
||||
mock.assert_request("eth_blockNumber", ()).unwrap();
|
||||
mock.assert_request("eth_blockNumber", ()).unwrap_err();
|
||||
}
|
||||
// 2 calls were made
|
||||
mock.assert_request("eth_blockNumber", ()).unwrap();
|
||||
mock.assert_request("eth_blockNumber", ()).unwrap();
|
||||
mock.assert_request("eth_blockNumber", ()).unwrap_err();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_declarative_middleware_stack() {
|
||||
let (provider, mock) = Provider::mocked();
|
||||
|
||||
let signer = LocalWallet::new(&mut thread_rng());
|
||||
let address = signer.address();
|
||||
let escalator = GeometricGasPrice::new(1.125, 60u64, None::<u64>);
|
||||
let gas_oracle = GasNow::new();
|
||||
|
||||
let provider = provider
|
||||
.wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock))
|
||||
.gas_oracle(gas_oracle)
|
||||
.with_signer(signer)
|
||||
.nonce_manager(address);
|
||||
|
||||
// push a response
|
||||
mock.push(U64::from(12u64)).unwrap();
|
||||
let block: U64 = provider.get_block_number().await.unwrap();
|
||||
assert_eq!(block.as_u64(), 12);
|
||||
|
||||
provider.get_block_number().await.unwrap_err();
|
||||
|
||||
// 2 calls were made
|
||||
mock.assert_request("eth_blockNumber", ()).unwrap();
|
||||
mock.assert_request("eth_blockNumber", ()).unwrap();
|
||||
mock.assert_request("eth_blockNumber", ()).unwrap_err();
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#![cfg(not(target_arch = "wasm32"))]
|
||||
|
||||
use ethers_core::types::*;
|
||||
use ethers_middleware::{
|
||||
gas_escalator::{Frequency, GasEscalatorMiddleware, GeometricGasPrice},
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
#![cfg(not(target_arch = "wasm32"))]
|
||||
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use ethers_core::{types::*, utils::Anvil};
|
||||
use ethers_middleware::gas_oracle::{
|
||||
EthGasStation, Etherchain, Etherscan, GasCategory, GasOracle, GasOracleError,
|
||||
GasOracleMiddleware,
|
||||
BlockNative, Etherchain, Etherscan, GasCategory, GasNow, GasOracle, GasOracleError,
|
||||
GasOracleMiddleware, Polygon, ProviderOracle, Result,
|
||||
};
|
||||
use ethers_providers::{Http, Middleware, Provider};
|
||||
use serial_test::serial;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct FakeGasOracle {
|
||||
|
@ -20,17 +18,18 @@ struct FakeGasOracle {
|
|||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl GasOracle for FakeGasOracle {
|
||||
async fn fetch(&self) -> Result<U256, GasOracleError> {
|
||||
async fn fetch(&self) -> Result<U256> {
|
||||
Ok(self.gas_price)
|
||||
}
|
||||
|
||||
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
|
||||
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
|
||||
Err(GasOracleError::Eip1559EstimationNotSupported)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn using_gas_oracle() {
|
||||
#[serial]
|
||||
async fn provider_using_gas_oracle() {
|
||||
let anvil = Anvil::new().spawn();
|
||||
|
||||
let from = anvil.addresses()[0];
|
||||
|
@ -38,11 +37,11 @@ async fn using_gas_oracle() {
|
|||
// connect to the network
|
||||
let provider = Provider::<Http>::try_from(anvil.endpoint()).unwrap();
|
||||
|
||||
// initial base fee
|
||||
let base_fee = 1_000_000_000u64;
|
||||
// assign a gas oracle to use
|
||||
let gas_oracle = FakeGasOracle { gas_price: (base_fee + 1337).into() };
|
||||
let expected_gas_price = gas_oracle.fetch().await.unwrap();
|
||||
let expected_gas_price = U256::from(1234567890_u64);
|
||||
let gas_oracle = FakeGasOracle { gas_price: expected_gas_price };
|
||||
let gas_price = gas_oracle.fetch().await.unwrap();
|
||||
assert_eq!(gas_price, expected_gas_price);
|
||||
|
||||
let provider = GasOracleMiddleware::new(provider, gas_oracle);
|
||||
|
||||
|
@ -55,35 +54,70 @@ async fn using_gas_oracle() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn eth_gas_station() {
|
||||
// initialize and fetch gas estimates from EthGasStation
|
||||
let eth_gas_station_oracle = EthGasStation::default();
|
||||
let data = eth_gas_station_oracle.fetch().await;
|
||||
data.unwrap();
|
||||
#[serial]
|
||||
async fn provider_oracle() {
|
||||
// spawn anvil and connect to it
|
||||
let anvil = Anvil::new().spawn();
|
||||
let provider = Provider::<Http>::try_from(anvil.endpoint()).unwrap();
|
||||
|
||||
// assert that provider.get_gas_price() and oracle.fetch() return the same value
|
||||
let expected_gas_price = provider.get_gas_price().await.unwrap();
|
||||
let provider_oracle = ProviderOracle::new(provider);
|
||||
let gas = provider_oracle.fetch().await.unwrap();
|
||||
assert_eq!(gas, expected_gas_price);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn blocknative() {
|
||||
let gas_now_oracle = BlockNative::default();
|
||||
let gas_price = gas_now_oracle.fetch().await.unwrap();
|
||||
assert!(gas_price > U256::zero());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "ETHGasStation is shutting down: https://twitter.com/ETHGasStation/status/1597341610777317376"]
|
||||
#[allow(deprecated)]
|
||||
async fn eth_gas_station() {
|
||||
let eth_gas_station_oracle = ethers_middleware::gas_oracle::EthGasStation::default();
|
||||
let gas_price = eth_gas_station_oracle.fetch().await.unwrap();
|
||||
assert!(gas_price > U256::zero());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "Etherchain / beaconcha.in's `gasPriceOracle` API currently returns 404: https://www.etherchain.org/api/gasPriceOracle"]
|
||||
async fn etherchain() {
|
||||
let etherchain_oracle = Etherchain::default();
|
||||
let gas_price = etherchain_oracle.fetch().await.unwrap();
|
||||
assert!(gas_price > U256::zero());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn etherscan() {
|
||||
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(etherscan_client.clone()).category(GasCategory::Fastest);
|
||||
let data = etherscan_oracle.fetch().await;
|
||||
data.unwrap_err();
|
||||
let error = etherscan_oracle.fetch().await.unwrap_err();
|
||||
assert!(matches!(error, GasOracleError::GasCategoryNotSupported));
|
||||
|
||||
// but fetching the `standard` gas price should work fine
|
||||
let etherscan_oracle = Etherscan::new(etherscan_client).category(GasCategory::SafeLow);
|
||||
|
||||
let data = etherscan_oracle.fetch().await;
|
||||
data.unwrap();
|
||||
let gas_price = etherscan_oracle.fetch().await.unwrap();
|
||||
assert!(gas_price > U256::zero());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn etherchain() {
|
||||
// initialize and fetch gas estimates from Etherchain
|
||||
let etherchain_oracle = Etherchain::default().category(GasCategory::Fast);
|
||||
let data = etherchain_oracle.fetch().await;
|
||||
data.unwrap();
|
||||
async fn gas_now() {
|
||||
let gas_now_oracle = GasNow::default();
|
||||
let gas_price = gas_now_oracle.fetch().await.unwrap();
|
||||
assert!(gas_price > U256::zero());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn polygon() {
|
||||
let polygon_oracle = Polygon::default();
|
||||
let gas_price = polygon_oracle.fetch().await.unwrap();
|
||||
assert!(gas_price > U256::zero());
|
||||
}
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
#![cfg(not(target_arch = "wasm32"))]
|
||||
#[tokio::test]
|
||||
#[cfg(not(feature = "celo"))]
|
||||
async fn nonce_manager() {
|
||||
use ethers_core::types::*;
|
||||
use ethers_middleware::{nonce_manager::NonceManagerMiddleware, signer::SignerMiddleware};
|
||||
use ethers_providers::Middleware;
|
||||
use ethers_signers::{LocalWallet, Signer};
|
||||
use std::time::Duration;
|
||||
#![cfg(all(not(target_arch = "wasm32"), not(feature = "celo")))]
|
||||
|
||||
use ethers_core::types::*;
|
||||
use ethers_middleware::{nonce_manager::NonceManagerMiddleware, signer::SignerMiddleware};
|
||||
use ethers_providers::Middleware;
|
||||
use ethers_signers::{LocalWallet, Signer};
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::test]
|
||||
async fn nonce_manager() {
|
||||
let provider = ethers_providers::GOERLI.provider().interval(Duration::from_millis(2000u64));
|
||||
let chain_id = provider.get_chainid().await.unwrap().as_u64();
|
||||
|
||||
let wallet = std::env::var("GOERLI_PRIVATE_KEY")
|
||||
.unwrap()
|
||||
.expect("GOERLI_PRIVATE_KEY is not defined")
|
||||
.parse::<LocalWallet>()
|
||||
.unwrap()
|
||||
.with_chain_id(chain_id);
|
||||
|
|
|
@ -1,14 +1,22 @@
|
|||
#![allow(unused)]
|
||||
use ethers_providers::{Http, JsonRpcClient, Middleware, Provider, GOERLI};
|
||||
|
||||
use ethers_contract::ContractFactory;
|
||||
use ethers_core::{
|
||||
types::{BlockNumber, TransactionRequest},
|
||||
utils::parse_units,
|
||||
abi::Abi,
|
||||
types::*,
|
||||
utils::{parse_ether, parse_units, Anvil},
|
||||
};
|
||||
use ethers_middleware::signer::SignerMiddleware;
|
||||
use ethers_providers::{Http, JsonRpcClient, Middleware, Provider, GOERLI};
|
||||
use ethers_signers::{coins_bip39::English, LocalWallet, MnemonicBuilder, Signer};
|
||||
use ethers_solc::Solc;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::{convert::TryFrom, iter::Cycle, sync::atomic::AtomicU8, time::Duration};
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
iter::Cycle,
|
||||
sync::{atomic::AtomicU8, Arc},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
static WALLETS: Lazy<TestWallets> = Lazy::new(|| {
|
||||
TestWallets {
|
||||
|
@ -22,8 +30,6 @@ static WALLETS: Lazy<TestWallets> = Lazy::new(|| {
|
|||
#[tokio::test]
|
||||
#[cfg(not(feature = "celo"))]
|
||||
async fn send_eth() {
|
||||
use ethers_core::utils::Anvil;
|
||||
|
||||
let anvil = Anvil::new().spawn();
|
||||
|
||||
// this private key belongs to the above mnemonic
|
||||
|
@ -95,10 +101,6 @@ async fn pending_txs_with_confirmations_testnet() {
|
|||
generic_pending_txs_test(provider, address).await;
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "celo"))]
|
||||
use ethers_core::types::{Address, Eip1559TransactionRequest};
|
||||
use ethers_core::utils::parse_ether;
|
||||
|
||||
// different keys to avoid nonce errors
|
||||
#[tokio::test]
|
||||
#[cfg(not(feature = "celo"))]
|
||||
|
@ -195,8 +197,6 @@ async fn test_send_transaction() {
|
|||
#[tokio::test]
|
||||
#[cfg(not(feature = "celo"))]
|
||||
async fn send_transaction_handles_tx_from_field() {
|
||||
use ethers_core::utils::Anvil;
|
||||
|
||||
// launch anvil
|
||||
let anvil = Anvil::new().spawn();
|
||||
|
||||
|
@ -240,14 +240,6 @@ async fn send_transaction_handles_tx_from_field() {
|
|||
#[tokio::test]
|
||||
#[cfg(feature = "celo")]
|
||||
async fn deploy_and_call_contract() {
|
||||
use ethers_contract::ContractFactory;
|
||||
use ethers_core::{
|
||||
abi::Abi,
|
||||
types::{BlockNumber, Bytes, H256, U256},
|
||||
};
|
||||
use ethers_solc::Solc;
|
||||
use std::sync::Arc;
|
||||
|
||||
// compiles the given contract and returns the ABI and Bytecode
|
||||
fn compile_contract(path: &str, name: &str) -> (Abi, Bytes) {
|
||||
let path = format!("./tests/solidity-contracts/{path}");
|
||||
|
|
|
@ -1,95 +1,93 @@
|
|||
#![cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(not(feature = "celo"))]
|
||||
mod tests {
|
||||
use ethers_core::{rand::thread_rng, types::TransactionRequest, utils::Anvil};
|
||||
use ethers_middleware::{
|
||||
gas_escalator::{Frequency, GasEscalatorMiddleware, GeometricGasPrice},
|
||||
gas_oracle::{EthGasStation, GasCategory, GasOracleMiddleware},
|
||||
nonce_manager::NonceManagerMiddleware,
|
||||
signer::SignerMiddleware,
|
||||
};
|
||||
use ethers_providers::{Http, Middleware, Provider};
|
||||
use ethers_signers::{LocalWallet, Signer};
|
||||
use std::convert::TryFrom;
|
||||
#![cfg(all(not(target_arch = "wasm32"), not(feature = "celo")))]
|
||||
|
||||
#[tokio::test]
|
||||
async fn mock_with_middleware() {
|
||||
let (provider, mock) = Provider::mocked();
|
||||
use ethers_core::{rand::thread_rng, types::TransactionRequest, utils::Anvil};
|
||||
use ethers_middleware::{
|
||||
gas_escalator::{Frequency, GasEscalatorMiddleware, GeometricGasPrice},
|
||||
gas_oracle::{GasCategory, GasNow, GasOracleMiddleware},
|
||||
nonce_manager::NonceManagerMiddleware,
|
||||
signer::SignerMiddleware,
|
||||
};
|
||||
use ethers_providers::{Http, Middleware, Provider};
|
||||
use ethers_signers::{LocalWallet, Signer};
|
||||
use std::convert::TryFrom;
|
||||
|
||||
// add a bunch of middlewares
|
||||
let gas_oracle = EthGasStation::new(None).category(GasCategory::SafeLow);
|
||||
let signer = LocalWallet::new(&mut thread_rng());
|
||||
let address = signer.address();
|
||||
let escalator = GeometricGasPrice::new(1.125, 60u64, None::<u64>);
|
||||
let provider = GasEscalatorMiddleware::new(provider, escalator, Frequency::PerBlock);
|
||||
let provider = GasOracleMiddleware::new(provider, gas_oracle);
|
||||
let provider = SignerMiddleware::new(provider, signer);
|
||||
let provider = NonceManagerMiddleware::new(provider, address);
|
||||
#[tokio::test]
|
||||
async fn mock_with_middleware() {
|
||||
let (provider, mock) = Provider::mocked();
|
||||
|
||||
// push a response
|
||||
use ethers_core::types::U64;
|
||||
mock.push(U64::from(12u64)).unwrap();
|
||||
let blk = provider.get_block_number().await.unwrap();
|
||||
assert_eq!(blk.as_u64(), 12);
|
||||
// add a bunch of middlewares
|
||||
let gas_oracle = GasNow::new().category(GasCategory::SafeLow);
|
||||
let signer = LocalWallet::new(&mut thread_rng());
|
||||
let address = signer.address();
|
||||
let escalator = GeometricGasPrice::new(1.125, 60u64, None::<u64>);
|
||||
let provider = GasEscalatorMiddleware::new(provider, escalator, Frequency::PerBlock);
|
||||
let provider = GasOracleMiddleware::new(provider, gas_oracle);
|
||||
let provider = SignerMiddleware::new(provider, signer);
|
||||
let provider = NonceManagerMiddleware::new(provider, address);
|
||||
|
||||
// now that the response is gone, there's nothing left
|
||||
// TODO: This returns:
|
||||
// MiddlewareError(
|
||||
// MiddlewareError(
|
||||
// MiddlewareError(
|
||||
// MiddlewareError(
|
||||
// JsonRpcClientError(EmptyResponses)
|
||||
// ))))
|
||||
// Can we flatten it in any way? Maybe inherent to the middleware
|
||||
// infrastructure
|
||||
provider.get_block_number().await.unwrap_err();
|
||||
// push a response
|
||||
use ethers_core::types::U64;
|
||||
mock.push(U64::from(12u64)).unwrap();
|
||||
let blk = provider.get_block_number().await.unwrap();
|
||||
assert_eq!(blk.as_u64(), 12);
|
||||
|
||||
// 2 calls were made
|
||||
mock.assert_request("eth_blockNumber", ()).unwrap();
|
||||
mock.assert_request("eth_blockNumber", ()).unwrap();
|
||||
mock.assert_request("eth_blockNumber", ()).unwrap_err();
|
||||
}
|
||||
// now that the response is gone, there's nothing left
|
||||
// TODO: This returns:
|
||||
// MiddlewareError(
|
||||
// MiddlewareError(
|
||||
// MiddlewareError(
|
||||
// MiddlewareError(
|
||||
// JsonRpcClientError(EmptyResponses)
|
||||
// ))))
|
||||
// Can we flatten it in any way? Maybe inherent to the middleware
|
||||
// infrastructure
|
||||
provider.get_block_number().await.unwrap_err();
|
||||
|
||||
#[tokio::test]
|
||||
async fn can_stack_middlewares() {
|
||||
let anvil = Anvil::new().block_time(5u64).spawn();
|
||||
let gas_oracle = EthGasStation::new(None).category(GasCategory::SafeLow);
|
||||
let signer: LocalWallet = anvil.keys()[0].clone().into();
|
||||
let address = signer.address();
|
||||
|
||||
// the base provider
|
||||
let provider = Arc::new(Provider::<Http>::try_from(anvil.endpoint()).unwrap());
|
||||
let chain_id = provider.get_chainid().await.unwrap().as_u64();
|
||||
let signer = signer.with_chain_id(chain_id);
|
||||
|
||||
// the Gas Price escalator middleware is the first middleware above the provider,
|
||||
// so that it receives the transaction last, after all the other middleware
|
||||
// have modified it accordingly
|
||||
let escalator = GeometricGasPrice::new(1.125, 60u64, None::<u64>);
|
||||
let provider = GasEscalatorMiddleware::new(provider, escalator, Frequency::PerBlock);
|
||||
|
||||
// The gas price middleware MUST be below the signing middleware for things to work
|
||||
let provider = GasOracleMiddleware::new(provider, gas_oracle);
|
||||
|
||||
// The signing middleware signs txs
|
||||
use std::sync::Arc;
|
||||
let provider = Arc::new(SignerMiddleware::new(provider, signer));
|
||||
|
||||
// The nonce manager middleware MUST be above the signing middleware so that it overrides
|
||||
// the nonce and the signer does not make any eth_getTransaction count calls
|
||||
let provider = NonceManagerMiddleware::new(provider, address);
|
||||
|
||||
let tx = TransactionRequest::new();
|
||||
let mut pending_txs = Vec::new();
|
||||
for _ in 0..10 {
|
||||
let pending = provider.send_transaction(tx.clone(), None).await.unwrap();
|
||||
let hash = *pending;
|
||||
let gas_price = provider.get_transaction(hash).await.unwrap().unwrap().gas_price;
|
||||
dbg!(gas_price);
|
||||
pending_txs.push(pending);
|
||||
}
|
||||
|
||||
let receipts = futures_util::future::join_all(pending_txs);
|
||||
dbg!(receipts.await);
|
||||
}
|
||||
// 2 calls were made
|
||||
mock.assert_request("eth_blockNumber", ()).unwrap();
|
||||
mock.assert_request("eth_blockNumber", ()).unwrap();
|
||||
mock.assert_request("eth_blockNumber", ()).unwrap_err();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn can_stack_middlewares() {
|
||||
let anvil = Anvil::new().block_time(5u64).spawn();
|
||||
let gas_oracle = GasNow::new().category(GasCategory::SafeLow);
|
||||
let signer: LocalWallet = anvil.keys()[0].clone().into();
|
||||
let address = signer.address();
|
||||
|
||||
// the base provider
|
||||
let provider = Arc::new(Provider::<Http>::try_from(anvil.endpoint()).unwrap());
|
||||
let chain_id = provider.get_chainid().await.unwrap().as_u64();
|
||||
let signer = signer.with_chain_id(chain_id);
|
||||
|
||||
// the Gas Price escalator middleware is the first middleware above the provider,
|
||||
// so that it receives the transaction last, after all the other middleware
|
||||
// have modified it accordingly
|
||||
let escalator = GeometricGasPrice::new(1.125, 60u64, None::<u64>);
|
||||
let provider = GasEscalatorMiddleware::new(provider, escalator, Frequency::PerBlock);
|
||||
|
||||
// The gas price middleware MUST be below the signing middleware for things to work
|
||||
let provider = GasOracleMiddleware::new(provider, gas_oracle);
|
||||
|
||||
// The signing middleware signs txs
|
||||
use std::sync::Arc;
|
||||
let provider = Arc::new(SignerMiddleware::new(provider, signer));
|
||||
|
||||
// The nonce manager middleware MUST be above the signing middleware so that it overrides
|
||||
// the nonce and the signer does not make any eth_getTransaction count calls
|
||||
let provider = NonceManagerMiddleware::new(provider, address);
|
||||
|
||||
let tx = TransactionRequest::new();
|
||||
let mut pending_txs = Vec::new();
|
||||
for _ in 0..10 {
|
||||
let pending = provider.send_transaction(tx.clone(), None).await.unwrap();
|
||||
let hash = *pending;
|
||||
let gas_price = provider.get_transaction(hash).await.unwrap().unwrap().gas_price;
|
||||
dbg!(gas_price);
|
||||
pending_txs.push(pending);
|
||||
}
|
||||
|
||||
let receipts = futures_util::future::join_all(pending_txs);
|
||||
dbg!(receipts.await);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#![cfg(not(target_arch = "wasm32"))]
|
||||
#![allow(unused)]
|
||||
#![cfg(all(not(target_arch = "wasm32"), not(feature = "celo")))]
|
||||
|
||||
use ethers_contract::{BaseContract, ContractFactory};
|
||||
use ethers_core::{abi::Abi, types::*, utils::Anvil};
|
||||
use ethers_middleware::{
|
||||
|
@ -24,7 +24,6 @@ fn compile_contract(path: &str, name: &str) -> (Abi, Bytes) {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(not(feature = "celo"))]
|
||||
async fn ds_proxy_transformer() {
|
||||
// randomness
|
||||
let mut rng = rand::thread_rng();
|
||||
|
@ -83,7 +82,6 @@ async fn ds_proxy_transformer() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(not(feature = "celo"))]
|
||||
async fn ds_proxy_code() {
|
||||
// randomness
|
||||
let mut rng = rand::thread_rng();
|
||||
|
|
Loading…
Reference in New Issue