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