Skip to main content

lightcone_sdk/websocket/
types.rs

1//! Message types for the Lightcone WebSocket protocol.
2//!
3//! This module contains all request and response types for the WebSocket API.
4
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8// ============================================================================
9// REQUEST TYPES (Client → Server)
10// ============================================================================
11
12/// Subscribe/Unsubscribe request wrapper
13#[derive(Debug, Clone, Serialize)]
14pub struct WsRequest {
15    pub method: String,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub params: Option<SubscribeParams>,
18}
19
20impl WsRequest {
21    /// Create a subscribe request
22    pub fn subscribe(params: SubscribeParams) -> Self {
23        Self {
24            method: "subscribe".to_string(),
25            params: Some(params),
26        }
27    }
28
29    /// Create an unsubscribe request
30    pub fn unsubscribe(params: SubscribeParams) -> Self {
31        Self {
32            method: "unsubscribe".to_string(),
33            params: Some(params),
34        }
35    }
36
37    /// Create a ping request
38    pub fn ping() -> Self {
39        Self {
40            method: "ping".to_string(),
41            params: None,
42        }
43    }
44}
45
46/// Subscription parameters (polymorphic)
47#[derive(Debug, Clone, Serialize)]
48#[serde(untagged)]
49pub enum SubscribeParams {
50    /// Subscribe to orderbook updates
51    BookUpdate {
52        #[serde(rename = "type")]
53        type_: &'static str,
54        orderbook_ids: Vec<String>,
55    },
56    /// Subscribe to trade executions
57    Trades {
58        #[serde(rename = "type")]
59        type_: &'static str,
60        orderbook_ids: Vec<String>,
61    },
62    /// Subscribe to user events
63    User {
64        #[serde(rename = "type")]
65        type_: &'static str,
66        user: String,
67    },
68    /// Subscribe to price history
69    PriceHistory {
70        #[serde(rename = "type")]
71        type_: &'static str,
72        orderbook_id: String,
73        resolution: String,
74        include_ohlcv: bool,
75    },
76    /// Subscribe to market events
77    Market {
78        #[serde(rename = "type")]
79        type_: &'static str,
80        market_pubkey: String,
81    },
82}
83
84impl SubscribeParams {
85    /// Create book update subscription params
86    pub fn book_update(orderbook_ids: Vec<String>) -> Self {
87        Self::BookUpdate {
88            type_: "book_update",
89            orderbook_ids,
90        }
91    }
92
93    /// Create trades subscription params
94    pub fn trades(orderbook_ids: Vec<String>) -> Self {
95        Self::Trades {
96            type_: "trades",
97            orderbook_ids,
98        }
99    }
100
101    /// Create user subscription params
102    pub fn user(user: String) -> Self {
103        Self::User {
104            type_: "user",
105            user,
106        }
107    }
108
109    /// Create price history subscription params
110    pub fn price_history(orderbook_id: String, resolution: String, include_ohlcv: bool) -> Self {
111        Self::PriceHistory {
112            type_: "price_history",
113            orderbook_id,
114            resolution,
115            include_ohlcv,
116        }
117    }
118
119    /// Create market subscription params
120    pub fn market(market_pubkey: String) -> Self {
121        Self::Market {
122            type_: "market",
123            market_pubkey,
124        }
125    }
126
127    /// Get the subscription type as a string
128    pub fn subscription_type(&self) -> &'static str {
129        match self {
130            Self::BookUpdate { .. } => "book_update",
131            Self::Trades { .. } => "trades",
132            Self::User { .. } => "user",
133            Self::PriceHistory { .. } => "price_history",
134            Self::Market { .. } => "market",
135        }
136    }
137}
138
139// ============================================================================
140// RESPONSE TYPES (Server → Client)
141// ============================================================================
142
143/// Raw message wrapper for initial parsing
144#[derive(Debug, Clone, Deserialize)]
145pub struct RawWsMessage {
146    #[serde(rename = "type")]
147    pub type_: String,
148    pub version: f32,
149    pub data: serde_json::Value,
150}
151
152/// Generic WebSocket message wrapper
153#[derive(Debug, Clone, Deserialize)]
154pub struct WsMessage<T> {
155    #[serde(rename = "type")]
156    pub type_: String,
157    pub version: f32,
158    pub data: T,
159}
160
161// ============================================================================
162// BOOK UPDATE TYPES
163// ============================================================================
164
165/// Orderbook snapshot/delta data
166#[derive(Debug, Clone, Deserialize)]
167pub struct BookUpdateData {
168    pub orderbook_id: String,
169    #[serde(default)]
170    pub timestamp: String,
171    #[serde(default)]
172    pub sequence: u64,
173    #[serde(default)]
174    pub bids: Vec<PriceLevel>,
175    #[serde(default)]
176    pub asks: Vec<PriceLevel>,
177    #[serde(default)]
178    pub is_snapshot: bool,
179    #[serde(default)]
180    pub resync: bool,
181    #[serde(default)]
182    pub message: Option<String>,
183}
184
185/// Price level in the orderbook
186#[derive(Debug, Clone, Deserialize, Serialize)]
187pub struct PriceLevel {
188    pub side: String,
189    /// Price as decimal string (e.g., "0.500000")
190    pub price: String,
191    /// Size as decimal string
192    pub size: String,
193}
194
195// ============================================================================
196// TRADE TYPES
197// ============================================================================
198
199/// Trade execution data
200#[derive(Debug, Clone, Deserialize)]
201pub struct TradeData {
202    pub orderbook_id: String,
203    /// Price as decimal string
204    pub price: String,
205    /// Size as decimal string
206    pub size: String,
207    pub side: String,
208    pub timestamp: String,
209    pub trade_id: String,
210    pub sequence: u64,
211}
212
213// ============================================================================
214// USER EVENT TYPES
215// ============================================================================
216
217/// User event data (snapshot, order_update, balance_update)
218#[derive(Debug, Clone, Deserialize)]
219pub struct UserEventData {
220    pub event_type: String,
221    #[serde(default)]
222    pub orders: Vec<Order>,
223    #[serde(default)]
224    pub balances: HashMap<String, BalanceEntry>,
225    #[serde(default)]
226    pub order: Option<OrderUpdate>,
227    #[serde(default)]
228    pub balance: Option<Balance>,
229    #[serde(default)]
230    pub market_pubkey: Option<String>,
231    #[serde(default)]
232    pub orderbook_id: Option<String>,
233    #[serde(default)]
234    pub deposit_mint: Option<String>,
235    #[serde(default)]
236    pub timestamp: Option<String>,
237}
238
239/// User order from snapshot
240#[derive(Debug, Clone, Deserialize, Serialize)]
241pub struct Order {
242    pub order_hash: String,
243    pub market_pubkey: String,
244    pub orderbook_id: String,
245    /// 0 = BUY, 1 = SELL
246    pub side: i32,
247    /// Maker amount as decimal string
248    pub maker_amount: String,
249    /// Taker amount as decimal string
250    pub taker_amount: String,
251    /// Remaining amount as decimal string
252    pub remaining: String,
253    /// Filled amount as decimal string
254    pub filled: String,
255    /// Price as decimal string
256    pub price: String,
257    pub created_at: i64,
258    pub expiration: i64,
259}
260
261/// Order update from real-time event
262#[derive(Debug, Clone, Deserialize, Serialize)]
263pub struct OrderUpdate {
264    pub order_hash: String,
265    /// Price as decimal string
266    pub price: String,
267    /// Fill amount as decimal string
268    pub fill_amount: String,
269    /// Remaining amount as decimal string
270    pub remaining: String,
271    /// Filled amount as decimal string
272    pub filled: String,
273    /// 0 = BUY, 1 = SELL
274    pub side: i32,
275    pub is_maker: bool,
276    pub created_at: i64,
277    #[serde(default)]
278    pub balance: Option<Balance>,
279}
280
281/// Balance containing outcome balances
282#[derive(Debug, Clone, Deserialize, Serialize)]
283pub struct Balance {
284    pub outcomes: Vec<OutcomeBalance>,
285}
286
287/// Individual outcome balance
288#[derive(Debug, Clone, Deserialize, Serialize)]
289pub struct OutcomeBalance {
290    pub outcome_index: i32,
291    pub mint: String,
292    /// Idle balance as decimal string
293    pub idle: String,
294    /// On-book balance as decimal string
295    pub on_book: String,
296}
297
298/// Balance entry from user snapshot
299#[derive(Debug, Clone, Deserialize, Serialize)]
300pub struct BalanceEntry {
301    pub market_pubkey: String,
302    pub deposit_mint: String,
303    pub outcomes: Vec<OutcomeBalance>,
304}
305
306// ============================================================================
307// PRICE HISTORY TYPES
308// ============================================================================
309
310/// Price history data (snapshot, update, heartbeat)
311#[derive(Debug, Clone, Deserialize)]
312pub struct PriceHistoryData {
313    pub event_type: String,
314    #[serde(default)]
315    pub orderbook_id: Option<String>,
316    #[serde(default)]
317    pub resolution: Option<String>,
318    #[serde(default)]
319    pub include_ohlcv: Option<bool>,
320    #[serde(default)]
321    pub prices: Vec<Candle>,
322    #[serde(default)]
323    pub last_timestamp: Option<i64>,
324    #[serde(default)]
325    pub server_time: Option<i64>,
326    #[serde(default)]
327    pub last_processed: Option<i64>,
328    // For updates (inline candle data)
329    #[serde(default)]
330    pub t: Option<i64>,
331    #[serde(default)]
332    pub o: Option<String>,
333    #[serde(default)]
334    pub h: Option<String>,
335    #[serde(default)]
336    pub l: Option<String>,
337    #[serde(default)]
338    pub c: Option<String>,
339    #[serde(default)]
340    pub v: Option<String>,
341    #[serde(default)]
342    pub m: Option<String>,
343    #[serde(default)]
344    pub bb: Option<String>,
345    #[serde(default)]
346    pub ba: Option<String>,
347}
348
349impl PriceHistoryData {
350    /// Convert inline candle data to a Candle struct (for update events)
351    pub fn to_candle(&self) -> Option<Candle> {
352        self.t.map(|t| Candle {
353            t,
354            o: self.o.clone(),
355            h: self.h.clone(),
356            l: self.l.clone(),
357            c: self.c.clone(),
358            v: self.v.clone(),
359            m: self.m.clone(),
360            bb: self.bb.clone(),
361            ba: self.ba.clone(),
362        })
363    }
364}
365
366/// OHLCV candle data
367#[derive(Debug, Clone, Deserialize, Serialize)]
368pub struct Candle {
369    /// Timestamp (Unix ms)
370    pub t: i64,
371    /// Open price as decimal string (null if no trades)
372    #[serde(default)]
373    pub o: Option<String>,
374    /// High price as decimal string (null if no trades)
375    #[serde(default)]
376    pub h: Option<String>,
377    /// Low price as decimal string (null if no trades)
378    #[serde(default)]
379    pub l: Option<String>,
380    /// Close price as decimal string (null if no trades)
381    #[serde(default)]
382    pub c: Option<String>,
383    /// Volume as decimal string (null if no trades)
384    #[serde(default)]
385    pub v: Option<String>,
386    /// Midpoint: (best_bid + best_ask) / 2 as decimal string
387    #[serde(default)]
388    pub m: Option<String>,
389    /// Best bid price as decimal string
390    #[serde(default)]
391    pub bb: Option<String>,
392    /// Best ask price as decimal string
393    #[serde(default)]
394    pub ba: Option<String>,
395}
396
397// ============================================================================
398// MARKET EVENT TYPES
399// ============================================================================
400
401/// Market event data
402#[derive(Debug, Clone, Deserialize)]
403pub struct MarketEventData {
404    /// Event type: "orderbook_created", "settled", "opened", "paused"
405    pub event_type: String,
406    pub market_pubkey: String,
407    #[serde(default)]
408    pub orderbook_id: Option<String>,
409    pub timestamp: String,
410}
411
412/// Market event types
413#[derive(Debug, Clone, Copy, PartialEq, Eq)]
414pub enum MarketEventType {
415    OrderbookCreated,
416    Settled,
417    Opened,
418    Paused,
419    Unknown,
420}
421
422impl From<&str> for MarketEventType {
423    fn from(s: &str) -> Self {
424        match s {
425            "orderbook_created" => Self::OrderbookCreated,
426            "settled" => Self::Settled,
427            "opened" => Self::Opened,
428            "paused" => Self::Paused,
429            _ => Self::Unknown,
430        }
431    }
432}
433
434// ============================================================================
435// ERROR TYPES
436// ============================================================================
437
438/// Error response from server
439#[derive(Debug, Clone, Deserialize)]
440pub struct ErrorData {
441    pub error: String,
442    pub code: String,
443    #[serde(default)]
444    pub orderbook_id: Option<String>,
445}
446
447/// Server error codes
448#[derive(Debug, Clone, Copy, PartialEq, Eq)]
449pub enum ErrorCode {
450    EngineUnavailable,
451    InvalidJson,
452    InvalidMethod,
453    RateLimited,
454    Unknown,
455}
456
457impl From<&str> for ErrorCode {
458    fn from(s: &str) -> Self {
459        match s {
460            "ENGINE_UNAVAILABLE" => Self::EngineUnavailable,
461            "INVALID_JSON" => Self::InvalidJson,
462            "INVALID_METHOD" => Self::InvalidMethod,
463            "RATE_LIMITED" => Self::RateLimited,
464            _ => Self::Unknown,
465        }
466    }
467}
468
469// ============================================================================
470// PONG TYPE
471// ============================================================================
472
473/// Pong response data (empty)
474#[derive(Debug, Clone, Deserialize)]
475pub struct PongData {}
476
477// ============================================================================
478// CLIENT EVENTS
479// ============================================================================
480
481/// Events emitted by the WebSocket client
482#[derive(Debug, Clone)]
483pub enum WsEvent {
484    /// Successfully connected to server
485    Connected,
486
487    /// Disconnected from server
488    Disconnected { reason: String },
489
490    /// Orderbook update received
491    BookUpdate {
492        orderbook_id: String,
493        is_snapshot: bool,
494    },
495
496    /// Trade executed
497    Trade {
498        orderbook_id: String,
499        trade: TradeData,
500    },
501
502    /// User event received
503    UserUpdate {
504        event_type: String,
505        user: String,
506    },
507
508    /// Price history update
509    PriceUpdate {
510        orderbook_id: String,
511        resolution: String,
512    },
513
514    /// Market event
515    MarketEvent {
516        event_type: String,
517        market_pubkey: String,
518    },
519
520    /// Error occurred
521    Error {
522        error: super::error::WebSocketError,
523    },
524
525    /// Resync required for an orderbook
526    ResyncRequired { orderbook_id: String },
527
528    /// Pong received
529    Pong,
530
531    /// Reconnecting
532    Reconnecting { attempt: u32 },
533}
534
535// ============================================================================
536// MESSAGE TYPE ENUM
537// ============================================================================
538
539/// Enum for all possible server message types
540#[derive(Debug, Clone, Copy, PartialEq, Eq)]
541pub enum MessageType {
542    BookUpdate,
543    Trades,
544    User,
545    PriceHistory,
546    Market,
547    Error,
548    Pong,
549    Unknown,
550}
551
552impl From<&str> for MessageType {
553    fn from(s: &str) -> Self {
554        match s {
555            "book_update" => Self::BookUpdate,
556            "trades" => Self::Trades,
557            "user" => Self::User,
558            "price_history" => Self::PriceHistory,
559            "market" => Self::Market,
560            "error" => Self::Error,
561            "pong" => Self::Pong,
562            _ => Self::Unknown,
563        }
564    }
565}
566
567// ============================================================================
568// SIDE HELPERS
569// ============================================================================
570
571/// Order side enum for user events
572#[derive(Debug, Clone, Copy, PartialEq, Eq)]
573pub enum Side {
574    Buy,
575    Sell,
576}
577
578impl From<i32> for Side {
579    fn from(value: i32) -> Self {
580        match value {
581            0 => Self::Buy,
582            _ => Self::Sell,
583        }
584    }
585}
586
587impl Side {
588    pub fn as_i32(&self) -> i32 {
589        match self {
590            Self::Buy => 0,
591            Self::Sell => 1,
592        }
593    }
594}
595
596/// Price level side (from orderbook updates)
597#[derive(Debug, Clone, Copy, PartialEq, Eq)]
598pub enum PriceLevelSide {
599    Bid,
600    Ask,
601}
602
603impl From<&str> for PriceLevelSide {
604    fn from(s: &str) -> Self {
605        match s {
606            "bid" => Self::Bid,
607            _ => Self::Ask,
608        }
609    }
610}
611
612#[cfg(test)]
613mod tests {
614    use super::*;
615
616    #[test]
617    fn test_side_conversion() {
618        assert_eq!(Side::from(0), Side::Buy);
619        assert_eq!(Side::from(1), Side::Sell);
620        assert_eq!(Side::Buy.as_i32(), 0);
621        assert_eq!(Side::Sell.as_i32(), 1);
622    }
623
624    #[test]
625    fn test_message_type_parsing() {
626        assert_eq!(MessageType::from("book_update"), MessageType::BookUpdate);
627        assert_eq!(MessageType::from("trades"), MessageType::Trades);
628        assert_eq!(MessageType::from("user"), MessageType::User);
629        assert_eq!(MessageType::from("unknown"), MessageType::Unknown);
630    }
631
632    #[test]
633    fn test_subscribe_params_serialization() {
634        let params = SubscribeParams::book_update(vec!["market1:ob1".to_string()]);
635        let json = serde_json::to_string(&params).unwrap();
636        assert!(json.contains("book_update"));
637        assert!(json.contains("market1:ob1"));
638    }
639
640    #[test]
641    fn test_ws_request_serialization() {
642        let request = WsRequest::ping();
643        let json = serde_json::to_string(&request).unwrap();
644        assert_eq!(json, r#"{"method":"ping"}"#);
645    }
646
647    #[test]
648    fn test_book_update_deserialization() {
649        let json = r#"{
650            "orderbook_id": "ob1",
651            "timestamp": "2024-01-01T00:00:00.000Z",
652            "sequence": 42,
653            "bids": [{"side": "bid", "price": "0.500000", "size": "0.001000"}],
654            "asks": [{"side": "ask", "price": "0.510000", "size": "0.000500"}],
655            "is_snapshot": true
656        }"#;
657        let data: BookUpdateData = serde_json::from_str(json).unwrap();
658        assert_eq!(data.orderbook_id, "ob1");
659        assert_eq!(data.sequence, 42);
660        assert!(data.is_snapshot);
661        assert_eq!(data.bids.len(), 1);
662        assert_eq!(data.bids[0].price, "0.500000");
663        assert_eq!(data.bids[0].size, "0.001000");
664        assert_eq!(data.asks.len(), 1);
665        assert_eq!(data.asks[0].price, "0.510000");
666        assert_eq!(data.asks[0].size, "0.000500");
667    }
668
669    #[test]
670    fn test_trade_deserialization() {
671        let json = r#"{
672            "orderbook_id": "ob1",
673            "price": "0.505000",
674            "size": "0.000250",
675            "side": "bid",
676            "timestamp": "2024-01-01T00:00:00.000Z",
677            "trade_id": "trade123",
678            "sequence": 1
679        }"#;
680        let data: TradeData = serde_json::from_str(json).unwrap();
681        assert_eq!(data.orderbook_id, "ob1");
682        assert_eq!(data.price, "0.505000");
683        assert_eq!(data.size, "0.000250");
684        assert_eq!(data.sequence, 1);
685    }
686}