1use std::fmt::Debug;
19
20use ahash::AHashMap;
21use nautilus_core::serialization::{
22 deserialize_decimal, deserialize_decimal_from_str, deserialize_optional_decimal,
23};
24use nautilus_model::{
25 data::{
26 Bar, FundingRateUpdate, IndexPriceUpdate, MarkPriceUpdate, OrderBookDeltas,
27 OrderBookDepth10, QuoteTick, TradeTick,
28 },
29 events::AccountState,
30 reports::PositionStatusReport,
31};
32use rust_decimal::Decimal;
33use serde::{
34 Deserialize, Serialize,
35 de::{self, IgnoredAny, MapAccess, SeqAccess, Visitor},
36};
37use serde_json::value::RawValue;
38use ustr::Ustr;
39
40use crate::{
41 common::enums::LighterCandleResolution,
42 http::models::{LighterOrder, LighterPriceLevel, LighterTrade},
43};
44
45#[derive(Debug, Clone)]
54pub enum NautilusWsMessage {
55 Trades(Vec<TradeTick>),
56 Quote(QuoteTick),
57 Deltas(OrderBookDeltas),
58 Depth10(Box<OrderBookDepth10>),
59 Bar(Bar),
60 MarkPrice(MarkPriceUpdate),
61 IndexPrice(IndexPriceUpdate),
62 FundingRate(FundingRateUpdate),
63 ExecutionReports(Vec<ExecutionReport>),
64 PositionSnapshot {
65 reports: Vec<PositionStatusReport>,
66 skipped_market_ids: Vec<i16>,
67 },
68 AccountState(Box<AccountState>),
69 SendTxAck {
70 tx_hash: Option<String>,
71 code: i64,
72 },
73 SendTxRejected {
74 source: SendTxRejectionSource,
75 code: Option<i64>,
76 message: String,
77 tx_hash: Option<String>,
78 },
79 Raw(serde_json::Value),
80 Reconnected,
81 AccountStreamFirstFrame(AccountStream),
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
92pub enum AccountStream {
93 Orders,
94 Trades,
95 Positions,
96 Assets,
97 UserStats,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub enum SendTxRejectionSource {
110 Ack,
111 BareError,
112}
113
114#[derive(Debug, Clone)]
127#[allow(
128 clippy::large_enum_variant,
129 reason = "payload variants are short-lived and consumed once on the venue-message channel"
130)]
131pub enum ExecutionReport {
132 Order(LighterOrder),
133 Fill(LighterTrade),
134}
135
136#[derive(Clone, Serialize, Deserialize)]
137#[serde(tag = "type")]
138pub enum LighterWsRequest {
139 #[serde(rename = "subscribe")]
140 Subscribe {
141 channel: String,
142 #[serde(skip_serializing_if = "Option::is_none")]
143 auth: Option<String>,
144 },
145 #[serde(rename = "unsubscribe")]
146 Unsubscribe { channel: String },
147 #[serde(rename = "jsonapi/sendtx")]
148 SendTx { data: LighterWsSendTx },
149}
150
151impl Debug for LighterWsRequest {
152 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157 match self {
158 Self::Subscribe { channel, auth } => f
159 .debug_struct(stringify!(Subscribe))
160 .field("channel", channel)
161 .field("authed", &auth.is_some())
162 .finish(),
163 Self::Unsubscribe { channel } => f
164 .debug_struct(stringify!(Unsubscribe))
165 .field("channel", channel)
166 .finish(),
167 Self::SendTx { data } => f
168 .debug_struct(stringify!(SendTx))
169 .field("data", data)
170 .finish(),
171 }
172 }
173}
174
175impl LighterWsRequest {
176 #[must_use]
177 pub fn subscribe(channel: impl Into<String>) -> Self {
178 Self::Subscribe {
179 channel: channel.into(),
180 auth: None,
181 }
182 }
183
184 #[must_use]
185 pub fn subscribe_auth(channel: impl Into<String>, auth: impl Into<String>) -> Self {
186 Self::Subscribe {
187 channel: channel.into(),
188 auth: Some(auth.into()),
189 }
190 }
191
192 #[must_use]
193 pub fn unsubscribe(channel: impl Into<String>) -> Self {
194 Self::Unsubscribe {
195 channel: channel.into(),
196 }
197 }
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct LighterWsSendTx {
208 pub tx_type: u8,
209 pub tx_info: Box<RawValue>,
210}
211
212#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
218pub enum LighterWsChannelKind {
219 OrderBook,
220 Ticker,
221 Trade,
222 Candle,
223 MarketStats,
224 SpotMarketStats,
225 AccountAll,
226 AccountOrders,
227 AccountAllOrders,
228 AccountAllTrades,
229 AccountAllPositions,
230 AccountAllAssets,
231 UserStats,
232 Height,
233}
234
235impl LighterWsChannelKind {
236 #[must_use]
238 pub const fn as_wire_str(self) -> &'static str {
239 match self {
240 Self::OrderBook => "order_book",
241 Self::Ticker => "ticker",
242 Self::Trade => "trade",
243 Self::Candle => "candle",
244 Self::MarketStats => "market_stats",
245 Self::SpotMarketStats => "spot_market_stats",
246 Self::AccountAll => "account_all",
247 Self::AccountOrders => "account_orders",
248 Self::AccountAllOrders => "account_all_orders",
249 Self::AccountAllTrades => "account_all_trades",
250 Self::AccountAllPositions => "account_all_positions",
251 Self::AccountAllAssets => "account_all_assets",
252 Self::UserStats => "user_stats",
253 Self::Height => "height",
254 }
255 }
256
257 #[must_use]
259 pub fn from_wire_str(wire_str: &str) -> Option<Self> {
260 match wire_str {
261 "order_book" => Some(Self::OrderBook),
262 "ticker" => Some(Self::Ticker),
263 "trade" => Some(Self::Trade),
264 "candle" => Some(Self::Candle),
265 "market_stats" => Some(Self::MarketStats),
266 "spot_market_stats" => Some(Self::SpotMarketStats),
267 "account_all" => Some(Self::AccountAll),
268 "account_orders" => Some(Self::AccountOrders),
269 "account_all_orders" => Some(Self::AccountAllOrders),
270 "account_all_trades" => Some(Self::AccountAllTrades),
271 "account_all_positions" => Some(Self::AccountAllPositions),
272 "account_all_assets" => Some(Self::AccountAllAssets),
273 "user_stats" => Some(Self::UserStats),
274 "height" => Some(Self::Height),
275 _ => None,
276 }
277 }
278}
279
280#[derive(Debug, Clone, PartialEq, Eq)]
281pub enum LighterWsChannel {
282 OrderBook(i16),
283 Ticker(i16),
284 MarketStats(LighterMarketSelection),
285 SpotMarketStats(LighterMarketSelection),
286 Trade(i16),
287 Candle {
288 market_index: i16,
289 resolution: LighterCandleResolution,
290 },
291 AccountAll(i64),
292 AccountOrders {
293 market_index: i16,
294 account_index: i64,
295 },
296 AccountAllOrders(i64),
297 AccountAllTrades(i64),
298 AccountAllPositions(i64),
299 AccountAllAssets(i64),
300 UserStats(i64),
301 Height,
302}
303
304impl LighterWsChannel {
305 #[must_use]
307 pub const fn kind(&self) -> LighterWsChannelKind {
308 match self {
309 Self::OrderBook(_) => LighterWsChannelKind::OrderBook,
310 Self::Ticker(_) => LighterWsChannelKind::Ticker,
311 Self::Trade(_) => LighterWsChannelKind::Trade,
312 Self::Candle { .. } => LighterWsChannelKind::Candle,
313 Self::MarketStats(_) => LighterWsChannelKind::MarketStats,
314 Self::SpotMarketStats(_) => LighterWsChannelKind::SpotMarketStats,
315 Self::AccountAll(_) => LighterWsChannelKind::AccountAll,
316 Self::AccountOrders { .. } => LighterWsChannelKind::AccountOrders,
317 Self::AccountAllOrders(_) => LighterWsChannelKind::AccountAllOrders,
318 Self::AccountAllTrades(_) => LighterWsChannelKind::AccountAllTrades,
319 Self::AccountAllPositions(_) => LighterWsChannelKind::AccountAllPositions,
320 Self::AccountAllAssets(_) => LighterWsChannelKind::AccountAllAssets,
321 Self::UserStats(_) => LighterWsChannelKind::UserStats,
322 Self::Height => LighterWsChannelKind::Height,
323 }
324 }
325
326 #[must_use]
327 pub fn subscription_channel(&self) -> String {
328 let kind = self.kind().as_wire_str();
329
330 match self {
331 Self::OrderBook(market_index)
332 | Self::Ticker(market_index)
333 | Self::Trade(market_index) => format!("{kind}/{market_index}"),
334 Self::Candle {
335 market_index,
336 resolution,
337 } => format!("{kind}/{market_index}/{}", resolution.as_str()),
338 Self::MarketStats(selection) | Self::SpotMarketStats(selection) => {
339 format!("{kind}/{}", selection.subscription_value())
340 }
341 Self::AccountAll(account_index)
342 | Self::AccountAllOrders(account_index)
343 | Self::AccountAllTrades(account_index)
344 | Self::AccountAllPositions(account_index)
345 | Self::AccountAllAssets(account_index)
346 | Self::UserStats(account_index) => format!("{kind}/{account_index}"),
347 Self::AccountOrders {
348 market_index,
349 account_index,
350 } => format!("{kind}/{market_index}/{account_index}"),
351 Self::Height => kind.to_string(),
352 }
353 }
354
355 #[must_use]
363 pub fn topic_key(&self) -> String {
364 self.subscription_channel().replace('/', ":")
365 }
366
367 #[must_use]
369 pub const fn requires_auth(&self) -> bool {
370 matches!(
371 self,
372 Self::AccountAll(_)
373 | Self::AccountOrders { .. }
374 | Self::AccountAllOrders(_)
375 | Self::AccountAllTrades(_)
376 | Self::AccountAllPositions(_)
377 | Self::AccountAllAssets(_)
378 | Self::UserStats(_)
379 )
380 }
381}
382
383#[derive(Debug, Copy, Clone, PartialEq, Eq)]
384pub enum LighterMarketSelection {
385 All,
386 Market(i16),
387}
388
389impl LighterMarketSelection {
390 fn subscription_value(self) -> String {
391 match self {
392 Self::All => "all".to_string(),
393 Self::Market(market_index) => market_index.to_string(),
394 }
395 }
396}
397
398#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
399#[serde(tag = "type")]
400pub enum LighterWsFrame {
401 #[serde(rename = "subscribed/order_book")]
402 OrderBookSnapshot {
403 channel: Ustr,
404 #[serde(default)]
405 last_updated_at: u64,
406 offset: i64,
407 order_book: LighterWsOrderBook,
408 timestamp: u64,
409 },
410 #[serde(rename = "update/order_book")]
411 OrderBook {
412 channel: Ustr,
413 last_updated_at: u64,
414 offset: i64,
415 order_book: LighterWsOrderBook,
416 timestamp: u64,
417 },
418 #[serde(rename = "subscribed/ticker")]
419 TickerSnapshot {
420 channel: Ustr,
421 #[serde(default)]
422 last_updated_at: u64,
423 nonce: i64,
424 ticker: LighterTicker,
425 timestamp: u64,
426 },
427 #[serde(rename = "update/ticker")]
428 Ticker {
429 channel: Ustr,
430 last_updated_at: u64,
431 nonce: i64,
432 ticker: LighterTicker,
433 timestamp: u64,
434 },
435 #[serde(rename = "update/market_stats", alias = "subscribed/market_stats")]
436 MarketStats {
437 channel: Ustr,
438 market_stats: LighterMarketStatsPayload,
439 timestamp: u64,
440 },
441 #[serde(
442 rename = "update/spot_market_stats",
443 alias = "subscribed/spot_market_stats"
444 )]
445 SpotMarketStats {
446 channel: Ustr,
447 spot_market_stats: LighterSpotMarketStatsPayload,
448 timestamp: u64,
449 },
450 #[serde(rename = "subscribed/trade")]
451 TradeSnapshot {
452 channel: Ustr,
453 #[serde(default, deserialize_with = "deserialize_trade_vec")]
454 liquidation_trades: Vec<LighterTrade>,
455 nonce: i64,
456 #[serde(default, deserialize_with = "deserialize_trade_vec")]
457 trades: Vec<LighterTrade>,
458 },
459 #[serde(rename = "update/trade")]
460 Trade {
461 channel: Ustr,
462 #[serde(default, deserialize_with = "deserialize_trade_vec")]
463 liquidation_trades: Vec<LighterTrade>,
464 nonce: i64,
465 #[serde(default, deserialize_with = "deserialize_trade_vec")]
466 trades: Vec<LighterTrade>,
467 },
468 #[serde(rename = "update/account_orders")]
469 AccountOrders {
470 account: i64,
471 channel: Ustr,
472 nonce: i64,
473 orders: AHashMap<Ustr, Vec<LighterOrder>>,
474 },
475 #[serde(
476 rename = "update/account_all_orders",
477 alias = "subscribed/account_all_orders"
478 )]
479 AccountAllOrders {
480 channel: Ustr,
481 orders: AHashMap<Ustr, Vec<LighterOrder>>,
482 },
483 #[serde(rename = "subscribed/account_all_trades")]
484 AccountAllTradesSnapshot {
485 channel: Ustr,
486 #[serde(default, deserialize_with = "deserialize_trade_vec")]
487 trades: Vec<LighterTrade>,
488 #[serde(deserialize_with = "deserialize_decimal")]
489 total_volume: Decimal,
490 #[serde(deserialize_with = "deserialize_decimal")]
491 monthly_volume: Decimal,
492 #[serde(deserialize_with = "deserialize_decimal")]
493 weekly_volume: Decimal,
494 #[serde(deserialize_with = "deserialize_decimal")]
495 daily_volume: Decimal,
496 },
497 #[serde(rename = "update/account_all_trades")]
498 AccountAllTrades {
499 channel: Ustr,
500 trades: AHashMap<Ustr, Vec<LighterTrade>>,
501 },
502 #[serde(
503 rename = "update/account_all_positions",
504 alias = "subscribed/account_all_positions"
505 )]
506 AccountAllPositions {
507 channel: Ustr,
508 positions: AHashMap<Ustr, LighterPosition>,
509 #[serde(default)]
510 shares: Vec<LighterPoolShares>,
511 last_funding_round: Option<AHashMap<Ustr, Decimal>>,
512 last_funding_discount: Option<AHashMap<Ustr, Decimal>>,
513 },
514 #[serde(
515 rename = "update/account_all_assets",
516 alias = "subscribed/account_all_assets"
517 )]
518 AccountAllAssets {
519 assets: AHashMap<Ustr, LighterAsset>,
520 channel: Ustr,
521 timestamp: u64,
522 },
523 #[serde(rename = "update/user_stats", alias = "subscribed/user_stats")]
524 UserStats {
525 channel: Ustr,
526 stats: LighterUserStats,
527 timestamp: u64,
528 },
529 #[serde(rename = "update/height")]
530 Height {
531 channel: Ustr,
532 height: i64,
533 timestamp: u64,
534 },
535 #[serde(rename = "subscribed/candle")]
536 CandleSnapshot {
537 channel: Ustr,
538 candles: Vec<LighterWsCandle>,
539 timestamp: u64,
540 },
541 #[serde(rename = "update/candle")]
542 Candle {
543 channel: Ustr,
544 candles: Vec<LighterWsCandle>,
545 timestamp: u64,
546 },
547}
548
549#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
550pub struct LighterWsCandle {
551 pub t: i64,
552 #[serde(deserialize_with = "deserialize_decimal")]
553 pub o: Decimal,
554 #[serde(deserialize_with = "deserialize_decimal")]
555 pub h: Decimal,
556 #[serde(deserialize_with = "deserialize_decimal")]
557 pub l: Decimal,
558 #[serde(deserialize_with = "deserialize_decimal")]
559 pub c: Decimal,
560 #[serde(deserialize_with = "deserialize_decimal")]
561 pub v: Decimal,
562 #[serde(default, rename = "V", deserialize_with = "deserialize_decimal")]
563 pub quote_volume: Decimal,
564 #[serde(default)]
565 pub i: i64,
566}
567
568#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
569pub struct LighterWsOrderBook {
570 pub code: i32,
571 pub asks: Vec<LighterPriceLevel>,
572 pub bids: Vec<LighterPriceLevel>,
573 pub offset: i64,
574 pub nonce: i64,
575 pub last_updated_at: u64,
576 pub begin_nonce: i64,
577}
578
579#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
580pub struct LighterTicker {
581 pub s: Ustr,
582 pub a: LighterPriceLevel,
583 pub b: LighterPriceLevel,
584 pub last_updated_at: u64,
585}
586
587#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
588#[serde(untagged)]
589pub enum LighterMarketStatsPayload {
590 All(AHashMap<Ustr, LighterMarketStats>),
591 One(Box<LighterMarketStats>),
592}
593
594#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
595pub struct LighterMarketStats {
596 pub symbol: Ustr,
597 pub market_id: i16,
598 #[serde(deserialize_with = "deserialize_decimal_from_str")]
599 pub index_price: Decimal,
600 #[serde(deserialize_with = "deserialize_decimal_from_str")]
601 pub mark_price: Decimal,
602 #[serde(deserialize_with = "deserialize_decimal_from_str")]
603 pub mid_price: Decimal,
604 #[serde(deserialize_with = "deserialize_decimal_from_str")]
605 pub open_interest: Decimal,
606 #[serde(deserialize_with = "deserialize_decimal_from_str")]
607 pub open_interest_limit: Decimal,
608 #[serde(deserialize_with = "deserialize_decimal_from_str")]
609 pub funding_clamp_small: Decimal,
610 #[serde(deserialize_with = "deserialize_decimal_from_str")]
611 pub funding_clamp_big: Decimal,
612 #[serde(deserialize_with = "deserialize_decimal_from_str")]
613 pub last_trade_price: Decimal,
614 #[serde(deserialize_with = "deserialize_decimal_from_str")]
615 pub current_funding_rate: Decimal,
616 #[serde(deserialize_with = "deserialize_decimal_from_str")]
617 pub funding_rate: Decimal,
618 pub funding_timestamp: u64,
619 #[serde(deserialize_with = "deserialize_decimal")]
620 pub daily_base_token_volume: Decimal,
621 #[serde(deserialize_with = "deserialize_decimal")]
622 pub daily_quote_token_volume: Decimal,
623 #[serde(deserialize_with = "deserialize_decimal")]
624 pub daily_price_low: Decimal,
625 #[serde(deserialize_with = "deserialize_decimal")]
626 pub daily_price_high: Decimal,
627 #[serde(deserialize_with = "deserialize_decimal")]
628 pub daily_price_change: Decimal,
629}
630
631#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
632#[serde(untagged)]
633pub enum LighterSpotMarketStatsPayload {
634 All(AHashMap<Ustr, LighterSpotMarketStats>),
635 One(Box<LighterSpotMarketStats>),
636}
637
638#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
639pub struct LighterSpotMarketStats {
640 pub symbol: Ustr,
641 pub market_id: i16,
642 #[serde(deserialize_with = "deserialize_decimal_from_str")]
643 pub index_price: Decimal,
644 #[serde(deserialize_with = "deserialize_decimal_from_str")]
645 pub mid_price: Decimal,
646 #[serde(deserialize_with = "deserialize_decimal_from_str")]
647 pub last_trade_price: Decimal,
648 #[serde(deserialize_with = "deserialize_decimal")]
649 pub daily_base_token_volume: Decimal,
650 #[serde(deserialize_with = "deserialize_decimal")]
651 pub daily_quote_token_volume: Decimal,
652 #[serde(deserialize_with = "deserialize_decimal")]
653 pub daily_price_low: Decimal,
654 #[serde(deserialize_with = "deserialize_decimal")]
655 pub daily_price_high: Decimal,
656 #[serde(deserialize_with = "deserialize_decimal")]
657 pub daily_price_change: Decimal,
658}
659
660#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
661pub struct LighterPosition {
662 pub market_id: i16,
663 pub symbol: Ustr,
664 #[serde(deserialize_with = "deserialize_decimal_from_str")]
665 pub initial_margin_fraction: Decimal,
666 pub open_order_count: i64,
667 pub pending_order_count: i64,
668 pub position_tied_order_count: i64,
669 pub sign: i8,
670 #[serde(deserialize_with = "deserialize_decimal_from_str")]
671 pub position: Decimal,
672 #[serde(deserialize_with = "deserialize_decimal_from_str")]
673 pub avg_entry_price: Decimal,
674 #[serde(deserialize_with = "deserialize_decimal_from_str")]
675 pub position_value: Decimal,
676 #[serde(deserialize_with = "deserialize_decimal_from_str")]
677 pub unrealized_pnl: Decimal,
678 #[serde(deserialize_with = "deserialize_decimal_from_str")]
679 pub realized_pnl: Decimal,
680 #[serde(deserialize_with = "deserialize_decimal_from_str")]
681 pub liquidation_price: Decimal,
682 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
683 pub total_funding_paid_out: Option<Decimal>,
684 pub margin_mode: i32,
685 #[serde(deserialize_with = "deserialize_decimal_from_str")]
686 pub allocated_margin: Decimal,
687 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
688 pub total_discount: Option<Decimal>,
689}
690
691#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
692pub struct LighterPoolShares {
693 pub public_pool_index: i64,
694 pub shares_amount: i64,
695 #[serde(deserialize_with = "deserialize_decimal_from_str")]
696 pub entry_usdc: Decimal,
697 #[serde(deserialize_with = "deserialize_decimal_from_str")]
698 pub principal_amount: Decimal,
699 pub entry_timestamp: u64,
700}
701
702#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
706pub struct LighterUserStatsScoped {
707 #[serde(deserialize_with = "deserialize_decimal_from_str")]
708 pub available_balance: Decimal,
709 #[serde(deserialize_with = "deserialize_decimal_from_str")]
710 pub buying_power: Decimal,
711 #[serde(deserialize_with = "deserialize_decimal_from_str")]
712 pub collateral: Decimal,
713 #[serde(deserialize_with = "deserialize_decimal_from_str")]
714 pub leverage: Decimal,
715 #[serde(deserialize_with = "deserialize_decimal_from_str")]
716 pub margin_usage: Decimal,
717 #[serde(deserialize_with = "deserialize_decimal_from_str")]
718 pub portfolio_value: Decimal,
719}
720
721#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
724pub struct LighterUserStats {
725 #[serde(default)]
726 pub account_trading_mode: i32,
727 #[serde(deserialize_with = "deserialize_decimal_from_str")]
728 pub available_balance: Decimal,
729 #[serde(deserialize_with = "deserialize_decimal_from_str")]
730 pub buying_power: Decimal,
731 #[serde(deserialize_with = "deserialize_decimal_from_str")]
732 pub collateral: Decimal,
733 #[serde(deserialize_with = "deserialize_decimal_from_str")]
734 pub leverage: Decimal,
735 #[serde(deserialize_with = "deserialize_decimal_from_str")]
736 pub margin_usage: Decimal,
737 #[serde(deserialize_with = "deserialize_decimal_from_str")]
738 pub portfolio_value: Decimal,
739 pub cross_stats: Option<LighterUserStatsScoped>,
740 pub total_stats: Option<LighterUserStatsScoped>,
741}
742
743#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
744pub struct LighterAsset {
745 pub symbol: Ustr,
746 pub asset_id: i16,
747 #[serde(deserialize_with = "deserialize_decimal_from_str")]
750 pub balance: Decimal,
751 #[serde(deserialize_with = "deserialize_decimal_from_str")]
753 pub locked_balance: Decimal,
754 #[serde(default, deserialize_with = "deserialize_decimal_from_str")]
757 pub margin_balance: Decimal,
758 #[serde(default)]
761 pub margin_mode: Ustr,
762}
763
764fn deserialize_trade_vec<'de, D>(deserializer: D) -> Result<Vec<LighterTrade>, D::Error>
765where
766 D: serde::Deserializer<'de>,
767{
768 struct TradeVecVisitor;
769
770 impl<'de> Visitor<'de> for TradeVecVisitor {
771 type Value = Vec<LighterTrade>;
772
773 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
774 formatter.write_str("trade array, object keyed by market, or null")
775 }
776
777 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
778 Ok(Vec::new())
779 }
780
781 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
782 Ok(Vec::new())
783 }
784
785 fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
786 let mut trades = Vec::with_capacity(seq.size_hint().unwrap_or(0));
787 while let Some(trade) = seq.next_element::<LighterTrade>()? {
788 trades.push(trade);
789 }
790 Ok(trades)
791 }
792
793 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
794 let mut trades = Vec::new();
795 while let Some((_, mut market_trades)) =
796 map.next_entry::<IgnoredAny, Vec<LighterTrade>>()?
797 {
798 trades.append(&mut market_trades);
799 }
800 Ok(trades)
801 }
802 }
803
804 deserializer.deserialize_any(TradeVecVisitor)
805}
806
807#[cfg(test)]
808mod tests {
809 use std::str::FromStr;
810
811 use rstest::rstest;
812 use serde_json::Value;
813
814 use super::*;
815
816 const WS_ORDER_BOOK_UPDATE: &str = include_str!("../../test_data/ws_order_book_update.json");
817 const WS_ORDER_BOOK_SUBSCRIBED: &str =
818 include_str!("../../test_data/ws_order_book_subscribed.json");
819 const WS_ORDER_BOOK_SUBSCRIBED_EMPTY: &str =
820 include_str!("../../test_data/ws_order_book_subscribed_empty.json");
821 const WS_TRADE_UPDATE: &str = include_str!("../../test_data/ws_trade_update.json");
822 const WS_TRADE_SUBSCRIBED: &str = include_str!("../../test_data/ws_trade_subscribed.json");
823 const WS_TICKER_UPDATE: &str = include_str!("../../test_data/ws_ticker_update.json");
824 const WS_TICKER_SUBSCRIBED: &str = include_str!("../../test_data/ws_ticker_subscribed.json");
825 const WS_TICKER_SUBSCRIBED_EMPTY: &str =
826 include_str!("../../test_data/ws_ticker_subscribed_empty.json");
827 const WS_MARKET_STATS_UPDATE_SINGLE: &str =
828 include_str!("../../test_data/ws_market_stats_update_single.json");
829 const WS_MARKET_STATS_SUBSCRIBED_SINGLE: &str =
830 include_str!("../../test_data/ws_market_stats_subscribed_single.json");
831 const WS_MARKET_STATS_UPDATE_ALL: &str =
832 include_str!("../../test_data/ws_market_stats_update_all.json");
833 const WS_SPOT_MARKET_STATS_UPDATE_SINGLE: &str =
834 include_str!("../../test_data/ws_spot_market_stats_update_single.json");
835 const WS_SPOT_MARKET_STATS_SUBSCRIBED_SINGLE: &str =
836 include_str!("../../test_data/ws_spot_market_stats_subscribed_single.json");
837 const WS_SPOT_MARKET_STATS_UPDATE_ALL: &str =
838 include_str!("../../test_data/ws_spot_market_stats_update_all.json");
839 const WS_ACCOUNT_ALL_ASSETS_UPDATE: &str =
840 include_str!("../../test_data/ws_account_all_assets_update.json");
841 const WS_ACCOUNT_ORDERS_UPDATE: &str =
842 include_str!("../../test_data/ws_account_orders_update.json");
843 const WS_ACCOUNT_ALL_TRADES_UPDATE: &str =
844 include_str!("../../test_data/ws_account_all_trades_update.json");
845 const WS_ACCOUNT_ALL_POSITIONS_UPDATE: &str =
846 include_str!("../../test_data/ws_account_all_positions_update.json");
847 const WS_HEIGHT_UPDATE: &str = include_str!("../../test_data/ws_height_update.json");
848 const WS_CANDLE_SUBSCRIBED: &str = include_str!("../../test_data/ws_candle_subscribed.json");
849 const WS_CANDLE_UPDATE: &str = include_str!("../../test_data/ws_candle_update.json");
850
851 #[rstest]
852 fn test_subscription_request_serializes_public_channel() {
853 let channel = LighterWsChannel::OrderBook(0).subscription_channel();
854 let request = LighterWsRequest::subscribe(channel);
855
856 let json = serde_json::to_string(&request).unwrap();
857
858 assert_eq!(
859 serde_json::from_str::<Value>(&json).unwrap(),
860 serde_json::json!({
861 "type": "subscribe",
862 "channel": "order_book/0",
863 }),
864 );
865 }
866
867 #[rstest]
868 fn test_subscription_request_serializes_auth_channel() {
869 let channel = LighterWsChannel::AccountOrders {
870 market_index: 0,
871 account_index: 1234,
872 }
873 .subscription_channel();
874 let request = LighterWsRequest::subscribe_auth(channel, "token");
875
876 let json = serde_json::to_string(&request).unwrap();
877
878 assert_eq!(
879 serde_json::from_str::<Value>(&json).unwrap(),
880 serde_json::json!({
881 "type": "subscribe",
882 "channel": "account_orders/0/1234",
883 "auth": "token",
884 }),
885 );
886 }
887
888 #[rstest]
889 fn test_subscribe_request_debug_redacts_auth_token() {
890 let token = "schnorr-signature-bytes-do-not-leak";
891 let request = LighterWsRequest::subscribe_auth("account_all/123", token);
892
893 let dbg = format!("{request:?}");
894
895 assert!(
896 !dbg.contains(token),
897 "Debug output must not contain the auth token, found: {dbg}",
898 );
899 assert!(dbg.contains("authed"), "Debug should include authed flag");
900 }
901
902 #[rstest]
903 #[case(LighterWsChannelKind::OrderBook)]
904 #[case(LighterWsChannelKind::Ticker)]
905 #[case(LighterWsChannelKind::Trade)]
906 #[case(LighterWsChannelKind::Candle)]
907 #[case(LighterWsChannelKind::MarketStats)]
908 #[case(LighterWsChannelKind::SpotMarketStats)]
909 #[case(LighterWsChannelKind::AccountAll)]
910 #[case(LighterWsChannelKind::AccountOrders)]
911 #[case(LighterWsChannelKind::AccountAllOrders)]
912 #[case(LighterWsChannelKind::AccountAllTrades)]
913 #[case(LighterWsChannelKind::AccountAllPositions)]
914 #[case(LighterWsChannelKind::AccountAllAssets)]
915 #[case(LighterWsChannelKind::Height)]
916 fn test_channel_kind_wire_round_trip(#[case] kind: LighterWsChannelKind) {
917 assert_eq!(
918 LighterWsChannelKind::from_wire_str(kind.as_wire_str()),
919 Some(kind),
920 );
921 }
922
923 #[rstest]
924 #[case("unknown_channel")]
925 #[case("ORDER_BOOK")]
926 #[case("")]
927 #[case("order_book:0")]
928 fn test_channel_kind_unknown_returns_none(#[case] input: &str) {
929 assert_eq!(LighterWsChannelKind::from_wire_str(input), None);
930 }
931
932 #[rstest]
933 fn test_order_book_frame_deserializes() {
934 let frame: LighterWsFrame = serde_json::from_str(WS_ORDER_BOOK_UPDATE).unwrap();
935
936 match frame {
937 LighterWsFrame::OrderBook {
938 channel,
939 order_book,
940 timestamp,
941 ..
942 } => {
943 assert_eq!(channel, Ustr::from("order_book:0"));
944 assert_eq!(order_book.asks.len(), 1);
945 assert_eq!(
946 order_book.asks[0].price,
947 Decimal::from_str("2064.54").unwrap()
948 );
949 assert_eq!(timestamp, 1_774_884_082_326);
950 }
951 _ => panic!("expected order book frame"),
952 }
953 }
954
955 #[rstest]
956 fn test_trade_frame_deserializes() {
957 let frame: LighterWsFrame = serde_json::from_str(WS_TRADE_UPDATE).unwrap();
958
959 match frame {
960 LighterWsFrame::Trade { trades, nonce, .. } => {
961 assert_eq!(nonce, 8_630_448_841);
962 assert_eq!(trades.len(), 1);
963 assert_eq!(trades[0].trade_id_str.as_deref(), Some("16164557907"));
964 }
965 _ => panic!("expected trade frame"),
966 }
967 }
968
969 #[rstest]
970 fn test_trade_frame_deserializes_null_liquidations() {
971 let payload = serde_json::json!({
972 "type": "update/trade",
973 "channel": "trade:1",
974 "liquidation_trades": null,
975 "nonce": 1,
976 "trades": []
977 });
978
979 let frame: LighterWsFrame = serde_json::from_value(payload).unwrap();
980
981 match frame {
982 LighterWsFrame::Trade {
983 liquidation_trades,
984 trades,
985 ..
986 } => {
987 assert!(liquidation_trades.is_empty());
988 assert!(trades.is_empty());
989 }
990 _ => panic!("expected trade frame"),
991 }
992 }
993
994 #[rstest]
995 fn test_trade_frame_deserializes_object_trades() {
996 let mut payload: Value = serde_json::from_str(WS_TRADE_UPDATE).unwrap();
997 let trades = payload.get_mut("trades").unwrap().take();
998 payload["trades"] = serde_json::json!({ "0": trades });
999
1000 let frame: LighterWsFrame = serde_json::from_value(payload).unwrap();
1001
1002 match frame {
1003 LighterWsFrame::Trade { trades, .. } => {
1004 assert_eq!(trades.len(), 1);
1005 assert_eq!(trades[0].trade_id_str.as_deref(), Some("16164557907"));
1006 }
1007 _ => panic!("expected trade frame"),
1008 }
1009 }
1010
1011 #[rstest]
1012 fn test_ticker_frame_deserializes() {
1013 let frame: LighterWsFrame = serde_json::from_str(WS_TICKER_UPDATE).unwrap();
1014
1015 match frame {
1016 LighterWsFrame::Ticker {
1017 channel,
1018 nonce,
1019 ticker,
1020 timestamp,
1021 ..
1022 } => {
1023 assert_eq!(channel, Ustr::from("ticker:0"));
1024 assert_eq!(nonce, 9_182_390_020);
1025 assert_eq!(ticker.s, Ustr::from("ETH"));
1026 assert_eq!(ticker.a.price, Decimal::from_str("2064.48").unwrap());
1027 assert_eq!(ticker.b.size, Decimal::from_str("1.0392").unwrap());
1028 assert_eq!(timestamp, 1_774_883_844_933);
1029 }
1030 _ => panic!("expected ticker frame"),
1031 }
1032 }
1033
1034 #[rstest]
1039 fn test_order_book_snapshot_frame_deserializes() {
1040 let frame: LighterWsFrame = serde_json::from_str(WS_ORDER_BOOK_SUBSCRIBED).unwrap();
1041
1042 match frame {
1043 LighterWsFrame::OrderBookSnapshot {
1044 channel,
1045 order_book,
1046 timestamp,
1047 ..
1048 } => {
1049 assert_eq!(channel, Ustr::from("order_book:0"));
1050 assert_eq!(order_book.bids.len(), 1);
1051 assert_eq!(
1052 order_book.bids[0].price,
1053 Decimal::from_str("2000.00").unwrap()
1054 );
1055 assert_eq!(order_book.asks.len(), 2);
1056 assert_eq!(
1057 order_book.asks[0].price,
1058 Decimal::from_str("2325.00").unwrap()
1059 );
1060 assert_eq!(order_book.nonce, 904_845);
1061 assert_eq!(timestamp, 1_778_138_582_602);
1062 }
1063 _ => panic!("expected order book snapshot frame, was {frame:?}"),
1064 }
1065 }
1066
1067 #[rstest]
1068 fn test_empty_order_book_snapshot_frame_deserializes() {
1069 let frame: LighterWsFrame = serde_json::from_str(WS_ORDER_BOOK_SUBSCRIBED_EMPTY).unwrap();
1070
1071 match frame {
1072 LighterWsFrame::OrderBookSnapshot {
1073 channel,
1074 last_updated_at,
1075 order_book,
1076 timestamp,
1077 ..
1078 } => {
1079 assert_eq!(channel, Ustr::from("order_book:39"));
1080 assert_eq!(last_updated_at, 0);
1081 assert!(order_book.asks.is_empty());
1082 assert!(order_book.bids.is_empty());
1083 assert_eq!(order_book.offset, 1);
1084 assert_eq!(order_book.nonce, 0);
1085 assert_eq!(timestamp, 1_778_138_582_602);
1086 }
1087 _ => panic!("expected empty order book snapshot frame, was {frame:?}"),
1088 }
1089 }
1090
1091 #[rstest]
1092 fn test_ticker_snapshot_frame_deserializes() {
1093 let frame: LighterWsFrame = serde_json::from_str(WS_TICKER_SUBSCRIBED).unwrap();
1094
1095 match frame {
1096 LighterWsFrame::TickerSnapshot {
1097 channel,
1098 nonce,
1099 ticker,
1100 timestamp,
1101 ..
1102 } => {
1103 assert_eq!(channel, Ustr::from("ticker:0"));
1104 assert_eq!(nonce, 904_895);
1105 assert_eq!(ticker.s, Ustr::from("ETH"));
1106 assert_eq!(ticker.a.price, Decimal::from_str("2325.00").unwrap());
1107 assert_eq!(ticker.b.price, Decimal::from_str("2000.00").unwrap());
1108 assert_eq!(timestamp, 1_778_138_582_640);
1109 }
1110 _ => panic!("expected ticker snapshot frame, was {frame:?}"),
1111 }
1112 }
1113
1114 #[rstest]
1115 fn test_empty_ticker_snapshot_frame_deserializes() {
1116 let frame: LighterWsFrame = serde_json::from_str(WS_TICKER_SUBSCRIBED_EMPTY).unwrap();
1117
1118 match frame {
1119 LighterWsFrame::TickerSnapshot {
1120 channel,
1121 last_updated_at,
1122 nonce,
1123 ticker,
1124 timestamp,
1125 ..
1126 } => {
1127 assert_eq!(channel, Ustr::from("ticker:39"));
1128 assert_eq!(last_updated_at, 0);
1129 assert_eq!(nonce, 2_475_051);
1130 assert_eq!(ticker.s, Ustr::from("ADA"));
1131 assert_eq!(ticker.a.price, Decimal::ZERO);
1132 assert_eq!(ticker.a.size, Decimal::ZERO);
1133 assert_eq!(ticker.b.price, Decimal::ZERO);
1134 assert_eq!(ticker.b.size, Decimal::ZERO);
1135 assert_eq!(timestamp, 1_778_138_582_640);
1136 }
1137 _ => panic!("expected empty ticker snapshot frame, was {frame:?}"),
1138 }
1139 }
1140
1141 #[rstest]
1142 fn test_trade_snapshot_frame_deserializes() {
1143 let frame: LighterWsFrame = serde_json::from_str(WS_TRADE_SUBSCRIBED).unwrap();
1144
1145 match frame {
1146 LighterWsFrame::TradeSnapshot {
1147 channel,
1148 nonce,
1149 trades,
1150 ..
1151 } => {
1152 assert_eq!(channel, Ustr::from("trade:0"));
1153 assert_eq!(nonce, 8_630_448_841);
1154 assert_eq!(trades.len(), 1);
1155 assert_eq!(trades[0].trade_id_str.as_deref(), Some("16164557907"));
1156 }
1157 _ => panic!("expected trade snapshot frame, was {frame:?}"),
1158 }
1159 }
1160
1161 #[rstest]
1162 fn test_market_stats_frame_deserializes_single_payload() {
1163 let frame: LighterWsFrame = serde_json::from_str(WS_MARKET_STATS_UPDATE_SINGLE).unwrap();
1164
1165 match frame {
1166 LighterWsFrame::MarketStats {
1167 channel,
1168 market_stats: LighterMarketStatsPayload::One(stats),
1169 timestamp,
1170 } => {
1171 assert_eq!(channel, Ustr::from("market_stats:0"));
1172 assert_eq!(stats.symbol, Ustr::from("ETH"));
1173 assert_eq!(stats.market_id, 0);
1174 assert_eq!(stats.mark_price, Decimal::from_str("2064.47").unwrap());
1175 assert_eq!(
1176 stats.daily_base_token_volume,
1177 Decimal::new(1_999_586_931, 4),
1178 );
1179 assert_eq!(timestamp, 1_774_883_844_933);
1180 }
1181 _ => panic!("expected single market stats frame"),
1182 }
1183 }
1184
1185 #[rstest]
1186 fn test_market_stats_subscribed_frame_deserializes_single_payload() {
1187 let frame: LighterWsFrame =
1188 serde_json::from_str(WS_MARKET_STATS_SUBSCRIBED_SINGLE).unwrap();
1189
1190 match frame {
1191 LighterWsFrame::MarketStats {
1192 channel,
1193 market_stats: LighterMarketStatsPayload::One(stats),
1194 timestamp,
1195 } => {
1196 assert_eq!(channel, Ustr::from("market_stats:1"));
1197 assert_eq!(stats.symbol, Ustr::from("BTC"));
1198 assert_eq!(stats.market_id, 1);
1199 assert_eq!(stats.mark_price, Decimal::from_str("64356.3").unwrap());
1200 assert_eq!(timestamp, 1_780_546_209_291);
1201 }
1202 _ => panic!("expected subscribed market stats frame"),
1203 }
1204 }
1205
1206 #[rstest]
1207 fn test_market_stats_frame_deserializes_all_payload() {
1208 let frame: LighterWsFrame = serde_json::from_str(WS_MARKET_STATS_UPDATE_ALL).unwrap();
1209
1210 match frame {
1211 LighterWsFrame::MarketStats {
1212 market_stats: LighterMarketStatsPayload::All(stats),
1213 ..
1214 } => {
1215 assert_eq!(stats.len(), 1);
1216 let stats = stats.get(&Ustr::from("0")).unwrap();
1217 assert_eq!(stats.symbol, Ustr::from("ETH"));
1218 assert_eq!(
1219 stats.open_interest,
1220 Decimal::from_str("27250.8411").unwrap()
1221 );
1222 }
1223 _ => panic!("expected all market stats frame"),
1224 }
1225 }
1226
1227 #[rstest]
1228 fn test_spot_market_stats_frame_deserializes_single_payload() {
1229 let frame: LighterWsFrame =
1230 serde_json::from_str(WS_SPOT_MARKET_STATS_UPDATE_SINGLE).unwrap();
1231
1232 match frame {
1233 LighterWsFrame::SpotMarketStats {
1234 channel,
1235 spot_market_stats: LighterSpotMarketStatsPayload::One(stats),
1236 timestamp,
1237 } => {
1238 assert_eq!(channel, Ustr::from("spot_market_stats:2048"));
1239 assert_eq!(stats.symbol, Ustr::from("USDC"));
1240 assert_eq!(stats.market_id, 2048);
1241 assert_eq!(stats.mid_price, Decimal::from_str("1.000001").unwrap());
1242 assert_eq!(stats.daily_base_token_volume, Decimal::from(1000));
1243 assert_eq!(timestamp, 1_774_883_844_933);
1244 }
1245 _ => panic!("expected single spot market stats frame"),
1246 }
1247 }
1248
1249 #[rstest]
1250 fn test_spot_market_stats_subscribed_frame_deserializes_single_payload() {
1251 let frame: LighterWsFrame =
1252 serde_json::from_str(WS_SPOT_MARKET_STATS_SUBSCRIBED_SINGLE).unwrap();
1253
1254 match frame {
1255 LighterWsFrame::SpotMarketStats {
1256 channel,
1257 spot_market_stats: LighterSpotMarketStatsPayload::One(stats),
1258 timestamp,
1259 } => {
1260 assert_eq!(channel, Ustr::from("spot_market_stats:2048"));
1261 assert_eq!(stats.symbol, Ustr::from("USDC"));
1262 assert_eq!(stats.market_id, 2048);
1263 assert_eq!(stats.mid_price, Decimal::from_str("1.000001").unwrap());
1264 assert_eq!(timestamp, 1_774_883_844_933);
1265 }
1266 _ => panic!("expected subscribed spot market stats frame"),
1267 }
1268 }
1269
1270 #[rstest]
1271 fn test_spot_market_stats_frame_deserializes_all_payload() {
1272 let frame: LighterWsFrame = serde_json::from_str(WS_SPOT_MARKET_STATS_UPDATE_ALL).unwrap();
1273
1274 match frame {
1275 LighterWsFrame::SpotMarketStats {
1276 spot_market_stats: LighterSpotMarketStatsPayload::All(stats),
1277 ..
1278 } => {
1279 assert_eq!(stats.len(), 1);
1280 let stats = stats.get(&Ustr::from("2048")).unwrap();
1281 assert_eq!(stats.symbol, Ustr::from("USDC"));
1282 assert_eq!(
1283 stats.last_trade_price,
1284 Decimal::from_str("1.000002").unwrap()
1285 );
1286 }
1287 _ => panic!("expected all spot market stats frame"),
1288 }
1289 }
1290
1291 #[rstest]
1292 fn test_account_all_assets_frame_deserializes() {
1293 let frame: LighterWsFrame = serde_json::from_str(WS_ACCOUNT_ALL_ASSETS_UPDATE).unwrap();
1297
1298 match frame {
1299 LighterWsFrame::AccountAllAssets {
1300 assets,
1301 channel,
1302 timestamp,
1303 } => {
1304 assert_eq!(channel, Ustr::from("account_all_assets:1234"));
1305 let asset = assets.get(&Ustr::from("3")).unwrap();
1306 assert_eq!(asset.symbol, Ustr::from("USDC"));
1307 assert_eq!(asset.asset_id, 3);
1308 assert_eq!(asset.balance, Decimal::from_str("10.000000").unwrap());
1309 assert_eq!(asset.locked_balance, Decimal::ZERO);
1310 assert_eq!(
1311 asset.margin_balance,
1312 Decimal::from_str("40.000000").unwrap()
1313 );
1314 assert_eq!(asset.margin_mode, Ustr::from("disabled"));
1315 assert_eq!(timestamp, 1_781_161_199_648);
1316 }
1317 _ => panic!("expected account all assets frame"),
1318 }
1319 }
1320
1321 #[rstest]
1322 fn test_account_all_assets_subscribed_frame_deserializes() {
1323 let payload = serde_json::json!({
1324 "type": "subscribed/account_all_assets",
1325 "channel": "account_all_assets:1234",
1326 "timestamp": 1778751230509u64,
1327 "assets": {
1328 "3": {
1329 "asset_id": 3,
1330 "balance": "9.660200",
1331 "locked_balance": "0.000000",
1332 "margin_balance": "9.955800",
1333 "margin_mode": "disabled",
1334 "symbol": "USDC"
1335 }
1336 }
1337 });
1338
1339 let frame: LighterWsFrame = serde_json::from_value(payload).unwrap();
1340
1341 match frame {
1342 LighterWsFrame::AccountAllAssets { assets, .. } => {
1343 assert_eq!(
1344 assets.get(&Ustr::from("3")).unwrap().symbol,
1345 Ustr::from("USDC")
1346 );
1347 }
1348 _ => panic!("expected account all assets frame"),
1349 }
1350 }
1351
1352 #[rstest]
1353 fn test_account_orders_frame_deserializes() {
1354 let frame: LighterWsFrame = serde_json::from_str(WS_ACCOUNT_ORDERS_UPDATE).unwrap();
1355
1356 match frame {
1357 LighterWsFrame::AccountOrders {
1358 account,
1359 channel,
1360 orders,
1361 ..
1362 } => {
1363 assert_eq!(account, 1234);
1364 assert_eq!(channel, Ustr::from("account_orders:0:1234"));
1365 let market_orders = orders.get(&Ustr::from("0")).unwrap();
1366 assert_eq!(market_orders.len(), 1);
1367 assert_eq!(market_orders[0].order_id, "281476929510110");
1368 assert_eq!(
1369 market_orders[0].filled_base_amount,
1370 Decimal::from_str("0.0020").unwrap(),
1371 );
1372 }
1373 _ => panic!("expected account orders frame, was {frame:?}"),
1374 }
1375 }
1376
1377 #[rstest]
1378 fn test_account_all_orders_subscribed_frame_deserializes_empty_side() {
1379 let frame: LighterWsFrame = serde_json::from_str(
1380 r#"{
1381 "type": "subscribed/account_all_orders",
1382 "channel": "account_all_orders:1234",
1383 "orders": {
1384 "3": [{
1385 "order_index": 1,
1386 "client_order_index": 2,
1387 "order_id": "1",
1388 "client_order_id": "2",
1389 "market_index": 3,
1390 "owner_account_index": 1234,
1391 "initial_base_amount": "100",
1392 "price": "0.100000",
1393 "nonce": 1,
1394 "remaining_base_amount": "100",
1395 "is_ask": false,
1396 "base_size": 100,
1397 "base_price": 100000,
1398 "filled_base_amount": "0",
1399 "filled_quote_amount": "0.000000",
1400 "side": "",
1401 "type": "limit",
1402 "time_in_force": "good-till-time",
1403 "reduce_only": false,
1404 "trigger_price": "0.000000",
1405 "order_expiry": 1781170441337,
1406 "status": "open",
1407 "trigger_status": "na",
1408 "trigger_time": 0,
1409 "parent_order_index": 0,
1410 "parent_order_id": "0",
1411 "to_trigger_order_id_0": "0",
1412 "to_trigger_order_id_1": "0",
1413 "to_cancel_order_id_0": "0",
1414 "integrator_fee_collector_index": "",
1415 "integrator_taker_fee": "",
1416 "integrator_maker_fee": "",
1417 "block_height": 1,
1418 "timestamp": 1778751241,
1419 "created_at": 1778751241,
1420 "updated_at": 1778751241,
1421 "transaction_time": 1778751241772524
1422 }]
1423 }
1424 }"#,
1425 )
1426 .unwrap();
1427
1428 match frame {
1429 LighterWsFrame::AccountAllOrders { orders, .. } => {
1430 let order = &orders.get(&Ustr::from("3")).unwrap()[0];
1431 assert_eq!(order.side, None);
1432 assert!(!order.is_ask);
1433 }
1434 _ => panic!("expected account all orders frame"),
1435 }
1436 }
1437
1438 #[rstest]
1439 fn test_account_all_trades_frame_deserializes() {
1440 let frame: LighterWsFrame = serde_json::from_str(WS_ACCOUNT_ALL_TRADES_UPDATE).unwrap();
1441
1442 match frame {
1443 LighterWsFrame::AccountAllTrades { channel, trades } => {
1444 assert_eq!(channel, Ustr::from("account_all_trades:1234"));
1445 let market_trades = trades.get(&Ustr::from("0")).unwrap();
1446 assert_eq!(market_trades.len(), 1);
1447 assert_eq!(market_trades[0].bid_account_id, 1234);
1448 assert_eq!(market_trades[0].taker_fee, Some(196));
1449 }
1450 _ => panic!("expected account all trades frame, was {frame:?}"),
1451 }
1452 }
1453
1454 #[rstest]
1455 fn test_account_all_positions_frame_deserializes() {
1456 let frame: LighterWsFrame = serde_json::from_str(WS_ACCOUNT_ALL_POSITIONS_UPDATE).unwrap();
1457
1458 match frame {
1459 LighterWsFrame::AccountAllPositions {
1460 channel, positions, ..
1461 } => {
1462 assert_eq!(channel, Ustr::from("account_all_positions:1234"));
1463 let position = positions.get(&Ustr::from("0")).unwrap();
1464 assert_eq!(position.market_id, 0);
1465 assert_eq!(position.position, Decimal::from_str("1.5000").unwrap());
1466 assert_eq!(position.sign, 1);
1467 }
1468 _ => panic!("expected account all positions frame, was {frame:?}"),
1469 }
1470 }
1471
1472 #[rstest]
1473 fn test_height_frame_deserializes() {
1474 let frame: LighterWsFrame = serde_json::from_str(WS_HEIGHT_UPDATE).unwrap();
1475
1476 match frame {
1477 LighterWsFrame::Height {
1478 channel,
1479 height,
1480 timestamp,
1481 } => {
1482 assert_eq!(channel, Ustr::from("height"));
1483 assert_eq!(height, 227_535_532);
1484 assert_eq!(timestamp, 1_774_883_844_933);
1485 }
1486 _ => panic!("expected height frame"),
1487 }
1488 }
1489
1490 #[rstest]
1491 fn test_candle_channel_subscription_channel_uses_slash() {
1492 let channel = LighterWsChannel::Candle {
1493 market_index: 0,
1494 resolution: LighterCandleResolution::OneMinute,
1495 };
1496
1497 assert_eq!(channel.subscription_channel(), "candle/0/1m");
1498 }
1499
1500 #[rstest]
1501 fn test_candle_channel_topic_key_uses_colon() {
1502 let channel = LighterWsChannel::Candle {
1503 market_index: 7,
1504 resolution: LighterCandleResolution::FiveMinute,
1505 };
1506
1507 assert_eq!(channel.topic_key(), "candle:7:5m");
1508 }
1509
1510 #[rstest]
1511 fn test_candle_channel_does_not_require_auth() {
1512 let channel = LighterWsChannel::Candle {
1513 market_index: 0,
1514 resolution: LighterCandleResolution::OneMinute,
1515 };
1516
1517 assert!(!channel.requires_auth());
1518 }
1519
1520 #[rstest]
1521 fn test_candle_snapshot_frame_deserializes() {
1522 let frame: LighterWsFrame = serde_json::from_str(WS_CANDLE_SUBSCRIBED).unwrap();
1523
1524 match frame {
1525 LighterWsFrame::CandleSnapshot {
1526 channel,
1527 candles,
1528 timestamp,
1529 } => {
1530 assert_eq!(channel, Ustr::from("candle:0:1m"));
1531 assert_eq!(timestamp, 1_778_821_471_842);
1532 assert_eq!(candles.len(), 1);
1533 let candle = &candles[0];
1534 assert_eq!(candle.t, 1_778_821_440_000);
1535 assert_eq!(candle.o, Decimal::from_str("2264.2").unwrap());
1536 assert_eq!(candle.h, Decimal::from_str("2264.34").unwrap());
1537 assert_eq!(candle.l, Decimal::from_str("2263.36").unwrap());
1538 assert_eq!(candle.c, Decimal::from_str("2263.97").unwrap());
1539 assert_eq!(candle.v, Decimal::from_str("13.2237").unwrap());
1543 assert_eq!(
1544 candle.quote_volume,
1545 Decimal::from_str("29934.60001199998").unwrap(),
1546 );
1547 assert_eq!(candle.i, 19_993_571_166);
1548 }
1549 _ => panic!("expected candle snapshot frame"),
1550 }
1551 }
1552
1553 #[rstest]
1554 fn test_candle_update_frame_deserializes() {
1555 let frame: LighterWsFrame = serde_json::from_str(WS_CANDLE_UPDATE).unwrap();
1556
1557 match frame {
1558 LighterWsFrame::Candle {
1559 channel,
1560 candles,
1561 timestamp,
1562 } => {
1563 assert_eq!(channel, Ustr::from("candle:0:1m"));
1564 assert_eq!(timestamp, 1_778_821_473_331);
1565 assert_eq!(candles.len(), 1);
1566 assert_eq!(candles[0].t, 1_778_821_440_000);
1567 assert_eq!(candles[0].c, Decimal::from_str("2263.89").unwrap());
1568 }
1569 _ => panic!("expected candle update frame"),
1570 }
1571 }
1572}