feat: fee estimation with custom/default fn (#369)
* feat: fee estimation with custom/default fn * fix: clippy and percentage calc fix * calculate priority fee only if base fee above threshold * chore: some comments * fix: use legacy tx for ganache * test: test a few cases for fee estimation using utils
This commit is contained in:
parent
635236f061
commit
ad779c867f
|
@ -399,6 +399,7 @@ mod eth_tests {
|
||||||
let _receipt = contract
|
let _receipt = contract
|
||||||
.method::<_, H256>("setValue", "hi".to_owned())
|
.method::<_, H256>("setValue", "hi".to_owned())
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
.legacy()
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|
|
@ -36,6 +36,7 @@ pub use rlp;
|
||||||
use crate::types::{Address, Bytes, U256};
|
use crate::types::{Address, Bytes, U256};
|
||||||
use k256::{ecdsa::SigningKey, EncodedPoint as K256PublicKey};
|
use k256::{ecdsa::SigningKey, EncodedPoint as K256PublicKey};
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
|
use std::ops::Neg;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
|
@ -47,6 +48,19 @@ pub enum FormatBytes32StringError {
|
||||||
/// 1 Ether = 1e18 Wei == 0x0de0b6b3a7640000 Wei
|
/// 1 Ether = 1e18 Wei == 0x0de0b6b3a7640000 Wei
|
||||||
pub const WEI_IN_ETHER: U256 = U256([0x0de0b6b3a7640000, 0x0, 0x0, 0x0]);
|
pub const WEI_IN_ETHER: U256 = U256([0x0de0b6b3a7640000, 0x0, 0x0, 0x0]);
|
||||||
|
|
||||||
|
/// The number of blocks from the past for which the fee rewards are fetched for fee estimation.
|
||||||
|
pub const EIP1559_FEE_ESTIMATION_PAST_BLOCKS: u64 = 10;
|
||||||
|
/// The default percentile of gas premiums that are fetched for fee estimation.
|
||||||
|
pub const EIP1559_FEE_ESTIMATION_REWARD_PERCENTILE: f64 = 5.0;
|
||||||
|
/// The default max priority fee per gas, used in case the base fee is within a threshold.
|
||||||
|
pub const EIP1559_FEE_ESTIMATION_DEFAULT_PRIORITY_FEE: u64 = 3_000_000_000;
|
||||||
|
/// The threshold for base fee below which we use the default priority fee, and beyond which we
|
||||||
|
/// estimate an appropriate value for priority fee.
|
||||||
|
pub const EIP1559_FEE_ESTIMATION_PRIORITY_FEE_TRIGGER: u64 = 100_000_000_000;
|
||||||
|
/// The threshold max change/difference (in %) at which we will ignore the fee history values
|
||||||
|
/// under it.
|
||||||
|
pub const EIP1559_FEE_ESTIMATION_THRESHOLD_MAX_CHANGE: i64 = 200;
|
||||||
|
|
||||||
/// Format the output for the user which prefer to see values
|
/// Format the output for the user which prefer to see values
|
||||||
/// in ether (instead of wei)
|
/// in ether (instead of wei)
|
||||||
///
|
///
|
||||||
|
@ -187,6 +201,92 @@ pub fn parse_bytes32_string(bytes: &[u8; 32]) -> Result<&str, std::str::Utf8Erro
|
||||||
std::str::from_utf8(&bytes[..length])
|
std::str::from_utf8(&bytes[..length])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The default EIP-1559 fee estimator which is based on the work by [MyCrypto](https://github.com/MyCryptoHQ/MyCrypto/blob/master/src/services/ApiService/Gas/eip1559.ts)
|
||||||
|
pub fn eip1559_default_estimator(base_fee_per_gas: U256, rewards: Vec<Vec<U256>>) -> (U256, U256) {
|
||||||
|
let max_priority_fee_per_gas =
|
||||||
|
if base_fee_per_gas < U256::from(EIP1559_FEE_ESTIMATION_PRIORITY_FEE_TRIGGER) {
|
||||||
|
U256::from(EIP1559_FEE_ESTIMATION_DEFAULT_PRIORITY_FEE)
|
||||||
|
} else {
|
||||||
|
std::cmp::max(
|
||||||
|
estimate_priority_fee(rewards),
|
||||||
|
U256::from(EIP1559_FEE_ESTIMATION_DEFAULT_PRIORITY_FEE),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let potential_max_fee = base_fee_surged(base_fee_per_gas);
|
||||||
|
let max_fee_per_gas = if max_priority_fee_per_gas > potential_max_fee {
|
||||||
|
max_priority_fee_per_gas + potential_max_fee
|
||||||
|
} else {
|
||||||
|
potential_max_fee
|
||||||
|
};
|
||||||
|
(max_fee_per_gas, max_priority_fee_per_gas)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn estimate_priority_fee(rewards: Vec<Vec<U256>>) -> U256 {
|
||||||
|
let mut rewards: Vec<U256> = rewards
|
||||||
|
.iter()
|
||||||
|
.map(|r| r[0])
|
||||||
|
.filter(|r| *r > U256::zero())
|
||||||
|
.collect();
|
||||||
|
if rewards.is_empty() {
|
||||||
|
return U256::zero();
|
||||||
|
}
|
||||||
|
if rewards.len() == 1 {
|
||||||
|
return rewards[0];
|
||||||
|
}
|
||||||
|
// Sort the rewards as we will eventually take the median.
|
||||||
|
rewards.sort();
|
||||||
|
|
||||||
|
// A copy of the same vector is created for convenience to calculate percentage change
|
||||||
|
// between subsequent fee values.
|
||||||
|
let mut rewards_copy = rewards.clone();
|
||||||
|
rewards_copy.rotate_left(1);
|
||||||
|
|
||||||
|
let mut percentage_change: Vec<i64> = rewards
|
||||||
|
.iter()
|
||||||
|
.zip(rewards_copy.iter())
|
||||||
|
.map(|(a, b)| {
|
||||||
|
if b > a {
|
||||||
|
((b - a).low_u32() as i64 * 100) / (a.low_u32() as i64)
|
||||||
|
} else {
|
||||||
|
(((a - b).low_u32() as i64 * 100) / (a.low_u32() as i64)).neg()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
percentage_change.pop();
|
||||||
|
|
||||||
|
// Fetch the max of the percentage change, and that element's index.
|
||||||
|
let max_change = percentage_change.iter().max().unwrap();
|
||||||
|
let max_change_index = percentage_change
|
||||||
|
.iter()
|
||||||
|
.position(|&c| c == *max_change)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// If we encountered a big change in fees at a certain position, then consider only
|
||||||
|
// the values >= it.
|
||||||
|
let values = if *max_change >= EIP1559_FEE_ESTIMATION_THRESHOLD_MAX_CHANGE
|
||||||
|
&& (max_change_index >= (rewards.len() / 2))
|
||||||
|
{
|
||||||
|
rewards[max_change_index..].to_vec()
|
||||||
|
} else {
|
||||||
|
rewards
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return the median.
|
||||||
|
values[values.len() / 2]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_fee_surged(base_fee_per_gas: U256) -> U256 {
|
||||||
|
if base_fee_per_gas <= U256::from(40_000_000_000u64) {
|
||||||
|
base_fee_per_gas * 2
|
||||||
|
} else if base_fee_per_gas <= U256::from(100_000_000_000u64) {
|
||||||
|
base_fee_per_gas * 16 / 10
|
||||||
|
} else if base_fee_per_gas <= U256::from(200_000_000_000u64) {
|
||||||
|
base_fee_per_gas * 14 / 10
|
||||||
|
} else {
|
||||||
|
base_fee_per_gas * 12 / 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A bit of hack to find an unused TCP port.
|
/// A bit of hack to find an unused TCP port.
|
||||||
///
|
///
|
||||||
/// Does not guarantee that the given port is unused after the function exists, just that it was
|
/// Does not guarantee that the given port is unused after the function exists, just that it was
|
||||||
|
@ -451,4 +551,33 @@ mod tests {
|
||||||
FormatBytes32StringError::TextTooLong
|
FormatBytes32StringError::TextTooLong
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_eip1559_default_estimator() {
|
||||||
|
// If the base fee is below the triggering base fee, we should get the default priority fee
|
||||||
|
// with the base fee surged.
|
||||||
|
let base_fee_per_gas = U256::from(EIP1559_FEE_ESTIMATION_PRIORITY_FEE_TRIGGER) - 1;
|
||||||
|
let rewards: Vec<Vec<U256>> = vec![vec![]];
|
||||||
|
let (base_fee, priority_fee) = eip1559_default_estimator(base_fee_per_gas, rewards);
|
||||||
|
assert_eq!(
|
||||||
|
priority_fee,
|
||||||
|
U256::from(EIP1559_FEE_ESTIMATION_DEFAULT_PRIORITY_FEE)
|
||||||
|
);
|
||||||
|
assert_eq!(base_fee, base_fee_surged(base_fee_per_gas));
|
||||||
|
|
||||||
|
// If the base fee is above the triggering base fee, we calculate the priority fee using
|
||||||
|
// the fee history (rewards).
|
||||||
|
let base_fee_per_gas = U256::from(EIP1559_FEE_ESTIMATION_PRIORITY_FEE_TRIGGER) + 1;
|
||||||
|
let rewards: Vec<Vec<U256>> = vec![
|
||||||
|
vec![100_000_000_000u64.into()],
|
||||||
|
vec![105_000_000_000u64.into()],
|
||||||
|
vec![102_000_000_000u64.into()],
|
||||||
|
]; // say, last 3 blocks
|
||||||
|
let (base_fee, priority_fee) = eip1559_default_estimator(base_fee_per_gas, rewards.clone());
|
||||||
|
assert_eq!(base_fee, base_fee_surged(base_fee_per_gas));
|
||||||
|
assert_eq!(priority_fee, estimate_priority_fee(rewards.clone()));
|
||||||
|
|
||||||
|
// The median should be taken because none of the changes are big enough to ignore values.
|
||||||
|
assert_eq!(estimate_priority_fee(rewards), 102_000_000_000u64.into());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,4 +106,8 @@ impl GasOracle for EthGasStation {
|
||||||
|
|
||||||
Ok(gas_price)
|
Ok(gas_price)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
|
||||||
|
Err(GasOracleError::Eip1559EstimationNotSupported)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,4 +77,8 @@ impl GasOracle for Etherchain {
|
||||||
|
|
||||||
Ok(gas_price)
|
Ok(gas_price)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
|
||||||
|
Err(GasOracleError::Eip1559EstimationNotSupported)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,4 +108,8 @@ impl GasOracle for Etherscan {
|
||||||
_ => Err(GasOracleError::GasCategoryNotSupported),
|
_ => Err(GasOracleError::GasCategoryNotSupported),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
|
||||||
|
Err(GasOracleError::Eip1559EstimationNotSupported)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,4 +80,8 @@ impl GasOracle for GasNow {
|
||||||
|
|
||||||
Ok(gas_price)
|
Ok(gas_price)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
|
||||||
|
Err(GasOracleError::Eip1559EstimationNotSupported)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,6 +59,13 @@ where
|
||||||
Ok(self.gas_oracle.fetch().await?)
|
Ok(self.gas_oracle.fetch().await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn estimate_eip1559_fees(
|
||||||
|
&self,
|
||||||
|
_: Option<fn(U256, Vec<Vec<U256>>) -> (U256, U256)>,
|
||||||
|
) -> Result<(U256, U256), Self::Error> {
|
||||||
|
Ok(self.gas_oracle.estimate_eip1559_fees().await?)
|
||||||
|
}
|
||||||
|
|
||||||
async fn send_transaction<T: Into<TypedTransaction> + Send + Sync>(
|
async fn send_transaction<T: Into<TypedTransaction> + Send + Sync>(
|
||||||
&self,
|
&self,
|
||||||
tx: T,
|
tx: T,
|
||||||
|
@ -77,8 +84,17 @@ where
|
||||||
inner.tx.gas_price = Some(self.get_gas_price().await?);
|
inner.tx.gas_price = Some(self.get_gas_price().await?);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TypedTransaction::Eip1559(_) => {
|
TypedTransaction::Eip1559(ref mut inner) => {
|
||||||
return Err(MiddlewareError::UnsupportedTxType);
|
if inner.max_priority_fee_per_gas.is_none() || inner.max_fee_per_gas.is_none() {
|
||||||
|
let (max_fee_per_gas, max_priority_fee_per_gas) =
|
||||||
|
self.estimate_eip1559_fees(None).await?;
|
||||||
|
if inner.max_priority_fee_per_gas.is_none() {
|
||||||
|
inner.max_priority_fee_per_gas = Some(max_priority_fee_per_gas);
|
||||||
|
}
|
||||||
|
if inner.max_fee_per_gas.is_none() {
|
||||||
|
inner.max_fee_per_gas = Some(max_fee_per_gas);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
self.inner
|
self.inner
|
||||||
|
|
|
@ -42,6 +42,9 @@ pub enum GasOracleError {
|
||||||
/// supported by the gas oracle API
|
/// supported by the gas oracle API
|
||||||
#[error("gas category not supported")]
|
#[error("gas category not supported")]
|
||||||
GasCategoryNotSupported,
|
GasCategoryNotSupported,
|
||||||
|
|
||||||
|
#[error("EIP-1559 gas estimation not supported")]
|
||||||
|
Eip1559EstimationNotSupported,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `GasOracle` is a trait that an underlying gas oracle needs to implement.
|
/// `GasOracle` is a trait that an underlying gas oracle needs to implement.
|
||||||
|
@ -80,4 +83,6 @@ pub trait GasOracle: Send + Sync + std::fmt::Debug {
|
||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
async fn fetch(&self) -> Result<U256, GasOracleError>;
|
async fn fetch(&self) -> Result<U256, GasOracleError>;
|
||||||
|
|
||||||
|
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -265,15 +265,19 @@ pub trait Middleware: Sync + Send + Debug {
|
||||||
inner.from = self.default_sender();
|
inner.from = self.default_sender();
|
||||||
}
|
}
|
||||||
|
|
||||||
let (max_priority_fee_per_gas, max_fee_per_gas, gas) = futures_util::try_join!(
|
let gas = maybe(inner.gas, self.estimate_gas(&tx_clone)).await?;
|
||||||
// TODO: Replace with algorithms using eth_feeHistory
|
|
||||||
maybe(inner.max_priority_fee_per_gas, self.get_gas_price()),
|
|
||||||
maybe(inner.max_fee_per_gas, self.get_gas_price()),
|
|
||||||
maybe(inner.gas, self.estimate_gas(&tx_clone)),
|
|
||||||
)?;
|
|
||||||
inner.gas = Some(gas);
|
inner.gas = Some(gas);
|
||||||
inner.max_fee_per_gas = Some(max_fee_per_gas);
|
|
||||||
inner.max_priority_fee_per_gas = Some(max_priority_fee_per_gas);
|
if inner.max_fee_per_gas.is_none() || inner.max_priority_fee_per_gas.is_none() {
|
||||||
|
let (max_fee_per_gas, max_priority_fee_per_gas) =
|
||||||
|
self.estimate_eip1559_fees(None).await?;
|
||||||
|
if inner.max_fee_per_gas.is_none() {
|
||||||
|
inner.max_fee_per_gas = Some(max_fee_per_gas);
|
||||||
|
}
|
||||||
|
if inner.max_priority_fee_per_gas.is_none() {
|
||||||
|
inner.max_priority_fee_per_gas = Some(max_priority_fee_per_gas);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -401,6 +405,16 @@ pub trait Middleware: Sync + Send + Debug {
|
||||||
self.inner().get_gas_price().await.map_err(FromErr::from)
|
self.inner().get_gas_price().await.map_err(FromErr::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn estimate_eip1559_fees(
|
||||||
|
&self,
|
||||||
|
estimator: Option<fn(U256, Vec<Vec<U256>>) -> (U256, U256)>,
|
||||||
|
) -> Result<(U256, U256), Self::Error> {
|
||||||
|
self.inner()
|
||||||
|
.estimate_eip1559_fees(estimator)
|
||||||
|
.await
|
||||||
|
.map_err(FromErr::from)
|
||||||
|
}
|
||||||
|
|
||||||
async fn get_accounts(&self) -> Result<Vec<Address>, Self::Error> {
|
async fn get_accounts(&self) -> Result<Vec<Address>, Self::Error> {
|
||||||
self.inner().get_accounts().await.map_err(FromErr::from)
|
self.inner().get_accounts().await.map_err(FromErr::from)
|
||||||
}
|
}
|
||||||
|
|
|
@ -261,6 +261,37 @@ impl<P: JsonRpcClient> Middleware for Provider<P> {
|
||||||
self.request("eth_gasPrice", ()).await
|
self.request("eth_gasPrice", ()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets a heuristic recommendation of max fee per gas and max priority fee per gas for
|
||||||
|
/// EIP-1559 compatible transactions.
|
||||||
|
async fn estimate_eip1559_fees(
|
||||||
|
&self,
|
||||||
|
estimator: Option<fn(U256, Vec<Vec<U256>>) -> (U256, U256)>,
|
||||||
|
) -> Result<(U256, U256), Self::Error> {
|
||||||
|
let base_fee_per_gas = self
|
||||||
|
.get_block(BlockNumber::Latest)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| ProviderError::CustomError("Latest block not found".into()))?
|
||||||
|
.base_fee_per_gas
|
||||||
|
.ok_or_else(|| ProviderError::CustomError("EIP-1559 not activated".into()))?;
|
||||||
|
|
||||||
|
let fee_history = self
|
||||||
|
.fee_history(
|
||||||
|
utils::EIP1559_FEE_ESTIMATION_PAST_BLOCKS,
|
||||||
|
BlockNumber::Latest,
|
||||||
|
&[utils::EIP1559_FEE_ESTIMATION_REWARD_PERCENTILE],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// use the provided fee estimator function, or fallback to the default implementation.
|
||||||
|
let (max_fee_per_gas, max_priority_fee_per_gas) = if let Some(es) = estimator {
|
||||||
|
es(base_fee_per_gas, fee_history.reward)
|
||||||
|
} else {
|
||||||
|
utils::eip1559_default_estimator(base_fee_per_gas, fee_history.reward)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((max_fee_per_gas, max_priority_fee_per_gas))
|
||||||
|
}
|
||||||
|
|
||||||
/// Gets the accounts on the node
|
/// Gets the accounts on the node
|
||||||
async fn get_accounts(&self) -> Result<Vec<Address>, ProviderError> {
|
async fn get_accounts(&self) -> Result<Vec<Address>, ProviderError> {
|
||||||
self.request("eth_accounts", ()).await
|
self.request("eth_accounts", ()).await
|
||||||
|
|
|
@ -199,6 +199,17 @@ mod eth_tests {
|
||||||
.into();
|
.into();
|
||||||
check_tx(&provider, tx, 2).await;
|
check_tx(&provider, tx, 2).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn eip1559_fee_estimation() {
|
||||||
|
let provider = Provider::<Http>::try_from(
|
||||||
|
"https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let (_max_fee_per_gas, _max_priority_fee_per_gas) =
|
||||||
|
provider.estimate_eip1559_fees(None).await.unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "celo")]
|
#[cfg(feature = "celo")]
|
||||||
|
|
Loading…
Reference in New Issue