1use crate::prelude::*;
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum WebSocketState {
17 Disconnected,
19 Connecting,
21 Connected,
23 Reconnecting,
25 Error,
27}
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
39pub enum SubscriptionChannel {
40 Orderbook,
43 Trades,
45 Orders,
47 Fills,
49 Markets,
51 Prices,
53 Positions,
55 Transactions,
57 OrderEvents,
59 LiveSports,
61 LiveEsports,
63 MarketLifecycle,
65
66 SubscribeMarketPrices,
69 SubscribePositions,
71 SubscribeTransactions,
73 SubscribeOrderEvents,
75 SubscribeLiveSports,
77 SubscribeLiveEsports,
79 SubscribeMarketLifecycle,
81 UnsubscribeMarketLifecycle,
83}
84
85impl SubscriptionChannel {
86 pub fn as_str(&self) -> &'static str {
88 match self {
89 Self::Orderbook => "orderbook",
90 Self::Trades => "trades",
91 Self::Orders => "orders",
92 Self::Fills => "fills",
93 Self::Markets => "markets",
94 Self::Prices => "prices",
95 Self::Positions => "positions",
96 Self::Transactions => "transactions",
97 Self::OrderEvents => "orderEvent",
98 Self::LiveSports => "liveSports",
99 Self::LiveEsports => "liveEsports",
100 Self::MarketLifecycle => "marketLifecycle",
101 Self::SubscribeMarketPrices => "subscribe_market_prices",
102 Self::SubscribePositions => "subscribe_positions",
103 Self::SubscribeTransactions => "subscribe_transactions",
104 Self::SubscribeOrderEvents => "subscribe_order_events",
105 Self::SubscribeLiveSports => "subscribe_live_sports",
106 Self::SubscribeLiveEsports => "subscribe_live_esports",
107 Self::SubscribeMarketLifecycle => "subscribe_market_lifecycle",
108 Self::UnsubscribeMarketLifecycle => "unsubscribe_market_lifecycle",
109 }
110 }
111}
112
113#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
119pub struct SubscriptionOptions {
120 #[serde(
122 rename = "marketSlug",
123 skip_serializing_if = "Option::is_none",
124 default
125 )]
126 pub market_slug: Option<String>,
127
128 #[serde(rename = "marketSlugs", skip_serializing_if = "Vec::is_empty", default)]
130 pub market_slugs: Vec<String>,
131
132 #[serde(
134 rename = "marketAddress",
135 skip_serializing_if = "Option::is_none",
136 default
137 )]
138 pub market_address: Option<String>,
139
140 #[serde(
142 rename = "marketAddresses",
143 skip_serializing_if = "Vec::is_empty",
144 default
145 )]
146 pub market_addresses: Vec<String>,
147
148 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
150 pub filters: BTreeMap<String, Value>,
151}
152
153#[derive(Clone, Debug)]
159pub struct WebSocketConfig {
160 pub url: String,
162 pub api_key: Option<String>,
164 pub auto_reconnect: bool,
166 pub reconnect_delay_ms: u64,
168 pub max_reconnect_attempts: u32,
170 pub timeout_ms: u64,
172}
173
174impl Default for WebSocketConfig {
175 fn default() -> Self {
176 Self {
177 url: "wss://ws.limitless.exchange/markets".to_string(),
178 api_key: std::env::var("LIMITLESS_API_KEY").ok(),
179 auto_reconnect: true,
180 reconnect_delay_ms: 1_000,
181 max_reconnect_attempts: 0,
182 timeout_ms: 10_000,
183 }
184 }
185}
186
187#[derive(Clone, Copy, Debug, PartialEq)]
197pub struct FlexFloat(pub f64);
198
199impl FlexFloat {
200 pub fn float64(self) -> f64 {
202 self.0
203 }
204}
205
206impl<'de> Deserialize<'de> for FlexFloat {
207 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
208 where
209 D: serde::Deserializer<'de>,
210 {
211 match Value::deserialize(deserializer)? {
212 Value::Number(n) => n
213 .as_f64()
214 .map(Self)
215 .ok_or_else(|| serde::de::Error::custom("expected f64-compatible number")),
216 Value::String(s) => s.parse::<f64>().map(Self).map_err(|err| {
217 serde::de::Error::custom(format!("cannot parse float '{s}': {err}"))
218 }),
219 other => Err(serde::de::Error::custom(format!(
220 "cannot deserialize FlexFloat from {other}"
221 ))),
222 }
223 }
224}
225
226impl Serialize for FlexFloat {
227 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
228 where
229 S: serde::Serializer,
230 {
231 serializer.serialize_f64(self.0)
232 }
233}
234
235pub type WsEvent = Value;
242
243#[derive(Clone, Debug, Serialize, Deserialize)]
251#[serde(untagged)]
252pub enum WsEventKind {
253 NewPriceData(NewPriceData),
255 OrderbookUpdate(OrderbookUpdate),
257 OraclePriceData(OraclePriceData),
259 TradeEvent(TradeEvent),
261 OrderUpdate(OrderUpdate),
263 FillEvent(FillEvent),
265 MarketUpdateEvent(MarketUpdateEvent),
267 TransactionEvent(TransactionEvent),
269 MarketCreatedEvent(MarketCreatedEvent),
271 MarketResolvedEvent(MarketResolvedEvent),
273 Positions(Value),
275 OrderEvent(Value),
277 LiveSports(Value),
279 LiveEsports(Value),
281 System(SystemEvent),
283 Authenticated(Value),
285 Exception(Value),
287 Error(Value),
289 Unknown(Value),
291}
292
293pub fn deserialize_event(event: &str, payload: &Value) -> Option<WsEventKind> {
298 match event {
299 "newPriceData" => serde_json::from_value::<NewPriceData>(payload.clone())
300 .ok()
301 .map(WsEventKind::NewPriceData),
302 "orderbookUpdate" => serde_json::from_value::<OrderbookUpdate>(payload.clone())
303 .ok()
304 .map(WsEventKind::OrderbookUpdate),
305 "oraclePriceData" => serde_json::from_value::<OraclePriceData>(payload.clone())
306 .ok()
307 .map(WsEventKind::OraclePriceData),
308 "trades" => serde_json::from_value::<TradeEvent>(payload.clone())
309 .ok()
310 .map(WsEventKind::TradeEvent),
311 "orders" => serde_json::from_value::<OrderUpdate>(payload.clone())
312 .ok()
313 .map(WsEventKind::OrderUpdate),
314 "fills" => serde_json::from_value::<FillEvent>(payload.clone())
315 .ok()
316 .map(WsEventKind::FillEvent),
317 "markets" => serde_json::from_value::<MarketUpdateEvent>(payload.clone())
318 .ok()
319 .map(WsEventKind::MarketUpdateEvent),
320 "transactions" => serde_json::from_value::<TransactionEvent>(payload.clone())
321 .ok()
322 .map(WsEventKind::TransactionEvent),
323 "marketCreated" => serde_json::from_value::<MarketCreatedEvent>(payload.clone())
324 .ok()
325 .map(WsEventKind::MarketCreatedEvent),
326 "marketResolved" => serde_json::from_value::<MarketResolvedEvent>(payload.clone())
327 .ok()
328 .map(WsEventKind::MarketResolvedEvent),
329 "positions" => Some(WsEventKind::Positions(payload.clone())),
330 "orderEvent" => Some(WsEventKind::OrderEvent(payload.clone())),
331 "liveSports" => Some(WsEventKind::LiveSports(payload.clone())),
332 "liveEsports" => Some(WsEventKind::LiveEsports(payload.clone())),
333 "system" => serde_json::from_value::<SystemEvent>(payload.clone())
334 .ok()
335 .map(WsEventKind::System),
336 "authenticated" => Some(WsEventKind::Authenticated(payload.clone())),
337 "exception" => Some(WsEventKind::Exception(payload.clone())),
338 "error" => Some(WsEventKind::Error(payload.clone())),
339 _other => Some(WsEventKind::Unknown(payload.clone())),
340 }
341}
342
343#[derive(Clone, Debug, Serialize, Deserialize)]
347pub struct OrderbookLevel {
348 pub price: f64,
349 pub size: f64,
350}
351
352#[derive(Clone, Debug, Serialize, Deserialize)]
354pub struct OrderbookData {
355 pub bids: Vec<OrderbookLevel>,
356 pub asks: Vec<OrderbookLevel>,
357 #[serde(rename = "tokenId")]
358 pub token_id: String,
359 #[serde(rename = "adjustedMidpoint")]
360 pub adjusted_midpoint: f64,
361 #[serde(rename = "maxSpread")]
362 pub max_spread: FlexFloat,
363 #[serde(rename = "minSize")]
364 pub min_size: FlexFloat,
365}
366
367#[derive(Clone, Debug, Serialize, Deserialize)]
369pub struct OrderbookUpdate {
370 #[serde(rename = "marketSlug")]
371 pub market_slug: String,
372 pub orderbook: OrderbookData,
373 pub timestamp: Value,
374}
375
376#[derive(Clone, Debug, Serialize, Deserialize)]
380pub struct TradeEvent {
381 #[serde(rename = "marketSlug")]
382 pub market_slug: String,
383 pub side: String,
384 pub price: f64,
385 pub size: f64,
386 pub timestamp: f64,
387 #[serde(rename = "tradeId")]
388 pub trade_id: String,
389}
390
391#[derive(Clone, Debug, Serialize, Deserialize)]
395pub struct OrderUpdate {
396 #[serde(rename = "orderId")]
397 pub order_id: String,
398 #[serde(rename = "marketSlug")]
399 pub market_slug: String,
400 pub side: String,
401 #[serde(default)]
402 pub price: Option<f64>,
403 pub size: f64,
404 pub filled: f64,
405 pub status: String,
406 pub timestamp: f64,
407}
408
409#[derive(Clone, Debug, Serialize, Deserialize)]
413pub struct FillEvent {
414 #[serde(rename = "orderId")]
415 pub order_id: String,
416 #[serde(rename = "marketSlug")]
417 pub market_slug: String,
418 pub side: String,
419 pub price: f64,
420 pub size: f64,
421 pub timestamp: f64,
422 #[serde(rename = "fillId")]
423 pub fill_id: String,
424}
425
426#[derive(Clone, Debug, Serialize, Deserialize)]
430pub struct MarketUpdateEvent {
431 #[serde(rename = "marketSlug")]
432 pub market_slug: String,
433 #[serde(rename = "lastPrice", default)]
434 pub last_price: Option<f64>,
435 #[serde(rename = "volume24h", default)]
436 pub volume_24h: Option<f64>,
437 #[serde(rename = "priceChange24h", default)]
438 pub price_change_24h: Option<f64>,
439 pub timestamp: f64,
440}
441
442#[derive(Clone, Debug, Serialize, Deserialize)]
446pub struct AmmPriceEntry {
447 #[serde(rename = "marketId")]
448 pub market_id: i32,
449 #[serde(rename = "marketAddress")]
450 pub market_address: String,
451 #[serde(rename = "yesPrice")]
452 pub yes_price: f64,
453 #[serde(rename = "noPrice")]
454 pub no_price: f64,
455}
456
457#[derive(Clone, Debug, Serialize, Deserialize)]
459pub struct NewPriceData {
460 #[serde(rename = "marketAddress")]
461 pub market_address: String,
462 #[serde(rename = "updatedPrices")]
463 pub updated_prices: Vec<AmmPriceEntry>,
464 #[serde(rename = "blockNumber")]
465 pub block_number: i64,
466 pub timestamp: Value,
467}
468
469#[derive(Clone, Debug, Serialize, Deserialize)]
473pub struct OraclePriceData {
474 #[serde(rename = "marketAddress", default)]
475 pub market_address: Option<String>,
476 #[serde(rename = "marketSlug")]
477 pub market_slug: String,
478 pub timestamp: i64,
479 pub value: f64,
480}
481
482#[derive(Clone, Debug, Serialize, Deserialize)]
486pub struct TransactionEvent {
487 #[serde(rename = "userId", default)]
488 pub user_id: Option<i32>,
489 #[serde(rename = "txHash", default)]
490 pub tx_hash: Option<String>,
491 pub status: String,
492 pub source: String,
493 pub timestamp: String,
494 #[serde(rename = "marketAddress", default)]
495 pub market_address: Option<String>,
496 #[serde(rename = "marketSlug", default)]
497 pub market_slug: Option<String>,
498 #[serde(rename = "tokenId", default)]
499 pub token_id: Option<String>,
500 #[serde(rename = "conditionId", default)]
501 pub condition_id: Option<String>,
502 #[serde(rename = "amountContracts", default)]
503 pub amount_contracts: Option<String>,
504 #[serde(rename = "amountCollateral", default)]
505 pub amount_collateral: Option<String>,
506 #[serde(default)]
507 pub price: Option<String>,
508 #[serde(default)]
509 pub side: Option<String>,
510}
511
512#[derive(Clone, Debug, Serialize, Deserialize)]
516pub struct MarketCreatedEvent {
517 pub slug: String,
518 pub title: String,
519 #[serde(rename = "type")]
520 pub market_type: String,
521 #[serde(rename = "groupSlug", default)]
522 pub group_slug: Option<String>,
523 #[serde(rename = "categoryIds", default)]
524 pub category_ids: Vec<i32>,
525 #[serde(rename = "createdAt")]
526 pub created_at: String,
527}
528
529#[derive(Clone, Debug, Serialize, Deserialize)]
531pub struct MarketResolvedEvent {
532 pub slug: String,
533 #[serde(rename = "type")]
534 pub market_type: String,
535 #[serde(rename = "winningOutcome")]
536 pub winning_outcome: String,
537 #[serde(rename = "winningIndex")]
538 pub winning_index: i32,
539 #[serde(rename = "resolutionDate")]
540 pub resolution_date: String,
541}
542
543#[derive(Clone, Debug, Serialize, Deserialize)]
549pub struct SystemEvent {
550 #[serde(default)]
551 pub message: Option<String>,
552 #[serde(default)]
553 pub markets: Option<Vec<String>>,
554}
555
556pub fn normalize_subscription_options(opts: SubscriptionOptions) -> SubscriptionOptions {
566 let mut opts = opts;
567 if opts.market_slugs.is_empty() {
568 if let Some(ref slug) = opts.market_slug {
569 opts.market_slugs = vec![slug.clone()];
570 }
571 }
572 if opts.market_addresses.is_empty() {
573 if let Some(ref addr) = opts.market_address {
574 opts.market_addresses = vec![addr.clone()];
575 }
576 }
577 opts
578}
579
580pub fn subscription_key(channel: SubscriptionChannel, opts: &SubscriptionOptions) -> String {
583 let slugs = if opts.market_slugs.is_empty() {
584 String::new()
585 } else {
586 let mut sorted: Vec<&str> = opts.market_slugs.iter().map(String::as_str).collect();
587 sorted.sort_unstable();
588 sorted.join(",")
589 };
590
591 let addresses = if opts.market_addresses.is_empty() {
592 String::new()
593 } else {
594 let mut sorted: Vec<&str> = opts.market_addresses.iter().map(String::as_str).collect();
595 sorted.sort_unstable();
596 sorted.join(",")
597 };
598
599 format!("{}|{}|{}", channel.as_str(), slugs, addresses)
600}
601
602pub fn channel_from_key(key: &str) -> Option<SubscriptionChannel> {
604 let channel_str = key.split('|').next().unwrap_or(key);
606 match channel_str {
607 "orderbook" => Some(SubscriptionChannel::Orderbook),
608 "trades" => Some(SubscriptionChannel::Trades),
609 "orders" => Some(SubscriptionChannel::Orders),
610 "fills" => Some(SubscriptionChannel::Fills),
611 "markets" => Some(SubscriptionChannel::Markets),
612 "prices" => Some(SubscriptionChannel::Prices),
613 "positions" => Some(SubscriptionChannel::Positions),
614 "transactions" => Some(SubscriptionChannel::Transactions),
615 "orderEvent" => Some(SubscriptionChannel::OrderEvents),
616 "liveSports" => Some(SubscriptionChannel::LiveSports),
617 "liveEsports" => Some(SubscriptionChannel::LiveEsports),
618 "marketLifecycle" => Some(SubscriptionChannel::MarketLifecycle),
619 "subscribe_market_prices" => Some(SubscriptionChannel::SubscribeMarketPrices),
620 "subscribe_positions" => Some(SubscriptionChannel::SubscribePositions),
621 "subscribe_transactions" => Some(SubscriptionChannel::SubscribeTransactions),
622 "subscribe_order_events" => Some(SubscriptionChannel::SubscribeOrderEvents),
623 "subscribe_live_sports" => Some(SubscriptionChannel::SubscribeLiveSports),
624 "subscribe_live_esports" => Some(SubscriptionChannel::SubscribeLiveEsports),
625 "subscribe_market_lifecycle" => Some(SubscriptionChannel::SubscribeMarketLifecycle),
626 "unsubscribe_market_lifecycle" => Some(SubscriptionChannel::UnsubscribeMarketLifecycle),
627 _ => None,
628 }
629}
630
631pub fn requires_websocket_auth(channel: SubscriptionChannel) -> bool {
633 matches!(
634 channel,
635 SubscriptionChannel::SubscribePositions
636 | SubscriptionChannel::SubscribeTransactions
637 | SubscriptionChannel::SubscribeOrderEvents
638 )
639}
640
641#[cfg(test)]
646mod tests {
647 use super::*;
648
649 #[test]
650 fn subscription_key_is_order_independent() {
651 let opts_a = SubscriptionOptions {
652 market_slugs: vec!["btc-above-100k".into(), "eth-merge".into()],
653 ..Default::default()
654 };
655 let opts_b = SubscriptionOptions {
656 market_slugs: vec!["eth-merge".into(), "btc-above-100k".into()],
657 ..Default::default()
658 };
659 assert_eq!(
660 subscription_key(SubscriptionChannel::SubscribeMarketPrices, &opts_a),
661 subscription_key(SubscriptionChannel::SubscribeMarketPrices, &opts_b),
662 );
663 }
664
665 #[test]
666 fn normalize_copies_singular_into_plural() {
667 let opts = SubscriptionOptions {
668 market_slug: Some("test-slug".into()),
669 market_address: Some("0xdead".into()),
670 ..Default::default()
671 };
672 let normalized = normalize_subscription_options(opts);
673 assert_eq!(normalized.market_slugs, vec!["test-slug"]);
674 assert_eq!(normalized.market_addresses, vec!["0xdead"]);
675 }
676
677 #[test]
678 fn normalize_preserves_existing_plurals() {
679 let opts = SubscriptionOptions {
680 market_slugs: vec!["existing".into()],
681 ..Default::default()
682 };
683 let normalized = normalize_subscription_options(opts);
684 assert_eq!(normalized.market_slugs, vec!["existing"]);
685 }
686
687 #[test]
688 fn channel_from_key_roundtrips() {
689 for channel in &[
690 SubscriptionChannel::Orderbook,
691 SubscriptionChannel::Trades,
692 SubscriptionChannel::SubscribeMarketPrices,
693 SubscriptionChannel::SubscribePositions,
694 SubscriptionChannel::OrderEvents,
695 SubscriptionChannel::MarketLifecycle,
696 ] {
697 let key = subscription_key(*channel, &SubscriptionOptions::default());
698 let recovered = channel_from_key(&key);
699 assert_eq!(
700 recovered,
701 Some(*channel),
702 "round-trip failed for {channel:?}"
703 );
704 }
705 }
706
707 #[test]
708 fn requires_auth_returns_true_for_private_channels() {
709 assert!(requires_websocket_auth(
710 SubscriptionChannel::SubscribePositions
711 ));
712 assert!(requires_websocket_auth(
713 SubscriptionChannel::SubscribeTransactions
714 ));
715 assert!(requires_websocket_auth(
716 SubscriptionChannel::SubscribeOrderEvents
717 ));
718 }
719
720 #[test]
721 fn requires_auth_returns_false_for_public_channels() {
722 assert!(!requires_websocket_auth(
723 SubscriptionChannel::SubscribeMarketPrices
724 ));
725 assert!(!requires_websocket_auth(
726 SubscriptionChannel::SubscribeMarketLifecycle
727 ));
728 }
729
730 #[test]
731 fn flexfloat_parses_number_and_string() {
732 let from_number: FlexFloat = serde_json::from_str("0.55").unwrap();
733 assert!((from_number.float64() - 0.55).abs() < f64::EPSILON);
734
735 let from_string: FlexFloat = serde_json::from_str(r#""0.55""#).unwrap();
736 assert!((from_string.float64() - 0.55).abs() < f64::EPSILON);
737 }
738
739 #[test]
740 fn websocket_channel_inventory_includes_all_server_events() {
741 let server_events = [
743 "orderbook",
744 "trades",
745 "orders",
746 "fills",
747 "markets",
748 "prices",
749 "positions",
750 "transactions",
751 "orderEvent",
752 "liveSports",
753 "liveEsports",
754 "marketLifecycle",
755 ];
756 for &event in &server_events {
757 assert!(
758 channel_from_key(event).is_some(),
759 "missing channel variant for server event '{event}'"
760 );
761 }
762 }
763}