1use std::collections::HashMap;
9
10use solana_address::Address;
11
12use super::record::{LegRecord, SwapMode};
13use crate::mints;
14
15const LAMPORTS_PER_SOL: f64 = 1e9;
16const ONE_SOL_LAMPORTS: u128 = 1_000_000_000;
18const USDC_UNITS_PER_USD: f64 = 1e6;
19const MIN_SAMPLES: usize = 3;
21
22struct Sample {
25 input_mint: Address,
26 output_mint: Address,
27 amount: u128,
28 original_output: u128,
29}
30
31#[derive(Default)]
34pub struct Samples(Vec<Sample>);
35
36impl Samples {
37 pub fn push(&mut self, record: &LegRecord) {
41 let Some(original_output) = record.original_realized_output_amount else {
42 return;
43 };
44 if record.swap_mode != SwapMode::ExactIn
45 || record.amount == 0
46 || original_output == 0
47 || record.original_failed
48 {
49 return;
50 }
51 if !mints::is_major(&record.input_mint) && !mints::is_major(&record.output_mint) {
53 return;
54 }
55 self.0.push(Sample {
56 input_mint: record.input_mint,
57 output_mint: record.output_mint,
58 amount: u128::from(record.amount),
59 original_output: u128::from(original_output),
60 });
61 }
62
63 pub fn prices(&self) -> Option<Prices> {
65 let sol_usd = sol_price(&self.0)?;
66 let mut prices = Prices {
67 sol_usd,
68 priced: priced_major_prices(&self.0, sol_usd),
69 exotic: HashMap::new(),
70 };
71 prices.exotic = exotic_unit_prices(&self.0, &prices);
72 Some(prices)
73 }
74}
75
76pub struct Prices {
78 sol_usd: f64,
79 priced: HashMap<Address, f64>,
81 exotic: HashMap<Address, f64>,
83}
84
85impl Prices {
86 pub const fn sol_usd(&self) -> f64 {
87 self.sol_usd
88 }
89
90 pub fn usd(&self, mint: &Address, raw: f64) -> Option<f64> {
96 self.scaled_major(mint, raw)
97 .or_else(|| self.exotic.get(mint).map(|unit| raw * unit))
98 }
99
100 pub fn usd_str(&self, mint: &str, raw: f64) -> Option<f64> {
102 self.usd(&mint.parse().ok()?, raw)
103 }
104
105 fn scaled_major(&self, mint: &Address, raw: f64) -> Option<f64> {
107 if *mint == mints::SOL {
108 return Some(raw / LAMPORTS_PER_SOL * self.sol_usd);
109 }
110 if let Some(decimals) = mints::stable_decimals(mint) {
111 return Some(raw / 10f64.powi(decimals as i32));
112 }
113 let decimals = mints::priced_decimals(mint)?;
114 let unit = self.priced.get(mint)?;
115 Some(raw / 10f64.powi(decimals as i32) * unit)
116 }
117}
118
119fn median(values: &mut [f64]) -> f64 {
120 values.sort_by(|a, b| a.total_cmp(b));
121 let n = values.len();
122 match n % 2 {
123 1 => values[n / 2],
124 _ => (values[n / 2 - 1] + values[n / 2]) / 2.0,
125 }
126}
127
128fn median_of(mut values: Vec<f64>) -> Option<f64> {
129 (values.len() >= MIN_SAMPLES).then(|| median(&mut values))
130}
131
132fn sol_price(samples: &[Sample]) -> Option<f64> {
133 let mut ratios: Vec<f64> = samples
134 .iter()
135 .filter_map(|sample| {
136 let (input, output) = (sample.input_mint, sample.output_mint);
137 if input == mints::SOL && output == mints::USDC && sample.amount >= ONE_SOL_LAMPORTS {
138 Some(
139 (sample.original_output as f64 / USDC_UNITS_PER_USD)
140 / (sample.amount as f64 / LAMPORTS_PER_SOL),
141 )
142 } else if input == mints::USDC
143 && output == mints::SOL
144 && sample.original_output >= ONE_SOL_LAMPORTS
145 {
146 Some(
147 (sample.amount as f64 / USDC_UNITS_PER_USD)
148 / (sample.original_output as f64 / LAMPORTS_PER_SOL),
149 )
150 } else {
151 None
152 }
153 })
154 .collect();
155 (!ratios.is_empty()).then(|| median(&mut ratios))
156}
157
158fn priced_major_prices(samples: &[Sample], sol_usd: f64) -> HashMap<Address, f64> {
160 let mut per_mint: HashMap<Address, Vec<f64>> = HashMap::new();
161 for sample in samples {
162 let oriented = [
163 (
164 sample.input_mint,
165 sample.output_mint,
166 sample.amount,
167 sample.original_output,
168 ),
169 (
170 sample.output_mint,
171 sample.input_mint,
172 sample.original_output,
173 sample.amount,
174 ),
175 ];
176 for (mint, other, mint_raw, other_raw) in oriented {
177 let Some(decimals) = mints::priced_decimals(&mint) else {
178 continue;
179 };
180 let other_usd = if other == mints::USDC || other == mints::USDT {
181 other_raw as f64 / USDC_UNITS_PER_USD
182 } else if other == mints::SOL {
183 other_raw as f64 / LAMPORTS_PER_SOL * sol_usd
184 } else {
185 continue;
186 };
187 per_mint
188 .entry(mint)
189 .or_default()
190 .push(other_usd / (mint_raw as f64 / 10f64.powi(decimals as i32)));
191 }
192 }
193 per_mint
194 .into_iter()
195 .filter_map(|(mint, ratios)| Some((mint, median_of(ratios)?)))
196 .collect()
197}
198
199fn exotic_unit_prices(samples: &[Sample], prices: &Prices) -> HashMap<Address, f64> {
201 let mut per_mint: HashMap<Address, Vec<f64>> = HashMap::new();
202 for sample in samples {
203 let (input, output) = (sample.input_mint, sample.output_mint);
204 match (mints::is_major(&input), mints::is_major(&output)) {
205 (true, false) => {
206 if let Some(usd) = prices.scaled_major(&input, sample.amount as f64) {
207 per_mint
208 .entry(output)
209 .or_default()
210 .push(usd / sample.original_output as f64);
211 }
212 }
213 (false, true) => {
214 if let Some(usd) = prices.scaled_major(&output, sample.original_output as f64) {
215 per_mint
216 .entry(input)
217 .or_default()
218 .push(usd / sample.amount as f64);
219 }
220 }
221 _ => {}
222 }
223 }
224 per_mint
225 .into_iter()
226 .filter_map(|(mint, ratios)| Some((mint, median_of(ratios)?)))
227 .collect()
228}
229
230#[cfg(test)]
231mod tests {
232 use solana_address::address;
233
234 use super::*;
235
236 const BONK: Address = address!("DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263");
238 const RAY: Address = address!("4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R");
240 const SOL_USD: f64 = 100.0;
242
243 fn sample_record(
244 input: Address,
245 output: Address,
246 amount: u64,
247 original_output: u64,
248 ) -> LegRecord {
249 LegRecord {
250 original_signature: "sig".to_string(),
251 input_mint: input,
252 output_mint: output,
253 amount,
254 swap_mode: SwapMode::ExactIn,
255 failed: false,
256 original_failed: false,
257 realized_output_amount: None,
258 original_realized_output_amount: Some(original_output),
259 route_plan: None,
260 original_route_plan: None,
261 realized_route_plan: None,
262 }
263 }
264
265 fn anchored() -> Samples {
268 let mut samples = Samples::default();
269 for (amount, output) in [
270 (1_000_000_000, 100_000_000),
271 (2_000_000_000, 200_000_000),
272 (3_000_000_000, 300_000_000),
273 ] {
274 samples.push(&sample_record(mints::SOL, mints::USDC, amount, output));
275 }
276 samples
277 }
278
279 #[test]
282 fn the_sol_anchor_ignores_swaps_under_one_sol() {
283 let mut samples = Samples::default();
284 for (amount, output) in [
287 (1_000, 1_000),
288 (2_000, 2_000),
289 (1_000_000_000, 100_000_000),
290 (2_000_000_000, 200_000_000),
291 ] {
292 samples.push(&sample_record(mints::SOL, mints::USDC, amount, output));
293 }
294 let prices = samples.prices().expect("anchored");
295 assert_eq!(prices.sol_usd(), SOL_USD);
296 }
297
298 #[test]
301 fn a_priced_major_medians_both_orientations_of_its_legs() {
302 let cbbtc = mints::PRICED_MAJORS[1].address;
303 assert_eq!(mints::priced_decimals(&cbbtc), Some(8));
304 let one_cbbtc: u64 = 1_00000000;
305
306 let mut samples = anchored();
307 for (input, output, amount, original_output) in [
309 (cbbtc, mints::USDC, one_cbbtc, 59_000_000_000),
310 (cbbtc, mints::USDC, 2 * one_cbbtc, 120_000_000_000),
311 (mints::USDC, cbbtc, 61_000_000_000, one_cbbtc),
312 ] {
313 samples.push(&sample_record(input, output, amount, original_output));
314 }
315
316 let prices = samples.prices().expect("anchored");
317 assert_eq!(prices.usd(&cbbtc, one_cbbtc as f64), Some(60_000.0));
318 assert_eq!(prices.usd(&cbbtc, one_cbbtc as f64 / 2.0), Some(30_000.0));
320 }
321
322 fn unit_price(lamports: f64, exotic_raw: f64) -> f64 {
325 lamports / LAMPORTS_PER_SOL * SOL_USD / exotic_raw
326 }
327
328 #[test]
331 fn an_exotic_is_priced_per_raw_unit_off_its_major_leg() {
332 let mut samples = anchored();
333 for (lamports, bonk) in [
336 (1_000_000_000, 1_000_000_000_000),
337 (2_000_000_000, 4_000_000_000_000),
338 (3_000_000_000, 3_000_000_000_000),
339 ] {
340 samples.push(&sample_record(mints::SOL, BONK, lamports, bonk));
341 }
342
343 let prices = samples.prices().expect("anchored");
344 let unit = unit_price(1_000_000_000.0, 1_000_000_000_000.0);
345 assert_eq!(prices.usd(&BONK, 1e12), Some(1e12 * unit));
346 assert_eq!(prices.usd(&BONK, 5e11), Some(5e11 * unit));
347 }
348
349 #[test]
353 fn a_stable_is_valued_at_its_own_decimals() {
354 let usde_mint = mints::STABLES[4].address;
355 let mut samples = anchored();
356 for _ in 0..3 {
357 samples.push(&sample_record(
358 usde_mint,
359 mints::USDC,
360 1_000_000_000,
361 1_000_000,
362 ));
363 }
364 let prices = samples.prices().expect("anchored");
365 assert_eq!(prices.usd(&usde_mint, 1e6), Some(1e6 / 1e9));
366 assert_eq!(prices.usd(&mints::USDC, 1e6), Some(1.0));
367 assert_eq!(prices.usd(&usde_mint, 1e9), Some(1.0));
369 assert_eq!(prices.usd(&mints::SOL, 1e9), Some(SOL_USD));
370 }
371
372 #[test]
373 fn a_price_needs_enough_samples() {
374 assert_eq!(median_of(vec![1.0, 2.0]), None);
375 assert_eq!(median_of(vec![1.0, 2.0, 3.0]), Some(2.0));
376 assert_eq!(median_of(vec![4.0, 1.0, 2.0, 3.0]), Some(2.5));
377 }
378
379 #[test]
380 fn an_unpriceable_mint_is_none_rather_than_zero() {
381 let prices = anchored().prices().expect("anchored");
382 assert_eq!(prices.usd_str("SomeLongtailMintNobodyQuoted", 1e6), None);
383 assert_eq!(prices.usd(&RAY, 1e6), None);
385 }
386}