1use std::{collections::BTreeMap, str::FromStr};
19
20use nautilus_core::serialization::{
21 deserialize_decimal, deserialize_decimal_from_str, deserialize_decimal_or_zero,
22 deserialize_optional_decimal,
23};
24use rust_decimal::Decimal;
25use serde::{Deserialize, Serialize, de};
26use ustr::Ustr;
27
28use crate::common::enums::{
29 LighterCandleResolution, LighterFundingResolution, LighterMarketStatus, LighterOrderKind,
30 LighterOrderSide, LighterOrderStatus, LighterOrderTimeInForce, LighterPositionMarginMode,
31 LighterProductType, LighterTradeType, LighterTriggerStatus,
32};
33
34#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
35pub struct LighterResultCode {
36 pub code: i32,
37 pub message: Option<String>,
38}
39
40#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
47pub struct LighterNextNonce {
48 pub code: i32,
49 pub message: Option<String>,
50 pub nonce: i64,
51}
52
53#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
58pub struct LighterAccountDetail {
59 pub account_index: u64,
60 pub account_type: u8,
61 pub status: i32,
62}
63
64#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
66pub struct LighterAccountsResponse {
67 pub code: i32,
68 pub message: Option<String>,
69 pub total: i64,
70 pub accounts: Vec<LighterAccountDetail>,
71}
72
73#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
82pub struct LighterMakerOnlyApiKeys {
83 pub code: i32,
84 pub message: Option<String>,
85 #[serde(default)]
86 pub api_key_indexes: Vec<i64>,
87}
88
89#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
90pub struct LighterSendTxRequest {
91 pub tx_type: u8,
92 pub tx_info: String,
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub price_protection: Option<bool>,
95}
96
97impl LighterSendTxRequest {
98 #[must_use]
99 pub fn new(tx_type: u8, tx_info: impl Into<String>) -> Self {
100 Self {
101 tx_type,
102 tx_info: tx_info.into(),
103 price_protection: None,
104 }
105 }
106
107 #[must_use]
108 pub const fn with_price_protection(mut self, price_protection: bool) -> Self {
109 self.price_protection = Some(price_protection);
110 self
111 }
112
113 pub(crate) fn form_fields(&self) -> Vec<(&'static str, String)> {
114 let mut fields = vec![
115 ("tx_type", self.tx_type.to_string()),
116 ("tx_info", self.tx_info.clone()),
117 ];
118
119 if let Some(price_protection) = self.price_protection {
120 fields.push(("price_protection", price_protection.to_string()));
121 }
122 fields
123 }
124}
125
126#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
127pub struct LighterSendTxBatchRequest {
128 pub tx_types: String,
129 pub tx_infos: String,
130}
131
132impl LighterSendTxBatchRequest {
133 #[must_use]
134 pub fn new(tx_types: impl Into<String>, tx_infos: impl Into<String>) -> Self {
135 Self {
136 tx_types: tx_types.into(),
137 tx_infos: tx_infos.into(),
138 }
139 }
140
141 pub(crate) fn form_fields(&self) -> Vec<(&'static str, String)> {
142 vec![
143 ("tx_types", self.tx_types.clone()),
144 ("tx_infos", self.tx_infos.clone()),
145 ]
146 }
147}
148
149#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
150pub struct LighterSendTxResponse {
151 pub code: i32,
152 pub message: Option<String>,
153 pub tx_hash: String,
154 pub predicted_execution_time_ms: i64,
155 pub volume_quota_remaining: Option<i64>,
156}
157
158#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
159pub struct LighterSendTxBatchResponse {
160 pub code: i32,
161 pub message: Option<String>,
162 pub tx_hash: Vec<String>,
163 pub predicted_execution_time_ms: i64,
164 pub volume_quota_remaining: Option<i64>,
165}
166
167#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
168pub struct LighterOrderBooks {
169 pub code: i32,
170 pub message: Option<String>,
171 #[serde(default, deserialize_with = "deserialize_null_vec")]
172 pub order_books: Vec<LighterOrderBook>,
173}
174
175#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
176pub struct LighterOrderBook {
177 pub symbol: Ustr,
178 pub market_id: i16,
179 pub market_type: LighterProductType,
180 pub base_asset_id: i16,
181 pub quote_asset_id: i16,
182 pub status: LighterMarketStatus,
183 #[serde(deserialize_with = "deserialize_decimal_from_str")]
184 pub taker_fee: Decimal,
185 #[serde(deserialize_with = "deserialize_decimal_from_str")]
186 pub maker_fee: Decimal,
187 #[serde(deserialize_with = "deserialize_decimal_from_str")]
188 pub liquidation_fee: Decimal,
189 #[serde(deserialize_with = "deserialize_decimal_from_str")]
190 pub min_base_amount: Decimal,
191 #[serde(deserialize_with = "deserialize_decimal_from_str")]
192 pub min_quote_amount: Decimal,
193 #[serde(deserialize_with = "deserialize_decimal_from_str")]
194 pub order_quote_limit: Decimal,
195 pub supported_size_decimals: u8,
196 pub supported_price_decimals: u8,
197 pub supported_quote_decimals: u8,
198}
199
200#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
201pub struct LighterOrderBookDetails {
202 pub code: i32,
203 pub message: Option<String>,
204 #[serde(default, deserialize_with = "deserialize_null_vec")]
205 pub order_book_details: Vec<LighterPerpOrderBookDetail>,
206 #[serde(default, deserialize_with = "deserialize_null_vec")]
207 pub spot_order_book_details: Vec<LighterSpotOrderBookDetail>,
208}
209
210#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
211pub struct LighterPerpOrderBookDetail {
212 #[serde(flatten)]
213 pub order_book: LighterOrderBook,
214 pub size_decimals: u8,
215 pub price_decimals: u8,
216 pub quote_multiplier: i64,
217 pub default_initial_margin_fraction: u16,
218 pub min_initial_margin_fraction: u16,
219 pub maintenance_margin_fraction: u16,
220 pub closeout_margin_fraction: u16,
221 #[serde(deserialize_with = "deserialize_decimal")]
222 pub last_trade_price: Decimal,
223 pub daily_trades_count: i64,
224 #[serde(deserialize_with = "deserialize_decimal")]
225 pub daily_base_token_volume: Decimal,
226 #[serde(deserialize_with = "deserialize_decimal")]
227 pub daily_quote_token_volume: Decimal,
228 #[serde(deserialize_with = "deserialize_decimal")]
229 pub daily_price_low: Decimal,
230 #[serde(deserialize_with = "deserialize_decimal")]
231 pub daily_price_high: Decimal,
232 #[serde(deserialize_with = "deserialize_decimal")]
233 pub daily_price_change: Decimal,
234 #[serde(deserialize_with = "deserialize_decimal")]
235 pub open_interest: Decimal,
236 #[serde(deserialize_with = "deserialize_decimal_btree_map")]
237 pub daily_chart: BTreeMap<String, Decimal>,
238 pub market_config: LighterMarketConfig,
239 pub strategy_index: u8,
240}
241
242#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
243pub struct LighterSpotOrderBookDetail {
244 #[serde(flatten)]
245 pub order_book: LighterOrderBook,
246 pub size_decimals: u8,
247 pub price_decimals: u8,
248 #[serde(deserialize_with = "deserialize_decimal")]
249 pub last_trade_price: Decimal,
250 pub daily_trades_count: i64,
251 #[serde(deserialize_with = "deserialize_decimal")]
252 pub daily_base_token_volume: Decimal,
253 #[serde(deserialize_with = "deserialize_decimal")]
254 pub daily_quote_token_volume: Decimal,
255 #[serde(deserialize_with = "deserialize_decimal")]
256 pub daily_price_low: Decimal,
257 #[serde(deserialize_with = "deserialize_decimal")]
258 pub daily_price_high: Decimal,
259 #[serde(deserialize_with = "deserialize_decimal")]
260 pub daily_price_change: Decimal,
261 #[serde(deserialize_with = "deserialize_decimal_btree_map")]
262 pub daily_chart: BTreeMap<String, Decimal>,
263}
264
265#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
266pub struct LighterMarketConfig {
267 pub market_margin_mode: LighterPositionMarginMode,
268 pub insurance_fund_account_index: i64,
269 pub liquidation_mode: i32,
270 pub force_reduce_only: bool,
271 pub trading_hours: String,
272 pub funding_fee_discounts_enabled: bool,
273 pub hidden: bool,
274}
275
276#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
277pub struct LighterOrderBookOrders {
278 pub code: i32,
279 pub message: Option<String>,
280 pub total_asks: i64,
281 #[serde(default, deserialize_with = "deserialize_null_vec")]
282 pub asks: Vec<LighterSimpleOrder>,
283 pub total_bids: i64,
284 #[serde(default, deserialize_with = "deserialize_null_vec")]
285 pub bids: Vec<LighterSimpleOrder>,
286}
287
288#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
289pub struct LighterSimpleOrder {
290 pub order_index: i64,
291 pub order_id: String,
292 pub owner_account_index: i64,
293 #[serde(deserialize_with = "deserialize_decimal_from_str")]
294 pub initial_base_amount: Decimal,
295 #[serde(deserialize_with = "deserialize_decimal_from_str")]
296 pub remaining_base_amount: Decimal,
297 #[serde(deserialize_with = "deserialize_decimal_from_str")]
298 pub price: Decimal,
299 pub order_expiry: i64,
300 pub transaction_time: i64,
301}
302
303#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
304pub struct LighterOrderBookDepth {
305 pub code: i32,
306 pub message: Option<String>,
307 #[serde(default, deserialize_with = "deserialize_null_vec")]
308 pub asks: Vec<LighterPriceLevel>,
309 #[serde(default, deserialize_with = "deserialize_null_vec")]
310 pub bids: Vec<LighterPriceLevel>,
311 pub offset: i64,
312 pub nonce: i64,
313}
314
315#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
324pub struct LighterPriceLevel {
325 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
326 pub price: Decimal,
327 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
328 pub size: Decimal,
329}
330
331#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
332pub struct LighterTrades {
333 pub code: i32,
334 pub message: Option<String>,
335 pub next_cursor: Option<String>,
336 #[serde(default, deserialize_with = "deserialize_null_vec")]
337 pub trades: Vec<LighterTrade>,
338}
339
340#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
341pub struct LighterCandles {
342 pub code: i32,
343 pub message: Option<String>,
344 #[serde(rename = "r")]
345 pub resolution: LighterCandleResolution,
346 #[serde(rename = "c", default, deserialize_with = "deserialize_null_vec")]
347 pub candles: Vec<LighterCandle>,
348}
349
350#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
351pub struct LighterCandle {
352 #[serde(rename = "t")]
353 pub timestamp: i64,
354 #[serde(rename = "o", default, deserialize_with = "deserialize_decimal")]
355 pub open: Decimal,
356 #[serde(rename = "h", default, deserialize_with = "deserialize_decimal")]
357 pub high: Decimal,
358 #[serde(rename = "l", default, deserialize_with = "deserialize_decimal")]
359 pub low: Decimal,
360 #[serde(rename = "c", default, deserialize_with = "deserialize_decimal")]
361 pub close: Decimal,
362 #[serde(rename = "v", default, deserialize_with = "deserialize_decimal")]
363 pub volume_base: Decimal,
364 #[serde(rename = "V", default, deserialize_with = "deserialize_decimal")]
365 pub volume_quote: Decimal,
366 #[serde(rename = "i")]
367 pub last_trade_id: i64,
368}
369
370#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
371pub struct LighterFundings {
372 pub code: i32,
373 pub message: Option<String>,
374 pub resolution: LighterFundingResolution,
375 #[serde(default, deserialize_with = "deserialize_null_vec")]
376 pub fundings: Vec<LighterFunding>,
377}
378
379#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
380pub struct LighterFunding {
381 pub timestamp: i64,
382 #[serde(deserialize_with = "deserialize_decimal")]
383 pub value: Decimal,
384 #[serde(deserialize_with = "deserialize_decimal")]
385 pub rate: Decimal,
386 pub direction: LighterFundingDirection,
387}
388
389#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
390#[serde(rename_all = "lowercase")]
391pub enum LighterFundingDirection {
392 Long,
393 Short,
394}
395
396#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
397pub struct LighterTrade {
398 pub trade_id: i64,
399 pub trade_id_str: Option<String>,
400 pub tx_hash: String,
401 #[serde(rename = "type")]
402 pub trade_type: LighterTradeType,
403 pub market_id: i16,
404 #[serde(deserialize_with = "deserialize_decimal_from_str")]
405 pub size: Decimal,
406 #[serde(deserialize_with = "deserialize_decimal_from_str")]
407 pub price: Decimal,
408 #[serde(deserialize_with = "deserialize_decimal_from_str")]
409 pub usd_amount: Decimal,
410 pub ask_id: i64,
411 pub ask_id_str: Option<String>,
412 pub bid_id: i64,
413 pub bid_id_str: Option<String>,
414 pub ask_client_id: i64,
415 pub ask_client_id_str: Option<String>,
416 pub bid_client_id: i64,
417 pub bid_client_id_str: Option<String>,
418 pub ask_account_id: i64,
419 pub bid_account_id: i64,
420 pub is_maker_ask: bool,
421 pub block_height: i64,
422 pub timestamp: i64,
423 pub taker_fee: Option<i32>,
424 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
425 pub taker_position_size_before: Option<Decimal>,
426 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
427 pub taker_entry_quote_before: Option<Decimal>,
428 pub taker_initial_margin_fraction_before: Option<u16>,
429 pub taker_position_sign_changed: Option<bool>,
430 pub maker_fee: Option<i32>,
431 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
432 pub maker_position_size_before: Option<Decimal>,
433 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
434 pub maker_entry_quote_before: Option<Decimal>,
435 pub maker_initial_margin_fraction_before: Option<u16>,
436 pub maker_position_sign_changed: Option<bool>,
437 pub transaction_time: i64,
438 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
439 pub ask_account_pnl: Option<Decimal>,
440 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
441 pub bid_account_pnl: Option<Decimal>,
442}
443
444#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
445pub struct LighterOrders {
446 pub code: i32,
447 pub message: Option<String>,
448 pub next_cursor: Option<String>,
449 #[serde(default, deserialize_with = "deserialize_null_vec")]
450 pub orders: Vec<LighterOrder>,
451}
452
453#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
454pub struct LighterOrder {
455 pub order_index: i64,
456 pub client_order_index: i64,
457 pub order_id: String,
458 pub client_order_id: String,
459 pub market_index: i16,
460 pub owner_account_index: i64,
461 #[serde(deserialize_with = "deserialize_decimal_from_str")]
462 pub initial_base_amount: Decimal,
463 #[serde(deserialize_with = "deserialize_decimal_from_str")]
464 pub price: Decimal,
465 pub nonce: i64,
466 #[serde(deserialize_with = "deserialize_decimal_from_str")]
467 pub remaining_base_amount: Decimal,
468 pub is_ask: bool,
469 pub base_size: i64,
470 pub base_price: i32,
471 #[serde(deserialize_with = "deserialize_decimal_from_str")]
472 pub filled_base_amount: Decimal,
473 #[serde(deserialize_with = "deserialize_decimal_from_str")]
474 pub filled_quote_amount: Decimal,
475 #[serde(default, deserialize_with = "deserialize_order_side")]
476 pub side: Option<LighterOrderSide>,
477 #[serde(rename = "type")]
478 pub order_type: LighterOrderKind,
479 pub time_in_force: LighterOrderTimeInForce,
480 pub reduce_only: bool,
481 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
483 pub trigger_price: Decimal,
484 pub order_expiry: i64,
485 pub status: LighterOrderStatus,
486 pub trigger_status: LighterTriggerStatus,
487 pub trigger_time: i64,
488 pub parent_order_index: i64,
489 pub parent_order_id: String,
490 pub to_trigger_order_id_0: String,
491 pub to_trigger_order_id_1: String,
492 pub to_cancel_order_id_0: String,
493 pub integrator_fee_collector_index: String,
494 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
495 pub integrator_taker_fee: Decimal,
496 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
497 pub integrator_maker_fee: Decimal,
498 pub block_height: i64,
499 pub timestamp: i64,
500 pub created_at: i64,
501 pub updated_at: i64,
502 pub transaction_time: i64,
503}
504
505#[derive(Deserialize)]
506struct DecimalFromAny(#[serde(deserialize_with = "deserialize_decimal")] Decimal);
507
508fn deserialize_decimal_btree_map<'de, D>(
509 deserializer: D,
510) -> Result<BTreeMap<String, Decimal>, D::Error>
511where
512 D: serde::Deserializer<'de>,
513{
514 BTreeMap::<String, DecimalFromAny>::deserialize(deserializer).map(|values| {
515 values
516 .into_iter()
517 .map(|(key, decimal)| (key, decimal.0))
518 .collect()
519 })
520}
521
522fn deserialize_null_vec<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
523where
524 D: serde::Deserializer<'de>,
525 T: Deserialize<'de>,
526{
527 Option::<Vec<T>>::deserialize(deserializer).map(Option::unwrap_or_default)
528}
529
530fn deserialize_order_side<'de, D>(deserializer: D) -> Result<Option<LighterOrderSide>, D::Error>
531where
532 D: serde::Deserializer<'de>,
533{
534 let value = Option::<String>::deserialize(deserializer)?;
535 match value.as_deref() {
536 None | Some("") => Ok(None),
537 Some(s) => LighterOrderSide::from_str(s)
538 .map(Some)
539 .map_err(|_| de::Error::unknown_variant(s, &["buy", "sell", ""])),
540 }
541}
542
543#[cfg(test)]
544mod tests {
545 use rstest::rstest;
546
547 use super::*;
548
549 const HTTP_ORDER_BOOK_DETAILS: &str =
550 include_str!("../../test_data/http_order_book_details.json");
551 const HTTP_RECENT_TRADES: &str = include_str!("../../test_data/http_recent_trades.json");
552 const HTTP_RECENT_TRADES_MISSING: &str =
553 include_str!("../../test_data/http_recent_trades_missing.json");
554 const HTTP_RECENT_TRADES_NULL: &str =
555 include_str!("../../test_data/http_recent_trades_null.json");
556 const HTTP_ORDER_BOOKS: &str = include_str!("../../test_data/http_order_books.json");
557 const HTTP_ORDER_BOOK_ORDERS: &str =
558 include_str!("../../test_data/http_order_book_orders.json");
559 const HTTP_ORDER_BOOK_DEPTH: &str = include_str!("../../test_data/http_order_book_depth.json");
560 const HTTP_ORDER_BOOK_DEPTH_NULL: &str =
561 include_str!("../../test_data/http_order_book_depth_null.json");
562 const HTTP_ORDERS: &str = include_str!("../../test_data/http_orders.json");
563 const HTTP_CANDLES: &str = include_str!("../../test_data/http_candles.json");
564 const HTTP_CANDLES_NULL: &str = include_str!("../../test_data/http_candles_null.json");
565 const HTTP_FUNDINGS: &str = include_str!("../../test_data/http_fundings.json");
566 const HTTP_ACCOUNT: &str = include_str!("../../test_data/http_account.json");
567
568 #[rstest]
569 fn test_account_response_deserializes_live_shape() {
570 let response: LighterAccountsResponse = serde_json::from_str(HTTP_ACCOUNT).unwrap();
571
572 assert_eq!(response.code, 200);
573 assert_eq!(response.total, 1);
574 assert_eq!(response.accounts.len(), 1);
575 let account = &response.accounts[0];
576 assert_eq!(account.account_index, 123_456);
577 assert_eq!(account.account_type, 0);
578 assert_eq!(account.status, 1);
579 }
580
581 #[rstest]
582 fn test_order_book_details_deserializes_live_shape() {
583 let details: LighterOrderBookDetails =
584 serde_json::from_str(HTTP_ORDER_BOOK_DETAILS).unwrap();
585
586 assert_eq!(details.code, 200);
587 assert_eq!(details.order_book_details.len(), 1);
588 assert_eq!(
589 details.order_book_details[0].order_book.market_type,
590 LighterProductType::Perp,
591 );
592 assert_eq!(details.order_book_details[0].price_decimals, 2);
593 assert_eq!(
594 details.order_book_details[0].last_trade_price,
595 Decimal::new(236_131, 2),
596 );
597 assert_eq!(
598 details.order_book_details[0].daily_base_token_volume,
599 Decimal::new(1_953_991_521, 4),
600 );
601 assert_eq!(
602 details.order_book_details[0]
603 .market_config
604 .market_margin_mode,
605 LighterPositionMarginMode::Cross,
606 );
607 assert!(details.spot_order_book_details.is_empty());
608 }
609
610 #[rstest]
611 fn test_recent_trades_allow_omitted_fee_fields() {
612 let trades: LighterTrades = serde_json::from_str(HTTP_RECENT_TRADES).unwrap();
613
614 assert_eq!(trades.trades.len(), 1);
615 assert_eq!(
616 trades.trades[0].trade_id_str.as_deref(),
617 Some("19211490282")
618 );
619 assert_eq!(trades.trades[0].taker_fee, None);
620 assert_eq!(trades.trades[0].maker_fee, Some(28));
621 }
622
623 #[rstest]
624 fn test_recent_trades_deserializes_null_trades_as_empty() {
625 let trades: LighterTrades = serde_json::from_str(HTTP_RECENT_TRADES_NULL).unwrap();
626
627 assert_eq!(trades.code, 200);
628 assert!(trades.trades.is_empty());
629 }
630
631 #[rstest]
632 fn test_recent_trades_deserializes_missing_trades_as_empty() {
633 let trades: LighterTrades = serde_json::from_str(HTTP_RECENT_TRADES_MISSING).unwrap();
634
635 assert_eq!(trades.code, 200);
636 assert!(trades.trades.is_empty());
637 }
638
639 #[rstest]
640 fn test_candles_deserializes_live_shape_with_omitted_raw_fields() {
641 let candles: LighterCandles = serde_json::from_str(HTTP_CANDLES).unwrap();
642
643 assert_eq!(candles.code, 200);
644 assert_eq!(candles.resolution, LighterCandleResolution::OneMinute);
645 assert_eq!(candles.candles.len(), 2);
646 assert_eq!(candles.candles[0].timestamp, 1_700_000_000_000);
647 assert_eq!(candles.candles[0].open, Decimal::new(236_111, 2));
648 assert_eq!(candles.candles[0].high, Decimal::new(236_222, 2));
649 assert_eq!(candles.candles[0].low, Decimal::new(236_000, 2));
650 assert_eq!(candles.candles[0].close, Decimal::new(236_131, 2));
651 assert_eq!(candles.candles[0].volume_base, Decimal::new(12_345, 4));
652 assert_eq!(candles.candles[0].last_trade_id, 19_211_490_282);
653 }
654
655 #[rstest]
656 fn test_candles_deserializes_null_candles_as_empty() {
657 let candles: LighterCandles = serde_json::from_str(HTTP_CANDLES_NULL).unwrap();
658
659 assert_eq!(candles.code, 200);
660 assert_eq!(candles.resolution, LighterCandleResolution::OneMinute);
661 assert!(candles.candles.is_empty());
662 }
663
664 #[rstest]
665 fn test_fundings_deserializes_live_shape() {
666 let fundings: LighterFundings = serde_json::from_str(HTTP_FUNDINGS).unwrap();
667
668 assert_eq!(fundings.code, 200);
669 assert_eq!(fundings.resolution, LighterFundingResolution::OneHour);
670 assert_eq!(fundings.fundings.len(), 2);
671 assert_eq!(fundings.fundings[0].timestamp, 1_778_702_400);
672 assert_eq!(fundings.fundings[0].rate, Decimal::new(12, 4));
673 assert_eq!(
674 fundings.fundings[0].direction,
675 LighterFundingDirection::Long
676 );
677 assert_eq!(
678 fundings.fundings[1].direction,
679 LighterFundingDirection::Short
680 );
681 }
682
683 #[rstest]
684 fn test_order_books_deserializes_live_shape() {
685 let order_books: LighterOrderBooks = serde_json::from_str(HTTP_ORDER_BOOKS).unwrap();
686
687 assert_eq!(order_books.code, 200);
688 assert_eq!(order_books.order_books.len(), 1);
689 assert_eq!(order_books.order_books[0].symbol, Ustr::from("ETH"));
690 assert_eq!(
691 order_books.order_books[0].market_type,
692 LighterProductType::Perp
693 );
694 assert_eq!(
695 order_books.order_books[0].status,
696 LighterMarketStatus::Active
697 );
698 assert_eq!(order_books.order_books[0].supported_price_decimals, 2);
699 }
700
701 #[rstest]
702 fn test_order_book_orders_deserializes_live_shape() {
703 let book: LighterOrderBookOrders = serde_json::from_str(HTTP_ORDER_BOOK_ORDERS).unwrap();
704
705 assert_eq!(book.total_asks, 1);
706 assert_eq!(book.asks[0].order_id, "281476929689581");
707 assert_eq!(book.asks[0].price, Decimal::from_str("2361.32").unwrap());
708 assert_eq!(book.total_bids, 1);
709 assert_eq!(
710 book.bids[0].remaining_base_amount,
711 Decimal::from_str("3.4125").unwrap(),
712 );
713 }
714
715 #[rstest]
716 fn test_order_book_depth_deserializes_live_shape() {
717 let depth: LighterOrderBookDepth = serde_json::from_str(HTTP_ORDER_BOOK_DEPTH).unwrap();
718
719 assert_eq!(depth.code, 200);
720 assert_eq!(depth.asks[0].price, Decimal::from_str("2352.74").unwrap());
721 assert_eq!(depth.bids[0].size, Decimal::from_str("0.2125").unwrap());
722 assert_eq!(depth.offset, 1_558_300);
723 assert_eq!(depth.nonce, 9_182_390_020);
724 }
725
726 #[rstest]
727 fn test_order_book_depth_deserializes_null_sides_as_empty() {
728 let depth: LighterOrderBookDepth =
729 serde_json::from_str(HTTP_ORDER_BOOK_DEPTH_NULL).unwrap();
730
731 assert_eq!(depth.code, 200);
732 assert!(depth.asks.is_empty());
733 assert!(depth.bids.is_empty());
734 assert_eq!(depth.offset, 1);
735 assert_eq!(depth.nonce, 0);
736 }
737
738 #[rstest]
739 fn test_orders_deserializes_live_shape() {
740 let orders: LighterOrders = serde_json::from_str(HTTP_ORDERS).unwrap();
741
742 assert_eq!(orders.next_cursor.as_deref(), Some("cursor-1"));
743 assert_eq!(orders.orders.len(), 1);
744 assert_eq!(orders.orders[0].order_type, LighterOrderKind::Limit);
745 assert_eq!(
746 orders.orders[0].time_in_force,
747 LighterOrderTimeInForce::GoodTillTime,
748 );
749 assert_eq!(orders.orders[0].status, LighterOrderStatus::Open);
750 assert_eq!(orders.orders[0].trigger_status, LighterTriggerStatus::Na);
751 assert_eq!(orders.orders[0].side, Some(LighterOrderSide::Sell));
752 assert!(orders.orders[0].is_ask);
753 }
754
755 #[rstest]
756 fn test_orders_allows_empty_side_with_is_ask() {
757 let mut value: serde_json::Value = serde_json::from_str(HTTP_ORDERS).unwrap();
758 value["orders"][0]["side"] = serde_json::Value::String(String::new());
759
760 let orders: LighterOrders = serde_json::from_value(value).unwrap();
761
762 assert_eq!(orders.orders[0].side, None);
763 assert!(orders.orders[0].is_ask);
764 }
765}