Skip to main content

df_hyperliquid_core/live/frame/
control.rs

1//! The venue's word on what was sent: an [`Ack`] when it took a subscribe or an unsubscribe, a
2//! [`Refusal`] in its own words when it refused something, a post included.
3
4use crate::live::{BadSubscription, Subscription};
5use crate::utils::Json;
6use serde::Deserialize;
7
8/// Which way a subscription went.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
10#[serde(rename_all = "lowercase")]
11pub enum Method {
12    Subscribe,
13    Unsubscribe,
14}
15
16/// The venue's acknowledgement, echoing the subscription as it read it: keys reordered, left
17/// out options filled in, the user in lowercase.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct Ack {
20    pub method: Method,
21    pub echo: Json,
22}
23
24impl Ack {
25    /// The subscription it names.
26    pub fn subscription(&self) -> Result<Subscription, BadSubscription> {
27        Subscription::parse(&self.echo)
28    }
29}
30
31/// The venue refusing something: `Already subscribed: {...}`, `Already unsubscribed: {...}`,
32/// `Error parsing JSON into valid websocket request: {...}` with what was sent.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct Refusal {
35    pub message: String,
36}
37
38impl Refusal {
39    /// The post it refuses: from an `id=<n>` at the end of the message, or the `id` of a post
40    /// it quotes back.
41    #[must_use]
42    pub fn post_id(&self) -> Option<u64> {
43        if let Some((_, digits)) = self.message.rsplit_once("id=")
44            && !digits.is_empty()
45            && digits.bytes().all(|byte| byte.is_ascii_digit())
46        {
47            return digits.parse().ok();
48        }
49        let quoted = self.quoted()?;
50        match quoted.get("method").and_then(Json::as_str) {
51            Some("post") => quoted.get("id").and_then(Json::as_u64),
52            _ => None,
53        }
54    }
55
56    /// The subscription it refuses, when it quotes one this crate reads.
57    #[must_use]
58    pub fn subscription(&self) -> Option<Subscription> {
59        let quoted = self.quoted()?;
60        let subscription = match quoted.get("subscription") {
61            Some(inside) => inside,
62            None if ["Already subscribed", "Already unsubscribed", "Invalid subscription"]
63                .iter()
64                .any(|start| self.message.starts_with(start)) =>
65            {
66                &quoted
67            }
68            None => return None,
69        };
70        Subscription::parse(subscription).ok()
71    }
72
73    /// The JSON the message ends with, when it ends with some.
74    fn quoted(&self) -> Option<Json> {
75        let at = self.message.find('{')?;
76        Json::parse(self.message.get(at..)?).ok()
77    }
78}
79
80#[cfg(test)]
81#[path = "tests/control.rs"]
82mod tests;