├── .github └── workflows │ ├── audit.yml │ └── general.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── TODO.md ├── examples ├── README.md ├── agent.rs ├── bridge.rs ├── cancel-order.rs ├── info │ ├── main.rs │ ├── perps.rs │ └── spot.rs ├── leverage.rs ├── modify-order.rs ├── normal-tpsl.rs ├── order-status.rs ├── place-order.rs ├── subaccount.rs ├── twap_order.rs ├── usd_send.rs ├── ws_all_mids.rs ├── ws_candle.rs ├── ws_l2_book.rs ├── ws_notification.rs ├── ws_order_updates.rs ├── ws_trades.rs ├── ws_user_event.rs └── ws_web_data.rs ├── src ├── api.rs ├── client.rs ├── config.rs ├── error.rs ├── exchange.rs ├── info.rs ├── lib.rs ├── types.rs ├── utils.rs └── websocket.rs └── tests └── utils.rs /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | name: Audit 2 | on: 3 | schedule: 4 | - cron: "0 0 * * *" 5 | push: 6 | paths: 7 | - "**/Cargo.toml" 8 | - "**/Cargo.lock" 9 | 10 | jobs: 11 | audit: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: taiki-e/install-action@cargo-deny 16 | - name: Scan for vulnerabilities 17 | run: cargo deny check advisories 18 | -------------------------------------------------------------------------------- /.github/workflows/general.yml: -------------------------------------------------------------------------------- 1 | name: General 2 | on: 3 | push: 4 | branches: 5 | - main 6 | env: 7 | CARGO_TERM_COLOR: always 8 | 9 | jobs: 10 | fmt: 11 | name: Rustfmt 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: dtolnay/rust-toolchain@stable 16 | with: 17 | components: rustfmt 18 | - name: Enforce formatting 19 | run: cargo fmt --check 20 | 21 | clippy: 22 | name: Clippy 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v3 26 | - uses: dtolnay/rust-toolchain@stable 27 | with: 28 | components: clippy 29 | - name: Linting 30 | run: cargo clippy -- -D warnings 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hyperliquid" 3 | version = "0.2.4" 4 | edition = "2021" 5 | license = "MIT" 6 | authors = ["Dennoh Peter "] 7 | description = "A Rust library for the Hyperliquid API" 8 | homepage = "https://hyperliquid.xyz/" 9 | categories = ["api-bindings", "cryptography::cryptocurrencies", "finance"] 10 | repository = "https://github.com/dennohpeter/hyperliquid" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | ethers = { version = "2.0.14", features = ["eip712"] } 16 | futures-util = "0.3.30" 17 | rmp-serde = "1.3.0" 18 | serde = { version = "1.0.210", features = ["derive"] } 19 | serde_json = "1.0.128" 20 | thiserror = "1.0.63" 21 | tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] } 22 | tokio-tungstenite = { version = "0.24.0", features = ["native-tls"] } 23 | uuid = { version = "1.10.0", features = ["v4", "serde"] } 24 | 25 | [dependencies.reqwest] 26 | version = "0.12.7" 27 | default-features = false 28 | features = ["json", "rustls-tls"] 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Dennoh Peter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Hyperliquid 2 | 3 | [![Rust](https://github.com/dennohpeter/hyperliquid/actions/workflows/general.yml/badge.svg)](https://github.com/dennohpeter/hyperliquid/actions/workflows/general.yml) 4 | [![Rust](https://github.com/dennohpeter/hyperliquid/actions/workflows/audit.yml/badge.svg)](https://github.com/dennohpeter/hyperliquid/actions/workflows/audit.yml) 5 | [![](https://img.shields.io/badge/License-MIT-green.svg)](./LICENSE) 6 | [![](https://img.shields.io/crates/v/hyperliquid)](https://crates.io/crates/hyperliquid) 7 | 8 | ### About 9 | 10 | A Rust library for Hyperliquid API 11 | 12 | ### Install 13 | 14 | `Cargo.toml` 15 | 16 | ```toml 17 | [dependencies] 18 | 19 | hyperliquid = { version = "0.2.4" } 20 | ``` 21 | 22 | ### Usage 23 | 24 | ```rust 25 | use hyperliquid::{Hyperliquid, Chain, Info}; 26 | 27 | #[tokio::main] 28 | async fn main() { 29 | let user: Address = "0xc64cc00b46101bd40aa1c3121195e85c0b0918d8" 30 | .parse() 31 | .expect("Invalid address"); 32 | 33 | 34 | let info:Info = Hyperliquid::new(Chain::Dev); 35 | 36 | // Retrieve exchange metadata 37 | let metadata = info.metadata().await.unwrap(); 38 | println!("Metadata \n{:?}", metadata.universe); 39 | } 40 | ``` 41 | 42 | ### Examples 43 | 44 | See `examples/` for examples. You can run any example with `cargo run --example `. 45 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | #### Features 2 | 3 | - [ ] Add support for `spotSend` on exchange 4 | - [ ] Update supported chains 5 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | 3 | - [x] Info 4 | 5 | - [x] Exchange 6 | 7 | - [x] Websocket 8 | 9 | ## Usage 10 | 11 | ```bash 12 | cargo run --release --example info 13 | ``` 14 | 15 | ```bash 16 | cargo run --release --example exchange 17 | ``` 18 | 19 | ```bash 20 | cargo run --release --example ws_all_mids 21 | ``` 22 | -------------------------------------------------------------------------------- /examples/agent.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Example assumes you already have a position on ETH so you can update margin 3 | */ 4 | 5 | use std::sync::Arc; 6 | 7 | use ethers::{ 8 | core::rand::thread_rng, 9 | signers::{LocalWallet, Signer}, 10 | }; 11 | use hyperliquid::{ 12 | types::{ 13 | exchange::request::{Limit, OrderRequest, OrderType, Tif}, 14 | Chain, 15 | }, 16 | Exchange, Hyperliquid, 17 | }; 18 | 19 | #[tokio::main] 20 | async fn main() { 21 | // Key was randomly generated for testing and shouldn't be used with any real funds 22 | let wallet: Arc = Arc::new( 23 | "e908f86dbb4d55ac876378565aafeabc187f6690f046459397b17d9b9a19688e" 24 | .parse() 25 | .unwrap(), 26 | ); 27 | 28 | let exchange: Exchange = Hyperliquid::new(Chain::ArbitrumTestnet); 29 | 30 | // Create a new wallet with the agent. This agent can't transfer or withdraw funds 31 | // but can place orders. 32 | 33 | let agent = Arc::new(LocalWallet::new(&mut thread_rng())); 34 | 35 | let agent_address = agent.address(); 36 | 37 | println!("Agent address: {:?}", agent_address); 38 | 39 | let res = exchange 40 | .approve_agent(wallet.clone(), agent_address, Some("WETH".to_string())) 41 | .await 42 | .unwrap(); 43 | 44 | println!("Response: {:?}", res); 45 | 46 | // place order with agent 47 | let order_type = OrderType::Limit(Limit { tif: Tif::Gtc }); 48 | let order = OrderRequest { 49 | asset: 4, 50 | is_buy: true, 51 | reduce_only: false, 52 | limit_px: "1700".to_string(), 53 | sz: "0.1".to_string(), 54 | order_type, 55 | cloid: None, 56 | }; 57 | let vault_address = None; 58 | 59 | println!("Placing order with agent..."); 60 | 61 | let response = exchange 62 | .place_order(agent.clone(), vec![order], vault_address) 63 | .await 64 | .expect("Failed to place order"); 65 | 66 | println!("Response: {:?}", response); 67 | } 68 | -------------------------------------------------------------------------------- /examples/bridge.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use ethers::signers::LocalWallet; 4 | use hyperliquid::{types::Chain, Exchange, Hyperliquid}; 5 | 6 | #[tokio::main] 7 | async fn main() { 8 | // Key was randomly generated for testing and shouldn't be used with any real funds 9 | let wallet: Arc = Arc::new( 10 | "8547bf37e4ac35e85d1e8afc2a2ba5c7f352b8a11ae916e9f14737737e8e0e47" 11 | .parse() 12 | .unwrap(), 13 | ); 14 | 15 | let exchange: Exchange = Hyperliquid::new(Chain::ArbitrumGoerli); 16 | 17 | let destination = "0x0D1d9635D0640821d15e323ac8AdADfA9c111414" 18 | .parse() 19 | .expect("Invalid address"); 20 | 21 | let usd = "10".to_string(); // USD 22 | 23 | println!("Withdrawing ${} from bridge to {:?}", usd, destination); 24 | 25 | let res = exchange 26 | .withdraw_from_bridge(wallet.clone(), destination, usd) 27 | .await 28 | .unwrap(); 29 | 30 | println!("Response: {:?}", res); 31 | } 32 | -------------------------------------------------------------------------------- /examples/cancel-order.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use ethers::signers::LocalWallet; 4 | use hyperliquid::{ 5 | types::{ 6 | exchange::{ 7 | request::{CancelByCloidRequest, CancelRequest, Limit, OrderRequest, OrderType, Tif}, 8 | response::{Response, Status, StatusType}, 9 | }, 10 | Chain, 11 | }, 12 | Exchange, Hyperliquid, 13 | }; 14 | use uuid::Uuid; 15 | 16 | #[tokio::main] 17 | async fn main() { 18 | // Key was randomly generated for testing and shouldn't be used with any real funds 19 | let wallet: Arc = Arc::new( 20 | "e908f86dbb4d55ac876378565aafeabc187f6690f046459397b17d9b9a19688e" 21 | .parse() 22 | .unwrap(), 23 | ); 24 | 25 | let exchange: Exchange = Hyperliquid::new(Chain::Dev); 26 | 27 | let order_type = OrderType::Limit(Limit { tif: Tif::Gtc }); 28 | 29 | let order = OrderRequest { 30 | asset: 4, 31 | is_buy: true, 32 | reduce_only: false, 33 | limit_px: "1800".to_string(), 34 | sz: "0.1".to_string(), 35 | order_type, 36 | cloid: None, 37 | }; 38 | 39 | let vault_address = None; 40 | 41 | println!("Placing order..."); 42 | let response = exchange 43 | .place_order(wallet.clone(), vec![order], vault_address) 44 | .await 45 | .expect("Failed to place order"); 46 | 47 | let response = match response { 48 | Response::Ok(order) => order, 49 | Response::Err(error) => panic!("Failed to place order: {:?}", error), 50 | }; 51 | 52 | let status_type = &response.data.unwrap(); 53 | 54 | let status = match status_type { 55 | StatusType::Statuses(statuses) => &statuses[0], 56 | _ => { 57 | panic!("Failed to place order: {:?}", status_type); 58 | } 59 | }; 60 | 61 | let oid = match status { 62 | Status::Filled(order) => order.oid, 63 | Status::Resting(order) => order.oid, 64 | _ => { 65 | panic!("Order is not filled or resting, status: {:?}", status); 66 | } 67 | }; 68 | 69 | println!("Order placed: {:?}", oid); 70 | 71 | tokio::time::sleep(std::time::Duration::from_secs(5)).await; 72 | 73 | // Cancel order 74 | 75 | println!("Cancelling order with oid {oid}."); 76 | let cancel = CancelRequest { asset: 4, oid }; 77 | 78 | let vault_address = None; 79 | 80 | let response = exchange 81 | .cancel_order(wallet.clone(), vec![cancel], vault_address) 82 | .await 83 | .expect("Failed to cancel order"); 84 | 85 | println!("Response: {:?}", response); 86 | 87 | let cloid = Uuid::new_v4(); 88 | 89 | println!("Placing order with cloid: {}", cloid.simple()); 90 | 91 | let order_type = OrderType::Limit(Limit { tif: Tif::Gtc }); 92 | 93 | let order = OrderRequest { 94 | asset: 4, 95 | is_buy: true, 96 | reduce_only: false, 97 | limit_px: "1700".to_string(), 98 | sz: "0.1".to_string(), 99 | order_type, 100 | cloid: Some(cloid), 101 | }; 102 | 103 | let vault_address = None; 104 | 105 | let response = exchange 106 | .place_order(wallet.clone(), vec![order], vault_address) 107 | .await 108 | .expect("Failed to place order"); 109 | 110 | let response = match response { 111 | Response::Ok(order) => order, 112 | Response::Err(error) => panic!("Failed to place order: {:?}", error), 113 | }; 114 | 115 | let data = &response.data.unwrap(); 116 | let status = match data { 117 | StatusType::Statuses(statuses) => &statuses[0], 118 | _ => { 119 | panic!("Failed to place order: {:?}", data); 120 | } 121 | }; 122 | 123 | let oid = match status { 124 | Status::Filled(order) => order.oid, 125 | Status::Resting(order) => order.oid, 126 | _ => panic!("Order is not filled or resting"), 127 | }; 128 | 129 | println!("Order placed: {:?}", oid); 130 | 131 | tokio::time::sleep(std::time::Duration::from_secs(5)).await; 132 | 133 | println!("Cancelling order with cloid: {}", cloid.simple()); 134 | let cancel = CancelByCloidRequest { asset: 4, cloid }; 135 | 136 | let response = exchange 137 | .cancel_order_by_cloid(wallet.clone(), vec![cancel], vault_address) 138 | .await 139 | .expect("Failed to cancel order"); 140 | 141 | println!("Response: {:?}", response); 142 | } 143 | -------------------------------------------------------------------------------- /examples/info/main.rs: -------------------------------------------------------------------------------- 1 | mod perps; 2 | mod spot; 3 | 4 | fn main() { 5 | perps::main(); 6 | spot::main(); 7 | } 8 | -------------------------------------------------------------------------------- /examples/info/perps.rs: -------------------------------------------------------------------------------- 1 | use std::{sync::Arc, time::SystemTime}; 2 | 3 | use ethers::{ 4 | signers::{LocalWallet, Signer}, 5 | types::Address, 6 | }; 7 | use hyperliquid::{ 8 | types::{ 9 | exchange::request::{Limit, OrderRequest, OrderType, Tif}, 10 | Chain, Oid, 11 | }, 12 | utils::{parse_price, parse_size}, 13 | Exchange, Hyperliquid, Info, 14 | }; 15 | use uuid::Uuid; 16 | 17 | const SEP: &str = "\n---"; 18 | 19 | #[tokio::main] 20 | pub async fn main() { 21 | // Key was randomly generated for testing and shouldn't be used with any real funds 22 | let wallet: Arc = Arc::new( 23 | "e908f86dbb4d55ac876378565aafeabc187f6690f046459397b17d9b9a19688e" 24 | .parse() 25 | .unwrap(), 26 | ); 27 | 28 | let user = wallet.address(); 29 | 30 | let info = Hyperliquid::new(Chain::ArbitrumTestnet); 31 | 32 | let exchange = Hyperliquid::new(Chain::ArbitrumTestnet); 33 | 34 | let now = SystemTime::now() 35 | .duration_since(SystemTime::UNIX_EPOCH) 36 | .expect("Time went backwards"); 37 | 38 | let now = now.as_millis() as u64; 39 | 40 | println!("Info Perps API Examples"); 41 | 42 | metadata(&info).await; 43 | mids(&info).await; 44 | contexts(&info).await; 45 | user_state(&info, user).await; 46 | batch_user_states(&info, vec![user]).await; 47 | open_orders(&info, user).await; 48 | frontend_open_orders(&info, user).await; 49 | user_fills(&info, user).await; 50 | user_fills_by_time(&info, user, now - 1000000, Some(now)).await; 51 | user_funding(&info, user).await; 52 | funding_history(&info).await; 53 | l2_book(&info).await; 54 | recent_trades(&info).await; 55 | candle_snapshot(&info).await; 56 | order_status(&info, &exchange, wallet).await; 57 | sub_accounts(&info, user).await; 58 | } 59 | 60 | async fn metadata(info: &Info) { 61 | let metadata = info.metadata().await.unwrap(); 62 | println!("{SEP}\nMetadata \n{:?}{SEP}", metadata.universe); 63 | } 64 | 65 | async fn mids(info: &Info) { 66 | let mids = info.mids().await.unwrap(); 67 | println!("Mids \n{:?}{SEP}", mids); 68 | } 69 | 70 | async fn contexts(info: &Info) { 71 | let contexts = info.contexts().await.unwrap(); 72 | println!("Asset Contexts \n{:?}{SEP}", contexts); 73 | } 74 | 75 | async fn user_state(info: &Info, user: Address) { 76 | let user_state = info.user_state(user).await.unwrap(); 77 | println!("User state for {user} \n{:?}{SEP}", user_state); 78 | } 79 | 80 | async fn batch_user_states(info: &Info, users: Vec
) { 81 | let user_states = info.user_states(users.clone()).await.unwrap(); 82 | println!("User states for {:?} \n{:?}{SEP}", users, user_states); 83 | } 84 | 85 | async fn open_orders(info: &Info, user: Address) { 86 | let open_orders = info.open_orders(user).await.unwrap(); 87 | println!("Open orders for {user} \n{:?}{SEP}", open_orders); 88 | } 89 | 90 | async fn frontend_open_orders(info: &Info, user: Address) { 91 | let open_orders = info.frontend_open_orders(user).await.unwrap(); 92 | println!("Frontend Open orders for {user} \n{:?}{SEP}", open_orders); 93 | } 94 | 95 | async fn user_fills(info: &Info, user: Address) { 96 | let user_fills = info.user_fills(user).await.unwrap(); 97 | println!("User fills for {user} \n{:?}{SEP}", user_fills); 98 | } 99 | 100 | async fn user_fills_by_time(info: &Info, user: Address, start_time: u64, end_time: Option) { 101 | let user_fills = info.user_fills_by_time(user, start_time, end_time).await; 102 | // .unwrap(); 103 | println!("User fills by time for {user} \n{:?}{SEP}", user_fills); 104 | } 105 | 106 | async fn user_funding(info: &Info, user: Address) { 107 | let start_timestamp = 1690540602225; 108 | let end_timestamp = 1690569402225; 109 | 110 | let user_funding = info 111 | .user_funding(user, start_timestamp, Some(end_timestamp)) 112 | .await 113 | .unwrap(); 114 | println!( 115 | "User funding for {user} between {start_timestamp} and {end_timestamp} \n{:?}{SEP}", 116 | user_funding 117 | ); 118 | } 119 | 120 | async fn funding_history(info: &Info) { 121 | let coin = "ETH"; 122 | 123 | let start_timestamp = 1690540602225; 124 | let end_timestamp = 1690569402225; 125 | 126 | let funding_history = info 127 | .funding_history(coin.to_string(), start_timestamp, Some(end_timestamp)) 128 | .await 129 | .unwrap(); 130 | println!( 131 | "Funding history for {coin} between {start_timestamp} and {end_timestamp} \n{:?}{SEP}", 132 | funding_history 133 | ); 134 | } 135 | 136 | async fn l2_book(info: &Info) { 137 | let coin = "ETH"; 138 | 139 | let l2_book = info.l2_book(coin.to_string()).await.unwrap(); 140 | println!("L2 book for {coin} \n{:?}{SEP}", l2_book); 141 | } 142 | 143 | async fn recent_trades(info: &Info) { 144 | let coin = "ETH"; 145 | 146 | let recent_trades = info.recent_trades(coin.to_string()).await.unwrap(); 147 | println!("Recent trades for {coin} \n{:?}{SEP}", recent_trades); 148 | } 149 | 150 | async fn candle_snapshot(info: &Info) { 151 | let coin = "ETH"; 152 | let interval = "15m"; 153 | let start_timestamp = 1690540602225; 154 | let end_timestamp = 1690569402225; 155 | 156 | let snapshot = info 157 | .candle_snapshot( 158 | coin.to_string(), 159 | interval.to_string(), 160 | start_timestamp, 161 | end_timestamp, 162 | ) 163 | .await 164 | .unwrap(); 165 | println!("Candle snapshot for {coin} between {start_timestamp} and {end_timestamp} with interval {interval} \n{:?}{SEP}",snapshot); 166 | } 167 | 168 | async fn order_status(info: &Info, exchange: &Exchange, wallet: Arc) { 169 | let user = wallet.address(); 170 | let vault_address = None; 171 | let cloid = Uuid::new_v4(); 172 | let order = OrderRequest { 173 | asset: 4, 174 | is_buy: true, 175 | reduce_only: false, 176 | limit_px: parse_price(2800.0), 177 | sz: parse_size(0.0331, 4), 178 | order_type: OrderType::Limit(Limit { tif: Tif::Gtc }), 179 | cloid: Some(cloid), 180 | }; 181 | 182 | println!("Placing order with cloid: {}{SEP}", cloid.simple()); 183 | let response = exchange 184 | .place_order(wallet, vec![order], vault_address) 185 | .await 186 | .expect("Failed to place order"); 187 | 188 | println!("Response: {:?}", response); 189 | 190 | tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; 191 | 192 | let order_status = info.order_status(user, Oid::Cloid(cloid)).await.unwrap(); 193 | 194 | println!( 195 | "Order status for {} \n{:?}{SEP}", 196 | cloid.simple(), 197 | order_status 198 | ); 199 | } 200 | 201 | async fn sub_accounts(info: &Info, user: Address) { 202 | let sub_accounts = info.sub_accounts(user).await.unwrap(); 203 | println!("Sub accounts for {user} \n{:?}{SEP}", sub_accounts); 204 | } 205 | -------------------------------------------------------------------------------- /examples/info/spot.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use ethers::{ 4 | signers::{LocalWallet, Signer}, 5 | types::Address, 6 | }; 7 | use hyperliquid::{types::Chain, Hyperliquid, Info}; 8 | 9 | const SEP: &str = "\n---"; 10 | 11 | #[tokio::main] 12 | pub async fn main() { 13 | // Key was randomly generated for testing and shouldn't be used with any real funds 14 | let wallet: Arc = Arc::new( 15 | "e908f86dbb4d55ac876378565aafeabc187f6690f046459397b17d9b9a19688e" 16 | .parse() 17 | .unwrap(), 18 | ); 19 | 20 | let user = wallet.address(); 21 | 22 | let info = Hyperliquid::new(Chain::Dev); 23 | 24 | println!("Info Spot API Examples"); 25 | 26 | spot_meta(&info).await; 27 | spot_meta_and_asset_ctxs(&info).await; 28 | spot_clearinghouse_state(&info, user).await; 29 | } 30 | 31 | async fn spot_meta(info: &Info) { 32 | let spot_meta = info.spot_meta().await.unwrap(); 33 | println!("{SEP}\nSpot Metadata \n{:?}{SEP}", spot_meta); 34 | } 35 | 36 | async fn spot_meta_and_asset_ctxs(info: &Info) { 37 | let spot_asset_ctxs = info.spot_meta_and_asset_ctxs().await.unwrap(); 38 | println!("Spot Asset Contexts \n{:?}{SEP}", spot_asset_ctxs); 39 | } 40 | 41 | async fn spot_clearinghouse_state(info: &Info, user: Address) { 42 | let states = info.spot_clearinghouse_state(user).await.unwrap(); 43 | println!("User spot state for {user} \n{:?}{SEP}", states); 44 | } 45 | -------------------------------------------------------------------------------- /examples/leverage.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use ethers::signers::{LocalWallet, Signer}; 4 | use hyperliquid::{types::Chain, Exchange, Hyperliquid, Info}; 5 | 6 | #[tokio::main] 7 | async fn main() { 8 | // Key was randomly generated for testing and shouldn't be used with any real funds 9 | let wallet: Arc = Arc::new( 10 | "e908f86dbb4d55ac876378565aafeabc187f6690f046459397b17d9b9a19688e" 11 | .parse() 12 | .unwrap(), 13 | ); 14 | 15 | let exchange: Exchange = Hyperliquid::new(Chain::Dev); 16 | 17 | let leverage = 2; 18 | let asset = 4; 19 | let is_cross = false; 20 | 21 | println!("Updating leverage to {}x ...", leverage); 22 | 23 | let res = exchange 24 | .update_leverage(wallet.clone(), leverage, asset, is_cross) 25 | .await 26 | .unwrap(); 27 | 28 | println!("Response: {:?}", res); 29 | 30 | let margin = 1; 31 | 32 | println!("--\nUpdating isolated margin for ETH to {margin}% ..."); 33 | 34 | let res = exchange 35 | .update_isolated_margin(wallet.clone(), asset, true, margin) 36 | .await 37 | .unwrap(); 38 | 39 | println!("Response: {:?}", res); 40 | 41 | let info: Info = Hyperliquid::new(Chain::Dev); 42 | 43 | // user state 44 | let res = info.user_state(wallet.address()).await.unwrap(); 45 | 46 | println!("--\nUser state: {:?}", res); 47 | } 48 | -------------------------------------------------------------------------------- /examples/modify-order.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use ethers::signers::LocalWallet; 4 | use hyperliquid::{ 5 | types::{ 6 | exchange::{ 7 | request::{Limit, ModifyRequest, OrderRequest, OrderType, Tif}, 8 | response::{Response, Status, StatusType}, 9 | }, 10 | Chain, 11 | }, 12 | Exchange, Hyperliquid, 13 | }; 14 | use uuid::Uuid; 15 | 16 | #[tokio::main] 17 | async fn main() { 18 | // Key was randomly generated for testing and shouldn't be used with any real funds 19 | let wallet: Arc = Arc::new( 20 | "e908f86dbb4d55ac876378565aafeabc187f6690f046459397b17d9b9a19688e" 21 | .parse() 22 | .unwrap(), 23 | ); 24 | 25 | let exchange: Exchange = Hyperliquid::new(Chain::Dev); 26 | 27 | let order_type = OrderType::Limit(Limit { tif: Tif::Gtc }); 28 | let cloid = Uuid::new_v4(); 29 | 30 | let order = OrderRequest { 31 | asset: 4, 32 | is_buy: true, 33 | reduce_only: false, 34 | limit_px: "1800".to_string(), 35 | sz: "0.1".to_string(), 36 | order_type, 37 | cloid: Some(cloid), 38 | }; 39 | 40 | let vault_address = None; 41 | 42 | println!("Placing order..."); 43 | let response = exchange 44 | .place_order(wallet.clone(), vec![order], vault_address) 45 | .await 46 | .expect("Failed to place order"); 47 | 48 | let response = match response { 49 | Response::Ok(order) => order, 50 | Response::Err(error) => panic!("Failed to place order: {:?}", error), 51 | }; 52 | 53 | let status_type = &response.data.unwrap(); 54 | 55 | let status = match status_type { 56 | StatusType::Statuses(statuses) => &statuses[0], 57 | _ => { 58 | panic!("Failed to place order: {:?}", status_type); 59 | } 60 | }; 61 | 62 | let oid = match status { 63 | Status::Filled(order) => order.oid, 64 | Status::Resting(order) => order.oid, 65 | _ => panic!("Order is not filled or resting"), 66 | }; 67 | 68 | println!("Order placed: {:?}", oid); 69 | 70 | tokio::time::sleep(std::time::Duration::from_secs(5)).await; 71 | 72 | let limit_px = "1710".to_string(); 73 | // Modifying the order 74 | println!("Modifying order {oid} limit price to {limit_px}."); 75 | 76 | let order = OrderRequest { 77 | asset: 4, 78 | is_buy: true, 79 | reduce_only: false, 80 | limit_px, 81 | sz: "0.1".to_string(), 82 | order_type: OrderType::Limit(Limit { tif: Tif::Gtc }), 83 | cloid: Some(cloid), 84 | }; 85 | 86 | let order = ModifyRequest { order, oid }; 87 | 88 | let response = exchange 89 | .modify_order(wallet.clone(), order, vault_address) 90 | .await 91 | .expect("Failed to modify order"); 92 | 93 | println!("Response: {:?}", response); 94 | } 95 | -------------------------------------------------------------------------------- /examples/normal-tpsl.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use ethers::signers::LocalWallet; 4 | use hyperliquid::{ 5 | types::{ 6 | exchange::request::{Limit, OrderRequest, OrderType, Tif, TpSl, Trigger}, 7 | Chain, 8 | }, 9 | utils::{parse_price, parse_size}, 10 | Exchange, Hyperliquid, 11 | }; 12 | #[tokio::main] 13 | async fn main() { 14 | // Key was randomly generated for testing and shouldn't be used with any real funds 15 | let wallet: Arc = Arc::new( 16 | "e908f86dbb4d55ac876378565aafeabc187f6690f046459397b17d9b9a19688e" 17 | .parse() 18 | .unwrap(), 19 | ); 20 | 21 | let exchange: Exchange = Hyperliquid::new(Chain::Dev); 22 | 23 | let asset = 4; 24 | let sz_decimals = 4; 25 | 26 | let normal = OrderRequest { 27 | asset, 28 | is_buy: true, 29 | reduce_only: false, 30 | limit_px: parse_price(2800.0), 31 | sz: parse_size(0.0331, sz_decimals), 32 | order_type: OrderType::Limit(Limit { tif: Tif::Gtc }), 33 | cloid: None, 34 | }; 35 | 36 | let tp = OrderRequest { 37 | asset, 38 | is_buy: false, 39 | reduce_only: true, 40 | limit_px: parse_price(2810.0), 41 | sz: parse_size(0.0331, sz_decimals), 42 | order_type: OrderType::Trigger(Trigger { 43 | is_market: true, 44 | trigger_px: parse_price(2810.0), 45 | tpsl: TpSl::Tp, 46 | }), 47 | cloid: None, 48 | }; 49 | 50 | let sl = OrderRequest { 51 | asset, 52 | is_buy: false, 53 | reduce_only: true, 54 | limit_px: parse_price(2750.0), 55 | sz: parse_size(0.0331, sz_decimals), 56 | order_type: OrderType::Trigger(Trigger { 57 | is_market: true, 58 | trigger_px: parse_price(2750.0), 59 | tpsl: TpSl::Tp, 60 | }), 61 | cloid: None, 62 | }; 63 | 64 | let vault_address = None; 65 | 66 | println!("Placing normal tpsl order..."); 67 | let response = exchange 68 | .normal_tpsl(wallet.clone(), vec![normal, tp, sl], vault_address) 69 | .await 70 | .expect("Failed to place order"); 71 | 72 | println!("Response: {:?}", response); 73 | 74 | println!("-----------------"); 75 | } 76 | -------------------------------------------------------------------------------- /examples/order-status.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use ethers::signers::{LocalWallet, Signer}; 4 | use hyperliquid::{ 5 | types::{ 6 | exchange::{ 7 | request::{Limit, OrderRequest, OrderType, Tif}, 8 | response::{Response, Status, StatusType}, 9 | }, 10 | Chain, Oid, 11 | }, 12 | utils::{parse_price, parse_size}, 13 | Exchange, Hyperliquid, Info, 14 | }; 15 | 16 | #[tokio::main] 17 | async fn main() { 18 | // Key was randomly generated for testing and shouldn't be used with any real funds 19 | let wallet: Arc = Arc::new( 20 | "e908f86dbb4d55ac876378565aafeabc187f6690f046459397b17d9b9a19688e" 21 | .parse() 22 | .unwrap(), 23 | ); 24 | 25 | let exchange: Exchange = Hyperliquid::new(Chain::Dev); 26 | let info: Info = Hyperliquid::new(Chain::Dev); 27 | 28 | let asset = 4; 29 | let sz_decimals = 4; 30 | 31 | let order_type = OrderType::Limit(Limit { tif: Tif::Gtc }); 32 | 33 | let order = OrderRequest { 34 | asset, 35 | is_buy: true, 36 | reduce_only: false, 37 | limit_px: parse_price(2800.0), 38 | sz: parse_size(0.0331, sz_decimals), 39 | order_type, 40 | cloid: None, 41 | }; 42 | 43 | let vault_address = None; 44 | 45 | println!("Placing order..."); 46 | let response = exchange 47 | .place_order(wallet.clone(), vec![order], vault_address) 48 | .await 49 | .expect("Failed to place order"); 50 | 51 | let response = match response { 52 | Response::Ok(order) => order, 53 | Response::Err(error) => panic!("Failed to place order: {:?}", error), 54 | }; 55 | 56 | println!("Response: {:?}", response.data); 57 | 58 | let status_type = &response.data.unwrap(); 59 | 60 | let status = match status_type { 61 | StatusType::Statuses(statuses) => &statuses[0], 62 | _ => { 63 | panic!("Failed to place order: {:?}", status_type); 64 | } 65 | }; 66 | 67 | let oid = match status { 68 | Status::Filled(order) => order.oid, 69 | Status::Resting(order) => order.oid, 70 | _ => panic!("Order is not filled or resting"), 71 | }; 72 | 73 | println!("-----------------"); 74 | 75 | println!("Fetching order {} status...", oid); 76 | 77 | let status = info 78 | .order_status(wallet.address(), Oid::Order(oid)) 79 | .await 80 | .expect("Failed to fetch order status"); 81 | 82 | println!("Order status: {:#?}", status.order); 83 | } 84 | -------------------------------------------------------------------------------- /examples/place-order.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use ethers::signers::LocalWallet; 4 | use hyperliquid::{ 5 | types::{ 6 | exchange::request::{Limit, OrderRequest, OrderType, Tif, TpSl, Trigger}, 7 | Chain, 8 | }, 9 | utils::{parse_price, parse_size}, 10 | Exchange, Hyperliquid, 11 | }; 12 | use uuid::Uuid; 13 | 14 | #[tokio::main] 15 | async fn main() { 16 | // Key was randomly generated for testing and shouldn't be used with any real funds 17 | let wallet: Arc = Arc::new( 18 | "e908f86dbb4d55ac876378565aafeabc187f6690f046459397b17d9b9a19688e" 19 | .parse() 20 | .unwrap(), 21 | ); 22 | 23 | let exchange: Exchange = Hyperliquid::new(Chain::Dev); 24 | 25 | let asset = 4; 26 | let sz_decimals = 4; 27 | 28 | let order_type = OrderType::Limit(Limit { tif: Tif::Gtc }); 29 | 30 | let order = OrderRequest { 31 | asset, 32 | is_buy: true, 33 | reduce_only: false, 34 | limit_px: parse_price(2800.0), 35 | sz: parse_size(0.0331, sz_decimals), 36 | order_type, 37 | cloid: None, 38 | }; 39 | 40 | let vault_address = None; 41 | 42 | println!("Placing order..."); 43 | let response = exchange 44 | .place_order(wallet.clone(), vec![order], vault_address) 45 | .await 46 | .expect("Failed to place order"); 47 | 48 | println!("Response: {:?}", response); 49 | 50 | println!("-----------------"); 51 | println!("Placing an order with cloid..."); 52 | 53 | let order_type = OrderType::Limit(Limit { tif: Tif::Gtc }); 54 | 55 | let cloid = Uuid::new_v4(); 56 | 57 | let order = OrderRequest { 58 | asset, 59 | is_buy: true, 60 | reduce_only: false, 61 | limit_px: parse_price(2800.0), 62 | sz: parse_size(0.0331, sz_decimals), 63 | order_type, 64 | cloid: Some(cloid), 65 | }; 66 | 67 | let response = exchange 68 | .place_order(wallet.clone(), vec![order], vault_address) 69 | .await 70 | .expect("Failed to place order"); 71 | 72 | println!("Response: {:?}", response); 73 | 74 | println!("-----------------"); 75 | println!("Placing a trigger order with tpsl..."); 76 | 77 | let order_type = OrderType::Trigger(Trigger { 78 | is_market: false, 79 | trigger_px: parse_price(2800.0), 80 | tpsl: TpSl::Tp, 81 | }); 82 | 83 | let order = OrderRequest { 84 | asset, 85 | is_buy: true, 86 | reduce_only: false, 87 | limit_px: parse_price(2800.0), 88 | sz: parse_size(0.0331, sz_decimals), 89 | order_type, 90 | cloid: Some(cloid), 91 | }; 92 | 93 | let response = exchange 94 | .place_order(wallet.clone(), vec![order], vault_address) 95 | .await 96 | .expect("Failed to place order"); 97 | 98 | println!("Response: {:?}", response); 99 | } 100 | -------------------------------------------------------------------------------- /examples/subaccount.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use ethers::signers::LocalWallet; 4 | use hyperliquid::{ 5 | types::{ 6 | exchange::response::{Response, StatusType}, 7 | Chain, 8 | }, 9 | Exchange, Hyperliquid, 10 | }; 11 | 12 | #[tokio::main] 13 | async fn main() { 14 | // Key was randomly generated for testing and shouldn't be used with any real funds 15 | let wallet: Arc = Arc::new( 16 | "e908f86dbb4d55ac876378565aafeabc187f6690f046459397b17d9b9a19688e" 17 | .parse() 18 | .unwrap(), 19 | ); 20 | 21 | let exchange: Exchange = Hyperliquid::new(Chain::ArbitrumTestnet); 22 | 23 | println!("Creating subaccount..."); 24 | let name = { 25 | let suffix = timestamp().to_string(); 26 | 27 | // slice the last 4 characters 28 | let suffix = &suffix[suffix.len() - 4..]; 29 | 30 | format!("Acc-{}", suffix) 31 | }; 32 | 33 | let response = exchange 34 | .create_sub_account(wallet.clone(), name.clone()) 35 | .await 36 | .expect("Failed to create subaccount"); 37 | 38 | let response = match response { 39 | Response::Ok(sub_account) => sub_account, 40 | Response::Err(error) => { 41 | panic!("Failed to create subaccount: {:?}", error) 42 | } 43 | }; 44 | 45 | let sub_account_user = match response.data { 46 | Some(StatusType::Address(address)) => address, 47 | _ => panic!("Failed to get subaccount address: {:?}", response), 48 | }; 49 | 50 | println!( 51 | "Subaccount created with name {} and user address: {:x} ✓", 52 | name, sub_account_user 53 | ); 54 | 55 | tokio::time::sleep(std::time::Duration::from_secs(10)).await; 56 | 57 | let new_name = { 58 | let suffix = timestamp().to_string(); 59 | 60 | // slice the last 4 characters 61 | let suffix = &suffix[suffix.len() - 4..]; 62 | 63 | format!("Acc-{}", suffix) 64 | }; 65 | 66 | println!("Renaming subaccount to: {}", new_name); 67 | 68 | let response = exchange 69 | .sub_account_modify(wallet.clone(), new_name, sub_account_user) 70 | .await 71 | .expect("Failed to rename subaccount"); 72 | 73 | let response = match response { 74 | Response::Ok(sub_account) => sub_account, 75 | Response::Err(error) => { 76 | panic!("Failed to rename subaccount: {:?}", error) 77 | } 78 | }; 79 | 80 | println!("Subaccount rename response: {:?} ✓", response); 81 | 82 | tokio::time::sleep(std::time::Duration::from_secs(5)).await; 83 | 84 | println!("Depositing 1 USD to subaccount..."); 85 | 86 | let usd = 1_000_000; 87 | 88 | let is_deposit = true; 89 | 90 | let response = exchange 91 | .sub_account_transfer(wallet.clone(), is_deposit, sub_account_user, usd) 92 | .await 93 | .expect("Failed to deposit funds"); 94 | 95 | let response = match response { 96 | Response::Ok(response) => response, 97 | Response::Err(error) => { 98 | panic!("Failed to deposit funds: {:?}", error) 99 | } 100 | }; 101 | 102 | println!("Deposit response: {:?} ✓", response); 103 | 104 | tokio::time::sleep(std::time::Duration::from_secs(10)).await; 105 | 106 | println!("Withdrawing funds from subaccount..."); 107 | 108 | let response = exchange 109 | .sub_account_transfer(wallet.clone(), !is_deposit, sub_account_user, usd) 110 | .await 111 | .expect("Failed to withdraw funds"); 112 | 113 | let response = match response { 114 | Response::Ok(response) => response, 115 | Response::Err(error) => { 116 | panic!("Failed to withdraw funds: {:?}", error) 117 | } 118 | }; 119 | 120 | println!("Withdraw response: {:?} ✓", response); 121 | } 122 | 123 | // timestamp in miliseconds using std::time::SystemTime 124 | fn timestamp() -> u64 { 125 | use std::time::{SystemTime, UNIX_EPOCH}; 126 | 127 | SystemTime::now() 128 | .duration_since(UNIX_EPOCH) 129 | .unwrap() 130 | .as_millis() as u64 131 | } 132 | -------------------------------------------------------------------------------- /examples/twap_order.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use ethers::signers::LocalWallet; 4 | use hyperliquid::{ 5 | types::{exchange::request::TwapRequest, Chain}, 6 | utils::parse_size, 7 | Exchange, Hyperliquid, 8 | }; 9 | 10 | #[tokio::main] 11 | async fn main() { 12 | // Key was randomly generated for testing and shouldn't be used with any real funds 13 | let wallet: Arc = Arc::new( 14 | "e908f86dbb4d55ac876378565aafeabc187f6690f046459397b17d9b9a19688e" 15 | .parse() 16 | .unwrap(), 17 | ); 18 | 19 | let exchange: Exchange = Hyperliquid::new(Chain::Dev); 20 | 21 | let asset = 0; 22 | let sz_decimals = 2; 23 | 24 | let twap = TwapRequest { 25 | asset, 26 | is_buy: true, 27 | reduce_only: false, 28 | duration: 10, 29 | sz: parse_size(13.85, sz_decimals), 30 | randomize: true, 31 | }; 32 | 33 | let vault_address = None; 34 | 35 | println!("Placing a TWAP order..."); 36 | let response = exchange 37 | .twap_order(wallet.clone(), twap, vault_address) 38 | .await 39 | .expect("Failed to place twap order"); 40 | 41 | println!("Response: {:?}", response); 42 | } 43 | -------------------------------------------------------------------------------- /examples/usd_send.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use ethers::signers::{LocalWallet, Signer}; 4 | use hyperliquid::{types::Chain, Exchange, Hyperliquid}; 5 | 6 | #[tokio::main] 7 | async fn main() { 8 | // Key was randomly generated for testing and shouldn't be used with any real funds 9 | let wallet: Arc = Arc::new( 10 | "e908f86dbb4d55ac876378565aafeabc187f6690f046459397b17d9b9a19688e" 11 | .parse() 12 | .unwrap(), 13 | ); 14 | 15 | let exchange: Exchange = Hyperliquid::new(Chain::ArbitrumTestnet); 16 | 17 | let destination = "0x0D1d9635D0640821d15e323ac8AdADfA9c111414" 18 | .parse() 19 | .expect("Invalid address"); 20 | 21 | let amount = "1".to_string(); // USD 22 | 23 | println!( 24 | "Transferring ${} from {:?} to {:?}", 25 | amount, 26 | wallet.address(), 27 | destination 28 | ); 29 | 30 | let res = exchange 31 | .usdc_transfer(wallet.clone(), destination, amount) 32 | .await 33 | .unwrap(); 34 | 35 | println!("Response: {:?}", res); 36 | } 37 | -------------------------------------------------------------------------------- /examples/ws_all_mids.rs: -------------------------------------------------------------------------------- 1 | use hyperliquid::{ 2 | types::{ 3 | websocket::{ 4 | request::{Channel, Subscription}, 5 | response::Response, 6 | }, 7 | Chain, 8 | }, 9 | Hyperliquid, Result, Websocket, 10 | }; 11 | 12 | #[tokio::main] 13 | async fn main() -> Result<()> { 14 | let mut ws: Websocket = Hyperliquid::new(Chain::Dev); 15 | 16 | ws.connect().await?; 17 | 18 | let subscription = Channel { 19 | id: 1, 20 | sub: Subscription::AllMids, 21 | }; 22 | 23 | ws.subscribe(&[subscription]).await?; 24 | 25 | let handler = |event: Response| async move { 26 | println!("Received All Mids: \n--\n{:?}", event); 27 | 28 | Ok(()) 29 | }; 30 | 31 | ws.next(handler).await?; 32 | 33 | ws.disconnect().await?; 34 | 35 | Ok(()) 36 | } 37 | -------------------------------------------------------------------------------- /examples/ws_candle.rs: -------------------------------------------------------------------------------- 1 | use hyperliquid::{ 2 | types::{ 3 | websocket::{ 4 | request::{Channel, Subscription}, 5 | response::Response, 6 | }, 7 | Chain, 8 | }, 9 | Hyperliquid, Result, Websocket, 10 | }; 11 | 12 | #[tokio::main] 13 | async fn main() -> Result<()> { 14 | let mut ws: Websocket = Hyperliquid::new(Chain::Dev); 15 | 16 | ws.connect().await?; 17 | 18 | let candle = Channel { 19 | id: 2, 20 | sub: Subscription::Candle { 21 | coin: "BTC".into(), 22 | interval: "5m".into(), 23 | }, 24 | }; 25 | 26 | ws.subscribe(&[candle]).await?; 27 | 28 | let handler = |event: Response| async move { 29 | println!("Received Candle: \n--\n{:?}", event); 30 | 31 | Ok(()) 32 | }; 33 | 34 | ws.next(handler).await?; 35 | 36 | ws.disconnect().await?; 37 | 38 | Ok(()) 39 | } 40 | -------------------------------------------------------------------------------- /examples/ws_l2_book.rs: -------------------------------------------------------------------------------- 1 | use hyperliquid::{ 2 | types::{ 3 | websocket::{ 4 | request::{Channel, Subscription}, 5 | response::Response, 6 | }, 7 | Chain, 8 | }, 9 | Hyperliquid, Result, Websocket, 10 | }; 11 | 12 | #[tokio::main] 13 | async fn main() -> Result<()> { 14 | let mut ws: Websocket = Hyperliquid::new(Chain::Dev); 15 | 16 | ws.connect().await?; 17 | 18 | let books = Channel { 19 | id: 3, 20 | sub: Subscription::L2Book { coin: "BTC".into() }, 21 | }; 22 | 23 | ws.subscribe(&[books]).await?; 24 | 25 | let handler = |event: Response| async move { 26 | println!("Received L2 Books: \n--\n{:?}", event); 27 | 28 | Ok(()) 29 | }; 30 | 31 | ws.next(handler).await?; 32 | 33 | ws.disconnect().await?; 34 | 35 | Ok(()) 36 | } 37 | -------------------------------------------------------------------------------- /examples/ws_notification.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use ethers::signers::{LocalWallet, Signer}; 4 | use hyperliquid::{ 5 | types::{ 6 | websocket::{ 7 | request::{Channel, Subscription}, 8 | response::Response, 9 | }, 10 | Chain, 11 | }, 12 | Hyperliquid, Result, Websocket, 13 | }; 14 | 15 | #[tokio::main] 16 | async fn main() -> Result<()> { 17 | // Key was randomly generated for testing and shouldn't be used with any real funds 18 | let wallet: Arc = Arc::new( 19 | "e908f86dbb4d55ac876378565aafeabc187f6690f046459397b17d9b9a19688e" 20 | .parse() 21 | .unwrap(), 22 | ); 23 | let mut ws: Websocket = Hyperliquid::new(Chain::Dev); 24 | 25 | ws.connect().await?; 26 | 27 | let notification = Channel { 28 | id: 2, 29 | sub: Subscription::Notification { 30 | user: wallet.address(), 31 | }, 32 | }; 33 | 34 | ws.subscribe(&[notification]).await?; 35 | 36 | let handler = |event: Response| async move { 37 | println!("Received Notification: \n--\n{:?}", event); 38 | 39 | Ok(()) 40 | }; 41 | 42 | ws.next(handler).await?; 43 | 44 | ws.disconnect().await?; 45 | 46 | Ok(()) 47 | } 48 | -------------------------------------------------------------------------------- /examples/ws_order_updates.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use ethers::signers::{LocalWallet, Signer}; 4 | use hyperliquid::{ 5 | types::{ 6 | websocket::{ 7 | request::{Channel, Subscription}, 8 | response::Response, 9 | }, 10 | Chain, 11 | }, 12 | Hyperliquid, Result, Websocket, 13 | }; 14 | 15 | #[tokio::main] 16 | async fn main() -> Result<()> { 17 | // Key was randomly generated for testing and shouldn't be used with any real funds 18 | let wallet: Arc = Arc::new( 19 | "e908f86dbb4d55ac876378565aafeabc187f6690f046459397b17d9b9a19688e" 20 | .parse() 21 | .unwrap(), 22 | ); 23 | 24 | let mut ws: Websocket = Hyperliquid::new(Chain::Dev); 25 | 26 | ws.connect().await?; 27 | 28 | let order_updates = Channel { 29 | id: 2, 30 | sub: Subscription::OrderUpdates { 31 | user: wallet.address(), 32 | }, 33 | }; 34 | 35 | ws.subscribe(&[order_updates]).await?; 36 | 37 | let handler = |event: Response| async move { 38 | println!("Received Order Updates: \n--\n{:?}", event); 39 | 40 | Ok(()) 41 | }; 42 | 43 | ws.next(handler).await?; 44 | 45 | ws.disconnect().await?; 46 | 47 | Ok(()) 48 | } 49 | -------------------------------------------------------------------------------- /examples/ws_trades.rs: -------------------------------------------------------------------------------- 1 | use hyperliquid::{ 2 | types::{ 3 | websocket::{ 4 | request::{Channel, Subscription}, 5 | response::Response, 6 | }, 7 | Chain, 8 | }, 9 | Hyperliquid, Result, Websocket, 10 | }; 11 | 12 | #[tokio::main] 13 | async fn main() -> Result<()> { 14 | let mut ws: Websocket = Hyperliquid::new(Chain::Dev); 15 | 16 | ws.connect().await?; 17 | 18 | let trades = Channel { 19 | id: 2, 20 | sub: Subscription::Trades { coin: "BTC".into() }, 21 | }; 22 | 23 | ws.subscribe(&[trades]).await?; 24 | 25 | let handler = |event: Response| async move { 26 | println!("Received Trades: \n--\n{:?}", event); 27 | 28 | Ok(()) 29 | }; 30 | 31 | ws.next(handler).await?; 32 | 33 | ws.disconnect().await?; 34 | 35 | Ok(()) 36 | } 37 | -------------------------------------------------------------------------------- /examples/ws_user_event.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use ethers::signers::{LocalWallet, Signer}; 4 | use hyperliquid::{ 5 | types::{ 6 | websocket::{ 7 | request::{Channel, Subscription}, 8 | response::Response, 9 | }, 10 | Chain, 11 | }, 12 | Hyperliquid, Result, Websocket, 13 | }; 14 | 15 | #[tokio::main] 16 | async fn main() -> Result<()> { 17 | // Key was randomly generated for testing and shouldn't be used with any real funds 18 | let wallet: Arc = Arc::new( 19 | "e908f86dbb4d55ac876378565aafeabc187f6690f046459397b17d9b9a19688e" 20 | .parse() 21 | .unwrap(), 22 | ); 23 | 24 | let mut ws: Websocket = Hyperliquid::new(Chain::Dev); 25 | 26 | ws.connect().await?; 27 | 28 | let user_event = Channel { 29 | id: 2, 30 | sub: Subscription::User { 31 | user: wallet.address(), 32 | }, 33 | }; 34 | 35 | ws.subscribe(&[user_event]).await?; 36 | 37 | let handler = |event: Response| async move { 38 | println!("Received User Events: \n--\n{:?}", event); 39 | 40 | Ok(()) 41 | }; 42 | 43 | ws.next(handler).await?; 44 | 45 | ws.disconnect().await?; 46 | 47 | Ok(()) 48 | } 49 | -------------------------------------------------------------------------------- /examples/ws_web_data.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use ethers::signers::{LocalWallet, Signer}; 4 | use hyperliquid::{ 5 | types::{ 6 | websocket::{ 7 | request::{Channel, Subscription}, 8 | response::Response, 9 | }, 10 | Chain, 11 | }, 12 | Hyperliquid, Result, Websocket, 13 | }; 14 | 15 | #[tokio::main] 16 | async fn main() -> Result<()> { 17 | // Key was randomly generated for testing and shouldn't be used with any real funds 18 | let wallet: Arc = Arc::new( 19 | "e908f86dbb4d55ac876378565aafeabc187f6690f046459397b17d9b9a19688e" 20 | .parse() 21 | .unwrap(), 22 | ); 23 | 24 | let mut ws: Websocket = Hyperliquid::new(Chain::Dev); 25 | 26 | ws.connect().await?; 27 | 28 | let web_data = Channel { 29 | id: 2, 30 | sub: Subscription::WebData { 31 | user: wallet.address(), 32 | }, 33 | }; 34 | 35 | ws.subscribe(&[web_data]).await?; 36 | 37 | let handler = |event: Response| async move { 38 | println!("Received Web Data: \n--\n{:?}", event); 39 | 40 | Ok(()) 41 | }; 42 | 43 | ws.next(handler).await?; 44 | 45 | ws.disconnect().await?; 46 | 47 | Ok(()) 48 | } 49 | -------------------------------------------------------------------------------- /src/api.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::{ 4 | client::Client, 5 | config::Config, 6 | exchange::Exchange, 7 | info::Info, 8 | types::{Chain, API}, 9 | Websocket, 10 | }; 11 | 12 | impl From<&API> for String { 13 | fn from(api: &API) -> Self { 14 | String::from(match api { 15 | API::Info => "/info", 16 | API::Exchange => "/exchange", 17 | }) 18 | } 19 | } 20 | 21 | pub trait Hyperliquid { 22 | fn new(chain: Chain) -> Self; 23 | fn new_with_config(chain: Chain, config: &Config) -> Self; 24 | } 25 | 26 | impl Hyperliquid for Info { 27 | fn new(chain: Chain) -> Self { 28 | let config = match chain { 29 | Chain::Arbitrum => Config::mainnet(), 30 | Chain::ArbitrumGoerli | Chain::ArbitrumTestnet => Config::testnet(), 31 | _ => Config::default(), 32 | }; 33 | Self::new_with_config(chain, &config) 34 | } 35 | fn new_with_config(chain: Chain, config: &Config) -> Self { 36 | Self { 37 | chain, 38 | client: Client::new(config.rest_endpoint.clone()), 39 | } 40 | } 41 | } 42 | 43 | impl Hyperliquid for Exchange { 44 | fn new(chain: Chain) -> Self { 45 | let config = match chain { 46 | Chain::Arbitrum => Config::mainnet(), 47 | Chain::ArbitrumGoerli | Chain::ArbitrumTestnet => Config::testnet(), 48 | _ => Config::default(), 49 | }; 50 | Self::new_with_config(chain, &config) 51 | } 52 | fn new_with_config(chain: Chain, config: &Config) -> Self { 53 | Self { 54 | chain, 55 | client: Client::new(config.rest_endpoint.clone()), 56 | } 57 | } 58 | } 59 | 60 | impl Hyperliquid for Websocket { 61 | fn new(chain: Chain) -> Self { 62 | let config = match chain { 63 | Chain::Arbitrum => Config::mainnet(), 64 | Chain::ArbitrumGoerli | Chain::ArbitrumTestnet => Config::testnet(), 65 | _ => Config::default(), 66 | }; 67 | Self::new_with_config(chain, &config) 68 | } 69 | fn new_with_config(_chain: Chain, config: &Config) -> Self { 70 | Self { 71 | url: config.ws_endpoint.clone(), 72 | stream: None, 73 | channels: HashMap::new(), 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | use reqwest::{ 2 | header::{HeaderMap, HeaderValue, CONTENT_TYPE}, 3 | Response, 4 | }; 5 | use serde::{de::DeserializeOwned, ser::Serialize}; 6 | 7 | use crate::{error::Result, types::API}; 8 | 9 | pub struct Client { 10 | inner_client: reqwest::Client, 11 | host: String, 12 | } 13 | 14 | impl Client { 15 | pub fn new(host: String) -> Self { 16 | Self { 17 | inner_client: reqwest::Client::new(), 18 | host, 19 | } 20 | } 21 | } 22 | 23 | impl Client { 24 | pub async fn post( 25 | &self, 26 | endpoint: &API, 27 | req: &impl Serialize, 28 | ) -> Result { 29 | let url = &format!("{}{}", self.host, String::from(endpoint)); 30 | 31 | let response = self 32 | .inner_client 33 | .post(url) 34 | .headers(self.build_headers()) 35 | .json(req) 36 | .send() 37 | .await?; 38 | 39 | self.handler(response).await 40 | } 41 | } 42 | 43 | impl Client { 44 | fn build_headers(&self) -> HeaderMap { 45 | let mut headers = HeaderMap::new(); 46 | headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); 47 | headers 48 | } 49 | 50 | async fn handler(&self, response: Response) -> Result { 51 | response.json::().await.map_err(Into::into) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | pub struct Config { 2 | pub rest_endpoint: String, 3 | pub ws_endpoint: String, 4 | } 5 | 6 | impl Default for Config { 7 | fn default() -> Self { 8 | Self::testnet() 9 | } 10 | } 11 | 12 | impl Config { 13 | pub fn mainnet() -> Self { 14 | Self { 15 | rest_endpoint: "https://api.hyperliquid.xyz".to_string(), 16 | ws_endpoint: "wss://api.hyperliquid.xyz/ws".to_string(), 17 | } 18 | } 19 | 20 | pub fn testnet() -> Self { 21 | Self { 22 | rest_endpoint: "https://api.hyperliquid-testnet.xyz".to_string(), 23 | ws_endpoint: "wss://api.hyperliquid-testnet.xyz/ws".to_string(), 24 | } 25 | } 26 | 27 | pub fn local() -> Self { 28 | Self { 29 | rest_endpoint: "http://localhost:3001".to_string(), 30 | ws_endpoint: "ws://localhost:3001/ws".to_string(), 31 | } 32 | } 33 | 34 | pub fn set_rest_endpoint(&mut self, endpoint: String) { 35 | self.rest_endpoint = endpoint; 36 | } 37 | 38 | pub fn set_ws_endpoint(&mut self, endpoint: String) { 39 | self.ws_endpoint = endpoint; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use ethers::signers::WalletError; 2 | use std::time::SystemTimeError; 3 | use thiserror::Error as ThisError; 4 | use tokio_tungstenite::tungstenite; 5 | use tungstenite::Error as WsError; 6 | 7 | use crate::types::websocket::request::Subscription; 8 | 9 | pub type Result = std::result::Result; 10 | 11 | #[derive(ThisError, Debug)] 12 | pub enum Error { 13 | #[error("Reqwest error: {0:?}")] 14 | Reqwest(reqwest::Error), 15 | #[error("Timestamp error: {0:?}")] 16 | TimestampError(SystemTimeError), 17 | #[error("Wallet error: {0:?}")] 18 | WalletError(WalletError), 19 | #[error("WS error: {0:?}")] 20 | WsError(WsError), 21 | #[error("Not connected")] 22 | NotConnected, 23 | #[error("JSON error: {0:?}")] 24 | Json(serde_json::Error), 25 | #[error("Not subscribed to channel with id {0}")] 26 | NotSubscribed(u64), 27 | #[error("Subscription failed: {0:?}")] 28 | SubscriptionFailed(Subscription), 29 | #[error("Missing subscription response: {0:?}")] 30 | MissingSubscriptionResponse(Subscription), 31 | #[error("Rmp serde error: {0:?}")] 32 | RmpSerdeError(String), 33 | #[error("Chain {0} not supported")] 34 | ChainNotSupported(String), 35 | } 36 | 37 | impl From for Error { 38 | fn from(e: reqwest::Error) -> Self { 39 | Self::Reqwest(e) 40 | } 41 | } 42 | 43 | impl From for Error { 44 | fn from(e: SystemTimeError) -> Self { 45 | Self::TimestampError(e) 46 | } 47 | } 48 | 49 | impl From for Error { 50 | fn from(e: WalletError) -> Self { 51 | Self::WalletError(e) 52 | } 53 | } 54 | 55 | impl From for Error { 56 | fn from(e: WsError) -> Self { 57 | Self::WsError(e) 58 | } 59 | } 60 | 61 | impl From for Error { 62 | fn from(e: serde_json::Error) -> Self { 63 | Self::Json(e) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/exchange.rs: -------------------------------------------------------------------------------- 1 | use std::{sync::Arc, time::SystemTime}; 2 | 3 | use ethers::{ 4 | signers::{LocalWallet, Signer}, 5 | types::{Address, Signature, H256}, 6 | utils::to_checksum, 7 | }; 8 | 9 | use crate::{ 10 | client::Client, 11 | error::Result, 12 | types::{ 13 | agent::l1, 14 | exchange::{ 15 | request::{ 16 | Action, ApproveAgent, CancelByCloidRequest, CancelRequest, Grouping, ModifyRequest, 17 | OrderRequest, Request, TwapRequest, UsdSend, Withdraw3, 18 | }, 19 | response::Response, 20 | }, 21 | Chain, HyperliquidChain, API, 22 | }, 23 | Error, 24 | }; 25 | 26 | /// Endpoint to interact with and trade on the Hyperliquid chain. 27 | pub struct Exchange { 28 | pub client: Client, 29 | pub chain: Chain, 30 | } 31 | 32 | impl Exchange { 33 | /// Place an order 34 | /// 35 | /// # Arguments 36 | /// * `wallet` - The wallet to sign the order with 37 | /// * `orders` - The orders to place 38 | /// * `vault_address` - If trading on behalf of a vault, its onchain address in 42-character hexadecimal format 39 | /// e.g. `0x0000000000000000000000000000000000000000` 40 | /// 41 | /// # Note 42 | /// * `cloid` in argument `order` is an optional 128 bit hex string, e.g. `0x1234567890abcdef1234567890abcdef` 43 | pub async fn place_order( 44 | &self, 45 | wallet: Arc, 46 | orders: Vec, 47 | vault_address: Option
, 48 | ) -> Result { 49 | let nonce = self.nonce()?; 50 | 51 | let action = Action::Order { 52 | grouping: Grouping::Na, 53 | orders, 54 | }; 55 | 56 | let connection_id = action.connection_id(vault_address, nonce)?; 57 | 58 | let signature = self.sign_l1_action(wallet, connection_id).await?; 59 | 60 | let request = Request { 61 | action, 62 | nonce, 63 | signature, 64 | vault_address, 65 | }; 66 | 67 | self.client.post(&API::Exchange, &request).await 68 | } 69 | 70 | /// Place a normal order with tpsl order 71 | /// 72 | /// # Arguments 73 | /// * `wallet` - The wallet to sign the order with 74 | /// * `orders` - The orders to place 75 | /// * `vault_address` - If trading on behalf of a vault, its onchain address in 42-character hexadecimal format 76 | /// e.g. `0x0000000000000000000000000000000000000000` 77 | /// 78 | /// # Note 79 | /// * `cloid` in argument `order` is an optional 128 bit hex string, e.g. `0x1234567890abcdef1234567890abcdef` 80 | pub async fn normal_tpsl( 81 | &self, 82 | wallet: Arc, 83 | orders: Vec, 84 | vault_address: Option
, 85 | ) -> Result { 86 | let nonce = self.nonce()?; 87 | 88 | let action = Action::Order { 89 | grouping: Grouping::NormalTpsl, 90 | orders, 91 | }; 92 | 93 | let connection_id = action.connection_id(vault_address, nonce)?; 94 | 95 | let signature = self.sign_l1_action(wallet, connection_id).await?; 96 | 97 | let request = Request { 98 | action, 99 | nonce, 100 | signature, 101 | vault_address, 102 | }; 103 | 104 | self.client.post(&API::Exchange, &request).await 105 | } 106 | 107 | /// Cancel an order 108 | /// 109 | /// # Arguments 110 | /// * `wallet` - The wallet to sign the order with 111 | /// * `cancels` - The orders to cancel 112 | /// * `vault_address` - If trading on behalf of a vault, its onchain address in 42-character hexadecimal format 113 | /// e.g. `0x0000000000000000000000000000000000000000` 114 | pub async fn cancel_order( 115 | &self, 116 | wallet: Arc, 117 | cancels: Vec, 118 | vault_address: Option
, 119 | ) -> Result { 120 | let nonce = self.nonce()?; 121 | 122 | let action = Action::Cancel { cancels }; 123 | 124 | let connection_id = action.connection_id(vault_address, nonce)?; 125 | 126 | let signature = self.sign_l1_action(wallet, connection_id).await?; 127 | 128 | let request = Request { 129 | action, 130 | nonce, 131 | signature, 132 | vault_address, 133 | }; 134 | 135 | self.client.post(&API::Exchange, &request).await 136 | } 137 | 138 | /// Cancel order(s) by client order id (cloid) 139 | /// 140 | /// # Arguments 141 | /// * `wallet` - The wallet to sign the order with 142 | /// * `cancels` - The client orders to cancel 143 | /// * `vault_address` - If trading on behalf of a vault, its onchain address in 42-character hexadecimal format 144 | /// e.g. `0x0000000000000000000000000000000000000000` 145 | /// 146 | /// Note: `cloid` in argument `cancel` is a 128 bit hex string, e.g. `0x1234567890abcdef1234567890abcdef` 147 | pub async fn cancel_order_by_cloid( 148 | &self, 149 | wallet: Arc, 150 | cancels: Vec, 151 | vault_address: Option
, 152 | ) -> Result { 153 | let nonce = self.nonce()?; 154 | 155 | let action = Action::CancelByCloid { cancels }; 156 | 157 | let connection_id = action.connection_id(vault_address, nonce)?; 158 | 159 | let signature = self.sign_l1_action(wallet, connection_id).await?; 160 | 161 | let request = Request { 162 | action, 163 | nonce, 164 | signature, 165 | vault_address, 166 | }; 167 | 168 | self.client.post(&API::Exchange, &request).await 169 | } 170 | 171 | /// Modify an order 172 | /// 173 | /// # Arguments 174 | /// * `wallet` - The wallet to sign the order with 175 | /// * `order` - The orders to modify 176 | /// * `vault_address` - If trading on behalf of a vault, its onchain address in 42-character hexadecimal format 177 | /// e.g. `0x0000000000000000000000000000000000000000` 178 | /// 179 | /// Note: `cloid` in argument `order` is an optional 128 bit hex string, e.g. `0x1234567890abcdef1234567890abcdef` 180 | pub async fn modify_order( 181 | &self, 182 | wallet: Arc, 183 | order: ModifyRequest, 184 | vault_address: Option
, 185 | ) -> Result { 186 | let nonce = self.nonce()?; 187 | 188 | let action = Action::Modify(order); 189 | 190 | let connection_id = action.connection_id(vault_address, nonce)?; 191 | 192 | let signature = self.sign_l1_action(wallet, connection_id).await?; 193 | 194 | let request = Request { 195 | action, 196 | nonce, 197 | signature, 198 | vault_address, 199 | }; 200 | 201 | self.client.post(&API::Exchange, &request).await 202 | } 203 | 204 | /// Batch modify orders 205 | /// 206 | /// # Arguments 207 | /// * `wallet` - The wallet to sign the order with 208 | /// * `orders` - The orders to modify 209 | /// * `vault_address` - If trading on behalf of a vault, its onchain address in 42-character hexadecimal format 210 | /// e.g. `0x0000000000000000000000000000000000000000` 211 | pub async fn batch_modify_orders( 212 | &self, 213 | wallet: Arc, 214 | orders: Vec, 215 | vault_address: Option
, 216 | ) -> Result { 217 | let nonce = self.nonce()?; 218 | 219 | let action = Action::BatchModify { modifies: orders }; 220 | 221 | let connection_id = action.connection_id(vault_address, nonce)?; 222 | 223 | let signature = self.sign_l1_action(wallet, connection_id).await?; 224 | 225 | let request = Request { 226 | action, 227 | nonce, 228 | signature, 229 | vault_address, 230 | }; 231 | 232 | self.client.post(&API::Exchange, &request).await 233 | } 234 | 235 | /// Update cross or isolated leverage on a coin 236 | /// 237 | /// # Arguments 238 | /// * `wallet` - The wallet to sign the order with 239 | /// * `leverage` - The new leverage to set 240 | /// * `asset` - The asset to set the leverage for 241 | /// * `is_cross` - true if cross leverage, false if isolated leverage 242 | pub async fn update_leverage( 243 | &self, 244 | wallet: Arc, 245 | leverage: u32, 246 | asset: u32, 247 | is_cross: bool, 248 | ) -> Result { 249 | let nonce = self.nonce()?; 250 | 251 | let action = Action::UpdateLeverage { 252 | asset, 253 | is_cross, 254 | leverage, 255 | }; 256 | 257 | let vault_address = None; 258 | 259 | let connection_id = action.connection_id(vault_address, nonce)?; 260 | 261 | let signature = self.sign_l1_action(wallet, connection_id).await?; 262 | 263 | let request = Request { 264 | action, 265 | nonce, 266 | signature, 267 | vault_address, 268 | }; 269 | 270 | self.client.post(&API::Exchange, &request).await 271 | } 272 | 273 | /// Add or remove margin from isolated position 274 | /// 275 | /// # Arguments 276 | /// * `wallet` - The wallet to sign the order with 277 | /// * `asset` - The asset to set the margin for 278 | /// * `is_buy` - true if adding margin, false if removing margin 279 | /// * `ntli` - The new margin to set 280 | pub async fn update_isolated_margin( 281 | &self, 282 | wallet: Arc, 283 | asset: u32, 284 | is_buy: bool, 285 | ntli: i64, 286 | ) -> Result { 287 | let nonce = self.nonce()?; 288 | 289 | let action = Action::UpdateIsolatedMargin { 290 | asset, 291 | is_buy, 292 | ntli, 293 | }; 294 | 295 | let vault_address = None; 296 | 297 | let connection_id = action.connection_id(vault_address, nonce)?; 298 | 299 | let signature = self.sign_l1_action(wallet, connection_id).await?; 300 | 301 | let request = Request { 302 | action, 303 | nonce, 304 | signature, 305 | vault_address, 306 | }; 307 | 308 | self.client.post(&API::Exchange, &request).await 309 | } 310 | 311 | /// Place a TWAP order 312 | /// # Arguments 313 | /// * `wallet` - The wallet to sign the order with 314 | /// * `twap` - The twap order to place 315 | /// * `vault_address` - If trading on behalf of a vault, its onchain address in 42-character hexadecimal format 316 | /// e.g. `0x0000000000000000000000000000000000000000` 317 | pub async fn twap_order( 318 | &self, 319 | wallet: Arc, 320 | twap: TwapRequest, 321 | vault_address: Option
, 322 | ) -> Result { 323 | let nonce = self.nonce()?; 324 | 325 | let action = Action::TwapOrder { twap }; 326 | 327 | let connection_id = action.connection_id(vault_address, nonce)?; 328 | 329 | let signature = self.sign_l1_action(wallet, connection_id).await?; 330 | 331 | let request = Request { 332 | action, 333 | nonce, 334 | signature, 335 | vault_address, 336 | }; 337 | 338 | self.client.post(&API::Exchange, &request).await 339 | } 340 | 341 | /// Send usd to another address. This transfer does not touch the EVM bridge. The signature 342 | /// format is human readable for wallet interfaces. 343 | /// 344 | /// # Arguments 345 | /// * `from` - The wallet to sign the transfer with 346 | /// * `destination` - The address to send the usd to 347 | /// * `amount` - The amount of usd to send 348 | pub async fn usdc_transfer( 349 | &self, 350 | from: Arc, 351 | destination: Address, 352 | amount: String, 353 | ) -> Result { 354 | let nonce = self.nonce()?; 355 | 356 | let hyperliquid_chain = match self.chain { 357 | Chain::Arbitrum => HyperliquidChain::Mainnet, 358 | Chain::ArbitrumTestnet => HyperliquidChain::Testnet, 359 | _ => return Err(Error::ChainNotSupported(self.chain.to_string())), 360 | }; 361 | 362 | let payload = UsdSend { 363 | signature_chain_id: 421614.into(), 364 | hyperliquid_chain, 365 | amount, 366 | destination: to_checksum(&destination, None), 367 | time: nonce, 368 | }; 369 | 370 | let signature = from.sign_typed_data(&payload).await?; 371 | 372 | let action = Action::UsdSend(payload); 373 | 374 | let request = Request { 375 | action, 376 | nonce, 377 | signature, 378 | vault_address: None, 379 | }; 380 | 381 | self.client.post(&API::Exchange, &request).await 382 | } 383 | 384 | /// Withdraw from bridge 385 | /// 386 | /// # Arguments 387 | /// * `wallet` - The wallet to sign the withdrawal with 388 | /// * `destination` - The address to send the usd to 389 | /// * `amount` - The amount of usd to send 390 | pub async fn withdraw_from_bridge( 391 | &self, 392 | wallet: Arc, 393 | destination: Address, 394 | amount: String, 395 | ) -> Result { 396 | let nonce = self.nonce()?; 397 | 398 | let hyperliquid_chain = match self.chain { 399 | Chain::Arbitrum => HyperliquidChain::Mainnet, 400 | Chain::ArbitrumTestnet => HyperliquidChain::Testnet, 401 | _ => return Err(Error::ChainNotSupported(self.chain.to_string())), 402 | }; 403 | 404 | let payload = Withdraw3 { 405 | hyperliquid_chain, 406 | signature_chain_id: 421614.into(), 407 | amount, 408 | destination: to_checksum(&destination, None), 409 | time: nonce, 410 | }; 411 | 412 | let signature = wallet.sign_typed_data(&payload).await?; 413 | 414 | let action = Action::Withdraw3(payload); 415 | 416 | let request = Request { 417 | action, 418 | nonce, 419 | signature, 420 | vault_address: None, 421 | }; 422 | 423 | self.client.post(&API::Exchange, &request).await 424 | } 425 | 426 | /// Approve an agent to trade on behalf of the user 427 | /// 428 | /// # Arguments 429 | /// * `wallet` - The wallet to sign the approval with 430 | /// * `agent_address` - The address of the agent to approve 431 | /// * `agent_name` - An optional name for the agent 432 | pub async fn approve_agent( 433 | &self, 434 | wallet: Arc, 435 | agent_address: Address, 436 | agent_name: Option, 437 | ) -> Result { 438 | let nonce = self.nonce()?; 439 | 440 | let hyperliquid_chain = match self.chain { 441 | Chain::Arbitrum => HyperliquidChain::Mainnet, 442 | Chain::ArbitrumTestnet => HyperliquidChain::Testnet, 443 | _ => return Err(Error::ChainNotSupported(self.chain.to_string())), 444 | }; 445 | 446 | let agent = ApproveAgent { 447 | hyperliquid_chain, 448 | signature_chain_id: 421614.into(), 449 | agent_address, 450 | nonce, 451 | agent_name, 452 | }; 453 | 454 | let signature = wallet.sign_typed_data(&agent).await?; 455 | 456 | let action = Action::ApproveAgent(agent); 457 | 458 | let request = Request { 459 | action, 460 | nonce, 461 | signature, 462 | vault_address: None, 463 | }; 464 | 465 | self.client.post(&API::Exchange, &request).await 466 | } 467 | 468 | /// Create subaccount for the user 469 | /// 470 | /// # Arguments 471 | /// * `wallet` - The wallet to create the subaccount with 472 | /// * `name` - The name of the subaccount 473 | pub async fn create_sub_account( 474 | &self, 475 | wallet: Arc, 476 | name: String, 477 | ) -> Result { 478 | let nonce = self.nonce()?; 479 | 480 | let action = Action::CreateSubAccount { name }; 481 | 482 | let vault_address = None; 483 | 484 | let connection_id = action.connection_id(vault_address, nonce)?; 485 | 486 | let signature = self.sign_l1_action(wallet, connection_id).await?; 487 | 488 | let request = Request { 489 | action, 490 | nonce, 491 | signature, 492 | vault_address, 493 | }; 494 | 495 | self.client.post(&API::Exchange, &request).await 496 | } 497 | 498 | /// Rename subaccount 499 | /// 500 | /// # Arguments 501 | /// * `wallet` - The wallet to sign the rename with 502 | /// * `name` - The new name of the subaccount 503 | /// * `sub_account_user` - The address of the subaccount to rename 504 | pub async fn sub_account_modify( 505 | &self, 506 | wallet: Arc, 507 | name: String, 508 | sub_account_user: Address, 509 | ) -> Result { 510 | let nonce = self.nonce()?; 511 | 512 | let action = Action::SubAccountModify { 513 | name, 514 | sub_account_user, 515 | }; 516 | 517 | let vault_address = None; 518 | 519 | let connection_id = action.connection_id(vault_address, nonce)?; 520 | 521 | let signature = self.sign_l1_action(wallet, connection_id).await?; 522 | 523 | let request = Request { 524 | action, 525 | nonce, 526 | signature, 527 | vault_address, 528 | }; 529 | 530 | self.client.post(&API::Exchange, &request).await 531 | } 532 | 533 | /// Transfer funds between subaccounts 534 | /// 535 | /// # Arguments 536 | /// * `wallet` - The wallet to sign the transfer with 537 | /// * `from` - The subaccount to transfer from 538 | pub async fn sub_account_transfer( 539 | &self, 540 | wallet: Arc, 541 | is_deposit: bool, 542 | sub_account_user: Address, 543 | usd: u64, 544 | ) -> Result { 545 | let nonce = self.nonce()?; 546 | 547 | let action = Action::SubAccountTransfer { 548 | is_deposit, 549 | sub_account_user, 550 | usd, 551 | }; 552 | 553 | let vault_address = None; 554 | 555 | let connection_id = action.connection_id(vault_address, nonce)?; 556 | 557 | let signature = self.sign_l1_action(wallet, connection_id).await?; 558 | 559 | let request = Request { 560 | action, 561 | nonce, 562 | signature, 563 | vault_address, 564 | }; 565 | 566 | self.client.post(&API::Exchange, &request).await 567 | } 568 | 569 | /// Set referrer for the user 570 | /// 571 | /// # Arguments 572 | /// * `wallet` - The wallet to sign the transfer with 573 | /// * `code` - The referrer code 574 | pub async fn set_referrer(&self, wallet: Arc, code: String) -> Result { 575 | let nonce = self.nonce()?; 576 | 577 | let action = Action::SetReferrer { code }; 578 | 579 | let vault_address = None; 580 | 581 | let connection_id = action.connection_id(vault_address, nonce)?; 582 | 583 | let signature = self.sign_l1_action(wallet, connection_id).await?; 584 | 585 | let request = Request { 586 | action, 587 | nonce, 588 | signature, 589 | vault_address, 590 | }; 591 | 592 | self.client.post(&API::Exchange, &request).await 593 | } 594 | 595 | /// Schedule a time in (UTC ms) to cancel all open orders 596 | /// 597 | /// # Arguments 598 | /// * `wallet` - The wallet to sign the transaction with 599 | /// * `time` - Optional time in milliseconds to cancel all open orders 600 | /// 601 | /// # Note 602 | /// * If `time` is `None`, then unsets any cancel time in the future. 603 | /// `time` must be atleast 5 seconds after the current time 604 | /// * Once the time is reached, all open orders will be cancelled and trigger count will be incremented. 605 | /// The max number of triggers is 10 per day. Trigger count resets at 00:00 UTC 606 | pub async fn schedule_cancel( 607 | &self, 608 | wallet: Arc, 609 | time: Option, 610 | ) -> Result { 611 | let nonce = self.nonce()?; 612 | 613 | let time = time.unwrap_or_else(|| nonce + 5000); 614 | 615 | let action = Action::ScheduleCancel { time }; 616 | 617 | let vault_address = None; 618 | 619 | let connection_id = action.connection_id(vault_address, nonce)?; 620 | 621 | let signature = self.sign_l1_action(wallet, connection_id).await?; 622 | 623 | let request = Request { 624 | action, 625 | nonce, 626 | signature, 627 | vault_address, 628 | }; 629 | 630 | self.client.post(&API::Exchange, &request).await 631 | } 632 | 633 | async fn sign_l1_action( 634 | &self, 635 | wallet: Arc, 636 | connection_id: H256, 637 | ) -> Result { 638 | let source = match self.chain { 639 | Chain::Arbitrum => "a".to_string(), 640 | Chain::ArbitrumTestnet => "b".to_string(), 641 | _ => return Err(Error::ChainNotSupported(self.chain.to_string())), 642 | }; 643 | 644 | let payload = l1::Agent { 645 | source, 646 | connection_id, 647 | }; 648 | 649 | Ok(wallet.sign_typed_data(&payload).await?) 650 | } 651 | 652 | /// get the next nonce to use 653 | fn nonce(&self) -> Result { 654 | let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?; 655 | 656 | Ok(now.as_millis() as u64) 657 | } 658 | } 659 | -------------------------------------------------------------------------------- /src/info.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use ethers::types::Address; 4 | 5 | use crate::{ 6 | client::Client, 7 | error::Result, 8 | types::{ 9 | info::{ 10 | request::{CandleSnapshotRequest, Request}, 11 | response::{ 12 | AssetContext, CandleSnapshot, FrontendOpenOrders, FundingHistory, L2Book, 13 | OpenOrder, OrderStatus, RecentTrades, SpotMeta, SpotMetaAndAssetCtxs, SubAccount, 14 | Universe, UserFill, UserFunding, UserSpotState, UserState, 15 | }, 16 | }, 17 | Chain, Oid, API, 18 | }, 19 | }; 20 | 21 | /// Endpoint to fetch information about the exchange and specific users. 22 | pub struct Info { 23 | pub client: Client, 24 | pub chain: Chain, 25 | } 26 | 27 | impl Info { 28 | /// Retrieve exchange metadata 29 | pub async fn metadata(&self) -> Result { 30 | self.client.post(&API::Info, &Request::Meta).await 31 | } 32 | 33 | /// Retrieve all mids for all actively traded coins 34 | pub async fn mids(&self) -> Result> { 35 | self.client.post(&API::Info, &Request::AllMids).await 36 | } 37 | 38 | /// Retrieve asset contexts i.e mark price, current funding, open interest, etc 39 | pub async fn contexts(&self) -> Result> { 40 | self.client 41 | .post(&API::Info, &Request::MetaAndAssetCtxs) 42 | .await 43 | } 44 | 45 | /// Retrieve a user's state to see user's open positions and margin summary 46 | /// 47 | /// # Arguments 48 | /// * `user` - The user's address in 42-character hexadecimal format; e.g. `0x0000000000000000000000000000000000000000` 49 | pub async fn user_state(&self, user: Address) -> Result { 50 | self.client 51 | .post(&API::Info, &Request::ClearinghouseState { user }) 52 | .await 53 | } 54 | 55 | /// Retrieve a user's state to see user's open positions and margin summary in batch 56 | /// 57 | /// # Arguments 58 | /// * `users` - A list of user addresses in 42-character hexadecimal format 59 | pub async fn user_states(&self, users: Vec
) -> Result> { 60 | self.client 61 | .post(&API::Info, &Request::BatchClearinghouseStates { users }) 62 | .await 63 | } 64 | 65 | /// Retrieve a user's open orders 66 | /// 67 | /// # Arguments 68 | /// * `user` - The user's address in 42-character hexadecimal format; e.g. `0x0000000000000000000000000000000000000000` 69 | pub async fn open_orders(&self, user: Address) -> Result> { 70 | self.client 71 | .post(&API::Info, &Request::OpenOrders { user }) 72 | .await 73 | } 74 | 75 | /// Retrieve a user's open orders with additional frontend info. 76 | /// This is useful for displaying orders in a UI 77 | /// 78 | /// # Arguments 79 | /// * `user` - The user's address in 42-character hexadecimal format; e.g. `0x0000000000000000000000000000000000000000` 80 | pub async fn frontend_open_orders(&self, user: Address) -> Result> { 81 | self.client 82 | .post(&API::Info, &Request::FrontendOpenOrders { user }) 83 | .await 84 | } 85 | 86 | /// Retrieve a user's Userfills 87 | /// 88 | /// # Arguments 89 | /// * `user` - The user's address in 42-character hexadecimal format; e.g. `0x0000000000000000000000000000000000000000` 90 | pub async fn user_fills(&self, user: Address) -> Result> { 91 | self.client 92 | .post(&API::Info, &Request::UserFills { user }) 93 | .await 94 | } 95 | 96 | /// Retrieve a user's fills by time 97 | /// 98 | /// # Arguments 99 | /// * `user` - The user's address in 42-character hexadecimal format; e.g. `0x0000000000000000000000000000000000000000` 100 | /// * `start_time` - Start time in milliseconds, inclusive 101 | /// * `end_time` - End time in milliseconds, inclusive. If `None`, it will default to the current time 102 | /// 103 | /// # Note 104 | /// * Number of fills is limited to 2000 105 | pub async fn user_fills_by_time( 106 | &self, 107 | user: Address, 108 | start_time: u64, 109 | end_time: Option, 110 | ) -> Result> { 111 | self.client 112 | .post( 113 | &API::Info, 114 | &Request::UserFillsByTime { 115 | user, 116 | start_time, 117 | end_time, 118 | }, 119 | ) 120 | .await 121 | } 122 | 123 | /// Retrieve a user's funding history 124 | /// 125 | /// # Arguments 126 | /// * `user` - The user's address in 42-character hexadecimal format; e.g. `0x0000000000000000000000000000000000000000` 127 | /// * `start_time` - Start time in milliseconds, inclusive 128 | /// * `end_time` - End time in milliseconds, inclusive. If `None`, it will default to the current time 129 | pub async fn user_funding( 130 | &self, 131 | user: Address, 132 | start_time: u64, 133 | end_time: Option, 134 | ) -> Result> { 135 | self.client 136 | .post( 137 | &API::Info, 138 | &Request::UserFunding { 139 | user, 140 | start_time, 141 | end_time, 142 | }, 143 | ) 144 | .await 145 | } 146 | 147 | /// Retrieve historical funding rates for a coin 148 | /// 149 | /// # Arguments 150 | /// * `coin` - The coin to retrieve funding history for e.g `BTC`, `ETH`, etc 151 | /// * `start_time` - Start time in milliseconds, inclusive 152 | /// * `end_time` - End time in milliseconds, inclusive. If `None`, it will default to the current time 153 | pub async fn funding_history( 154 | &self, 155 | coin: String, 156 | start_time: u64, 157 | end_time: Option, 158 | ) -> Result> { 159 | self.client 160 | .post( 161 | &API::Info, 162 | &Request::FundingHistory { 163 | coin, 164 | start_time, 165 | end_time, 166 | }, 167 | ) 168 | .await 169 | } 170 | 171 | /// Retrieve the L2 order book for a coin 172 | /// 173 | /// # Arguments 174 | /// * `coin` - The coin to retrieve the L2 order book for e.g `BTC`, `ETH`, etc 175 | pub async fn l2_book(&self, coin: String) -> Result { 176 | self.client 177 | .post(&API::Info, &Request::L2Book { coin }) 178 | .await 179 | } 180 | 181 | /// Retrieve the recent trades for a coin 182 | /// 183 | /// # Arguments 184 | /// * `coin` - The coin to retrieve the recent trades for 185 | pub async fn recent_trades(&self, coin: String) -> Result> { 186 | self.client 187 | .post(&API::Info, &Request::RecentTrades { coin }) 188 | .await 189 | } 190 | 191 | /// Retrieve candle snapshot for a coin 192 | /// 193 | /// # Arguments 194 | /// * `coin` - The coin to retrieve the candle snapshot for e.g `BTC`, `ETH`, etc 195 | /// * `interval` - The interval to retrieve the candle snapshot for 196 | /// * `start_time` - Start time in milliseconds, inclusive 197 | /// * `end_time` - End time in milliseconds, inclusive. 198 | pub async fn candle_snapshot( 199 | &self, 200 | coin: String, 201 | interval: String, 202 | start_time: u64, 203 | end_time: u64, 204 | ) -> Result> { 205 | self.client 206 | .post( 207 | &API::Info, 208 | &Request::CandleSnapshot { 209 | req: CandleSnapshotRequest { 210 | coin, 211 | interval, 212 | start_time, 213 | end_time, 214 | }, 215 | }, 216 | ) 217 | .await 218 | } 219 | 220 | /// Query the status of an order by `oid` or `cloid` 221 | /// 222 | /// # Arguments 223 | /// * `user` - The user's address in 42-character hexadecimal format; e.g. `0x0000000000000000000000000000000000000000` 224 | /// * `oid` - The order id either u64 representing the order id or 16-byte hex string representing the client order id 225 | pub async fn order_status(&self, user: Address, oid: Oid) -> Result { 226 | self.client 227 | .post(&API::Info, &Request::OrderStatus { user, oid }) 228 | .await 229 | } 230 | 231 | /// Query user sub-accounts 232 | /// 233 | /// # Arguments 234 | /// * `user` - The user's address in 42-character hexadecimal format; e.g. `0x0000000000000000000000000000000000000000` 235 | pub async fn sub_accounts(&self, user: Address) -> Result>> { 236 | self.client 237 | .post(&API::Info, &Request::SubAccounts { user }) 238 | .await 239 | } 240 | } 241 | 242 | impl Info { 243 | /// Retrieve spot metadata 244 | pub async fn spot_meta(&self) -> Result { 245 | self.client.post(&API::Info, &Request::SpotMeta).await 246 | } 247 | 248 | /// Retrieve spot asset contexts 249 | pub async fn spot_meta_and_asset_ctxs(&self) -> Result> { 250 | self.client 251 | .post(&API::Info, &Request::SpotMetaAndAssetCtxs) 252 | .await 253 | } 254 | 255 | /// Retrieve a user's token balances 256 | /// 257 | /// # Arguments 258 | /// * `user` - The user's address in 42-character hexadecimal format; e.g. `0x0000000000000000000000000000000000000000` 259 | pub async fn spot_clearinghouse_state(&self, user: Address) -> Result { 260 | self.client 261 | .post(&API::Info, &Request::SpotClearinghouseState { user }) 262 | .await 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod api; 2 | mod client; 3 | mod config; 4 | mod error; 5 | mod exchange; 6 | mod info; 7 | mod websocket; 8 | 9 | pub use api::Hyperliquid; 10 | pub use config::Config; 11 | pub use error::{Error, Result}; 12 | pub use exchange::Exchange; 13 | pub use info::Info; 14 | pub use websocket::Websocket; 15 | 16 | pub mod types; 17 | pub mod utils; 18 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::as_hex; 2 | use serde::{Deserialize, Serialize}; 3 | use uuid::Uuid; 4 | 5 | #[derive(Debug, Serialize, Deserialize, Clone, Copy)] 6 | #[serde(rename_all = "PascalCase")] 7 | pub enum Chain { 8 | Dev = 1337, 9 | 10 | Arbitrum = 42161, 11 | ArbitrumTestnet = 421611, 12 | ArbitrumGoerli = 421613, 13 | ArbitrumSepolia = 421614, 14 | ArbitrumNova = 42170, 15 | } 16 | 17 | impl std::fmt::Display for Chain { 18 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 19 | write!( 20 | f, 21 | "{}", 22 | match self { 23 | Chain::Dev => "Dev", 24 | Chain::Arbitrum => "Arbitrum", 25 | Chain::ArbitrumTestnet => "ArbitrumTestnet", 26 | Chain::ArbitrumGoerli => "ArbitrumGoerli", 27 | Chain::ArbitrumSepolia => "ArbitrumSepolia", 28 | Chain::ArbitrumNova => "ArbitrumNova", 29 | } 30 | ) 31 | } 32 | } 33 | 34 | #[derive(Debug, Serialize, Deserialize, Clone, Copy)] 35 | #[serde(rename_all = "PascalCase")] 36 | pub enum HyperliquidChain { 37 | Mainnet, 38 | Testnet, 39 | } 40 | 41 | impl std::fmt::Display for HyperliquidChain { 42 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 43 | write!( 44 | f, 45 | "{}", 46 | match self { 47 | HyperliquidChain::Mainnet => "Mainnet", 48 | HyperliquidChain::Testnet => "Testnet", 49 | } 50 | ) 51 | } 52 | } 53 | 54 | pub enum API { 55 | Info, 56 | Exchange, 57 | } 58 | 59 | pub type Cloid = Uuid; 60 | 61 | #[derive(Debug, Serialize, Deserialize)] 62 | #[serde(untagged)] 63 | pub enum Oid { 64 | Order(u64), 65 | #[serde(serialize_with = "as_hex")] 66 | Cloid(Cloid), 67 | } 68 | 69 | #[derive(Debug, Serialize, Deserialize)] 70 | #[serde(rename_all = "UPPERCASE")] 71 | pub enum Side { 72 | B, 73 | A, 74 | } 75 | pub mod agent { 76 | pub mod l1 { 77 | use ethers::{ 78 | contract::{Eip712, EthAbiType}, 79 | types::H256, 80 | }; 81 | 82 | #[derive(Eip712, Clone, EthAbiType)] 83 | #[eip712( 84 | name = "Exchange", 85 | version = "1", 86 | chain_id = 1337, 87 | verifying_contract = "0x0000000000000000000000000000000000000000" 88 | )] 89 | pub struct Agent { 90 | pub source: String, 91 | pub connection_id: H256, 92 | } 93 | } 94 | } 95 | 96 | pub mod info { 97 | pub mod request { 98 | use ethers::types::Address; 99 | use serde::{Deserialize, Serialize}; 100 | 101 | use crate::types::Oid; 102 | 103 | #[derive(Debug, Serialize, Deserialize)] 104 | #[serde(rename_all = "camelCase")] 105 | pub struct CandleSnapshotRequest { 106 | pub coin: String, 107 | pub interval: String, 108 | pub start_time: u64, 109 | pub end_time: u64, 110 | } 111 | 112 | #[derive(Debug, Serialize, Deserialize)] 113 | #[serde(rename_all = "camelCase", tag = "type")] 114 | pub enum Request { 115 | Meta, 116 | AllMids, 117 | MetaAndAssetCtxs, 118 | ClearinghouseState { 119 | user: Address, 120 | }, 121 | BatchClearinghouseStates { 122 | users: Vec
, 123 | }, 124 | OpenOrders { 125 | user: Address, 126 | }, 127 | 128 | FrontendOpenOrders { 129 | user: Address, 130 | }, 131 | UserFills { 132 | user: Address, 133 | }, 134 | #[serde(rename_all = "camelCase")] 135 | UserFillsByTime { 136 | user: Address, 137 | start_time: u64, 138 | #[serde(skip_serializing_if = "Option::is_none")] 139 | end_time: Option, 140 | }, 141 | #[serde(rename_all = "camelCase")] 142 | UserFunding { 143 | user: Address, 144 | start_time: u64, 145 | end_time: Option, 146 | }, 147 | #[serde(rename_all = "camelCase")] 148 | FundingHistory { 149 | coin: String, 150 | start_time: u64, 151 | end_time: Option, 152 | }, 153 | L2Book { 154 | coin: String, 155 | }, 156 | RecentTrades { 157 | coin: String, 158 | }, 159 | CandleSnapshot { 160 | req: CandleSnapshotRequest, 161 | }, 162 | OrderStatus { 163 | user: Address, 164 | oid: Oid, 165 | }, 166 | SubAccounts { 167 | user: Address, 168 | }, 169 | 170 | SpotMeta, 171 | 172 | SpotMetaAndAssetCtxs, 173 | 174 | SpotClearinghouseState { 175 | user: Address, 176 | }, 177 | } 178 | } 179 | 180 | pub mod response { 181 | use ethers::types::Address; 182 | use serde::{Deserialize, Serialize}; 183 | 184 | use crate::types::Side; 185 | 186 | #[derive(Debug, Serialize, Deserialize)] 187 | #[serde(rename_all = "camelCase")] 188 | pub struct Asset { 189 | pub name: String, 190 | pub sz_decimals: u64, 191 | pub max_leverage: u64, 192 | pub only_isolated: bool, 193 | } 194 | 195 | #[derive(Debug, Serialize, Deserialize)] 196 | #[serde(rename_all = "camelCase")] 197 | pub struct Universe { 198 | pub universe: Vec, 199 | } 200 | 201 | #[derive(Debug, Serialize, Deserialize)] 202 | #[serde(untagged)] 203 | pub enum ImpactPx { 204 | String(String), 205 | StringArray(Vec), 206 | } 207 | 208 | #[derive(Debug, Serialize, Deserialize)] 209 | #[serde(rename_all = "camelCase")] 210 | pub struct Ctx { 211 | pub funding: String, 212 | pub open_interest: String, 213 | pub prev_day_px: String, 214 | pub day_ntl_vlm: String, 215 | pub premium: Option, 216 | pub oracle_px: String, 217 | pub mark_px: String, 218 | pub mid_px: Option, 219 | pub impact_pxs: Option, 220 | } 221 | 222 | #[derive(Debug, Serialize, Deserialize)] 223 | #[serde(untagged)] 224 | pub enum AssetContext { 225 | Meta(Universe), 226 | Ctx(Vec), 227 | } 228 | 229 | #[derive(Debug, Serialize, Deserialize)] 230 | #[serde(rename_all = "camelCase")] 231 | pub struct CumFunding { 232 | pub all_time: String, 233 | pub since_change: String, 234 | pub since_open: String, 235 | } 236 | 237 | #[derive(Debug, Serialize, Deserialize)] 238 | pub struct Leverage { 239 | #[serde(rename = "type")] 240 | pub type_: String, 241 | pub value: u32, 242 | } 243 | 244 | #[derive(Debug, Serialize, Deserialize)] 245 | #[serde(rename_all = "camelCase")] 246 | 247 | pub struct Position { 248 | pub coin: String, 249 | pub cum_funding: CumFunding, 250 | pub entry_px: Option, 251 | pub leverage: Leverage, 252 | pub liquidation_px: Option, 253 | pub margin_used: String, 254 | pub max_leverage: u32, 255 | pub position_value: String, 256 | pub return_on_equity: String, 257 | pub szi: String, 258 | pub unrealized_pnl: String, 259 | } 260 | 261 | #[derive(Debug, Serialize, Deserialize)] 262 | #[serde(rename_all = "camelCase")] 263 | pub struct AssetPosition { 264 | pub position: Position, 265 | #[serde(rename = "type")] 266 | pub type_: String, 267 | } 268 | 269 | #[derive(Debug, Serialize, Deserialize)] 270 | #[serde(rename_all = "camelCase")] 271 | pub struct MarginSummary { 272 | pub account_value: String, 273 | pub total_margin_used: String, 274 | pub total_ntl_pos: String, 275 | pub total_raw_usd: String, 276 | } 277 | 278 | #[derive(Debug, Serialize, Deserialize)] 279 | #[serde(rename_all = "camelCase")] 280 | pub struct UserState { 281 | pub asset_positions: Vec, 282 | pub margin_summary: MarginSummary, 283 | pub cross_margin_summary: MarginSummary, 284 | pub withdrawable: String, 285 | pub time: u64, 286 | pub cross_maintenance_margin_used: String, 287 | } 288 | 289 | #[derive(Debug, Serialize, Deserialize)] 290 | #[serde(rename_all = "camelCase")] 291 | pub struct OpenOrder { 292 | pub coin: String, 293 | pub limit_px: String, 294 | pub oid: u64, 295 | pub side: Side, 296 | pub sz: String, 297 | pub timestamp: u64, 298 | } 299 | 300 | #[derive(Debug, Serialize, Deserialize)] 301 | #[serde(rename_all = "camelCase")] 302 | pub struct FrontendOpenOrders { 303 | pub coin: String, 304 | pub is_position_tpsl: bool, 305 | pub is_trigger: bool, 306 | pub limit_px: String, 307 | pub oid: u64, 308 | pub order_type: String, 309 | pub orig_sz: String, 310 | pub reduce_only: bool, 311 | pub side: Side, 312 | pub sz: String, 313 | pub timestamp: u64, 314 | pub trigger_condition: String, 315 | pub trigger_px: String, 316 | } 317 | 318 | #[derive(Debug, Serialize, Deserialize)] 319 | #[serde(rename_all = "camelCase")] 320 | pub struct UserFill { 321 | pub coin: String, 322 | pub px: String, 323 | pub sz: String, 324 | pub side: Side, 325 | pub time: u64, 326 | pub start_position: String, 327 | pub dir: String, 328 | pub closed_pnl: String, 329 | pub hash: String, 330 | pub oid: u64, 331 | pub crossed: bool, 332 | pub fee: String, 333 | } 334 | 335 | #[derive(Debug, Serialize, Deserialize)] 336 | #[serde(rename_all = "camelCase")] 337 | pub struct Delta { 338 | pub coin: String, 339 | pub funding_rate: String, 340 | pub szi: String, 341 | #[serde(rename = "type")] 342 | pub type_: String, 343 | pub usdc: String, 344 | } 345 | 346 | #[derive(Debug, Serialize, Deserialize)] 347 | #[serde(rename_all = "camelCase")] 348 | pub struct UserFunding { 349 | pub delta: Delta, 350 | pub hash: String, 351 | pub time: u64, 352 | } 353 | 354 | #[derive(Debug, Serialize, Deserialize)] 355 | #[serde(rename_all = "camelCase")] 356 | pub struct FundingHistory { 357 | pub coin: String, 358 | pub funding_rate: String, 359 | pub premium: String, 360 | pub time: u64, 361 | } 362 | 363 | #[derive(Debug, Serialize, Deserialize)] 364 | #[serde(rename_all = "camelCase")] 365 | pub struct Level { 366 | pub px: String, 367 | pub sz: String, 368 | pub n: u64, 369 | } 370 | 371 | #[derive(Debug, Serialize, Deserialize)] 372 | pub struct L2Book { 373 | pub coin: String, 374 | pub levels: Vec>, 375 | pub time: u64, 376 | } 377 | 378 | #[derive(Debug, Serialize, Deserialize)] 379 | #[serde(rename_all = "camelCase")] 380 | pub struct RecentTrades { 381 | pub coin: String, 382 | pub side: Side, 383 | pub px: String, 384 | pub sz: String, 385 | pub hash: String, 386 | pub time: u64, 387 | } 388 | 389 | #[derive(Debug, Serialize, Deserialize)] 390 | #[serde(rename_all = "camelCase")] 391 | pub struct CandleSnapshot { 392 | #[serde(rename = "T")] 393 | pub t_: u64, 394 | pub c: String, 395 | pub h: String, 396 | pub i: String, 397 | pub l: String, 398 | pub n: u64, 399 | pub o: String, 400 | pub s: String, 401 | pub t: u64, 402 | pub v: String, 403 | } 404 | 405 | #[derive(Debug, Serialize, Deserialize)] 406 | #[serde(rename_all = "camelCase")] 407 | pub struct OrderInfo { 408 | pub children: Vec>, 409 | pub cloid: Option, 410 | pub coin: String, 411 | pub is_position_tpsl: bool, 412 | pub is_trigger: bool, 413 | pub limit_px: String, 414 | pub oid: i64, 415 | pub order_type: String, 416 | pub orig_sz: String, 417 | pub reduce_only: bool, 418 | pub side: String, 419 | pub sz: String, 420 | pub tif: Option, 421 | pub timestamp: i64, 422 | pub trigger_condition: String, 423 | pub trigger_px: String, 424 | } 425 | 426 | #[derive(Debug, Serialize, Deserialize)] 427 | #[serde(rename_all = "camelCase")] 428 | pub struct Order { 429 | pub order: OrderInfo, 430 | pub status: String, 431 | pub status_timestamp: i64, 432 | } 433 | 434 | #[derive(Debug, Serialize, Deserialize)] 435 | #[serde(rename_all = "camelCase")] 436 | pub struct OrderStatus { 437 | pub order: Option, 438 | pub status: String, 439 | } 440 | 441 | #[derive(Debug, Serialize, Deserialize)] 442 | #[serde(rename_all = "camelCase")] 443 | pub struct SubAccount { 444 | pub clearinghouse_state: UserState, 445 | pub master: Address, 446 | pub name: String, 447 | pub sub_account_user: Address, 448 | } 449 | 450 | #[derive(Debug, Serialize, Deserialize)] 451 | #[serde(rename_all = "camelCase")] 452 | pub struct SpotAsset { 453 | pub index: u64, 454 | pub is_canonical: bool, 455 | pub name: String, 456 | pub sz_decimals: u64, 457 | pub token_id: String, 458 | pub wei_decimals: u64, 459 | } 460 | 461 | #[derive(Debug, Serialize, Deserialize)] 462 | #[serde(rename_all = "camelCase")] 463 | pub struct SpotUniverse { 464 | pub index: u64, 465 | pub is_canonical: bool, 466 | pub name: String, 467 | pub tokens: Vec, 468 | } 469 | 470 | #[derive(Debug, Serialize, Deserialize)] 471 | #[serde(rename_all = "camelCase")] 472 | pub struct SpotMeta { 473 | pub tokens: Vec, 474 | pub universe: Vec, 475 | } 476 | 477 | #[derive(Debug, Serialize, Deserialize)] 478 | #[serde(rename_all = "camelCase")] 479 | pub struct SpotCtx { 480 | pub circulating_supply: String, 481 | pub coin: String, 482 | pub day_ntl_vlm: String, 483 | pub mark_px: String, 484 | pub mid_px: Option, 485 | pub prev_day_px: String, 486 | } 487 | 488 | #[derive(Debug, Serialize, Deserialize)] 489 | #[serde(untagged)] 490 | pub enum SpotMetaAndAssetCtxs { 491 | Meta(SpotMeta), 492 | Ctx(Vec), 493 | } 494 | 495 | #[derive(Debug, Serialize, Deserialize)] 496 | pub struct Balance { 497 | pub coin: String, 498 | pub hold: String, 499 | pub total: String, 500 | } 501 | 502 | #[derive(Debug, Serialize, Deserialize)] 503 | #[serde(rename_all = "camelCase")] 504 | pub struct UserSpotState { 505 | pub balances: Vec, 506 | } 507 | } 508 | } 509 | 510 | pub mod exchange { 511 | pub mod request { 512 | 513 | use ethers::{ 514 | abi::{encode, ParamType, Token, Tokenizable}, 515 | types::{ 516 | transaction::eip712::{ 517 | encode_eip712_type, make_type_hash, EIP712Domain, Eip712, Eip712Error, 518 | }, 519 | Address, Signature, H256, U256, 520 | }, 521 | utils::keccak256, 522 | }; 523 | use serde::{Deserialize, Serialize}; 524 | 525 | use crate::{ 526 | types::{Cloid, HyperliquidChain}, 527 | utils::{as_hex, as_hex_option}, 528 | Error, Result, 529 | }; 530 | 531 | #[derive(Debug, Serialize, Deserialize)] 532 | #[serde(rename_all = "PascalCase")] 533 | pub enum Tif { 534 | Gtc, 535 | Ioc, 536 | Alo, 537 | FrontendMarket, 538 | } 539 | 540 | #[derive(Debug, Serialize, Deserialize)] 541 | #[serde(rename_all = "camelCase")] 542 | pub struct Limit { 543 | pub tif: Tif, 544 | } 545 | 546 | #[derive(Debug, Serialize, Deserialize)] 547 | #[serde(rename_all = "lowercase")] 548 | pub enum TpSl { 549 | Tp, 550 | Sl, 551 | } 552 | 553 | #[derive(Debug, Serialize, Deserialize)] 554 | #[serde(rename_all = "camelCase")] 555 | pub struct Trigger { 556 | pub is_market: bool, 557 | pub trigger_px: String, 558 | pub tpsl: TpSl, 559 | } 560 | 561 | #[derive(Debug, Serialize, Deserialize)] 562 | #[serde(rename_all = "camelCase")] 563 | pub enum OrderType { 564 | Limit(Limit), 565 | Trigger(Trigger), 566 | } 567 | 568 | #[derive(Debug, Serialize, Deserialize)] 569 | #[serde(rename_all = "camelCase")] 570 | pub struct OrderRequest { 571 | #[serde(rename = "a", alias = "asset")] 572 | pub asset: u32, 573 | #[serde(rename = "b", alias = "isBuy")] 574 | pub is_buy: bool, 575 | #[serde(rename = "p", alias = "limitPx")] 576 | pub limit_px: String, 577 | #[serde(rename = "s", alias = "sz")] 578 | pub sz: String, 579 | #[serde(rename = "r", alias = "reduceOnly", default)] 580 | pub reduce_only: bool, 581 | #[serde(rename = "t", alias = "orderType")] 582 | pub order_type: OrderType, 583 | #[serde( 584 | rename = "c", 585 | alias = "cloid", 586 | serialize_with = "as_hex_option", 587 | skip_serializing_if = "Option::is_none" 588 | )] 589 | pub cloid: Option, 590 | } 591 | 592 | #[derive(Debug, Serialize, Deserialize)] 593 | #[serde(rename_all = "camelCase")] 594 | pub enum Grouping { 595 | Na, 596 | NormalTpsl, 597 | } 598 | 599 | #[derive(Debug, Serialize, Deserialize)] 600 | #[serde(rename_all = "camelCase")] 601 | pub struct CancelRequest { 602 | #[serde(rename = "a", alias = "asset")] 603 | pub asset: u32, 604 | #[serde(rename = "o", alias = "oid")] 605 | pub oid: u64, 606 | } 607 | 608 | #[derive(Debug, Serialize, Deserialize)] 609 | #[serde(rename_all = "camelCase")] 610 | pub struct CancelByCloidRequest { 611 | pub asset: u32, 612 | #[serde(serialize_with = "as_hex")] 613 | pub cloid: Cloid, 614 | } 615 | 616 | #[derive(Debug, Serialize, Deserialize)] 617 | #[serde(rename_all = "camelCase")] 618 | pub struct ModifyRequest { 619 | pub oid: u64, 620 | pub order: OrderRequest, 621 | } 622 | 623 | #[derive(Debug, Serialize, Deserialize)] 624 | #[serde(rename_all = "camelCase")] 625 | pub struct TwapRequest { 626 | #[serde(rename = "a", alias = "asset")] 627 | pub asset: u32, 628 | #[serde(rename = "b", alias = "isBuy")] 629 | pub is_buy: bool, 630 | #[serde(rename = "s", alias = "sz")] 631 | pub sz: String, 632 | #[serde(rename = "r", alias = "reduceOnly", default)] 633 | pub reduce_only: bool, 634 | /// Running Time (5m - 24h) 635 | #[serde(rename = "m", alias = "duration")] 636 | pub duration: u64, 637 | /// if set to true, the size of each sub-trade will be automatically adjusted 638 | /// within a certain range, typically upto to 20% higher or lower than the original trade size 639 | #[serde(rename = "t", alias = "randomize")] 640 | pub randomize: bool, 641 | } 642 | 643 | #[derive(Debug, Serialize, Deserialize)] 644 | #[serde(rename_all = "camelCase")] 645 | pub struct Withdraw3 { 646 | pub signature_chain_id: U256, 647 | pub hyperliquid_chain: HyperliquidChain, 648 | pub destination: String, 649 | pub amount: String, 650 | pub time: u64, 651 | } 652 | 653 | impl Eip712 for Withdraw3 { 654 | type Error = Eip712Error; 655 | 656 | fn domain(&self) -> std::result::Result { 657 | Ok(EIP712Domain { 658 | name: Some("HyperliquidSignTransaction".into()), 659 | version: Some("1".into()), 660 | chain_id: Some(self.signature_chain_id), 661 | verifying_contract: Some(Address::zero()), 662 | salt: None, 663 | }) 664 | } 665 | 666 | fn type_hash() -> std::result::Result<[u8; 32], Self::Error> { 667 | Ok(make_type_hash( 668 | "HyperliquidTransaction:Withdraw".into(), 669 | &[ 670 | ("hyperliquidChain".to_string(), ParamType::String), 671 | ("destination".to_string(), ParamType::String), 672 | ("amount".to_string(), ParamType::String), 673 | ("time".to_string(), ParamType::Uint(64)), 674 | ], 675 | )) 676 | } 677 | 678 | fn struct_hash(&self) -> std::result::Result<[u8; 32], Self::Error> { 679 | Ok(keccak256(encode(&[ 680 | Token::Uint(Self::type_hash()?.into()), 681 | encode_eip712_type(self.hyperliquid_chain.to_string().into_token()), 682 | encode_eip712_type(self.destination.clone().into_token()), 683 | encode_eip712_type(self.amount.clone().into_token()), 684 | encode_eip712_type(self.time.into_token()), 685 | ]))) 686 | } 687 | } 688 | 689 | #[derive(Debug, Serialize, Deserialize)] 690 | #[serde(rename_all = "camelCase")] 691 | pub struct Agent { 692 | pub source: String, 693 | pub connection_id: H256, 694 | } 695 | 696 | #[derive(Debug, Serialize, Deserialize)] 697 | #[serde(rename_all = "camelCase")] 698 | pub struct UsdSend { 699 | pub signature_chain_id: U256, 700 | pub hyperliquid_chain: HyperliquidChain, 701 | pub destination: String, 702 | pub amount: String, 703 | pub time: u64, 704 | } 705 | 706 | impl Eip712 for UsdSend { 707 | type Error = Eip712Error; 708 | 709 | fn domain(&self) -> std::result::Result { 710 | Ok(EIP712Domain { 711 | name: Some("HyperliquidSignTransaction".into()), 712 | version: Some("1".into()), 713 | chain_id: Some(self.signature_chain_id), 714 | verifying_contract: Some(Address::zero()), 715 | salt: None, 716 | }) 717 | } 718 | 719 | fn type_hash() -> std::result::Result<[u8; 32], Self::Error> { 720 | Ok(make_type_hash( 721 | "HyperliquidTransaction:UsdSend".into(), 722 | &[ 723 | ("hyperliquidChain".to_string(), ParamType::String), 724 | ("destination".to_string(), ParamType::String), 725 | ("amount".to_string(), ParamType::String), 726 | ("time".to_string(), ParamType::Uint(64)), 727 | ], 728 | )) 729 | } 730 | 731 | fn struct_hash(&self) -> std::result::Result<[u8; 32], Self::Error> { 732 | Ok(keccak256(encode(&[ 733 | Token::Uint(Self::type_hash()?.into()), 734 | encode_eip712_type(self.hyperliquid_chain.to_string().into_token()), 735 | encode_eip712_type(self.destination.clone().into_token()), 736 | encode_eip712_type(self.amount.clone().into_token()), 737 | encode_eip712_type(self.time.into_token()), 738 | ]))) 739 | } 740 | } 741 | 742 | #[derive(Debug, Serialize, Deserialize)] 743 | #[serde(rename_all = "camelCase")] 744 | pub struct ApproveAgent { 745 | pub signature_chain_id: U256, 746 | pub hyperliquid_chain: HyperliquidChain, 747 | pub agent_address: Address, 748 | #[serde(skip_serializing_if = "Option::is_none")] 749 | pub agent_name: Option, 750 | pub nonce: u64, 751 | } 752 | 753 | impl Eip712 for ApproveAgent { 754 | type Error = Eip712Error; 755 | 756 | fn domain(&self) -> std::result::Result { 757 | Ok(EIP712Domain { 758 | name: Some("HyperliquidSignTransaction".into()), 759 | version: Some("1".into()), 760 | chain_id: Some(self.signature_chain_id), 761 | verifying_contract: Some(Address::zero()), 762 | salt: None, 763 | }) 764 | } 765 | 766 | fn type_hash() -> std::result::Result<[u8; 32], Self::Error> { 767 | Ok(make_type_hash( 768 | "HyperliquidTransaction:ApproveAgent".into(), 769 | &[ 770 | ("hyperliquidChain".to_string(), ParamType::String), 771 | ("agentAddress".to_string(), ParamType::Address), 772 | ("agentName".to_string(), ParamType::String), 773 | ("nonce".to_string(), ParamType::Uint(64)), 774 | ], 775 | )) 776 | } 777 | 778 | fn struct_hash(&self) -> std::result::Result<[u8; 32], Self::Error> { 779 | Ok(keccak256(encode(&[ 780 | Token::Uint(Self::type_hash()?.into()), 781 | encode_eip712_type(self.hyperliquid_chain.to_string().into_token()), 782 | encode_eip712_type(self.agent_address.into_token()), 783 | encode_eip712_type(self.agent_name.clone().unwrap_or_default().into_token()), 784 | encode_eip712_type(self.nonce.into_token()), 785 | ]))) 786 | } 787 | } 788 | 789 | #[derive(Debug, Serialize, Deserialize)] 790 | #[serde(rename_all = "camelCase", tag = "type")] 791 | pub enum Action { 792 | Order { 793 | orders: Vec, 794 | grouping: Grouping, 795 | }, 796 | Cancel { 797 | cancels: Vec, 798 | }, 799 | CancelByCloid { 800 | cancels: Vec, 801 | }, 802 | 803 | Modify(ModifyRequest), 804 | 805 | BatchModify { 806 | modifies: Vec, 807 | }, 808 | TwapOrder { 809 | twap: TwapRequest, 810 | }, 811 | UsdSend(UsdSend), 812 | 813 | Withdraw3(Withdraw3), 814 | #[serde(rename_all = "camelCase")] 815 | UpdateLeverage { 816 | asset: u32, 817 | is_cross: bool, 818 | leverage: u32, 819 | }, 820 | #[serde(rename_all = "camelCase")] 821 | UpdateIsolatedMargin { 822 | asset: u32, 823 | is_buy: bool, 824 | ntli: i64, 825 | }, 826 | ApproveAgent(ApproveAgent), 827 | CreateSubAccount { 828 | name: String, 829 | }, 830 | #[serde(rename_all = "camelCase")] 831 | SubAccountModify { 832 | sub_account_user: Address, 833 | name: String, 834 | }, 835 | #[serde(rename_all = "camelCase")] 836 | SubAccountTransfer { 837 | sub_account_user: Address, 838 | is_deposit: bool, 839 | usd: u64, 840 | }, 841 | SetReferrer { 842 | code: String, 843 | }, 844 | ScheduleCancel { 845 | time: u64, 846 | }, 847 | } 848 | 849 | impl Action { 850 | /// create connection id for agent 851 | pub fn connection_id( 852 | &self, 853 | vault_address: Option
, 854 | nonce: u64, 855 | ) -> Result { 856 | let mut encoded = rmp_serde::to_vec_named(self) 857 | .map_err(|e| Error::RmpSerdeError(e.to_string()))?; 858 | 859 | encoded.extend((nonce).to_be_bytes()); 860 | 861 | if let Some(address) = vault_address { 862 | encoded.push(1); 863 | encoded.extend(address.to_fixed_bytes()); 864 | } else { 865 | encoded.push(0) 866 | } 867 | 868 | Ok(keccak256(encoded).into()) 869 | } 870 | } 871 | 872 | #[derive(Debug, Serialize, Deserialize)] 873 | #[serde(rename_all = "camelCase")] 874 | pub struct Request { 875 | pub action: Action, 876 | pub nonce: u64, 877 | pub signature: Signature, 878 | #[serde(skip_serializing_if = "Option::is_none")] 879 | pub vault_address: Option
, 880 | } 881 | } 882 | 883 | pub mod response { 884 | use ethers::types::Address; 885 | use serde::{Deserialize, Serialize}; 886 | 887 | #[derive(Debug, Serialize, Deserialize)] 888 | pub struct Resting { 889 | pub oid: u64, 890 | } 891 | 892 | #[derive(Debug, Serialize, Deserialize)] 893 | #[serde(rename_all = "camelCase")] 894 | pub struct Filled { 895 | pub oid: u64, 896 | pub total_sz: String, 897 | pub avg_px: String, 898 | } 899 | 900 | #[derive(Debug, Serialize, Deserialize)] 901 | #[serde(rename_all = "camelCase")] 902 | pub struct TwapId { 903 | pub twap_id: u64, 904 | } 905 | 906 | #[derive(Debug, Serialize, Deserialize)] 907 | #[serde(rename_all = "camelCase")] 908 | pub enum Status { 909 | Resting(Resting), 910 | Filled(Filled), 911 | Error(String), 912 | Success, 913 | WaitingForFill, 914 | WaitingForTrigger, 915 | Running(TwapId), 916 | } 917 | 918 | #[derive(Debug, Serialize, Deserialize)] 919 | #[serde(rename_all = "camelCase")] 920 | pub enum StatusType { 921 | Statuses(Vec), 922 | Status(Status), 923 | #[serde(untagged)] 924 | Address(Address), 925 | } 926 | 927 | #[derive(Debug, Serialize, Deserialize)] 928 | pub struct Data { 929 | #[serde(rename = "type")] 930 | pub type_: String, 931 | pub data: Option, 932 | } 933 | 934 | #[derive(Debug, Serialize, Deserialize)] 935 | #[serde(rename_all = "camelCase", tag = "status", content = "response")] 936 | pub enum Response { 937 | Ok(Data), 938 | Err(String), 939 | } 940 | } 941 | } 942 | 943 | pub mod websocket { 944 | pub mod request { 945 | use ethers::types::Address; 946 | use serde::{Deserialize, Serialize}; 947 | 948 | #[derive(Debug, Serialize, Deserialize, Clone)] 949 | #[serde(rename_all = "camelCase", tag = "type")] 950 | pub enum Subscription { 951 | AllMids, 952 | Notification { user: Address }, 953 | OrderUpdates { user: Address }, 954 | User { user: Address }, 955 | WebData { user: Address }, 956 | L2Book { coin: String }, 957 | Trades { coin: String }, 958 | Candle { coin: String, interval: String }, 959 | } 960 | 961 | #[derive(Clone)] 962 | pub struct Channel { 963 | pub id: u64, 964 | pub sub: Subscription, 965 | } 966 | 967 | #[derive(Debug, Serialize, Deserialize)] 968 | #[serde(rename_all = "lowercase")] 969 | pub enum Method { 970 | Subscribe, 971 | Unsubscribe, 972 | } 973 | 974 | #[derive(Debug, Serialize, Deserialize)] 975 | #[serde(rename_all = "camelCase")] 976 | pub struct Request { 977 | pub method: Method, 978 | pub subscription: Subscription, 979 | } 980 | } 981 | 982 | pub mod response { 983 | use std::collections::HashMap; 984 | 985 | use ethers::types::{Address, TxHash}; 986 | use serde::{Deserialize, Serialize}; 987 | use serde_json::Value; 988 | 989 | use crate::types::{ 990 | info::response::{CandleSnapshot, Ctx, Universe, UserFill, UserState}, 991 | Side, 992 | }; 993 | 994 | #[derive(Debug, Serialize, Deserialize)] 995 | pub struct AllMids { 996 | pub mids: HashMap, 997 | } 998 | 999 | #[derive(Debug, Serialize, Deserialize)] 1000 | pub struct Notification { 1001 | pub notification: String, 1002 | } 1003 | 1004 | #[derive(Debug, Serialize, Deserialize)] 1005 | pub struct LedgerUpdate { 1006 | pub hash: TxHash, 1007 | pub delta: Value, 1008 | pub time: u64, 1009 | } 1010 | 1011 | #[derive(Debug, Serialize, Deserialize)] 1012 | #[serde(rename_all = "camelCase")] 1013 | pub struct WebData { 1014 | pub user_state: UserState, 1015 | pub lending_vaults: Option>, 1016 | pub total_vault_equity: String, 1017 | pub open_orders: Vec, 1018 | pub fills: Vec, 1019 | pub whitelisted: bool, 1020 | pub ledger_updates: Vec, 1021 | pub agent_address: Address, 1022 | pub pending_withdraws: Option>, 1023 | pub cum_ledger: String, 1024 | pub meta: Universe, 1025 | pub asset_contexts: Option>, 1026 | pub order_history: Vec, 1027 | pub server_time: u64, 1028 | pub is_vault: bool, 1029 | pub user: Address, 1030 | } 1031 | 1032 | #[derive(Debug, Serialize, Deserialize)] 1033 | pub struct WsTrade { 1034 | pub coin: String, 1035 | pub side: String, 1036 | pub px: String, 1037 | pub sz: String, 1038 | pub hash: TxHash, 1039 | pub time: u64, 1040 | } 1041 | 1042 | #[derive(Debug, Serialize, Deserialize)] 1043 | pub struct WsLevel { 1044 | pub px: String, 1045 | pub sz: String, 1046 | pub n: u64, 1047 | } 1048 | 1049 | #[derive(Debug, Serialize, Deserialize)] 1050 | pub struct WsBook { 1051 | pub coin: String, 1052 | pub levels: Vec>, 1053 | pub time: u64, 1054 | } 1055 | 1056 | #[derive(Debug, Serialize, Deserialize)] 1057 | #[serde(rename_all = "camelCase")] 1058 | pub struct WsBasicOrder { 1059 | pub coin: String, 1060 | pub side: Side, 1061 | pub limit_px: String, 1062 | pub sz: String, 1063 | pub oid: u64, 1064 | pub timestamp: u64, 1065 | pub orig_sz: String, 1066 | #[serde(default)] 1067 | pub reduce_only: bool, 1068 | } 1069 | 1070 | #[derive(Debug, Serialize, Deserialize)] 1071 | #[serde(rename_all = "camelCase")] 1072 | pub struct WsOrder { 1073 | pub order: WsBasicOrder, 1074 | pub status: String, 1075 | pub status_timestamp: u64, 1076 | } 1077 | 1078 | #[derive(Debug, Serialize, Deserialize)] 1079 | #[serde(rename_all = "camelCase")] 1080 | pub struct WsUserFunding { 1081 | pub time: u64, 1082 | pub coin: String, 1083 | pub usdc: String, 1084 | pub szi: String, 1085 | pub funding_rate: String, 1086 | } 1087 | 1088 | #[derive(Debug, Serialize, Deserialize)] 1089 | #[serde(rename_all = "snake_case")] 1090 | pub struct WsLiquidation { 1091 | pub liq: u64, 1092 | pub liquidator: String, 1093 | pub liquidated_user: String, 1094 | pub liquidated_ntl_pos: String, 1095 | pub liquidated_account_value: String, 1096 | } 1097 | 1098 | #[derive(Debug, Serialize, Deserialize)] 1099 | #[serde(rename_all = "camelCase")] 1100 | pub struct WsNonUserCancel { 1101 | pub oid: u64, 1102 | pub coin: String, 1103 | } 1104 | 1105 | #[derive(Debug, Serialize, Deserialize)] 1106 | #[serde(rename_all = "camelCase", untagged)] 1107 | pub enum WsUserEvent { 1108 | WsFill(Vec), 1109 | WsUserFunding(WsUserFunding), 1110 | WsLiquidation(WsLiquidation), 1111 | WsNonUserCancel(Vec), 1112 | } 1113 | 1114 | #[derive(Debug, Serialize, Deserialize)] 1115 | pub struct Channel { 1116 | pub method: String, 1117 | pub subscription: Value, 1118 | } 1119 | 1120 | #[derive(Debug, Serialize, Deserialize)] 1121 | #[serde(rename_all = "camelCase", tag = "channel", content = "data")] 1122 | pub enum Response { 1123 | AllMids(AllMids), 1124 | Notification(Notification), 1125 | WebData(WebData), 1126 | Candle(CandleSnapshot), 1127 | L2Book(WsBook), 1128 | Trades(Vec), 1129 | OrderUpdates(Vec), 1130 | User(WsUserEvent), 1131 | SubscriptionResponse(Channel), 1132 | } 1133 | } 1134 | } 1135 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use serde::Serializer; 2 | use uuid::Uuid; 3 | 4 | /// Parse price to the accepted number of decimals 5 | /// Prices can have up to 5 significant figures, but no more than 6 decimals places 6 | /// 7 | /// # Examples 8 | /// ``` 9 | /// use hyperliquid::utils::parse_price; 10 | /// 11 | /// assert_eq!(parse_price(1234.5), "1234.5"); 12 | /// assert_eq!(parse_price(1234.56), "1234.5"); 13 | /// assert_eq!(parse_price(0.001234), "0.001234"); 14 | /// assert_eq!(parse_price(0.0012345), "0.001234"); 15 | /// assert_eq!(parse_price(1.2345678), "1.2345"); 16 | /// ``` 17 | pub fn parse_price(px: f64) -> String { 18 | let px = format!("{px:.6}"); 19 | 20 | let px = if px.starts_with("0.") { 21 | px 22 | } else { 23 | let px: Vec<&str> = px.split('.').collect(); 24 | let whole = px[0]; 25 | let decimals = px[1]; 26 | 27 | let diff = 5 - whole.len(); // 0 28 | let sep = if diff > 0 { "." } else { "" }; 29 | 30 | format!("{whole}{sep}{decimals:.0$}", diff) 31 | }; 32 | 33 | let px = remove_trailing_zeros(&px); 34 | 35 | positive(px) 36 | } 37 | 38 | /// Parse size to the accepted number of decimals. 39 | /// Sizes are rounded to the szDecimals of that asset. 40 | /// For example, if szDecimals = 3 then 1.001 is a valid size but 1.0001 is not 41 | /// You can find the szDecimals for an asset by making a `meta` request to the `info` endpoint 42 | /// 43 | /// # Examples 44 | /// ``` 45 | /// use hyperliquid::utils::parse_size; 46 | /// 47 | /// assert_eq!(parse_size(1.001, 3), "1.001"); 48 | /// assert_eq!(parse_size(1.001, 2), "1"); 49 | /// assert_eq!(parse_size(1.0001, 3), "1"); 50 | /// assert_eq!(parse_size(1000.0, 0), "1000"); 51 | /// ``` 52 | 53 | pub fn parse_size(sz: f64, sz_decimals: u32) -> String { 54 | let sz = format!("{sz:.0$}", sz_decimals as usize); 55 | 56 | let px = remove_trailing_zeros(&sz); 57 | 58 | positive(px) 59 | } 60 | 61 | fn remove_trailing_zeros(s: &str) -> String { 62 | let mut s = s.to_string(); 63 | while s.ends_with('0') && s.contains('.') { 64 | s.pop(); 65 | } 66 | if s.ends_with('.') { 67 | s.pop(); 68 | } 69 | s 70 | } 71 | 72 | fn positive(value: String) -> String { 73 | if value.starts_with('-') { 74 | "0".to_string() 75 | } else { 76 | value 77 | } 78 | } 79 | 80 | pub fn as_hex_option(cloid: &Option, s: S) -> Result 81 | where 82 | S: Serializer, 83 | { 84 | if let Some(cloid) = cloid { 85 | s.serialize_str(&format!("0x{}", cloid.simple())) 86 | } else { 87 | s.serialize_none() 88 | } 89 | } 90 | 91 | pub fn as_hex(cloid: &Uuid, s: S) -> Result 92 | where 93 | S: Serializer, 94 | { 95 | s.serialize_str(&format!("0x{}", cloid.simple())) 96 | } 97 | -------------------------------------------------------------------------------- /src/websocket.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use futures_util::{Future, SinkExt, StreamExt}; 4 | use tokio::net::TcpStream; 5 | use tokio_tungstenite::{connect_async, tungstenite::Message, MaybeTlsStream, WebSocketStream}; 6 | 7 | use crate::{ 8 | error::{Error, Result}, 9 | types::websocket::{ 10 | request::{Channel, Method, Request}, 11 | response::Response, 12 | }, 13 | }; 14 | 15 | pub struct Websocket { 16 | pub stream: Option>>, 17 | pub channels: HashMap, 18 | pub url: String, 19 | } 20 | 21 | impl Websocket { 22 | /// Returns `true` if the websocket is connected 23 | pub async fn is_connected(&self) -> bool { 24 | self.stream.is_some() 25 | } 26 | 27 | /// Connect to the websocket 28 | pub async fn connect(&mut self) -> Result<()> { 29 | let (stream, _) = connect_async(&self.url).await?; 30 | self.stream = Some(stream); 31 | 32 | Ok(()) 33 | } 34 | 35 | /// Disconnect from the websocket 36 | pub async fn disconnect(&mut self) -> Result<()> { 37 | self.unsubscribe_all().await?; 38 | 39 | self.stream = None; 40 | Ok(()) 41 | } 42 | 43 | /// Subscribe to the given channels 44 | /// - `channels` - The channels to subscribe to 45 | pub async fn subscribe(&mut self, channels: &[Channel]) -> Result<()> { 46 | self.send(channels, true).await?; 47 | 48 | channels.iter().for_each(|channel| { 49 | self.channels.insert(channel.id, channel.clone()); 50 | }); 51 | 52 | Ok(()) 53 | } 54 | 55 | /// Unsubscribe from the given channels 56 | /// - `channels` - The channels to unsubscribe from 57 | pub async fn unsubscribe(&mut self, ids: &[u64]) -> Result<()> { 58 | let channels = ids 59 | .iter() 60 | .map(|id| { 61 | self.channels 62 | .get(id) 63 | .ok_or_else(|| Error::NotSubscribed(*id)) 64 | .cloned() 65 | }) 66 | .collect::>>()?; 67 | 68 | self.send(&channels, false).await?; 69 | 70 | channels.iter().for_each(|channel| { 71 | self.channels.remove(&channel.id); 72 | }); 73 | 74 | Ok(()) 75 | } 76 | 77 | /// Unsubscribe from all channels 78 | pub async fn unsubscribe_all(&mut self) -> Result<()> { 79 | let channels: Vec = self.channels.values().cloned().collect(); 80 | 81 | self.send(&channels, false).await 82 | } 83 | 84 | pub async fn next(&mut self, handler: F) -> Result> 85 | where 86 | F: Fn(Response) -> Fut, 87 | Fut: Future>, 88 | { 89 | if let Some(stream) = &mut self.stream { 90 | while let Some(message) = stream.next().await { 91 | if let Message::Text(text) = message? { 92 | let response = serde_json::from_str(&text)?; 93 | 94 | (handler)(response).await?; 95 | } 96 | } 97 | } 98 | 99 | Ok(None) 100 | } 101 | 102 | /// Send a message request 103 | /// - `channels` is a list of subscriptions to send 104 | /// - `subscribe` is a boolean indicating whether to subscribe or unsubscribe 105 | async fn send(&mut self, channels: &[Channel], subscribe: bool) -> Result<()> { 106 | if let Some(stream) = &mut self.stream { 107 | for channel in channels { 108 | let method = if subscribe { 109 | Method::Subscribe 110 | } else { 111 | Method::Unsubscribe 112 | }; 113 | 114 | let request = Request { 115 | method, 116 | subscription: channel.sub.clone(), 117 | }; 118 | 119 | let message = Message::Text(serde_json::to_string(&request)?); 120 | 121 | stream.send(message).await?; 122 | } 123 | 124 | return Ok(()); 125 | } 126 | 127 | Err(Error::NotConnected) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /tests/utils.rs: -------------------------------------------------------------------------------- 1 | use hyperliquid::utils::{parse_price, parse_size}; 2 | 3 | #[test] 4 | fn test_parse_price() { 5 | assert_eq!(parse_price(1234.5), "1234.5"); 6 | assert_eq!(parse_price(1234.56), "1234.5"); 7 | assert_eq!(parse_price(0.001234), "0.001234"); 8 | assert_eq!(parse_price(0.0012345), "0.001234"); 9 | assert_eq!(parse_price(1.2345678), "1.2345"); 10 | } 11 | 12 | #[test] 13 | fn test_parse_size() { 14 | assert_eq!(parse_size(1.001, 3), "1.001"); 15 | assert_eq!(parse_size(1.001, 2), "1"); 16 | assert_eq!(parse_size(1.0001, 3), "1"); 17 | 18 | assert_eq!(parse_size(1.001, 0), "1"); 19 | 20 | assert_eq!(parse_size(1.001, 5), "1.001"); 21 | } 22 | --------------------------------------------------------------------------------