From 13a16812f5efe39f79cae5e61b99b8aee40916d0 Mon Sep 17 00:00:00 2001 From: Elijah Hampton Date: Sat, 27 Dec 2025 20:50:50 -0500 Subject: [PATCH 1/8] Correct param types to signing funcs --- assets/rhyperliquid.png | Bin 0 -> 100643 bytes src/api/exchange.rs | 44 ++++++++++++++++++++-------------------- src/bin/cli.rs | 37 +++++++++++++++++++++++---------- src/cli/mod.rs | 35 +++++++++++++++++++++++++++++++- src/signature/sign.rs | 6 +++--- src/types/info/spot.rs | 4 ++-- src/types/info/user.rs | 32 ++++++++++++++--------------- 7 files changed, 103 insertions(+), 55 deletions(-) create mode 100644 assets/rhyperliquid.png diff --git a/assets/rhyperliquid.png b/assets/rhyperliquid.png new file mode 100644 index 0000000000000000000000000000000000000000..8ffaf5fd47e51dd92f3cfb4ef6b2b9db916b7ca2 GIT binary patch literal 100643 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

9#8#B zIcj$-2XslB(ZN(+9(xVB66H~Vsp=&Mx=*4QWJ==>*JPt#H^44)=WJ|aX=_W>VlnvR%qA?n=Jp zpu>F5{X54l^IIMpmdq|Oukixqb*C@;|3&Sr!OD0|+`Aw^iQCQ@YYz}_a3>|qM5b;m z%;YTkyg#oe5inK(Z&@W8agTfOmbn80@sVi#G-nT`K-|$!svCgS;Ytl-L|ghZMiSlu z+T&g=uhuv{wG{Q=TLYQF-SKKm26CML{3!|@1beTJL4r4hLX3F@wL3l&PQt2&CMY%I z-EuRg$o$C@(rfwCVI?S^_6PDS{q(sX6q8YrfH||x+%~;fc%(wTH`&P`HGHWt&e0m?xyJIY zq>)sU9ALkkLK3xJ0CevnJbFB0L+sHOaqVN8u(me9X|p1R(6%)$=HLJM>$i-&uD!dV zDsV?m{=o#{N&F<~q&2j#J?I7MDUp7wWDxT%n=`#E6A}?&5EXZKCUS_j_2)kYm+|lr zQQlKinY%iJ_{2KUi<}xdSXYv=HOT50vBq{aslsTe+dif@1rBb@NoMocJ-*m-@2*L| zZYa`o4kn~zs;8&-D@9Ay>nBNt;oB7Mw<=#RJHD<+WxH$aKcT0Aqki-Iq$;M94br8? z4@RRjxB@3VhXJ<}eDn5UuB1~HqKqsw##GnW*JoI%cxuD?_?gL-Dtcn0eV#QSIg8oK z%(gLaAAFH<%h=+{80eGyz9It3SJF>HejbH+qV>jyMxHoXhv*QmD*AvA+z5ueK6q@2 zvl}}wH7YO2vuqW@NInb%!TSLZcpCy?dVcvj7_MIjS<`=pf0p3?ybF%u87hbGLXO=3 zZw~j<>D-6A8*=Vn3BiAc!Egoy!nDq$T(ef87J+72uz;^8fSUa_O23bA_l`s?rFXOtWSAqgjp4=1KfjV>a4Mwdv?ee{`LA0h-0drOYE9iGJcN&BV} w30HR&w(H=<^mq34QP^Ix{X6m%Q8b0&RaL^#f9lp(@D}8OnvQDm{bz6g1LJq~00000 literal 0 HcmV?d00001 diff --git a/src/api/exchange.rs b/src/api/exchange.rs index ad7fa64..23c9366 100644 --- a/src/api/exchange.rs +++ b/src/api/exchange.rs @@ -94,7 +94,7 @@ impl<'client> ExchangeApi<'client> { order: OrderRequest, grouping: Grouping, builder: Option, - vault_address: Option

, + vault_address: Option, expires_after: Option, ) -> Result { self.bulk_orders(vec![order], grouping, builder, vault_address, expires_after) @@ -113,7 +113,7 @@ impl<'client> ExchangeApi<'client> { orders: Vec, grouping: Grouping, builder: Option, - vault_address: Option
, + vault_address: Option, expires_after: Option, ) -> Result { let signer = self @@ -130,7 +130,7 @@ impl<'client> ExchangeApi<'client> { builder, }; - let sig = sign_l1_action(&signer, &action, vault_address, nonce, expires_after, false)?; + let sig = sign_l1_action(&signer, &action, vault_address.clone(), nonce, expires_after, false)?; let signature = Eip712Signature { r: format!("0x{:x}", sig.r()), @@ -171,7 +171,7 @@ impl<'client> ExchangeApi<'client> { pub async fn cancel_order( &self, cancel: CancelRequest, - vault_address: Option
, + vault_address: Option, expires_after: Option, ) -> Result { self.bulk_cancel_orders(vec![cancel], vault_address, expires_after) @@ -188,7 +188,7 @@ impl<'client> ExchangeApi<'client> { pub async fn bulk_cancel_orders( &self, cancels: Vec, - vault_address: Option
, + vault_address: Option, expires_after: Option, ) -> Result { let signer = self @@ -203,7 +203,7 @@ impl<'client> ExchangeApi<'client> { let nonce = current_time_millis(); - let sig = sign_l1_action(&signer, &action, vault_address, nonce, expires_after, false)?; + let sig = sign_l1_action(&signer, &action, vault_address.clone(), nonce, expires_after, false)?; let signature = Eip712Signature { r: format!("0x{:x}", sig.r()), @@ -247,7 +247,7 @@ impl<'client> ExchangeApi<'client> { pub async fn schedule_cancel( &self, time: Option, - vault_address: Option
, + vault_address: Option, expires_after: Option, ) -> Result { let signer = self @@ -265,7 +265,7 @@ impl<'client> ExchangeApi<'client> { let sig = sign_l1_action( &signer, &action, - vault_address, + vault_address.clone(), nonce, expires_after, self.client.is_mainnet(), @@ -311,7 +311,7 @@ impl<'client> ExchangeApi<'client> { pub async fn modify_an_order( &self, request: ModifyRequest, - vault_address: Option
, + vault_address: Option, expires_after: Option, ) -> Result { let signer = self @@ -330,7 +330,7 @@ impl<'client> ExchangeApi<'client> { let sig = sign_l1_action( &signer, &action, - vault_address, + vault_address.clone(), nonce, expires_after, self.client.is_mainnet(), @@ -375,7 +375,7 @@ impl<'client> ExchangeApi<'client> { pub async fn modify_multiple_orders( &self, requests: Vec, - vault_address: Option
, + vault_address: Option, expires_after: Option, ) -> Result { let signer = self @@ -393,7 +393,7 @@ impl<'client> ExchangeApi<'client> { let sig = sign_l1_action( &signer, &action, - vault_address, + vault_address.clone(), nonce, expires_after, self.client.is_mainnet(), @@ -442,7 +442,7 @@ impl<'client> ExchangeApi<'client> { asset: u32, is_buy: bool, ntli: u32, - vault_address: Option
, + vault_address: Option, expires_after: Option, ) -> Result { let signer = self @@ -462,7 +462,7 @@ impl<'client> ExchangeApi<'client> { let sig = sign_l1_action( &signer, &action, - vault_address, + vault_address.clone(), nonce, expires_after, self.client.is_mainnet(), @@ -511,7 +511,7 @@ impl<'client> ExchangeApi<'client> { asset: u32, is_cross: bool, leverage: u32, - vault_address: Option
, + vault_address: Option, expires_after: Option, ) -> Result { let signer = self @@ -531,7 +531,7 @@ impl<'client> ExchangeApi<'client> { let sig = sign_l1_action( &signer, &action, - vault_address, + vault_address.clone(), nonce, expires_after, self.client.is_mainnet(), @@ -724,7 +724,7 @@ impl<'client> ExchangeApi<'client> { destination_dex: Address, token: String, amount: String, - from_sub_account: Option
, + from_sub_account: Option, ) -> Result { let signer = self .client @@ -904,7 +904,7 @@ impl<'client> ExchangeApi<'client> { /// * `expires_after` - Optional expiration timestamp in milliseconds pub async fn deposit_or_withdraw_from_a_vault( &self, - vault_address: Address, + vault_address: String, is_deposit: bool, usd_amount: u64, expires_after: Option, @@ -1064,7 +1064,7 @@ impl<'client> ExchangeApi<'client> { pub async fn place_twap_order( &self, request: TwapRequest, - vault_address: Option
, + vault_address: Option, expires_after: Option, ) -> Result { let signer = self @@ -1082,7 +1082,7 @@ impl<'client> ExchangeApi<'client> { let sig = sign_l1_action( &signer, &action, - vault_address, + vault_address.clone(), nonce, expires_after, self.client.is_mainnet(), @@ -1129,7 +1129,7 @@ impl<'client> ExchangeApi<'client> { &self, asset: usize, twap_id: u32, - vault_address: Option
, + vault_address: Option, expires_after: Option, ) -> Result { let signer = self @@ -1148,7 +1148,7 @@ impl<'client> ExchangeApi<'client> { let sig = sign_l1_action( &signer, &action, - vault_address, + vault_address.clone(), nonce, expires_after, self.client.is_mainnet(), diff --git a/src/bin/cli.rs b/src/bin/cli.rs index e4571d3..ca599e6 100644 --- a/src/bin/cli.rs +++ b/src/bin/cli.rs @@ -4,30 +4,25 @@ use clap::{Parser, Subcommand}; use rhyperliquid::{ cli::{Cli, Commands}, init_tracing::init_tracing, - types::info::user::CandleSnapshotRequest, + types::{info::user::CandleSnapshotRequest, ws::SubscriptionResponse}, HyperliquidClientBuilder, }; use std::env; #[tokio::main] async fn main() -> Result<(), Box> { - #[cfg(feature = "cli")] init_tracing(); - #[cfg(feature = "cli")] let cli = Cli::parse(); #[allow(clippy::expect_used)] - #[cfg(feature = "cli")] let signer = env::var("HL_PRIVATE_KEY").expect("HL_PRIVATE_KEY env var is missing"); - #[cfg(feature = "cli")] let mut hyperliquid = &mut HyperliquidClientBuilder::new(); // Check if the user provided a network, default to testnet - #[cfg(feature = "cli")] if let Some(network) = cli.network { - match network.to_lowercase().as_str() { + match (network as String).to_lowercase().as_str() { "testnet" => { hyperliquid = hyperliquid.testnet(); } @@ -41,24 +36,30 @@ async fn main() -> Result<(), Box> { } // Check if user provides permission to check env var for signer key - #[cfg(feature = "cli")] + if let Some(signer_permission) = cli.allow_signer_key_env { if signer_permission { hyperliquid = hyperliquid.with_wallet(LocalSigner::from_slice(signer.as_bytes())?); } } - #[cfg(feature = "cli")] let client = hyperliquid.build()?; - #[cfg(feature = "cli")] + let info_api = &client.info(); - #[cfg(feature = "cli")] + let mut subs = client.subscriptions().await?; + let mut events = subs.events; + match cli.command { Commands::AllMids { dex } => { let all_mids = info_api.all_mids(dex).await?; tracing::info!("{:?}", all_mids); } + Commands::SubscribeAllMids { dex } => { + while let Ok(SubscriptionResponse::AllMids(ws_all_mids)) = events.recv().await { + tracing::info!("{}", serde_json::to_string_pretty(&ws_all_mids)?); + } + } Commands::OpenOrders { user, dex } => { let open_orders = info_api.open_orders(&user, dex.as_deref()).await?; tracing::info!("{:?}", open_orders); @@ -107,6 +108,15 @@ async fn main() -> Result<(), Box> { .await?; tracing::info!("{:?}", l2_book); } + Commands::SubscribeL2Book { + coin, + n_sig_figs, + mantissa, + } => { + while let Ok(SubscriptionResponse::L2Book(ws_l2_book)) = events.recv().await { + tracing::info!("{}", serde_json::to_string_pretty(&ws_l2_book)?); + } + } Commands::CandleSnapshot { coin, interval, @@ -124,6 +134,11 @@ async fn main() -> Result<(), Box> { tracing::info!("{:?}", snapshot); } + Commands::SubscribeCandleSnapshot { coin, interval } => { + while let Ok(SubscriptionResponse::Candle(ws_candle)) = events.recv().await { + tracing::info!("{}", serde_json::to_string_pretty(&ws_candle)?); + } + } Commands::HistoricalOrders { user } => { let historical_orders = info_api.historical_orders(&user).await?; tracing::info!("{:?}", historical_orders); diff --git a/src/cli/mod.rs b/src/cli/mod.rs index d461700..945b083 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,4 +1,6 @@ use clap::{Parser, Subcommand}; +#[allow(unused_imports)] +use crate::types::exchange::OrderRequest; #[derive(Subcommand)] pub enum Commands { @@ -6,6 +8,10 @@ pub enum Commands { #[arg(short, long)] dex: Option, }, + SubscribeAllMids { + #[arg(long)] + dex: Option, + }, OpenOrders { #[arg(short, long)] user: String, @@ -52,6 +58,14 @@ pub enum Commands { #[arg(short, long)] mantissa: Option, }, + SubscribeL2Book { + #[arg(short, long)] + coin: String, + #[arg(short, long)] + n_sig_figs: Option, + #[arg(short, long)] + mantissa: Option, + }, CandleSnapshot { #[arg(short, long)] coin: String, @@ -62,6 +76,12 @@ pub enum Commands { #[arg(short, long)] end_time: u64, }, + SubscribeCandleSnapshot { + #[arg(short, long)] + coin: String, + #[arg(short, long)] + interval: String, + }, HistoricalOrders { #[arg(short, long)] user: String, @@ -95,7 +115,20 @@ pub enum Commands { UserFees { #[arg(short, long)] user: String, - }, + } + // PlaceOrder { + // /// An array of [`OrderRequests`] + // #[arg(short, long)] + // requests: serde_json::Value, + // #[arg(short, long)] + // grouping: String, // `na`, `ntpsl`, `ptpsl` + // #[arg(short, long)] + // builder: String, + // #[arg(short, long)] + // vault_address: Option, + // #[arg(short, long)] + // expires_after: u64, + // }, } #[derive(Parser)] diff --git a/src/signature/sign.rs b/src/signature/sign.rs index e7c391e..4c99b61 100644 --- a/src/signature/sign.rs +++ b/src/signature/sign.rs @@ -45,7 +45,7 @@ where pub fn sign_l1_action( wallet: &PrivateKeySigner, action: &impl Serialize, - vault_address: Option
, + vault_address: Option, nonce: u64, expires_after: Option, is_mainnet: bool, @@ -114,7 +114,7 @@ pub fn sign_l1_action( /// Returns the resulting `B256` hash. fn action_hash( action: &impl Serialize, - vault_address: Option
, + vault_address: Option, nonce: u64, expires_after: Option, ) -> Result { @@ -124,7 +124,7 @@ fn action_hash( if let Some(addr) = vault_address { data.push(0x01); - data.extend_from_slice(addr.as_slice()); + data.extend_from_slice(addr.as_bytes()); } else { data.push(0x00); } diff --git a/src/types/info/spot.rs b/src/types/info/spot.rs index 05ae0c1..1b9f0c8 100644 --- a/src/types/info/spot.rs +++ b/src/types/info/spot.rs @@ -7,13 +7,13 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct EvmContract { - address: String, + pub address: String, #[serde( default, alias = "evm_extra_wei_decimals", alias = "evmExtraWeiDecimals" )] - evm_extra_wei_decimals: i64, + pub evm_extra_wei_decimals: i64, } /// Response type for POST /info with type "spotMeta" diff --git a/src/types/info/user.rs b/src/types/info/user.rs index 0ce9880..48e4e8c 100644 --- a/src/types/info/user.rs +++ b/src/types/info/user.rs @@ -254,17 +254,17 @@ pub enum OrderWithStatus { /// Represents a Bid or Ask in the [`L2BookSnapshot`]. #[derive(Debug, Serialize, Deserialize)] pub struct BidOrAsk { - px: rust_decimal::Decimal, - sz: rust_decimal::Decimal, - n: u64, + pub px: rust_decimal::Decimal, + pub sz: rust_decimal::Decimal, + pub n: u64, } /// Response for request with type "l2Book" #[derive(Debug, Serialize, Deserialize)] pub struct L2BookSnapshot { - coin: String, - time: u64, - levels: (Vec, Vec), + pub coin: String, + pub time: u64, + pub levels: (Vec, Vec), } #[derive(Debug, Serialize, Deserialize)] @@ -280,16 +280,16 @@ pub struct CandleSnapshotRequest { #[allow(nonstandard_style)] #[derive(Debug, Serialize, Deserialize)] pub struct Candle { - T: u64, - c: String, - h: String, - i: String, - l: String, - n: u64, - o: String, - s: String, - t: u64, - v: String, + pub T: u64, + pub c: String, + pub h: String, + pub i: String, + pub l: String, + pub n: u64, + pub o: String, + pub s: String, + pub t: u64, + pub v: String, } pub type CandleSnapshot = Vec; From 66d62d6b420dde3ed77f277699b39604701957a2 Mon Sep 17 00:00:00 2001 From: Elijah Hampton Date: Sun, 28 Dec 2025 15:33:25 -0500 Subject: [PATCH 2/8] Updateds tracing to log INFO level traces only. Adds subscriptions commands and operations to CLI. Updates CLI documentation and feature flags. --- README.md | 236 +++++++++++++++++++++++---------- src/api/exchange.rs | 18 ++- src/api/subscription/mod.rs | 1 - src/api/subscription/sender.rs | 70 ---------- src/api/subscription/ws.rs | 10 +- src/bin/cli.rs | 180 +++++++++++++++++++++---- src/cli/mod.rs | 81 ++++++++--- src/init_tracing.rs | 2 +- 8 files changed, 406 insertions(+), 192 deletions(-) delete mode 100644 src/api/subscription/sender.rs diff --git a/README.md b/README.md index 8d7f95d..db3db31 100644 --- a/README.md +++ b/README.md @@ -6,23 +6,12 @@ A Rust SDK for [Hyperliquid](https://hyperliquid.xyz), the high-performance perp ## Features -- Market data queries (spot & perpetuals) -- Account information retrieval -- Order book snapshots -- Historical candle data -- User fills and funding history -- Order placement and cancellation -- Position management -- Vault operations -- Command-line interface for quick queries - -## Project Status - -- [x] Complete REST Info API (market data) -- [x] EIP-712 authentication & message signing -- [x] Exchange API (order placement, cancellation) -- [x] WebSocket streaming (real-time data feeds) -- [x] CLI for market data and account queries +- **Market Data**: Real-time and historical price data, orderbooks, candles +- **Account Management**: Positions, balances, fills, funding history +- **Trading Operations**: Order placement, cancellation, modification, leverage management +- **Vault Operations**: Vault queries and equity tracking +- **WebSocket Streaming**: Low-latency real-time data feeds with automatic reconnection +- **CLI Interface**: Production-ready command-line tool for traders and developers. Currently the CLI does not support queries from the Exchange API. ## Quick Start @@ -81,56 +70,129 @@ cargo run --bin cli --features=cli -- candle-snapshot \ cargo run --bin cli --features=cli -- vault-details --vault-address 0x... ``` -#### CLI Options - -**Global Flags:** -- `--network ` - Network to connect to (default: mainnet) -- `--allow-signer-key-env` - Allow reading private key from `HL_PRIVATE_KEY` environment variable - -**Available Commands:** -- `all-mids` - Get mid prices for all assets -- `open-orders` - Get user's open orders -- `frontend-open-orders` - Get frontend-formatted open orders -- `user-fills` - Get user's fill history -- `user-fills-by-time` - Get fills within time range -- `user-rate-limit` - Check user's rate limit status -- `order-status` - Get status of specific order -- `l2-book` - Get L2 orderbook snapshot -- `candle-snapshot` - Get historical candle data -- `historical-orders` - Get user's historical orders -- `sub-accounts` - Get user's sub-accounts -- `vault-details` - Get vault information -- `user-vault-equities` - Get user's vault equity -- `user-role` - Get user's role information -- `portfolio` - Get user's portfolio -- `referral` - Get user's referral information -- `user-fees` - Get user's fee information +### WebSocket Subscriptions -Use `--help` on any command for detailed parameter information: +Subscribe to real-time data feeds using the WebSocket interface: ```bash -cargo run --bin cli --features=cli -- l2-book --help +# Subscribe to open orders updates (requires HL_PRIVATE_KEY env var) +export HL_PRIVATE_KEY=your_private_key_here +RUST_LOG=info cargo run --bin cli --features=cli -- \ + --subscriptions true \ + subscribe-open-orders \ + --user 0xYourAddress + +# Subscribe to order book updates +cargo run --bin cli --features=cli -- \ + --subscriptions true \ + subscribe-l2-book \ + --coin BTC + +# Subscribe to all mid prices +cargo run --bin cli --features=cli -- \ + --subscriptions true \ + subscribe-all-mids + +# Subscribe to trades +cargo run --bin cli --features=cli -- \ + --subscriptions true \ + subscribe-trades \ + --coin ETH ``` -## Trading - -For order placement and cancellation examples, see [`examples/basic_order.rs`](examples/basic_order.rs). - -For account transfer examples, see [`examples/account_transfer.rs`](examples/account_transfer.rs). +WebSocket subscriptions stream live updates until interrupted with `Ctrl+C`. Use `RUST_LOG=info` to see subscription confirmations and data updates. + +## CLI Reference + +### Global Options + +| Flag | Description | Default | +|------|-------------|---------| +| `--network ` | Network to connect to | `mainnet` | +| `--allow-signer-key-env` | Allow reading private key from `HL_PRIVATE_KEY` | `false` | +| `--subscriptions ` | Enable WebSocket subscription mode | `false` | + +### Commands + +#### Market Data + +| Command | Description | +|---------|-------------| +| `all-mids` | Get mid prices for all assets | +| `l2-book` | Get L2 orderbook snapshot | +| `candle-snapshot` | Get historical candle data | + +#### Account & Portfolio + +| Command | Description | +|---------|-------------| +| `open-orders` | Get user's open orders | +| `frontend-open-orders` | Get frontend-formatted open orders | +| `user-fills` | Get user's fill history | +| `user-fills-by-time` | Get fills within time range | +| `user-rate-limit` | Check user's rate limit status | +| `order-status` | Get status of specific order | +| `historical-orders` | Get user's historical orders | +| `portfolio` | Get user's portfolio | +| `user-fees` | Get user's fee information | +| `sub-accounts` | Get user's sub-accounts | +| `user-role` | Get user's role information | +| `referral` | Get user's referral information | + +#### Vault Operations + +| Command | Description | +|---------|-------------| +| `vault-details` | Get vault information | +| `user-vault-equities` | Get user's vault equity | + +#### WebSocket Subscriptions + +**Market Data Streams** + +| Command | Description | +|---------|-------------| +| `subscribe-all-mids` | Stream all mid prices | +| `subscribe-l2-book` | Stream orderbook updates | +| `subscribe-candle-snapshot` | Stream candle updates | +| `subscribe-active-asset-ctx` | Stream active asset context | +| `subscribe-bbo` | Stream best bid/offer updates | + +**Account & Trading Streams** + +| Command | Description | +|---------|-------------| +| `subscribe-open-orders` | Stream open orders updates | +| `subscribe-user-events` | Stream user trading events | +| `subscribe-user-fills` | Stream user fill updates | +| `subscribe-user-funding` | Stream user funding updates | +| `subscribe-user-non-funding-ledger-updates` | Stream non-funding ledger updates | +| `subscribe-active-asset-data` | Stream active asset data for user | + +**Advanced Streams** + +| Command | Description | +|---------|-------------| +| `subscribe-notifications` | Stream user notifications | +| `subscribe-web-data3` | Stream web data updates | +| `subscribe-twap-states` | Stream TWAP order states | +| `subscribe-clearinghouse-state` | Stream clearinghouse state | +| `subscribe-user-twap-slice-fills` | Stream TWAP slice fills | +| `subscribe-user-twap-history` | Stream TWAP order history | -For position leverage and managing an isolated position see [`examples/leverage_position.rs`](examples/leverage_position.rs). - -For advanced order examples see [`examples/advanced_order.rs`](examples/advanced_order.rs). - -For TWAP order placement see [`examples/twap_order.rs`](examples/twap_order.rs). - -## Stability and API Guarantees -This crate is under active development. +Use `--help` on any command for detailed parameter information: +```bash +cargo run --bin cli --features=cli -- l2-book --help +``` -**Breaking changes** may occur between minor versions (0.1 -> 0.2). +## Trading Examples -**Public API** is subject to refinement based on usage feedback. +Comprehensive examples demonstrating real trading workflows: -**Testnet testing** is strongly recommended before mainnet use. +- **[`basic_order.rs`](examples/basic_order.rs)** - Order placement and cancellation +- **[`account_transfer.rs`](examples/account_transfer.rs)** - Account transfers +- **[`leverage_position.rs`](examples/leverage_position.rs)** - Position leverage management +- **[`advanced_order.rs`](examples/advanced_order.rs)** - Advanced order types +- **[`twap_order.rs`](examples/twap_order.rs)** - TWAP order placement ## Installation @@ -139,26 +201,22 @@ This crate is under active development. Add to your `Cargo.toml`: ```toml [dependencies] -rhyperliquid = "0.1" +rhyperliquid = "0.2" tokio = { version = "1.41", features = ["full"] } ``` ### CLI Installation ```bash -# Install from source with CLI support -cargo install --path . --features=cli --bin cli +# Install from crates.io +cargo install rhyperliquid --features=cli -# Or clone and build +# Or install from source git clone https://github.com/elijahhampton/rhyperliquid.git cd rhyperliquid -cargo build --release --features=cli --bin cli - -# Binary will be at target/release/cli +cargo install --path . --features=cli --bin cli ``` -### From Source - -Clone and build the repository: +### Building from Source ```bash # Clone the repository git clone https://github.com/elijahhampton/rhyperliquid.git @@ -177,13 +235,30 @@ cargo test --all-features cargo doc --open ``` +## Stability and API Guarantees + +This crate is under active development. + +- **Breaking changes** may occur between minor versions (0.1 → 0.2) +- **Public API** is subject to refinement based on usage feedback +- **Testnet testing** is strongly recommended before mainnet use + +## Documentation + +- [API Documentation](https://docs.rs/rhyperliquid) +- [Hyperliquid Official Docs](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api) +- [Examples](./examples) + ## Getting Help -If you have any questions, first see if the answer to your question can be found in the [Hyperliquid Docs](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api). -If the answer is not there: +If you have questions: -- Open a discussion with your question, or -- Open an issue with the bug +1. Check the [Hyperliquid API Documentation](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api) +2. Search existing [GitHub Issues](https://github.com/elijahhampton/rhyperliquid/issues) +3. Open a new [Discussion](https://github.com/elijahhampton/rhyperliquid/discussions) for questions +4. Open an [Issue](https://github.com/elijahhampton/rhyperliquid/issues/new) for bugs + +## Requirements ### Minimum Supported Rust Version (MSRV) @@ -195,3 +270,18 @@ rustc --version # Update if needed rustup update stable ``` + +## License + +Licensed under either of: + +- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +## Contributing + +Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/src/api/exchange.rs b/src/api/exchange.rs index 23c9366..90f4c30 100644 --- a/src/api/exchange.rs +++ b/src/api/exchange.rs @@ -130,7 +130,14 @@ impl<'client> ExchangeApi<'client> { builder, }; - let sig = sign_l1_action(&signer, &action, vault_address.clone(), nonce, expires_after, false)?; + let sig = sign_l1_action( + &signer, + &action, + vault_address.clone(), + nonce, + expires_after, + false, + )?; let signature = Eip712Signature { r: format!("0x{:x}", sig.r()), @@ -203,7 +210,14 @@ impl<'client> ExchangeApi<'client> { let nonce = current_time_millis(); - let sig = sign_l1_action(&signer, &action, vault_address.clone(), nonce, expires_after, false)?; + let sig = sign_l1_action( + &signer, + &action, + vault_address.clone(), + nonce, + expires_after, + false, + )?; let signature = Eip712Signature { r: format!("0x{:x}", sig.r()), diff --git a/src/api/subscription/mod.rs b/src/api/subscription/mod.rs index de20205..5845388 100644 --- a/src/api/subscription/mod.rs +++ b/src/api/subscription/mod.rs @@ -1,4 +1,3 @@ -mod sender; mod ws; pub use ws::{StreamMessage, SubscriptionClient, SubscriptionConfig}; diff --git a/src/api/subscription/sender.rs b/src/api/subscription/sender.rs deleted file mode 100644 index f79a301..0000000 --- a/src/api/subscription/sender.rs +++ /dev/null @@ -1,70 +0,0 @@ -use crate::types::ws::{ - WsActiveAssetCtx, WsActiveAssetData, WsAllMids, WsBbo, WsBook, WsCandle, WsClearinghouseState, - WsNotification, WsOpenOrders, WsTrade, WsTwapStates, WsUserEvent, WsUserFills, WsUserFundings, - WsUserNonFundingLedgerUpdate, WsUserTwapHistory, WsUserTwapSliceFills, WsWebData3, -}; -use tokio::sync::broadcast::Sender; - -/// A mapping of subscription identifiers to sender channels and subscription -/// parameters. -#[derive(Clone, Default)] -pub struct StreamSenders { - pub(crate) all_mids: (Option>, Option), - pub(crate) candle: (Option>, Option, Option), - pub(crate) trades: ( - Option>, - Option, - Option, - Option, - ), - pub(crate) l2book: ( - Option>, - Option, - Option, - Option, - ), - pub(crate) notifications: (Option>, Option), - pub(crate) webdata3: (Option>, Option), - pub(crate) twap_states: (Option>, Option), - pub(crate) clearinghouse_state: (Option>, Option), - pub(crate) open_orders: (Option>, Option), - pub(crate) user_events: (Option>, Option), - pub(crate) user_fills: (Option>, Option), - pub(crate) user_funding: (Option>, Option), - pub(crate) user_non_funding_ledger_updates: - (Option>, Option), - pub(crate) active_asset_ctx: (Option>, Option), - pub(crate) active_asset_data: ( - Option>, - Option, - Option, - ), - pub(crate) user_twap_slice_fills: (Option>, Option), - pub(crate) user_twap_history: (Option>, Option), - pub(crate) bbo: (Option>, Option), -} - -impl StreamSenders { - pub fn new() -> Self { - Self { - all_mids: (None, None), - candle: (None, None, None), - trades: (None, None, None, None), - l2book: (None, None, None, None), - notifications: (None, None), - webdata3: (None, None), - twap_states: (None, None), - clearinghouse_state: (None, None), - open_orders: (None, None), - user_events: (None, None), - user_fills: (None, None), - user_funding: (None, None), - user_non_funding_ledger_updates: (None, None), - active_asset_ctx: (None, None), - active_asset_data: (None, None, None), - user_twap_slice_fills: (None, None), - user_twap_history: (None, None), - bbo: (None, None), - } - } -} diff --git a/src/api/subscription/ws.rs b/src/api/subscription/ws.rs index 04bc320..9840b0e 100644 --- a/src/api/subscription/ws.rs +++ b/src/api/subscription/ws.rs @@ -52,8 +52,8 @@ pub enum SubscriptionSpec { }, L2Book { coin: String, - n_sig_figs: Option, - mantissa: Option, + n_sig_figs: Option, + mantissa: Option, }, Trades { coin: String, @@ -514,7 +514,7 @@ impl SubscriptionClient { /// /// # Returns /// A bounded `tokio::sync::broadcast::Sender` - pub async fn subscribe_candle( + pub async fn subscribe_candle_snapshot( &mut self, coin: impl Into + Serialize + Clone, interval: String, @@ -564,8 +564,8 @@ impl SubscriptionClient { pub async fn subscribe_l2_book( &mut self, coin: impl Into + Serialize, - n_sig_figs: Option, - mantissa: Option, + n_sig_figs: Option, + mantissa: Option, ) -> Result<()> { let coin = coin.into(); let spec = SubscriptionSpec::L2Book { diff --git a/src/bin/cli.rs b/src/bin/cli.rs index ca599e6..d927c5e 100644 --- a/src/bin/cli.rs +++ b/src/bin/cli.rs @@ -17,7 +17,6 @@ async fn main() -> Result<(), Box> { #[allow(clippy::expect_used)] let signer = env::var("HL_PRIVATE_KEY").expect("HL_PRIVATE_KEY env var is missing"); - let mut hyperliquid = &mut HyperliquidClientBuilder::new(); // Check if the user provided a network, default to testnet @@ -35,8 +34,13 @@ async fn main() -> Result<(), Box> { hyperliquid = hyperliquid.testnet(); } - // Check if user provides permission to check env var for signer key + if let Some(subscriptions) = cli.subscriptions { + if subscriptions == true { + hyperliquid = hyperliquid.with_subscriptions(); + } + } + // Check if user provides permission to check env var for signer key if let Some(signer_permission) = cli.allow_signer_key_env { if signer_permission { hyperliquid = hyperliquid.with_wallet(LocalSigner::from_slice(signer.as_bytes())?); @@ -44,31 +48,26 @@ async fn main() -> Result<(), Box> { } let client = hyperliquid.build()?; - let info_api = &client.info(); - let mut subs = client.subscriptions().await?; - let mut events = subs.events; match cli.command { Commands::AllMids { dex } => { let all_mids = info_api.all_mids(dex).await?; tracing::info!("{:?}", all_mids); - } - Commands::SubscribeAllMids { dex } => { - while let Ok(SubscriptionResponse::AllMids(ws_all_mids)) = events.recv().await { - tracing::info!("{}", serde_json::to_string_pretty(&ws_all_mids)?); - } + return Ok(()); } Commands::OpenOrders { user, dex } => { let open_orders = info_api.open_orders(&user, dex.as_deref()).await?; tracing::info!("{:?}", open_orders); + return Ok(()); } Commands::FrontendOpenOrders { user, dex } => { let frontend_open_orders = info_api .open_orders_with_additional_info(&user, dex.as_deref()) .await?; tracing::info!("{:?}", frontend_open_orders); + return Ok(()); } Commands::UserFills { user, @@ -76,6 +75,7 @@ async fn main() -> Result<(), Box> { } => { let user_fills = info_api.fills(&user, aggregate_by_time).await?; tracing::info!({"{:?}", user_fills}); + return Ok(()); } Commands::UserFillsByTime { user, @@ -87,16 +87,19 @@ async fn main() -> Result<(), Box> { .fills_by_time(&user, start_time, end_time, aggregate_by_time) .await?; tracing::info!("{:?}", fills_by_time); + return Ok(()); } Commands::UserRateLimit { user } => { let user_rate_limit = info_api.rate_limits(&user).await?; tracing::info!("{:?}", user_rate_limit); + return Ok(()); } Commands::OrderStatus { user, oid } => { let order_status = info_api .order_status(&user, rhyperliquid::types::info::OrderId::Numeric(oid)) .await?; tracing::info!("{:?}", order_status); + return Ok(()); } Commands::L2Book { coin, @@ -107,15 +110,7 @@ async fn main() -> Result<(), Box> { .l2_book_snapshot(&coin, n_sig_figs, mantissa) .await?; tracing::info!("{:?}", l2_book); - } - Commands::SubscribeL2Book { - coin, - n_sig_figs, - mantissa, - } => { - while let Ok(SubscriptionResponse::L2Book(ws_l2_book)) = events.recv().await { - tracing::info!("{}", serde_json::to_string_pretty(&ws_l2_book)?); - } + return Ok(()); } Commands::CandleSnapshot { coin, @@ -133,15 +128,12 @@ async fn main() -> Result<(), Box> { .await?; tracing::info!("{:?}", snapshot); - } - Commands::SubscribeCandleSnapshot { coin, interval } => { - while let Ok(SubscriptionResponse::Candle(ws_candle)) = events.recv().await { - tracing::info!("{}", serde_json::to_string_pretty(&ws_candle)?); - } + return Ok(()); } Commands::HistoricalOrders { user } => { let historical_orders = info_api.historical_orders(&user).await?; tracing::info!("{:?}", historical_orders); + return Ok(()); } Commands::SubAccounts { user } => { let sub_accounts = info_api.subaccounts(&user).await?; @@ -150,6 +142,7 @@ async fn main() -> Result<(), Box> { } else { tracing::info!("User {:?} does not have subaccounts.", user); } + return Ok(()); } Commands::VaultDetails { vault_address, @@ -159,28 +152,165 @@ async fn main() -> Result<(), Box> { .vault_details(&vault_address, user.as_deref()) .await?; tracing::info!("{:?}", vault_details); + return Ok(()); } Commands::UserVaultEquities { user } => { let equities = info_api.vault_deposits(&user).await?; tracing::info!("{:?}", equities); + return Ok(()); } Commands::UserRole { user } => { let role = info_api.role(&user).await?; tracing::info!("{:?}", role); + return Ok(()); } Commands::Portfolio { user } => { let portfolio = info_api.portfolio(&user).await?; tracing::info!("{:?}", portfolio); + return Ok(()); } Commands::Referral { user } => { let referral = info_api.portfolio(&user).await?; tracing::info!("{:?}", referral); + return Ok(()); } Commands::UserFees { user } => { let user_fees = info_api.fees(&user).await?; tracing::info!("{:?}", user_fees); + return Ok(()); + } + Commands::SubscribeOpenOrders { user } => { + subs.subscribe_open_orders(user).await?; + } + Commands::SubscribeAllMids { dex } => { + subs.subscribe_all_mids(dex).await?; + } + Commands::SubscribeL2Book { + coin, + n_sig_figs, + mantissa, + } => { + subs.subscribe_l2_book(coin, n_sig_figs, mantissa).await?; + } + Commands::SubscribeCandleSnapshot { coin, interval } => { + subs.subscribe_candle_snapshot(coin, interval).await?; + } + Commands::SubscribeNotifications { user } => { + subs.subscribe_notifications(user).await?; + } + Commands::SubscribeWebData3 { user } => { + subs.subscribe_webdata3(user).await?; + } + Commands::SubscribeTwapStates { user } => { + subs.subscribe_twap_states(user).await?; + } + Commands::SubscribeClearinghouseState { user } => { + subs.subscribe_clearinghouse_state(user).await?; + } + Commands::SubscribeUserEvents { user } => { + subs.subscribe_user_events(user).await?; + } + Commands::SubscribeUserFills { user } => { + subs.subscribe_user_fills(user).await?; + } + Commands::SubscribeUserFunding { user } => { + subs.subscribe_user_funding(user).await?; + } + Commands::SubscribeUserNonFundingLedgerUpdates { user } => { + subs.subscribe_user_non_funding_ledger_updates(user).await?; + } + Commands::SubscribeActiveAssetCtx { coin } => { + subs.subscribe_active_asset_ctx(coin).await?; + } + Commands::SubscribeActiveAssetData { user, coin } => { + subs.subscribe_active_asset_data(user, coin).await?; + } + Commands::SubscribeUserTwapSliceFills { user } => { + subs.subscribe_user_twap_slice_fills(user).await?; + } + Commands::SubscribeUserTwapHistory { user } => { + subs.subscribe_user_twap_history(user).await?; + } + Commands::SubscribeBbo { user } => { + subs.subscribe_bbo(user).await?; } } - Ok(()) + let mut events = subs.events; + + loop { + match events.recv().await? { + SubscriptionResponse::AllMids(ws_all_mids) => { + tracing::info!("{}", serde_json::to_string_pretty(&ws_all_mids)?); + } + SubscriptionResponse::L2Book(ws_l2_book) => { + tracing::info!("{}", serde_json::to_string_pretty(&ws_l2_book)?); + } + SubscriptionResponse::Candle(ws_candle) => { + tracing::info!("{}", serde_json::to_string_pretty(&ws_candle)?); + } + SubscriptionResponse::Notification(ws_notification) => { + tracing::info!("{}", serde_json::to_string_pretty(&ws_notification)?); + } + SubscriptionResponse::WebData3(ws_webdata3) => { + tracing::info!("{}", serde_json::to_string_pretty(&ws_webdata3)?); + } + SubscriptionResponse::TwapStates(ws_twap_states) => { + tracing::info!("{}", serde_json::to_string_pretty(&ws_twap_states)?); + } + SubscriptionResponse::ClearinghouseState(ws_clearinghouse_state) => { + tracing::info!("{}", serde_json::to_string_pretty(&ws_clearinghouse_state)?); + } + SubscriptionResponse::OpenOrders(ws_open_orders) => { + tracing::info!("{}", serde_json::to_string_pretty(&ws_open_orders)?); + } + SubscriptionResponse::UserEvents(ws_user_events) => { + tracing::info!("{}", serde_json::to_string_pretty(&ws_user_events)?); + } + SubscriptionResponse::UserFills(ws_fills) => { + tracing::info!("{}", serde_json::to_string_pretty(&ws_fills)?); + } + SubscriptionResponse::UserFundings(ws_fundings) => { + tracing::info!("{}", serde_json::to_string_pretty(&ws_fundings)?); + } + SubscriptionResponse::UserNonFundingLedgerUpdates( + ws_user_non_funding_ledger_updates, + ) => { + tracing::info!( + "{}", + serde_json::to_string_pretty(&ws_user_non_funding_ledger_updates)? + ); + } + SubscriptionResponse::ActiveAssetCtx(ws_active_asset_ctx) => { + tracing::info!("{}", serde_json::to_string_pretty(&ws_active_asset_ctx)?); + } + SubscriptionResponse::ActiveAssetData(ws_active_asset_data) => { + tracing::info!("{}", serde_json::to_string_pretty(&ws_active_asset_data)?); + } + SubscriptionResponse::UserTwapSliceFills(ws_user_twap_slice_fills) => { + tracing::info!( + "{}", + serde_json::to_string_pretty(&ws_user_twap_slice_fills)? + ); + } + SubscriptionResponse::UserTwapHistory(ws_user_twap_history) => { + tracing::info!("{}", serde_json::to_string_pretty(&ws_user_twap_history)?); + } + SubscriptionResponse::Bbo(ws_bbo) => { + tracing::info!("{}", serde_json::to_string_pretty(&ws_bbo)?); + } + SubscriptionResponse::Error(ws_error_response) => { + tracing::info!("{:?}", serde_json::to_string_pretty(&ws_error_response)); + } + SubscriptionResponse::SubscriptionResponse(subscription_confirmation) => { + tracing::info!("Subscription confirmed {:?}", subscription_confirmation); + } + SubscriptionResponse::Pong => { + tracing::info!("Received `pong` response from the server"); + } + SubscriptionResponse::Trades(ws_trades) => { + tracing::info!("{:?}", ws_trades); + } + } + } } diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 945b083..0f16dea 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,6 +1,6 @@ -use clap::{Parser, Subcommand}; #[allow(unused_imports)] use crate::types::exchange::OrderRequest; +use clap::{Parser, Subcommand}; #[derive(Subcommand)] pub enum Commands { @@ -115,27 +115,78 @@ pub enum Commands { UserFees { #[arg(short, long)] user: String, - } - // PlaceOrder { - // /// An array of [`OrderRequests`] - // #[arg(short, long)] - // requests: serde_json::Value, - // #[arg(short, long)] - // grouping: String, // `na`, `ntpsl`, `ptpsl` - // #[arg(short, long)] - // builder: String, - // #[arg(short, long)] - // vault_address: Option, - // #[arg(short, long)] - // expires_after: u64, - // }, + }, + SubscribeNotifications { + #[arg(short, long)] + user: String, + }, + SubscribeWebData3 { + #[arg(short, long)] + user: String, + }, + SubscribeTwapStates { + #[arg(short, long)] + user: String, + }, + SubscribeClearinghouseState { + #[arg(short, long)] + user: String, + }, + SubscribeOpenOrders { + #[arg(short, long)] + user: String, + }, + SubscribeUserEvents { + #[arg(short, long)] + user: String, + }, + SubscribeUserFills { + #[arg(short, long)] + user: String, + }, + SubscribeUserFunding { + #[arg(short, long)] + user: String, + }, + SubscribeUserNonFundingLedgerUpdates { + #[arg(short, long)] + user: String, + }, + SubscribeActiveAssetCtx { + #[arg(short, long)] + coin: String, + }, + SubscribeActiveAssetData { + #[arg(short, long)] + user: String, + #[arg(short, long)] + coin: String, + }, + SubscribeUserTwapSliceFills { + #[arg(short, long)] + user: String, + }, + SubscribeUserTwapHistory { + #[arg(short, long)] + user: String, + }, + SubscribeBbo { + #[arg(short, long)] + user: String, + }, } #[derive(Parser)] pub struct Cli { + /// Enables subscriptions through the CLI + #[arg(short, long)] + pub subscriptions: Option, + + /// Picks a specific network, i.e. 'testnet' or 'mainnet' #[arg(short, long)] pub network: Option, + /// Allows CLI to use HL_PRIVATE_KEY env var in configuration (requires HL_PRIVATE_KEY end var) #[arg(short, long)] pub allow_signer_key_env: Option, diff --git a/src/init_tracing.rs b/src/init_tracing.rs index f9cbdf3..a06d266 100644 --- a/src/init_tracing.rs +++ b/src/init_tracing.rs @@ -1,6 +1,6 @@ #[allow(dead_code)] pub fn init_tracing() { tracing_subscriber::fmt() - .with_max_level(tracing::Level::TRACE) + .with_max_level(tracing::Level::INFO) .init(); } From fca423cda0d9b3c0c39ec013e3fa8f46e69af7b7 Mon Sep 17 00:00:00 2001 From: Elijah Hampton Date: Mon, 29 Dec 2025 21:53:43 -0500 Subject: [PATCH 3/8] Completes order and cancel order cli commands. --- README.md | 11 ++- examples/subscriptions.rs | 3 +- src/api/exchange.rs | 4 +- src/bin/cli.rs | 54 +++++++++-- src/cli/arguments.rs | 173 ++++++++++++++++++++++++++++++++++++ src/cli/mod.rs | 12 +-- src/types/exchange/mod.rs | 4 +- src/types/exchange/order.rs | 16 +++- src/types/info/spot.rs | 4 +- 9 files changed, 256 insertions(+), 25 deletions(-) create mode 100644 src/cli/arguments.rs diff --git a/README.md b/README.md index db3db31..922b37a 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ async fn main() -> Result<(), Box> { ### CLI Usage -The CLI provides quick access to market data and account information from your terminal. +The CLI provides quick access to market data and account information from your terminal. Below you can find a list of all supported CLI commands. rhyperliquid intentionally only supports place_order and cancel_order (by order id) from the CLI. Other Exchange API commands will be added by request or open source contribution. ```bash # Run CLI commands cargo run --bin cli --features=cli -- [OPTIONS] @@ -99,7 +99,7 @@ cargo run --bin cli --features=cli -- \ --coin ETH ``` -WebSocket subscriptions stream live updates until interrupted with `Ctrl+C`. Use `RUST_LOG=info` to see subscription confirmations and data updates. +WebSocket subscriptions stream live updates until interrupted with `Ctrl+C`. ## CLI Reference @@ -113,6 +113,13 @@ WebSocket subscriptions stream live updates until interrupted with `Ctrl+C`. Use ### Commands +#### Exchange API + +| Command | Description | +|---------|-------------| +| `order` | Place an order | +| `cancel` | Cancel an order by its order id | + #### Market Data | Command | Description | diff --git a/examples/subscriptions.rs b/examples/subscriptions.rs index 7cb3fb3..9769b98 100644 --- a/examples/subscriptions.rs +++ b/examples/subscriptions.rs @@ -13,7 +13,8 @@ async fn main() -> Result<(), Box> { let user = user(); subs.subscribe_all_mids(None).await?; - subs.subscribe_candle("BTC", "5m".to_string()).await?; + subs.subscribe_candle_snapshot("BTC", "5m".to_string()) + .await?; subs.subscribe_l2_book("BTC", None, None).await?; subs.subscribe_trades("BTC").await?; subs.subscribe_notifications(user.clone()).await?; diff --git a/src/api/exchange.rs b/src/api/exchange.rs index 90f4c30..cdcbd43 100644 --- a/src/api/exchange.rs +++ b/src/api/exchange.rs @@ -747,7 +747,7 @@ impl<'client> ExchangeApi<'client> { let nonce = current_time_millis(); - let from_sub_account_param = from_sub_account.map_or_else(String::new, |v| v.to_string()); + let from_sub_account_param = from_sub_account.unwrap_or_default(); let action = SendAssetAction { type_: "sendAsset".to_string(), @@ -930,7 +930,7 @@ impl<'client> ExchangeApi<'client> { let action = VaultTransferAction { type_: "vaultTransfer".to_string(), - vault_address: vault_address.to_string(), + vault_address: vault_address.clone(), is_deposit, usd: usd_amount, }; diff --git a/src/bin/cli.rs b/src/bin/cli.rs index d927c5e..25d7932 100644 --- a/src/bin/cli.rs +++ b/src/bin/cli.rs @@ -1,5 +1,6 @@ #![allow(unused_imports, clippy::too_many_lines)] -use alloy::signers::local::LocalSigner; +use alloy::{hex, signers::local::LocalSigner}; +use anyhow::anyhow; use clap::{Parser, Subcommand}; use rhyperliquid::{ cli::{Cli, Commands}, @@ -16,12 +17,12 @@ async fn main() -> Result<(), Box> { let cli = Cli::parse(); #[allow(clippy::expect_used)] - let signer = env::var("HL_PRIVATE_KEY").expect("HL_PRIVATE_KEY env var is missing"); let mut hyperliquid = &mut HyperliquidClientBuilder::new(); // Check if the user provided a network, default to testnet if let Some(network) = cli.network { - match (network as String).to_lowercase().as_str() { + let network: String = network; + match network.to_lowercase().as_str() { "testnet" => { hyperliquid = hyperliquid.testnet(); } @@ -34,21 +35,24 @@ async fn main() -> Result<(), Box> { hyperliquid = hyperliquid.testnet(); } - if let Some(subscriptions) = cli.subscriptions { - if subscriptions == true { - hyperliquid = hyperliquid.with_subscriptions(); - } - } + // Enable subscriptions API + hyperliquid = hyperliquid.with_subscriptions(); // Check if user provides permission to check env var for signer key if let Some(signer_permission) = cli.allow_signer_key_env { if signer_permission { - hyperliquid = hyperliquid.with_wallet(LocalSigner::from_slice(signer.as_bytes())?); + let signer_str = std::env::var("HL_PRIVATE_KEY")?; + + let key_hex = signer_str.strip_prefix("0x").unwrap_or(&signer_str); + let key_bytes = hex::decode(key_hex)?; + + hyperliquid = hyperliquid.with_wallet(LocalSigner::from_slice(&key_bytes)?); } } let client = hyperliquid.build()?; let info_api = &client.info(); + let exchange_api = &client.exchange(); let mut subs = client.subscriptions().await?; match cli.command { @@ -179,6 +183,38 @@ async fn main() -> Result<(), Box> { tracing::info!("{:?}", user_fees); return Ok(()); } + Commands::Order(cmd_args) => { + let meta = info_api.spot_metadata().await?; + + let asset_idx = meta + .tokens + .iter() + .position(|asset| asset.name == cmd_args.coin()) + .ok_or_else(|| anyhow!("Unknown coin: {}", cmd_args.coin()))?; + + let (order, grouping, builder, vault, expires) = + cmd_args.build(u32::try_from(asset_idx).expect("index conversion to fit into u32")); + + exchange_api + .place_order(order, grouping, builder, vault, expires) + .await?; + } + Commands::Cancel(cmd_args) => { + let meta = info_api.spot_metadata().await?; + + let asset_idx = meta + .tokens + .iter() + .position(|asset| asset.name == cmd_args.coin()) + .ok_or_else(|| anyhow!("Unknown coin: {}", cmd_args.coin()))?; + + let (cancel_req, vault_address, expires_after) = + cmd_args.build(u32::try_from(asset_idx).expect("index conversion to fit into u32")); + + exchange_api + .cancel_order(cancel_req, vault_address, expires_after) + .await?; + } Commands::SubscribeOpenOrders { user } => { subs.subscribe_open_orders(user).await?; } diff --git a/src/cli/arguments.rs b/src/cli/arguments.rs new file mode 100644 index 0000000..4b6e651 --- /dev/null +++ b/src/cli/arguments.rs @@ -0,0 +1,173 @@ +use crate::cli::OrderRequest; +use crate::types::exchange::order::OrderSide; +use crate::types::exchange::{ + Builder, CancelRequest, Grouping, LimitOrder, Tif, Tpsl, TriggerOrder, +}; +#[allow(unused_imports)] +use clap::{Args, Command, Parser, ValueEnum}; +use serde::{Deserialize, Serialize}; + +/// CLI specific order type argument for order request. +#[derive(ValueEnum, Clone, Serialize, Deserialize)] +pub enum OrderTypeArg { + #[serde(rename = "limit")] + Limit, + #[serde(rename = "trigger")] + Trigger, +} + +/// Command for placing an order +#[derive(Args)] +pub struct OrderCmd { + coin: String, + #[arg(value_enum)] + side: OrderSide, + size: String, + + // Order type (limit vs trigger) + #[arg(long, default_value = "limit")] + #[arg(value_enum)] + order_type: OrderTypeArg, + + // Limit order fields + #[arg(long, required_if_eq("order_type", "limit"))] + price: Option, + + #[arg(long, default_value = "gtc")] + #[arg(value_enum)] + tif: Tif, + + // Trigger order fields + #[arg(long, required_if_eq("order_type", "trigger"))] + trigger_price: Option, + + #[arg(long, requires = "trigger_price")] + #[arg(value_enum)] + tpsl: Option, + + #[arg(long)] + trigger_market: bool, + + // Optional flags + #[arg(long)] + reduce_only: bool, + + #[arg(long)] + cloid: Option, + + // Function params + #[arg(long, default_value = "na")] + #[arg(value_enum)] + grouping: Grouping, + + #[arg(long)] + vault_address: Option, + + #[arg(long)] + expires_after: Option, + + // Builder fee + #[arg(long)] + builder_address: Option, + + #[arg(long, requires = "builder_address")] + builder_fee: Option, +} + +impl OrderCmd { + /// Builds the order request using the `asset_idx` and + /// the command line arguments. + pub fn build( + self, + asset_idx: u32, + ) -> ( + OrderRequest, + Grouping, + Option, + Option, + Option, + ) { + use crate::types::exchange::{OrderRequest, OrderType}; + + let order_type = match self.order_type { + OrderTypeArg::Limit => OrderType::Limit(LimitOrder { tif: self.tif }), + OrderTypeArg::Trigger => OrderType::Trigger(TriggerOrder { + is_market: self.trigger_market, + trigger_px: self + .trigger_price + .clone() + .expect("--trigger price required for trigger orders"), + tpsl: self.tpsl.expect("--tpsl required for trigger orders"), + }), + }; + + let builder = self.builder_address.map(|b| Builder { + b, + f: self.builder_fee.expect(""), + }); + + let price = match self.order_type { + OrderTypeArg::Limit => self.price.ok_or("--price required for limit orders"), + OrderTypeArg::Trigger => Ok(self.trigger_price.clone().unwrap_or_default()), + }; + + let order = OrderRequest { + a: asset_idx, + b: matches!(self.side, OrderSide::Buy), + p: price.expect("--price required for limit orders"), + s: self.size, + r: self.reduce_only, + t: order_type, + c: self.cloid, + }; + + ( + order, + self.grouping, + builder, + self.vault_address, + self.expires_after, + ) + } + + pub fn coin(&self) -> &str { + &self.coin + } +} + +/// Cmd arguments for a cancel order request +#[derive(Args)] +pub struct CancelOrderByOidCmd { + /// Name of coin + coin: String, + + /// Order id + o: u64, + + /// Asset index + #[arg(long)] + a: Option, + + #[arg(long)] + vault_address: Option, + + #[arg(long)] + expires_after: Option, +} + +impl CancelOrderByOidCmd { + /// Builds the order request using the `asset_idx` and + /// the command line arguments. + pub fn build(self, asset_idx: u32) -> (CancelRequest, Option, Option) { + let cancel_req = CancelRequest { + a: self.a.unwrap_or(asset_idx), + o: self.o, + }; + + (cancel_req, self.vault_address, self.expires_after) + } + + pub fn coin(&self) -> &str { + &self.coin + } +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 0f16dea..03298a4 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,9 +1,15 @@ +mod arguments; + +pub use arguments::{CancelOrderByOidCmd, OrderCmd, OrderTypeArg}; + #[allow(unused_imports)] use crate::types::exchange::OrderRequest; use clap::{Parser, Subcommand}; #[derive(Subcommand)] pub enum Commands { + Order(OrderCmd), + Cancel(CancelOrderByOidCmd), AllMids { #[arg(short, long)] dex: Option, @@ -178,15 +184,11 @@ pub enum Commands { #[derive(Parser)] pub struct Cli { - /// Enables subscriptions through the CLI - #[arg(short, long)] - pub subscriptions: Option, - /// Picks a specific network, i.e. 'testnet' or 'mainnet' #[arg(short, long)] pub network: Option, - /// Allows CLI to use HL_PRIVATE_KEY env var in configuration (requires HL_PRIVATE_KEY end var) + /// Allows CLI to use `HL_PRIVATE_KEY` env var in configuration (requires `HL_PRIVATE_KEY` end var) #[arg(short, long)] pub allow_signer_key_env: Option, diff --git a/src/types/exchange/mod.rs b/src/types/exchange/mod.rs index bbb8ae8..05158f2 100644 --- a/src/types/exchange/mod.rs +++ b/src/types/exchange/mod.rs @@ -8,8 +8,8 @@ pub use order::{ BatchModifyAction, Builder, CDepositAction, CWithdrawAction, CancelAction, CancelByCloidAction, CancelByCloidRequest, CancelRequest, Cloid, Grouping, LimitOrder, ModifyAction, ModifyRequest, NoopAction, OrderAction, OrderRequest, OrderType, ReserveRequestWeightAction, - ScheduleCancelAction, SendAssetAction, SpotSendAction, Tif, TokenDelegateAction, TriggerOrder, - TwapCancelAction, TwapOrderAction, TwapRequest, UpdateIsolatedMarginAction, + ScheduleCancelAction, SendAssetAction, SpotSendAction, Tif, TokenDelegateAction, Tpsl, + TriggerOrder, TwapCancelAction, TwapOrderAction, TwapRequest, UpdateIsolatedMarginAction, UpdateLeverageAction, UsdClassTransferAction, UsdSendAction, UserDexAbstractionAction, ValidatorL1StreamAction, VaultTransferAction, WithdrawAction, }; diff --git a/src/types/exchange/order.rs b/src/types/exchange/order.rs index 86f78b0..b92f5ad 100644 --- a/src/types/exchange/order.rs +++ b/src/types/exchange/order.rs @@ -1,5 +1,6 @@ use crate::types::serialize::serialize_chain_id_as_hex; use crate::{signature::eip712::Eip712, types::info::perpetual::AssetInfo}; +use clap::ValueEnum; /// Request and response types for the exchange endpoint used to interact /// with and trade on the Hyperliquid chain. @@ -27,7 +28,7 @@ fn eip_712_domain(chain_id: u64) -> Eip712Domain { pub type Cloid = String; /// Time-in-force for limit orders -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(ValueEnum, Debug, Clone, Serialize, Deserialize)] pub enum Tif { /// Add liquidity only (post only) #[serde(rename = "Alo")] @@ -57,7 +58,7 @@ pub struct TriggerOrder { pub tpsl: Tpsl, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(ValueEnum, Debug, Clone, Serialize, Deserialize)] pub enum Tpsl { #[serde(rename = "tp")] Tp, @@ -75,6 +76,15 @@ pub enum OrderType { Trigger(TriggerOrder), } +/// Order side, i.e. buy or sell for the order +#[derive(ValueEnum, Clone, Serialize, Deserialize)] +pub enum OrderSide { + #[serde(rename = "buy")] + Buy, + #[serde(rename = "sell")] + Sell, +} + /// Builder fee configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Builder { @@ -133,7 +143,7 @@ impl OrderRequest { } /// Grouping type for orders -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(ValueEnum, Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum Grouping { #[serde(rename = "na")] diff --git a/src/types/info/spot.rs b/src/types/info/spot.rs index 1b9f0c8..75098e3 100644 --- a/src/types/info/spot.rs +++ b/src/types/info/spot.rs @@ -1,7 +1,8 @@ -use rust_decimal::Decimal; /// Response types for the info endpoints that are specific to spot. /// Additional information for endpoint responses can be found /// here: `` + +use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -28,6 +29,7 @@ pub struct SpotToken { pub is_canonical: bool, pub evm_contract: Option, pub full_name: Option, + #[serde(with = "rust_decimal::serde::str")] pub deployer_trading_fee_share: Decimal, } From 68e02feb6081a80b5e6025f71c9c7ac7be35575b Mon Sep 17 00:00:00 2001 From: Elijah Hampton Date: Sun, 4 Jan 2026 01:32:22 -0500 Subject: [PATCH 4/8] Fix serialization of Decimal types --- examples/advanced_order.rs | 2 +- examples/aligned_quote_token_status.rs | 6 ++---- examples/basic_order.rs | 2 +- examples/spot_market_data.rs | 22 +++++++++++----------- 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/examples/advanced_order.rs b/examples/advanced_order.rs index c859ee8..9d466ae 100644 --- a/examples/advanced_order.rs +++ b/examples/advanced_order.rs @@ -35,7 +35,7 @@ async fn main() -> Result<(), Box> { // Get current price let all_mids = info_api.all_mids(None).await?; - let doge_price = all_mids.get(&asset_id).ok_or(HyperliquidError::Internal( + let doge_price = all_mids.0.get(&asset_id).ok_or(HyperliquidError::Internal( "Missing asset in universe".to_string(), ))?; let price_decimal = Decimal::from_str(&doge_price.to_string())?; diff --git a/examples/aligned_quote_token_status.rs b/examples/aligned_quote_token_status.rs index 67879e1..571ec5c 100644 --- a/examples/aligned_quote_token_status.rs +++ b/examples/aligned_quote_token_status.rs @@ -1,6 +1,6 @@ #![allow(clippy::all)] use rhyperliquid::{ - example_helpers::{testnet_client, user}, + example_helpers::testnet_client, init_tracing::init_tracing, }; @@ -9,9 +9,7 @@ async fn main() -> Result<(), Box> { init_tracing(); let hyperliquid = testnet_client()?; - let user = user(); - - let status = hyperliquid.info().aligned_quote_token_status(&user).await?; + let status = hyperliquid.info().aligned_quote_token_status(0).await?; tracing::info!("{:?}", status); diff --git a/examples/basic_order.rs b/examples/basic_order.rs index 741a7bf..8f78a0e 100644 --- a/examples/basic_order.rs +++ b/examples/basic_order.rs @@ -37,7 +37,7 @@ async fn main() -> Result<(), Box> { let asset_id = format!("@{}", idx); let all_mids = info_api.all_mids(None).await?; - let doge_price = all_mids.get(&asset_id).ok_or(HyperliquidError::Internal( + let doge_price = all_mids.0.get(&asset_id).ok_or(HyperliquidError::Internal( "Missing asset in universe".to_string(), ))?; diff --git a/examples/spot_market_data.rs b/examples/spot_market_data.rs index f7ce66c..f195e54 100644 --- a/examples/spot_market_data.rs +++ b/examples/spot_market_data.rs @@ -23,17 +23,17 @@ async fn main() -> Result<(), Box> { .await?; tracing::info!("{:?}", spot_deploy_action_information); - let spot_pair_deploy_auction_information = hyperliquid - .info() - .spot_pair_deploy_auction_information() - .await?; - tracing::info!("{:?}", spot_pair_deploy_auction_information); - - let token_information = hyperliquid - .info() - .token_information("0x00000000000000000000000000000000") - .await?; - tracing::info!("{:?}", token_information); + // let spot_pair_deploy_auction_information = hyperliquid + // .info() + // .spot_pair_deploy_auction_information() + // .await?; + // tracing::info!("{:?}", spot_pair_deploy_auction_information); + + // let token_information = hyperliquid + // .info() + // .token_information("0x00000000000000000000000000000000") + // .await?; + // tracing::info!("{:?}", token_information); Ok(()) } From 98d71f787f4d52edb30cda41a43f1b2d38080712 Mon Sep 17 00:00:00 2001 From: Elijah Hampton Date: Sun, 4 Jan 2026 14:28:02 -0500 Subject: [PATCH 5/8] Fix decimal deserialization for twap order response types --- examples/twap_order.rs | 2 +- src/types/info/user.rs | 44 +++++++++++++++++++++++++++++------------- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/examples/twap_order.rs b/examples/twap_order.rs index 8f751e3..3a9798a 100644 --- a/examples/twap_order.rs +++ b/examples/twap_order.rs @@ -34,7 +34,7 @@ async fn main() -> Result<(), Box> { // Get current price to calculate size let all_mids = info_api.all_mids(None).await?; let default_decimal = Decimal::new(0, 0); - let doge_price = all_mids.get(&asset_id).unwrap_or(&default_decimal); + let doge_price = all_mids.0.get(&asset_id).unwrap_or(&default_decimal); let price_decimal = Decimal::from_str(&doge_price.to_string())?; // Calculate size: $50 notional / price diff --git a/src/types/info/user.rs b/src/types/info/user.rs index c122d82..2895818 100644 --- a/src/types/info/user.rs +++ b/src/types/info/user.rs @@ -32,10 +32,11 @@ pub struct OpenOrdersRequest { #[derive(Debug, Deserialize)] pub struct OpenOrder { pub coin: String, - #[serde(rename = "limitPx")] + #[serde(rename = "limitPx", with = "rust_decimal::serde::str")] pub limit_px: Decimal, pub oid: u64, pub side: String, + #[serde(with = "rust_decimal::serde::str")] pub sz: Decimal, pub timestamp: u64, } @@ -62,21 +63,22 @@ pub struct FrontendOpenOrder { pub is_position_tpsl: bool, #[serde(rename = "isTrigger")] pub is_trigger: bool, - #[serde(rename = "limitPx")] + #[serde(rename = "limitPx", with = "rust_decimal::serde::str")] pub limit_px: Decimal, pub oid: u64, #[serde(rename = "orderType")] pub order_type: String, - #[serde(rename = "origSz")] + #[serde(rename = "origSz", with = "rust_decimal::serde::str")] pub orig_sz: Decimal, #[serde(rename = "reduceOnly")] pub reduce_only: bool, pub side: String, + #[serde(with = "rust_decimal::serde::str")] pub sz: Decimal, pub timestamp: u64, #[serde(rename = "triggerCondition")] pub trigger_condition: String, - #[serde(rename = "triggerPx")] + #[serde(rename = "triggerPx", with = "rust_decimal::serde::str")] pub trigger_px: Decimal, } @@ -100,7 +102,7 @@ pub struct UserFillsRequest { #[derive(Debug, Deserialize)] pub struct PerpetualFill { - #[serde(rename = "closedPnl")] + #[serde(rename = "closedPnl", with = "rust_decimal::serde::str")] pub closed_pnl: Decimal, /// Refer to `` for more information /// on how asset IDs work. @@ -109,13 +111,16 @@ pub struct PerpetualFill { pub dir: String, pub hash: String, pub oid: u64, + #[serde(with = "rust_decimal::serde::str")] pub px: Decimal, pub side: String, - #[serde(rename = "startPosition")] + #[serde(rename = "startPosition", with = "rust_decimal::serde::str")] pub start_position: Decimal, + #[serde(with = "rust_decimal::serde::str")] pub sz: Decimal, pub time: u64, // The total fee, inclusive of builderFee. + #[serde(with = "rust_decimal::serde::str")] pub fee: Decimal, #[serde(rename = "feeToken")] pub fee_token: String, @@ -143,17 +148,21 @@ pub struct SpotFill { /// Refer to `` for more information /// on how asset IDs work. pub coin: String, + #[serde(with = "rust_decimal::serde::str")] pub px: Decimal, + #[serde(with = "rust_decimal::serde::str")] pub sz: Decimal, pub side: String, pub time: u64, - #[serde(rename = "startPosition")] + #[serde(rename = "startPosition", with = "rust_decimal::serde::str")] pub start_position: Decimal, pub dir: String, + #[serde(with = "rust_decimal::serde::str")] pub closed_pnl: Decimal, pub hash: String, pub oid: u64, pub crossed: bool, + #[serde(with = "rust_decimal::serde::str")] pub fee: Decimal, pub tid: u64, #[serde(rename = "feeToken")] @@ -331,8 +340,9 @@ pub struct Children; pub struct Order { pub coin: String, pub side: String, - #[serde(rename = "limitPx")] + #[serde(rename = "limitPx", with = "rust_decimal::serde::str")] pub limit_px: Decimal, + #[serde(with = "rust_decimal::serde::str")] pub sz: Decimal, pub oid: u64, pub timestamp: u64, @@ -340,7 +350,7 @@ pub struct Order { pub trigger_condition: String, #[serde(rename = "isTrigger")] pub is_trigger: bool, - #[serde(rename = "triggerPx")] + #[serde(rename = "triggerPx", with = "rust_decimal::serde::str")] pub trigger_px: Decimal, pub children: Vec, #[serde(rename = "isPositionTpsl")] @@ -349,7 +359,7 @@ pub struct Order { pub reduce_only: bool, #[serde(rename = "orderType")] pub order_type: String, - #[serde(rename = "origSz")] + #[serde(rename = "origSz", with = "rust_decimal::serde::str")] pub orig_sz: Decimal, pub tif: String, pub cloid: Option, @@ -368,19 +378,22 @@ pub type UserHistoricalOrders = Vec; #[derive(Debug, Deserialize)] pub struct SliceFill { - #[serde(rename = "closedPnl")] + #[serde(rename = "closedPnl", with = "rust_decimal::serde::str")] pub closed_pnl: Decimal, pub coin: String, pub crossed: bool, pub dir: String, pub hash: String, pub oid: u64, + #[serde(with = "rust_decimal::serde::str")] pub px: Decimal, pub side: String, - #[serde(rename = "startPosition")] + #[serde(rename = "startPosition", with = "rust_decimal::serde::str")] pub start_position: Decimal, + #[serde(with = "rust_decimal::serde::str")] pub sz: Decimal, pub time: u64, + #[serde(with = "rust_decimal::serde::str")] pub fee: Decimal, #[serde(rename = "feeToken")] pub fee_token: String, @@ -484,9 +497,11 @@ pub struct SpotState { pub struct SpotBalance { pub coin: String, pub token: u64, + #[serde(with = "rust_decimal::serde::str")] pub total: Decimal, + #[serde(with = "rust_decimal::serde::str")] pub hold: Decimal, - #[serde(rename = "entryNtl")] + #[serde(rename = "entryNtl", with = "rust_decimal::serde::str")] pub entry_ntl: Decimal, } @@ -538,6 +553,7 @@ pub struct Follower { pub user: String, #[serde(rename = "vaultEquity", with = "rust_decimal::serde::str")] pub vault_equity: Decimal, + #[serde(with = "rust_decimal::serde::str")] pub pnl: Decimal, #[serde(rename = "allTimePnl", with = "rust_decimal::serde::str")] pub all_time_pnl: Decimal, @@ -571,6 +587,7 @@ pub struct VaultDetails { pub leader: String, pub description: String, pub portfolio: Vec, + #[serde(with = "rust_decimal::serde::str")] pub apr: Decimal, #[serde(rename = "followerState")] pub follower_state: Option, // null in example @@ -604,6 +621,7 @@ pub struct UserRequest { pub struct VaultDeposit { #[serde(rename = "vaultAddress")] pub vault_address: String, + #[serde(with = "rust_decimal::serde::str")] pub equity: Decimal, } From ab61a042e4e13fe37c201457dbb7e4d8477776d5 Mon Sep 17 00:00:00 2001 From: Elijah Hampton Date: Sun, 4 Jan 2026 14:30:41 -0500 Subject: [PATCH 6/8] clippy,fmt --- README.md | 6 +-- examples/aligned_quote_token_status.rs | 5 +-- src/api/info.rs | 5 ++- src/types/info/perpetual.rs | 26 +++++------ src/types/info/spot.rs | 5 +-- src/types/info/user.rs | 60 +++++++++++++++----------- src/types/ws.rs | 7 +-- 7 files changed, 59 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 66fe452..e277fa3 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,13 @@ More concretely, our principles are: 1. **Type Safety & Financial Precision**: Every API response, order type, and market data structure is modeled in Rust's type system to catch invalid requests at compile time. We use `rust_decimal` for all financial calculations to eliminate floating-point errors. While network and API errors remain runtime concerns, the library prevents entire classes of invalid requests before they're sent. -2. **Correctness Over Speed**: rhyperliquid prioritizes getting trading operations right. This means proper EIP-712 signing implementation, correct msgpack serialization for action hashing, and careful handling of Hyperliquid's WebSocket heartbeat protocol. We optimize client-side performance where it matters, but understand that network latency to Hyperliquid's servers will dominate round-trip times. +2. **Correctness Over Speed**: rhyperliquid prioritizes getting trading operations right. This means proper EIP-712 signing implementation, correct msgpack serialization for action hashing, and careful handling of Hyperliquid's WebSocket heartbeat protocol. -3. **Modularity**: The crate is structured as composable components. Whether you need just the REST client for backtesting, WebSocket streams for live data, or the full CLI for manual trading, you can depend on only what you need. All public APIs are documented with examples showing real-world usage patterns. +3. **Modularity**: The crate is structured as composable components. Whether you need just the REST client for backtesting, WebSocket streams for live data, or the CLI for manual trading. All public APIs are documented with examples showing real-world usage patterns. 4. **Developer Experience**: We believe trading infrastructure should be approachable. The library provides builder patterns for configuration, comprehensive error messages that explain what went wrong, and examples organized by user workflows rather than individual functions. Both library users and CLI users should find the interface intuitive. -5. **Battle-Tested Foundations**: By leveraging proven crates (tokio for async, reqwest for HTTP, Alloy for Ethereum cryptography), we build on solid foundations rather than reinventing implementations. Our authentication follows Hyperliquid's exact specifications for both testnet and mainnet environments. +5. **Battle-Tested Foundations**: By leveraging proven crates (tokio for async, reqwest for HTTP, Alloy for Ethereum cryptography), we build on solid foundations rather than reinventing implementations. Our authentication follows Hyperliquid's specifications for both testnet and mainnet environments. 6. **Open & Extensible**: rhyperliquid is free open source software licensed under Apache/MIT. This enables anyone to build proprietary strategies, modify the client for their needs, or integrate it into larger systems without licensing concerns. We welcome contributions that improve reliability, add features, or enhance documentation. diff --git a/examples/aligned_quote_token_status.rs b/examples/aligned_quote_token_status.rs index 571ec5c..1e2e3ee 100644 --- a/examples/aligned_quote_token_status.rs +++ b/examples/aligned_quote_token_status.rs @@ -1,8 +1,5 @@ #![allow(clippy::all)] -use rhyperliquid::{ - example_helpers::testnet_client, - init_tracing::init_tracing, -}; +use rhyperliquid::{example_helpers::testnet_client, init_tracing::init_tracing}; #[tokio::main] async fn main() -> Result<(), Box> { diff --git a/src/api/info.rs b/src/api/info.rs index 9ea06cd..d96a433 100644 --- a/src/api/info.rs +++ b/src/api/info.rs @@ -572,7 +572,10 @@ impl<'client> InfoApi<'client> { self.post(payload).await } - pub async fn aligned_quote_token_status(&self, token: u32) -> Result> { + pub async fn aligned_quote_token_status( + &self, + token: u32, + ) -> Result> { let payload = json!({ "type": "alignedQuoteTokenInfo", "token": token diff --git a/src/types/info/perpetual.rs b/src/types/info/perpetual.rs index aff671a..da3c768 100644 --- a/src/types/info/perpetual.rs +++ b/src/types/info/perpetual.rs @@ -62,22 +62,22 @@ pub struct PerpetualsMetadata { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AssetContext { - #[serde(with = "rust_decimal::serde::str")] + #[serde(with = "rust_decimal::serde::str")] pub day_ntl_vlm: Decimal, - #[serde(with = "rust_decimal::serde::str")] + #[serde(with = "rust_decimal::serde::str")] pub funding: Decimal, pub impact_pxs: Option<[String; 2]>, - #[serde(with = "rust_decimal::serde::str")] + #[serde(with = "rust_decimal::serde::str")] pub mark_px: Decimal, - #[serde(with = "rust_decimal::serde::str_option")] + #[serde(with = "rust_decimal::serde::str_option")] pub mid_px: Option, - #[serde(with = "rust_decimal::serde::str")] + #[serde(with = "rust_decimal::serde::str")] pub open_interest: Decimal, - #[serde(with = "rust_decimal::serde::str")] + #[serde(with = "rust_decimal::serde::str")] pub oracle_px: Decimal, - #[serde(with = "rust_decimal::serde::str_option")] + #[serde(with = "rust_decimal::serde::str_option")] pub premium: Option, - #[serde(with = "rust_decimal::serde::str")] + #[serde(with = "rust_decimal::serde::str")] pub prev_day_px: Decimal, } @@ -218,7 +218,7 @@ pub struct ActiveAssetData { pub leverage: Leverage, pub max_trade_szs: [String; 2], pub available_to_trade: [String; 2], - #[serde(with = "rust_decimal::serde::str")] + #[serde(with = "rust_decimal::serde::str")] pub mark_px: Decimal, } @@ -226,11 +226,11 @@ pub struct ActiveAssetData { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PerpDexLimits { - #[serde(with = "rust_decimal::serde::str")] + #[serde(with = "rust_decimal::serde::str")] pub total_oi_cap: Decimal, - #[serde(with = "rust_decimal::serde::str")] + #[serde(with = "rust_decimal::serde::str")] pub oi_sz_cap_per_perp: Decimal, - #[serde(with = "rust_decimal::serde::str")] + #[serde(with = "rust_decimal::serde::str")] pub max_transfer_ntl: Decimal, /// Array of [coin, cap] pairs pub coin_to_oi_cap: Vec<(Decimal, Decimal)>, @@ -240,6 +240,6 @@ pub struct PerpDexLimits { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PerpDexStatus { - #[serde(with = "rust_decimal::serde::str")] + #[serde(with = "rust_decimal::serde::str")] pub total_net_deposit: Decimal, } diff --git a/src/types/info/spot.rs b/src/types/info/spot.rs index 4e9e97e..f33cd7b 100644 --- a/src/types/info/spot.rs +++ b/src/types/info/spot.rs @@ -1,7 +1,6 @@ /// Response types for the info endpoints that are specific to spot. /// Additional information for endpoint responses can be found /// here: `` - use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; @@ -55,9 +54,9 @@ pub struct SpotMetadata { pub struct SpotAssetContext { #[serde(with = "rust_decimal::serde::str")] pub day_ntl_vlm: Decimal, - #[serde(with = "rust_decimal::serde::str_option")] + #[serde(with = "rust_decimal::serde::str_option")] pub mark_px: Option, - #[serde(with = "rust_decimal::serde::str_option")] + #[serde(with = "rust_decimal::serde::str_option")] pub mid_px: Option, #[serde(with = "rust_decimal::serde::str_option")] pub prev_day_px: Option, diff --git a/src/types/info/user.rs b/src/types/info/user.rs index 2895818..0049b90 100644 --- a/src/types/info/user.rs +++ b/src/types/info/user.rs @@ -5,16 +5,13 @@ use rust_decimal::{self, Decimal}; use serde::de::Error as _; use serde::{Deserialize, Deserializer, Serialize}; -use std::collections::HashMap; use serde_with::{serde_as, DisplayFromStr}; +use std::collections::HashMap; /// Response to "allMids" request type. #[serde_as] #[derive(Debug, Deserialize)] -pub struct AllMids( - #[serde_as(as = "HashMap<_, DisplayFromStr>")] - pub HashMap -); +pub struct AllMids(#[serde_as(as = "HashMap<_, DisplayFromStr>")] pub HashMap); /// Request for "openOrders" request type. #[derive(Debug, Serialize)] @@ -157,7 +154,7 @@ pub struct SpotFill { #[serde(rename = "startPosition", with = "rust_decimal::serde::str")] pub start_position: Decimal, pub dir: String, - #[serde(with = "rust_decimal::serde::str")] + #[serde(with = "rust_decimal::serde::str")] pub closed_pnl: Decimal, pub hash: String, pub oid: u64, @@ -269,9 +266,9 @@ pub enum OrderWithStatus { /// Represents a Bid or Ask in the [`L2BookSnapshot`]. #[derive(Debug, Serialize, Deserialize)] pub struct BidOrAsk { - #[serde(with = "rust_decimal::serde::str")] + #[serde(with = "rust_decimal::serde::str")] pub px: rust_decimal::Decimal, - #[serde(with = "rust_decimal::serde::str")] + #[serde(with = "rust_decimal::serde::str")] pub sz: rust_decimal::Decimal, pub n: u64, } @@ -416,7 +413,10 @@ pub struct ClearinghouseState { pub margin_summary: MarginSummary, #[serde(rename = "crossMarginSummary")] pub cross_margin_summary: MarginSummary, - #[serde(rename = "crossMaintenanceMarginUsed", with = "rust_decimal::serde::str")] + #[serde( + rename = "crossMaintenanceMarginUsed", + with = "rust_decimal::serde::str" + )] pub cross_maintenance_margin_used: Decimal, #[serde(with = "rust_decimal::serde::str")] pub withdrawable: Decimal, @@ -449,20 +449,20 @@ pub struct AssetPosition { #[serde(rename_all = "camelCase")] pub struct Position { pub coin: String, - #[serde(with = "rust_decimal::serde::str")] + #[serde(with = "rust_decimal::serde::str")] pub szi: Decimal, pub leverage: Leverage, - #[serde(with = "rust_decimal::serde::str")] + #[serde(with = "rust_decimal::serde::str")] pub entry_px: Decimal, - #[serde(with = "rust_decimal::serde::str")] + #[serde(with = "rust_decimal::serde::str")] pub position_value: Decimal, - #[serde(with = "rust_decimal::serde::str")] + #[serde(with = "rust_decimal::serde::str")] pub unrealized_pnl: Decimal, - #[serde(with = "rust_decimal::serde::str")] + #[serde(with = "rust_decimal::serde::str")] pub return_on_equity: Decimal, - #[serde(with = "rust_decimal::serde::str")] + #[serde(with = "rust_decimal::serde::str")] pub liquidation_px: Decimal, - #[serde(with = "rust_decimal::serde::str")] + #[serde(with = "rust_decimal::serde::str")] pub margin_used: Decimal, pub max_leverage: u32, pub cum_funding: CumFunding, @@ -480,12 +480,12 @@ pub struct Leverage { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CumFunding { - #[serde(with = "rust_decimal::serde::str")] + #[serde(with = "rust_decimal::serde::str")] pub all_time: Decimal, - #[serde(with = "rust_decimal::serde::str")] + #[serde(with = "rust_decimal::serde::str")] pub since_open: Decimal, - #[serde(with = "rust_decimal::serde::str")] - pub since_change: Decimal + #[serde(with = "rust_decimal::serde::str")] + pub since_change: Decimal, } #[derive(Debug, Serialize, Deserialize)] @@ -541,8 +541,7 @@ pub struct PortfolioEntry( #[derive(Debug, Serialize, Deserialize)] pub struct HistoryPoint( pub u64, - #[serde(with = "rust_decimal::serde::str")] - pub Decimal + #[serde(with = "rust_decimal::serde::str")] pub Decimal, ); #[derive(Debug, Serialize, Deserialize)] @@ -703,9 +702,15 @@ pub enum ReferrerData { pub struct ReferralState { #[serde(rename = "cumVlm", with = "rust_decimal::serde::str")] pub cum_vlm: Decimal, - #[serde(rename = "cumRewardedFeesSinceReferred", with = "rust_decimal::serde::str")] + #[serde( + rename = "cumRewardedFeesSinceReferred", + with = "rust_decimal::serde::str" + )] pub cum_rewarded_fees_since_referred: Decimal, - #[serde(rename = "cumFeesRewardedToReferrer", with = "rust_decimal::serde::str")] + #[serde( + rename = "cumFeesRewardedToReferrer", + with = "rust_decimal::serde::str" + )] pub cum_fees_rewarded_to_referrer: Decimal, #[serde(rename = "timeJoined")] pub time_joined: u64, @@ -743,7 +748,7 @@ pub struct DailyUserVlm { pub user_cross: Decimal, #[serde(rename = "userAdd", with = "rust_decimal::serde::str")] pub user_add: Decimal, - #[serde(with = "rust_decimal::serde::str")] + #[serde(with = "rust_decimal::serde::str")] pub exchange: Decimal, } @@ -829,7 +834,10 @@ pub struct UserFees { pub trial: Option, #[serde(rename = "feeTrialEscrow", with = "rust_decimal::serde::str")] pub fee_trial_escrow: Decimal, - #[serde(rename = "nextTrialAvailableTimestamp", with = "rust_decimal::serde::str_option")] + #[serde( + rename = "nextTrialAvailableTimestamp", + with = "rust_decimal::serde::str_option" + )] pub next_trial_available_timestamp: Option, #[serde(rename = "stakingLink")] pub staking_link: Option, diff --git a/src/types/ws.rs b/src/types/ws.rs index 4cdf3a3..f2fc74f 100644 --- a/src/types/ws.rs +++ b/src/types/ws.rs @@ -6,7 +6,7 @@ use std::collections::HashMap; use std::primitive::str; use crate::types::serialize::decimal_array; -use serde_with::{serde_as}; +use serde_with::serde_as; /// WebSocket trade data #[derive(Debug, Clone, Serialize, Deserialize)] @@ -67,10 +67,7 @@ pub struct WsNotification { /// All mid prices #[derive(Debug, Clone, Serialize, Deserialize)] #[serde_as] -pub struct WsAllMids( - #[serde_as(as = "HashMap<_, DisplayFromStr>")] - pub HashMap, -); +pub struct WsAllMids(#[serde_as(as = "HashMap<_, DisplayFromStr>")] pub HashMap); /// Candlestick data #[derive(Debug, Clone, Serialize, Deserialize)] From 27d435c886ac696fd6f207919b02b23ce863daeb Mon Sep 17 00:00:00 2001 From: Elijah Hampton Date: Sun, 4 Jan 2026 14:33:02 -0500 Subject: [PATCH 7/8] Update CHANGELOG --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0b8030..65f5264 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,16 @@ ### Added - WebSocket streaming support for real-time market data - Subscription APIs for orderbook, trades, candles, and user events -- CLI binary for terminal-based queries (`--features=cli`) -- CLI commands for market data and account management +- CLI binary for terminal-based queries (`--features cli`) +- CLI commands for order placement and cancellation - Network selection via `--network` flag (mainnet/testnet) - Environment-based authentication for CLI via `HL_PRIVATE_KEY` ### Changed - Project status: WebSocket and CLI marked as complete +- Removes subscription command line arguments +- Fixes rust_decimal::Decimal serde deserialization for response types. +- Updates AllMids from 'type' to 'struct' to support correct response format. ## [0.1.0] - 2025-12-10 From b40a773f89819b237388649b2c31cfa2c422bf80 Mon Sep 17 00:00:00 2001 From: Elijah Hampton Date: Sun, 4 Jan 2026 14:33:33 -0500 Subject: [PATCH 8/8] CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65f5264..b90d330 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Added - WebSocket streaming support for real-time market data -- Subscription APIs for orderbook, trades, candles, and user events +- Adds complete support for Subscription API - CLI binary for terminal-based queries (`--features cli`) - CLI commands for order placement and cancellation - Network selection via `--network` flag (mainnet/testnet)