(feat) gas oracle support (#56)
* (feat) gas oracle support * (refactor) make a separate module | fix clippy warning * add gas oracle to client using dynamic dispatch * fix doc build in multicall module * gas oracle returns U256 * support gas price fetching from client * avoid querying for unsupported gas categories * changes based on PR review * add support for gasnow API, refactor gwei to wei
This commit is contained in:
parent
ca2ec0aadd
commit
237f011259
|
@ -244,6 +244,17 @@ version = "0.1.10"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c74d84029116787153e02106bf53e66828452a4b325cc8652b788b5967c0a0b6"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "1.1.1"
|
||||
|
@ -497,6 +508,7 @@ dependencies = [
|
|||
"reqwest",
|
||||
"rustc-hex",
|
||||
"serde",
|
||||
"serde-aux",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
|
@ -1058,6 +1070,25 @@ dependencies = [
|
|||
"winapi 0.3.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_cpus"
|
||||
version = "1.13.0"
|
||||
|
@ -1447,6 +1478,18 @@ dependencies = [
|
|||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde-aux"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae50f53d4b01e854319c1f5b854cd59471f054ea7e554988850d3f36ca1dc852"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.112"
|
||||
|
|
|
@ -81,7 +81,8 @@ pub static ADDRESS_BOOK: Lazy<HashMap<U256, Address>> = Lazy::new(|| {
|
|||
/// .parse::<Wallet>()?.connect(provider);
|
||||
///
|
||||
/// // create the contract object. This will be used to construct the calls for multicall
|
||||
/// let contract = Contract::new(address, abi, client.clone());
|
||||
/// let client = Arc::new(client);
|
||||
/// let contract = Contract::new(address, abi, Arc::clone(&client));
|
||||
///
|
||||
/// // note that these [`ContractCall`]s are futures, and need to be `.await`ed to resolve.
|
||||
/// // But we will let `Multicall` to take care of that for us
|
||||
|
@ -92,7 +93,7 @@ pub static ADDRESS_BOOK: Lazy<HashMap<U256, Address>> = Lazy::new(|| {
|
|||
/// // the Multicall contract and we set that to `None`. If you wish to provide the address
|
||||
/// // for the Multicall contract, you can pass the `Some(multicall_addr)` argument.
|
||||
/// // Construction of the `Multicall` instance follows the builder pattern
|
||||
/// let multicall = Multicall::new(client.clone(), None)
|
||||
/// let multicall = Multicall::new(Arc::clone(&client), None)
|
||||
/// .await?
|
||||
/// .add_call(first_call)
|
||||
/// .add_call(second_call);
|
||||
|
|
|
@ -38,6 +38,9 @@ tokio = { version = "0.2.21", default-features = false, optional = true }
|
|||
real-tokio-native-tls = { package = "tokio-native-tls", version = "0.1.0", optional = true }
|
||||
async-tls = { version = "0.7.0", optional = true }
|
||||
|
||||
# needed for parsing while deserialization in gas oracles
|
||||
serde-aux = "0.6.1"
|
||||
|
||||
[dev-dependencies]
|
||||
ethers = { version = "0.1.3", path = "../ethers" }
|
||||
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
use ethers_core::types::U256;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use reqwest::Client;
|
||||
use serde::Deserialize;
|
||||
use url::Url;
|
||||
|
||||
use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError, GWEI_TO_WEI};
|
||||
|
||||
const ETH_GAS_STATION_URL_PREFIX: &str = "https://ethgasstation.info/api/ethgasAPI.json";
|
||||
|
||||
/// A client over HTTP for the [EthGasStation](https://ethgasstation.info/api/ethgasAPI.json) gas tracker API
|
||||
/// that implements the `GasOracle` trait
|
||||
#[derive(Debug)]
|
||||
pub struct EthGasStation {
|
||||
client: Client,
|
||||
url: Url,
|
||||
gas_category: GasCategory,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EthGasStationResponse {
|
||||
#[serde(rename = "safeLow")]
|
||||
safe_low: u64,
|
||||
average: u64,
|
||||
fast: u64,
|
||||
fastest: u64,
|
||||
}
|
||||
|
||||
impl EthGasStation {
|
||||
pub fn new(api_key: Option<&'static str>) -> Self {
|
||||
let url = match api_key {
|
||||
Some(key) => format!("{}?api-key={}", ETH_GAS_STATION_URL_PREFIX, key),
|
||||
None => ETH_GAS_STATION_URL_PREFIX.to_string(),
|
||||
};
|
||||
|
||||
let url = Url::parse(&url).expect("invalid url");
|
||||
|
||||
EthGasStation {
|
||||
client: Client::new(),
|
||||
url,
|
||||
gas_category: GasCategory::Standard,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn category(mut self, gas_category: GasCategory) -> Self {
|
||||
self.gas_category = gas_category;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl GasOracle for EthGasStation {
|
||||
async fn fetch(&self) -> Result<U256, GasOracleError> {
|
||||
let res = self
|
||||
.client
|
||||
.get(self.url.as_ref())
|
||||
.send()
|
||||
.await?
|
||||
.json::<EthGasStationResponse>()
|
||||
.await?;
|
||||
|
||||
let gas_price = match self.gas_category {
|
||||
GasCategory::SafeLow => U256::from((res.safe_low * GWEI_TO_WEI) / 10),
|
||||
GasCategory::Standard => U256::from((res.average * GWEI_TO_WEI) / 10),
|
||||
GasCategory::Fast => U256::from((res.fast * GWEI_TO_WEI) / 10),
|
||||
GasCategory::Fastest => U256::from((res.fastest * GWEI_TO_WEI) / 10),
|
||||
};
|
||||
|
||||
Ok(gas_price)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
use ethers_core::types::U256;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use reqwest::Client;
|
||||
use serde::Deserialize;
|
||||
use serde_aux::prelude::*;
|
||||
use url::Url;
|
||||
|
||||
use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError, GWEI_TO_WEI};
|
||||
|
||||
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
|
||||
/// that implements the `GasOracle` trait
|
||||
#[derive(Debug)]
|
||||
pub struct Etherchain {
|
||||
client: Client,
|
||||
url: Url,
|
||||
gas_category: GasCategory,
|
||||
}
|
||||
|
||||
impl Default for Etherchain {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EtherchainResponse {
|
||||
#[serde(deserialize_with = "deserialize_number_from_string")]
|
||||
#[serde(rename = "safeLow")]
|
||||
safe_low: f32,
|
||||
#[serde(deserialize_with = "deserialize_number_from_string")]
|
||||
standard: f32,
|
||||
#[serde(deserialize_with = "deserialize_number_from_string")]
|
||||
fast: f32,
|
||||
#[serde(deserialize_with = "deserialize_number_from_string")]
|
||||
fastest: f32,
|
||||
}
|
||||
|
||||
impl Etherchain {
|
||||
pub fn new() -> Self {
|
||||
let url = Url::parse(ETHERCHAIN_URL).expect("invalid url");
|
||||
|
||||
Etherchain {
|
||||
client: Client::new(),
|
||||
url,
|
||||
gas_category: GasCategory::Standard,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn category(mut self, gas_category: GasCategory) -> Self {
|
||||
self.gas_category = gas_category;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl GasOracle for Etherchain {
|
||||
async fn fetch(&self) -> Result<U256, GasOracleError> {
|
||||
let res = self
|
||||
.client
|
||||
.get(self.url.as_ref())
|
||||
.send()
|
||||
.await?
|
||||
.json::<EtherchainResponse>()
|
||||
.await?;
|
||||
|
||||
let gas_price = match self.gas_category {
|
||||
GasCategory::SafeLow => U256::from((res.safe_low as u64) * GWEI_TO_WEI),
|
||||
GasCategory::Standard => U256::from((res.standard as u64) * GWEI_TO_WEI),
|
||||
GasCategory::Fast => U256::from((res.fast as u64) * GWEI_TO_WEI),
|
||||
GasCategory::Fastest => U256::from((res.fastest as u64) * GWEI_TO_WEI),
|
||||
};
|
||||
|
||||
Ok(gas_price)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
use ethers_core::types::U256;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use reqwest::Client;
|
||||
use serde::Deserialize;
|
||||
use serde_aux::prelude::*;
|
||||
use url::Url;
|
||||
|
||||
use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError, GWEI_TO_WEI};
|
||||
|
||||
const ETHERSCAN_URL_PREFIX: &str =
|
||||
"https://api.etherscan.io/api?module=gastracker&action=gasoracle";
|
||||
|
||||
/// A client over HTTP for the [Etherscan](https://api.etherscan.io/api?module=gastracker&action=gasoracle) gas tracker API
|
||||
/// that implements the `GasOracle` trait
|
||||
#[derive(Debug)]
|
||||
pub struct Etherscan {
|
||||
client: Client,
|
||||
url: Url,
|
||||
gas_category: GasCategory,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EtherscanResponse {
|
||||
result: EtherscanResponseInner,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EtherscanResponseInner {
|
||||
#[serde(deserialize_with = "deserialize_number_from_string")]
|
||||
#[serde(rename = "SafeGasPrice")]
|
||||
safe_gas_price: u64,
|
||||
#[serde(deserialize_with = "deserialize_number_from_string")]
|
||||
#[serde(rename = "ProposeGasPrice")]
|
||||
propose_gas_price: u64,
|
||||
}
|
||||
|
||||
impl Etherscan {
|
||||
pub fn new(api_key: Option<&'static str>) -> Self {
|
||||
let url = match api_key {
|
||||
Some(key) => format!("{}&apikey={}", ETHERSCAN_URL_PREFIX, key),
|
||||
None => ETHERSCAN_URL_PREFIX.to_string(),
|
||||
};
|
||||
|
||||
let url = Url::parse(&url).expect("invalid url");
|
||||
|
||||
Etherscan {
|
||||
client: Client::new(),
|
||||
url,
|
||||
gas_category: GasCategory::Standard,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn category(mut self, gas_category: GasCategory) -> Self {
|
||||
self.gas_category = gas_category;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl GasOracle for Etherscan {
|
||||
async fn fetch(&self) -> Result<U256, GasOracleError> {
|
||||
if matches!(self.gas_category, GasCategory::Fast | GasCategory::Fastest) {
|
||||
return Err(GasOracleError::GasCategoryNotSupported);
|
||||
}
|
||||
|
||||
let res = self
|
||||
.client
|
||||
.get(self.url.as_ref())
|
||||
.send()
|
||||
.await?
|
||||
.json::<EtherscanResponse>()
|
||||
.await?;
|
||||
|
||||
match self.gas_category {
|
||||
GasCategory::SafeLow => Ok(U256::from(res.result.safe_gas_price * GWEI_TO_WEI)),
|
||||
GasCategory::Standard => Ok(U256::from(res.result.propose_gas_price * GWEI_TO_WEI)),
|
||||
_ => Err(GasOracleError::GasCategoryNotSupported),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
use ethers_core::types::U256;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use reqwest::Client;
|
||||
use serde::Deserialize;
|
||||
use url::Url;
|
||||
|
||||
use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError};
|
||||
|
||||
const GAS_NOW_URL: &str = "https://www.gasnow.org/api/v1/gas/price";
|
||||
|
||||
/// A client over HTTP for the [GasNow](https://www.gasnow.org/api/v1/gas/price) gas tracker API
|
||||
/// that implements the `GasOracle` trait
|
||||
#[derive(Debug)]
|
||||
pub struct GasNow {
|
||||
client: Client,
|
||||
url: Url,
|
||||
gas_category: GasCategory,
|
||||
}
|
||||
|
||||
impl Default for GasNow {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct GasNowResponse {
|
||||
data: GasNowResponseInner,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct GasNowResponseInner {
|
||||
#[serde(rename = "top50")]
|
||||
top_50: u64,
|
||||
#[serde(rename = "top200")]
|
||||
top_200: u64,
|
||||
#[serde(rename = "top400")]
|
||||
top_400: u64,
|
||||
}
|
||||
|
||||
impl GasNow {
|
||||
pub fn new() -> Self {
|
||||
let url = Url::parse(GAS_NOW_URL).expect("invalid url");
|
||||
|
||||
Self {
|
||||
client: Client::new(),
|
||||
url,
|
||||
gas_category: GasCategory::Standard,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn category(mut self, gas_category: GasCategory) -> Self {
|
||||
self.gas_category = gas_category;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl GasOracle for GasNow {
|
||||
async fn fetch(&self) -> Result<U256, GasOracleError> {
|
||||
let res = self
|
||||
.client
|
||||
.get(self.url.as_ref())
|
||||
.send()
|
||||
.await?
|
||||
.json::<GasNowResponse>()
|
||||
.await?;
|
||||
|
||||
let gas_price = match self.gas_category {
|
||||
GasCategory::SafeLow => U256::from(res.data.top_400),
|
||||
GasCategory::Standard => U256::from(res.data.top_200),
|
||||
_ => U256::from(res.data.top_50),
|
||||
};
|
||||
|
||||
Ok(gas_price)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
mod eth_gas_station;
|
||||
pub use eth_gas_station::EthGasStation;
|
||||
|
||||
mod etherchain;
|
||||
pub use etherchain::Etherchain;
|
||||
|
||||
mod etherscan;
|
||||
pub use etherscan::Etherscan;
|
||||
|
||||
mod gas_now;
|
||||
pub use gas_now::GasNow;
|
||||
|
||||
use ethers_core::types::U256;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use reqwest::Error as ReqwestError;
|
||||
use thiserror::Error;
|
||||
|
||||
const GWEI_TO_WEI: u64 = 1000000000;
|
||||
|
||||
/// Various gas price categories. Choose one of the available
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||
pub enum GasCategory {
|
||||
SafeLow,
|
||||
Standard,
|
||||
Fast,
|
||||
Fastest,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
/// Error thrown when fetching data from the `GasOracle`
|
||||
pub enum GasOracleError {
|
||||
/// An internal error in the HTTP request made from the underlying
|
||||
/// gas oracle
|
||||
#[error(transparent)]
|
||||
HttpClientError(#[from] ReqwestError),
|
||||
|
||||
/// An internal error thrown when the required gas category is not
|
||||
/// supported by the gas oracle API
|
||||
#[error("gas category not supported")]
|
||||
GasCategoryNotSupported,
|
||||
}
|
||||
|
||||
/// `GasOracle` is a trait that an underlying gas oracle needs to implement.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use ethers::providers::{
|
||||
/// gas_oracle::{EthGasStation, Etherscan, GasCategory, GasOracle},
|
||||
/// };
|
||||
///
|
||||
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let eth_gas_station_oracle = EthGasStation::new(Some("my-api-key"));
|
||||
/// let etherscan_oracle = EthGasStation::new(None).category(GasCategory::SafeLow);
|
||||
///
|
||||
/// let data_1 = eth_gas_station_oracle.fetch().await?;
|
||||
/// let data_2 = etherscan_oracle.fetch().await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[async_trait]
|
||||
pub trait GasOracle: Send + Sync + std::fmt::Debug {
|
||||
/// Makes an asynchronous HTTP query to the underlying `GasOracle`
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use ethers::providers::{
|
||||
/// gas_oracle::{Etherchain, GasCategory, GasOracle},
|
||||
/// };
|
||||
///
|
||||
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let etherchain_oracle = Etherchain::new().category(GasCategory::Fastest);
|
||||
/// let data = etherchain_oracle.fetch().await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
async fn fetch(&self) -> Result<U256, GasOracleError>;
|
||||
}
|
|
@ -107,6 +107,8 @@ mod provider;
|
|||
// ENS support
|
||||
mod ens;
|
||||
|
||||
pub mod gas_oracle;
|
||||
|
||||
mod pending_transaction;
|
||||
pub use pending_transaction::PendingTransaction;
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
#![allow(unused_braces)]
|
||||
use ethers::providers::{Http, Provider};
|
||||
use ethers::providers::{
|
||||
gas_oracle::{EthGasStation, Etherchain, Etherscan, GasCategory, GasNow, GasOracle},
|
||||
Http, Provider,
|
||||
};
|
||||
use std::{convert::TryFrom, time::Duration};
|
||||
|
||||
#[cfg(not(feature = "celo"))]
|
||||
|
@ -73,6 +76,36 @@ mod eth_tests {
|
|||
generic_pending_txs_test(provider).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gas_oracle() {
|
||||
// initialize and fetch gas estimates from EthGasStation
|
||||
let eth_gas_station_oracle = EthGasStation::new(None);
|
||||
let data_1 = eth_gas_station_oracle.fetch().await;
|
||||
assert!(data_1.is_ok());
|
||||
|
||||
// initialize and fetch gas estimates from Etherscan
|
||||
// since etherscan does not support `fastest` category, we expect an error
|
||||
let etherscan_oracle = Etherscan::new(None).category(GasCategory::Fastest);
|
||||
let data_2 = etherscan_oracle.fetch().await;
|
||||
assert!(data_2.is_err());
|
||||
|
||||
// but fetching the `standard` gas price should work fine
|
||||
let etherscan_oracle_2 = Etherscan::new(None).category(GasCategory::SafeLow);
|
||||
|
||||
let data_3 = etherscan_oracle_2.fetch().await;
|
||||
assert!(data_3.is_ok());
|
||||
|
||||
// initialize and fetch gas estimates from Etherchain
|
||||
let etherchain_oracle = Etherchain::new().category(GasCategory::Fast);
|
||||
let data_4 = etherchain_oracle.fetch().await;
|
||||
assert!(data_4.is_ok());
|
||||
|
||||
// initialize and fetch gas estimates from Etherchain
|
||||
let gas_now_oracle = GasNow::new().category(GasCategory::Fastest);
|
||||
let data_5 = gas_now_oracle.fetch().await;
|
||||
assert!(data_5.is_ok());
|
||||
}
|
||||
|
||||
async fn generic_pending_txs_test<P: JsonRpcClient>(provider: Provider<P>) {
|
||||
let accounts = provider.get_accounts().await.unwrap();
|
||||
|
||||
|
|
|
@ -3,13 +3,16 @@ use crate::Signer;
|
|||
use ethers_core::types::{
|
||||
Address, BlockNumber, Bytes, NameOrAddress, Signature, TransactionRequest, TxHash,
|
||||
};
|
||||
use ethers_providers::{JsonRpcClient, Provider, ProviderError};
|
||||
use ethers_providers::{
|
||||
gas_oracle::{GasOracle, GasOracleError},
|
||||
JsonRpcClient, Provider, ProviderError,
|
||||
};
|
||||
|
||||
use futures_util::{future::ok, join};
|
||||
use std::{future::Future, ops::Deref, time::Duration};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Debug)]
|
||||
/// A client provides an interface for signing and broadcasting locally signed transactions
|
||||
/// It Derefs to [`Provider`], which allows interacting with the Ethereum JSON-RPC provider
|
||||
/// via the same API. Sending transactions also supports using [ENS](https://ens.domains/) as a receiver. If you will
|
||||
|
@ -70,6 +73,7 @@ pub struct Client<P, S> {
|
|||
pub(crate) provider: Provider<P>,
|
||||
pub(crate) signer: Option<S>,
|
||||
pub(crate) address: Address,
|
||||
pub(crate) gas_oracle: Option<Box<dyn GasOracle>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
|
@ -79,6 +83,10 @@ pub enum ClientError {
|
|||
/// Throw when the call to the provider fails
|
||||
ProviderError(#[from] ProviderError),
|
||||
|
||||
#[error(transparent)]
|
||||
/// Throw when a call to the gas oracle fails
|
||||
GasOracleError(#[from] GasOracleError),
|
||||
|
||||
#[error(transparent)]
|
||||
/// Thrown when the internal call to the signer fails
|
||||
SignerError(#[from] Box<dyn std::error::Error + Send + Sync>),
|
||||
|
@ -91,8 +99,8 @@ pub enum ClientError {
|
|||
// Helper functions for locally signing transactions
|
||||
impl<P, S> Client<P, S>
|
||||
where
|
||||
S: Signer,
|
||||
P: JsonRpcClient,
|
||||
S: Signer,
|
||||
{
|
||||
/// Creates a new client from the provider and signer.
|
||||
pub fn new(provider: Provider<P>, signer: S) -> Self {
|
||||
|
@ -101,6 +109,7 @@ where
|
|||
provider,
|
||||
signer: Some(signer),
|
||||
address,
|
||||
gas_oracle: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -151,6 +160,13 @@ where
|
|||
tx.from = Some(self.address());
|
||||
}
|
||||
|
||||
// assign gas price if a gas oracle has been provided
|
||||
if let Some(gas_oracle) = &self.gas_oracle {
|
||||
if let Ok(gas_price) = gas_oracle.fetch().await {
|
||||
tx.gas_price = Some(gas_price);
|
||||
}
|
||||
}
|
||||
|
||||
// will poll and await the futures concurrently
|
||||
let (gas_price, gas, nonce) = join!(
|
||||
maybe(tx.gas_price, self.provider.get_gas_price()),
|
||||
|
@ -186,27 +202,19 @@ where
|
|||
/// calls.
|
||||
///
|
||||
/// Clones internally.
|
||||
pub fn with_signer(&self, signer: S) -> Self
|
||||
where
|
||||
P: Clone,
|
||||
{
|
||||
let mut this = self.clone();
|
||||
this.address = signer.address();
|
||||
this.signer = Some(signer);
|
||||
this
|
||||
pub fn with_signer(&mut self, signer: S) -> &Self {
|
||||
self.address = signer.address();
|
||||
self.signer = Some(signer);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the provider and returns a mutable reference to self so that it can be used in chained
|
||||
/// calls.
|
||||
///
|
||||
/// Clones internally.
|
||||
pub fn with_provider(&self, provider: Provider<P>) -> Self
|
||||
where
|
||||
P: Clone,
|
||||
{
|
||||
let mut this = self.clone();
|
||||
this.provider = provider;
|
||||
this
|
||||
pub fn with_provider(&mut self, provider: Provider<P>) -> &Self {
|
||||
self.provider = provider;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the address which will be used for interacting with the blockchain.
|
||||
|
@ -236,6 +244,12 @@ where
|
|||
self.provider = provider;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the gas oracle to query for gas estimates while broadcasting transactions
|
||||
pub fn gas_oracle(mut self, gas_oracle: Box<dyn GasOracle>) -> Self {
|
||||
self.gas_oracle = Some(gas_oracle);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Calls the future if `item` is None, otherwise returns a `futures::ok`
|
||||
|
@ -267,6 +281,7 @@ impl<P: JsonRpcClient, S> From<Provider<P>> for Client<P, S> {
|
|||
provider,
|
||||
signer: None,
|
||||
address: Address::zero(),
|
||||
gas_oracle: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -121,6 +121,7 @@ impl Wallet {
|
|||
address,
|
||||
signer: Some(self),
|
||||
provider,
|
||||
gas_oracle: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
use ethers::{
|
||||
providers::{Http, Provider},
|
||||
providers::{
|
||||
gas_oracle::{Etherchain, GasCategory, GasOracle},
|
||||
Http, Provider,
|
||||
},
|
||||
signers::Wallet,
|
||||
types::TransactionRequest,
|
||||
};
|
||||
|
@ -71,6 +74,36 @@ mod eth_tests {
|
|||
|
||||
assert!(balance_before > balance_after);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn using_gas_oracle() {
|
||||
let ganache = Ganache::new().spawn();
|
||||
|
||||
// this private key belongs to the above mnemonic
|
||||
let wallet: Wallet = ganache.keys()[0].clone().into();
|
||||
let wallet2: Wallet = ganache.keys()[1].clone().into();
|
||||
|
||||
// connect to the network
|
||||
let provider = Provider::<Http>::try_from(ganache.endpoint())
|
||||
.unwrap()
|
||||
.interval(Duration::from_millis(10u64));
|
||||
|
||||
// connect the wallet to the provider
|
||||
let client = wallet.connect(provider);
|
||||
|
||||
// assign a gas oracle to use
|
||||
let gas_oracle = Etherchain::new().category(GasCategory::Fastest);
|
||||
let expected_gas_price = gas_oracle.fetch().await.unwrap();
|
||||
|
||||
let client = client.gas_oracle(Box::new(gas_oracle));
|
||||
|
||||
// broadcast a transaction
|
||||
let tx = TransactionRequest::new().to(wallet2.address()).value(10000);
|
||||
let tx_hash = client.send_transaction(tx, None).await.unwrap();
|
||||
|
||||
let tx = client.get_transaction(tx_hash).await.unwrap();
|
||||
assert_eq!(tx.gas_price, expected_gas_price);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "celo")]
|
||||
|
|
Loading…
Reference in New Issue