use std::sync::{OnceLock, RwLock};
use std::time::Duration;
use solana_commitment_config::CommitmentConfig;
use solana_pubkey::Pubkey;
use solana_rpc_client::nonblocking::rpc_client::RpcClient;
use tracing::warn;
use crate::tui::config::{rpc_http_url_from_env, DEFAULT_COMPUTE_UNIT_PRICE_MICRO_LAMPORTS};
const REFRESH_INTERVAL: Duration = Duration::from_secs(10);
const QUERY_TIMEOUT: Duration = Duration::from_secs(3);
const FEE_PERCENTILE: usize = 90;
const MIN_AUTO_PRIORITY_FEE: u64 = DEFAULT_COMPUTE_UNIT_PRICE_MICRO_LAMPORTS;
const PHOENIX_HOT_ACCOUNTS: &[&str] = &[
"EtrnLzgbS7nMMy5fbD42kXiUzGg8XQzJ972Xtk1cjWih",
"GdxfTLSsdSY37G6fZoYtdGDSfgFnbT2EmRpuePZxWShS",
"2zskx2iyCvb6Stg7RBZkt1f6MrF4dpYtMG3yMvKwqtUZ",
"2nHGAaEw3D5dd4hVueaUNoygkQFmoeKqRQWnSPqSMFUC",
"HCrPXLByGqRh2szQi3gj7oRdRVBNi1gccAyn4CQCT3HK",
"2U32rSzzrQS3eVmGHsnbw5kcqKF3wQXpHGd3hMq5YJok",
];
fn phoenix_hot_accounts() -> &'static [Pubkey] {
static PARSED: OnceLock<Vec<Pubkey>> = OnceLock::new();
PARSED.get_or_init(|| {
PHOENIX_HOT_ACCOUNTS
.iter()
.filter_map(|s| match s.parse::<Pubkey>() {
Ok(pk) => Some(pk),
Err(e) => {
warn!(account = %s, error = %e, "failed to parse Phoenix hot account");
None
}
})
.collect()
})
}
fn cache() -> &'static RwLock<Option<u64>> {
static CACHE: OnceLock<RwLock<Option<u64>>> = OnceLock::new();
CACHE.get_or_init(|| RwLock::new(None))
}
pub fn current_auto_priority_fee() -> Option<u64> {
cache().read().ok().and_then(|g| *g)
}
fn store(fee: u64) {
if let Ok(mut g) = cache().write() {
*g = Some(fee);
}
}
fn percentile(samples: &mut [u64], pct: usize) -> Option<u64> {
if samples.is_empty() {
return None;
}
samples.sort_unstable();
let n = samples.len();
let idx = (pct * n).div_ceil(100);
let idx = idx.saturating_sub(1).min(n - 1);
Some(samples[idx])
}
fn bounded_percentile(samples: &mut [u64]) -> Option<u64> {
percentile(samples, FEE_PERCENTILE).map(|fee| fee.max(MIN_AUTO_PRIORITY_FEE))
}
pub fn spawn_auto_priority_fee_refresh() {
static SPAWNED: OnceLock<()> = OnceLock::new();
if SPAWNED.set(()).is_err() {
return;
}
tokio::spawn(async move {
let mut interval = tokio::time::interval(REFRESH_INTERVAL);
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
let mut current_url: Option<String> = None;
let mut client: Option<RpcClient> = None;
let accounts = phoenix_hot_accounts();
loop {
interval.tick().await;
let url = rpc_http_url_from_env();
if current_url.as_deref() != Some(url.as_str()) {
client = Some(RpcClient::new_with_commitment(
url.clone(),
CommitmentConfig::processed(),
));
current_url = Some(url);
}
let Some(rpc) = client.as_ref() else { continue };
let fetch =
tokio::time::timeout(QUERY_TIMEOUT, rpc.get_recent_prioritization_fees(accounts));
let Ok(Ok(fees)) = fetch.await else { continue };
let mut samples: Vec<u64> = fees.into_iter().map(|f| f.prioritization_fee).collect();
if let Some(fee) = bounded_percentile(&mut samples) {
store(fee);
}
}
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn percentile_handles_empty_slice() {
let mut empty: Vec<u64> = vec![];
assert_eq!(percentile(&mut empty, 75), None);
}
#[test]
fn percentile_p75_picks_nearest_rank() {
let mut v: Vec<u64> = (1..=100).collect();
assert_eq!(percentile(&mut v, 75), Some(75));
}
#[test]
fn percentile_p100_picks_max() {
let mut v = vec![10, 20, 30, 40, 50];
assert_eq!(percentile(&mut v, 100), Some(50));
}
#[test]
fn percentile_handles_single_element() {
let mut v = vec![42];
assert_eq!(percentile(&mut v, 75), Some(42));
}
#[test]
fn percentile_handles_all_zeros() {
let mut v = vec![0u64; 50];
assert_eq!(percentile(&mut v, 75), Some(0));
}
#[test]
fn bounded_percentile_floors_zero_at_min() {
let mut v = vec![0u64; 100];
assert_eq!(bounded_percentile(&mut v), Some(MIN_AUTO_PRIORITY_FEE));
}
#[test]
fn bounded_percentile_returns_none_for_empty() {
let mut v: Vec<u64> = vec![];
assert_eq!(bounded_percentile(&mut v), None);
}
#[test]
fn bounded_percentile_passes_through_high_values() {
let mut v: Vec<u64> = (1..=100).map(|i| i * 1_000).collect();
assert_eq!(bounded_percentile(&mut v), Some(90_000));
}
#[test]
fn phoenix_hot_accounts_all_parse() {
assert_eq!(phoenix_hot_accounts().len(), PHOENIX_HOT_ACCOUNTS.len());
}
}