hyperliquid_ws_post/
ws_post.rs1use std::{env, time::Duration};
19
20use nautilus_hyperliquid::{
21 common::{consts::ws_url, credential::EvmPrivateKey},
22 signing::{HyperliquidActionType, HyperliquidEip712Signer, SignRequest, TimeNonce},
23 websocket::{
24 client::HyperliquidWebSocketClient,
25 messages::{ActionPayload, ActionRequest, SignatureData, TimeInForceRequest},
26 post::{Grouping, OrderBuilder},
27 },
28};
29use tracing::level_filters::LevelFilter;
30use tracing_subscriber::{EnvFilter, fmt};
31
32#[tokio::main]
33async fn main() -> Result<(), Box<dyn std::error::Error>> {
34 let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
36 fmt()
37 .with_env_filter(env_filter)
38 .with_max_level(LevelFilter::INFO)
39 .init();
40
41 let args: Vec<String> = env::args().collect();
42 let testnet = args.get(1).is_some_and(|s| s == "testnet");
43 let ws_url = ws_url(testnet);
44
45 tracing::info!(component = "ws_post", %ws_url, ?testnet, "connecting");
46 let client = HyperliquidWebSocketClient::connect(ws_url).await?;
47 tracing::info!(component = "ws_post", "websocket connected");
48
49 let book = client.info_l2_book("BTC", Duration::from_secs(2)).await?;
50 let best_bid = book
51 .levels
52 .first()
53 .and_then(|bids| bids.first())
54 .map(|l| l.px.clone())
55 .unwrap_or_default();
56 let best_ask = book
57 .levels
58 .get(1)
59 .and_then(|asks| asks.first())
60 .map(|l| l.px.clone())
61 .unwrap_or_default();
62 tracing::info!(component = "ws_post", best_bid = %best_bid, best_ask = %best_ask, "BTC top of book");
63
64 let should_send = env::var("HYPERLIQUID_SEND")
66 .map(|v| v == "1")
67 .unwrap_or(false);
68 if !should_send {
69 tracing::warn!(
70 component = "ws_post",
71 "skipping action: set HYPERLIQUID_SEND=1 to send the order"
72 );
73 return Ok(());
74 }
75
76 if best_bid.is_empty() {
77 tracing::warn!(
78 component = "ws_post",
79 "no best bid available; aborting action"
80 );
81 return Ok(());
82 }
83
84 let action: ActionRequest = OrderBuilder::new()
86 .grouping(Grouping::Na)
87 .push_limit(
88 0, true, best_bid.clone(), "0.001", false,
93 TimeInForceRequest::Alo, Some("test-cloid-1".to_string()),
95 )
96 .build();
97
98 let private_key_str = env::var("HYPERLIQUID_PK").map_err(
100 |_| "HYPERLIQUID_PK environment variable not set. Example: export HYPERLIQUID_PK=0x...",
101 )?;
102 let private_key = EvmPrivateKey::new(private_key_str)?;
103 let signer = HyperliquidEip712Signer::new(private_key);
104
105 let action_json = serde_json::to_value(&action)?;
107
108 let nonce = TimeNonce::now_millis();
110
111 let sign_request = SignRequest {
113 action: action_json,
114 time_nonce: nonce,
115 action_type: HyperliquidActionType::UserSigned,
116 };
117
118 let signature_bundle = signer.sign(&sign_request)?;
119
120 let sig = signature_bundle.signature;
123 if sig.len() != 132 || !sig.starts_with("0x") {
124 return Err(format!("Invalid signature format: {}", sig).into());
125 }
126
127 let signature = SignatureData {
128 r: format!("0x{}", &sig[2..66]), s: format!("0x{}", &sig[66..130]), v: format!("0x{}", &sig[130..132]), };
132
133 tracing::info!(component = "ws_post", "action signed successfully");
134
135 let payload = ActionPayload {
136 action,
137 nonce: nonce.as_millis() as u64,
138 signature,
139 vault_address: None,
140 };
141
142 match client
143 .post_action_raw(payload, Duration::from_secs(2))
144 .await
145 {
146 Ok(resp) => tracing::info!(component = "ws_post", ?resp, "action response (success)"),
147 Err(e) => {
148 tracing::warn!(component = "ws_post", error = %e, "action failed")
149 }
150 }
151
152 Ok(())
153}