1use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8#[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 pub fn subscribe(params: SubscribeParams) -> Self {
23 Self {
24 method: "subscribe".to_string(),
25 params: Some(params),
26 }
27 }
28
29 pub fn unsubscribe(params: SubscribeParams) -> Self {
31 Self {
32 method: "unsubscribe".to_string(),
33 params: Some(params),
34 }
35 }
36
37 pub fn ping() -> Self {
39 Self {
40 method: "ping".to_string(),
41 params: None,
42 }
43 }
44}
45
46#[derive(Debug, Clone, Serialize)]
48#[serde(untagged)]
49pub enum SubscribeParams {
50 BookUpdate {
52 #[serde(rename = "type")]
53 type_: &'static str,
54 orderbook_ids: Vec<String>,
55 },
56 Trades {
58 #[serde(rename = "type")]
59 type_: &'static str,
60 orderbook_ids: Vec<String>,
61 },
62 User {
64 #[serde(rename = "type")]
65 type_: &'static str,
66 user: String,
67 },
68 PriceHistory {
70 #[serde(rename = "type")]
71 type_: &'static str,
72 orderbook_id: String,
73 resolution: String,
74 include_ohlcv: bool,
75 },
76 Market {
78 #[serde(rename = "type")]
79 type_: &'static str,
80 market_pubkey: String,
81 },
82}
83
84impl SubscribeParams {
85 pub fn book_update(orderbook_ids: Vec<String>) -> Self {
87 Self::BookUpdate {
88 type_: "book_update",
89 orderbook_ids,
90 }
91 }
92
93 pub fn trades(orderbook_ids: Vec<String>) -> Self {
95 Self::Trades {
96 type_: "trades",
97 orderbook_ids,
98 }
99 }
100
101 pub fn user(user: String) -> Self {
103 Self::User {
104 type_: "user",
105 user,
106 }
107 }
108
109 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 pub fn market(market_pubkey: String) -> Self {
121 Self::Market {
122 type_: "market",
123 market_pubkey,
124 }
125 }
126
127 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#[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#[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#[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#[derive(Debug, Clone, Deserialize, Serialize)]
187pub struct PriceLevel {
188 pub side: String,
189 pub price: String,
191 pub size: String,
193}
194
195#[derive(Debug, Clone, Deserialize)]
201pub struct TradeData {
202 pub orderbook_id: String,
203 pub price: String,
205 pub size: String,
207 pub side: String,
208 pub timestamp: String,
209 pub trade_id: String,
210 pub sequence: u64,
211}
212
213#[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#[derive(Debug, Clone, Deserialize, Serialize)]
241pub struct Order {
242 pub order_hash: String,
243 pub market_pubkey: String,
244 pub orderbook_id: String,
245 pub side: i32,
247 pub maker_amount: String,
249 pub taker_amount: String,
251 pub remaining: String,
253 pub filled: String,
255 pub price: String,
257 pub created_at: i64,
258 pub expiration: i64,
259}
260
261#[derive(Debug, Clone, Deserialize, Serialize)]
263pub struct OrderUpdate {
264 pub order_hash: String,
265 pub price: String,
267 pub fill_amount: String,
269 pub remaining: String,
271 pub filled: String,
273 pub side: i32,
275 pub is_maker: bool,
276 pub created_at: i64,
277 #[serde(default)]
278 pub balance: Option<Balance>,
279}
280
281#[derive(Debug, Clone, Deserialize, Serialize)]
283pub struct Balance {
284 pub outcomes: Vec<OutcomeBalance>,
285}
286
287#[derive(Debug, Clone, Deserialize, Serialize)]
289pub struct OutcomeBalance {
290 pub outcome_index: i32,
291 pub mint: String,
292 pub idle: String,
294 pub on_book: String,
296}
297
298#[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#[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 #[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 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#[derive(Debug, Clone, Deserialize, Serialize)]
368pub struct Candle {
369 pub t: i64,
371 #[serde(default)]
373 pub o: Option<String>,
374 #[serde(default)]
376 pub h: Option<String>,
377 #[serde(default)]
379 pub l: Option<String>,
380 #[serde(default)]
382 pub c: Option<String>,
383 #[serde(default)]
385 pub v: Option<String>,
386 #[serde(default)]
388 pub m: Option<String>,
389 #[serde(default)]
391 pub bb: Option<String>,
392 #[serde(default)]
394 pub ba: Option<String>,
395}
396
397#[derive(Debug, Clone, Deserialize)]
403pub struct MarketEventData {
404 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#[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#[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#[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#[derive(Debug, Clone, Deserialize)]
475pub struct PongData {}
476
477#[derive(Debug, Clone)]
483pub enum WsEvent {
484 Connected,
486
487 Disconnected { reason: String },
489
490 BookUpdate {
492 orderbook_id: String,
493 is_snapshot: bool,
494 },
495
496 Trade {
498 orderbook_id: String,
499 trade: TradeData,
500 },
501
502 UserUpdate {
504 event_type: String,
505 user: String,
506 },
507
508 PriceUpdate {
510 orderbook_id: String,
511 resolution: String,
512 },
513
514 MarketEvent {
516 event_type: String,
517 market_pubkey: String,
518 },
519
520 Error {
522 error: super::error::WebSocketError,
523 },
524
525 ResyncRequired { orderbook_id: String },
527
528 Pong,
530
531 Reconnecting { attempt: u32 },
533}
534
535#[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#[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#[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(¶ms).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}