1use serde::{Deserialize, Serialize};
2
3pub const PRICE_SCALE: i128 = 100_000_000;
7
8#[derive(
15 Debug, Clone, Serialize, Deserialize, PartialEq, borsh::BorshSerialize, borsh::BorshDeserialize,
16)]
17pub struct AccountLeaf {
18 pub cash: i128,
20 pub nonce: u64,
21 pub margin_mode: u8,
22 pub liquidation_state: u8,
23}
24
25#[derive(
30 Debug, Clone, Serialize, Deserialize, PartialEq, borsh::BorshSerialize, borsh::BorshDeserialize,
31)]
32pub struct OptionPositionLeaf {
33 pub quantity: i128,
35 pub entry_price: i128,
37}
38
39#[derive(
44 Debug, Clone, Serialize, Deserialize, PartialEq, borsh::BorshSerialize, borsh::BorshDeserialize,
45)]
46pub struct OrderLeaf {
47 pub order_id: u64,
49 pub symbol: String,
51 pub side: u8,
53 pub price: i128,
55 pub remaining_size: i128,
57 pub original_size: i128,
59 pub client_id: Option<String>,
61 pub mmp_enabled: bool,
63}
64
65#[derive(
71 Debug, Clone, Serialize, Deserialize, PartialEq, borsh::BorshSerialize, borsh::BorshDeserialize,
72)]
73pub struct PerpPositionLeaf {
74 pub size: i128,
76 pub entry_price: i128,
78}
79
80#[derive(
85 Debug, Clone, Serialize, Deserialize, PartialEq, borsh::BorshSerialize, borsh::BorshDeserialize,
86)]
87pub struct MmpConfigLeaf {
88 pub enabled: bool,
89 pub interval_ms: i64,
90 pub frozen_time_ms: i64,
91 pub qty_limit: Option<i128>,
93 pub delta_limit: Option<i128>,
95 pub vega_limit: Option<i128>,
97}
98
99#[derive(
104 Debug, Clone, Serialize, Deserialize, PartialEq, borsh::BorshSerialize, borsh::BorshDeserialize,
105)]
106pub struct InstrumentLeaf {
107 pub expired: bool,
108 pub trading_mode: u8,
110 pub expiry: u64,
111 pub strike: i128,
113 pub option_type: u8,
115}
116
117#[derive(
123 Debug, Clone, Serialize, Deserialize, PartialEq, borsh::BorshSerialize, borsh::BorshDeserialize,
124)]
125pub struct OracleLeaf {
126 pub spot_price: i128,
128 pub iv_surface_hash: [u8; 32],
130}
131
132#[derive(
135 Debug, Clone, Serialize, Deserialize, PartialEq, borsh::BorshSerialize, borsh::BorshDeserialize,
136)]
137pub struct GlobalLeaf {
138 pub next_order_id: u64,
139 pub next_trade_id: u64,
140 pub command_chain_root: [u8; 32],
141 pub command_chain_seq: u64,
142}
143
144#[derive(
150 Debug, Clone, Serialize, Deserialize, PartialEq, borsh::BorshSerialize, borsh::BorshDeserialize,
151)]
152pub struct RiskLeaf {
153 pub equity: i128,
155 pub initial_margin: i128,
157 pub maintenance_margin: i128,
159 pub scanning_risk: i128,
161 pub health_nonce: u64,
163}
164
165pub fn leaf_to_bytes<T: borsh::BorshSerialize>(leaf: &T) -> Vec<u8> {
167 borsh::to_vec(leaf).expect("borsh serialization should not fail for fixed-size types")
168}
169
170pub fn leaf_from_bytes<T: borsh::BorshDeserialize>(bytes: &[u8]) -> Result<T, std::io::Error> {
172 borsh::from_slice(bytes)
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn account_leaf_round_trip() {
181 let leaf = AccountLeaf {
182 cash: 1_000_000 * PRICE_SCALE,
183 nonce: 42,
184 margin_mode: 1,
185 liquidation_state: 0,
186 };
187 let bytes = leaf_to_bytes(&leaf);
188 let decoded: AccountLeaf = leaf_from_bytes(&bytes).unwrap();
189 assert_eq!(leaf, decoded);
190 }
191
192 #[test]
193 fn risk_leaf_round_trip() {
194 let leaf = RiskLeaf {
195 equity: 500_000 * PRICE_SCALE,
196 initial_margin: 200_000 * PRICE_SCALE,
197 maintenance_margin: 100_000 * PRICE_SCALE,
198 scanning_risk: 150_000 * PRICE_SCALE,
199 health_nonce: 7,
200 };
201 let bytes = leaf_to_bytes(&leaf);
202 let decoded: RiskLeaf = leaf_from_bytes(&bytes).unwrap();
203 assert_eq!(leaf, decoded);
204 }
205
206 #[test]
207 fn global_leaf_round_trip() {
208 let leaf = GlobalLeaf {
209 next_order_id: 12345,
210 next_trade_id: 6789,
211 command_chain_root: [0xAB; 32],
212 command_chain_seq: 100,
213 };
214 let bytes = leaf_to_bytes(&leaf);
215 let decoded: GlobalLeaf = leaf_from_bytes(&bytes).unwrap();
216 assert_eq!(leaf, decoded);
217 }
218
219 #[test]
220 fn borsh_encoding_is_deterministic() {
221 let leaf = OptionPositionLeaf {
222 quantity: 100_000_000,
223 entry_price: 9500000,
224 };
225 let a = leaf_to_bytes(&leaf);
226 let b = leaf_to_bytes(&leaf);
227 assert_eq!(a, b);
228 }
229
230 #[test]
231 fn perp_leaf_i128_preserves_precision() {
232 let leaf = PerpPositionLeaf {
233 size: 1_234_567_89, entry_price: 95_000_50_000_000, };
236 let bytes = leaf_to_bytes(&leaf);
237 let decoded: PerpPositionLeaf = leaf_from_bytes(&bytes).unwrap();
238 assert_eq!(leaf, decoded);
239 }
240
241 #[test]
242 fn mmp_limits_i128_preserves_fractional() {
243 let leaf = MmpConfigLeaf {
244 enabled: true,
245 interval_ms: 1000,
246 frozen_time_ms: 5000,
247 qty_limit: Some(1_50_000_000), delta_limit: Some(50_000_000), vega_limit: Some(25_000_000), };
251 let bytes = leaf_to_bytes(&leaf);
252 let decoded: MmpConfigLeaf = leaf_from_bytes(&bytes).unwrap();
253 assert_eq!(leaf, decoded);
254 }
255}
256
257#[cfg(kani)]
262mod kani_proofs {
263 use super::*;
264
265 #[kani::proof]
267 fn fast_account_leaf_round_trip_all() {
268 let leaf = AccountLeaf {
269 cash: kani::any(),
270 nonce: kani::any(),
271 margin_mode: kani::any(),
272 liquidation_state: kani::any(),
273 };
274 let bytes = leaf_to_bytes(&leaf);
275 let decoded: AccountLeaf = leaf_from_bytes(&bytes).unwrap();
276 assert_eq!(leaf.cash, decoded.cash);
277 assert_eq!(leaf.nonce, decoded.nonce);
278 assert_eq!(leaf.margin_mode, decoded.margin_mode);
279 assert_eq!(leaf.liquidation_state, decoded.liquidation_state);
280 }
281
282 #[kani::proof]
284 fn fast_option_position_leaf_round_trip_all() {
285 let leaf = OptionPositionLeaf {
286 quantity: kani::any(),
287 entry_price: kani::any(),
288 };
289 let bytes = leaf_to_bytes(&leaf);
290 let decoded: OptionPositionLeaf = leaf_from_bytes(&bytes).unwrap();
291 assert_eq!(leaf.quantity, decoded.quantity);
292 assert_eq!(leaf.entry_price, decoded.entry_price);
293 }
294
295 #[kani::proof]
297 fn fast_perp_position_leaf_round_trip_all() {
298 let leaf = PerpPositionLeaf {
299 size: kani::any(),
300 entry_price: kani::any(),
301 };
302 let bytes = leaf_to_bytes(&leaf);
303 let decoded: PerpPositionLeaf = leaf_from_bytes(&bytes).unwrap();
304 assert_eq!(leaf.size, decoded.size);
305 assert_eq!(leaf.entry_price, decoded.entry_price);
306 }
307
308 #[kani::proof]
310 fn fast_risk_leaf_round_trip_all() {
311 let leaf = RiskLeaf {
312 equity: kani::any(),
313 initial_margin: kani::any(),
314 maintenance_margin: kani::any(),
315 scanning_risk: kani::any(),
316 health_nonce: kani::any(),
317 };
318 let bytes = leaf_to_bytes(&leaf);
319 let decoded: RiskLeaf = leaf_from_bytes(&bytes).unwrap();
320 assert_eq!(leaf.equity, decoded.equity);
321 assert_eq!(leaf.initial_margin, decoded.initial_margin);
322 assert_eq!(leaf.maintenance_margin, decoded.maintenance_margin);
323 assert_eq!(leaf.scanning_risk, decoded.scanning_risk);
324 assert_eq!(leaf.health_nonce, decoded.health_nonce);
325 }
326
327 #[kani::proof]
330 fn fast_global_leaf_round_trip_all() {
331 let leaf = GlobalLeaf {
332 next_order_id: kani::any(),
333 next_trade_id: kani::any(),
334 command_chain_root: kani::any(),
335 command_chain_seq: kani::any(),
336 };
337 let bytes = leaf_to_bytes(&leaf);
338 let decoded: GlobalLeaf = leaf_from_bytes(&bytes).unwrap();
339 assert_eq!(leaf.next_order_id, decoded.next_order_id);
340 assert_eq!(leaf.next_trade_id, decoded.next_trade_id);
341 assert_eq!(leaf.command_chain_root, decoded.command_chain_root);
342 assert_eq!(leaf.command_chain_seq, decoded.command_chain_seq);
343 }
344
345 #[kani::proof]
347 fn fast_account_leaf_serialize_no_panic() {
348 let leaf = AccountLeaf {
349 cash: kani::any(),
350 nonce: kani::any(),
351 margin_mode: kani::any(),
352 liquidation_state: kani::any(),
353 };
354 let _ = leaf_to_bytes(&leaf);
355 }
356
357 #[kani::proof]
359 fn fast_borsh_deterministic() {
360 let quantity: i128 = kani::any();
361 let entry_price: i128 = kani::any();
362 let leaf = OptionPositionLeaf {
363 quantity,
364 entry_price,
365 };
366 let a = leaf_to_bytes(&leaf);
367 let b = leaf_to_bytes(&leaf);
368 assert_eq!(a, b);
369 }
370}