use std::collections::HashMap;
use solana_address::Address;
use super::record::{LegRecord, SwapMode};
use crate::mints;
const LAMPORTS_PER_SOL: f64 = 1e9;
const ONE_SOL_LAMPORTS: u128 = 1_000_000_000;
const USDC_UNITS_PER_USD: f64 = 1e6;
const MIN_SAMPLES: usize = 3;
struct Sample {
input_mint: Address,
output_mint: Address,
amount: u128,
original_output: u128,
}
#[derive(Default)]
pub struct Samples(Vec<Sample>);
impl Samples {
pub fn push(&mut self, record: &LegRecord) {
let Some(original_output) = record.original_realized_output_amount else {
return;
};
if record.swap_mode != SwapMode::ExactIn
|| record.amount == 0
|| original_output == 0
|| record.original_failed
{
return;
}
if !mints::is_major(&record.input_mint) && !mints::is_major(&record.output_mint) {
return;
}
self.0.push(Sample {
input_mint: record.input_mint,
output_mint: record.output_mint,
amount: u128::from(record.amount),
original_output: u128::from(original_output),
});
}
pub fn prices(&self) -> Option<Prices> {
let sol_usd = sol_price(&self.0)?;
let mut prices = Prices {
sol_usd,
priced: priced_major_prices(&self.0, sol_usd),
exotic: HashMap::new(),
};
prices.exotic = exotic_unit_prices(&self.0, &prices);
Some(prices)
}
}
pub struct Prices {
sol_usd: f64,
priced: HashMap<Address, f64>,
exotic: HashMap<Address, f64>,
}
impl Prices {
pub const fn sol_usd(&self) -> f64 {
self.sol_usd
}
pub fn usd(&self, mint: &Address, raw: f64) -> Option<f64> {
self.scaled_major(mint, raw)
.or_else(|| self.exotic.get(mint).map(|unit| raw * unit))
}
pub fn usd_str(&self, mint: &str, raw: f64) -> Option<f64> {
self.usd(&mint.parse().ok()?, raw)
}
fn scaled_major(&self, mint: &Address, raw: f64) -> Option<f64> {
if *mint == mints::SOL {
return Some(raw / LAMPORTS_PER_SOL * self.sol_usd);
}
if let Some(decimals) = mints::stable_decimals(mint) {
return Some(raw / 10f64.powi(decimals as i32));
}
let decimals = mints::priced_decimals(mint)?;
let unit = self.priced.get(mint)?;
Some(raw / 10f64.powi(decimals as i32) * unit)
}
}
fn median(values: &mut [f64]) -> f64 {
values.sort_by(|a, b| a.total_cmp(b));
let n = values.len();
match n % 2 {
1 => values[n / 2],
_ => (values[n / 2 - 1] + values[n / 2]) / 2.0,
}
}
fn median_of(mut values: Vec<f64>) -> Option<f64> {
(values.len() >= MIN_SAMPLES).then(|| median(&mut values))
}
fn sol_price(samples: &[Sample]) -> Option<f64> {
let mut ratios: Vec<f64> = samples
.iter()
.filter_map(|sample| {
let (input, output) = (sample.input_mint, sample.output_mint);
if input == mints::SOL && output == mints::USDC && sample.amount >= ONE_SOL_LAMPORTS {
Some(
(sample.original_output as f64 / USDC_UNITS_PER_USD)
/ (sample.amount as f64 / LAMPORTS_PER_SOL),
)
} else if input == mints::USDC
&& output == mints::SOL
&& sample.original_output >= ONE_SOL_LAMPORTS
{
Some(
(sample.amount as f64 / USDC_UNITS_PER_USD)
/ (sample.original_output as f64 / LAMPORTS_PER_SOL),
)
} else {
None
}
})
.collect();
(!ratios.is_empty()).then(|| median(&mut ratios))
}
fn priced_major_prices(samples: &[Sample], sol_usd: f64) -> HashMap<Address, f64> {
let mut per_mint: HashMap<Address, Vec<f64>> = HashMap::new();
for sample in samples {
let oriented = [
(
sample.input_mint,
sample.output_mint,
sample.amount,
sample.original_output,
),
(
sample.output_mint,
sample.input_mint,
sample.original_output,
sample.amount,
),
];
for (mint, other, mint_raw, other_raw) in oriented {
let Some(decimals) = mints::priced_decimals(&mint) else {
continue;
};
let other_usd = if other == mints::USDC || other == mints::USDT {
other_raw as f64 / USDC_UNITS_PER_USD
} else if other == mints::SOL {
other_raw as f64 / LAMPORTS_PER_SOL * sol_usd
} else {
continue;
};
per_mint
.entry(mint)
.or_default()
.push(other_usd / (mint_raw as f64 / 10f64.powi(decimals as i32)));
}
}
per_mint
.into_iter()
.filter_map(|(mint, ratios)| Some((mint, median_of(ratios)?)))
.collect()
}
fn exotic_unit_prices(samples: &[Sample], prices: &Prices) -> HashMap<Address, f64> {
let mut per_mint: HashMap<Address, Vec<f64>> = HashMap::new();
for sample in samples {
let (input, output) = (sample.input_mint, sample.output_mint);
match (mints::is_major(&input), mints::is_major(&output)) {
(true, false) => {
if let Some(usd) = prices.scaled_major(&input, sample.amount as f64) {
per_mint
.entry(output)
.or_default()
.push(usd / sample.original_output as f64);
}
}
(false, true) => {
if let Some(usd) = prices.scaled_major(&output, sample.original_output as f64) {
per_mint
.entry(input)
.or_default()
.push(usd / sample.amount as f64);
}
}
_ => {}
}
}
per_mint
.into_iter()
.filter_map(|(mint, ratios)| Some((mint, median_of(ratios)?)))
.collect()
}
#[cfg(test)]
mod tests {
use solana_address::address;
use super::*;
const BONK: Address = address!("DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263");
const RAY: Address = address!("4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R");
const SOL_USD: f64 = 100.0;
fn sample_record(
input: Address,
output: Address,
amount: u64,
original_output: u64,
) -> LegRecord {
LegRecord {
original_signature: "sig".to_string(),
input_mint: input,
output_mint: output,
amount,
swap_mode: SwapMode::ExactIn,
failed: false,
original_failed: false,
realized_output_amount: None,
original_realized_output_amount: Some(original_output),
route_plan: None,
original_route_plan: None,
realized_route_plan: None,
}
}
fn anchored() -> Samples {
let mut samples = Samples::default();
for (amount, output) in [
(1_000_000_000, 100_000_000),
(2_000_000_000, 200_000_000),
(3_000_000_000, 300_000_000),
] {
samples.push(&sample_record(mints::SOL, mints::USDC, amount, output));
}
samples
}
#[test]
fn the_sol_anchor_ignores_swaps_under_one_sol() {
let mut samples = Samples::default();
for (amount, output) in [
(1_000, 1_000),
(2_000, 2_000),
(1_000_000_000, 100_000_000),
(2_000_000_000, 200_000_000),
] {
samples.push(&sample_record(mints::SOL, mints::USDC, amount, output));
}
let prices = samples.prices().expect("anchored");
assert_eq!(prices.sol_usd(), SOL_USD);
}
#[test]
fn a_priced_major_medians_both_orientations_of_its_legs() {
let cbbtc = mints::PRICED_MAJORS[1].address;
assert_eq!(mints::priced_decimals(&cbbtc), Some(8));
let one_cbbtc: u64 = 1_00000000;
let mut samples = anchored();
for (input, output, amount, original_output) in [
(cbbtc, mints::USDC, one_cbbtc, 59_000_000_000),
(cbbtc, mints::USDC, 2 * one_cbbtc, 120_000_000_000),
(mints::USDC, cbbtc, 61_000_000_000, one_cbbtc),
] {
samples.push(&sample_record(input, output, amount, original_output));
}
let prices = samples.prices().expect("anchored");
assert_eq!(prices.usd(&cbbtc, one_cbbtc as f64), Some(60_000.0));
assert_eq!(prices.usd(&cbbtc, one_cbbtc as f64 / 2.0), Some(30_000.0));
}
fn unit_price(lamports: f64, exotic_raw: f64) -> f64 {
lamports / LAMPORTS_PER_SOL * SOL_USD / exotic_raw
}
#[test]
fn an_exotic_is_priced_per_raw_unit_off_its_major_leg() {
let mut samples = anchored();
for (lamports, bonk) in [
(1_000_000_000, 1_000_000_000_000),
(2_000_000_000, 4_000_000_000_000),
(3_000_000_000, 3_000_000_000_000),
] {
samples.push(&sample_record(mints::SOL, BONK, lamports, bonk));
}
let prices = samples.prices().expect("anchored");
let unit = unit_price(1_000_000_000.0, 1_000_000_000_000.0);
assert_eq!(prices.usd(&BONK, 1e12), Some(1e12 * unit));
assert_eq!(prices.usd(&BONK, 5e11), Some(5e11 * unit));
}
#[test]
fn a_stable_is_valued_at_its_own_decimals() {
let usde_mint = mints::STABLES[4].address;
let mut samples = anchored();
for _ in 0..3 {
samples.push(&sample_record(
usde_mint,
mints::USDC,
1_000_000_000,
1_000_000,
));
}
let prices = samples.prices().expect("anchored");
assert_eq!(prices.usd(&usde_mint, 1e6), Some(1e6 / 1e9));
assert_eq!(prices.usd(&mints::USDC, 1e6), Some(1.0));
assert_eq!(prices.usd(&usde_mint, 1e9), Some(1.0));
assert_eq!(prices.usd(&mints::SOL, 1e9), Some(SOL_USD));
}
#[test]
fn a_price_needs_enough_samples() {
assert_eq!(median_of(vec![1.0, 2.0]), None);
assert_eq!(median_of(vec![1.0, 2.0, 3.0]), Some(2.0));
assert_eq!(median_of(vec![4.0, 1.0, 2.0, 3.0]), Some(2.5));
}
#[test]
fn an_unpriceable_mint_is_none_rather_than_zero() {
let prices = anchored().prices().expect("anchored");
assert_eq!(prices.usd_str("SomeLongtailMintNobodyQuoted", 1e6), None);
assert_eq!(prices.usd(&RAY, 1e6), None);
}
}