Skip to main content

chainrpc_core/
gas.rs

1//! EIP-1559 gas estimation utilities.
2//!
3//! Provides speed-tier gas recommendations based on fee history.
4
5use std::time::Duration;
6
7use serde::Serialize;
8
9/// Gas speed tier.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
11pub enum GasSpeed {
12    /// Economy tier (~10th percentile).
13    Slow,
14    /// Standard tier (~50th percentile).
15    Standard,
16    /// Fast tier (~90th percentile).
17    Fast,
18    /// Urgent tier (~99th percentile).
19    Urgent,
20}
21
22/// EIP-1559 gas recommendation for a single speed tier.
23#[derive(Debug, Clone, Serialize)]
24pub struct GasEstimate {
25    /// Recommended max fee per gas (in wei).
26    pub max_fee_per_gas: u128,
27    /// Recommended max priority fee per gas (tip, in wei).
28    pub max_priority_fee_per_gas: u128,
29    /// Estimated time to inclusion.
30    pub estimated_time: Option<Duration>,
31    /// Speed tier this estimate corresponds to.
32    pub speed: GasSpeed,
33}
34
35/// Complete gas recommendation across all speed tiers.
36#[derive(Debug, Clone, Serialize)]
37pub struct GasRecommendation {
38    /// Current base fee (from latest block).
39    pub base_fee: u128,
40    /// Slow/economy estimate.
41    pub slow: GasEstimate,
42    /// Standard estimate.
43    pub standard: GasEstimate,
44    /// Fast estimate.
45    pub fast: GasEstimate,
46    /// Urgent estimate.
47    pub urgent: GasEstimate,
48    /// Block number this recommendation is based on.
49    pub block_number: u64,
50}
51
52/// Compute gas recommendations from fee history data.
53///
54/// `base_fee` is the current base fee in wei.
55/// `priority_fees` is a sorted (ascending) list of recent priority fee samples.
56/// `block_number` is the current block number.
57pub fn compute_gas_recommendation(
58    base_fee: u128,
59    priority_fees: &[u128],
60    block_number: u64,
61) -> GasRecommendation {
62    let n = priority_fees.len();
63
64    let percentile = |pct: f64| -> u128 {
65        if n == 0 {
66            return 1_000_000_000; // 1 gwei default
67        }
68        let idx = ((pct / 100.0) * (n as f64 - 1.0)).round() as usize;
69        let idx = idx.min(n - 1);
70        priority_fees[idx]
71    };
72
73    let slow_tip = percentile(10.0);
74    let standard_tip = percentile(50.0);
75    let fast_tip = percentile(90.0);
76    let urgent_tip = percentile(99.0);
77
78    // Base fee can increase up to 12.5% per block.
79    // Apply safety multipliers:
80    // slow: 1.0x base (risk of waiting)
81    // standard: 1.125x (one block increase)
82    // fast: 1.25x (two block increases)
83    // urgent: 1.5x (multiple block increases)
84    let make_estimate =
85        |tip: u128, multiplier: f64, speed: GasSpeed, est_time: Option<Duration>| {
86            let adjusted_base = (base_fee as f64 * multiplier) as u128;
87            GasEstimate {
88                max_fee_per_gas: adjusted_base + tip,
89                max_priority_fee_per_gas: tip,
90                estimated_time: est_time,
91                speed,
92            }
93        };
94
95    GasRecommendation {
96        base_fee,
97        slow: make_estimate(
98            slow_tip,
99            1.0,
100            GasSpeed::Slow,
101            Some(Duration::from_secs(120)),
102        ),
103        standard: make_estimate(
104            standard_tip,
105            1.125,
106            GasSpeed::Standard,
107            Some(Duration::from_secs(30)),
108        ),
109        fast: make_estimate(
110            fast_tip,
111            1.25,
112            GasSpeed::Fast,
113            Some(Duration::from_secs(15)),
114        ),
115        urgent: make_estimate(
116            urgent_tip,
117            1.5,
118            GasSpeed::Urgent,
119            Some(Duration::from_secs(6)),
120        ),
121        block_number,
122    }
123}
124
125/// Apply a safety margin to a gas estimate (multiply gas limit).
126///
127/// Returns `estimated_gas * multiplier`, rounded up.
128pub fn apply_gas_margin(estimated_gas: u64, multiplier: f64) -> u64 {
129    (estimated_gas as f64 * multiplier).ceil() as u64
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    fn sample_priority_fees() -> Vec<u128> {
137        let mut fees: Vec<u128> = (1..=100)
138            .map(|i| i * 100_000_000) // 0.1 to 10 gwei
139            .collect();
140        fees.sort();
141        fees
142    }
143
144    #[test]
145    fn compute_recommendation_basic() {
146        let base_fee = 30_000_000_000u128; // 30 gwei
147        let fees = sample_priority_fees();
148
149        let rec = compute_gas_recommendation(base_fee, &fees, 1000);
150
151        assert_eq!(rec.base_fee, base_fee);
152        assert_eq!(rec.block_number, 1000);
153
154        // Slow should have lower fees than urgent
155        assert!(rec.slow.max_fee_per_gas < rec.urgent.max_fee_per_gas);
156        assert!(rec.slow.max_priority_fee_per_gas < rec.urgent.max_priority_fee_per_gas);
157
158        // Standard should be between slow and fast
159        assert!(rec.standard.max_fee_per_gas > rec.slow.max_fee_per_gas);
160        assert!(rec.standard.max_fee_per_gas < rec.fast.max_fee_per_gas);
161    }
162
163    #[test]
164    fn compute_recommendation_empty_fees() {
165        let rec = compute_gas_recommendation(30_000_000_000, &[], 1000);
166        // Should use defaults (1 gwei) for priority fees
167        assert!(rec.slow.max_priority_fee_per_gas > 0);
168    }
169
170    #[test]
171    fn gas_margin_application() {
172        assert_eq!(apply_gas_margin(100_000, 1.2), 120_000);
173        assert_eq!(apply_gas_margin(21_000, 1.0), 21_000);
174        assert_eq!(apply_gas_margin(50_000, 1.5), 75_000);
175    }
176
177    #[test]
178    fn speed_tiers_ordering() {
179        let rec = compute_gas_recommendation(10_000_000_000, &sample_priority_fees(), 1);
180
181        assert!(rec.slow.max_fee_per_gas <= rec.standard.max_fee_per_gas);
182        assert!(rec.standard.max_fee_per_gas <= rec.fast.max_fee_per_gas);
183        assert!(rec.fast.max_fee_per_gas <= rec.urgent.max_fee_per_gas);
184    }
185
186    #[test]
187    fn serializable() {
188        let rec = compute_gas_recommendation(10_000_000_000, &sample_priority_fees(), 1);
189        let json = serde_json::to_string(&rec).unwrap();
190        assert!(json.contains("base_fee"));
191        assert!(json.contains("max_fee_per_gas"));
192    }
193}