reth_rpc_eth_api/helpers/
fee.rs1use super::LoadBlock;
4use crate::FromEthApiError;
5use alloy_consensus::BlockHeader;
6use alloy_eips::eip7840::BlobParams;
7use alloy_primitives::U256;
8use alloy_rpc_types_eth::{BlockNumberOrTag, FeeHistory};
9use futures::Future;
10use reth_chainspec::{ChainSpecProvider, EthChainSpec};
11use reth_primitives_traits::BlockBody;
12use reth_rpc_eth_types::{
13 fee_history::calculate_reward_percentiles_for_block, utils::checked_blob_gas_used_ratio,
14 EthApiError, FeeHistoryCache, FeeHistoryEntry, GasPriceOracle, RpcInvalidTransactionError,
15};
16use reth_storage_api::{BlockIdReader, BlockReaderIdExt, HeaderProvider, ProviderHeader};
17use tracing::debug;
18
19pub trait EthFees:
22 LoadFee<
23 Provider: ChainSpecProvider<ChainSpec: EthChainSpec<Header = ProviderHeader<Self::Provider>>>,
24>
25{
26 fn gas_price(&self) -> impl Future<Output = Result<U256, Self::Error>> + Send
30 where
31 Self: LoadBlock,
32 {
33 LoadFee::gas_price(self)
34 }
35
36 fn blob_base_fee(&self) -> impl Future<Output = Result<U256, Self::Error>> + Send
38 where
39 Self: LoadBlock,
40 {
41 LoadFee::blob_base_fee(self)
42 }
43
44 fn suggested_priority_fee(&self) -> impl Future<Output = Result<U256, Self::Error>> + Send
46 where
47 Self: 'static,
48 {
49 LoadFee::suggested_priority_fee(self)
50 }
51
52 fn fee_history(
57 &self,
58 mut block_count: u64,
59 mut newest_block: BlockNumberOrTag,
60 reward_percentiles: Option<Vec<f64>>,
61 ) -> impl Future<Output = Result<FeeHistory, Self::Error>> + Send {
62 async move {
63 if block_count == 0 {
64 return Ok(FeeHistory::default())
65 }
66
67 if reward_percentiles.as_ref().map(|perc| perc.len() as u64) >
69 Some(self.gas_oracle().config().max_reward_percentile_count)
70 {
71 return Err(EthApiError::InvalidRewardPercentiles.into())
72 }
73
74 let max_fee_history = if reward_percentiles.is_none() {
76 self.gas_oracle().config().max_header_history
77 } else {
78 self.gas_oracle().config().max_block_history
79 };
80
81 if block_count > max_fee_history {
82 debug!(
83 requested = block_count,
84 truncated = max_fee_history,
85 "Sanitizing fee history block count"
86 );
87 block_count = max_fee_history
88 }
89
90 if newest_block.is_pending() {
91 newest_block = BlockNumberOrTag::Latest;
93 }
94
95 let end_block = self
96 .provider()
97 .block_number_for_id(newest_block.into())
98 .map_err(Self::Error::from_eth_err)?
99 .ok_or(EthApiError::HeaderNotFound(newest_block.into()))?;
100
101 let end_block_plus = end_block + 1;
103 if end_block_plus < block_count {
105 block_count = end_block_plus;
106 }
107
108 if let Some(percentiles) = &reward_percentiles &&
113 (percentiles.iter().any(|p| *p < 0.0 || *p > 100.0) ||
114 percentiles.windows(2).any(|w| w[0] > w[1]))
115 {
116 return Err(EthApiError::InvalidRewardPercentiles.into())
117 }
118
119 let start_block = end_block_plus - block_count;
125
126 let mut base_fee_per_gas: Vec<u128> = Vec::new();
128 let mut gas_used_ratio: Vec<f64> = Vec::new();
129
130 let mut base_fee_per_blob_gas: Vec<u128> = Vec::new();
131 let mut blob_gas_used_ratio: Vec<f64> = Vec::new();
132
133 let mut rewards: Vec<Vec<u128>> = Vec::new();
134
135 let fee_entries = self.fee_history_cache().get_history(start_block, end_block).await;
137
138 if let Some(fee_entries) = fee_entries {
139 if fee_entries.len() != block_count as usize {
140 return Err(EthApiError::InvalidBlockRange.into())
141 }
142
143 for entry in &fee_entries {
144 base_fee_per_gas
145 .push(entry.header.base_fee_per_gas().unwrap_or_default() as u128);
146 gas_used_ratio.push(entry.gas_used_ratio);
147 base_fee_per_blob_gas.push(entry.base_fee_per_blob_gas.unwrap_or_default());
148 blob_gas_used_ratio.push(entry.blob_gas_used_ratio);
149
150 if let Some(percentiles) = &reward_percentiles {
151 let mut block_rewards = Vec::with_capacity(percentiles.len());
152 for &percentile in percentiles {
153 block_rewards.push(self.approximate_percentile(entry, percentile));
154 }
155 rewards.push(block_rewards);
156 }
157 }
158 let last_entry = fee_entries.last().expect("is not empty");
159
160 base_fee_per_gas.push(
163 self.provider()
164 .chain_spec()
165 .next_block_base_fee(&last_entry.header, last_entry.header.timestamp())
166 .unwrap_or_default() as u128,
167 );
168
169 base_fee_per_blob_gas.push(last_entry.next_block_blob_fee().unwrap_or_default());
170 } else {
171 let headers = self.provider()
173 .sealed_headers_range(start_block..=end_block)
174 .map_err(Self::Error::from_eth_err)?;
175 if headers.len() != block_count as usize {
176 return Err(EthApiError::InvalidBlockRange.into())
177 }
178
179 let chain_spec = self.provider().chain_spec();
180 for header in &headers {
181 base_fee_per_gas.push(header.base_fee_per_gas().unwrap_or_default() as u128);
182 gas_used_ratio.push(header.gas_used() as f64 / header.gas_limit() as f64);
183
184 let blob_params = chain_spec
185 .blob_params_at_timestamp(header.timestamp())
186 .unwrap_or_else(BlobParams::cancun);
187
188 base_fee_per_blob_gas.push(header.blob_fee(blob_params).unwrap_or_default());
189 blob_gas_used_ratio.push(
190 checked_blob_gas_used_ratio(
191 header.blob_gas_used().unwrap_or_default(),
192 blob_params.max_blob_gas_per_block(),
193 )
194 );
195
196 if let Some(percentiles) = &reward_percentiles {
198 let (block, receipts) = self.cache()
199 .get_block_and_receipts(header.hash())
200 .await
201 .map_err(Self::Error::from_eth_err)?
202 .ok_or(EthApiError::InvalidBlockRange)?;
203 rewards.push(
204 calculate_reward_percentiles_for_block(
205 percentiles,
206 header.gas_used(),
207 header.base_fee_per_gas().unwrap_or_default(),
208 block.body().transactions(),
209 &receipts,
210 )
211 .unwrap_or_default(),
212 );
213 }
214 }
215
216 let last_header = headers.last().expect("is present");
222 base_fee_per_gas.push(
223 chain_spec
224 .next_block_base_fee(last_header.header(), last_header.timestamp())
225 .unwrap_or_default() as u128,
226 );
227 base_fee_per_blob_gas.push(
230 last_header
231 .maybe_next_block_blob_fee(
232 chain_spec.blob_params_at_timestamp(last_header.timestamp())
233 ).unwrap_or_default()
234 );
235 };
236
237 Ok(FeeHistory {
238 base_fee_per_gas,
239 gas_used_ratio,
240 base_fee_per_blob_gas,
241 blob_gas_used_ratio,
242 oldest_block: start_block,
243 reward: reward_percentiles.map(|_| rewards),
244 })
245 }
246 }
247
248 fn approximate_percentile(
251 &self,
252 entry: &FeeHistoryEntry<ProviderHeader<Self::Provider>>,
253 requested_percentile: f64,
254 ) -> u128 {
255 let resolution = self.fee_history_cache().resolution();
256 let rounded_percentile =
257 (requested_percentile * resolution as f64).round() / resolution as f64;
258 let clamped_percentile = rounded_percentile.clamp(0.0, 100.0);
259
260 let index = (clamped_percentile / (1.0 / resolution as f64)).round() as usize;
262 entry.rewards.get(index).copied().unwrap_or_default()
264 }
265}
266
267pub trait LoadFee: LoadBlock
271where
272 Self::Provider: BlockReaderIdExt,
273{
274 fn gas_oracle(&self) -> &GasPriceOracle<Self::Provider>;
278
279 fn fee_history_cache(&self) -> &FeeHistoryCache<ProviderHeader<Self::Provider>>;
283
284 fn legacy_gas_price(
287 &self,
288 gas_price: Option<U256>,
289 ) -> impl Future<Output = Result<U256, Self::Error>> + Send {
290 async move {
291 match gas_price {
292 Some(gas_price) => Ok(gas_price),
293 None => {
294 self.gas_price().await
296 }
297 }
298 }
299 }
300
301 fn eip1559_fees(
306 &self,
307 base_fee: Option<U256>,
308 max_priority_fee_per_gas: Option<U256>,
309 ) -> impl Future<Output = Result<(U256, U256), Self::Error>> + Send {
310 async move {
311 let base_fee = match base_fee {
312 Some(base_fee) => base_fee,
313 None => {
314 let base_fee = self
316 .recovered_block(BlockNumberOrTag::Pending.into())
317 .await?
318 .ok_or(EthApiError::HeaderNotFound(BlockNumberOrTag::Pending.into()))?
319 .base_fee_per_gas()
320 .ok_or(EthApiError::InvalidTransaction(
321 RpcInvalidTransactionError::TxTypeNotSupported,
322 ))?;
323 U256::from(base_fee)
324 }
325 };
326
327 let max_priority_fee_per_gas = match max_priority_fee_per_gas {
328 Some(max_priority_fee_per_gas) => max_priority_fee_per_gas,
329 None => self.suggested_priority_fee().await?,
330 };
331 Ok((base_fee, max_priority_fee_per_gas))
332 }
333 }
334
335 fn eip4844_blob_fee(
337 &self,
338 blob_fee: Option<U256>,
339 ) -> impl Future<Output = Result<U256, Self::Error>> + Send {
340 async move {
341 match blob_fee {
342 Some(blob_fee) => Ok(blob_fee),
343 None => self.blob_base_fee().await,
344 }
345 }
346 }
347
348 fn gas_price(&self) -> impl Future<Output = Result<U256, Self::Error>> + Send {
352 async move {
353 let header = self.provider().latest_header().map_err(Self::Error::from_eth_err)?;
354 let suggested_tip = self.suggested_priority_fee().await?;
355 let base_fee = header.and_then(|h| h.base_fee_per_gas()).unwrap_or_default();
356 Ok(suggested_tip + U256::from(base_fee))
357 }
358 }
359
360 fn blob_base_fee(&self) -> impl Future<Output = Result<U256, Self::Error>> + Send {
362 async move {
363 self.provider()
364 .latest_header()
365 .map_err(Self::Error::from_eth_err)?
366 .and_then(|h| {
367 h.maybe_next_block_blob_fee(
368 self.provider().chain_spec().blob_params_at_timestamp(h.timestamp()),
369 )
370 })
371 .ok_or(EthApiError::ExcessBlobGasNotSet.into())
372 .map(U256::from)
373 }
374 }
375
376 fn suggested_priority_fee(&self) -> impl Future<Output = Result<U256, Self::Error>> + Send
378 where
379 Self: 'static,
380 {
381 async move { self.gas_oracle().suggest_tip_cap().await.map_err(Self::Error::from_eth_err) }
382 }
383}