use std::time::Duration;
use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum GasSpeed {
Slow,
Standard,
Fast,
Urgent,
}
#[derive(Debug, Clone, Serialize)]
pub struct GasEstimate {
pub max_fee_per_gas: u128,
pub max_priority_fee_per_gas: u128,
pub estimated_time: Option<Duration>,
pub speed: GasSpeed,
}
#[derive(Debug, Clone, Serialize)]
pub struct GasRecommendation {
pub base_fee: u128,
pub slow: GasEstimate,
pub standard: GasEstimate,
pub fast: GasEstimate,
pub urgent: GasEstimate,
pub block_number: u64,
}
pub fn compute_gas_recommendation(
base_fee: u128,
priority_fees: &[u128],
block_number: u64,
) -> GasRecommendation {
let n = priority_fees.len();
let percentile = |pct: f64| -> u128 {
if n == 0 {
return 1_000_000_000; }
let idx = ((pct / 100.0) * (n as f64 - 1.0)).round() as usize;
let idx = idx.min(n - 1);
priority_fees[idx]
};
let slow_tip = percentile(10.0);
let standard_tip = percentile(50.0);
let fast_tip = percentile(90.0);
let urgent_tip = percentile(99.0);
let make_estimate =
|tip: u128, multiplier: f64, speed: GasSpeed, est_time: Option<Duration>| {
let adjusted_base = (base_fee as f64 * multiplier) as u128;
GasEstimate {
max_fee_per_gas: adjusted_base + tip,
max_priority_fee_per_gas: tip,
estimated_time: est_time,
speed,
}
};
GasRecommendation {
base_fee,
slow: make_estimate(
slow_tip,
1.0,
GasSpeed::Slow,
Some(Duration::from_secs(120)),
),
standard: make_estimate(
standard_tip,
1.125,
GasSpeed::Standard,
Some(Duration::from_secs(30)),
),
fast: make_estimate(
fast_tip,
1.25,
GasSpeed::Fast,
Some(Duration::from_secs(15)),
),
urgent: make_estimate(
urgent_tip,
1.5,
GasSpeed::Urgent,
Some(Duration::from_secs(6)),
),
block_number,
}
}
pub fn apply_gas_margin(estimated_gas: u64, multiplier: f64) -> u64 {
(estimated_gas as f64 * multiplier).ceil() as u64
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_priority_fees() -> Vec<u128> {
let mut fees: Vec<u128> = (1..=100)
.map(|i| i * 100_000_000) .collect();
fees.sort();
fees
}
#[test]
fn compute_recommendation_basic() {
let base_fee = 30_000_000_000u128; let fees = sample_priority_fees();
let rec = compute_gas_recommendation(base_fee, &fees, 1000);
assert_eq!(rec.base_fee, base_fee);
assert_eq!(rec.block_number, 1000);
assert!(rec.slow.max_fee_per_gas < rec.urgent.max_fee_per_gas);
assert!(rec.slow.max_priority_fee_per_gas < rec.urgent.max_priority_fee_per_gas);
assert!(rec.standard.max_fee_per_gas > rec.slow.max_fee_per_gas);
assert!(rec.standard.max_fee_per_gas < rec.fast.max_fee_per_gas);
}
#[test]
fn compute_recommendation_empty_fees() {
let rec = compute_gas_recommendation(30_000_000_000, &[], 1000);
assert!(rec.slow.max_priority_fee_per_gas > 0);
}
#[test]
fn gas_margin_application() {
assert_eq!(apply_gas_margin(100_000, 1.2), 120_000);
assert_eq!(apply_gas_margin(21_000, 1.0), 21_000);
assert_eq!(apply_gas_margin(50_000, 1.5), 75_000);
}
#[test]
fn speed_tiers_ordering() {
let rec = compute_gas_recommendation(10_000_000_000, &sample_priority_fees(), 1);
assert!(rec.slow.max_fee_per_gas <= rec.standard.max_fee_per_gas);
assert!(rec.standard.max_fee_per_gas <= rec.fast.max_fee_per_gas);
assert!(rec.fast.max_fee_per_gas <= rec.urgent.max_fee_per_gas);
}
#[test]
fn serializable() {
let rec = compute_gas_recommendation(10_000_000_000, &sample_priority_fees(), 1);
let json = serde_json::to_string(&rec).unwrap();
assert!(json.contains("base_fee"));
assert!(json.contains("max_fee_per_gas"));
}
}