reth_rpc_eth_api/helpers/
fee.rs
1use 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::EthChainSpec;
11use reth_primitives_traits::BlockBody;
12use reth_provider::{BlockIdReader, ChainSpecProvider, HeaderProvider};
13use reth_rpc_eth_types::{
14 fee_history::calculate_reward_percentiles_for_block, EthApiError, FeeHistoryCache,
15 FeeHistoryEntry, GasPriceOracle, RpcInvalidTransactionError,
16};
17use tracing::debug;
18
19pub trait EthFees: LoadFee {
22 fn gas_price(&self) -> impl Future<Output = Result<U256, Self::Error>> + Send
26 where
27 Self: LoadBlock,
28 {
29 LoadFee::gas_price(self)
30 }
31
32 fn blob_base_fee(&self) -> impl Future<Output = Result<U256, Self::Error>> + Send
34 where
35 Self: LoadBlock,
36 {
37 LoadFee::blob_base_fee(self)
38 }
39
40 fn suggested_priority_fee(&self) -> impl Future<Output = Result<U256, Self::Error>> + Send
42 where
43 Self: 'static,
44 {
45 LoadFee::suggested_priority_fee(self)
46 }
47
48 fn fee_history(
53 &self,
54 mut block_count: u64,
55 mut newest_block: BlockNumberOrTag,
56 reward_percentiles: Option<Vec<f64>>,
57 ) -> impl Future<Output = Result<FeeHistory, Self::Error>> + Send {
58 async move {
59 if block_count == 0 {
60 return Ok(FeeHistory::default())
61 }
62
63 if reward_percentiles.as_ref().map(|perc| perc.len() as u64) >
65 Some(self.gas_oracle().config().max_reward_percentile_count)
66 {
67 return Err(EthApiError::InvalidRewardPercentiles.into())
68 }
69
70 let max_fee_history = if reward_percentiles.is_none() {
72 self.gas_oracle().config().max_header_history
73 } else {
74 self.gas_oracle().config().max_block_history
75 };
76
77 if block_count > max_fee_history {
78 debug!(
79 requested = block_count,
80 truncated = max_fee_history,
81 "Sanitizing fee history block count"
82 );
83 block_count = max_fee_history
84 }
85
86 if newest_block.is_pending() {
87 newest_block = BlockNumberOrTag::Latest;
89 block_count = block_count.saturating_sub(1);
91 }
92
93 let end_block = self
94 .provider()
95 .block_number_for_id(newest_block.into())
96 .map_err(Self::Error::from_eth_err)?
97 .ok_or(EthApiError::HeaderNotFound(newest_block.into()))?;
98
99 let end_block_plus = end_block + 1;
101 if end_block_plus < block_count {
103 block_count = end_block_plus;
104 }
105
106 if let Some(percentiles) = &reward_percentiles {
111 if percentiles.windows(2).any(|w| w[0] > w[1] || w[0] > 100.) {
112 return Err(EthApiError::InvalidRewardPercentiles.into())
113 }
114 }
115
116 let start_block = end_block_plus - block_count;
122
123 let mut base_fee_per_gas: Vec<u128> = Vec::new();
125 let mut gas_used_ratio: Vec<f64> = Vec::new();
126
127 let mut base_fee_per_blob_gas: Vec<u128> = Vec::new();
128 let mut blob_gas_used_ratio: Vec<f64> = Vec::new();
129
130 let mut rewards: Vec<Vec<u128>> = Vec::new();
131
132 let fee_entries = self.fee_history_cache().get_history(start_block, end_block).await;
134
135 if let Some(fee_entries) = fee_entries {
136 if fee_entries.len() != block_count as usize {
137 return Err(EthApiError::InvalidBlockRange.into())
138 }
139
140 for entry in &fee_entries {
141 base_fee_per_gas.push(entry.base_fee_per_gas as u128);
142 gas_used_ratio.push(entry.gas_used_ratio);
143 base_fee_per_blob_gas.push(entry.base_fee_per_blob_gas.unwrap_or_default());
144 blob_gas_used_ratio.push(entry.blob_gas_used_ratio);
145
146 if let Some(percentiles) = &reward_percentiles {
147 let mut block_rewards = Vec::with_capacity(percentiles.len());
148 for &percentile in percentiles {
149 block_rewards.push(self.approximate_percentile(entry, percentile));
150 }
151 rewards.push(block_rewards);
152 }
153 }
154 let last_entry = fee_entries.last().expect("is not empty");
155
156 base_fee_per_gas
159 .push(last_entry.next_block_base_fee(self.provider().chain_spec()) as u128);
160
161 base_fee_per_blob_gas.push(last_entry.next_block_blob_fee().unwrap_or_default());
162 } else {
163 let headers = self.provider()
165 .sealed_headers_range(start_block..=end_block)
166 .map_err(Self::Error::from_eth_err)?;
167 if headers.len() != block_count as usize {
168 return Err(EthApiError::InvalidBlockRange.into())
169 }
170
171
172 for header in &headers {
173 base_fee_per_gas.push(header.base_fee_per_gas().unwrap_or_default() as u128);
174 gas_used_ratio.push(header.gas_used() as f64 / header.gas_limit() as f64);
175
176 let blob_params = self.provider()
177 .chain_spec()
178 .blob_params_at_timestamp(header.timestamp())
179 .unwrap_or_else(BlobParams::cancun);
180
181 base_fee_per_blob_gas.push(header.blob_fee(blob_params).unwrap_or_default());
182 blob_gas_used_ratio.push(
183 header.blob_gas_used().unwrap_or_default() as f64
184 / alloy_eips::eip4844::MAX_DATA_GAS_PER_BLOCK as f64,
185 );
186
187 if let Some(percentiles) = &reward_percentiles {
189 let (block, receipts) = self.cache()
190 .get_block_and_receipts(header.hash())
191 .await
192 .map_err(Self::Error::from_eth_err)?
193 .ok_or(EthApiError::InvalidBlockRange)?;
194 rewards.push(
195 calculate_reward_percentiles_for_block(
196 percentiles,
197 header.gas_used(),
198 header.base_fee_per_gas().unwrap_or_default(),
199 block.body().transactions(),
200 &receipts,
201 )
202 .unwrap_or_default(),
203 );
204 }
205 }
206
207 let last_header = headers.last().expect("is present");
213 base_fee_per_gas.push(
214 last_header.next_block_base_fee(
215 self.provider()
216 .chain_spec()
217 .base_fee_params_at_timestamp(last_header.timestamp())).unwrap_or_default() as u128
218 );
219
220 base_fee_per_blob_gas.push(
223 last_header
224 .maybe_next_block_blob_fee(
225 self.provider().chain_spec().blob_params_at_timestamp(last_header.timestamp())
226 ).unwrap_or_default()
227 );
228 };
229
230 Ok(FeeHistory {
231 base_fee_per_gas,
232 gas_used_ratio,
233 base_fee_per_blob_gas,
234 blob_gas_used_ratio,
235 oldest_block: start_block,
236 reward: reward_percentiles.map(|_| rewards),
237 })
238 }
239 }
240
241 fn approximate_percentile(&self, entry: &FeeHistoryEntry, requested_percentile: f64) -> u128 {
244 let resolution = self.fee_history_cache().resolution();
245 let rounded_percentile =
246 (requested_percentile * resolution as f64).round() / resolution as f64;
247 let clamped_percentile = rounded_percentile.clamp(0.0, 100.0);
248
249 let index = (clamped_percentile / (1.0 / resolution as f64)).round() as usize;
251 entry.rewards.get(index).copied().unwrap_or_default()
253 }
254}
255
256pub trait LoadFee: LoadBlock {
260 fn gas_oracle(&self) -> &GasPriceOracle<Self::Provider>;
264
265 fn fee_history_cache(&self) -> &FeeHistoryCache;
269
270 fn legacy_gas_price(
273 &self,
274 gas_price: Option<U256>,
275 ) -> impl Future<Output = Result<U256, Self::Error>> + Send {
276 async move {
277 match gas_price {
278 Some(gas_price) => Ok(gas_price),
279 None => {
280 self.gas_price().await
282 }
283 }
284 }
285 }
286
287 fn eip1559_fees(
292 &self,
293 base_fee: Option<U256>,
294 max_priority_fee_per_gas: Option<U256>,
295 ) -> impl Future<Output = Result<(U256, U256), Self::Error>> + Send {
296 async move {
297 let base_fee = match base_fee {
298 Some(base_fee) => base_fee,
299 None => {
300 let base_fee = self
302 .recovered_block(BlockNumberOrTag::Pending.into())
303 .await?
304 .ok_or(EthApiError::HeaderNotFound(BlockNumberOrTag::Pending.into()))?
305 .base_fee_per_gas()
306 .ok_or(EthApiError::InvalidTransaction(
307 RpcInvalidTransactionError::TxTypeNotSupported,
308 ))?;
309 U256::from(base_fee)
310 }
311 };
312
313 let max_priority_fee_per_gas = match max_priority_fee_per_gas {
314 Some(max_priority_fee_per_gas) => max_priority_fee_per_gas,
315 None => self.suggested_priority_fee().await?,
316 };
317 Ok((base_fee, max_priority_fee_per_gas))
318 }
319 }
320
321 fn eip4844_blob_fee(
323 &self,
324 blob_fee: Option<U256>,
325 ) -> impl Future<Output = Result<U256, Self::Error>> + Send {
326 async move {
327 match blob_fee {
328 Some(blob_fee) => Ok(blob_fee),
329 None => self.blob_base_fee().await,
330 }
331 }
332 }
333
334 fn gas_price(&self) -> impl Future<Output = Result<U256, Self::Error>> + Send {
338 let header = self.recovered_block(BlockNumberOrTag::Latest.into());
339 let suggested_tip = self.suggested_priority_fee();
340 async move {
341 let (header, suggested_tip) = futures::try_join!(header, suggested_tip)?;
342 let base_fee = header.and_then(|h| h.base_fee_per_gas()).unwrap_or_default();
343 Ok(suggested_tip + U256::from(base_fee))
344 }
345 }
346
347 fn blob_base_fee(&self) -> impl Future<Output = Result<U256, Self::Error>> + Send {
349 async move {
350 self.recovered_block(BlockNumberOrTag::Latest.into())
351 .await?
352 .and_then(|h| {
353 h.maybe_next_block_blob_fee(
354 self.provider().chain_spec().blob_params_at_timestamp(h.timestamp()),
355 )
356 })
357 .ok_or(EthApiError::ExcessBlobGasNotSet.into())
358 .map(U256::from)
359 }
360 }
361
362 fn suggested_priority_fee(&self) -> impl Future<Output = Result<U256, Self::Error>> + Send
364 where
365 Self: 'static,
366 {
367 async move { self.gas_oracle().suggest_tip_cap().await.map_err(Self::Error::from_eth_err) }
368 }
369}