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