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