use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum PriorityLevel {
Min,
Low,
Medium,
High,
VeryHigh,
UnsafeMax,
}
impl PriorityLevel {
#[must_use]
pub fn percentile(self) -> f64 {
match self {
Self::Min => 0.0,
Self::Low => 25.0,
Self::Medium => 50.0,
Self::High => 75.0,
Self::VeryHigh => 95.0,
Self::UnsafeMax => 100.0,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PriorityFeeLevels {
pub min: f64,
pub low: f64,
pub medium: f64,
pub high: f64,
pub very_high: f64,
pub unsafe_max: f64,
}
#[must_use]
pub fn compute_levels(fees: &[u64]) -> PriorityFeeLevels {
if fees.is_empty() {
return PriorityFeeLevels::default();
}
let mut sorted = fees.to_vec();
sorted.sort_unstable();
PriorityFeeLevels {
min: percentile_at(&sorted, PriorityLevel::Min) as f64,
low: percentile_at(&sorted, PriorityLevel::Low) as f64,
medium: percentile_at(&sorted, PriorityLevel::Medium) as f64,
high: percentile_at(&sorted, PriorityLevel::High) as f64,
very_high: percentile_at(&sorted, PriorityLevel::VeryHigh) as f64,
unsafe_max: percentile_at(&sorted, PriorityLevel::UnsafeMax) as f64,
}
}
#[must_use]
pub fn percentile_at(sorted: &[u64], level: PriorityLevel) -> u64 {
if sorted.is_empty() {
return 0;
}
let n = sorted.len();
let p = level.percentile();
let idx = ((p / 100.0) * (n as f64 - 1.0)).round() as usize;
sorted[idx.min(n - 1)]
}
#[cfg(test)]
#[allow(clippy::float_cmp)] mod tests {
use super::*;
#[test]
fn empty_input_returns_all_zeros() {
let levels = compute_levels(&[]);
assert_eq!(levels, PriorityFeeLevels::default());
}
#[test]
fn single_fee_returns_that_fee_at_every_level() {
let levels = compute_levels(&[1000]);
assert_eq!(levels.min, 1000.0);
assert_eq!(levels.low, 1000.0);
assert_eq!(levels.medium, 1000.0);
assert_eq!(levels.high, 1000.0);
assert_eq!(levels.very_high, 1000.0);
assert_eq!(levels.unsafe_max, 1000.0);
}
#[test]
fn monotonic_ladder_across_distribution() {
let fees: Vec<u64> = (1..=100).map(|i| i * 1000).collect();
let levels = compute_levels(&fees);
assert!(levels.min <= levels.low);
assert!(levels.low <= levels.medium);
assert!(levels.medium <= levels.high);
assert!(levels.high <= levels.very_high);
assert!(levels.very_high <= levels.unsafe_max);
assert_eq!(levels.min, 1_000.0);
assert_eq!(levels.medium, 51_000.0);
assert_eq!(levels.unsafe_max, 100_000.0);
}
#[test]
fn input_order_doesnt_affect_output() {
let ordered = vec![100u64, 200, 300, 400, 500, 600, 700, 800, 900, 1000];
let shuffled = vec![500u64, 1000, 300, 700, 100, 900, 200, 800, 400, 600];
assert_eq!(compute_levels(&ordered), compute_levels(&shuffled));
}
#[test]
fn percentile_at_is_consistent_with_compute_levels() {
let mut sorted = vec![10u64, 20, 30, 40, 50, 60, 70, 80, 90, 100];
sorted.sort_unstable();
let levels = compute_levels(&sorted);
assert_eq!(
percentile_at(&sorted, PriorityLevel::Min) as f64,
levels.min
);
assert_eq!(
percentile_at(&sorted, PriorityLevel::Medium) as f64,
levels.medium
);
assert_eq!(
percentile_at(&sorted, PriorityLevel::UnsafeMax) as f64,
levels.unsafe_max
);
}
#[test]
fn priority_level_serializes_camel_case() {
let json = serde_json::to_string(&PriorityLevel::VeryHigh).unwrap();
assert_eq!(json, "\"veryHigh\"");
let json = serde_json::to_string(&PriorityLevel::UnsafeMax).unwrap();
assert_eq!(json, "\"unsafeMax\"");
}
}