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