mod cache;
mod drift;
mod kamino;
mod pyra;
pub use cache::Cache;
pub use drift::{
DriftUser, HistoricalOracleData, InsuranceFund, SpotBalanceType, SpotMarket, SpotPosition,
};
pub use kamino::{
KaminoBigFractionBytes, KaminoBorrowOrder, KaminoBorrowRateCurve, KaminoCurvePoint,
KaminoElevationGroup, KaminoFixedTermBorrowRolloverConfig, KaminoLastUpdate,
KaminoObligation, KaminoObligationCollateral, KaminoObligationLiquidity,
KaminoObligationOrder, KaminoPriceHeuristic, KaminoPythConfiguration, KaminoReserve,
KaminoReserveCollateral, KaminoReserveConfig, KaminoReserveFees, KaminoReserveLiquidity,
KaminoScopeConfiguration, KaminoSwitchboardConfiguration, KaminoTokenInfo,
KaminoWithdrawQueue, KaminoWithdrawTicket, KaminoWithdrawalCaps, KAMINO_FRACTION_SCALE,
};
pub use pyra::{
DepositAddressSolAccount, DepositAddressSplAccount, ProtocolId, SpendLimitsOrderAccount,
TimeLock, Vault, WithdrawOrderAccount,
};
pub type VaultCache = Cache<Vault>;
pub type SpotMarketCache = Cache<SpotMarket>;
pub type DriftUserCache = Cache<DriftUser>;
pub type WithdrawOrderCache = Cache<WithdrawOrderAccount>;
pub type SpendLimitsOrderCache = Cache<SpendLimitsOrderAccount>;
pub type DepositAddressSplCache = Cache<DepositAddressSplAccount>;
pub type DepositAddressSolCache = Cache<DepositAddressSolAccount>;
#[cfg(test)]
#[allow(
clippy::allow_attributes,
clippy::allow_attributes_without_reason,
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::arithmetic_side_effects,
reason = "test code"
)]
mod tests {
use crate::pyra::ProtocolId;
use super::*;
#[test]
fn cache_deserialize_with_slot() {
let json = r#"{"account":{"spot_positions":[]},"last_updated_slot":285847350}"#;
let cache: Cache<DriftUser> = serde_json::from_str(json).unwrap();
assert_eq!(cache.last_updated_slot, 285847350);
assert!(cache.account.spot_positions.is_empty());
}
#[test]
fn cache_deserialize_without_slot() {
let json = r#"{"account":{"spot_positions":[]}}"#;
let cache: Cache<DriftUser> = serde_json::from_str(json).unwrap();
assert_eq!(cache.last_updated_slot, 0);
}
#[test]
fn spot_market_partial_fields() {
let json = r#"{"market_index":1,"decimals":6,"cumulative_deposit_interest":100,"cumulative_borrow_interest":50,"initial_asset_weight":8000,"initial_liability_weight":12000,"imf_factor":0,"scale_initial_asset_weight_start":0}"#;
let market: SpotMarket = serde_json::from_str(json).unwrap();
assert_eq!(market.market_index, 1);
assert_eq!(market.decimals, 6);
assert_eq!(market.initial_asset_weight, 8000);
assert_eq!(market.initial_liability_weight, 12000);
assert_eq!(market.deposit_balance, 0);
}
#[test]
fn spot_market_missing_core_field_fails() {
let json = r#"{"market_index":1}"#;
let result = serde_json::from_str::<SpotMarket>(json);
assert!(result.is_err());
let json = r#"{"market_index":1,"decimals":6,"cumulative_deposit_interest":100,"cumulative_borrow_interest":50}"#;
let result = serde_json::from_str::<SpotMarket>(json);
assert!(result.is_err());
}
#[test]
fn spot_position_with_balance_type() {
let json = r#"{"scaled_balance":1000000,"market_index":0,"balance_type":"Deposit"}"#;
let pos: SpotPosition = serde_json::from_str(json).unwrap();
assert_eq!(pos.scaled_balance, 1000000);
assert_eq!(pos.balance_type, SpotBalanceType::Deposit);
}
#[test]
fn spot_position_borrow() {
let json =
r#"{"scaled_balance":500,"market_index":1,"balance_type":"Borrow","open_orders":2}"#;
let pos: SpotPosition = serde_json::from_str(json).unwrap();
assert_eq!(pos.balance_type, SpotBalanceType::Borrow);
assert_eq!(pos.open_orders, 2);
}
#[test]
fn drift_user_with_positions() {
let json = r#"{
"spot_positions": [
{"scaled_balance":1000,"market_index":0,"balance_type":"Deposit"},
{"scaled_balance":500,"market_index":1,"balance_type":"Borrow"}
]
}"#;
let user: DriftUser = serde_json::from_str(json).unwrap();
assert_eq!(user.spot_positions.len(), 2);
assert_eq!(user.spot_positions[0].market_index, 0);
assert_eq!(user.spot_positions[1].balance_type, SpotBalanceType::Borrow);
}
#[test]
fn vault_all_fields_required() {
let json = r#"{"owner":[1,2,3],"bump":1,"spend_limit_per_transaction":100,"spend_limit_per_timeframe":1000,"remaining_spend_limit_per_timeframe":500,"next_timeframe_reset_timestamp":12345,"timeframe_in_seconds":86400}"#;
let vault: Vault = serde_json::from_str(json).unwrap();
assert_eq!(vault.owner, vec![1, 2, 3]);
assert_eq!(vault.spend_limit_per_transaction, 100);
assert_eq!(vault.timeframe_in_seconds, 86400);
}
#[test]
fn vault_missing_field_fails() {
let json = r#"{"owner":[1,2,3]}"#;
let result = serde_json::from_str::<Vault>(json);
assert!(result.is_err());
}
#[test]
fn time_lock_deserializes_snake_case() {
let zero_pubkey = serde_json::to_string(&solana_pubkey::Pubkey::default()).unwrap();
let json = format!(
r#"{{"owner":{pk},"payer":{pk},"release_slot":42}}"#,
pk = zero_pubkey
);
let tl: TimeLock = serde_json::from_str(&json).unwrap();
assert_eq!(tl.release_slot, 42);
}
#[test]
fn time_lock_serializes_camel_case() {
let tl = TimeLock {
owner: solana_pubkey::Pubkey::default(),
payer: solana_pubkey::Pubkey::default(),
release_slot: 42,
};
let json = serde_json::to_string(&tl).unwrap();
assert!(json.contains("releaseSlot"));
assert!(!json.contains("release_slot"));
}
#[test]
fn withdraw_order_deserializes_snake_case() {
let pk = serde_json::to_string(&solana_pubkey::Pubkey::default()).unwrap();
let json = format!(
concat!(
r#"{{"time_lock":{{"owner":{pk},"payer":{pk},"release_slot":100}},"#,
r#""amount_base_units":5000,"protocol_id":"Drift","asset_id":0,"reduce_only":false,"#,
r#""destination":{pk}}}"#,
),
pk = pk
);
let order: WithdrawOrderAccount = serde_json::from_str(&json).unwrap();
assert_eq!(order.amount_base_units, 5000);
assert_eq!(order.asset_id, 0);
assert_eq!(order.protocol_id as u8, ProtocolId::Drift as u8);
}
#[test]
fn spot_market_roundtrip() {
let market = SpotMarket {
pubkey: vec![],
market_index: 1,
initial_asset_weight: 8000,
initial_liability_weight: 0,
imf_factor: 0,
scale_initial_asset_weight_start: 0,
decimals: 9,
cumulative_deposit_interest: 1_050_000_000_000,
cumulative_borrow_interest: 0,
deposit_balance: 0,
borrow_balance: 0,
optimal_utilization: 0,
optimal_borrow_rate: 0,
max_borrow_rate: 0,
min_borrow_rate: 0,
insurance_fund: InsuranceFund::default(),
historical_oracle_data: HistoricalOracleData::default(),
oracle: None,
};
let json = serde_json::to_string(&market).unwrap();
let deserialized: SpotMarket = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.market_index, 1);
assert_eq!(deserialized.cumulative_deposit_interest, 1_050_000_000_000);
}
#[test]
fn cache_spot_market_roundtrip() {
let json = r#"{"account":{"market_index":0,"decimals":6,"cumulative_deposit_interest":100,"cumulative_borrow_interest":50,"initial_asset_weight":8000,"initial_liability_weight":12000,"imf_factor":0,"scale_initial_asset_weight_start":0},"last_updated_slot":12345}"#;
let deserialized: Cache<SpotMarket> = serde_json::from_str(json).unwrap();
assert_eq!(deserialized.account.market_index, 0);
assert_eq!(deserialized.last_updated_slot, 12345);
}
#[test]
fn spot_market_ignores_unknown_fields() {
let json = r#"{"market_index":1,"some_future_field":"value","decimals":6,"cumulative_deposit_interest":0,"cumulative_borrow_interest":0,"initial_asset_weight":8000,"initial_liability_weight":12000,"imf_factor":0,"scale_initial_asset_weight_start":0}"#;
let market: SpotMarket = serde_json::from_str(json).unwrap();
assert_eq!(market.market_index, 1);
}
#[test]
fn kamino_reserve_roundtrip() {
let mut reserve = KaminoReserve::default();
reserve.liquidity.total_available_amount = 1_000_000;
reserve.liquidity.mint_decimals = 6;
reserve.collateral.mint_total_supply = 999;
reserve.config.loan_to_value_pct = 80;
reserve.last_update.slot = 100;
let json = serde_json::to_string(&reserve).unwrap();
let deserialized: KaminoReserve = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.liquidity.total_available_amount, 1_000_000);
assert_eq!(deserialized.liquidity.mint_decimals, 6);
assert_eq!(deserialized.collateral.mint_total_supply, 999);
assert_eq!(deserialized.config.loan_to_value_pct, 80);
assert_eq!(deserialized.last_update.slot, 100);
}
#[test]
fn kamino_obligation_roundtrip() {
let mut obligation = KaminoObligation::default();
obligation.deposits[0].deposited_amount = 5000;
obligation.deposits[0].market_value_sf = 42;
obligation.borrows[0].borrowed_amount_sf = 100;
obligation.borrows[0].market_value_sf = 50;
obligation.has_debt = 1;
let json = serde_json::to_string(&obligation).unwrap();
let deserialized: KaminoObligation = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.deposits[0].deposited_amount, 5000);
assert_eq!(deserialized.borrows[0].borrowed_amount_sf, 100);
assert_eq!(deserialized.has_debt, 1);
assert_eq!(deserialized.deposits.len(), 8);
assert_eq!(deserialized.borrows.len(), 5);
}
#[test]
fn kamino_withdraw_ticket_roundtrip() {
let pk = serde_json::to_string(&solana_pubkey::Pubkey::default()).unwrap();
let json = format!(
concat!(
r#"{{"sequence_number":42,"owner":{pk},"reserve":{pk},"#,
r#""user_destination_liquidity_ta":{pk},"#,
r#""queued_collateral_amount":1000,"created_at_timestamp":12345,"invalid":0}}"#,
),
pk = pk
);
let ticket: KaminoWithdrawTicket = serde_json::from_str(&json).unwrap();
assert_eq!(ticket.sequence_number, 42);
assert_eq!(ticket.queued_collateral_amount, 1000);
let serialized = serde_json::to_string(&ticket).unwrap();
let deserialized: KaminoWithdrawTicket = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized.sequence_number, 42);
}
#[test]
fn cache_kamino_obligation() {
let obligation = KaminoObligation::default();
let cache = Cache {
account: obligation,
last_updated_slot: 300,
};
let json = serde_json::to_string(&cache).unwrap();
let deserialized: Cache<KaminoObligation> = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.last_updated_slot, 300);
assert_eq!(deserialized.account.deposits[0].deposited_amount, 0);
}
#[test]
fn cache_kamino_reserve() {
let mut reserve = KaminoReserve::default();
reserve.liquidity.total_available_amount = 1_000_000;
let cache = Cache {
account: reserve,
last_updated_slot: 400,
};
let json = serde_json::to_string(&cache).unwrap();
let deserialized: Cache<KaminoReserve> = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.last_updated_slot, 400);
assert_eq!(deserialized.account.liquidity.total_available_amount, 1_000_000);
}
#[test]
fn kamino_elevation_group_defaults() {
let group = KaminoElevationGroup::default();
assert_eq!(group.id, 0);
assert_eq!(group.ltv_pct, 0);
assert_eq!(group.liquidation_threshold_pct, 0);
assert_eq!(group.max_liquidation_bonus_bps, 0);
assert_eq!(group.allow_new_loans, 0);
assert_eq!(group.max_reserves_as_collateral, 0);
assert_eq!(group.debt_reserve, solana_pubkey::Pubkey::default());
}
}