mirror of
https://github.com/paradigmxyz/reth.git
synced 2026-01-29 09:08:05 -05:00
feat: implement eth_feeHistory (#2083)
Co-authored-by: Dan Cline <6798349+Rjected@users.noreply.github.com> Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
This commit is contained in:
@@ -278,6 +278,43 @@ impl Transaction {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: dedup with effective_tip_per_gas
|
||||
/// Determine the effective gas limit for the given transaction and base fee.
|
||||
/// If the base fee is `None`, the `max_priority_fee_per_gas`, or gas price for non-EIP1559
|
||||
/// transactions is returned.
|
||||
///
|
||||
/// If the `max_fee_per_gas` is less than the base fee, `None` returned.
|
||||
pub fn effective_gas_price(&self, base_fee: Option<u64>) -> Option<u128> {
|
||||
if let Some(base_fee) = base_fee {
|
||||
let max_fee_per_gas = self.max_fee_per_gas();
|
||||
if max_fee_per_gas < base_fee as u128 {
|
||||
None
|
||||
} else {
|
||||
let effective_max_fee = max_fee_per_gas - base_fee as u128;
|
||||
Some(std::cmp::min(effective_max_fee, self.priority_fee_or_price()))
|
||||
}
|
||||
} else {
|
||||
Some(self.priority_fee_or_price())
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the max priority fee per gas if the transaction is an EIP-1559 transaction, and
|
||||
/// otherwise return the gas price.
|
||||
///
|
||||
/// # Warning
|
||||
///
|
||||
/// This is different than the `max_priority_fee_per_gas` method, which returns `None` for
|
||||
/// non-EIP-1559 transactions.
|
||||
pub(crate) fn priority_fee_or_price(&self) -> u128 {
|
||||
match self {
|
||||
Transaction::Legacy(TxLegacy { gas_price, .. }) |
|
||||
Transaction::Eip2930(TxEip2930 { gas_price, .. }) => *gas_price,
|
||||
Transaction::Eip1559(TxEip1559 { max_priority_fee_per_gas, .. }) => {
|
||||
*max_priority_fee_per_gas
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the effective miner gas tip cap (`gasTipCap`) for the given base fee.
|
||||
///
|
||||
/// Returns `None` if the basefee is higher than the [Transaction::max_fee_per_gas].
|
||||
|
||||
@@ -4,6 +4,33 @@ use serde::{Deserialize, Serialize};
|
||||
use std::{num::NonZeroUsize, sync::Arc};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
/// Internal struct to calculate reward percentiles
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TxGasAndReward {
|
||||
/// gas used by a block
|
||||
pub gas_used: u128,
|
||||
/// minimum between max_priority_fee_per_gas or max_fee_per_gas - base_fee_for_block
|
||||
pub reward: u128,
|
||||
}
|
||||
|
||||
impl PartialOrd for TxGasAndReward {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
// compare only the reward
|
||||
// see:
|
||||
// <https://github.com/ethereum/go-ethereum/blob/ee8e83fa5f6cb261dad2ed0a7bbcde4930c41e6c/eth/gasprice/feehistory.go#L85>
|
||||
self.reward.partial_cmp(&other.reward)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for TxGasAndReward {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
// compare only the reward
|
||||
// see:
|
||||
// <https://github.com/ethereum/go-ethereum/blob/ee8e83fa5f6cb261dad2ed0a7bbcde4930c41e6c/eth/gasprice/feehistory.go#L85>
|
||||
self.reward.cmp(&other.reward)
|
||||
}
|
||||
}
|
||||
|
||||
/// Response type for `eth_feeHistory`
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
||||
@@ -19,7 +19,7 @@ mod work;
|
||||
pub use account::*;
|
||||
pub use block::*;
|
||||
pub use call::CallRequest;
|
||||
pub use fee::{FeeHistory, FeeHistoryCache, FeeHistoryCacheItem};
|
||||
pub use fee::{FeeHistory, FeeHistoryCache, FeeHistoryCacheItem, TxGasAndReward};
|
||||
pub use filter::*;
|
||||
pub use index::Index;
|
||||
pub use log::Log;
|
||||
|
||||
188
crates/rpc/rpc/src/eth/api/fees.rs
Normal file
188
crates/rpc/rpc/src/eth/api/fees.rs
Normal file
@@ -0,0 +1,188 @@
|
||||
//! Contains RPC handler implementations for fee history.
|
||||
|
||||
use crate::{
|
||||
eth::error::{EthApiError, EthResult, InvalidTransactionError},
|
||||
EthApi,
|
||||
};
|
||||
use reth_network_api::NetworkInfo;
|
||||
use reth_primitives::{BlockId, U256};
|
||||
use reth_provider::{BlockProvider, EvmEnvProvider, StateProviderFactory};
|
||||
use reth_rpc_types::{FeeHistory, FeeHistoryCacheItem, TxGasAndReward};
|
||||
use reth_transaction_pool::TransactionPool;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
impl<Client, Pool, Network> EthApi<Client, Pool, Network>
|
||||
where
|
||||
Pool: TransactionPool + Clone + 'static,
|
||||
Client: BlockProvider + StateProviderFactory + EvmEnvProvider + 'static,
|
||||
Network: NetworkInfo + Send + Sync + 'static,
|
||||
{
|
||||
/// Reports the fee history, for the given amount of blocks, up until the newest block
|
||||
/// provided.
|
||||
pub(crate) async fn fee_history(
|
||||
&self,
|
||||
block_count: u64,
|
||||
newest_block: BlockId,
|
||||
reward_percentiles: Option<Vec<f64>>,
|
||||
) -> EthResult<FeeHistory> {
|
||||
if block_count == 0 {
|
||||
return Ok(FeeHistory::default())
|
||||
}
|
||||
|
||||
let Some(previous_to_end_block) = self.inner.client.block_number_for_id(newest_block)? else { return Err(EthApiError::UnknownBlockNumber)};
|
||||
let end_block = previous_to_end_block + 1;
|
||||
|
||||
if end_block < block_count {
|
||||
return Err(EthApiError::InvalidBlockRange)
|
||||
}
|
||||
|
||||
let mut start_block = end_block - block_count;
|
||||
|
||||
if block_count == 1 {
|
||||
start_block = previous_to_end_block;
|
||||
}
|
||||
|
||||
// if not provided the percentiles are []
|
||||
let reward_percentiles = reward_percentiles.unwrap_or_default();
|
||||
|
||||
// checks for rewardPercentile's sorted-ness
|
||||
// check if any of rewardPercentile is greater than 100
|
||||
// pre 1559 blocks, return 0 for baseFeePerGas
|
||||
for window in reward_percentiles.windows(2) {
|
||||
if window[0] >= window[1] {
|
||||
return Err(EthApiError::InvalidRewardPercentile(window[1]))
|
||||
}
|
||||
|
||||
if window[0] < 0.0 || window[0] > 100.0 {
|
||||
return Err(EthApiError::InvalidRewardPercentile(window[0]))
|
||||
}
|
||||
}
|
||||
|
||||
let mut fee_history_cache = self.fee_history_cache.0.lock().await;
|
||||
|
||||
// Sorted map that's populated in two rounds:
|
||||
// 1. Cache entries until first non-cached block
|
||||
// 2. Database query from the first non-cached block
|
||||
let mut fee_history_cache_items = BTreeMap::new();
|
||||
|
||||
let mut first_non_cached_block = None;
|
||||
let mut last_non_cached_block = None;
|
||||
for block in start_block..=end_block {
|
||||
// Check if block exists in cache, and move it to the head of the list if so
|
||||
if let Some(fee_history_cache_item) = fee_history_cache.get(&block) {
|
||||
fee_history_cache_items.insert(block, fee_history_cache_item.clone());
|
||||
} else {
|
||||
// If block doesn't exist in cache, set it as a first non-cached block to query it
|
||||
// from the database
|
||||
first_non_cached_block.get_or_insert(block);
|
||||
// And last non-cached block, so we could query the database until we reach it
|
||||
last_non_cached_block = Some(block);
|
||||
}
|
||||
}
|
||||
|
||||
// If we had any cache misses, query the database starting with the first non-cached block
|
||||
// and ending with the last
|
||||
if let (Some(start_block), Some(end_block)) =
|
||||
(first_non_cached_block, last_non_cached_block)
|
||||
{
|
||||
let header_range = start_block..=end_block;
|
||||
|
||||
let headers = self.inner.client.headers_range(header_range.clone())?;
|
||||
let transactions_by_block =
|
||||
self.inner.client.transactions_by_block_range(header_range)?;
|
||||
|
||||
let header_tx = headers.iter().zip(&transactions_by_block);
|
||||
|
||||
// We should receive exactly the amount of blocks missing from the cache
|
||||
if headers.len() != (end_block - start_block + 1) as usize {
|
||||
return Err(EthApiError::InvalidBlockRange)
|
||||
}
|
||||
|
||||
// We should receive exactly the amount of blocks missing from the cache
|
||||
if transactions_by_block.len() != (end_block - start_block + 1) as usize {
|
||||
return Err(EthApiError::InvalidBlockRange)
|
||||
}
|
||||
|
||||
for (header, transactions) in header_tx {
|
||||
let base_fee_per_gas: U256 = header.base_fee_per_gas.
|
||||
unwrap_or_default(). // Zero for pre-EIP-1559 blocks
|
||||
try_into().unwrap(); // u64 -> U256 won't fail
|
||||
let gas_used_ratio = header.gas_used as f64 / header.gas_limit as f64;
|
||||
|
||||
let mut sorter = Vec::with_capacity(transactions.len());
|
||||
for transaction in transactions.iter() {
|
||||
let reward = transaction
|
||||
.effective_gas_price(header.base_fee_per_gas)
|
||||
.ok_or(InvalidTransactionError::FeeCapTooLow)?;
|
||||
|
||||
sorter.push(TxGasAndReward { gas_used: header.gas_used as u128, reward })
|
||||
}
|
||||
|
||||
sorter.sort();
|
||||
|
||||
let mut rewards = Vec::with_capacity(reward_percentiles.len());
|
||||
let mut sum_gas_used = sorter[0].gas_used;
|
||||
let mut tx_index = 0;
|
||||
|
||||
for percentile in reward_percentiles.iter() {
|
||||
let threshold_gas_used = (header.gas_used as f64) * percentile / 100_f64;
|
||||
while sum_gas_used < threshold_gas_used as u128 && tx_index < transactions.len()
|
||||
{
|
||||
tx_index += 1;
|
||||
sum_gas_used += sorter[tx_index].gas_used;
|
||||
}
|
||||
|
||||
rewards.push(U256::from(sorter[tx_index].reward));
|
||||
}
|
||||
|
||||
let fee_history_cache_item = FeeHistoryCacheItem {
|
||||
hash: None,
|
||||
base_fee_per_gas,
|
||||
gas_used_ratio,
|
||||
reward: Some(rewards),
|
||||
};
|
||||
|
||||
// Insert missing cache entries in the map for further response composition from
|
||||
// it
|
||||
fee_history_cache_items.insert(header.number, fee_history_cache_item.clone());
|
||||
// And populate the cache with new entries
|
||||
fee_history_cache.push(header.number, fee_history_cache_item);
|
||||
}
|
||||
}
|
||||
|
||||
// get the first block in the range from the db
|
||||
let oldest_block_hash =
|
||||
self.inner.client.block_hash(start_block)?.ok_or(EthApiError::UnknownBlockNumber)?;
|
||||
|
||||
// Set the hash in cache items if the block is present in the cache
|
||||
if let Some(cache_item) = fee_history_cache_items.get_mut(&start_block) {
|
||||
cache_item.hash = Some(oldest_block_hash);
|
||||
}
|
||||
|
||||
if let Some(cache_item) = fee_history_cache.get_mut(&start_block) {
|
||||
cache_item.hash = Some(oldest_block_hash);
|
||||
}
|
||||
|
||||
// `fee_history_cache_items` now contains full requested block range (populated from both
|
||||
// cache and database), so we can iterate over it in order and populate the response fields
|
||||
let base_fee_per_gas =
|
||||
fee_history_cache_items.values().map(|item| item.base_fee_per_gas).collect();
|
||||
|
||||
let mut gas_used_ratio: Vec<f64> =
|
||||
fee_history_cache_items.values().map(|item| item.gas_used_ratio).collect();
|
||||
|
||||
let mut rewards: Vec<Vec<_>> =
|
||||
fee_history_cache_items.values().filter_map(|item| item.reward.clone()).collect();
|
||||
|
||||
// gasUsedRatio doesn't have data for next block in this case the last block
|
||||
gas_used_ratio.pop();
|
||||
rewards.pop();
|
||||
|
||||
Ok(FeeHistory {
|
||||
base_fee_per_gas,
|
||||
gas_used_ratio,
|
||||
oldest_block: U256::from(start_block),
|
||||
reward: Some(rewards),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ use std::{num::NonZeroUsize, sync::Arc};
|
||||
|
||||
mod block;
|
||||
mod call;
|
||||
mod fees;
|
||||
mod server;
|
||||
mod sign;
|
||||
mod state;
|
||||
|
||||
@@ -5,27 +5,24 @@ use super::EthApiSpec;
|
||||
use crate::{
|
||||
eth::{
|
||||
api::{EthApi, EthTransactions},
|
||||
error::{ensure_success, EthApiError},
|
||||
error::ensure_success,
|
||||
},
|
||||
result::{internal_rpc_err, ToRpcResult},
|
||||
};
|
||||
use jsonrpsee::core::RpcResult as Result;
|
||||
use reth_network_api::NetworkInfo;
|
||||
use reth_primitives::{
|
||||
serde_helper::JsonStorageKey, AccessListWithGasUsed, Address, BlockId, BlockNumberOrTag, Bytes,
|
||||
Header, H256, H64, U256, U64,
|
||||
H256, H64, U256, U64,
|
||||
};
|
||||
use reth_provider::{BlockProvider, EvmEnvProvider, HeaderProvider, StateProviderFactory};
|
||||
use reth_rpc_api::EthApiServer;
|
||||
use reth_rpc_types::{
|
||||
state::StateOverride, CallRequest, EIP1186AccountProofResponse, FeeHistory,
|
||||
FeeHistoryCacheItem, Index, RichBlock, SyncStatus, TransactionReceipt, TransactionRequest,
|
||||
Work,
|
||||
state::StateOverride, CallRequest, EIP1186AccountProofResponse, FeeHistory, Index, RichBlock,
|
||||
SyncStatus, TransactionReceipt, TransactionRequest, Work,
|
||||
};
|
||||
use reth_transaction_pool::TransactionPool;
|
||||
|
||||
use reth_network_api::NetworkInfo;
|
||||
use serde_json::Value;
|
||||
use std::collections::BTreeMap;
|
||||
use tracing::trace;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
@@ -270,94 +267,8 @@ where
|
||||
reward_percentiles: Option<Vec<f64>>,
|
||||
) -> Result<FeeHistory> {
|
||||
trace!(target: "rpc::eth", ?block_count, ?newest_block, ?reward_percentiles, "Serving eth_feeHistory");
|
||||
let block_count = block_count.as_u64();
|
||||
|
||||
if block_count == 0 {
|
||||
return Ok(FeeHistory::default())
|
||||
}
|
||||
|
||||
let Some(end_block) = self.inner.client.block_number_for_id(newest_block).to_rpc_result()? else { return Err(EthApiError::UnknownBlockNumber.into())};
|
||||
|
||||
if end_block < block_count {
|
||||
return Err(EthApiError::InvalidBlockRange.into())
|
||||
}
|
||||
|
||||
let start_block = end_block - block_count;
|
||||
|
||||
let mut fee_history_cache = self.fee_history_cache.0.lock().await;
|
||||
|
||||
// Sorted map that's populated in two rounds:
|
||||
// 1. Cache entries until first non-cached block
|
||||
// 2. Database query from the first non-cached block
|
||||
let mut fee_history_cache_items = BTreeMap::new();
|
||||
|
||||
let mut first_non_cached_block = None;
|
||||
let mut last_non_cached_block = None;
|
||||
for block in start_block..=end_block {
|
||||
// Check if block exists in cache, and move it to the head of the list if so
|
||||
if let Some(fee_history_cache_item) = fee_history_cache.get(&block) {
|
||||
fee_history_cache_items.insert(block, fee_history_cache_item.clone());
|
||||
} else {
|
||||
// If block doesn't exist in cache, set it as a first non-cached block to query it
|
||||
// from the database
|
||||
first_non_cached_block.get_or_insert(block);
|
||||
// And last non-cached block, so we could query the database until we reach it
|
||||
last_non_cached_block = Some(block);
|
||||
}
|
||||
}
|
||||
|
||||
// If we had any cache misses, query the database starting with the first non-cached block
|
||||
// and ending with the last
|
||||
if let (Some(start_block), Some(end_block)) =
|
||||
(first_non_cached_block, last_non_cached_block)
|
||||
{
|
||||
let headers: Vec<Header> =
|
||||
self.inner.client.headers_range(start_block..=end_block).to_rpc_result()?;
|
||||
|
||||
// We should receive exactly the amount of blocks missing from the cache
|
||||
if headers.len() != (end_block - start_block + 1) as usize {
|
||||
return Err(EthApiError::InvalidBlockRange.into())
|
||||
}
|
||||
|
||||
for header in headers {
|
||||
let base_fee_per_gas = header.base_fee_per_gas.
|
||||
unwrap_or_default(). // Zero for pre-EIP-1559 blocks
|
||||
try_into().unwrap(); // u64 -> U256 won't fail
|
||||
let gas_used_ratio = header.gas_used as f64 / header.gas_limit as f64;
|
||||
|
||||
let fee_history_cache_item = FeeHistoryCacheItem {
|
||||
hash: None,
|
||||
base_fee_per_gas,
|
||||
gas_used_ratio,
|
||||
reward: None, // TODO: calculate rewards per transaction
|
||||
};
|
||||
|
||||
// Insert missing cache entries in the map for further response composition from it
|
||||
fee_history_cache_items.insert(header.number, fee_history_cache_item.clone());
|
||||
// And populate the cache with new entries
|
||||
fee_history_cache.push(header.number, fee_history_cache_item);
|
||||
}
|
||||
}
|
||||
|
||||
let oldest_block_hash = self.inner.client.block_hash(start_block).to_rpc_result()?.unwrap();
|
||||
|
||||
fee_history_cache_items.get_mut(&start_block).unwrap().hash = Some(oldest_block_hash);
|
||||
fee_history_cache.get_mut(&start_block).unwrap().hash = Some(oldest_block_hash);
|
||||
|
||||
// `fee_history_cache_items` now contains full requested block range (populated from both
|
||||
// cache and database), so we can iterate over it in order and populate the response fields
|
||||
Ok(FeeHistory {
|
||||
base_fee_per_gas: fee_history_cache_items
|
||||
.values()
|
||||
.map(|item| item.base_fee_per_gas)
|
||||
.collect(),
|
||||
gas_used_ratio: fee_history_cache_items
|
||||
.values()
|
||||
.map(|item| item.gas_used_ratio)
|
||||
.collect(),
|
||||
oldest_block: U256::from_be_bytes(oldest_block_hash.0),
|
||||
reward: None,
|
||||
})
|
||||
return Ok(EthApi::fee_history(self, block_count.as_u64(), newest_block, reward_percentiles)
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// Handler for: `eth_maxPriorityFeePerGas`
|
||||
@@ -449,7 +360,7 @@ mod tests {
|
||||
};
|
||||
use rand::random;
|
||||
use reth_network_api::test_utils::NoopNetwork;
|
||||
use reth_primitives::{Block, BlockNumberOrTag, Header, H256, U256};
|
||||
use reth_primitives::{Block, BlockNumberOrTag, Header, TransactionSigned, H256, U256};
|
||||
use reth_provider::test_utils::{MockEthProvider, NoopProvider};
|
||||
use reth_rpc_api::EthApiServer;
|
||||
use reth_transaction_pool::test_utils::testing_pool;
|
||||
@@ -464,7 +375,13 @@ mod tests {
|
||||
EthStateCache::spawn(NoopProvider::default(), Default::default()),
|
||||
);
|
||||
|
||||
let response = eth_api.fee_history(1.into(), BlockNumberOrTag::Latest.into(), None).await;
|
||||
let response = <EthApi<_, _, _> as EthApiServer>::fee_history(
|
||||
ð_api,
|
||||
1.into(),
|
||||
BlockNumberOrTag::Latest.into(),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
assert!(matches!(response, RpcResult::Err(RpcError::Call(CallError::Custom(_)))));
|
||||
let Err(RpcError::Call(CallError::Custom(error_object))) = response else { unreachable!() };
|
||||
assert_eq!(error_object.code(), INVALID_PARAMS_CODE);
|
||||
@@ -492,7 +409,39 @@ mod tests {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
mock_provider.add_block(hash, Block { header: header.clone(), ..Default::default() });
|
||||
let mut transactions = vec![];
|
||||
for _ in 0..100 {
|
||||
let random_fee: u128 = random();
|
||||
|
||||
if let Some(base_fee_per_gas) = header.base_fee_per_gas {
|
||||
let transaction = TransactionSigned {
|
||||
transaction: reth_primitives::Transaction::Eip1559(
|
||||
reth_primitives::TxEip1559 {
|
||||
max_priority_fee_per_gas: random_fee,
|
||||
max_fee_per_gas: random_fee + base_fee_per_gas as u128,
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
transactions.push(transaction);
|
||||
} else {
|
||||
let transaction = TransactionSigned {
|
||||
transaction: reth_primitives::Transaction::Legacy(
|
||||
reth_primitives::TxLegacy { ..Default::default() },
|
||||
),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
transactions.push(transaction);
|
||||
}
|
||||
}
|
||||
|
||||
mock_provider.add_block(
|
||||
hash,
|
||||
Block { header: header.clone(), body: transactions, ..Default::default() },
|
||||
);
|
||||
mock_provider.add_header(hash, header);
|
||||
|
||||
oldest_block.get_or_insert(hash);
|
||||
@@ -501,6 +450,8 @@ mod tests {
|
||||
.push(base_fee_per_gas.map(|fee| U256::try_from(fee).unwrap()).unwrap_or_default());
|
||||
}
|
||||
|
||||
gas_used_ratios.pop();
|
||||
|
||||
let eth_api = EthApi::new(
|
||||
mock_provider,
|
||||
testing_pool(),
|
||||
@@ -508,17 +459,31 @@ mod tests {
|
||||
EthStateCache::spawn(NoopProvider::default(), Default::default()),
|
||||
);
|
||||
|
||||
let response =
|
||||
eth_api.fee_history((newest_block + 1).into(), newest_block.into(), None).await;
|
||||
let response = <EthApi<_, _, _> as EthApiServer>::fee_history(
|
||||
ð_api,
|
||||
(newest_block + 1).into(),
|
||||
newest_block.into(),
|
||||
Some(vec![10.0]),
|
||||
)
|
||||
.await;
|
||||
assert!(matches!(response, RpcResult::Err(RpcError::Call(CallError::Custom(_)))));
|
||||
let Err(RpcError::Call(CallError::Custom(error_object))) = response else { unreachable!() };
|
||||
assert_eq!(error_object.code(), INVALID_PARAMS_CODE);
|
||||
|
||||
// newest_block is finalized
|
||||
let fee_history =
|
||||
eth_api.fee_history(block_count.into(), newest_block.into(), None).await.unwrap();
|
||||
eth_api.fee_history(block_count, (newest_block - 1).into(), None).await.unwrap();
|
||||
|
||||
assert_eq!(fee_history.base_fee_per_gas, base_fees_per_gas);
|
||||
assert_eq!(fee_history.gas_used_ratio, gas_used_ratios);
|
||||
assert_eq!(fee_history.oldest_block, U256::from_be_bytes(oldest_block.unwrap().0));
|
||||
assert_eq!(fee_history.oldest_block, U256::from(newest_block - block_count));
|
||||
|
||||
// newest_block is pending
|
||||
let fee_history =
|
||||
eth_api.fee_history(block_count, (newest_block - 1).into(), None).await.unwrap();
|
||||
|
||||
assert_eq!(fee_history.base_fee_per_gas, base_fees_per_gas);
|
||||
assert_eq!(fee_history.gas_used_ratio, gas_used_ratios);
|
||||
assert_eq!(fee_history.oldest_block, U256::from(newest_block - block_count));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,9 @@ pub enum EthApiError {
|
||||
/// When tracer config does not match the tracer
|
||||
#[error("invalid tracer config")]
|
||||
InvalidTracerConfig,
|
||||
/// Percentile array is invalid
|
||||
#[error("invalid reward percentile")]
|
||||
InvalidRewardPercentile(f64),
|
||||
}
|
||||
|
||||
impl From<EthApiError> for RpcError {
|
||||
@@ -83,6 +86,7 @@ impl From<EthApiError> for RpcError {
|
||||
rpc_error_with_code(EthRpcErrorCode::ResourceNotFound.code(), error.to_string())
|
||||
}
|
||||
EthApiError::Unsupported(msg) => internal_rpc_err(msg),
|
||||
EthApiError::InvalidRewardPercentile(msg) => internal_rpc_err(msg.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user