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, KaminoBorrowOrder, KaminoBorrowRateCurve, KaminoCurvePoint,
19 KaminoElevationGroup, KaminoFixedTermBorrowRolloverConfig, KaminoLastUpdate, KaminoObligation,
20 KaminoObligationCollateral, KaminoObligationLiquidity, KaminoObligationOrder,
21 KaminoPriceHeuristic, KaminoPythConfiguration, KaminoReserve, KaminoReserveCollateral,
22 KaminoReserveConfig, KaminoReserveFees, KaminoReserveLiquidity, KaminoScopeConfiguration,
23 KaminoSwitchboardConfiguration, KaminoTokenInfo, KaminoWithdrawQueue, KaminoWithdrawTicket,
24 KaminoWithdrawalCaps, KAMINO_FRACTION_SCALE,
25};
26pub use pyra::{
27 DepositAddressSolAccount, DepositAddressSplAccount, ProtocolId, SpendLimitsOrderAccount,
28 TimeLock, Vault, WithdrawOrderAccount,
29};
30
31pub type VaultCache = Cache<Vault>;
32pub type SpotMarketCache = Cache<SpotMarket>;
33pub type DriftUserCache = Cache<DriftUser>;
34pub type WithdrawOrderCache = Cache<WithdrawOrderAccount>;
35pub type SpendLimitsOrderCache = Cache<SpendLimitsOrderAccount>;
36pub type DepositAddressSplCache = Cache<DepositAddressSplAccount>;
37pub type DepositAddressSolCache = Cache<DepositAddressSolAccount>;
38
39#[cfg(test)]
40#[allow(
41 clippy::allow_attributes,
42 clippy::allow_attributes_without_reason,
43 clippy::unwrap_used,
44 clippy::expect_used,
45 clippy::panic,
46 clippy::arithmetic_side_effects
47)]
48mod tests {
49 use crate::pyra::ProtocolId;
50
51 use super::*;
52
53 #[test]
54 fn cache_deserialize_with_slot() {
55 let json = r#"{"account":{"spot_positions":[]},"last_updated_slot":285847350}"#;
56 let cache: Cache<DriftUser> = serde_json::from_str(json).unwrap();
57 assert_eq!(cache.last_updated_slot, 285847350);
58 assert!(cache.account.spot_positions.is_empty());
59 }
60
61 #[test]
62 fn cache_deserialize_without_slot() {
63 let json = r#"{"account":{"spot_positions":[]}}"#;
64 let cache: Cache<DriftUser> = serde_json::from_str(json).unwrap();
65 assert_eq!(cache.last_updated_slot, 0);
66 }
67
68 #[test]
69 fn spot_market_partial_fields() {
70 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}"#;
73 let market: SpotMarket = serde_json::from_str(json).unwrap();
74 assert_eq!(market.market_index, 1);
75 assert_eq!(market.decimals, 6);
76 assert_eq!(market.initial_asset_weight, 8000);
77 assert_eq!(market.initial_liability_weight, 12000);
78 assert_eq!(market.deposit_balance, 0);
80 }
81
82 #[test]
83 fn spot_market_missing_core_field_fails() {
84 let json = r#"{"market_index":1}"#;
86 let result = serde_json::from_str::<SpotMarket>(json);
87 assert!(result.is_err());
88
89 let json = r#"{"market_index":1,"decimals":6,"cumulative_deposit_interest":100,"cumulative_borrow_interest":50}"#;
91 let result = serde_json::from_str::<SpotMarket>(json);
92 assert!(result.is_err());
93 }
94
95 #[test]
96 fn spot_position_with_balance_type() {
97 let json = r#"{"scaled_balance":1000000,"market_index":0,"balance_type":"Deposit"}"#;
98 let pos: SpotPosition = serde_json::from_str(json).unwrap();
99 assert_eq!(pos.scaled_balance, 1000000);
100 assert_eq!(pos.balance_type, SpotBalanceType::Deposit);
101 }
102
103 #[test]
104 fn spot_position_borrow() {
105 let json =
106 r#"{"scaled_balance":500,"market_index":1,"balance_type":"Borrow","open_orders":2}"#;
107 let pos: SpotPosition = serde_json::from_str(json).unwrap();
108 assert_eq!(pos.balance_type, SpotBalanceType::Borrow);
109 assert_eq!(pos.open_orders, 2);
110 }
111
112 #[test]
113 fn drift_user_with_positions() {
114 let json = r#"{
115 "spot_positions": [
116 {"scaled_balance":1000,"market_index":0,"balance_type":"Deposit"},
117 {"scaled_balance":500,"market_index":1,"balance_type":"Borrow"}
118 ]
119 }"#;
120 let user: DriftUser = serde_json::from_str(json).unwrap();
121 assert_eq!(user.spot_positions.len(), 2);
122 assert_eq!(user.spot_positions[0].market_index, 0);
123 assert_eq!(user.spot_positions[1].balance_type, SpotBalanceType::Borrow);
124 }
125
126 #[test]
127 fn vault_all_fields_required() {
128 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}"#;
129 let vault: Vault = serde_json::from_str(json).unwrap();
130 assert_eq!(vault.owner, vec![1, 2, 3]);
131 assert_eq!(vault.spend_limit_per_transaction, 100);
132 assert_eq!(vault.timeframe_in_seconds, 86400);
133 }
134
135 #[test]
136 fn vault_missing_field_fails() {
137 let json = r#"{"owner":[1,2,3]}"#;
139 let result = serde_json::from_str::<Vault>(json);
140 assert!(result.is_err());
141 }
142
143 #[test]
144 fn time_lock_deserializes_snake_case() {
145 let zero_pubkey = serde_json::to_string(&solana_pubkey::Pubkey::default()).unwrap();
147 let json = format!(
148 r#"{{"owner":{pk},"payer":{pk},"release_slot":42}}"#,
149 pk = zero_pubkey
150 );
151 let tl: TimeLock = serde_json::from_str(&json).unwrap();
152 assert_eq!(tl.release_slot, 42);
153 }
154
155 #[test]
156 fn time_lock_serializes_camel_case() {
157 let tl = TimeLock {
158 owner: solana_pubkey::Pubkey::default(),
159 payer: solana_pubkey::Pubkey::default(),
160 release_slot: 42,
161 };
162 let json = serde_json::to_string(&tl).unwrap();
163 assert!(json.contains("releaseSlot"));
164 assert!(!json.contains("release_slot"));
165 }
166
167 #[test]
168 fn withdraw_order_deserializes_snake_case() {
169 let pk = serde_json::to_string(&solana_pubkey::Pubkey::default()).unwrap();
170 let json = format!(
171 concat!(
172 r#"{{"time_lock":{{"owner":{pk},"payer":{pk},"release_slot":100}},"#,
173 r#""amount_base_units":5000,"protocol_id":"Drift","asset_id":0,"reduce_only":false,"#,
174 r#""destination":{pk}}}"#,
175 ),
176 pk = pk
177 );
178 let order: WithdrawOrderAccount = serde_json::from_str(&json).unwrap();
179 assert_eq!(order.amount_base_units, 5000);
180 assert_eq!(order.asset_id, 0);
181 assert_eq!(order.protocol_id as u8, ProtocolId::Drift as u8);
182 }
183
184 #[test]
185 fn spot_market_roundtrip() {
186 let market = SpotMarket {
187 pubkey: vec![],
188 market_index: 1,
189 initial_asset_weight: 8000,
190 initial_liability_weight: 0,
191 imf_factor: 0,
192 scale_initial_asset_weight_start: 0,
193 decimals: 9,
194 cumulative_deposit_interest: 1_050_000_000_000,
195 cumulative_borrow_interest: 0,
196 deposit_balance: 0,
197 borrow_balance: 0,
198 optimal_utilization: 0,
199 optimal_borrow_rate: 0,
200 max_borrow_rate: 0,
201 min_borrow_rate: 0,
202 insurance_fund: InsuranceFund::default(),
203 historical_oracle_data: HistoricalOracleData::default(),
204 oracle: None,
205 };
206 let json = serde_json::to_string(&market).unwrap();
207 let deserialized: SpotMarket = serde_json::from_str(&json).unwrap();
208 assert_eq!(deserialized.market_index, 1);
209 assert_eq!(deserialized.cumulative_deposit_interest, 1_050_000_000_000);
210 }
211
212 #[test]
213 fn cache_spot_market_roundtrip() {
214 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}"#;
215 let deserialized: Cache<SpotMarket> = serde_json::from_str(json).unwrap();
216 assert_eq!(deserialized.account.market_index, 0);
217 assert_eq!(deserialized.last_updated_slot, 12345);
218 }
219
220 #[test]
221 fn spot_market_ignores_unknown_fields() {
222 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}"#;
223 let market: SpotMarket = serde_json::from_str(json).unwrap();
225 assert_eq!(market.market_index, 1);
226 }
227
228 #[test]
231 fn kamino_reserve_roundtrip() {
232 let mut reserve = KaminoReserve::default();
233 reserve.liquidity.total_available_amount = 1_000_000;
234 reserve.liquidity.mint_decimals = 6;
235 reserve.collateral.mint_total_supply = 999;
236 reserve.config.loan_to_value_pct = 80;
237 reserve.last_update.slot = 100;
238
239 let json = serde_json::to_string(&reserve).unwrap();
240 let deserialized: KaminoReserve = serde_json::from_str(&json).unwrap();
241 assert_eq!(deserialized.liquidity.total_available_amount, 1_000_000);
242 assert_eq!(deserialized.liquidity.mint_decimals, 6);
243 assert_eq!(deserialized.collateral.mint_total_supply, 999);
244 assert_eq!(deserialized.config.loan_to_value_pct, 80);
245 assert_eq!(deserialized.last_update.slot, 100);
246 }
247
248 #[test]
249 fn kamino_obligation_roundtrip() {
250 let mut obligation = KaminoObligation::default();
251 obligation.deposits[0].deposited_amount = 5000;
252 obligation.deposits[0].market_value_sf = 42;
253 obligation.borrows[0].borrowed_amount_sf = 100;
254 obligation.borrows[0].market_value_sf = 50;
255 obligation.has_debt = 1;
256
257 let json = serde_json::to_string(&obligation).unwrap();
258 let deserialized: KaminoObligation = serde_json::from_str(&json).unwrap();
259 assert_eq!(deserialized.deposits[0].deposited_amount, 5000);
260 assert_eq!(deserialized.borrows[0].borrowed_amount_sf, 100);
261 assert_eq!(deserialized.has_debt, 1);
262 assert_eq!(deserialized.deposits.len(), 8);
264 assert_eq!(deserialized.borrows.len(), 5);
265 }
266
267 #[test]
268 fn kamino_withdraw_ticket_roundtrip() {
269 let pk = serde_json::to_string(&solana_pubkey::Pubkey::default()).unwrap();
270 let json = format!(
271 concat!(
272 r#"{{"sequence_number":42,"owner":{pk},"reserve":{pk},"#,
273 r#""user_destination_liquidity_ta":{pk},"#,
274 r#""queued_collateral_amount":1000,"created_at_timestamp":12345,"invalid":0}}"#,
275 ),
276 pk = pk
277 );
278 let ticket: KaminoWithdrawTicket = serde_json::from_str(&json).unwrap();
279 assert_eq!(ticket.sequence_number, 42);
280 assert_eq!(ticket.queued_collateral_amount, 1000);
281
282 let serialized = serde_json::to_string(&ticket).unwrap();
283 let deserialized: KaminoWithdrawTicket = serde_json::from_str(&serialized).unwrap();
284 assert_eq!(deserialized.sequence_number, 42);
285 }
286
287 #[test]
288 fn cache_kamino_obligation() {
289 let obligation = KaminoObligation::default();
290 let cache = Cache {
291 account: obligation,
292 last_updated_slot: 300,
293 };
294 let json = serde_json::to_string(&cache).unwrap();
295 let deserialized: Cache<KaminoObligation> = serde_json::from_str(&json).unwrap();
296 assert_eq!(deserialized.last_updated_slot, 300);
297 assert_eq!(deserialized.account.deposits[0].deposited_amount, 0);
299 }
300
301 #[test]
302 fn cache_kamino_reserve() {
303 let mut reserve = KaminoReserve::default();
304 reserve.liquidity.total_available_amount = 1_000_000;
305 let cache = Cache {
306 account: reserve,
307 last_updated_slot: 400,
308 };
309 let json = serde_json::to_string(&cache).unwrap();
310 let deserialized: Cache<KaminoReserve> = serde_json::from_str(&json).unwrap();
311 assert_eq!(deserialized.last_updated_slot, 400);
312 assert_eq!(
313 deserialized.account.liquidity.total_available_amount,
314 1_000_000
315 );
316 }
317
318 #[test]
319 fn kamino_elevation_group_defaults() {
320 let group = KaminoElevationGroup::default();
321 assert_eq!(group.id, 0);
322 assert_eq!(group.ltv_pct, 0);
323 assert_eq!(group.liquidation_threshold_pct, 0);
324 assert_eq!(group.max_liquidation_bonus_bps, 0);
325 assert_eq!(group.allow_new_loans, 0);
326 assert_eq!(group.max_reserves_as_collateral, 0);
327 assert_eq!(group.debt_reserve, solana_pubkey::Pubkey::default());
328 }
329
330 #[test]
333 fn kamino_obligation_rejects_partial_json() {
334 let pk = serde_json::to_string(&solana_pubkey::Pubkey::default()).unwrap();
336 let json = format!(r#"{{"owner":{pk},"tag":0}}"#, pk = pk);
337 let result = serde_json::from_str::<KaminoObligation>(&json);
338 assert!(
339 result.is_err(),
340 "partial KaminoObligation should fail without serde(default)"
341 );
342 }
343
344 #[test]
345 fn kamino_reserve_rejects_partial_json() {
346 let json = r#"{"liquidity":{"mint_decimals":6,"total_available_amount":0}}"#;
348 let result = serde_json::from_str::<KaminoReserve>(&json);
349 assert!(
350 result.is_err(),
351 "partial KaminoReserve should fail without serde(default)"
352 );
353 }
354
355 #[test]
356 fn kamino_reserve_fees_rejects_empty_json() {
357 let result = serde_json::from_str::<KaminoReserveFees>("{}");
358 assert!(
359 result.is_err(),
360 "empty JSON should fail for KaminoReserveFees without serde(default)"
361 );
362 }
363}