Skip to main content

df_hyperliquid_core/live/subscription/
parse.rs

1//! Reading a subscription object back ([`Subscription::parse`]), in any key order, as the venue
2//! echoes it or as it was sent.
3
4use crate::live::{BadSubscription, Subscription};
5use crate::markets::Interval;
6use crate::utils::{Address, Json};
7
8/// Every `type`, in the docs' order and then the SDK's.
9const KINDS: [&str; 27] = [
10    "allMids",
11    "notification",
12    "webData3",
13    "twapStates",
14    "clearinghouseState",
15    "openOrders",
16    "candle",
17    "l2Book",
18    "trades",
19    "orderUpdates",
20    "userEvents",
21    "userFills",
22    "userFundings",
23    "userNonFundingLedgerUpdates",
24    "activeAssetCtx",
25    "activeAssetData",
26    "userTwapSliceFills",
27    "userTwapHistory",
28    "bbo",
29    "spotState",
30    "allDexsClearinghouseState",
31    "allDexsAssetCtxs",
32    "outcomeMetaUpdates",
33    "fastAssetCtxs",
34    "assetCtxs",
35    "spotAssetCtxs",
36    "userHistoricalOrders",
37];
38
39impl Subscription {
40    /// Reads a subscription object, in any key order, as the venue echoes it or as it was sent.
41    /// Keys it does not know are passed over; `null`, `false` and `""` read as left out.
42    pub fn parse(json: &Json) -> Result<Subscription, BadSubscription> {
43        if !matches!(json, Json::Object(_)) {
44            return Err(BadSubscription::NotAnObject);
45        }
46        let named = json.get("type").ok_or(BadSubscription::Missing { kind: "subscription", field: "type" })?;
47        let named = named.as_str().ok_or(BadSubscription::Wrong {
48            kind: "subscription",
49            field: "type",
50            expected: "a string",
51        })?;
52        let kind = KINDS
53            .into_iter()
54            .find(|kind| *kind == named)
55            .ok_or_else(|| BadSubscription::UnknownType(named.to_owned()))?;
56        let fields = Fields { json, kind };
57        Ok(match kind {
58            "allMids" => Subscription::AllMids { dex: fields.dex()? },
59            "notification" => Subscription::Notification { user: fields.user()? },
60            "webData3" => Subscription::WebData3 { user: fields.user()? },
61            "twapStates" => Subscription::TwapStates { user: fields.user()?, dex: fields.dex()? },
62            "clearinghouseState" => Subscription::ClearinghouseState { user: fields.user()?, dex: fields.dex()? },
63            "openOrders" => Subscription::OpenOrders { user: fields.user()?, dex: fields.dex()? },
64            "candle" => Subscription::Candle { coin: fields.coin()?, interval: fields.interval()? },
65            "l2Book" => Subscription::L2Book {
66                coin: fields.coin()?,
67                n_sig_figs: fields.small("nSigFigs")?,
68                mantissa: fields.small("mantissa")?,
69                fast: fields.flag("fast")?,
70            },
71            "trades" => Subscription::Trades { coin: fields.coin()? },
72            "orderUpdates" => Subscription::OrderUpdates { user: fields.user()? },
73            "userEvents" => Subscription::UserEvents { user: fields.user()? },
74            "userFills" => {
75                Subscription::UserFills { user: fields.user()?, aggregate_by_time: fields.flag("aggregateByTime")? }
76            }
77            "userFundings" => Subscription::UserFundings { user: fields.user()? },
78            "userNonFundingLedgerUpdates" => Subscription::UserNonFundingLedgerUpdates { user: fields.user()? },
79            "activeAssetCtx" => Subscription::ActiveAssetCtx { coin: fields.coin()? },
80            "activeAssetData" => Subscription::ActiveAssetData { user: fields.user()?, coin: fields.coin()? },
81            "userTwapSliceFills" => Subscription::UserTwapSliceFills { user: fields.user()? },
82            "userTwapHistory" => Subscription::UserTwapHistory { user: fields.user()? },
83            "bbo" => Subscription::Bbo { coin: fields.coin()? },
84            "spotState" => Subscription::SpotState {
85                user: fields.user()?,
86                ignore_portfolio_margin: fields.flag("ignorePortfolioMargin")?,
87            },
88            "allDexsClearinghouseState" => Subscription::AllDexsClearinghouseState { user: fields.user()? },
89            "allDexsAssetCtxs" => Subscription::AllDexsAssetCtxs,
90            "outcomeMetaUpdates" => Subscription::OutcomeMetaUpdates,
91            "fastAssetCtxs" => Subscription::FastAssetCtxs,
92            "assetCtxs" => Subscription::AssetCtxs { dex: fields.dex()? },
93            "spotAssetCtxs" => Subscription::SpotAssetCtxs,
94            "userHistoricalOrders" => Subscription::UserHistoricalOrders { user: fields.user()? },
95            other => return Err(BadSubscription::UnknownType(other.to_owned())),
96        })
97    }
98}
99
100/// The fields of one subscription object, read for the `kind` it names.
101struct Fields<'a> {
102    json: &'a Json,
103    kind: &'static str,
104}
105
106impl Fields<'_> {
107    fn wrong(&self, field: &'static str, expected: &'static str) -> BadSubscription {
108        BadSubscription::Wrong { kind: self.kind, field, expected }
109    }
110
111    /// A field that is there and not `null`.
112    fn present(&self, field: &str) -> Option<&Json> {
113        self.json.get(field).filter(|value| !matches!(value, Json::Null))
114    }
115
116    fn string(&self, field: &'static str) -> Result<&str, BadSubscription> {
117        let value = self.present(field).ok_or(BadSubscription::Missing { kind: self.kind, field })?;
118        value.as_str().ok_or_else(|| self.wrong(field, "a string"))
119    }
120
121    fn user(&self) -> Result<Address, BadSubscription> {
122        self.string("user")?.parse().map_err(|_| self.wrong("user", "an address"))
123    }
124
125    fn coin(&self) -> Result<String, BadSubscription> {
126        self.string("coin").map(ToOwned::to_owned)
127    }
128
129    fn interval(&self) -> Result<Interval, BadSubscription> {
130        Interval::parse(self.string("interval")?).ok_or_else(|| self.wrong("interval", "an interval the docs list"))
131    }
132
133    fn dex(&self) -> Result<String, BadSubscription> {
134        match self.present("dex") {
135            None => Ok(String::new()),
136            Some(value) => value.as_str().map(ToOwned::to_owned).ok_or_else(|| self.wrong("dex", "a string")),
137        }
138    }
139
140    fn flag(&self, field: &'static str) -> Result<bool, BadSubscription> {
141        match self.present(field) {
142            None => Ok(false),
143            Some(value) => value.as_bool().ok_or_else(|| self.wrong(field, "true or false")),
144        }
145    }
146
147    fn small(&self, field: &'static str) -> Result<Option<u8>, BadSubscription> {
148        match self.present(field) {
149            None => Ok(None),
150            Some(value) => value
151                .as_u64()
152                .and_then(|number| u8::try_from(number).ok())
153                .map(Some)
154                .ok_or_else(|| self.wrong(field, "a small whole number")),
155        }
156    }
157}
158
159#[cfg(test)]
160#[path = "tests/parse.rs"]
161mod tests;