use std::collections::HashMap;
use solana_pubkey::Pubkey;
use pyra_margin::{get_kamino_borrow_balance, get_kamino_deposit_balance};
use pyra_types::{Cache, KaminoObligation, KaminoReserve, KAMINO_FRACTION_SCALE};
use crate::{RedisClient, RedisError, RedisKey, RedisResult};
fn market_price_from_reserve(reserve: &KaminoReserve) -> f64 {
reserve.liquidity.market_price_sf as f64 / KAMINO_FRACTION_SCALE as f64
}
fn balance_to_value_cents(token_balance: i128, decimals: u32, price: f64) -> RedisResult<i64> {
let decimals_pow = 10_f64.powi(i32::try_from(decimals).map_err(|_| RedisError::MathOverflow)?);
let value = (token_balance as f64) / decimals_pow * price * 100.0;
let rounded = value.round();
if rounded.is_finite() && rounded >= i64::MIN as f64 && rounded <= i64::MAX as f64 {
Ok(rounded as i64)
} else {
Err(RedisError::MathOverflow)
}
}
pub struct VaultKaminoPositionData {
pub obligation: Cache<KaminoObligation>,
pub reserves: HashMap<Pubkey, KaminoReserve>,
pub prices: HashMap<Pubkey, f64>,
}
pub struct AllKaminoPositionsData {
pub obligations: Vec<(Pubkey, Cache<KaminoObligation>)>,
pub reserves: HashMap<Pubkey, KaminoReserve>,
pub prices: HashMap<Pubkey, f64>,
}
impl RedisClient {
pub async fn fetch_vault_kamino_position_data(
&self,
vault_address: &Pubkey,
lending_market: &Pubkey,
reserve_pubkeys: &[Pubkey],
) -> RedisResult<VaultKaminoPositionData> {
let obligation_key = RedisKey::kamino_obligation(vault_address, lending_market).to_string();
let mut keys = vec![obligation_key];
for pk in reserve_pubkeys {
keys.push(RedisKey::kamino_reserve(pk).to_string());
}
let values = self.mget(&keys).await?;
let obligation_raw = values
.first()
.and_then(|v| v.as_ref())
.ok_or_else(|| RedisError::NotFound("KaminoObligation not found in Redis".into()))?;
let obligation: Cache<KaminoObligation> = serde_json::from_str(obligation_raw)?;
let mut reserves: HashMap<Pubkey, KaminoReserve> = HashMap::new();
let mut prices: HashMap<Pubkey, f64> = HashMap::new();
for (i, pk) in reserve_pubkeys.iter().enumerate() {
if let Some(Some(raw)) =
values.get(1usize.checked_add(i).ok_or(RedisError::MathOverflow)?)
{
if let Ok(cached) = serde_json::from_str::<Cache<KaminoReserve>>(raw) {
let price = market_price_from_reserve(&cached.account);
prices.insert(*pk, price);
reserves.insert(*pk, cached.account);
}
}
}
Ok(VaultKaminoPositionData {
obligation,
reserves,
prices,
})
}
pub async fn fetch_all_kamino_positions(
&self,
reserve_pubkeys: &[Pubkey],
) -> RedisResult<AllKaminoPositionsData> {
let obligation_keys = self.scan_keys(&RedisKey::kamino_obligation_glob()).await?;
let prefix_with_colon = format!("{}:", RedisKey::KAMINO_OBLIGATION_PREFIX);
let vault_addresses: Vec<Option<Pubkey>> = obligation_keys
.iter()
.map(|k| {
k.strip_prefix(prefix_with_colon.as_str())
.and_then(|rest| rest.split(':').next())
.and_then(|s| s.parse::<Pubkey>().ok())
})
.collect();
let num_obligations = obligation_keys.len();
let mut all_keys: Vec<String> = obligation_keys;
for pk in reserve_pubkeys {
all_keys.push(RedisKey::kamino_reserve(pk).to_string());
}
let values = self.mget(&all_keys).await?;
let mut obligations: Vec<(Pubkey, Cache<KaminoObligation>)> = Vec::new();
for (i, vault_pk) in vault_addresses.iter().enumerate() {
if let (Some(pk), Some(Some(raw))) = (vault_pk, values.get(i)) {
if let Ok(cached) = serde_json::from_str::<Cache<KaminoObligation>>(raw) {
obligations.push((*pk, cached));
}
}
}
let mut reserves: HashMap<Pubkey, KaminoReserve> = HashMap::new();
let mut prices: HashMap<Pubkey, f64> = HashMap::new();
for (i, pk) in reserve_pubkeys.iter().enumerate() {
let offset = num_obligations
.checked_add(i)
.ok_or(RedisError::MathOverflow)?;
if let Some(Some(raw)) = values.get(offset) {
if let Ok(cached) = serde_json::from_str::<Cache<KaminoReserve>>(raw) {
let price = market_price_from_reserve(&cached.account);
prices.insert(*pk, price);
reserves.insert(*pk, cached.account);
}
}
}
Ok(AllKaminoPositionsData {
obligations,
reserves,
prices,
})
}
}
pub fn compute_kamino_position_values(data: &VaultKaminoPositionData) -> RedisResult<Vec<i64>> {
compute_user_kamino_position_values(&data.obligation.account, &data.reserves, &data.prices)
}
pub fn compute_kamino_asset_data(
data: &VaultKaminoPositionData,
) -> RedisResult<Vec<(Pubkey, i64, i64)>> {
compute_user_kamino_asset_data(&data.obligation.account, &data.reserves, &data.prices)
}
pub fn compute_user_kamino_position_values(
obligation: &KaminoObligation,
reserves: &HashMap<Pubkey, KaminoReserve>,
prices: &HashMap<Pubkey, f64>,
) -> RedisResult<Vec<i64>> {
let mut results = Vec::new();
for deposit in &obligation.deposits {
if deposit.deposited_amount == 0 {
continue;
}
let Some(reserve) = reserves.get(&deposit.deposit_reserve) else {
continue;
};
let Some(&price) = prices.get(&deposit.deposit_reserve) else {
continue;
};
let balance = get_kamino_deposit_balance(deposit, reserve)?;
let decimals =
u32::try_from(reserve.liquidity.mint_decimals).map_err(|_| RedisError::MathOverflow)?;
let cents = balance_to_value_cents(balance, decimals, price)?;
results.push(cents);
}
for borrow in &obligation.borrows {
if borrow.borrowed_amount_sf == 0 {
continue;
}
let Some(reserve) = reserves.get(&borrow.borrow_reserve) else {
continue;
};
let Some(&price) = prices.get(&borrow.borrow_reserve) else {
continue;
};
let balance = get_kamino_borrow_balance(borrow, reserve)?;
let decimals =
u32::try_from(reserve.liquidity.mint_decimals).map_err(|_| RedisError::MathOverflow)?;
let cents = balance_to_value_cents(balance, decimals, price)?;
results.push(cents);
}
Ok(results)
}
pub fn compute_user_kamino_asset_data(
obligation: &KaminoObligation,
reserves: &HashMap<Pubkey, KaminoReserve>,
prices: &HashMap<Pubkey, f64>,
) -> RedisResult<Vec<(Pubkey, i64, i64)>> {
let mut results = Vec::new();
for deposit in &obligation.deposits {
if deposit.deposited_amount == 0 {
continue;
}
let Some(reserve) = reserves.get(&deposit.deposit_reserve) else {
continue;
};
let Some(&price) = prices.get(&deposit.deposit_reserve) else {
continue;
};
let balance_i128 = get_kamino_deposit_balance(deposit, reserve)?;
let balance = i64::try_from(balance_i128).map_err(|_| RedisError::MathOverflow)?;
let decimals =
u32::try_from(reserve.liquidity.mint_decimals).map_err(|_| RedisError::MathOverflow)?;
let cents = balance_to_value_cents(balance_i128, decimals, price)?;
results.push((deposit.deposit_reserve, balance, cents));
}
for borrow in &obligation.borrows {
if borrow.borrowed_amount_sf == 0 {
continue;
}
let Some(reserve) = reserves.get(&borrow.borrow_reserve) else {
continue;
};
let Some(&price) = prices.get(&borrow.borrow_reserve) else {
continue;
};
let balance_i128 = get_kamino_borrow_balance(borrow, reserve)?;
let balance = i64::try_from(balance_i128).map_err(|_| RedisError::MathOverflow)?;
let decimals =
u32::try_from(reserve.liquidity.mint_decimals).map_err(|_| RedisError::MathOverflow)?;
let cents = balance_to_value_cents(balance_i128, decimals, price)?;
results.push((borrow.borrow_reserve, balance, cents));
}
Ok(results)
}
#[cfg(test)]
#[allow(
clippy::allow_attributes,
clippy::allow_attributes_without_reason,
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::arithmetic_side_effects
)]
mod tests {
use super::*;
use pyra_types::{
KaminoBigFractionBytes, KaminoBorrowRateCurve, KaminoCurvePoint,
KaminoObligationCollateral, KaminoObligationLiquidity, KaminoReserveCollateral,
KaminoReserveConfig, KaminoReserveFees, KaminoReserveLiquidity, KaminoWithdrawalCaps,
};
fn test_pubkey(seed: u8) -> Pubkey {
Pubkey::new_from_array([seed; 32])
}
const FRACTION_ONE: u128 = 1 << 60;
fn rate_to_bsf(rate: u128) -> KaminoBigFractionBytes {
KaminoBigFractionBytes {
value: [rate as u64, (rate >> 64) as u64, 0, 0],
}
}
fn make_reserve(mint_decimals: u64, market_price_sf: u128) -> KaminoReserve {
KaminoReserve {
liquidity: KaminoReserveLiquidity {
total_available_amount: 1_000_000,
cumulative_borrow_rate_bsf: rate_to_bsf(FRACTION_ONE),
mint_decimals,
market_price_sf,
..Default::default()
},
collateral: KaminoReserveCollateral {
mint_total_supply: 1_000_000,
..Default::default()
},
config: KaminoReserveConfig {
loan_to_value_pct: 80,
liquidation_threshold_pct: 85,
protocol_take_rate_pct: 10,
protocol_liquidation_fee_pct: 5,
borrow_factor_pct: 100,
deposit_limit: u64::MAX,
borrow_limit: u64::MAX,
fees: KaminoReserveFees {
origination_fee_sf: 0,
flash_loan_fee_sf: 0,
},
borrow_rate_curve: KaminoBorrowRateCurve {
points: [KaminoCurvePoint {
utilization_rate_bps: 0,
borrow_rate_bps: 0,
}; 11],
},
deposit_withdrawal_cap: KaminoWithdrawalCaps {
config_capacity: 0,
current_total: 0,
last_interval_start_timestamp: 0,
config_interval_length_seconds: 0,
},
debt_withdrawal_cap: KaminoWithdrawalCaps {
config_capacity: 0,
current_total: 0,
last_interval_start_timestamp: 0,
config_interval_length_seconds: 0,
},
elevation_groups: [0; 20],
..Default::default()
},
..Default::default()
}
}
fn make_obligation(
deposits_vec: Vec<KaminoObligationCollateral>,
borrows_vec: Vec<KaminoObligationLiquidity>,
) -> KaminoObligation {
let mut deposits = <[KaminoObligationCollateral; 8]>::default();
for (i, d) in deposits_vec.into_iter().enumerate() {
deposits[i] = d;
}
let mut borrows = <[KaminoObligationLiquidity; 5]>::default();
for (i, b) in borrows_vec.into_iter().enumerate() {
borrows[i] = b;
}
KaminoObligation {
deposits,
borrows,
..Default::default()
}
}
#[test]
fn market_price_extraction() {
let reserve = make_reserve(6, KAMINO_FRACTION_SCALE);
let price = market_price_from_reserve(&reserve);
assert!((price - 1.0).abs() < f64::EPSILON);
}
#[test]
fn market_price_extraction_150_usd() {
let price_sf = KAMINO_FRACTION_SCALE
.checked_mul(150)
.expect("test value fits");
let reserve = make_reserve(9, price_sf);
let price = market_price_from_reserve(&reserve);
assert!((price - 150.0).abs() < 0.001);
}
#[test]
fn vault_position_data_struct_construction() {
let reserve_pk = test_pubkey(1);
let reserve = make_reserve(6, KAMINO_FRACTION_SCALE);
let mut reserves = HashMap::new();
reserves.insert(reserve_pk, reserve);
let mut prices = HashMap::new();
prices.insert(reserve_pk, 1.0);
let deposit = KaminoObligationCollateral {
deposit_reserve: reserve_pk,
deposited_amount: 1_000_000,
market_value_sf: KAMINO_FRACTION_SCALE,
..Default::default()
};
let obligation = make_obligation(vec![deposit], vec![]);
let data = VaultKaminoPositionData {
obligation: Cache {
account: obligation,
last_updated_slot: 12345,
},
reserves,
prices,
};
assert_eq!(data.obligation.last_updated_slot, 12345);
assert_eq!(data.reserves.len(), 1);
assert_eq!(data.prices.len(), 1);
}
#[test]
fn all_positions_data_struct_construction() {
let data = AllKaminoPositionsData {
obligations: vec![],
reserves: HashMap::new(),
prices: HashMap::new(),
};
assert!(data.obligations.is_empty());
assert!(data.reserves.is_empty());
assert!(data.prices.is_empty());
}
#[test]
fn compute_values_empty_obligation() {
let obligation = make_obligation(vec![], vec![]);
let data = VaultKaminoPositionData {
obligation: Cache {
account: obligation,
last_updated_slot: 0,
},
reserves: HashMap::new(),
prices: HashMap::new(),
};
let values = compute_kamino_position_values(&data).unwrap();
assert!(values.is_empty());
}
#[test]
fn compute_values_single_deposit() {
let reserve_pk = test_pubkey(1);
let reserve = make_reserve(6, KAMINO_FRACTION_SCALE);
let deposit = KaminoObligationCollateral {
deposit_reserve: reserve_pk,
deposited_amount: 1_000_000, market_value_sf: 0,
..Default::default()
};
let obligation = make_obligation(vec![deposit], vec![]);
let mut reserves = HashMap::new();
reserves.insert(reserve_pk, reserve);
let mut prices = HashMap::new();
prices.insert(reserve_pk, 1.0);
let values = compute_user_kamino_position_values(&obligation, &reserves, &prices).unwrap();
assert_eq!(values.len(), 1);
assert_eq!(values[0], 100); }
#[test]
fn compute_values_deposit_and_borrow() {
let deposit_pk = test_pubkey(1);
let borrow_pk = test_pubkey(2);
let deposit_reserve = make_reserve(6, KAMINO_FRACTION_SCALE);
let borrow_reserve = make_reserve(6, KAMINO_FRACTION_SCALE);
let deposit = KaminoObligationCollateral {
deposit_reserve: deposit_pk,
deposited_amount: 2_000_000,
market_value_sf: 0,
..Default::default()
};
let borrow = KaminoObligationLiquidity {
borrow_reserve: borrow_pk,
cumulative_borrow_rate_bsf: rate_to_bsf(FRACTION_ONE),
borrowed_amount_sf: 500_000 * FRACTION_ONE,
market_value_sf: 0,
borrow_factor_adjusted_market_value_sf: 0,
..Default::default()
};
let obligation = make_obligation(vec![deposit], vec![borrow]);
let mut reserves = HashMap::new();
reserves.insert(deposit_pk, deposit_reserve);
reserves.insert(borrow_pk, borrow_reserve);
let mut prices = HashMap::new();
prices.insert(deposit_pk, 1.0);
prices.insert(borrow_pk, 1.0);
let values = compute_user_kamino_position_values(&obligation, &reserves, &prices).unwrap();
assert_eq!(values.len(), 2);
assert_eq!(values[0], 200); assert_eq!(values[1], -50); }
#[test]
fn compute_values_skips_zero_deposit() {
let reserve_pk = test_pubkey(1);
let reserve = make_reserve(6, KAMINO_FRACTION_SCALE);
let deposit = KaminoObligationCollateral {
deposit_reserve: reserve_pk,
deposited_amount: 0,
market_value_sf: 0,
..Default::default()
};
let obligation = make_obligation(vec![deposit], vec![]);
let mut reserves = HashMap::new();
reserves.insert(reserve_pk, reserve);
let mut prices = HashMap::new();
prices.insert(reserve_pk, 1.0);
let values = compute_user_kamino_position_values(&obligation, &reserves, &prices).unwrap();
assert!(values.is_empty());
}
#[test]
fn compute_values_skips_missing_reserve() {
let reserve_pk = test_pubkey(1);
let deposit = KaminoObligationCollateral {
deposit_reserve: reserve_pk,
deposited_amount: 1_000_000,
market_value_sf: 0,
..Default::default()
};
let obligation = make_obligation(vec![deposit], vec![]);
let reserves = HashMap::new();
let mut prices = HashMap::new();
prices.insert(reserve_pk, 1.0);
let values = compute_user_kamino_position_values(&obligation, &reserves, &prices).unwrap();
assert!(values.is_empty());
}
#[test]
fn compute_values_skips_missing_price() {
let reserve_pk = test_pubkey(1);
let reserve = make_reserve(6, KAMINO_FRACTION_SCALE);
let deposit = KaminoObligationCollateral {
deposit_reserve: reserve_pk,
deposited_amount: 1_000_000,
market_value_sf: 0,
..Default::default()
};
let obligation = make_obligation(vec![deposit], vec![]);
let mut reserves = HashMap::new();
reserves.insert(reserve_pk, reserve);
let prices = HashMap::new();
let values = compute_user_kamino_position_values(&obligation, &reserves, &prices).unwrap();
assert!(values.is_empty());
}
#[test]
fn compute_values_high_price_sol() {
let reserve_pk = test_pubkey(1);
let price_sf = KAMINO_FRACTION_SCALE.checked_mul(150).unwrap();
let reserve = make_reserve(9, price_sf);
let deposit = KaminoObligationCollateral {
deposit_reserve: reserve_pk,
deposited_amount: 100_000_000, market_value_sf: 0,
..Default::default()
};
let obligation = make_obligation(vec![deposit], vec![]);
let mut reserves = HashMap::new();
reserves.insert(reserve_pk, reserve);
let mut prices = HashMap::new();
prices.insert(reserve_pk, 150.0);
let values = compute_user_kamino_position_values(&obligation, &reserves, &prices).unwrap();
assert_eq!(values.len(), 1);
assert_eq!(values[0], 1500); }
#[test]
fn compute_asset_data_returns_tuples() {
let reserve_pk = test_pubkey(1);
let reserve = make_reserve(6, KAMINO_FRACTION_SCALE);
let deposit = KaminoObligationCollateral {
deposit_reserve: reserve_pk,
deposited_amount: 5_000_000,
market_value_sf: 0,
..Default::default()
};
let obligation = make_obligation(vec![deposit], vec![]);
let mut reserves = HashMap::new();
reserves.insert(reserve_pk, reserve);
let mut prices = HashMap::new();
prices.insert(reserve_pk, 1.0);
let data = compute_user_kamino_asset_data(&obligation, &reserves, &prices).unwrap();
assert_eq!(data.len(), 1);
let (pk, token_balance, value_cents) = data[0];
assert_eq!(pk, reserve_pk);
assert_eq!(token_balance, 5_000_000);
assert_eq!(value_cents, 500); }
#[test]
fn compute_position_values_delegates_to_user_variant() {
let reserve_pk = test_pubkey(1);
let reserve = make_reserve(6, KAMINO_FRACTION_SCALE);
let deposit = KaminoObligationCollateral {
deposit_reserve: reserve_pk,
deposited_amount: 1_000_000,
market_value_sf: 0,
..Default::default()
};
let obligation = Cache {
account: make_obligation(vec![deposit], vec![]),
last_updated_slot: 12345,
};
let mut reserves = HashMap::new();
reserves.insert(reserve_pk, reserve);
let mut prices = HashMap::new();
prices.insert(reserve_pk, 1.0);
let vpd = VaultKaminoPositionData {
obligation,
reserves,
prices,
};
let values = compute_kamino_position_values(&vpd).unwrap();
assert_eq!(values.len(), 1);
assert_eq!(values[0], 100);
}
}