Skip to main content

nautilus_hyperliquid/websocket/
enums.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use serde::{Deserialize, Serialize};
17use strum::{AsRefStr, Display, EnumIter, EnumString};
18
19/// WebSocket channel names for Hyperliquid.
20#[derive(
21    Clone,
22    Debug,
23    Display,
24    PartialEq,
25    Eq,
26    Hash,
27    AsRefStr,
28    EnumIter,
29    EnumString,
30    Serialize,
31    Deserialize,
32)]
33pub enum HyperliquidWsChannel {
34    #[serde(rename = "subscriptionResponse")]
35    SubscriptionResponse,
36    #[serde(rename = "trades")]
37    Trades,
38    #[serde(rename = "l2Book")]
39    L2Book,
40    #[serde(rename = "bbo")]
41    Bbo,
42    #[serde(rename = "candle")]
43    Candle,
44    #[serde(rename = "allMids")]
45    AllMids,
46    #[serde(rename = "allDexsAssetCtxs")]
47    AllDexsAssetCtxs,
48    #[serde(rename = "notification")]
49    Notification,
50    #[serde(rename = "orderUpdates")]
51    OrderUpdates,
52    #[serde(rename = "userEvents")]
53    UserEvents,
54    #[serde(rename = "userFills")]
55    UserFills,
56    #[serde(rename = "userFundings")]
57    UserFundings,
58    #[serde(rename = "userNonFundingLedgerUpdates")]
59    UserNonFundingLedgerUpdates,
60    #[serde(rename = "activeAssetCtx")]
61    ActiveAssetCtx,
62    #[serde(rename = "activeSpotAssetCtx")]
63    ActiveSpotAssetCtx,
64    #[serde(rename = "activeAssetData")]
65    ActiveAssetData,
66    #[serde(rename = "userTwapSliceFills")]
67    UserTwapSliceFills,
68    #[serde(rename = "userTwapHistory")]
69    UserTwapHistory,
70    #[serde(rename = "webData2")]
71    WebData2,
72    /// Generic user channel - Hyperliquid sends fills/events on this channel.
73    #[serde(rename = "user")]
74    User,
75    #[serde(rename = "post")]
76    Post,
77    #[serde(rename = "pong")]
78    Pong,
79    #[serde(rename = "error")]
80    Error,
81}
82
83impl HyperliquidWsChannel {
84    /// Returns the string representation of the channel.
85    pub fn as_str(&self) -> &'static str {
86        match self {
87            Self::SubscriptionResponse => "subscriptionResponse",
88            Self::Trades => "trades",
89            Self::L2Book => "l2Book",
90            Self::Bbo => "bbo",
91            Self::Candle => "candle",
92            Self::AllMids => "allMids",
93            Self::AllDexsAssetCtxs => "allDexsAssetCtxs",
94            Self::Notification => "notification",
95            Self::OrderUpdates => "orderUpdates",
96            Self::UserEvents => "userEvents",
97            Self::UserFills => "userFills",
98            Self::UserFundings => "userFundings",
99            Self::UserNonFundingLedgerUpdates => "userNonFundingLedgerUpdates",
100            Self::ActiveAssetCtx => "activeAssetCtx",
101            Self::ActiveSpotAssetCtx => "activeSpotAssetCtx",
102            Self::ActiveAssetData => "activeAssetData",
103            Self::UserTwapSliceFills => "userTwapSliceFills",
104            Self::UserTwapHistory => "userTwapHistory",
105            Self::WebData2 => "webData2",
106            Self::User => "user",
107            Self::Post => "post",
108            Self::Pong => "pong",
109            Self::Error => "error",
110        }
111    }
112
113    /// Returns true if this is a public channel (does not require authentication).
114    pub fn is_public(&self) -> bool {
115        matches!(
116            self,
117            Self::SubscriptionResponse
118                | Self::Trades
119                | Self::L2Book
120                | Self::Bbo
121                | Self::Candle
122                | Self::AllMids
123                | Self::AllDexsAssetCtxs
124                | Self::ActiveAssetCtx
125                | Self::ActiveSpotAssetCtx
126                | Self::Notification
127                | Self::Pong
128                | Self::Error
129        )
130    }
131
132    /// Returns true if this is a private channel (requires authentication).
133    pub fn is_private(&self) -> bool {
134        !self.is_public()
135    }
136
137    /// Parses a channel from its wire-format string (e.g., "allMids").
138    pub fn from_wire_str(s: &str) -> Option<Self> {
139        serde_json::from_value(serde_json::Value::String(s.to_string())).ok()
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use std::str::FromStr;
146
147    use rstest::rstest;
148    use serde_json;
149    use strum::IntoEnumIterator;
150
151    use super::*;
152
153    #[rstest]
154    #[case(HyperliquidWsChannel::Trades, r#""trades""#)]
155    #[case(HyperliquidWsChannel::L2Book, r#""l2Book""#)]
156    #[case(HyperliquidWsChannel::UserFills, r#""userFills""#)]
157    #[case(HyperliquidWsChannel::Bbo, r#""bbo""#)]
158    #[case(
159        HyperliquidWsChannel::SubscriptionResponse,
160        r#""subscriptionResponse""#
161    )]
162    fn test_channel_serialization(#[case] channel: HyperliquidWsChannel, #[case] expected: &str) {
163        assert_eq!(serde_json::to_string(&channel).unwrap(), expected);
164    }
165
166    #[rstest]
167    #[case(r#""trades""#, HyperliquidWsChannel::Trades)]
168    #[case(r#""l2Book""#, HyperliquidWsChannel::L2Book)]
169    #[case(r#""userEvents""#, HyperliquidWsChannel::UserEvents)]
170    #[case(r#""bbo""#, HyperliquidWsChannel::Bbo)]
171    #[case(r#""pong""#, HyperliquidWsChannel::Pong)]
172    fn test_channel_deserialization(#[case] json: &str, #[case] expected: HyperliquidWsChannel) {
173        assert_eq!(
174            serde_json::from_str::<HyperliquidWsChannel>(json).unwrap(),
175            expected
176        );
177    }
178
179    #[rstest]
180    #[case(HyperliquidWsChannel::Trades, "trades")]
181    #[case(HyperliquidWsChannel::L2Book, "l2Book")]
182    #[case(HyperliquidWsChannel::UserFills, "userFills")]
183    #[case(
184        HyperliquidWsChannel::UserNonFundingLedgerUpdates,
185        "userNonFundingLedgerUpdates"
186    )]
187    #[case(HyperliquidWsChannel::Bbo, "bbo")]
188    fn test_as_str_method(#[case] channel: HyperliquidWsChannel, #[case] expected: &str) {
189        assert_eq!(channel.as_str(), expected);
190    }
191
192    #[rstest]
193    fn test_display_trait() {
194        assert_eq!(format!("{}", HyperliquidWsChannel::Trades), "Trades");
195        assert_eq!(format!("{}", HyperliquidWsChannel::L2Book), "L2Book");
196        assert_eq!(format!("{}", HyperliquidWsChannel::UserFills), "UserFills");
197    }
198
199    #[rstest]
200    fn test_is_public_channel() {
201        assert!(HyperliquidWsChannel::Trades.is_public());
202        assert!(HyperliquidWsChannel::L2Book.is_public());
203        assert!(HyperliquidWsChannel::Bbo.is_public());
204        assert!(HyperliquidWsChannel::SubscriptionResponse.is_public());
205        assert!(HyperliquidWsChannel::Pong.is_public());
206
207        assert!(!HyperliquidWsChannel::OrderUpdates.is_public());
208        assert!(!HyperliquidWsChannel::UserEvents.is_public());
209        assert!(!HyperliquidWsChannel::UserFills.is_public());
210        assert!(!HyperliquidWsChannel::UserFundings.is_public());
211        assert!(!HyperliquidWsChannel::UserNonFundingLedgerUpdates.is_public());
212        assert!(!HyperliquidWsChannel::Post.is_public());
213    }
214
215    #[rstest]
216    fn test_is_private_channel() {
217        assert!(!HyperliquidWsChannel::Trades.is_private());
218        assert!(!HyperliquidWsChannel::L2Book.is_private());
219        assert!(!HyperliquidWsChannel::Bbo.is_private());
220
221        assert!(HyperliquidWsChannel::OrderUpdates.is_private());
222        assert!(HyperliquidWsChannel::UserEvents.is_private());
223        assert!(HyperliquidWsChannel::UserFills.is_private());
224        assert!(HyperliquidWsChannel::UserFundings.is_private());
225        assert!(HyperliquidWsChannel::UserNonFundingLedgerUpdates.is_private());
226        assert!(HyperliquidWsChannel::Post.is_private());
227    }
228
229    #[rstest]
230    fn test_enum_iter() {
231        let channels: Vec<HyperliquidWsChannel> = HyperliquidWsChannel::iter().collect();
232        assert_eq!(channels.len(), 23);
233        assert!(channels.contains(&HyperliquidWsChannel::Trades));
234        assert!(channels.contains(&HyperliquidWsChannel::L2Book));
235        assert!(channels.contains(&HyperliquidWsChannel::UserFills));
236        assert!(channels.contains(&HyperliquidWsChannel::Candle));
237        assert!(channels.contains(&HyperliquidWsChannel::AllMids));
238        assert!(channels.contains(&HyperliquidWsChannel::AllDexsAssetCtxs));
239        assert!(channels.contains(&HyperliquidWsChannel::Notification));
240    }
241
242    #[rstest]
243    fn test_from_str() {
244        assert_eq!(
245            HyperliquidWsChannel::from_str("Trades").unwrap(),
246            HyperliquidWsChannel::Trades
247        );
248        assert_eq!(
249            HyperliquidWsChannel::from_str("L2Book").unwrap(),
250            HyperliquidWsChannel::L2Book
251        );
252        assert_eq!(
253            HyperliquidWsChannel::from_str("UserFills").unwrap(),
254            HyperliquidWsChannel::UserFills
255        );
256
257        assert!(HyperliquidWsChannel::from_str("InvalidChannel").is_err());
258    }
259}