Skip to main content

df_hyperliquid_core/live/
request.rs

1//! The frames sent: subscribe, unsubscribe, ping, and a post, which carries an info read or a
2//! signed action over the socket instead of over HTTP. Each is one text frame, as
3//! `JSON.stringify` would write it, keys in the docs' order.
4
5use crate::live::Subscription;
6use crate::utils::Json;
7use crate::utils::json::BadJson;
8
9/// The heartbeat. The venue answers `{"channel":"pong"}`.
10pub const PING: &str = r#"{"method":"ping"}"#;
11
12/// `{"method":"subscribe","subscription":{...}}`.
13#[must_use]
14pub fn subscribe(subscription: &Subscription) -> String {
15    wrap("subscribe", subscription)
16}
17
18/// `{"method":"unsubscribe","subscription":{...}}`, with the same object the subscribe sent:
19/// the venue matches the options too.
20#[must_use]
21pub fn unsubscribe(subscription: &Subscription) -> String {
22    wrap("unsubscribe", subscription)
23}
24
25fn wrap(method: &str, subscription: &Subscription) -> String {
26    Json::Object(vec![
27        ("method".to_owned(), Json::String(method.to_owned())),
28        ("subscription".to_owned(), subscription.to_json()),
29    ])
30    .compact()
31}
32
33/// What a post carries: an info read, as it would be posted to `/info`, or a signed action, the
34/// whole `/exchange` body. Either is kept as written, key order and digits, since an action's
35/// signature is over exactly what was built.
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum Post {
38    Info(Json),
39    Action(Json),
40}
41
42impl Post {
43    /// An info read, the body `/info` would take: `{"type":"l2Book","coin":"ETH"}`.
44    pub fn info(request: &str) -> Result<Post, BadJson> {
45        Json::parse(request).map(Post::Info)
46    }
47
48    /// A signed action, the body `/exchange` would take: `{"action":..,"nonce":..,
49    /// "signature":..,"vaultAddress":..}`. Like `/exchange`, never sent twice: a post that
50    /// goes unanswered may still have landed.
51    pub fn action(body: &str) -> Result<Post, BadJson> {
52        Json::parse(body).map(Post::Action)
53    }
54
55    /// `info` or `action`, as the request's `type` names it.
56    #[must_use]
57    pub const fn kind(&self) -> &'static str {
58        match self {
59            Post::Info(_) => "info",
60            Post::Action(_) => "action",
61        }
62    }
63
64    #[must_use]
65    pub const fn payload(&self) -> &Json {
66        match self {
67            Post::Info(payload) | Post::Action(payload) => payload,
68        }
69    }
70}
71
72/// `{"method":"post","id":..,"request":{"type":"info"|"action","payload":..}}`. The answer comes
73/// on the channel `post` with the same `id`.
74#[must_use]
75pub fn post(id: u64, request: &Post) -> String {
76    Json::Object(vec![
77        ("method".to_owned(), Json::String("post".to_owned())),
78        ("id".to_owned(), Json::Number(id.to_string())),
79        (
80            "request".to_owned(),
81            Json::Object(vec![
82                ("type".to_owned(), Json::String(request.kind().to_owned())),
83                ("payload".to_owned(), request.payload().clone()),
84            ]),
85        ),
86    ])
87    .compact()
88}
89
90#[cfg(test)]
91#[path = "tests/request.rs"]
92mod tests;