1use std::collections::HashMap;
7
8use solana_pubkey::Pubkey;
9
10use pyra_margin::{get_kamino_borrow_balance, get_kamino_deposit_balance};
11use pyra_types::{Cache, KaminoObligation, KaminoReserve, KAMINO_FRACTION_SCALE};
12
13use crate::{RedisClient, RedisError, RedisKey, RedisResult};
14
15fn market_price_from_reserve(reserve: &KaminoReserve) -> f64 {
19 reserve.liquidity.market_price_sf as f64 / KAMINO_FRACTION_SCALE as f64
20}
21
22fn balance_to_value_cents(token_balance: i128, decimals: u32, price: f64) -> RedisResult<i64> {
29 let decimals_pow = 10_f64.powi(i32::try_from(decimals).map_err(|_| RedisError::MathOverflow)?);
30 let value = (token_balance as f64) / decimals_pow * price * 100.0;
31 let rounded = value.round();
32 if rounded.is_finite() && rounded >= i64::MIN as f64 && rounded <= i64::MAX as f64 {
33 Ok(rounded as i64)
34 } else {
35 Err(RedisError::MathOverflow)
36 }
37}
38
39pub struct VaultKaminoPositionData {
43 pub obligation: Cache<KaminoObligation>,
45 pub reserves: HashMap<Pubkey, KaminoReserve>,
47 pub prices: HashMap<Pubkey, f64>,
49}
50
51pub struct AllKaminoPositionsData {
53 pub obligations: Vec<(Pubkey, Cache<KaminoObligation>)>,
55 pub reserves: HashMap<Pubkey, KaminoReserve>,
57 pub prices: HashMap<Pubkey, f64>,
59}
60
61impl RedisClient {
64 pub async fn fetch_vault_kamino_position_data(
71 &self,
72 vault_address: &Pubkey,
73 lending_market: &Pubkey,
74 reserve_pubkeys: &[Pubkey],
75 ) -> RedisResult<VaultKaminoPositionData> {
76 let obligation_key = RedisKey::kamino_obligation(vault_address, lending_market).to_string();
77 let mut keys = vec![obligation_key];
78 for pk in reserve_pubkeys {
79 keys.push(RedisKey::kamino_reserve(pk).to_string());
80 }
81
82 let values = self.mget(&keys).await?;
83
84 let obligation_raw = values
85 .first()
86 .and_then(|v| v.as_ref())
87 .ok_or_else(|| RedisError::NotFound("KaminoObligation not found in Redis".into()))?;
88 let obligation: Cache<KaminoObligation> = serde_json::from_str(obligation_raw)?;
89
90 let mut reserves: HashMap<Pubkey, KaminoReserve> = HashMap::new();
91 let mut prices: HashMap<Pubkey, f64> = HashMap::new();
92
93 for (i, pk) in reserve_pubkeys.iter().enumerate() {
94 if let Some(Some(raw)) =
95 values.get(1usize.checked_add(i).ok_or(RedisError::MathOverflow)?)
96 {
97 if let Ok(cached) = serde_json::from_str::<Cache<KaminoReserve>>(raw) {
98 let price = market_price_from_reserve(&cached.account);
99 prices.insert(*pk, price);
100 reserves.insert(*pk, cached.account);
101 }
102 }
103 }
104
105 Ok(VaultKaminoPositionData {
106 obligation,
107 reserves,
108 prices,
109 })
110 }
111
112 pub async fn fetch_all_kamino_positions(
117 &self,
118 reserve_pubkeys: &[Pubkey],
119 ) -> RedisResult<AllKaminoPositionsData> {
120 let obligation_keys = self.scan_keys(&RedisKey::kamino_obligation_glob()).await?;
122
123 let prefix_with_colon = format!("{}:", RedisKey::KAMINO_OBLIGATION_PREFIX);
124 let vault_addresses: Vec<Option<Pubkey>> = obligation_keys
125 .iter()
126 .map(|k| {
127 k.strip_prefix(prefix_with_colon.as_str())
128 .and_then(|rest| rest.split(':').next())
129 .and_then(|s| s.parse::<Pubkey>().ok())
130 })
131 .collect();
132
133 let num_obligations = obligation_keys.len();
135 let mut all_keys: Vec<String> = obligation_keys;
136
137 for pk in reserve_pubkeys {
138 all_keys.push(RedisKey::kamino_reserve(pk).to_string());
139 }
140
141 let values = self.mget(&all_keys).await?;
142
143 let mut obligations: Vec<(Pubkey, Cache<KaminoObligation>)> = Vec::new();
145 for (i, vault_pk) in vault_addresses.iter().enumerate() {
146 if let (Some(pk), Some(Some(raw))) = (vault_pk, values.get(i)) {
147 if let Ok(cached) = serde_json::from_str::<Cache<KaminoObligation>>(raw) {
148 obligations.push((*pk, cached));
149 }
150 }
151 }
152
153 let mut reserves: HashMap<Pubkey, KaminoReserve> = HashMap::new();
155 let mut prices: HashMap<Pubkey, f64> = HashMap::new();
156
157 for (i, pk) in reserve_pubkeys.iter().enumerate() {
158 let offset = num_obligations
159 .checked_add(i)
160 .ok_or(RedisError::MathOverflow)?;
161 if let Some(Some(raw)) = values.get(offset) {
162 if let Ok(cached) = serde_json::from_str::<Cache<KaminoReserve>>(raw) {
163 let price = market_price_from_reserve(&cached.account);
164 prices.insert(*pk, price);
165 reserves.insert(*pk, cached.account);
166 }
167 }
168 }
169
170 Ok(AllKaminoPositionsData {
171 obligations,
172 reserves,
173 prices,
174 })
175 }
176}
177
178pub fn compute_kamino_position_values(data: &VaultKaminoPositionData) -> RedisResult<Vec<i64>> {
184 compute_user_kamino_position_values(&data.obligation.account, &data.reserves, &data.prices)
185}
186
187pub fn compute_kamino_asset_data(
189 data: &VaultKaminoPositionData,
190) -> RedisResult<Vec<(Pubkey, i64, i64)>> {
191 compute_user_kamino_asset_data(&data.obligation.account, &data.reserves, &data.prices)
192}
193
194pub fn compute_user_kamino_position_values(
197 obligation: &KaminoObligation,
198 reserves: &HashMap<Pubkey, KaminoReserve>,
199 prices: &HashMap<Pubkey, f64>,
200) -> RedisResult<Vec<i64>> {
201 let mut results = Vec::new();
202
203 for deposit in &obligation.deposits {
204 if deposit.deposited_amount == 0 {
205 continue;
206 }
207 let Some(reserve) = reserves.get(&deposit.deposit_reserve) else {
208 continue;
209 };
210 let Some(&price) = prices.get(&deposit.deposit_reserve) else {
211 continue;
212 };
213 let balance = get_kamino_deposit_balance(deposit, reserve)?;
214 let decimals =
215 u32::try_from(reserve.liquidity.mint_decimals).map_err(|_| RedisError::MathOverflow)?;
216 let cents = balance_to_value_cents(balance, decimals, price)?;
217 results.push(cents);
218 }
219
220 for borrow in &obligation.borrows {
221 if borrow.borrowed_amount_sf == 0 {
222 continue;
223 }
224 let Some(reserve) = reserves.get(&borrow.borrow_reserve) else {
225 continue;
226 };
227 let Some(&price) = prices.get(&borrow.borrow_reserve) else {
228 continue;
229 };
230 let balance = get_kamino_borrow_balance(borrow, reserve)?;
231 let decimals =
232 u32::try_from(reserve.liquidity.mint_decimals).map_err(|_| RedisError::MathOverflow)?;
233 let cents = balance_to_value_cents(balance, decimals, price)?;
234 results.push(cents);
235 }
236
237 Ok(results)
238}
239
240pub fn compute_user_kamino_asset_data(
244 obligation: &KaminoObligation,
245 reserves: &HashMap<Pubkey, KaminoReserve>,
246 prices: &HashMap<Pubkey, f64>,
247) -> RedisResult<Vec<(Pubkey, i64, i64)>> {
248 let mut results = Vec::new();
249
250 for deposit in &obligation.deposits {
251 if deposit.deposited_amount == 0 {
252 continue;
253 }
254 let Some(reserve) = reserves.get(&deposit.deposit_reserve) else {
255 continue;
256 };
257 let Some(&price) = prices.get(&deposit.deposit_reserve) else {
258 continue;
259 };
260 let balance_i128 = get_kamino_deposit_balance(deposit, reserve)?;
261 let balance = i64::try_from(balance_i128).map_err(|_| RedisError::MathOverflow)?;
262 let decimals =
263 u32::try_from(reserve.liquidity.mint_decimals).map_err(|_| RedisError::MathOverflow)?;
264 let cents = balance_to_value_cents(balance_i128, decimals, price)?;
265 results.push((deposit.deposit_reserve, balance, cents));
266 }
267
268 for borrow in &obligation.borrows {
269 if borrow.borrowed_amount_sf == 0 {
270 continue;
271 }
272 let Some(reserve) = reserves.get(&borrow.borrow_reserve) else {
273 continue;
274 };
275 let Some(&price) = prices.get(&borrow.borrow_reserve) else {
276 continue;
277 };
278 let balance_i128 = get_kamino_borrow_balance(borrow, reserve)?;
279 let balance = i64::try_from(balance_i128).map_err(|_| RedisError::MathOverflow)?;
280 let decimals =
281 u32::try_from(reserve.liquidity.mint_decimals).map_err(|_| RedisError::MathOverflow)?;
282 let cents = balance_to_value_cents(balance_i128, decimals, price)?;
283 results.push((borrow.borrow_reserve, balance, cents));
284 }
285
286 Ok(results)
287}
288
289#[cfg(test)]
290#[allow(
291 clippy::allow_attributes,
292 clippy::allow_attributes_without_reason,
293 clippy::unwrap_used,
294 clippy::expect_used,
295 clippy::panic,
296 clippy::arithmetic_side_effects
297)]
298mod tests {
299 use super::*;
300 use pyra_types::{
301 KaminoBigFractionBytes, KaminoBorrowRateCurve, KaminoCurvePoint,
302 KaminoObligationCollateral, KaminoObligationLiquidity, KaminoReserveCollateral,
303 KaminoReserveConfig, KaminoReserveFees, KaminoReserveLiquidity, KaminoWithdrawalCaps,
304 };
305 fn test_pubkey(seed: u8) -> Pubkey {
306 Pubkey::new_from_array([seed; 32])
307 }
308
309 const FRACTION_ONE: u128 = 1 << 60;
310
311 fn rate_to_bsf(rate: u128) -> KaminoBigFractionBytes {
312 KaminoBigFractionBytes {
313 value: [rate as u64, (rate >> 64) as u64, 0, 0],
314 }
315 }
316
317 fn make_reserve(mint_decimals: u64, market_price_sf: u128) -> KaminoReserve {
318 KaminoReserve {
319 liquidity: KaminoReserveLiquidity {
320 total_available_amount: 1_000_000,
321 cumulative_borrow_rate_bsf: rate_to_bsf(FRACTION_ONE),
322 mint_decimals,
323 market_price_sf,
324 ..Default::default()
325 },
326 collateral: KaminoReserveCollateral {
327 mint_total_supply: 1_000_000,
328 ..Default::default()
329 },
330 config: KaminoReserveConfig {
331 loan_to_value_pct: 80,
332 liquidation_threshold_pct: 85,
333 protocol_take_rate_pct: 10,
334 protocol_liquidation_fee_pct: 5,
335 borrow_factor_pct: 100,
336 deposit_limit: u64::MAX,
337 borrow_limit: u64::MAX,
338 fees: KaminoReserveFees {
339 origination_fee_sf: 0,
340 flash_loan_fee_sf: 0,
341 },
342 borrow_rate_curve: KaminoBorrowRateCurve {
343 points: [KaminoCurvePoint {
344 utilization_rate_bps: 0,
345 borrow_rate_bps: 0,
346 }; 11],
347 },
348 deposit_withdrawal_cap: KaminoWithdrawalCaps {
349 config_capacity: 0,
350 current_total: 0,
351 last_interval_start_timestamp: 0,
352 config_interval_length_seconds: 0,
353 },
354 debt_withdrawal_cap: KaminoWithdrawalCaps {
355 config_capacity: 0,
356 current_total: 0,
357 last_interval_start_timestamp: 0,
358 config_interval_length_seconds: 0,
359 },
360 elevation_groups: [0; 20],
361 ..Default::default()
362 },
363 ..Default::default()
364 }
365 }
366
367 fn make_obligation(
368 deposits_vec: Vec<KaminoObligationCollateral>,
369 borrows_vec: Vec<KaminoObligationLiquidity>,
370 ) -> KaminoObligation {
371 let mut deposits = <[KaminoObligationCollateral; 8]>::default();
372 for (i, d) in deposits_vec.into_iter().enumerate() {
373 deposits[i] = d;
374 }
375 let mut borrows = <[KaminoObligationLiquidity; 5]>::default();
376 for (i, b) in borrows_vec.into_iter().enumerate() {
377 borrows[i] = b;
378 }
379 KaminoObligation {
380 deposits,
381 borrows,
382 ..Default::default()
383 }
384 }
385
386 #[test]
387 fn market_price_extraction() {
388 let reserve = make_reserve(6, KAMINO_FRACTION_SCALE);
390 let price = market_price_from_reserve(&reserve);
391 assert!((price - 1.0).abs() < f64::EPSILON);
392 }
393
394 #[test]
395 fn market_price_extraction_150_usd() {
396 let price_sf = KAMINO_FRACTION_SCALE
398 .checked_mul(150)
399 .expect("test value fits");
400 let reserve = make_reserve(9, price_sf);
401 let price = market_price_from_reserve(&reserve);
402 assert!((price - 150.0).abs() < 0.001);
403 }
404
405 #[test]
406 fn vault_position_data_struct_construction() {
407 let reserve_pk = test_pubkey(1);
408 let reserve = make_reserve(6, KAMINO_FRACTION_SCALE);
409
410 let mut reserves = HashMap::new();
411 reserves.insert(reserve_pk, reserve);
412 let mut prices = HashMap::new();
413 prices.insert(reserve_pk, 1.0);
414
415 let deposit = KaminoObligationCollateral {
416 deposit_reserve: reserve_pk,
417 deposited_amount: 1_000_000,
418 market_value_sf: KAMINO_FRACTION_SCALE,
419 ..Default::default()
420 };
421 let obligation = make_obligation(vec![deposit], vec![]);
422
423 let data = VaultKaminoPositionData {
424 obligation: Cache {
425 account: obligation,
426 last_updated_slot: 12345,
427 },
428 reserves,
429 prices,
430 };
431
432 assert_eq!(data.obligation.last_updated_slot, 12345);
433 assert_eq!(data.reserves.len(), 1);
434 assert_eq!(data.prices.len(), 1);
435 }
436
437 #[test]
438 fn all_positions_data_struct_construction() {
439 let data = AllKaminoPositionsData {
440 obligations: vec![],
441 reserves: HashMap::new(),
442 prices: HashMap::new(),
443 };
444 assert!(data.obligations.is_empty());
445 assert!(data.reserves.is_empty());
446 assert!(data.prices.is_empty());
447 }
448
449 #[test]
450 fn compute_values_empty_obligation() {
451 let obligation = make_obligation(vec![], vec![]);
452 let data = VaultKaminoPositionData {
453 obligation: Cache {
454 account: obligation,
455 last_updated_slot: 0,
456 },
457 reserves: HashMap::new(),
458 prices: HashMap::new(),
459 };
460 let values = compute_kamino_position_values(&data).unwrap();
461 assert!(values.is_empty());
462 }
463
464 #[test]
465 fn compute_values_single_deposit() {
466 let reserve_pk = test_pubkey(1);
467 let reserve = make_reserve(6, KAMINO_FRACTION_SCALE);
469
470 let deposit = KaminoObligationCollateral {
471 deposit_reserve: reserve_pk,
472 deposited_amount: 1_000_000, market_value_sf: 0,
474 ..Default::default()
475 };
476 let obligation = make_obligation(vec![deposit], vec![]);
477
478 let mut reserves = HashMap::new();
479 reserves.insert(reserve_pk, reserve);
480 let mut prices = HashMap::new();
481 prices.insert(reserve_pk, 1.0);
482
483 let values = compute_user_kamino_position_values(&obligation, &reserves, &prices).unwrap();
484 assert_eq!(values.len(), 1);
485 assert_eq!(values[0], 100); }
487
488 #[test]
489 fn compute_values_deposit_and_borrow() {
490 let deposit_pk = test_pubkey(1);
491 let borrow_pk = test_pubkey(2);
492
493 let deposit_reserve = make_reserve(6, KAMINO_FRACTION_SCALE);
494 let borrow_reserve = make_reserve(6, KAMINO_FRACTION_SCALE);
495
496 let deposit = KaminoObligationCollateral {
497 deposit_reserve: deposit_pk,
498 deposited_amount: 2_000_000,
499 market_value_sf: 0,
500 ..Default::default()
501 };
502 let borrow = KaminoObligationLiquidity {
503 borrow_reserve: borrow_pk,
504 cumulative_borrow_rate_bsf: rate_to_bsf(FRACTION_ONE),
505 borrowed_amount_sf: 500_000 * FRACTION_ONE,
506 market_value_sf: 0,
507 borrow_factor_adjusted_market_value_sf: 0,
508 ..Default::default()
509 };
510 let obligation = make_obligation(vec![deposit], vec![borrow]);
511
512 let mut reserves = HashMap::new();
513 reserves.insert(deposit_pk, deposit_reserve);
514 reserves.insert(borrow_pk, borrow_reserve);
515 let mut prices = HashMap::new();
516 prices.insert(deposit_pk, 1.0);
517 prices.insert(borrow_pk, 1.0);
518
519 let values = compute_user_kamino_position_values(&obligation, &reserves, &prices).unwrap();
520 assert_eq!(values.len(), 2);
521 assert_eq!(values[0], 200); assert_eq!(values[1], -50); }
524
525 #[test]
526 fn compute_values_skips_zero_deposit() {
527 let reserve_pk = test_pubkey(1);
528 let reserve = make_reserve(6, KAMINO_FRACTION_SCALE);
529
530 let deposit = KaminoObligationCollateral {
531 deposit_reserve: reserve_pk,
532 deposited_amount: 0,
533 market_value_sf: 0,
534 ..Default::default()
535 };
536 let obligation = make_obligation(vec![deposit], vec![]);
537
538 let mut reserves = HashMap::new();
539 reserves.insert(reserve_pk, reserve);
540 let mut prices = HashMap::new();
541 prices.insert(reserve_pk, 1.0);
542
543 let values = compute_user_kamino_position_values(&obligation, &reserves, &prices).unwrap();
544 assert!(values.is_empty());
545 }
546
547 #[test]
548 fn compute_values_skips_missing_reserve() {
549 let reserve_pk = test_pubkey(1);
550
551 let deposit = KaminoObligationCollateral {
552 deposit_reserve: reserve_pk,
553 deposited_amount: 1_000_000,
554 market_value_sf: 0,
555 ..Default::default()
556 };
557 let obligation = make_obligation(vec![deposit], vec![]);
558
559 let reserves = HashMap::new();
560 let mut prices = HashMap::new();
561 prices.insert(reserve_pk, 1.0);
562
563 let values = compute_user_kamino_position_values(&obligation, &reserves, &prices).unwrap();
564 assert!(values.is_empty());
565 }
566
567 #[test]
568 fn compute_values_skips_missing_price() {
569 let reserve_pk = test_pubkey(1);
570 let reserve = make_reserve(6, KAMINO_FRACTION_SCALE);
571
572 let deposit = KaminoObligationCollateral {
573 deposit_reserve: reserve_pk,
574 deposited_amount: 1_000_000,
575 market_value_sf: 0,
576 ..Default::default()
577 };
578 let obligation = make_obligation(vec![deposit], vec![]);
579
580 let mut reserves = HashMap::new();
581 reserves.insert(reserve_pk, reserve);
582 let prices = HashMap::new();
583
584 let values = compute_user_kamino_position_values(&obligation, &reserves, &prices).unwrap();
585 assert!(values.is_empty());
586 }
587
588 #[test]
589 fn compute_values_high_price_sol() {
590 let reserve_pk = test_pubkey(1);
591 let price_sf = KAMINO_FRACTION_SCALE.checked_mul(150).unwrap();
593 let reserve = make_reserve(9, price_sf);
594
595 let deposit = KaminoObligationCollateral {
596 deposit_reserve: reserve_pk,
597 deposited_amount: 100_000_000, market_value_sf: 0,
599 ..Default::default()
600 };
601 let obligation = make_obligation(vec![deposit], vec![]);
602
603 let mut reserves = HashMap::new();
604 reserves.insert(reserve_pk, reserve);
605 let mut prices = HashMap::new();
606 prices.insert(reserve_pk, 150.0);
607
608 let values = compute_user_kamino_position_values(&obligation, &reserves, &prices).unwrap();
609 assert_eq!(values.len(), 1);
610 assert_eq!(values[0], 1500); }
612
613 #[test]
614 fn compute_asset_data_returns_tuples() {
615 let reserve_pk = test_pubkey(1);
616 let reserve = make_reserve(6, KAMINO_FRACTION_SCALE);
617
618 let deposit = KaminoObligationCollateral {
619 deposit_reserve: reserve_pk,
620 deposited_amount: 5_000_000,
621 market_value_sf: 0,
622 ..Default::default()
623 };
624 let obligation = make_obligation(vec![deposit], vec![]);
625
626 let mut reserves = HashMap::new();
627 reserves.insert(reserve_pk, reserve);
628 let mut prices = HashMap::new();
629 prices.insert(reserve_pk, 1.0);
630
631 let data = compute_user_kamino_asset_data(&obligation, &reserves, &prices).unwrap();
632 assert_eq!(data.len(), 1);
633 let (pk, token_balance, value_cents) = data[0];
634 assert_eq!(pk, reserve_pk);
635 assert_eq!(token_balance, 5_000_000);
636 assert_eq!(value_cents, 500); }
638
639 #[test]
640 fn compute_position_values_delegates_to_user_variant() {
641 let reserve_pk = test_pubkey(1);
642 let reserve = make_reserve(6, KAMINO_FRACTION_SCALE);
643
644 let deposit = KaminoObligationCollateral {
645 deposit_reserve: reserve_pk,
646 deposited_amount: 1_000_000,
647 market_value_sf: 0,
648 ..Default::default()
649 };
650 let obligation = Cache {
651 account: make_obligation(vec![deposit], vec![]),
652 last_updated_slot: 12345,
653 };
654
655 let mut reserves = HashMap::new();
656 reserves.insert(reserve_pk, reserve);
657 let mut prices = HashMap::new();
658 prices.insert(reserve_pk, 1.0);
659
660 let vpd = VaultKaminoPositionData {
661 obligation,
662 reserves,
663 prices,
664 };
665 let values = compute_kamino_position_values(&vpd).unwrap();
666 assert_eq!(values.len(), 1);
667 assert_eq!(values[0], 100);
668 }
669}