1use std::time::Duration;
6
7use serde::Serialize;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
11pub enum GasSpeed {
12 Slow,
14 Standard,
16 Fast,
18 Urgent,
20}
21
22#[derive(Debug, Clone, Serialize)]
24pub struct GasEstimate {
25 pub max_fee_per_gas: u128,
27 pub max_priority_fee_per_gas: u128,
29 pub estimated_time: Option<Duration>,
31 pub speed: GasSpeed,
33}
34
35#[derive(Debug, Clone, Serialize)]
37pub struct GasRecommendation {
38 pub base_fee: u128,
40 pub slow: GasEstimate,
42 pub standard: GasEstimate,
44 pub fast: GasEstimate,
46 pub urgent: GasEstimate,
48 pub block_number: u64,
50}
51
52pub 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; }
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 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
125pub 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) .collect();
140 fees.sort();
141 fees
142 }
143
144 #[test]
145 fn compute_recommendation_basic() {
146 let base_fee = 30_000_000_000u128; 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 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 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 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}