1mod cache;
9mod drift;
10mod kamino;
11mod pyra;
12
13pub use cache::Cache;
14pub use drift::{
15 DriftUser, HistoricalOracleData, InsuranceFund, SpotBalanceType, SpotMarket, SpotPosition,
16};
17pub use kamino::{
18 KaminoBigFractionBytes, KaminoBorrowRateCurve, KaminoCurvePoint, KaminoElevationGroup,
19 KaminoLastUpdate, KaminoObligation, KaminoObligationCollateral, KaminoObligationLiquidity,
20 KaminoReserve, KaminoReserveCollateral, KaminoReserveConfig, KaminoReserveFees,
21 KaminoReserveLiquidity, KaminoWithdrawTicket, KaminoWithdrawalCaps, KAMINO_FRACTION_SCALE,
22};
23pub use pyra::{
24 DepositAddressSolAccount, DepositAddressSplAccount, SpendLimitsOrderAccount, TimeLock, Vault,
25 WithdrawOrderAccount,
26};
27
28pub type VaultCache = Cache<Vault>;
29pub type SpotMarketCache = Cache<SpotMarket>;
30pub type DriftUserCache = Cache<DriftUser>;
31pub type WithdrawOrderCache = Cache<WithdrawOrderAccount>;
32pub type SpendLimitsOrderCache = Cache<SpendLimitsOrderAccount>;
33pub type DepositAddressSplCache = Cache<DepositAddressSplAccount>;
34pub type DepositAddressSolCache = Cache<DepositAddressSolAccount>;
35
36#[cfg(test)]
37mod tests {
38 use crate::pyra::ProtocolId;
39
40 use super::*;
41
42 #[test]
43 fn cache_deserialize_with_slot() {
44 let json = r#"{"account":{"spot_positions":[]},"last_updated_slot":285847350}"#;
45 let cache: Cache<DriftUser> = serde_json::from_str(json).unwrap();
46 assert_eq!(cache.last_updated_slot, 285847350);
47 assert!(cache.account.spot_positions.is_empty());
48 }
49
50 #[test]
51 fn cache_deserialize_without_slot() {
52 let json = r#"{"account":{"spot_positions":[]}}"#;
53 let cache: Cache<DriftUser> = serde_json::from_str(json).unwrap();
54 assert_eq!(cache.last_updated_slot, 0);
55 }
56
57 #[test]
58 fn spot_market_partial_fields() {
59 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}"#;
62 let market: SpotMarket = serde_json::from_str(json).unwrap();
63 assert_eq!(market.market_index, 1);
64 assert_eq!(market.decimals, 6);
65 assert_eq!(market.initial_asset_weight, 8000);
66 assert_eq!(market.initial_liability_weight, 12000);
67 assert_eq!(market.deposit_balance, 0);
69 }
70
71 #[test]
72 fn spot_market_missing_core_field_fails() {
73 let json = r#"{"market_index":1}"#;
75 let result = serde_json::from_str::<SpotMarket>(json);
76 assert!(result.is_err());
77
78 let json = r#"{"market_index":1,"decimals":6,"cumulative_deposit_interest":100,"cumulative_borrow_interest":50}"#;
80 let result = serde_json::from_str::<SpotMarket>(json);
81 assert!(result.is_err());
82 }
83
84 #[test]
85 fn spot_position_with_balance_type() {
86 let json = r#"{"scaled_balance":1000000,"market_index":0,"balance_type":"Deposit"}"#;
87 let pos: SpotPosition = serde_json::from_str(json).unwrap();
88 assert_eq!(pos.scaled_balance, 1000000);
89 assert_eq!(pos.balance_type, SpotBalanceType::Deposit);
90 }
91
92 #[test]
93 fn spot_position_borrow() {
94 let json =
95 r#"{"scaled_balance":500,"market_index":1,"balance_type":"Borrow","open_orders":2}"#;
96 let pos: SpotPosition = serde_json::from_str(json).unwrap();
97 assert_eq!(pos.balance_type, SpotBalanceType::Borrow);
98 assert_eq!(pos.open_orders, 2);
99 }
100
101 #[test]
102 fn drift_user_with_positions() {
103 let json = r#"{
104 "spot_positions": [
105 {"scaled_balance":1000,"market_index":0,"balance_type":"Deposit"},
106 {"scaled_balance":500,"market_index":1,"balance_type":"Borrow"}
107 ]
108 }"#;
109 let user: DriftUser = serde_json::from_str(json).unwrap();
110 assert_eq!(user.spot_positions.len(), 2);
111 assert_eq!(user.spot_positions[0].market_index, 0);
112 assert_eq!(user.spot_positions[1].balance_type, SpotBalanceType::Borrow);
113 }
114
115 #[test]
116 fn vault_all_fields_required() {
117 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}"#;
118 let vault: Vault = serde_json::from_str(json).unwrap();
119 assert_eq!(vault.owner, vec![1, 2, 3]);
120 assert_eq!(vault.spend_limit_per_transaction, 100);
121 assert_eq!(vault.timeframe_in_seconds, 86400);
122 }
123
124 #[test]
125 fn vault_missing_field_fails() {
126 let json = r#"{"owner":[1,2,3]}"#;
128 let result = serde_json::from_str::<Vault>(json);
129 assert!(result.is_err());
130 }
131
132 #[test]
133 fn time_lock_deserializes_snake_case() {
134 let zero_pubkey = serde_json::to_string(&solana_pubkey::Pubkey::default()).unwrap();
136 let json = format!(
137 r#"{{"owner":{pk},"payer":{pk},"release_slot":42}}"#,
138 pk = zero_pubkey
139 );
140 let tl: TimeLock = serde_json::from_str(&json).unwrap();
141 assert_eq!(tl.release_slot, 42);
142 }
143
144 #[test]
145 fn time_lock_serializes_camel_case() {
146 let tl = TimeLock {
147 owner: solana_pubkey::Pubkey::default(),
148 payer: solana_pubkey::Pubkey::default(),
149 release_slot: 42,
150 };
151 let json = serde_json::to_string(&tl).unwrap();
152 assert!(json.contains("releaseSlot"));
153 assert!(!json.contains("release_slot"));
154 }
155
156 #[test]
157 fn withdraw_order_deserializes_snake_case() {
158 let pk = serde_json::to_string(&solana_pubkey::Pubkey::default()).unwrap();
159 let json = format!(
160 concat!(
161 r#"{{"time_lock":{{"owner":{pk},"payer":{pk},"release_slot":100}},"#,
162 r#""amount_base_units":5000,"protocol_id":"Drift","asset_id":0,"reduce_only":false,"#,
163 r#""destination":{pk}}}"#,
164 ),
165 pk = pk
166 );
167 let order: WithdrawOrderAccount = serde_json::from_str(&json).unwrap();
168 assert_eq!(order.amount_base_units, 5000);
169 assert_eq!(order.asset_id, 0);
170 assert_eq!(order.protocol_id as u8, ProtocolId::Drift as u8);
171 }
172
173 #[test]
174 fn spot_market_roundtrip() {
175 let market = SpotMarket {
176 pubkey: vec![],
177 market_index: 1,
178 initial_asset_weight: 8000,
179 initial_liability_weight: 0,
180 imf_factor: 0,
181 scale_initial_asset_weight_start: 0,
182 decimals: 9,
183 cumulative_deposit_interest: 1_050_000_000_000,
184 cumulative_borrow_interest: 0,
185 deposit_balance: 0,
186 borrow_balance: 0,
187 optimal_utilization: 0,
188 optimal_borrow_rate: 0,
189 max_borrow_rate: 0,
190 min_borrow_rate: 0,
191 insurance_fund: InsuranceFund::default(),
192 historical_oracle_data: HistoricalOracleData::default(),
193 oracle: None,
194 };
195 let json = serde_json::to_string(&market).unwrap();
196 let deserialized: SpotMarket = serde_json::from_str(&json).unwrap();
197 assert_eq!(deserialized.market_index, 1);
198 assert_eq!(deserialized.cumulative_deposit_interest, 1_050_000_000_000);
199 }
200
201 #[test]
202 fn cache_spot_market_roundtrip() {
203 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}"#;
204 let deserialized: Cache<SpotMarket> = serde_json::from_str(json).unwrap();
205 assert_eq!(deserialized.account.market_index, 0);
206 assert_eq!(deserialized.last_updated_slot, 12345);
207 }
208
209 #[test]
210 fn spot_market_ignores_unknown_fields() {
211 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}"#;
212 let market: SpotMarket = serde_json::from_str(json).unwrap();
214 assert_eq!(market.market_index, 1);
215 }
216
217 fn kamino_reserve_json() -> String {
221 let pk = serde_json::to_string(&solana_pubkey::Pubkey::default()).unwrap();
222 format!(
223 concat!(
224 r#"{{"lending_market":{pk},"#,
225 r#""liquidity":{{"mint_pubkey":{pk},"supply_vault":{pk},"fee_vault":{pk},"#,
226 r#""total_available_amount":1000000,"borrowed_amount_sf":500,"#,
227 r#""cumulative_borrow_rate_bsf":{{"value":[1,0,0,0]}},"#,
228 r#""mint_decimals":6,"market_price_sf":42}},"#,
229 r#""collateral":{{"mint_pubkey":{pk},"supply_vault":{pk},"mint_total_supply":999}},"#,
230 r#""config":{{"loan_to_value_pct":80,"liquidation_threshold_pct":85,"#,
231 r#""protocol_take_rate_pct":10,"protocol_liquidation_fee_pct":5,"#,
232 r#""borrow_factor_pct":100,"deposit_limit":1000000,"borrow_limit":500000,"#,
233 r#""fees":{{"origination_fee_sf":1,"flash_loan_fee_sf":2}},"#,
234 r#""borrow_rate_curve":{{"points":["#,
235 r#"{{"utilization_rate_bps":0,"borrow_rate_bps":0}},"#,
236 r#"{{"utilization_rate_bps":1000,"borrow_rate_bps":100}},"#,
237 r#"{{"utilization_rate_bps":2000,"borrow_rate_bps":200}},"#,
238 r#"{{"utilization_rate_bps":3000,"borrow_rate_bps":300}},"#,
239 r#"{{"utilization_rate_bps":4000,"borrow_rate_bps":400}},"#,
240 r#"{{"utilization_rate_bps":5000,"borrow_rate_bps":500}},"#,
241 r#"{{"utilization_rate_bps":6000,"borrow_rate_bps":600}},"#,
242 r#"{{"utilization_rate_bps":7000,"borrow_rate_bps":700}},"#,
243 r#"{{"utilization_rate_bps":8000,"borrow_rate_bps":800}},"#,
244 r#"{{"utilization_rate_bps":9000,"borrow_rate_bps":900}},"#,
245 r#"{{"utilization_rate_bps":10000,"borrow_rate_bps":1000}}"#,
246 r#"]}},"#,
247 r#""deposit_withdrawal_cap":{{"config_capacity":100,"current_total":50,"#,
248 r#""last_interval_start_timestamp":0,"config_interval_length_seconds":3600}},"#,
249 r#""debt_withdrawal_cap":{{"config_capacity":100,"current_total":50,"#,
250 r#""last_interval_start_timestamp":0,"config_interval_length_seconds":3600}},"#,
251 r#""elevation_groups":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},"#,
252 r#""last_update":{{"slot":100,"stale":0}}}}"#,
253 ),
254 pk = pk
255 )
256 }
257
258 #[test]
259 fn kamino_reserve_roundtrip() {
260 let json = kamino_reserve_json();
261 let reserve: KaminoReserve = serde_json::from_str(&json).unwrap();
262 assert_eq!(reserve.liquidity.total_available_amount, 1000000);
263 assert_eq!(reserve.liquidity.mint_decimals, 6);
264 assert_eq!(reserve.collateral.mint_total_supply, 999);
265 assert_eq!(reserve.config.loan_to_value_pct, 80);
266 assert_eq!(reserve.last_update.slot, 100);
267
268 let serialized = serde_json::to_string(&reserve).unwrap();
270 let deserialized: KaminoReserve = serde_json::from_str(&serialized).unwrap();
271 assert_eq!(deserialized.liquidity.total_available_amount, 1000000);
272 }
273
274 #[test]
275 fn kamino_obligation_roundtrip() {
276 let pk = serde_json::to_string(&solana_pubkey::Pubkey::default()).unwrap();
277 let json = format!(
278 concat!(
279 r#"{{"lending_market":{pk},"#,
280 r#""deposits":[{{"deposit_reserve":{pk},"deposited_amount":5000,"market_value_sf":42}}],"#,
281 r#""borrows":[{{"borrow_reserve":{pk},"#,
282 r#""cumulative_borrow_rate_bsf":{{"value":[1,0,0,0]}},"#,
283 r#""borrowed_amount_sf":100,"market_value_sf":50,"#,
284 r#""borrow_factor_adjusted_market_value_sf":55}}],"#,
285 r#""deposited_value_sf":5000,"borrowed_assets_market_value_sf":100,"#,
286 r#""allowed_borrow_value_sf":4000,"unhealthy_borrow_value_sf":4500,"#,
287 r#""has_debt":1,"elevation_group":0,"#,
288 r#""last_update":{{"slot":200,"stale":0}}}}"#,
289 ),
290 pk = pk
291 );
292 let obligation: KaminoObligation = serde_json::from_str(&json).unwrap();
293 assert_eq!(obligation.deposits.len(), 1);
294 assert_eq!(obligation.deposits[0].deposited_amount, 5000);
295 assert_eq!(obligation.borrows.len(), 1);
296 assert_eq!(obligation.has_debt, 1);
297
298 let serialized = serde_json::to_string(&obligation).unwrap();
299 let deserialized: KaminoObligation = serde_json::from_str(&serialized).unwrap();
300 assert_eq!(deserialized.deposits[0].deposited_amount, 5000);
301 }
302
303 #[test]
304 fn kamino_obligation_owner_defaults() {
305 let pk = serde_json::to_string(&solana_pubkey::Pubkey::default()).unwrap();
307 let json = format!(
308 concat!(
309 r#"{{"lending_market":{pk},"deposits":[],"borrows":[],"#,
310 r#""deposited_value_sf":0,"borrowed_assets_market_value_sf":0,"#,
311 r#""allowed_borrow_value_sf":0,"unhealthy_borrow_value_sf":0,"#,
312 r#""has_debt":0,"elevation_group":0,"#,
313 r#""last_update":{{"slot":0,"stale":0}}}}"#,
314 ),
315 pk = pk
316 );
317 let obligation: KaminoObligation = serde_json::from_str(&json).unwrap();
318 assert_eq!(obligation.owner, solana_pubkey::Pubkey::default());
319 }
320
321 #[test]
322 fn kamino_withdraw_ticket_roundtrip() {
323 let pk = serde_json::to_string(&solana_pubkey::Pubkey::default()).unwrap();
324 let json = format!(
325 concat!(
326 r#"{{"sequence_number":42,"owner":{pk},"reserve":{pk},"#,
327 r#""user_destination_liquidity_ta":{pk},"#,
328 r#""queued_collateral_amount":1000,"created_at_timestamp":12345,"invalid":0}}"#,
329 ),
330 pk = pk
331 );
332 let ticket: KaminoWithdrawTicket = serde_json::from_str(&json).unwrap();
333 assert_eq!(ticket.sequence_number, 42);
334 assert_eq!(ticket.queued_collateral_amount, 1000);
335
336 let serialized = serde_json::to_string(&ticket).unwrap();
337 let deserialized: KaminoWithdrawTicket = serde_json::from_str(&serialized).unwrap();
338 assert_eq!(deserialized.sequence_number, 42);
339 }
340
341 #[test]
342 fn kamino_reserve_fees_defaults() {
343 let json = r#"{}"#;
345 let fees: KaminoReserveFees = serde_json::from_str(json).unwrap();
346 assert_eq!(fees.origination_fee_sf, 0);
347 assert_eq!(fees.flash_loan_fee_sf, 0);
348 }
349
350 #[test]
351 fn cache_kamino_obligation() {
352 let pk = serde_json::to_string(&solana_pubkey::Pubkey::default()).unwrap();
353 let json = format!(
354 concat!(
355 r#"{{"account":{{"lending_market":{pk},"deposits":[],"borrows":[],"#,
356 r#""deposited_value_sf":0,"borrowed_assets_market_value_sf":0,"#,
357 r#""allowed_borrow_value_sf":0,"unhealthy_borrow_value_sf":0,"#,
358 r#""has_debt":0,"elevation_group":0,"#,
359 r#""last_update":{{"slot":0,"stale":0}}}},"#,
360 r#""last_updated_slot":300}}"#,
361 ),
362 pk = pk
363 );
364 let cache: Cache<KaminoObligation> = serde_json::from_str(&json).unwrap();
365 assert_eq!(cache.last_updated_slot, 300);
366 assert!(cache.account.deposits.is_empty());
367 }
368
369 #[test]
370 fn cache_kamino_reserve() {
371 let reserve_json = kamino_reserve_json();
372 let json = format!(r#"{{"account":{},"last_updated_slot":400}}"#, reserve_json);
373 let cache: Cache<KaminoReserve> = serde_json::from_str(&json).unwrap();
374 assert_eq!(cache.last_updated_slot, 400);
375 assert_eq!(cache.account.liquidity.total_available_amount, 1000000);
376 }
377
378 #[test]
379 fn kamino_elevation_group_defaults() {
380 let group = KaminoElevationGroup::default();
381 assert_eq!(group.id, 0);
382 assert_eq!(group.ltv_pct, 0);
383 assert_eq!(group.liquidation_threshold_pct, 0);
384 assert_eq!(group.max_liquidation_bonus_bps, 0);
385 assert_eq!(group.allow_new_loans, 0);
386 assert_eq!(group.max_reserves_as_collateral, 0);
387 assert_eq!(group.debt_reserve, solana_pubkey::Pubkey::default());
388 }
389}