hyperliquid_ws_post/
ws-post.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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
16//! Minimal WS post example: info (l2Book) and a stubbed order action.
17
18use std::{env, time::Duration};
19
20use nautilus_hyperliquid::{
21    common::consts::ws_url,
22    websocket::{
23        client::HyperliquidWebSocketClient,
24        messages::{ActionPayload, ActionRequest, SignatureData, TimeInForceRequest},
25        post::{Grouping, OrderBuilder},
26    },
27};
28use tracing::{info, level_filters::LevelFilter, warn};
29use tracing_subscriber::{EnvFilter, fmt};
30
31#[tokio::main]
32async fn main() -> Result<(), Box<dyn std::error::Error>> {
33    // Structured logging with env-controlled filter (e.g. RUST_LOG=debug)
34    let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
35    fmt()
36        .with_env_filter(env_filter)
37        .with_max_level(LevelFilter::INFO)
38        .init();
39
40    let args: Vec<String> = env::args().collect();
41    let testnet = args.get(1).is_some_and(|s| s == "testnet");
42    let ws_url = ws_url(testnet);
43
44    info!(component = "ws_post", %ws_url, ?testnet, "connecting");
45    let mut client = HyperliquidWebSocketClient::connect(ws_url).await?;
46    info!(component = "ws_post", "websocket connected");
47
48    let book = client.info_l2_book("BTC", Duration::from_secs(2)).await?;
49    let best_bid = book
50        .levels
51        .first()
52        .and_then(|bids| bids.first())
53        .map(|l| l.px.clone())
54        .unwrap_or_default();
55    let best_ask = book
56        .levels
57        .get(1)
58        .and_then(|asks| asks.first())
59        .map(|l| l.px.clone())
60        .unwrap_or_default();
61    info!(component = "ws_post", best_bid = %best_bid, best_ask = %best_ask, "BTC top of book");
62
63    // Only attempt the action when explicitly requested (HL_SEND=1).
64    let should_send = std::env::var("HL_SEND").map(|v| v == "1").unwrap_or(false);
65    if !should_send {
66        warn!(
67            component = "ws_post",
68            "skipping action: set HL_SEND=1 to send the stubbed order"
69        );
70        return Ok(());
71    }
72
73    if best_bid.is_empty() {
74        warn!(
75            component = "ws_post",
76            "no best bid available; aborting action"
77        );
78        return Ok(());
79    }
80
81    // === ACTION (stub): place a post-only limit (requires real signature!) ===
82    let action: ActionRequest = OrderBuilder::new()
83        .grouping(Grouping::Na)
84        .push_limit(
85            /*asset*/ 0, // BTC (adapter maps 0 → BTC)
86            /*is_buy*/ true, // buy
87            /*px*/ best_bid.clone(), // price from book
88            /*sz*/ "0.001", // size
89            /*reduce_only*/ false,
90            TimeInForceRequest::Alo, // post-only
91            Some("test-cloid-1".to_string()),
92        )
93        .build();
94
95    // TODO: sign properly; below is a placeholder signature (r,s,v must be valid!)
96    let payload = ActionPayload {
97        action,
98        nonce: 0, // e.g., time-based nonce or your NonceManager
99        signature: SignatureData {
100            r: "0x0".into(),
101            s: "0x0".into(),
102            v: "0x1b".into(),
103        },
104        vault_address: None,
105    };
106
107    match client
108        .post_action_raw(payload, Duration::from_secs(2))
109        .await
110    {
111        Ok(resp) => info!(component = "ws_post", ?resp, "action response"),
112        Err(e) => {
113            warn!(component = "ws_post", error = %e, "action failed (expected with dummy signature)")
114        }
115    }
116
117    Ok(())
118}