From 4c2a9a673aed4f5b194d056fa2f5598a81c8411c Mon Sep 17 00:00:00 2001 From: Remco Bloemen Date: Wed, 4 May 2022 12:11:31 -0700 Subject: [PATCH 01/13] Pass reqwest Client to constructors --- .../src/gas_oracle/blocknative.rs | 24 +++++++++++-------- .../src/gas_oracle/eth_gas_station.rs | 21 +++++++++------- .../src/gas_oracle/etherchain.rs | 16 ++++++------- ethers-middleware/src/gas_oracle/gas_now.rs | 16 ++++++------- ethers-middleware/tests/gas_oracle.rs | 4 ++-- 5 files changed, 44 insertions(+), 37 deletions(-) diff --git a/ethers-middleware/src/gas_oracle/blocknative.rs b/ethers-middleware/src/gas_oracle/blocknative.rs index a7afb77cb..4cfaa9c8d 100644 --- a/ethers-middleware/src/gas_oracle/blocknative.rs +++ b/ethers-middleware/src/gas_oracle/blocknative.rs @@ -1,12 +1,9 @@ use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError, GWEI_TO_WEI}; use async_trait::async_trait; use ethers_core::types::U256; -use reqwest::{ - header::{HeaderMap, HeaderValue, AUTHORIZATION}, - Client, ClientBuilder, -}; +use reqwest::{header::AUTHORIZATION, Client}; use serde::Deserialize; -use std::{collections::HashMap, convert::TryInto, iter::FromIterator}; +use std::{collections::HashMap, convert::TryInto}; use url::Url; const BLOCKNATIVE_GAS_PRICE_ENDPOINT: &str = "https://api.blocknative.com/gasprices/blockprices"; @@ -26,6 +23,7 @@ fn gas_category_to_confidence(gas_category: &GasCategory) -> u64 { pub struct BlockNative { client: Client, url: Url, + api_key: String, gas_category: GasCategory, } @@ -85,12 +83,10 @@ pub struct BaseFeeEstimate { impl BlockNative { /// Creates a new [BlockNative](https://www.blocknative.com/gas-estimator) gas oracle - pub fn new(api_key: &str) -> Self { - let header_value = HeaderValue::from_str(api_key).unwrap(); - let headers = HeaderMap::from_iter([(AUTHORIZATION, header_value)]); - let client = ClientBuilder::new().default_headers(headers).build().unwrap(); + pub fn new(client: Client, api_key: String) -> Self { Self { client, + api_key, url: BLOCKNATIVE_GAS_PRICE_ENDPOINT.try_into().unwrap(), gas_category: GasCategory::Standard, } @@ -105,7 +101,15 @@ impl BlockNative { /// Perform request to Blocknative, decode response pub async fn request(&self) -> Result { - Ok(self.client.get(self.url.as_ref()).send().await?.error_for_status()?.json().await?) + self.client + .get(self.url.as_ref()) + .header(AUTHORIZATION, &self.api_key) + .send() + .await? + .error_for_status()? + .json() + .await + .map_err(GasOracleError::HttpClientError) } } diff --git a/ethers-middleware/src/gas_oracle/eth_gas_station.rs b/ethers-middleware/src/gas_oracle/eth_gas_station.rs index 6e2510c5a..3d40c9bd1 100644 --- a/ethers-middleware/src/gas_oracle/eth_gas_station.rs +++ b/ethers-middleware/src/gas_oracle/eth_gas_station.rs @@ -61,15 +61,12 @@ pub struct EthGasStationResponse { impl EthGasStation { /// Creates a new [EthGasStation](https://docs.ethgasstation.info/) gas oracle - 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 new(client: Client, api_key: Option<&'static str>) -> Self { + let mut url = Url::parse(ETH_GAS_STATION_URL_PREFIX).expect("invalid url"); + if let Some(key) = api_key { + url.query_pairs_mut().append_pair("api-key", key); + } + EthGasStation { client, url, gas_category: GasCategory::Standard } } /// Sets the gas price category to be used when fetching the gas price. @@ -84,6 +81,12 @@ impl EthGasStation { } } +impl Default for EthGasStation { + fn default() -> Self { + Self::new(Client::new(), None) + } +} + #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl GasOracle for EthGasStation { diff --git a/ethers-middleware/src/gas_oracle/etherchain.rs b/ethers-middleware/src/gas_oracle/etherchain.rs index f74187a8c..76d5ecb92 100644 --- a/ethers-middleware/src/gas_oracle/etherchain.rs +++ b/ethers-middleware/src/gas_oracle/etherchain.rs @@ -18,12 +18,6 @@ pub struct Etherchain { gas_category: GasCategory, } -impl Default for Etherchain { - fn default() -> Self { - Self::new() - } -} - #[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd)] #[serde(rename_all = "camelCase")] pub struct EtherchainResponse { @@ -37,10 +31,10 @@ pub struct EtherchainResponse { impl Etherchain { /// Creates a new [Etherchain](https://etherchain.org/tools/gasPriceOracle) gas price oracle. - pub fn new() -> Self { + pub fn new(client: Client) -> Self { let url = Url::parse(ETHERCHAIN_URL).expect("invalid url"); - Etherchain { client: Client::new(), url, gas_category: GasCategory::Standard } + Etherchain { client, url, gas_category: GasCategory::Standard } } /// Sets the gas price category to be used when fetching the gas price. @@ -55,6 +49,12 @@ impl Etherchain { } } +impl Default for Etherchain { + fn default() -> Self { + Self::new(Client::new()) + } +} + #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl GasOracle for Etherchain { diff --git a/ethers-middleware/src/gas_oracle/gas_now.rs b/ethers-middleware/src/gas_oracle/gas_now.rs index 77e4b6aaa..b99e42fef 100644 --- a/ethers-middleware/src/gas_oracle/gas_now.rs +++ b/ethers-middleware/src/gas_oracle/gas_now.rs @@ -18,12 +18,6 @@ pub struct GasNow { gas_category: GasCategory, } -impl Default for GasNow { - fn default() -> Self { - Self::new() - } -} - #[derive(Deserialize)] struct GasNowResponseWrapper { data: GasNowResponse, @@ -39,10 +33,10 @@ pub struct GasNowResponse { impl GasNow { /// Creates a new [GasNow](https://gasnow.org) gas price oracle. - pub fn new() -> Self { + pub fn new(client: Client) -> Self { let url = Url::parse(GAS_NOW_URL).expect("invalid url"); - Self { client: Client::new(), url, gas_category: GasCategory::Standard } + Self { url, gas_category: GasCategory::Standard } } /// Sets the gas price category to be used when fetching the gas price. @@ -63,6 +57,12 @@ impl GasNow { } } +impl Default for GasNow { + fn default() -> Self { + Self::new(Client::new()) + } +} + #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl GasOracle for GasNow { diff --git a/ethers-middleware/tests/gas_oracle.rs b/ethers-middleware/tests/gas_oracle.rs index c58f9e282..5967fa2d9 100644 --- a/ethers-middleware/tests/gas_oracle.rs +++ b/ethers-middleware/tests/gas_oracle.rs @@ -57,7 +57,7 @@ async fn using_gas_oracle() { #[tokio::test] async fn eth_gas_station() { // initialize and fetch gas estimates from EthGasStation - let eth_gas_station_oracle = EthGasStation::new(None); + let eth_gas_station_oracle = EthGasStation::default(); let data = eth_gas_station_oracle.fetch().await; assert!(data.is_ok()); } @@ -83,7 +83,7 @@ async fn etherscan() { #[tokio::test] async fn etherchain() { // initialize and fetch gas estimates from Etherchain - let etherchain_oracle = Etherchain::new().category(GasCategory::Fast); + let etherchain_oracle = Etherchain::default().category(GasCategory::Fast); let data = etherchain_oracle.fetch().await; assert!(data.is_ok()); } From ee877cda22dc0f4ae13ed827c8f0323211a7bead Mon Sep 17 00:00:00 2001 From: Remco Bloemen Date: Wed, 4 May 2022 13:09:57 -0700 Subject: [PATCH 02/13] Add Median oracle aggregator --- ethers-middleware/src/gas_oracle/median.rs | 85 ++++++++++++++++++++++ ethers-middleware/src/gas_oracle/mod.rs | 6 ++ 2 files changed, 91 insertions(+) create mode 100644 ethers-middleware/src/gas_oracle/median.rs diff --git a/ethers-middleware/src/gas_oracle/median.rs b/ethers-middleware/src/gas_oracle/median.rs new file mode 100644 index 000000000..3d31c7dd0 --- /dev/null +++ b/ethers-middleware/src/gas_oracle/median.rs @@ -0,0 +1,85 @@ +use crate::gas_oracle::{GasOracle, GasOracleError}; +use async_trait::async_trait; +use ethers_core::types::U256; +use futures_util::future::join_all; +use std::fmt::Debug; +use tracing::warn; + +#[derive(Debug)] +pub struct Median<'a> { + oracles: Vec>, +} + +/// Computes the median gas price from a selection of oracles. +/// +/// Don't forget to set a timeout on the source oracles. By default +/// the reqwest based oracles will never time out. +impl<'a> Median<'a> { + pub fn new(oracles: Vec>) -> Self { + Self { oracles } + } + + pub fn add(&mut self, oracle: T) { + self.oracles.push(Box::new(oracle)); + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl GasOracle for Median<'_> { + async fn fetch(&self) -> Result { + // Process the oracles in parallel + let futures = self.oracles.iter().map(|oracle| oracle.fetch()); + let results = join_all(futures).await; + + // Filter out any errors + let values = self.oracles.iter().zip(results).filter_map(|(oracle, result)| match result { + Ok(value) => Some(value), + Err(err) => { + warn!("Failed to fetch gas price from {:?}: {}", oracle, err); + None + } + }); + let mut values = values.collect::>(); + if values.is_empty() { + return Err(GasOracleError::NoValues) + } + + // Sort the values and return the median + values.sort(); + Ok(values[values.len() / 2]) + } + + async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> { + // Process the oracles in parallel + let futures = self.oracles.iter().map(|oracle| oracle.estimate_eip1559_fees()); + let results = join_all(futures).await; + + // Filter out any errors + let values = self.oracles.iter().zip(results).filter_map(|(oracle, result)| match result { + Ok(value) => Some(value), + Err(err) => { + warn!("Failed to fetch gas price from {:?}: {}", oracle, err); + None + } + }); + let mut max_fee_per_gas = Vec::with_capacity(self.oracles.len()); + let mut max_priority_fee_per_gas = Vec::with_capacity(self.oracles.len()); + for (fee, priority) in values { + max_fee_per_gas.push(fee); + max_priority_fee_per_gas.push(priority); + } + assert_eq!(max_fee_per_gas.len(), max_priority_fee_per_gas.len()); + if max_fee_per_gas.is_empty() { + return Err(GasOracleError::NoValues) + } + + // Sort the values and return the median + max_fee_per_gas.sort(); + max_priority_fee_per_gas.sort(); + Ok(( + max_fee_per_gas[max_fee_per_gas.len() / 2], + max_priority_fee_per_gas[max_priority_fee_per_gas.len() / 2], + )) + } +} diff --git a/ethers-middleware/src/gas_oracle/mod.rs b/ethers-middleware/src/gas_oracle/mod.rs index 59170a39b..b861ae16d 100644 --- a/ethers-middleware/src/gas_oracle/mod.rs +++ b/ethers-middleware/src/gas_oracle/mod.rs @@ -13,6 +13,9 @@ pub use etherscan::Etherscan; mod middleware; pub use middleware::{GasOracleMiddleware, MiddlewareError}; +mod median; +pub use median::Median; + use ethers_core::types::U256; use async_trait::async_trait; @@ -58,6 +61,9 @@ pub enum GasOracleError { #[error("EIP-1559 gas estimation not supported")] Eip1559EstimationNotSupported, + + #[error("None of the oracles returned a value")] + NoValues, } /// `GasOracle` is a trait that an underlying gas oracle needs to implement. From b263daa74b043554d10f29613a23e1a0246309bd Mon Sep 17 00:00:00 2001 From: Remco Bloemen Date: Wed, 4 May 2022 14:20:49 -0700 Subject: [PATCH 03/13] DRY --- ethers-middleware/src/gas_oracle/median.rs | 43 ++++++++++------------ 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/ethers-middleware/src/gas_oracle/median.rs b/ethers-middleware/src/gas_oracle/median.rs index 3d31c7dd0..6a7fa4175 100644 --- a/ethers-middleware/src/gas_oracle/median.rs +++ b/ethers-middleware/src/gas_oracle/median.rs @@ -2,9 +2,11 @@ use crate::gas_oracle::{GasOracle, GasOracleError}; use async_trait::async_trait; use ethers_core::types::U256; use futures_util::future::join_all; -use std::fmt::Debug; +use std::{fmt::Debug, future::Future}; use tracing::warn; +// TODO: Weighted median + #[derive(Debug)] pub struct Median<'a> { oracles: Vec>, @@ -22,14 +24,14 @@ impl<'a> Median<'a> { pub fn add(&mut self, oracle: T) { self.oracles.push(Box::new(oracle)); } -} -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl GasOracle for Median<'_> { - async fn fetch(&self) -> Result { + pub async fn query_all(&'a self, mut f: Fn) -> Result, GasOracleError> + where + Fn: FnMut(&'a dyn GasOracle) -> Fut, + Fut: Future>, + { // Process the oracles in parallel - let futures = self.oracles.iter().map(|oracle| oracle.fetch()); + let futures = self.oracles.iter().map(|oracle| f(oracle.as_ref())); let results = join_all(futures).await; // Filter out any errors @@ -40,29 +42,25 @@ impl GasOracle for Median<'_> { None } }); - let mut values = values.collect::>(); + let values = values.collect::>(); if values.is_empty() { return Err(GasOracleError::NoValues) } + Ok(values) + } +} - // Sort the values and return the median +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl GasOracle for Median<'_> { + async fn fetch(&self) -> Result { + let mut values = self.query_all(|oracle| oracle.fetch()).await?; values.sort(); Ok(values[values.len() / 2]) } async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> { - // Process the oracles in parallel - let futures = self.oracles.iter().map(|oracle| oracle.estimate_eip1559_fees()); - let results = join_all(futures).await; - - // Filter out any errors - let values = self.oracles.iter().zip(results).filter_map(|(oracle, result)| match result { - Ok(value) => Some(value), - Err(err) => { - warn!("Failed to fetch gas price from {:?}: {}", oracle, err); - None - } - }); + let values = self.query_all(|oracle| oracle.estimate_eip1559_fees()).await?; let mut max_fee_per_gas = Vec::with_capacity(self.oracles.len()); let mut max_priority_fee_per_gas = Vec::with_capacity(self.oracles.len()); for (fee, priority) in values { @@ -70,9 +68,6 @@ impl GasOracle for Median<'_> { max_priority_fee_per_gas.push(priority); } assert_eq!(max_fee_per_gas.len(), max_priority_fee_per_gas.len()); - if max_fee_per_gas.is_empty() { - return Err(GasOracleError::NoValues) - } // Sort the values and return the median max_fee_per_gas.sort(); From bb90c03d8127efb1387d2b6c69291404bda50c9f Mon Sep 17 00:00:00 2001 From: Remco Bloemen Date: Wed, 4 May 2022 18:40:05 -0700 Subject: [PATCH 04/13] Weighted median --- ethers-middleware/src/gas_oracle/median.rs | 106 ++++++++++++++------- 1 file changed, 74 insertions(+), 32 deletions(-) diff --git a/ethers-middleware/src/gas_oracle/median.rs b/ethers-middleware/src/gas_oracle/median.rs index 6a7fa4175..7a089aa88 100644 --- a/ethers-middleware/src/gas_oracle/median.rs +++ b/ethers-middleware/src/gas_oracle/median.rs @@ -5,11 +5,9 @@ use futures_util::future::join_all; use std::{fmt::Debug, future::Future}; use tracing::warn; -// TODO: Weighted median - -#[derive(Debug)] +#[derive(Default, Debug)] pub struct Median<'a> { - oracles: Vec>, + oracles: Vec<(f32, Box)>, } /// Computes the median gas price from a selection of oracles. @@ -17,31 +15,39 @@ pub struct Median<'a> { /// Don't forget to set a timeout on the source oracles. By default /// the reqwest based oracles will never time out. impl<'a> Median<'a> { - pub fn new(oracles: Vec>) -> Self { - Self { oracles } + pub fn new() -> Self { + Self::default() } pub fn add(&mut self, oracle: T) { - self.oracles.push(Box::new(oracle)); + self.add_weighted(1.0, oracle) + } + + pub fn add_weighted(&mut self, weight: f32, oracle: T) { + assert!(weight > 0.0); + self.oracles.push((weight, Box::new(oracle))); } - pub async fn query_all(&'a self, mut f: Fn) -> Result, GasOracleError> + pub async fn query_all(&'a self, mut f: Fn) -> Result, GasOracleError> where Fn: FnMut(&'a dyn GasOracle) -> Fut, Fut: Future>, { // 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())); let results = join_all(futures).await; // Filter out any errors - let values = self.oracles.iter().zip(results).filter_map(|(oracle, result)| match result { - Ok(value) => Some(value), - Err(err) => { - warn!("Failed to fetch gas price from {:?}: {}", oracle, err); - None - } - }); + let values = + self.oracles.iter().zip(results).filter_map( + |((weight, oracle), result)| match result { + Ok(value) => Some((*weight, value)), + Err(err) => { + warn!("Failed to fetch gas price from {:?}: {}", oracle, err); + None + } + }, + ); let values = values.collect::>(); if values.is_empty() { return Err(GasOracleError::NoValues) @@ -55,26 +61,62 @@ impl<'a> Median<'a> { impl GasOracle for Median<'_> { async fn fetch(&self) -> Result { let mut values = self.query_all(|oracle| oracle.fetch()).await?; - values.sort(); - Ok(values[values.len() / 2]) + // `query_all` guarantees `values` is not empty + Ok(*weighted_fractile_by_key(0.5, &mut values, |fee| fee).unwrap()) } async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> { - let values = self.query_all(|oracle| oracle.estimate_eip1559_fees()).await?; - let mut max_fee_per_gas = Vec::with_capacity(self.oracles.len()); - let mut max_priority_fee_per_gas = Vec::with_capacity(self.oracles.len()); - for (fee, priority) in values { - max_fee_per_gas.push(fee); - max_priority_fee_per_gas.push(priority); - } - assert_eq!(max_fee_per_gas.len(), max_priority_fee_per_gas.len()); - - // Sort the values and return the median - max_fee_per_gas.sort(); - max_priority_fee_per_gas.sort(); + let mut values = self.query_all(|oracle| oracle.estimate_eip1559_fees()).await?; + // `query_all` guarantees `values` is not empty Ok(( - max_fee_per_gas[max_fee_per_gas.len() / 2], - max_priority_fee_per_gas[max_priority_fee_per_gas.len() / 2], + weighted_fractile_by_key(0.5, &mut values, |(max_fee, _)| max_fee).unwrap().0, + weighted_fractile_by_key(0.5, &mut values, |(_, priority_fee)| priority_fee).unwrap().1, )) } } + +/// Weighted fractile by key +/// +/// Sort the values in place by key and return the weighted fractile value such +/// that `fractile` fraction of the values by weight are less than or equal to +/// the value. +/// +/// Returns None if the values are empty. +/// +/// Note: it doesn't handle NaNs or other special float values. +/// +/// See +/// +/// # Panics +/// +/// Panics if [`fractile`] is not in the range $[0, 1]$. +fn weighted_fractile_by_key<'a, T, F, K>( + fractile: f32, + values: &'a mut [(f32, T)], + mut key: F, +) -> Option<&'a T> +where + F: for<'b> FnMut(&'b T) -> &'b K, + K: Ord, +{ + assert!((0.0..=1.0).contains(&fractile)); + if values.is_empty() { + return None + } + let weight_rank = fractile * values.iter().map(|(weight, _)| *weight).sum::(); + values.sort_unstable_by(|a, b| key(&a.1).cmp(key(&b.1))); + let mut cumulative_weight = 0.0_f32; + for (weight, value) in values.iter() { + cumulative_weight += *weight; + if cumulative_weight >= weight_rank { + return Some(value) + } + } + // By the last element, cumulative_weight == weight_rank and we should have + // returned already. Assume there is a slight rounding error causing + // cumulative_weight to be slightly less than expected. In this case the last + // element is appropriate. This is not exactly right, since the last + // elements may have zero weight. + // `values` is not empty. + Some(&values.last().unwrap().1) +} From 150a71e7fcf434ca957a940145fb9bd2bce8e140 Mon Sep 17 00:00:00 2001 From: Remco Bloemen Date: Wed, 4 May 2022 20:16:40 -0700 Subject: [PATCH 05/13] Add cache layer --- Cargo.lock | 12 ++++ ethers-middleware/Cargo.toml | 1 + ethers-middleware/src/gas_oracle/cache.rs | 70 +++++++++++++++++++++++ ethers-middleware/src/gas_oracle/mod.rs | 3 + 4 files changed, 86 insertions(+) create mode 100644 ethers-middleware/src/gas_oracle/cache.rs diff --git a/Cargo.lock b/Cargo.lock index 6909794e6..3d5770f6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1292,6 +1292,7 @@ dependencies = [ "ethers-providers", "ethers-signers", "ethers-solc", + "futures-locks", "futures-util", "hex", "instant", @@ -1588,6 +1589,17 @@ version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" +[[package]] +name = "futures-locks" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb42d4fb72227be5778429f9ef5240a38a358925a49f05b5cf702ce7c7e558a" +dependencies = [ + "futures-channel", + "futures-task", + "tokio", +] + [[package]] name = "futures-macro" version = "0.3.21" diff --git a/ethers-middleware/Cargo.toml b/ethers-middleware/Cargo.toml index 5cf39a1df..e16d26ff7 100644 --- a/ethers-middleware/Cargo.toml +++ b/ethers-middleware/Cargo.toml @@ -24,6 +24,7 @@ async-trait = { version = "0.1.50", default-features = false } serde = { version = "1.0.124", default-features = false, features = ["derive"] } thiserror = { version = "1.0.31", default-features = false } futures-util = { version = "^0.3" } +futures-locks = { version = "0.7" } tracing = { version = "0.1.34", default-features = false } tracing-futures = { version = "0.2.5", default-features = false } diff --git a/ethers-middleware/src/gas_oracle/cache.rs b/ethers-middleware/src/gas_oracle/cache.rs new file mode 100644 index 000000000..6f8b162b6 --- /dev/null +++ b/ethers-middleware/src/gas_oracle/cache.rs @@ -0,0 +1,70 @@ +use crate::gas_oracle::{GasOracle, GasOracleError}; +use async_trait::async_trait; +use ethers_core::types::U256; +use futures_locks::RwLock; +use std::{ + fmt::Debug, + future::Future, + time::{Duration, Instant}, +}; + +#[derive(Debug)] +pub struct Cache { + inner: T, + validity: Duration, + fee: Cached, + eip1559: Cached<(U256, U256)>, +} + +#[derive(Default, Debug)] +struct Cached(RwLock>); + +impl Cached { + async fn get(&self, validity: Duration, fetch: F) -> Result + where + F: FnOnce() -> Fut, + Fut: Future>, + { + // Try with a read lock + { + let lock = self.0.read().await; + if let Some((last_fetch, value)) = lock.as_ref() { + if Instant::now().duration_since(*last_fetch) < validity { + return Ok(value.clone()) + } + } + } + // Acquire a write lock + { + let mut lock = self.0.write().await; + // Check again, a concurrent thread may have raced us to the write. + if let Some((last_fetch, value)) = lock.as_ref() { + if Instant::now().duration_since(*last_fetch) < validity { + return Ok(value.clone()) + } + } + // Set a fresh value + let value = fetch().await?; + *lock = Some((Instant::now(), value.clone())); + Ok(value) + } + } +} + +impl Cache { + 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 GasOracle for Cache { + async fn fetch(&self) -> Result { + 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 + } +} diff --git a/ethers-middleware/src/gas_oracle/mod.rs b/ethers-middleware/src/gas_oracle/mod.rs index b861ae16d..a87e35d67 100644 --- a/ethers-middleware/src/gas_oracle/mod.rs +++ b/ethers-middleware/src/gas_oracle/mod.rs @@ -16,6 +16,9 @@ pub use middleware::{GasOracleMiddleware, MiddlewareError}; mod median; pub use median::Median; +mod cache; +pub use cache::Cache; + use ethers_core::types::U256; use async_trait::async_trait; From 9471a6ac6656a187f8c43c53751ff6ff2f58d94e Mon Sep 17 00:00:00 2001 From: Remco Bloemen Date: Wed, 4 May 2022 20:57:34 -0700 Subject: [PATCH 06/13] Simplify lifetimes --- .../src/gas_oracle/eth_gas_station.rs | 2 +- ethers-middleware/src/gas_oracle/median.rs | 21 +++++++++++-------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/ethers-middleware/src/gas_oracle/eth_gas_station.rs b/ethers-middleware/src/gas_oracle/eth_gas_station.rs index 3d40c9bd1..34892ab12 100644 --- a/ethers-middleware/src/gas_oracle/eth_gas_station.rs +++ b/ethers-middleware/src/gas_oracle/eth_gas_station.rs @@ -61,7 +61,7 @@ pub struct EthGasStationResponse { impl EthGasStation { /// Creates a new [EthGasStation](https://docs.ethgasstation.info/) gas oracle - pub fn new(client: Client, api_key: Option<&'static str>) -> Self { + pub fn new(client: Client, api_key: Option<&str>) -> Self { let mut url = Url::parse(ETH_GAS_STATION_URL_PREFIX).expect("invalid url"); if let Some(key) = api_key { url.query_pairs_mut().append_pair("api-key", key); diff --git a/ethers-middleware/src/gas_oracle/median.rs b/ethers-middleware/src/gas_oracle/median.rs index 7a089aa88..2149ce41e 100644 --- a/ethers-middleware/src/gas_oracle/median.rs +++ b/ethers-middleware/src/gas_oracle/median.rs @@ -6,29 +6,32 @@ use std::{fmt::Debug, future::Future}; use tracing::warn; #[derive(Default, Debug)] -pub struct Median<'a> { - oracles: Vec<(f32, Box)>, +pub struct Median { + oracles: Vec<(f32, Box)>, } /// Computes the median gas price from a selection of oracles. /// /// Don't forget to set a timeout on the source oracles. By default /// the reqwest based oracles will never time out. -impl<'a> Median<'a> { +impl Median { pub fn new() -> Self { Self::default() } - pub fn add(&mut self, oracle: T) { + pub fn add(&mut self, oracle: T) { self.add_weighted(1.0, oracle) } - pub fn add_weighted(&mut self, weight: f32, oracle: T) { + pub fn add_weighted(&mut self, weight: f32, oracle: T) { assert!(weight > 0.0); self.oracles.push((weight, Box::new(oracle))); } - pub async fn query_all(&'a self, mut f: Fn) -> Result, GasOracleError> + pub async fn query_all<'a, Fn, Fut, O>( + &'a self, + mut f: Fn, + ) -> Result, GasOracleError> where Fn: FnMut(&'a dyn GasOracle) -> Fut, Fut: Future>, @@ -58,7 +61,7 @@ impl<'a> Median<'a> { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl GasOracle for Median<'_> { +impl GasOracle for Median { async fn fetch(&self) -> Result { let mut values = self.query_all(|oracle| oracle.fetch()).await?; // `query_all` guarantees `values` is not empty @@ -115,8 +118,8 @@ where // By the last element, cumulative_weight == weight_rank and we should have // returned already. Assume there is a slight rounding error causing // cumulative_weight to be slightly less than expected. In this case the last - // element is appropriate. This is not exactly right, since the last - // elements may have zero weight. + // element is appropriate. (This is not exactly right, since the last + // elements may have zero weight.) // `values` is not empty. Some(&values.last().unwrap().1) } From 366d46da0da7ccee9ee527f20e666e5872d881b1 Mon Sep 17 00:00:00 2001 From: Remco Bloemen Date: Wed, 4 May 2022 21:07:50 -0700 Subject: [PATCH 07/13] Add with_client constructors --- ethers-middleware/src/gas_oracle/blocknative.rs | 9 +++++++-- ethers-middleware/src/gas_oracle/eth_gas_station.rs | 9 +++++++-- ethers-middleware/src/gas_oracle/etherchain.rs | 9 +++++++-- ethers-middleware/src/gas_oracle/gas_now.rs | 7 ++++++- 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/ethers-middleware/src/gas_oracle/blocknative.rs b/ethers-middleware/src/gas_oracle/blocknative.rs index 4cfaa9c8d..11832c5a5 100644 --- a/ethers-middleware/src/gas_oracle/blocknative.rs +++ b/ethers-middleware/src/gas_oracle/blocknative.rs @@ -82,8 +82,13 @@ pub struct BaseFeeEstimate { } impl BlockNative { - /// Creates a new [BlockNative](https://www.blocknative.com/gas-estimator) gas oracle - pub fn new(client: Client, api_key: String) -> Self { + /// 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, diff --git a/ethers-middleware/src/gas_oracle/eth_gas_station.rs b/ethers-middleware/src/gas_oracle/eth_gas_station.rs index 34892ab12..683caf374 100644 --- a/ethers-middleware/src/gas_oracle/eth_gas_station.rs +++ b/ethers-middleware/src/gas_oracle/eth_gas_station.rs @@ -61,7 +61,12 @@ pub struct EthGasStationResponse { impl EthGasStation { /// Creates a new [EthGasStation](https://docs.ethgasstation.info/) gas oracle - pub fn new(client: Client, api_key: Option<&str>) -> Self { + pub fn new(api_key: Option<&str>) -> Self { + Self::with_client(Client::new(), api_key) + } + + /// Same as [`Self::new`] but with a custom [`Client`]. + pub fn with_client(client: Client, api_key: Option<&str>) -> Self { let mut url = Url::parse(ETH_GAS_STATION_URL_PREFIX).expect("invalid url"); if let Some(key) = api_key { url.query_pairs_mut().append_pair("api-key", key); @@ -83,7 +88,7 @@ impl EthGasStation { impl Default for EthGasStation { fn default() -> Self { - Self::new(Client::new(), None) + Self::new(None) } } diff --git a/ethers-middleware/src/gas_oracle/etherchain.rs b/ethers-middleware/src/gas_oracle/etherchain.rs index 76d5ecb92..3fc94e6a6 100644 --- a/ethers-middleware/src/gas_oracle/etherchain.rs +++ b/ethers-middleware/src/gas_oracle/etherchain.rs @@ -31,7 +31,12 @@ pub struct EtherchainResponse { impl Etherchain { /// Creates a new [Etherchain](https://etherchain.org/tools/gasPriceOracle) gas price oracle. - pub fn new(client: Client) -> Self { + pub fn new() -> Self { + Self::with_client(Client::new()) + } + + /// Same as [`Self::new`] but with a custom [`Client`]. + pub fn with_client(client: Client) -> Self { let url = Url::parse(ETHERCHAIN_URL).expect("invalid url"); Etherchain { client, url, gas_category: GasCategory::Standard } @@ -51,7 +56,7 @@ impl Etherchain { impl Default for Etherchain { fn default() -> Self { - Self::new(Client::new()) + Self::new() } } diff --git a/ethers-middleware/src/gas_oracle/gas_now.rs b/ethers-middleware/src/gas_oracle/gas_now.rs index b99e42fef..cf652064f 100644 --- a/ethers-middleware/src/gas_oracle/gas_now.rs +++ b/ethers-middleware/src/gas_oracle/gas_now.rs @@ -33,7 +33,12 @@ pub struct GasNowResponse { impl GasNow { /// Creates a new [GasNow](https://gasnow.org) gas price oracle. - pub fn new(client: Client) -> Self { + 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(GAS_NOW_URL).expect("invalid url"); Self { url, gas_category: GasCategory::Standard } From 25b071f4801abfcb0f2038429089c10d111c6e84 Mon Sep 17 00:00:00 2001 From: Remco Bloemen Date: Wed, 4 May 2022 21:16:59 -0700 Subject: [PATCH 08/13] Update GasNow urls --- ethers-middleware/src/gas_oracle/gas_now.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ethers-middleware/src/gas_oracle/gas_now.rs b/ethers-middleware/src/gas_oracle/gas_now.rs index cf652064f..a1444ff10 100644 --- a/ethers-middleware/src/gas_oracle/gas_now.rs +++ b/ethers-middleware/src/gas_oracle/gas_now.rs @@ -7,9 +7,9 @@ use url::Url; use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError}; -const GAS_NOW_URL: &str = "https://www.gasnow.org/api/v3/gas/price"; +const GAS_NOW_URL: &str = "https://etherchain.org/api/gasnow"; -/// A client over HTTP for the [GasNow](https://www.gasnow.org/api/v1/gas/price) gas tracker API +/// A client over HTTP for the [Etherchain GasNow](https://etherchain.org/tools/gasnow) gas tracker API /// that implements the `GasOracle` trait #[derive(Clone, Debug)] pub struct GasNow { @@ -32,7 +32,7 @@ pub struct GasNowResponse { } impl GasNow { - /// Creates a new [GasNow](https://gasnow.org) gas price oracle. + /// Creates a new [Etherchain GasNow](https://etherchain.org/tools/gasnow) gas price oracle. pub fn new() -> Self { Self::with_client(Client::new()) } From 879948989a217e9a151fd9212e53796a3fd2c348 Mon Sep 17 00:00:00 2001 From: Remco Bloemen Date: Thu, 5 May 2022 11:41:36 -0700 Subject: [PATCH 09/13] Add u256_from_f64_saturating --- ethers-core/src/types/mod.rs | 3 + ethers-core/src/types/u256.rs | 100 ++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 ethers-core/src/types/u256.rs diff --git a/ethers-core/src/types/mod.rs b/ethers-core/src/types/mod.rs index 4c3b8a122..b19273fff 100644 --- a/ethers-core/src/types/mod.rs +++ b/ethers-core/src/types/mod.rs @@ -21,6 +21,9 @@ pub use address_or_bytes::AddressOrBytes; mod path_or_string; pub use path_or_string::PathOrString; +mod u256; +pub use u256::*; + mod i256; pub use i256::{Sign, I256}; diff --git a/ethers-core/src/types/u256.rs b/ethers-core/src/types/u256.rs new file mode 100644 index 000000000..4b91ede10 --- /dev/null +++ b/ethers-core/src/types/u256.rs @@ -0,0 +1,100 @@ +use ethabi::ethereum_types::U256; + +/// Convert a floating point value to it's nearest f64 integer. +/// +/// It is saturating, so values $\ge 2^{256}$ will be rounded +/// to [`U245::max_value()`] and values $< 0$ to zero. This includes +/// positive and negative infinity. +/// +/// TODO: Move to ethabi::ethereum_types::U256. +/// TODO: Add [`I256`] version. +/// +/// # Panics +/// +/// Panics if [`f`] is NaN. +pub fn u256_from_f64_saturating(mut f: f64) -> U256 { + if f.is_nan() { + panic!("NaN is not a valid value for U256"); + } + if f < 0.5 { + return U256::zero() + } + if f >= 1.157_920_892_373_162e77_f64 { + return U256::max_value() + } + // All non-normal cases should have been handled above + assert!(f.is_normal()); + // Turn nearest rounding into truncated rounding + f += 0.5; + + // Parse IEEE-754 double into + // sign should be zero, exponent should be >= 0. + let bits = f.to_bits(); + let sign = bits >> 63; + assert!(sign == 0); + let biased_exponent = (bits >> 52) & 0x7ff; + assert!(biased_exponent >= 1023); + let exponent = biased_exponent - 1023; + let fraction = bits & 0xfffffffffffff; + let mantissa = 0x10000000000000 | fraction; + if exponent >= (256 - 52) { + U256::max_value() + } else if exponent < 52 { + // Truncate mantissa + U256([mantissa, 0, 0, 0]) >> (52 - exponent) + } else { + U256([mantissa, 0, 0, 0]) << (exponent - 52) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::f64; + + #[test] + fn test_small_integers() { + for i in 0..=255 { + let f = i as f64; + let u = u256_from_f64_saturating(f); + assert_eq!(u, U256::from(i)); + } + } + + #[test] + fn test_small_integers_round_down() { + for i in 0..=255 { + let f = (i as f64) + 0.499; + let u = u256_from_f64_saturating(f); + assert_eq!(u, U256::from(i)); + } + } + + #[test] + fn test_small_integers_round_up() { + for i in 0..=255 { + let f = (i as f64) - 0.5; + let u = u256_from_f64_saturating(f); + assert_eq!(u, U256::from(i)); + } + } + + #[test] + fn test_infinities() { + assert_eq!(u256_from_f64_saturating(f64::INFINITY), U256::max_value()); + assert_eq!(u256_from_f64_saturating(f64::NEG_INFINITY), U256::zero()); + } + + #[test] + fn test_saturating() { + assert_eq!(u256_from_f64_saturating(-1.0), U256::zero()); + assert_eq!(u256_from_f64_saturating(1e90_f64), U256::max_value()); + } + + #[test] + fn test_large() { + // Check with e.g. `python3 -c 'print(int(1.0e36))'` + assert_eq!(u256_from_f64_saturating(1.0e36_f64), U256::from_dec_str("1000000000000000042420637374017961984").unwrap()); + assert_eq!(u256_from_f64_saturating(f64::consts::PI * 2.0e60_f64), U256::from_dec_str("6283185307179586084560863929317662625677330590403879287914496").unwrap()); + } +} From 27da5e2de3154537e401c2b22385f7878ac54f7d Mon Sep 17 00:00:00 2001 From: Remco Bloemen Date: Thu, 5 May 2022 12:14:32 -0700 Subject: [PATCH 10/13] Add polygon oracle --- ethers-core/src/types/u256.rs | 11 ++- ethers-middleware/src/gas_oracle/mod.rs | 6 ++ ethers-middleware/src/gas_oracle/polygon.rs | 91 +++++++++++++++++++++ 3 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 ethers-middleware/src/gas_oracle/polygon.rs diff --git a/ethers-core/src/types/u256.rs b/ethers-core/src/types/u256.rs index 4b91ede10..0f3e0b3ce 100644 --- a/ethers-core/src/types/u256.rs +++ b/ethers-core/src/types/u256.rs @@ -94,7 +94,14 @@ mod tests { #[test] fn test_large() { // Check with e.g. `python3 -c 'print(int(1.0e36))'` - assert_eq!(u256_from_f64_saturating(1.0e36_f64), U256::from_dec_str("1000000000000000042420637374017961984").unwrap()); - assert_eq!(u256_from_f64_saturating(f64::consts::PI * 2.0e60_f64), U256::from_dec_str("6283185307179586084560863929317662625677330590403879287914496").unwrap()); + assert_eq!( + u256_from_f64_saturating(1.0e36_f64), + U256::from_dec_str("1000000000000000042420637374017961984").unwrap() + ); + assert_eq!( + u256_from_f64_saturating(f64::consts::PI * 2.0e60_f64), + U256::from_dec_str("6283185307179586084560863929317662625677330590403879287914496") + .unwrap() + ); } } diff --git a/ethers-middleware/src/gas_oracle/mod.rs b/ethers-middleware/src/gas_oracle/mod.rs index a87e35d67..155c277cb 100644 --- a/ethers-middleware/src/gas_oracle/mod.rs +++ b/ethers-middleware/src/gas_oracle/mod.rs @@ -19,6 +19,9 @@ pub use median::Median; mod cache; pub use cache::Cache; +mod polygon; +pub use polygon::Polygon; + use ethers_core::types::U256; use async_trait::async_trait; @@ -67,6 +70,9 @@ pub enum GasOracleError { #[error("None of the oracles returned a value")] NoValues, + + #[error("Chain is not supported by the oracle")] + UnsupportedChain, } /// `GasOracle` is a trait that an underlying gas oracle needs to implement. diff --git a/ethers-middleware/src/gas_oracle/polygon.rs b/ethers-middleware/src/gas_oracle/polygon.rs new file mode 100644 index 000000000..a450a3ab9 --- /dev/null +++ b/ethers-middleware/src/gas_oracle/polygon.rs @@ -0,0 +1,91 @@ +use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError}; +use async_trait::async_trait; +use ethers_core::types::{u256_from_f64_saturating, Chain, U256}; +use reqwest::Client; +use serde::Deserialize; +use url::Url; + +const GAS_PRICE_ENDPOINT: &str = "https://gasstation-mainnet.matic.network/v2"; +const MUMBAI_GAS_PRICE_ENDPOINT: &str = "https://gasstation-mumbai.matic.today/v2"; + +/// The [Polygon](https://docs.polygon.technology/docs/develop/tools/polygon-gas-station/) gas station API +/// Queries over HTTP and implements the `GasOracle` trait +#[derive(Clone, Debug)] +pub struct Polygon { + client: Client, + url: Url, + gas_category: GasCategory, +} + +/// The response from the Polygon gas station API. +/// Gas prices are in Gwei. +#[derive(Debug, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Response { + estimated_base_fee: f64, + safe_low: GasEstimate, + standard: GasEstimate, + fast: GasEstimate, +} + +#[derive(Clone, Copy, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct GasEstimate { + max_priority_fee: f64, + max_fee: f64, +} + +impl Polygon { + pub fn new(chain: Chain) -> Result { + Self::with_client(Client::new(), chain) + } + + pub fn with_client(client: Client, chain: Chain) -> Result { + // TODO: Sniff chain from chain id. + let url = match chain { + Chain::Polygon => Url::parse(GAS_PRICE_ENDPOINT).unwrap(), + Chain::PolygonMumbai => Url::parse(MUMBAI_GAS_PRICE_ENDPOINT).unwrap(), + _ => return Err(GasOracleError::UnsupportedChain), + }; + Ok(Self { client, url, gas_category: GasCategory::Standard }) + } + + /// Sets the gas price category to be used when fetching the gas price. + #[must_use] + pub fn category(mut self, gas_category: GasCategory) -> Self { + self.gas_category = gas_category; + self + } + + /// Perform request to Blocknative, decode response + pub async fn request(&self) -> Result<(f64, GasEstimate), GasOracleError> { + let response: Response = + self.client.get(self.url.as_ref()).send().await?.error_for_status()?.json().await?; + let estimate = match self.gas_category { + GasCategory::SafeLow => response.safe_low, + GasCategory::Standard => response.standard, + GasCategory::Fast => response.fast, + GasCategory::Fastest => response.fast, + }; + Ok((response.estimated_base_fee, estimate)) + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl GasOracle for Polygon { + async fn fetch(&self) -> Result { + let (base_fee, estimate) = self.request().await?; + let fee = base_fee + estimate.max_priority_fee; + Ok(from_gwei(fee)) + } + + async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> { + let (_, estimate) = self.request().await?; + Ok((from_gwei(estimate.max_fee), from_gwei(estimate.max_priority_fee))) + } +} + +fn from_gwei(gwei: f64) -> U256 { + u256_from_f64_saturating(gwei * 1.0e18_f64) +} From d2c558742c709ee466faf67de69163ec408ff839 Mon Sep 17 00:00:00 2001 From: Remco Bloemen Date: Thu, 5 May 2022 14:56:30 -0700 Subject: [PATCH 11/13] Fixes --- ethers-core/src/types/u256.rs | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/ethers-core/src/types/u256.rs b/ethers-core/src/types/u256.rs index 0f3e0b3ce..18d2f37b1 100644 --- a/ethers-core/src/types/u256.rs +++ b/ethers-core/src/types/u256.rs @@ -1,17 +1,17 @@ use ethabi::ethereum_types::U256; -/// Convert a floating point value to it's nearest f64 integer. +/// Convert a floating point value to its nearest f64 integer. /// /// It is saturating, so values $\ge 2^{256}$ will be rounded -/// to [`U245::max_value()`] and values $< 0$ to zero. This includes +/// to [`U256::max_value()`] and values $< 0$ to zero. This includes /// positive and negative infinity. /// /// TODO: Move to ethabi::ethereum_types::U256. -/// TODO: Add [`I256`] version. +/// TODO: Add [`super::I256`] version. /// /// # Panics /// -/// Panics if [`f`] is NaN. +/// Panics if `f` is NaN. pub fn u256_from_f64_saturating(mut f: f64) -> U256 { if f.is_nan() { panic!("NaN is not a valid value for U256"); @@ -27,8 +27,8 @@ pub fn u256_from_f64_saturating(mut f: f64) -> U256 { // Turn nearest rounding into truncated rounding f += 0.5; - // Parse IEEE-754 double into - // sign should be zero, exponent should be >= 0. + // Parse IEEE-754 double into U256 + // Sign should be zero, exponent should be >= 0. let bits = f.to_bits(); let sign = bits >> 63; assert!(sign == 0); @@ -37,12 +37,15 @@ pub fn u256_from_f64_saturating(mut f: f64) -> U256 { let exponent = biased_exponent - 1023; let fraction = bits & 0xfffffffffffff; let mantissa = 0x10000000000000 | fraction; - if exponent >= (256 - 52) { + if exponent > 255 { + dbg!(); U256::max_value() } else if exponent < 52 { + dbg!(); // Truncate mantissa U256([mantissa, 0, 0, 0]) >> (52 - exponent) } else { + dbg!(); U256([mantissa, 0, 0, 0]) << (exponent - 52) } } @@ -103,5 +106,16 @@ mod tests { U256::from_dec_str("6283185307179586084560863929317662625677330590403879287914496") .unwrap() ); + assert_eq!( + u256_from_f64_saturating(5.78960446186581e76_f64), + U256::from_dec_str("57896044618658097711785492504343953926634992332820282019728792003956564819968") + .unwrap() + ); + assert_eq!( + u256_from_f64_saturating(1.157920892373161e77_f64), + U256::from_dec_str("115792089237316105435040506505232477503392813560534822796089932171514352762880") + .unwrap() + ); + } } From 6243a043ccb6a53beacb86def1737236e45fa39f Mon Sep 17 00:00:00 2001 From: Remco Bloemen Date: Thu, 5 May 2022 15:11:41 -0700 Subject: [PATCH 12/13] Fix lints --- ethers-core/src/types/u256.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/ethers-core/src/types/u256.rs b/ethers-core/src/types/u256.rs index 18d2f37b1..6d5974f52 100644 --- a/ethers-core/src/types/u256.rs +++ b/ethers-core/src/types/u256.rs @@ -108,14 +108,17 @@ mod tests { ); assert_eq!( u256_from_f64_saturating(5.78960446186581e76_f64), - U256::from_dec_str("57896044618658097711785492504343953926634992332820282019728792003956564819968") - .unwrap() + U256::from_dec_str( + "57896044618658097711785492504343953926634992332820282019728792003956564819968" + ) + .unwrap() ); assert_eq!( u256_from_f64_saturating(1.157920892373161e77_f64), - U256::from_dec_str("115792089237316105435040506505232477503392813560534822796089932171514352762880") - .unwrap() + U256::from_dec_str( + "115792089237316105435040506505232477503392813560534822796089932171514352762880" + ) + .unwrap() ); - } } From 83e8cbbbaae7d920ffd91c9b59c3db4f5fa10177 Mon Sep 17 00:00:00 2001 From: Remco Bloemen Date: Thu, 5 May 2022 15:36:20 -0700 Subject: [PATCH 13/13] Remove dbg statements --- ethers-core/src/types/u256.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/ethers-core/src/types/u256.rs b/ethers-core/src/types/u256.rs index 6d5974f52..6d3187ffa 100644 --- a/ethers-core/src/types/u256.rs +++ b/ethers-core/src/types/u256.rs @@ -38,14 +38,11 @@ pub fn u256_from_f64_saturating(mut f: f64) -> U256 { let fraction = bits & 0xfffffffffffff; let mantissa = 0x10000000000000 | fraction; if exponent > 255 { - dbg!(); U256::max_value() } else if exponent < 52 { - dbg!(); // Truncate mantissa U256([mantissa, 0, 0, 0]) >> (52 - exponent) } else { - dbg!(); U256([mantissa, 0, 0, 0]) << (exponent - 52) } }