Skip to main content

ethrex_rpc/eth/
gas_price.rs

1//! Gas price estimation for `eth_gasPrice` RPC method.
2//!
3//! This module implements the gas price oracle that estimates a reasonable
4//! gas price based on recent block history and network conditions.
5
6use crate::rpc::{RpcApiContext, RpcHandler};
7use crate::utils::RpcErr;
8use ethrex_blockchain::BlockchainType;
9use serde_json::Value;
10
11/// Handler for the `eth_gasPrice` RPC method.
12///
13/// Returns the current gas price in wei as a hexadecimal string.
14/// The price is calculated as: `base_fee + estimated_priority_fee + operator_fee (L2 only)`.
15///
16/// # Algorithm
17///
18/// 1. Gets the base fee from the latest block header
19/// 2. Estimates a reasonable priority fee (gas tip) by analyzing recent transactions
20/// 3. For L2 nodes, adds the operator fee if configured
21///
22/// # Example Response
23///
24/// ```json
25/// "0x3b9aca00"  // 1 Gwei in hexadecimal
26/// ```
27#[derive(Debug, Clone)]
28pub struct GasPrice;
29
30impl RpcHandler for GasPrice {
31    fn parse(_: &Option<Vec<Value>>) -> Result<Self, RpcErr> {
32        Ok(GasPrice {})
33    }
34
35    async fn handle(&self, context: RpcApiContext) -> Result<Value, RpcErr> {
36        let latest_block_number = context.storage.get_latest_block_number().await?;
37        let latest_header = context
38            .storage
39            .get_block_header(latest_block_number)?
40            .ok_or(RpcErr::Internal(format!(
41                "Missing latest block with number {latest_block_number}"
42            )))?;
43        let Some(base_fee) = latest_header.base_fee_per_gas else {
44            return Err(RpcErr::Internal(
45                "Error calculating gas price: missing base_fee on block".to_string(),
46            ));
47        };
48        let estimated_gas_tip = context
49            .gas_tip_estimator
50            .lock()
51            .await
52            .estimate_gas_tip(&context.storage)
53            .await?;
54        // To complete the gas price, we need to add the base fee to the estimated gas tip.
55        let mut gas_price = base_fee + estimated_gas_tip;
56
57        // Add the operator fee to the gas price if configured
58        if let BlockchainType::L2(l2_config) = &context.blockchain.options.r#type {
59            let fee_config = *l2_config
60                .fee_config
61                .read()
62                .map_err(|_| RpcErr::Internal("Fee config lock was poisoned".to_string()))?;
63            if let Some(operator_fee_config) = &fee_config.operator_fee_config {
64                gas_price += operator_fee_config.operator_fee_per_gas;
65            }
66        }
67
68        let gas_as_hex = format!("0x{gas_price:x}");
69        Ok(serde_json::Value::String(gas_as_hex))
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::GasPrice;
76    use crate::test_utils::{
77        BASE_PRICE_IN_WEI, add_eip1559_tx_blocks, add_legacy_tx_blocks, add_mixed_tx_blocks,
78        setup_store,
79    };
80
81    use crate::test_utils::default_context_with_storage;
82    use crate::{
83        rpc::{RpcHandler, map_http_requests},
84        utils::{RpcRequest, parse_json_hex},
85    };
86    use ethrex_common::types::MIN_GAS_TIP;
87    use serde_json::json;
88
89    #[tokio::test]
90    async fn test_for_legacy_txs() {
91        let storage = setup_store().await;
92        let context = default_context_with_storage(storage).await;
93
94        add_legacy_tx_blocks(&context.storage, 100, 10).await;
95
96        let gas_price = GasPrice {};
97        let response = gas_price.handle(context).await.unwrap();
98        let parsed_result = parse_json_hex(&response).unwrap();
99        assert_eq!(parsed_result, 2 * BASE_PRICE_IN_WEI);
100    }
101
102    #[tokio::test]
103    async fn test_for_eip_1559_txs() {
104        let storage = setup_store().await;
105        let context = default_context_with_storage(storage).await;
106
107        add_eip1559_tx_blocks(&context.storage, 100, 10).await;
108
109        let gas_price = GasPrice {};
110        let response = gas_price.handle(context).await.unwrap();
111        let parsed_result = parse_json_hex(&response).unwrap();
112        assert_eq!(parsed_result, 2 * BASE_PRICE_IN_WEI);
113    }
114
115    #[tokio::test]
116    async fn test_with_mixed_transactions() {
117        let storage = setup_store().await;
118        let context = default_context_with_storage(storage).await;
119
120        add_mixed_tx_blocks(&context.storage, 100, 10).await;
121
122        let gas_price = GasPrice {};
123        let response = gas_price.handle(context).await.unwrap();
124        let parsed_result = parse_json_hex(&response).unwrap();
125        assert_eq!(parsed_result, 2 * BASE_PRICE_IN_WEI);
126    }
127
128    #[tokio::test]
129    async fn test_with_not_enough_blocks_or_transactions() {
130        let storage = setup_store().await;
131        let context = default_context_with_storage(storage).await;
132
133        add_mixed_tx_blocks(&context.storage, 100, 0).await;
134
135        let gas_price = GasPrice {};
136        let response = gas_price.handle(context).await.unwrap();
137        let parsed_result = parse_json_hex(&response).unwrap();
138        assert_eq!(parsed_result, BASE_PRICE_IN_WEI + MIN_GAS_TIP);
139    }
140
141    #[tokio::test]
142    async fn test_with_no_blocks_but_genesis() {
143        let storage = setup_store().await;
144        let context = default_context_with_storage(storage).await;
145        let gas_price = GasPrice {};
146        // genesis base fee is = BASE_PRICE_IN_WEI
147        let expected_gas_price = BASE_PRICE_IN_WEI + MIN_GAS_TIP;
148        let response = gas_price.handle(context).await.unwrap();
149        let parsed_result = parse_json_hex(&response).unwrap();
150        assert_eq!(parsed_result, expected_gas_price);
151    }
152
153    #[tokio::test]
154    async fn request_smoke_test() {
155        let raw_json = json!(
156        {
157            "jsonrpc":"2.0",
158            "method":"eth_gasPrice",
159            "id":1
160        });
161        let expected_response = json!("0x3b9aca00");
162        let request: RpcRequest = serde_json::from_value(raw_json).expect("Test json is not valid");
163        let storage = setup_store().await;
164        let context = default_context_with_storage(storage).await;
165
166        add_legacy_tx_blocks(&context.storage, 100, 1).await;
167
168        let response = map_http_requests(&request, context).await.unwrap();
169        assert_eq!(response, expected_response)
170    }
171}