├── .gitignore ├── ferrofluid-background.png ├── src ├── signers │ ├── mod.rs │ ├── privy.rs │ └── signer.rs ├── lib.rs ├── types │ ├── mod.rs │ ├── responses.rs │ ├── symbol.rs │ ├── eip712.rs │ ├── requests.rs │ ├── ws.rs │ ├── actions.rs │ ├── info_types.rs │ └── symbols.rs ├── providers │ ├── mod.rs │ ├── order_tracker.rs │ ├── nonce.rs │ ├── agent.rs │ ├── batcher.rs │ ├── info.rs │ └── websocket.rs ├── errors.rs ├── constants.rs └── utils │ └── mod.rs ├── .rustfmt.toml ├── .github └── workflows │ └── rust.yml ├── LICENSE ├── examples ├── 00_symbols.rs ├── 01_info_types.rs ├── 05_builder_orders.rs ├── 04_websocket.rs ├── 03_exchange_provider.rs └── 02_info_provider.rs ├── Cargo.toml ├── tests ├── approve_agent_test.rs ├── order_tracking_test.rs └── managed_exchange_test.rs └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /ferrofluid-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ControlCplusControlV/ferrofluid/HEAD/ferrofluid-background.png -------------------------------------------------------------------------------- /src/signers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod signer; 2 | pub mod privy; 3 | 4 | pub use signer::{AlloySigner, HyperliquidSignature, HyperliquidSigner, SignerError}; 5 | pub use privy::{PrivySigner, PrivyError}; 6 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 90 2 | edition = "2021" 3 | use_try_shorthand = true 4 | use_field_init_shorthand = true 5 | imports_indent = "Block" 6 | reorder_imports = true 7 | group_imports = "StdExternalCrate" 8 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod constants; 2 | pub mod errors; 3 | pub mod providers; 4 | pub mod signers; 5 | pub mod types; 6 | pub mod utils; 7 | 8 | // Re-export commonly used items at crate root 9 | pub use constants::Network; 10 | pub use errors::HyperliquidError; 11 | pub use providers::{ 12 | ExchangeProvider, InfoProvider, 13 | WsProvider, RawWsProvider, ManagedWsProvider, WsConfig, 14 | ManagedExchangeProvider 15 | }; 16 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | -------------------------------------------------------------------------------- /src/types/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod actions; 2 | pub mod eip712; 3 | pub mod info_types; 4 | pub mod requests; 5 | pub mod responses; 6 | pub mod symbol; 7 | pub mod symbols; 8 | pub mod ws; 9 | 10 | // Re-export commonly used types 11 | pub use actions::*; 12 | pub use eip712::{EncodeEip712, HyperliquidAction, encode_value}; 13 | pub use info_types::*; 14 | pub use requests::*; 15 | pub use responses::*; 16 | pub use symbol::Symbol; 17 | // Re-export symbols prelude for convenience 18 | pub use symbols::prelude; 19 | -------------------------------------------------------------------------------- /src/providers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod exchange; 2 | pub mod info; 3 | pub mod websocket; 4 | pub mod nonce; 5 | pub mod agent; 6 | pub mod batcher; 7 | pub mod order_tracker; 8 | 9 | // Raw providers (backwards compatibility) 10 | pub use exchange::RawExchangeProvider as ExchangeProvider; 11 | pub use info::InfoProvider; 12 | pub use websocket::RawWsProvider as WsProvider; 13 | 14 | // Explicit raw exports 15 | pub use exchange::RawExchangeProvider; 16 | pub use websocket::RawWsProvider; 17 | 18 | // Managed providers 19 | pub use exchange::{ManagedExchangeProvider, ManagedExchangeConfig}; 20 | pub use websocket::{ManagedWsProvider, WsConfig}; 21 | 22 | // Common types 23 | pub use exchange::OrderBuilder; 24 | pub use info::RateLimiter; 25 | pub use websocket::SubscriptionId; 26 | pub use batcher::OrderHandle; 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 ControlCplusControlV 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 | -------------------------------------------------------------------------------- /examples/00_symbols.rs: -------------------------------------------------------------------------------- 1 | //! Example showing how to use the Symbol type 2 | 3 | use ferrofluid::types::{Symbol, symbols}; 4 | 5 | fn main() { 6 | // Using predefined constants 7 | println!("BTC symbol: {}", symbols::BTC); 8 | println!("Is BTC a perp? {}", symbols::BTC.is_perp()); 9 | 10 | println!("HYPE spot symbol: {}", symbols::HYPE_USDC); 11 | println!("Is HYPE a spot? {}", symbols::HYPE_USDC.is_spot()); 12 | 13 | // Creating symbols at runtime 14 | let new_coin = symbols::symbol("NEWCOIN"); 15 | println!("New coin: {}", new_coin); 16 | 17 | // From string literals 18 | let btc: Symbol = "BTC".into(); 19 | println!("BTC from string: {}", btc); 20 | 21 | // From String 22 | let eth = String::from("ETH"); 23 | let eth_symbol: Symbol = eth.into(); 24 | println!("ETH from String: {}", eth_symbol); 25 | 26 | // Function that accepts symbols 27 | print_symbol_info(symbols::BTC); 28 | print_symbol_info("DOGE"); 29 | print_symbol_info(String::from("@999")); 30 | } 31 | 32 | fn print_symbol_info(symbol: impl Into) { 33 | let sym = symbol.into(); 34 | println!( 35 | "Symbol: {}, Type: {}", 36 | sym, 37 | if sym.is_perp() { "Perpetual" } else { "Spot" } 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug)] 4 | pub enum HyperliquidError { 5 | #[error("rate limited: {available} tokens available, {required} required")] 6 | RateLimited { available: u32, required: u32 }, 7 | 8 | #[error("network error: {0}")] 9 | Network(String), 10 | 11 | #[error("hyper http error: {0}")] 12 | HyperHttp(#[from] hyper::http::Error), 13 | 14 | #[error("json parsing error: {0}")] 15 | Json(#[from] simd_json::Error), 16 | 17 | #[error("serde json error: {0}")] 18 | SerdeJson(#[from] serde_json::Error), 19 | 20 | #[error("invalid response: {0}")] 21 | InvalidResponse(String), 22 | 23 | #[error("asset not found: {0}")] 24 | AssetNotFound(String), 25 | 26 | #[error("signer error: {0}")] 27 | Signer(#[from] crate::signers::signer::SignerError), 28 | 29 | #[error("invalid URL: {0}")] 30 | InvalidUrl(#[from] url::ParseError), 31 | 32 | #[error("HTTP error: status {status}, body: {body}")] 33 | Http { status: u16, body: String }, 34 | 35 | #[error("WebSocket error: {0}")] 36 | WebSocket(String), 37 | 38 | #[error("Serialization error: {0}")] 39 | Serialize(String), 40 | 41 | #[error("unauthorized: {0}")] 42 | Unauthorized(String), 43 | 44 | #[error("invalid request: {0}")] 45 | InvalidRequest(String), 46 | } 47 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ferrofluid" 3 | description = "A Rust library for interacting with the Hyperliquid Protocol" 4 | version = "0.1.1" 5 | edition = "2021" 6 | rust-version = "1.70" 7 | authors = ["Copy Paste"] 8 | license = "MIT" 9 | repository = "https://github.com/ControlCplusControlV/ferrofluid" 10 | 11 | [dependencies] 12 | alloy = { version = "0.1", features = [ "full" ] } 13 | 14 | # HTTP client stack 15 | hyper = { version = "1", features = ["client", "http2"] } 16 | hyper-util = { version = "0.1", features = ["client", "http2"] } 17 | hyper-rustls = "0.27" # TLS support 18 | http-body-util = "0.1" 19 | tower = { version = "0.4", features = ["timeout", "limit", "retry"] } 20 | http = "1" 21 | 22 | # WebSocket 23 | fastwebsockets = { version = "0.6", features = ["upgrade", "simd"] } 24 | rustls = { version = "0.23", features = ["aws_lc_rs"] } 25 | rustls-native-certs = "0.7" 26 | tokio-rustls = "0.26" 27 | 28 | # JSON parsing 29 | simd-json = "0.13" 30 | serde = { version = "1.0", features = ["derive"] } 31 | serde_json = "1.0" 32 | 33 | # Async runtime 34 | tokio = { version = "1.38", features = ["full"] } 35 | async-trait = "0.1" 36 | 37 | # Error handling 38 | thiserror = "1.0" 39 | 40 | # Logging 41 | tracing = "0.1" 42 | 43 | # Utilities 44 | bytes = "1.6" 45 | dashmap = "6" 46 | parking_lot = "0.12" 47 | url = "2.5" 48 | hex = "0.4" 49 | uuid = { version = "1.10", features = ["v4", "serde"] } 50 | rmp-serde = "1.1" 51 | base64 = "0.22" 52 | rust_decimal = { version = "1.36", features = ["serde"] } 53 | rand = "0.8" 54 | reqwest = { version = "0.12", features = ["json"] } 55 | 56 | [dev-dependencies] 57 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 58 | criterion = { version = "0.5", features = ["html_reports"] } 59 | tokio-test = "0.4" 60 | hex = "0.4" 61 | reqwest = { version = "0.12", features = ["json"] } 62 | clap = { version = "4.5", features = ["derive", "env"] } 63 | 64 | 65 | [profile.release] 66 | lto = "fat" 67 | codegen-units = 1 68 | opt-level = 3 69 | -------------------------------------------------------------------------------- /examples/01_info_types.rs: -------------------------------------------------------------------------------- 1 | //! Example showing how to use the info types with minimal implementation 2 | //! This is just to demonstrate the flat type structure 3 | 4 | use std::collections::HashMap; 5 | 6 | use ferrofluid::types::{L2SnapshotResponse, UserStateResponse}; 7 | 8 | fn main() { 9 | // Example of deserializing API responses into our flat types 10 | 11 | // All mids response - returns HashMap directly 12 | let mids_json = r#"{"BTC": "45000.0", "ETH": "3000.0"}"#; 13 | let mids: HashMap = serde_json::from_str(mids_json).unwrap(); 14 | println!("BTC mid price: {:?}", mids.get("BTC")); 15 | 16 | // L2 book response - levels[0] = bids, levels[1] = asks 17 | let l2_json = r#"{ 18 | "coin": "BTC", 19 | "levels": [ 20 | [{"n": 1, "px": "44999", "sz": "0.5"}], 21 | [{"n": 1, "px": "45001", "sz": "0.5"}] 22 | ], 23 | "time": 1234567890 24 | }"#; 25 | let l2: L2SnapshotResponse = serde_json::from_str(l2_json).unwrap(); 26 | println!("Best bid: {:?}", l2.levels[0].first()); 27 | println!("Best ask: {:?}", l2.levels[1].first()); 28 | 29 | // User state - direct field access 30 | let user_json = r#"{ 31 | "assetPositions": [], 32 | "crossMarginSummary": { 33 | "accountValue": "10000.0", 34 | "totalMarginUsed": "0.0", 35 | "totalNtlPos": "0.0", 36 | "totalRawUsd": "10000.0" 37 | }, 38 | "marginSummary": { 39 | "accountValue": "10000.0", 40 | "totalMarginUsed": "0.0", 41 | "totalNtlPos": "0.0", 42 | "totalRawUsd": "10000.0" 43 | }, 44 | "withdrawable": "10000.0" 45 | }"#; 46 | let user_state: UserStateResponse = serde_json::from_str(user_json).unwrap(); 47 | println!( 48 | "Account value: {}", 49 | user_state.cross_margin_summary.account_value 50 | ); 51 | 52 | // Finding a position - user does it themselves 53 | let btc_position = user_state 54 | .asset_positions 55 | .iter() 56 | .find(|ap| ap.position.coin == "BTC") 57 | .map(|ap| &ap.position); 58 | println!("BTC position: {:?}", btc_position); 59 | } 60 | -------------------------------------------------------------------------------- /tests/approve_agent_test.rs: -------------------------------------------------------------------------------- 1 | //! Test for ApproveAgent EIP-712 signing 2 | 3 | #[cfg(test)] 4 | mod tests { 5 | use ferrofluid::types::actions::ApproveAgent; 6 | use ferrofluid::types::eip712::HyperliquidAction; 7 | use alloy::primitives::{address, keccak256}; 8 | 9 | #[test] 10 | fn test_approve_agent_type_hash() { 11 | let expected = keccak256( 12 | "HyperliquidTransaction:ApproveAgent(string hyperliquidChain,address agentAddress,string agentName,uint64 nonce)" 13 | ); 14 | assert_eq!(ApproveAgent::type_hash(), expected); 15 | } 16 | 17 | #[test] 18 | fn test_approve_agent_serialization() { 19 | let action = ApproveAgent { 20 | signature_chain_id: 421614, 21 | hyperliquid_chain: "Testnet".to_string(), 22 | agent_address: address!("1234567890123456789012345678901234567890"), 23 | agent_name: Some("Test Agent".to_string()), 24 | nonce: 1234567890, 25 | }; 26 | 27 | // Serialize to JSON 28 | let json = serde_json::to_string(&action).unwrap(); 29 | let parsed: serde_json::Value = serde_json::from_str(&json).unwrap(); 30 | 31 | // Check that address is serialized as hex string 32 | assert_eq!( 33 | parsed["agentAddress"].as_str().unwrap(), 34 | "0x1234567890123456789012345678901234567890" 35 | ); 36 | assert_eq!(parsed["hyperliquidChain"].as_str().unwrap(), "Testnet"); 37 | assert_eq!(parsed["agentName"].as_str().unwrap(), "Test Agent"); 38 | assert_eq!(parsed["nonce"].as_u64().unwrap(), 1234567890); 39 | } 40 | 41 | #[test] 42 | fn test_approve_agent_struct_hash() { 43 | let action = ApproveAgent { 44 | signature_chain_id: 421614, 45 | hyperliquid_chain: "Testnet".to_string(), 46 | agent_address: address!("0D1d9635D0640821d15e323ac8AdADfA9c111414"), 47 | agent_name: None, 48 | nonce: 1690393044548, 49 | }; 50 | 51 | // Test that struct hash is computed 52 | let struct_hash = action.struct_hash(); 53 | // Just verify it's not zero 54 | assert_ne!(struct_hash, alloy::primitives::B256::ZERO); 55 | } 56 | } -------------------------------------------------------------------------------- /src/types/responses.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | // ==================== Order Status Types ==================== 4 | 5 | #[derive(Debug, Clone, Deserialize)] 6 | pub struct RestingOrder { 7 | pub oid: u64, 8 | } 9 | 10 | #[derive(Debug, Clone, Deserialize)] 11 | #[serde(rename_all = "camelCase")] 12 | pub struct FilledOrder { 13 | pub total_sz: String, 14 | pub avg_px: String, 15 | pub oid: u64, 16 | } 17 | 18 | #[derive(Debug, Clone, Deserialize)] 19 | #[serde(rename_all = "camelCase")] 20 | pub enum ExchangeDataStatus { 21 | Success, 22 | WaitingForFill, 23 | WaitingForTrigger, 24 | Error(String), 25 | Resting(RestingOrder), 26 | Filled(FilledOrder), 27 | } 28 | 29 | // ==================== Exchange Response Types ==================== 30 | 31 | #[derive(Debug, Clone, Deserialize)] 32 | pub struct ExchangeDataStatuses { 33 | pub statuses: Vec, 34 | } 35 | 36 | #[derive(Debug, Clone, Deserialize)] 37 | pub struct ExchangeResponse { 38 | #[serde(rename = "type")] 39 | pub response_type: String, 40 | pub data: Option, 41 | } 42 | 43 | #[derive(Debug, Clone, Deserialize)] 44 | #[serde(rename_all = "camelCase")] 45 | #[serde(tag = "status", content = "response")] 46 | pub enum ExchangeResponseStatus { 47 | Ok(ExchangeResponse), 48 | Err(String), 49 | } 50 | 51 | // ==================== Convenience Methods ==================== 52 | 53 | impl ExchangeResponseStatus { 54 | /// Check if the response was successful 55 | pub fn is_ok(&self) -> bool { 56 | matches!(self, Self::Ok(_)) 57 | } 58 | 59 | /// Get the error message if this was an error response 60 | pub fn error(&self) -> Option<&str> { 61 | match self { 62 | Self::Err(msg) => Some(msg), 63 | _ => None, 64 | } 65 | } 66 | 67 | /// Get the inner response if successful 68 | pub fn into_result(self) -> Result { 69 | match self { 70 | Self::Ok(response) => Ok(response), 71 | Self::Err(msg) => Err(msg), 72 | } 73 | } 74 | } 75 | 76 | impl ExchangeDataStatus { 77 | /// Check if this status represents a successful order 78 | pub fn is_success(&self) -> bool { 79 | matches!(self, Self::Success | Self::Resting(_) | Self::Filled(_)) 80 | } 81 | 82 | /// Get order ID if available 83 | pub fn order_id(&self) -> Option { 84 | match self { 85 | Self::Resting(order) => Some(order.oid), 86 | Self::Filled(order) => Some(order.oid), 87 | _ => None, 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/constants.rs: -------------------------------------------------------------------------------- 1 | // ==================== Network Configuration ==================== 2 | 3 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 4 | pub enum Network { 5 | Mainnet, 6 | Testnet, 7 | } 8 | 9 | impl Network { 10 | pub fn api_url(&self) -> &'static str { 11 | match self { 12 | Network::Mainnet => "https://api.hyperliquid.xyz", 13 | Network::Testnet => "https://api.hyperliquid-testnet.xyz", 14 | } 15 | } 16 | 17 | pub fn ws_url(&self) -> &'static str { 18 | match self { 19 | Network::Mainnet => "wss://api.hyperliquid.xyz/ws", 20 | Network::Testnet => "wss://api.hyperliquid-testnet.xyz/ws", 21 | } 22 | } 23 | } 24 | 25 | // ==================== Chain Configuration ==================== 26 | 27 | // Chain IDs 28 | pub const CHAIN_ID_MAINNET: u64 = 42161; // Arbitrum One 29 | pub const CHAIN_ID_TESTNET: u64 = 421614; // Arbitrum Sepolia 30 | 31 | // Agent Sources 32 | pub const AGENT_SOURCE_MAINNET: &str = "a"; 33 | pub const AGENT_SOURCE_TESTNET: &str = "b"; 34 | 35 | // Exchange Endpoints 36 | pub const EXCHANGE_ENDPOINT_MAINNET: &str = "https://api.hyperliquid.xyz/exchange"; 37 | pub const EXCHANGE_ENDPOINT_TESTNET: &str = 38 | "https://api.hyperliquid-testnet.xyz/exchange"; 39 | 40 | // ==================== Rate Limit Weights ==================== 41 | 42 | // Info endpoints 43 | pub const WEIGHT_ALL_MIDS: u32 = 2; 44 | pub const WEIGHT_L2_BOOK: u32 = 1; 45 | pub const WEIGHT_USER_STATE: u32 = 2; 46 | pub const WEIGHT_USER_FILLS: u32 = 2; 47 | pub const WEIGHT_USER_FUNDING: u32 = 2; 48 | pub const WEIGHT_USER_FEES: u32 = 1; 49 | pub const WEIGHT_OPEN_ORDERS: u32 = 1; 50 | pub const WEIGHT_ORDER_STATUS: u32 = 1; 51 | pub const WEIGHT_RECENT_TRADES: u32 = 1; 52 | pub const WEIGHT_CANDLES: u32 = 2; 53 | pub const WEIGHT_FUNDING_HISTORY: u32 = 2; 54 | pub const WEIGHT_TOKEN_BALANCES: u32 = 1; 55 | pub const WEIGHT_REFERRAL: u32 = 1; 56 | 57 | // Exchange endpoints (these have higher weights) 58 | pub const WEIGHT_PLACE_ORDER: u32 = 3; 59 | pub const WEIGHT_CANCEL_ORDER: u32 = 2; 60 | pub const WEIGHT_MODIFY_ORDER: u32 = 3; 61 | pub const WEIGHT_BULK_ORDER: u32 = 10; 62 | pub const WEIGHT_BULK_CANCEL: u32 = 8; 63 | 64 | // ==================== Rate Limit Configuration ==================== 65 | 66 | pub const RATE_LIMIT_MAX_TOKENS: u32 = 1200; 67 | pub const RATE_LIMIT_REFILL_RATE: u32 = 600; // per minute 68 | 69 | // ==================== Time Constants ==================== 70 | 71 | pub const NONCE_WINDOW_MS: u64 = 60_000; // 60 seconds 72 | 73 | // ==================== Order Constants ==================== 74 | 75 | pub const TIF_GTC: &str = "Gtc"; 76 | pub const TIF_IOC: &str = "Ioc"; 77 | pub const TIF_ALO: &str = "Alo"; 78 | 79 | pub const TPSL_TP: &str = "tp"; 80 | pub const TPSL_SL: &str = "sl"; 81 | -------------------------------------------------------------------------------- /src/types/symbol.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// A trading symbol that can be either a compile-time constant or runtime string 6 | #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] 7 | #[serde(transparent)] 8 | pub struct Symbol(Cow<'static, str>); 9 | 10 | impl Symbol { 11 | /// Create a compile-time constant symbol 12 | pub const fn from_static(s: &'static str) -> Self { 13 | Symbol(Cow::Borrowed(s)) 14 | } 15 | 16 | /// Get the symbol as a string slice 17 | pub fn as_str(&self) -> &str { 18 | &self.0 19 | } 20 | 21 | /// Check if this is a spot symbol (starts with @) 22 | pub fn is_spot(&self) -> bool { 23 | self.0.starts_with('@') 24 | } 25 | 26 | /// Check if this is a perpetual symbol 27 | pub fn is_perp(&self) -> bool { 28 | !self.is_spot() 29 | } 30 | } 31 | 32 | // Display for nice printing 33 | impl std::fmt::Display for Symbol { 34 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 35 | write!(f, "{}", self.0) 36 | } 37 | } 38 | 39 | // AsRef for compatibility 40 | impl AsRef for Symbol { 41 | fn as_ref(&self) -> &str { 42 | self.as_str() 43 | } 44 | } 45 | 46 | // Ergonomic conversions 47 | impl From<&'static str> for Symbol { 48 | fn from(s: &'static str) -> Self { 49 | Symbol(Cow::Borrowed(s)) 50 | } 51 | } 52 | 53 | impl From for Symbol { 54 | fn from(s: String) -> Self { 55 | Symbol(Cow::Owned(s)) 56 | } 57 | } 58 | 59 | impl From<&String> for Symbol { 60 | fn from(s: &String) -> Self { 61 | Symbol(Cow::Owned(s.clone())) 62 | } 63 | } 64 | 65 | // Allow &Symbol to work with Into APIs 66 | impl From<&Symbol> for Symbol { 67 | fn from(s: &Symbol) -> Self { 68 | s.clone() 69 | } 70 | } 71 | 72 | #[cfg(test)] 73 | mod tests { 74 | use super::*; 75 | 76 | #[test] 77 | fn test_symbol_creation() { 78 | let static_sym = Symbol::from_static("BTC"); 79 | assert_eq!(static_sym.as_str(), "BTC"); 80 | assert!(static_sym.is_perp()); 81 | assert!(!static_sym.is_spot()); 82 | 83 | let owned_sym = Symbol::from("ETH".to_string()); 84 | assert_eq!(owned_sym.as_str(), "ETH"); 85 | 86 | let spot_sym = Symbol::from_static("@107"); 87 | assert!(spot_sym.is_spot()); 88 | assert!(!spot_sym.is_perp()); 89 | } 90 | 91 | #[test] 92 | fn test_symbol_conversions() { 93 | // From &'static str 94 | let sym: Symbol = "BTC".into(); 95 | assert_eq!(sym.as_str(), "BTC"); 96 | 97 | // From String 98 | let sym: Symbol = String::from("ETH").into(); 99 | assert_eq!(sym.as_str(), "ETH"); 100 | 101 | // From &String 102 | let s = String::from("SOL"); 103 | let sym: Symbol = (&s).into(); 104 | assert_eq!(sym.as_str(), "SOL"); 105 | } 106 | 107 | #[test] 108 | fn test_symbol_equality() { 109 | let sym1 = Symbol::from_static("BTC"); 110 | let sym2 = Symbol::from("BTC".to_string()); 111 | assert_eq!(sym1, sym2); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /examples/05_builder_orders.rs: -------------------------------------------------------------------------------- 1 | //! Example of using builder functionality for orders 2 | 3 | use alloy::primitives::{B256, address}; 4 | use alloy::signers::local::PrivateKeySigner; 5 | use ferrofluid::{providers::ExchangeProvider, signers::AlloySigner}; 6 | 7 | #[tokio::main] 8 | async fn main() -> Result<(), Box> { 9 | // Example private key (DO NOT USE IN PRODUCTION) 10 | let private_key = B256::from([1u8; 32]); 11 | let signer = PrivateKeySigner::from_bytes(&private_key)?; 12 | let hyperliquid_signer = AlloySigner { inner: signer }; 13 | 14 | // Builder address (example) 15 | let builder_address = address!("1234567890123456789012345678901234567890"); 16 | 17 | // Create exchange provider with builder configured 18 | let exchange = ExchangeProvider::mainnet_builder(hyperliquid_signer, builder_address); 19 | 20 | println!( 21 | "Exchange provider configured with builder: {:?}", 22 | exchange.builder() 23 | ); 24 | 25 | // Now all orders placed through this provider will automatically include builder info 26 | 27 | // Example 1: Simple order (builder fee = 0) 28 | // Note: In a real application, you would check the result 29 | // BTC perpetual is asset index 0 30 | let _order = exchange.order(0).limit_buy("109000", "0.001").send(); 31 | println!("Order would be placed with default builder fee"); 32 | 33 | // Example 2: Order with specific builder fee 34 | // ETH perpetual is asset index 1 35 | let order_request = exchange.order(1).limit_sell("2700", "0.1").build()?; 36 | 37 | // Place with specific builder fee (e.g., 10 bps = 10) 38 | // Note: In a real application, you would await this 39 | let _result = exchange.place_order_with_builder_fee(&order_request, 10); 40 | println!("Order would be placed with 10 bps builder fee"); 41 | 42 | // Example 3: Bulk orders with builder 43 | let orders = vec![ 44 | exchange.order(0).limit_buy("108000", "0.001").build()?, 45 | exchange.order(0).limit_buy("107000", "0.001").build()?, 46 | ]; 47 | 48 | // All orders in the bulk will include builder info 49 | let _result = exchange.bulk_orders(orders); 50 | println!("Bulk orders would be placed with builder"); 51 | 52 | // Example 4: Creating provider with all options 53 | let signer2 = PrivateKeySigner::from_bytes(&private_key)?; 54 | let hyperliquid_signer2 = AlloySigner { inner: signer2 }; 55 | 56 | let _exchange_full = ExchangeProvider::mainnet_with_options( 57 | hyperliquid_signer2, 58 | None, // No vault 59 | None, // No agent 60 | Some(builder_address), // With builder 61 | ); 62 | 63 | println!("Provider created with builder support!"); 64 | 65 | // Example 5: Using builder with approved fee 66 | // First approve the builder for a max fee rate (done once) 67 | let private_key3 = B256::from([2u8; 32]); 68 | let signer3 = PrivateKeySigner::from_bytes(&private_key3)?; 69 | let hyperliquid_signer3 = AlloySigner { inner: signer3 }; 70 | let exchange3 = ExchangeProvider::mainnet(hyperliquid_signer3); 71 | 72 | // Approve builder for max 50 bps (0.5%) 73 | let _approval = exchange3.approve_builder_fee(builder_address, "0.005".to_string()); 74 | println!("Builder fee approval would be sent"); 75 | 76 | Ok(()) 77 | } 78 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | /// Macro for user actions that use HyperliquidSignTransaction domain 2 | /// All user actions must have signature_chain_id as their first field 3 | #[macro_export] 4 | macro_rules! hyperliquid_action { 5 | ( 6 | $(#[$meta:meta])* 7 | struct $name:ident { 8 | pub signature_chain_id: u64, 9 | $( 10 | $(#[$field_meta:meta])* 11 | pub $field:ident: $type:ty 12 | ),* $(,)? 13 | } 14 | => $type_string:literal 15 | => encode($($encode_field:ident),* $(,)?) 16 | ) => { 17 | $(#[$meta])* 18 | #[derive(Debug, Clone, serde::Serialize)] 19 | #[serde(rename_all = "camelCase")] 20 | pub struct $name { 21 | pub signature_chain_id: u64, 22 | $( 23 | $(#[$field_meta])* 24 | pub $field: $type, 25 | )* 26 | } 27 | 28 | impl $crate::types::eip712::HyperliquidAction for $name { 29 | const TYPE_STRING: &'static str = $type_string; 30 | const USE_PREFIX: bool = true; 31 | 32 | fn chain_id(&self) -> Option { 33 | Some(self.signature_chain_id) 34 | } 35 | 36 | fn encode_data(&self) -> Vec { 37 | let mut encoded = Vec::new(); 38 | encoded.extend_from_slice(&Self::type_hash()[..]); 39 | $( 40 | encoded.extend_from_slice(&$crate::types::eip712::encode_value(&self.$encode_field)[..]); 41 | )* 42 | encoded 43 | } 44 | } 45 | }; 46 | } 47 | 48 | /// Macro for L1 actions that use the Exchange domain 49 | #[macro_export] 50 | macro_rules! l1_action { 51 | ( 52 | $(#[$meta:meta])* 53 | struct $name:ident { 54 | $( 55 | $(#[$field_meta:meta])* 56 | pub $field:ident: $type:ty 57 | ),* $(,)? 58 | } 59 | => $type_string:literal 60 | => encode($($encode_field:ident),* $(,)?) 61 | ) => { 62 | $(#[$meta])* 63 | #[derive(Debug, Clone, serde::Serialize)] 64 | #[serde(rename_all = "camelCase")] 65 | pub struct $name { 66 | $( 67 | $(#[$field_meta])* 68 | pub $field: $type, 69 | )* 70 | } 71 | 72 | impl $crate::types::eip712::HyperliquidAction for $name { 73 | const TYPE_STRING: &'static str = $type_string; 74 | const USE_PREFIX: bool = false; 75 | 76 | // L1 actions use the Exchange domain with chain ID 1337 77 | fn domain(&self) -> alloy::sol_types::Eip712Domain { 78 | alloy::sol_types::eip712_domain! { 79 | name: "Exchange", 80 | version: "1", 81 | chain_id: 1337u64, 82 | verifying_contract: alloy::primitives::address!("0000000000000000000000000000000000000000"), 83 | } 84 | } 85 | 86 | fn encode_data(&self) -> Vec { 87 | let mut encoded = Vec::new(); 88 | encoded.extend_from_slice(&Self::type_hash()[..]); 89 | $( 90 | encoded.extend_from_slice(&$crate::types::eip712::encode_value(&self.$encode_field)[..]); 91 | )* 92 | encoded 93 | } 94 | } 95 | }; 96 | } 97 | -------------------------------------------------------------------------------- /examples/04_websocket.rs: -------------------------------------------------------------------------------- 1 | //! Example of using the WebSocket provider for real-time data 2 | 3 | use ferrofluid::{Network, providers::WsProvider, types::ws::Message}; 4 | 5 | #[tokio::main] 6 | async fn main() -> Result<(), Box> { 7 | // Install crypto provider for rustls 8 | rustls::crypto::aws_lc_rs::default_provider() 9 | .install_default() 10 | .expect("Failed to install crypto provider"); 11 | // Connect to WebSocket 12 | let mut ws = WsProvider::connect(Network::Mainnet).await?; 13 | println!("Connected to Hyperliquid WebSocket"); 14 | 15 | // Subscribe to BTC order book 16 | let (_btc_book_id, mut btc_book_rx) = ws.subscribe_l2_book("BTC").await?; 17 | println!("Subscribed to BTC L2 book"); 18 | 19 | // Subscribe to all mid prices 20 | let (_mids_id, mut mids_rx) = ws.subscribe_all_mids().await?; 21 | println!("Subscribed to all mids"); 22 | 23 | // Start reading messages 24 | ws.start_reading().await?; 25 | 26 | // Handle messages for a limited time (10 seconds for demo) 27 | let mut message_count = 0; 28 | let timeout = tokio::time::sleep(std::time::Duration::from_secs(10)); 29 | tokio::pin!(timeout); 30 | 31 | loop { 32 | tokio::select! { 33 | // Handle BTC book updates 34 | Some(msg) = btc_book_rx.recv() => { 35 | if let Message::L2Book(book) = msg { 36 | println!("BTC book update:"); 37 | println!(" Coin: {}", book.data.coin); 38 | println!(" Time: {}", book.data.time); 39 | if let Some(bids) = book.data.levels.first() { 40 | if let Some(best_bid) = bids.first() { 41 | println!(" Best bid: {} @ {}", best_bid.sz, best_bid.px); 42 | } 43 | } 44 | if let Some(asks) = book.data.levels.get(1) { 45 | if let Some(best_ask) = asks.first() { 46 | println!(" Best ask: {} @ {}", best_ask.sz, best_ask.px); 47 | } 48 | } 49 | message_count += 1; 50 | } 51 | } 52 | 53 | // Handle all mids updates 54 | Some(msg) = mids_rx.recv() => { 55 | if let Message::AllMids(mids) = msg { 56 | println!("\nMid prices update:"); 57 | for (coin, price) in mids.data.mids.iter().take(5) { 58 | println!(" {}: {}", coin, price); 59 | } 60 | println!(" ... and {} more", mids.data.mids.len().saturating_sub(5)); 61 | message_count += 1; 62 | } 63 | } 64 | 65 | // Handle timeout 66 | _ = &mut timeout => { 67 | println!("\nDemo timeout reached after 10 seconds"); 68 | break; 69 | } 70 | 71 | // Handle channel closure 72 | else => { 73 | println!("\nAll channels closed, exiting"); 74 | break; 75 | } 76 | } 77 | 78 | // Optional: Exit after certain number of messages 79 | if message_count >= 20 { 80 | println!("\nReceived {} messages, exiting demo", message_count); 81 | break; 82 | } 83 | } 84 | 85 | println!("WebSocket demo completed successfully!"); 86 | Ok(()) 87 | } 88 | -------------------------------------------------------------------------------- /src/types/eip712.rs: -------------------------------------------------------------------------------- 1 | use alloy::primitives::{Address, B256, U256, keccak256}; 2 | use alloy::sol_types::Eip712Domain; 3 | 4 | pub trait HyperliquidAction: Sized + serde::Serialize { 5 | /// The EIP-712 type string (without HyperliquidTransaction: prefix) 6 | const TYPE_STRING: &'static str; 7 | 8 | /// Whether this uses the HyperliquidTransaction: prefix 9 | const USE_PREFIX: bool = true; 10 | 11 | /// Get chain ID for domain construction 12 | /// Override this method for actions with signature_chain_id 13 | fn chain_id(&self) -> Option { 14 | None 15 | } 16 | 17 | /// Get the EIP-712 domain for this action 18 | fn domain(&self) -> Eip712Domain { 19 | let chain_id = self.chain_id().unwrap_or(1); // Default to mainnet 20 | alloy::sol_types::eip712_domain! { 21 | name: "HyperliquidSignTransaction", 22 | version: "1", 23 | chain_id: chain_id, 24 | verifying_contract: alloy::primitives::address!("0000000000000000000000000000000000000000"), 25 | } 26 | } 27 | 28 | fn type_hash() -> B256 { 29 | let type_string = if Self::USE_PREFIX { 30 | format!("HyperliquidTransaction:{}", Self::TYPE_STRING) 31 | } else { 32 | Self::TYPE_STRING.to_string() 33 | }; 34 | keccak256(type_string.as_bytes()) 35 | } 36 | 37 | /// Encode the struct data according to EIP-712 rules 38 | /// Default implementation - should be overridden for proper field ordering 39 | fn encode_data(&self) -> Vec { 40 | // This is a placeholder - each action should implement proper encoding 41 | // based on its TYPE_STRING field order 42 | let mut encoded = Vec::new(); 43 | encoded.extend_from_slice(&Self::type_hash()[..]); 44 | // Subclasses should implement the rest 45 | encoded 46 | } 47 | 48 | fn struct_hash(&self) -> B256 { 49 | keccak256(self.encode_data()) 50 | } 51 | 52 | fn eip712_signing_hash(&self, domain: &Eip712Domain) -> B256 { 53 | let domain_separator = domain.separator(); 54 | let struct_hash = self.struct_hash(); 55 | 56 | let mut buf = Vec::with_capacity(66); 57 | buf.push(0x19); 58 | buf.push(0x01); 59 | buf.extend_from_slice(&domain_separator[..]); 60 | buf.extend_from_slice(&struct_hash[..]); 61 | 62 | keccak256(&buf) 63 | } 64 | } 65 | 66 | /// Encode a value according to EIP-712 rules 67 | pub fn encode_value(value: &T) -> [u8; 32] { 68 | value.encode_eip712() 69 | } 70 | 71 | /// Trait for types that can be encoded in EIP-712 72 | pub trait EncodeEip712 { 73 | fn encode_eip712(&self) -> [u8; 32]; 74 | } 75 | 76 | impl EncodeEip712 for String { 77 | fn encode_eip712(&self) -> [u8; 32] { 78 | keccak256(self.as_bytes()).into() 79 | } 80 | } 81 | 82 | impl EncodeEip712 for u64 { 83 | fn encode_eip712(&self) -> [u8; 32] { 84 | U256::from(*self).to_be_bytes::<32>() 85 | } 86 | } 87 | 88 | impl EncodeEip712 for B256 { 89 | fn encode_eip712(&self) -> [u8; 32] { 90 | (*self).into() 91 | } 92 | } 93 | 94 | impl EncodeEip712 for Address { 95 | fn encode_eip712(&self) -> [u8; 32] { 96 | let mut result = [0u8; 32]; 97 | result[12..].copy_from_slice(self.as_slice()); 98 | result 99 | } 100 | } 101 | 102 | impl EncodeEip712 for Option { 103 | fn encode_eip712(&self) -> [u8; 32] { 104 | match self { 105 | Some(v) => v.encode_eip712(), 106 | None => keccak256("".as_bytes()).into(), // Empty string hash for None 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/providers/order_tracker.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::{Arc, RwLock}; 3 | use uuid::Uuid; 4 | 5 | use crate::types::requests::OrderRequest; 6 | use crate::types::responses::ExchangeResponseStatus; 7 | 8 | #[derive(Clone, Debug)] 9 | pub struct TrackedOrder { 10 | pub cloid: Uuid, 11 | pub order: OrderRequest, 12 | pub timestamp: u64, 13 | pub status: OrderStatus, 14 | pub response: Option, 15 | } 16 | 17 | #[derive(Clone, Debug, PartialEq)] 18 | pub enum OrderStatus { 19 | Pending, 20 | Submitted, 21 | Failed(String), 22 | } 23 | 24 | #[derive(Clone)] 25 | pub struct OrderTracker { 26 | orders: Arc>>, 27 | } 28 | 29 | impl OrderTracker { 30 | pub fn new() -> Self { 31 | Self { 32 | orders: Arc::new(RwLock::new(HashMap::new())), 33 | } 34 | } 35 | 36 | /// Track a new order 37 | pub fn track_order(&self, cloid: Uuid, order: OrderRequest, timestamp: u64) { 38 | let tracked = TrackedOrder { 39 | cloid, 40 | order, 41 | timestamp, 42 | status: OrderStatus::Pending, 43 | response: None, 44 | }; 45 | 46 | let mut orders = self.orders.write().unwrap(); 47 | orders.insert(cloid, tracked); 48 | } 49 | 50 | /// Update order status after submission 51 | pub fn update_order_status( 52 | &self, 53 | cloid: &Uuid, 54 | status: OrderStatus, 55 | response: Option, 56 | ) { 57 | let mut orders = self.orders.write().unwrap(); 58 | if let Some(order) = orders.get_mut(cloid) { 59 | order.status = status; 60 | order.response = response; 61 | } 62 | } 63 | 64 | /// Get a specific order by CLOID 65 | pub fn get_order(&self, cloid: &Uuid) -> Option { 66 | let orders = self.orders.read().unwrap(); 67 | orders.get(cloid).cloned() 68 | } 69 | 70 | /// Get all tracked orders 71 | pub fn get_all_orders(&self) -> Vec { 72 | let orders = self.orders.read().unwrap(); 73 | orders.values().cloned().collect() 74 | } 75 | 76 | /// Get orders by status 77 | pub fn get_orders_by_status(&self, status: &OrderStatus) -> Vec { 78 | let orders = self.orders.read().unwrap(); 79 | orders 80 | .values() 81 | .filter(|order| &order.status == status) 82 | .cloned() 83 | .collect() 84 | } 85 | 86 | /// Get pending orders 87 | pub fn get_pending_orders(&self) -> Vec { 88 | self.get_orders_by_status(&OrderStatus::Pending) 89 | } 90 | 91 | /// Get submitted orders 92 | pub fn get_submitted_orders(&self) -> Vec { 93 | self.get_orders_by_status(&OrderStatus::Submitted) 94 | } 95 | 96 | /// Get failed orders 97 | pub fn get_failed_orders(&self) -> Vec { 98 | let orders = self.orders.read().unwrap(); 99 | orders 100 | .values() 101 | .filter(|order| matches!(order.status, OrderStatus::Failed(_))) 102 | .cloned() 103 | .collect() 104 | } 105 | 106 | /// Clear all tracked orders 107 | pub fn clear(&self) { 108 | let mut orders = self.orders.write().unwrap(); 109 | orders.clear(); 110 | } 111 | 112 | /// Get the number of tracked orders 113 | pub fn len(&self) -> usize { 114 | let orders = self.orders.read().unwrap(); 115 | orders.len() 116 | } 117 | 118 | /// Check if tracking is empty 119 | pub fn is_empty(&self) -> bool { 120 | let orders = self.orders.read().unwrap(); 121 | orders.is_empty() 122 | } 123 | } 124 | 125 | impl Default for OrderTracker { 126 | fn default() -> Self { 127 | Self::new() 128 | } 129 | } -------------------------------------------------------------------------------- /examples/03_exchange_provider.rs: -------------------------------------------------------------------------------- 1 | use alloy::primitives::Address; 2 | use alloy::signers::local::PrivateKeySigner; 3 | use ferrofluid::constants::TIF_GTC; 4 | use ferrofluid::types::requests::OrderRequest; 5 | use ferrofluid::{ExchangeProvider, signers::AlloySigner}; 6 | use uuid::Uuid; 7 | 8 | #[tokio::main] 9 | async fn main() -> Result<(), Box> { 10 | // Initialize the crypto provider for TLS 11 | rustls::crypto::CryptoProvider::install_default( 12 | rustls::crypto::aws_lc_rs::default_provider(), 13 | ) 14 | .expect("Failed to install rustls crypto provider"); 15 | // Create a test signer (DO NOT USE IN PRODUCTION) 16 | let private_key = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; 17 | let signer = private_key.parse::()?; 18 | let alloy_signer = AlloySigner { inner: signer }; 19 | 20 | // Create ExchangeProvider for testnet 21 | let exchange = ExchangeProvider::testnet(alloy_signer); 22 | 23 | println!("ExchangeProvider created successfully!"); 24 | 25 | // Example 1: Create an order with client order ID 26 | let cloid = Uuid::new_v4(); 27 | let _order = OrderRequest::limit( 28 | 0, // BTC-USD asset ID 29 | true, // buy 30 | "45000.0", // price 31 | "0.01", // size 32 | TIF_GTC, 33 | ); 34 | 35 | println!("\nOrder created:"); 36 | println!("- Asset: 0 (BTC-USD)"); 37 | println!("- Side: Buy"); 38 | println!("- Price: $45,000"); 39 | println!("- Size: 0.01"); 40 | println!("- Client Order ID: {}", cloid); 41 | 42 | // Example 2: Using OrderBuilder pattern 43 | let _builder_order = exchange 44 | .order(0) 45 | .buy() 46 | .limit_px("45000.0") 47 | .size("0.01") 48 | .cloid(Uuid::new_v4()); 49 | 50 | println!("\nOrderBuilder created successfully!"); 51 | 52 | // Example 3: Create bulk orders with mixed tracking 53 | let orders_with_ids = vec![ 54 | ( 55 | OrderRequest::limit(0, true, "44900.0", "0.01", TIF_GTC), 56 | Some(Uuid::new_v4()), 57 | ), 58 | ( 59 | OrderRequest::limit(0, true, "44800.0", "0.01", TIF_GTC), 60 | None, 61 | ), 62 | ( 63 | OrderRequest::limit(0, true, "44700.0", "0.01", TIF_GTC), 64 | Some(Uuid::new_v4()), 65 | ), 66 | ]; 67 | 68 | println!("\nBulk orders created:"); 69 | for (i, (order, cloid)) in orders_with_ids.iter().enumerate() { 70 | println!( 71 | "- Order {}: price={}, cloid={:?}", 72 | i + 1, 73 | &order.limit_px, 74 | cloid.as_ref().map(|id| id.to_string()) 75 | ); 76 | } 77 | 78 | // Example 4: Different constructor types 79 | let vault_address: Address = "0x742d35Cc6634C0532925a3b844Bc9e7595f8fA49".parse()?; 80 | let vault_signer = private_key.parse::()?; 81 | let _vault_exchange = ExchangeProvider::testnet_vault( 82 | AlloySigner { 83 | inner: vault_signer, 84 | }, 85 | vault_address, 86 | ); 87 | println!( 88 | "\nVault ExchangeProvider created for address: {}", 89 | vault_address 90 | ); 91 | 92 | let agent_address: Address = "0x742d35Cc6634C0532925a3b844Bc9e7595f8fA49".parse()?; 93 | let agent_signer = private_key.parse::()?; 94 | let _agent_exchange = ExchangeProvider::testnet_agent( 95 | AlloySigner { 96 | inner: agent_signer, 97 | }, 98 | agent_address, 99 | ); 100 | println!( 101 | "Agent ExchangeProvider created for address: {}", 102 | agent_address 103 | ); 104 | 105 | println!("\nAll examples completed successfully!"); 106 | println!("\nNOTE: This example only tests object creation, not actual API calls."); 107 | println!("To test actual trading, you would need:"); 108 | println!("1. A funded testnet account"); 109 | println!("2. Valid asset IDs for the testnet"); 110 | println!("3. Appropriate risk controls"); 111 | 112 | Ok(()) 113 | } 114 | -------------------------------------------------------------------------------- /tests/order_tracking_test.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use alloy::signers::local::PrivateKeySigner; 4 | use ferrofluid::{ 5 | constants::TIF_GTC, 6 | types::requests::OrderRequest, 7 | ExchangeProvider, signers::AlloySigner, 8 | }; 9 | use uuid::Uuid; 10 | use std::sync::Once; 11 | 12 | static INIT: Once = Once::new(); 13 | 14 | fn init_crypto() { 15 | INIT.call_once(|| { 16 | rustls::crypto::CryptoProvider::install_default( 17 | rustls::crypto::aws_lc_rs::default_provider(), 18 | ) 19 | .expect("Failed to install rustls crypto provider"); 20 | }); 21 | } 22 | 23 | fn create_test_exchange() -> ExchangeProvider> { 24 | init_crypto(); 25 | let private_key = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; 26 | let signer = private_key.parse::().unwrap(); 27 | let alloy_signer = AlloySigner { inner: signer }; 28 | 29 | ExchangeProvider::testnet(alloy_signer) 30 | .with_order_tracking() 31 | } 32 | 33 | #[test] 34 | fn test_order_tracking_initialization() { 35 | let exchange = create_test_exchange(); 36 | assert_eq!(exchange.tracked_order_count(), 0); 37 | assert!(exchange.get_all_tracked_orders().is_empty()); 38 | } 39 | 40 | #[test] 41 | fn test_order_tracking_methods() { 42 | let exchange = create_test_exchange(); 43 | 44 | // Test empty state 45 | assert_eq!(exchange.get_all_tracked_orders().len(), 0); 46 | assert_eq!(exchange.get_pending_orders().len(), 0); 47 | assert_eq!(exchange.get_submitted_orders().len(), 0); 48 | assert_eq!(exchange.get_failed_orders().len(), 0); 49 | 50 | // Test get by non-existent cloid 51 | let fake_cloid = Uuid::new_v4(); 52 | assert!(exchange.get_tracked_order(&fake_cloid).is_none()); 53 | } 54 | 55 | #[test] 56 | fn test_clear_tracked_orders() { 57 | let exchange = create_test_exchange(); 58 | 59 | // Clear should work even when empty 60 | exchange.clear_tracked_orders(); 61 | assert_eq!(exchange.tracked_order_count(), 0); 62 | } 63 | 64 | #[test] 65 | fn test_order_builder_with_tracking() { 66 | let exchange = create_test_exchange(); 67 | 68 | // Create order using builder 69 | let _order_builder = exchange 70 | .order(0) 71 | .buy() 72 | .limit_px("45000.0") 73 | .size("0.01") 74 | .cloid(Uuid::new_v4()); 75 | 76 | // Builder doesn't automatically track until order is placed 77 | assert_eq!(exchange.tracked_order_count(), 0); 78 | } 79 | 80 | #[test] 81 | fn test_tracking_disabled_by_default() { 82 | init_crypto(); 83 | let private_key = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; 84 | let signer = private_key.parse::().unwrap(); 85 | let alloy_signer = AlloySigner { inner: signer }; 86 | 87 | // Create exchange without tracking 88 | let exchange = ExchangeProvider::testnet(alloy_signer); 89 | 90 | // Methods should return empty results 91 | assert_eq!(exchange.tracked_order_count(), 0); 92 | assert_eq!(exchange.get_all_tracked_orders().len(), 0); 93 | assert_eq!(exchange.get_pending_orders().len(), 0); 94 | 95 | // Clear should be safe to call 96 | exchange.clear_tracked_orders(); 97 | } 98 | 99 | #[tokio::test] 100 | async fn test_order_tracking_with_mock_placement() { 101 | let exchange = create_test_exchange(); 102 | 103 | // Create a test order 104 | let _order = OrderRequest::limit(0, true, "45000.0", "0.01", TIF_GTC); 105 | 106 | // Before placing, no orders tracked 107 | assert_eq!(exchange.tracked_order_count(), 0); 108 | 109 | // Note: Actually placing the order would require a valid connection 110 | // This test verifies the tracking infrastructure is in place 111 | } 112 | } -------------------------------------------------------------------------------- /src/types/requests.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use uuid::Uuid; 3 | 4 | // ==================== Order Types ==================== 5 | 6 | #[derive(Debug, Clone, Serialize, Deserialize)] 7 | #[serde(rename_all = "camelCase")] 8 | pub struct OrderRequest { 9 | #[serde(rename = "a")] 10 | pub asset: u32, 11 | #[serde(rename = "b")] 12 | pub is_buy: bool, 13 | #[serde(rename = "p")] 14 | pub limit_px: String, 15 | #[serde(rename = "s")] 16 | pub sz: String, 17 | #[serde(rename = "r", default)] 18 | pub reduce_only: bool, 19 | #[serde(rename = "t")] 20 | pub order_type: OrderType, 21 | #[serde(rename = "c", skip_serializing_if = "Option::is_none")] 22 | pub cloid: Option, 23 | } 24 | 25 | #[derive(Debug, Clone, Serialize, Deserialize)] 26 | #[serde(rename_all = "camelCase")] 27 | pub enum OrderType { 28 | Limit(Limit), 29 | Trigger(Trigger), 30 | } 31 | 32 | #[derive(Debug, Clone, Serialize, Deserialize)] 33 | pub struct Limit { 34 | pub tif: String, // "Alo", "Ioc", "Gtc" 35 | } 36 | 37 | #[derive(Debug, Clone, Serialize, Deserialize)] 38 | #[serde(rename_all = "camelCase")] 39 | pub struct Trigger { 40 | #[serde(rename = "triggerPx")] 41 | pub trigger_px: String, 42 | #[serde(rename = "isMarket")] 43 | pub is_market: bool, 44 | pub tpsl: String, // "tp" or "sl" 45 | } 46 | 47 | // ==================== Cancel Types ==================== 48 | 49 | #[derive(Debug, Clone, Serialize, Deserialize)] 50 | pub struct CancelRequest { 51 | #[serde(rename = "a")] 52 | pub asset: u32, 53 | #[serde(rename = "o")] 54 | pub oid: u64, 55 | } 56 | 57 | #[derive(Debug, Clone, Serialize, Deserialize)] 58 | pub struct CancelRequestCloid { 59 | pub asset: u32, 60 | pub cloid: String, 61 | } 62 | 63 | // ==================== Modify Types ==================== 64 | 65 | #[derive(Debug, Clone, Serialize, Deserialize)] 66 | pub struct ModifyRequest { 67 | pub oid: u64, 68 | pub order: OrderRequest, 69 | } 70 | 71 | // ==================== Builder Types ==================== 72 | 73 | #[derive(Debug, Clone, Default, Serialize, Deserialize)] 74 | #[serde(rename_all = "camelCase")] 75 | pub struct BuilderInfo { 76 | #[serde(rename = "b")] 77 | pub builder: String, 78 | #[serde(rename = "f")] 79 | pub fee: u64, 80 | } 81 | 82 | // ==================== Convenience Methods ==================== 83 | 84 | impl OrderRequest { 85 | /// Create a limit order 86 | pub fn limit( 87 | asset: u32, 88 | is_buy: bool, 89 | limit_px: impl Into, 90 | sz: impl Into, 91 | tif: impl Into, 92 | ) -> Self { 93 | Self { 94 | asset, 95 | is_buy, 96 | limit_px: limit_px.into(), 97 | sz: sz.into(), 98 | reduce_only: false, 99 | order_type: OrderType::Limit(Limit { tif: tif.into() }), 100 | cloid: None, 101 | } 102 | } 103 | 104 | /// Create a trigger order (stop loss or take profit) 105 | pub fn trigger( 106 | asset: u32, 107 | is_buy: bool, 108 | trigger_px: impl Into, 109 | sz: impl Into, 110 | tpsl: impl Into, 111 | is_market: bool, 112 | ) -> Self { 113 | Self { 114 | asset, 115 | is_buy, 116 | limit_px: "0".to_string(), // Triggers don't use limit_px 117 | sz: sz.into(), 118 | reduce_only: false, 119 | order_type: OrderType::Trigger(Trigger { 120 | trigger_px: trigger_px.into(), 121 | is_market, 122 | tpsl: tpsl.into(), 123 | }), 124 | cloid: None, 125 | } 126 | } 127 | 128 | /// Set client order ID 129 | pub fn with_cloid(mut self, cloid: Option) -> Self { 130 | self.cloid = cloid.map(|id| format!("{:032x}", id.as_u128())); 131 | self 132 | } 133 | 134 | /// Set reduce only 135 | pub fn reduce_only(mut self, reduce_only: bool) -> Self { 136 | self.reduce_only = reduce_only; 137 | self 138 | } 139 | } 140 | 141 | // Convenience constructors for Cancel types 142 | impl CancelRequest { 143 | pub fn new(asset: u32, oid: u64) -> Self { 144 | Self { asset, oid } 145 | } 146 | } 147 | 148 | impl CancelRequestCloid { 149 | pub fn new(asset: u32, cloid: Uuid) -> Self { 150 | Self { 151 | asset, 152 | cloid: format!("{:032x}", cloid.as_u128()), 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /tests/managed_exchange_test.rs: -------------------------------------------------------------------------------- 1 | //! Tests for ManagedExchangeProvider 2 | 3 | use ferrofluid::{ 4 | providers::{ManagedExchangeProvider, OrderHandle}, 5 | types::requests::{OrderRequest, OrderType, Limit}, 6 | constants::*, 7 | }; 8 | use alloy::signers::local::PrivateKeySigner; 9 | use std::time::Duration; 10 | 11 | #[tokio::test] 12 | async fn test_managed_provider_creation() { 13 | // Initialize CryptoProvider for rustls 14 | rustls::crypto::CryptoProvider::install_default( 15 | rustls::crypto::ring::default_provider() 16 | ).ok(); 17 | 18 | // Create a test signer 19 | let signer = PrivateKeySigner::random(); 20 | 21 | // Create managed provider with default config 22 | let exchange = ManagedExchangeProvider::builder(signer) 23 | .with_network(ferrofluid::Network::Testnet) 24 | .build() 25 | .await; 26 | 27 | assert!(exchange.is_ok()); 28 | } 29 | 30 | #[tokio::test] 31 | async fn test_managed_provider_with_batching() { 32 | // Initialize CryptoProvider for rustls 33 | rustls::crypto::CryptoProvider::install_default( 34 | rustls::crypto::ring::default_provider() 35 | ).ok(); 36 | 37 | let signer = PrivateKeySigner::random(); 38 | 39 | // Create with batching enabled 40 | let exchange = ManagedExchangeProvider::builder(signer) 41 | .with_network(ferrofluid::Network::Testnet) 42 | .with_auto_batching(Duration::from_millis(50)) 43 | .without_agent_rotation() // Disable for testing 44 | .build() 45 | .await 46 | .unwrap(); 47 | 48 | // Create a test order 49 | let order = OrderRequest { 50 | asset: 0, 51 | is_buy: true, 52 | limit_px: "50000".to_string(), 53 | sz: "0.01".to_string(), 54 | reduce_only: false, 55 | order_type: OrderType::Limit(Limit { tif: TIF_GTC.to_string() }), 56 | cloid: None, 57 | }; 58 | 59 | // Place order should return pending handle 60 | let handle = exchange.place_order(&order).await.unwrap(); 61 | 62 | match handle { 63 | OrderHandle::Pending { .. } => { 64 | // Expected for batched orders 65 | } 66 | OrderHandle::Immediate(_) => { 67 | panic!("Expected pending handle for batched order"); 68 | } 69 | } 70 | } 71 | 72 | #[tokio::test] 73 | async fn test_alo_order_detection() { 74 | let order = OrderRequest { 75 | asset: 0, 76 | is_buy: true, 77 | limit_px: "50000".to_string(), 78 | sz: "0.01".to_string(), 79 | reduce_only: false, 80 | order_type: OrderType::Limit(Limit { tif: "Alo".to_string() }), 81 | cloid: None, 82 | }; 83 | 84 | assert!(order.is_alo()); 85 | 86 | let regular_order = OrderRequest { 87 | asset: 0, 88 | is_buy: true, 89 | limit_px: "50000".to_string(), 90 | sz: "0.01".to_string(), 91 | reduce_only: false, 92 | order_type: OrderType::Limit(Limit { tif: "Gtc".to_string() }), 93 | cloid: None, 94 | }; 95 | 96 | assert!(!regular_order.is_alo()); 97 | } 98 | 99 | #[test] 100 | fn test_nonce_generation() { 101 | use ferrofluid::providers::nonce::NonceManager; 102 | 103 | let manager = NonceManager::new(false); 104 | 105 | let nonce1 = manager.next_nonce(None); 106 | let nonce2 = manager.next_nonce(None); 107 | 108 | assert!(nonce2 > nonce1); 109 | assert!(NonceManager::is_valid_nonce(nonce1)); 110 | assert!(NonceManager::is_valid_nonce(nonce2)); 111 | } 112 | 113 | #[test] 114 | fn test_nonce_isolation() { 115 | use ferrofluid::providers::nonce::NonceManager; 116 | use alloy::primitives::Address; 117 | 118 | let manager = NonceManager::new(true); 119 | let addr1 = Address::new([1u8; 20]); 120 | let addr2 = Address::new([2u8; 20]); 121 | 122 | // Get initial nonces - these should have different millisecond timestamps 123 | let n1_1 = manager.next_nonce(Some(addr1)); 124 | std::thread::sleep(std::time::Duration::from_millis(1)); 125 | let n2_1 = manager.next_nonce(Some(addr2)); 126 | std::thread::sleep(std::time::Duration::from_millis(1)); 127 | let n1_2 = manager.next_nonce(Some(addr1)); 128 | std::thread::sleep(std::time::Duration::from_millis(1)); 129 | let n2_2 = manager.next_nonce(Some(addr2)); 130 | 131 | // Each address should have independent, increasing nonces 132 | assert!(n1_2 > n1_1, "addr1 nonces should increase"); 133 | assert!(n2_2 > n2_1, "addr2 nonces should increase"); 134 | 135 | // Verify counter independence using the manager's get_counter method 136 | assert_eq!(manager.get_counter(Some(addr1)), 2); // addr1 has 2 nonces 137 | assert_eq!(manager.get_counter(Some(addr2)), 2); // addr2 has 2 nonces 138 | 139 | // The nonces themselves should be unique 140 | assert_ne!(n1_1, n2_1); 141 | assert_ne!(n1_2, n2_2); 142 | } -------------------------------------------------------------------------------- /src/providers/nonce.rs: -------------------------------------------------------------------------------- 1 | //! Nonce management for Hyperliquid's sliding window system 2 | 3 | use std::sync::atomic::{AtomicU64, Ordering}; 4 | use std::time::{SystemTime, UNIX_EPOCH}; 5 | use alloy::primitives::Address; 6 | use dashmap::DashMap; 7 | 8 | /// Manages nonces for Hyperliquid's sliding window system 9 | /// 10 | /// Hyperliquid stores the 100 highest nonces per address and requires: 11 | /// - New nonce > smallest in the set 12 | /// - Never reuse a nonce 13 | /// - Nonces must be within (T - 2 days, T + 1 day) 14 | #[derive(Debug)] 15 | pub struct NonceManager { 16 | /// Separate nonce counters per address for subaccount isolation 17 | counters: DashMap, 18 | /// Global counter for addresses without isolation 19 | global_counter: AtomicU64, 20 | /// Whether to isolate nonces per address 21 | isolate_per_address: bool, 22 | } 23 | 24 | impl NonceManager { 25 | /// Create a new nonce manager 26 | pub fn new(isolate_per_address: bool) -> Self { 27 | Self { 28 | counters: DashMap::new(), 29 | global_counter: AtomicU64::new(0), 30 | isolate_per_address, 31 | } 32 | } 33 | 34 | /// Get the next nonce for an optional address 35 | pub fn next_nonce(&self, address: Option
) -> u64 { 36 | let now = SystemTime::now() 37 | .duration_since(UNIX_EPOCH) 38 | .unwrap() 39 | .as_millis() as u64; 40 | 41 | // Get counter increment 42 | let counter = if self.isolate_per_address && address.is_some() { 43 | let addr = address.unwrap(); 44 | self.counters 45 | .entry(addr) 46 | .or_insert_with(|| AtomicU64::new(0)) 47 | .fetch_add(1, Ordering::Relaxed) 48 | } else { 49 | self.global_counter.fetch_add(1, Ordering::Relaxed) 50 | }; 51 | 52 | // Add sub-millisecond offset to ensure uniqueness 53 | // This handles rapid-fire orders within the same millisecond 54 | now.saturating_add(counter % 1000) 55 | } 56 | 57 | /// Reset counter for a specific address (useful after agent rotation) 58 | pub fn reset_address(&self, address: Address) { 59 | if let Some(counter) = self.counters.get_mut(&address) { 60 | counter.store(0, Ordering::Relaxed); 61 | } 62 | } 63 | 64 | /// Get current counter value for monitoring 65 | pub fn get_counter(&self, address: Option
) -> u64 { 66 | if let Some(addr) = address { 67 | if self.isolate_per_address { 68 | self.counters 69 | .get(&addr) 70 | .map(|c| c.load(Ordering::Relaxed)) 71 | .unwrap_or(0) 72 | } else { 73 | self.global_counter.load(Ordering::Relaxed) 74 | } 75 | } else { 76 | self.global_counter.load(Ordering::Relaxed) 77 | } 78 | } 79 | 80 | /// Check if a nonce is within valid time bounds 81 | pub fn is_valid_nonce(nonce: u64) -> bool { 82 | let now = SystemTime::now() 83 | .duration_since(UNIX_EPOCH) 84 | .unwrap() 85 | .as_millis() as u64; 86 | 87 | // Must be within (T - 2 days, T + 1 day) 88 | const TWO_DAYS_MS: u64 = 2 * 24 * 60 * 60 * 1000; 89 | const ONE_DAY_MS: u64 = 24 * 60 * 60 * 1000; 90 | 91 | nonce > now.saturating_sub(TWO_DAYS_MS) && nonce < now.saturating_add(ONE_DAY_MS) 92 | } 93 | } 94 | 95 | #[cfg(test)] 96 | mod tests { 97 | use super::*; 98 | 99 | #[test] 100 | fn test_nonce_uniqueness() { 101 | let manager = NonceManager::new(false); 102 | 103 | let nonce1 = manager.next_nonce(None); 104 | let nonce2 = manager.next_nonce(None); 105 | 106 | assert_ne!(nonce1, nonce2); 107 | assert!(nonce2 > nonce1); 108 | } 109 | 110 | #[test] 111 | fn test_address_isolation() { 112 | let manager = NonceManager::new(true); 113 | let addr1 = Address::new([1u8; 20]); 114 | let addr2 = Address::new([2u8; 20]); 115 | 116 | // Get nonces for different addresses 117 | let n1_1 = manager.next_nonce(Some(addr1)); 118 | let n2_1 = manager.next_nonce(Some(addr2)); 119 | let n1_2 = manager.next_nonce(Some(addr1)); 120 | let n2_2 = manager.next_nonce(Some(addr2)); 121 | 122 | // Each address should have independent counters 123 | assert!(n1_2 > n1_1); 124 | assert!(n2_2 > n2_1); 125 | 126 | // Counters should be independent 127 | assert_eq!(manager.get_counter(Some(addr1)), 2); 128 | assert_eq!(manager.get_counter(Some(addr2)), 2); 129 | } 130 | 131 | #[test] 132 | fn test_nonce_validity() { 133 | let now = SystemTime::now() 134 | .duration_since(UNIX_EPOCH) 135 | .unwrap() 136 | .as_millis() as u64; 137 | 138 | // Valid: current time 139 | assert!(NonceManager::is_valid_nonce(now)); 140 | 141 | // Valid: 1 day ago 142 | assert!(NonceManager::is_valid_nonce(now - 24 * 60 * 60 * 1000)); 143 | 144 | // Invalid: 3 days ago 145 | assert!(!NonceManager::is_valid_nonce(now - 3 * 24 * 60 * 60 * 1000)); 146 | 147 | // Invalid: 2 days in future 148 | assert!(!NonceManager::is_valid_nonce(now + 2 * 24 * 60 * 60 * 1000)); 149 | } 150 | } -------------------------------------------------------------------------------- /examples/02_info_provider.rs: -------------------------------------------------------------------------------- 1 | use std::time::{SystemTime, UNIX_EPOCH}; 2 | 3 | use ferrofluid::providers::InfoProvider; 4 | 5 | #[tokio::main] 6 | async fn main() -> Result<(), Box> { 7 | // Initialize tracing 8 | tracing_subscriber::fmt::init(); 9 | 10 | // Create mainnet provider 11 | let info = InfoProvider::mainnet(); 12 | 13 | // 1. Test all_mids endpoint 14 | println!("=== Testing all_mids ==="); 15 | match info.all_mids().await { 16 | Ok(mids) => { 17 | println!("Found {} mid prices", mids.len()); 18 | // Print first 5 entries 19 | for (coin, price) in mids.iter().take(5) { 20 | println!("{}: {}", coin, price); 21 | } 22 | } 23 | Err(e) => println!("Error fetching mids: {}", e), 24 | } 25 | 26 | // 2. Test l2_book endpoint 27 | println!("\n=== Testing l2_book for BTC ==="); 28 | match info.l2_book("BTC").await { 29 | Ok(book) => { 30 | println!("BTC Order Book at time {}", book.time); 31 | println!( 32 | "Levels: {} bid levels, {} ask levels", 33 | book.levels[0].len(), 34 | book.levels[1].len() 35 | ); 36 | 37 | // Show top 3 levels each side 38 | println!("\nTop 3 Bids:"); 39 | for level in book.levels[0].iter().take(3) { 40 | println!( 41 | " Price: {}, Size: {}, Count: {}", 42 | level.px, level.sz, level.n 43 | ); 44 | } 45 | 46 | println!("\nTop 3 Asks:"); 47 | for level in book.levels[1].iter().take(3) { 48 | println!( 49 | " Price: {}, Size: {}, Count: {}", 50 | level.px, level.sz, level.n 51 | ); 52 | } 53 | } 54 | Err(e) => println!("Error fetching L2 book: {}", e), 55 | } 56 | 57 | // 3. Test recent_trades endpoint 58 | println!("\n=== Testing recent_trades for ETH ==="); 59 | match info.recent_trades("ETH").await { 60 | Ok(trades) => { 61 | println!("Recent ETH trades: {} trades", trades.len()); 62 | for trade in trades.iter().take(5) { 63 | println!( 64 | " Time: {}, Side: {}, Price: {}, Size: {}", 65 | trade.time, trade.side, trade.px, trade.sz 66 | ); 67 | } 68 | } 69 | Err(e) => println!("Error fetching recent trades: {}", e), 70 | } 71 | 72 | // 4. Test candles with builder pattern 73 | println!("\n=== Testing candles for SOL ==="); 74 | let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64; 75 | let one_hour_ago = now - (60 * 60 * 1000); // 1 hour in milliseconds 76 | 77 | match info 78 | .candles("SOL") 79 | .interval("15m") 80 | .time_range(one_hour_ago, now) 81 | .send() 82 | .await 83 | { 84 | Ok(candles) => { 85 | println!("SOL 15m candles: {} candles", candles.len()); 86 | for candle in candles.iter().take(3) { 87 | println!( 88 | " Time: {}-{}, O: {}, H: {}, L: {}, C: {}, V: {}", 89 | candle.time_open, 90 | candle.time_close, 91 | candle.open, 92 | candle.high, 93 | candle.low, 94 | candle.close, 95 | candle.vlm 96 | ); 97 | } 98 | } 99 | Err(e) => println!("Error fetching candles: {}", e), 100 | } 101 | 102 | // 5. Test meta endpoint 103 | println!("\n=== Testing meta endpoint ==="); 104 | match info.meta().await { 105 | Ok(meta) => { 106 | println!("Found {} assets in universe", meta.universe.len()); 107 | for asset in meta.universe.iter().take(5) { 108 | println!( 109 | " {}: decimals={}, max_leverage={}, isolated_only={}", 110 | asset.name, 111 | asset.sz_decimals, 112 | asset.max_leverage, 113 | asset.only_isolated 114 | ); 115 | } 116 | } 117 | Err(e) => println!("Error fetching meta: {}", e), 118 | } 119 | 120 | // 6. Test spot_meta endpoint 121 | println!("\n=== Testing spot_meta endpoint ==="); 122 | match info.spot_meta().await { 123 | Ok(spot_meta) => { 124 | println!("Found {} spot pairs", spot_meta.universe.len()); 125 | println!("Found {} tokens", spot_meta.tokens.len()); 126 | 127 | println!("\nFirst 5 spot pairs:"); 128 | for pair in spot_meta.universe.iter().take(5) { 129 | println!( 130 | " {}: index={}, canonical={}, tokens={:?}", 131 | pair.name, pair.index, pair.is_canonical, pair.tokens 132 | ); 133 | } 134 | 135 | println!("\nFirst 5 tokens:"); 136 | for token in spot_meta.tokens.iter().take(5) { 137 | println!( 138 | " {}: index={}, wei_decimals={}, token_id={}", 139 | token.name, 140 | token.index, 141 | token.wei_decimals, 142 | &token.token_id[..16] 143 | ); 144 | } 145 | } 146 | Err(e) => println!("Error fetching spot meta: {}", e), 147 | } 148 | 149 | // 7. Test funding_history with builder 150 | println!("\n=== Testing funding_history for BTC ==="); 151 | let one_day_ago = now - (24 * 60 * 60 * 1000); // 24 hours in milliseconds 152 | 153 | match info 154 | .funding_history("BTC") 155 | .time_range(one_day_ago, now) 156 | .send() 157 | .await 158 | { 159 | Ok(history) => { 160 | println!("BTC funding history: {} entries", history.len()); 161 | for entry in history.iter().take(5) { 162 | println!( 163 | " Time: {}, Rate: {}, Premium: {}", 164 | entry.time, entry.funding_rate, entry.premium 165 | ); 166 | } 167 | } 168 | Err(e) => println!("Error fetching funding history: {}", e), 169 | } 170 | 171 | println!("\n=== All tests completed ==="); 172 | Ok(()) 173 | } 174 | -------------------------------------------------------------------------------- /src/signers/privy.rs: -------------------------------------------------------------------------------- 1 | use alloy::{ 2 | primitives::{Address, B256}, 3 | }; 4 | use async_trait::async_trait; 5 | use base64::{engine::general_purpose, Engine as _}; 6 | use reqwest::{Client, StatusCode}; 7 | use serde::Deserialize; 8 | use serde_json::{json, Value}; 9 | use std::{error::Error, fmt, sync::Arc}; 10 | 11 | use crate::signers::{HyperliquidSignature, HyperliquidSigner, SignerError}; 12 | 13 | const PRIVY_API: &str = "https://api.privy.io/v1"; 14 | 15 | /// Privy-specific errors 16 | #[derive(Debug)] 17 | pub enum PrivyError { 18 | Http(reqwest::Error), 19 | Api(StatusCode, String), 20 | Serde(serde_json::Error), 21 | Hex(hex::FromHexError), 22 | InvalidSignature, 23 | MissingEnvVar(String), 24 | } 25 | 26 | impl fmt::Display for PrivyError { 27 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 28 | use PrivyError::*; 29 | match self { 30 | Http(e) => write!(f, "network error: {e}"), 31 | Api(code, s) => write!(f, "privy error {code}: {s}"), 32 | Serde(e) => write!(f, "serde error: {e}"), 33 | Hex(e) => write!(f, "hex decode error: {e}"), 34 | InvalidSignature => write!(f, "cannot parse signature from response"), 35 | MissingEnvVar(var) => write!(f, "missing environment variable: {var}"), 36 | } 37 | } 38 | } 39 | 40 | impl Error for PrivyError {} 41 | 42 | impl From for PrivyError { 43 | fn from(e: reqwest::Error) -> Self { 44 | Self::Http(e) 45 | } 46 | } 47 | 48 | impl From for PrivyError { 49 | fn from(e: serde_json::Error) -> Self { 50 | Self::Serde(e) 51 | } 52 | } 53 | 54 | impl From for PrivyError { 55 | fn from(e: hex::FromHexError) -> Self { 56 | Self::Hex(e) 57 | } 58 | } 59 | 60 | /// Privy signer implementation for Hyperliquid 61 | #[derive(Clone)] 62 | pub struct PrivySigner { 63 | client: Arc, 64 | wallet_id: String, 65 | address: Address, 66 | app_id: String, 67 | basic_auth: String, 68 | } 69 | 70 | impl PrivySigner { 71 | /// Create a new Privy signer 72 | /// Reads PRIVY_APP_ID and PRIVY_SECRET from environment variables 73 | pub fn new(wallet_id: String, address: Address) -> Result { 74 | let app_id = std::env::var("PRIVY_APP_ID") 75 | .map_err(|_| PrivyError::MissingEnvVar("PRIVY_APP_ID".to_string()))?; 76 | let secret = std::env::var("PRIVY_SECRET") 77 | .map_err(|_| PrivyError::MissingEnvVar("PRIVY_SECRET".to_string()))?; 78 | 79 | let creds = general_purpose::STANDARD.encode(format!("{app_id}:{secret}")); 80 | 81 | Ok(Self { 82 | client: Arc::new(Client::builder().build()?), 83 | wallet_id, 84 | address, 85 | app_id, 86 | basic_auth: format!("Basic {creds}"), 87 | }) 88 | } 89 | 90 | /// Create a new Privy signer with explicit credentials 91 | pub fn with_credentials( 92 | wallet_id: String, 93 | address: Address, 94 | app_id: String, 95 | secret: String, 96 | ) -> Result { 97 | let creds = general_purpose::STANDARD.encode(format!("{app_id}:{secret}")); 98 | 99 | Ok(Self { 100 | client: Arc::new(Client::builder().build()?), 101 | wallet_id, 102 | address, 103 | app_id, 104 | basic_auth: format!("Basic {creds}"), 105 | }) 106 | } 107 | 108 | /// Internal RPC helper 109 | async fn rpc Deserialize<'de>>( 110 | &self, 111 | body: Value, 112 | ) -> Result { 113 | let url = format!("{PRIVY_API}/wallets/{}/rpc", self.wallet_id); 114 | let resp = self 115 | .client 116 | .post(url) 117 | .header("Authorization", &self.basic_auth) 118 | .header("privy-app-id", &self.app_id) 119 | .header("Content-Type", "application/json") 120 | .json(&body) 121 | .send() 122 | .await?; 123 | 124 | let status = resp.status(); 125 | if !status.is_success() { 126 | let txt = resp.text().await.unwrap_or_default(); 127 | return Err(PrivyError::Api(status, txt)); 128 | } 129 | 130 | Ok(resp.json::().await?) 131 | } 132 | } 133 | 134 | #[derive(Deserialize)] 135 | struct SignResponse { 136 | data: SignData, 137 | } 138 | 139 | #[derive(Deserialize)] 140 | struct SignData { 141 | signature: String, 142 | } 143 | 144 | #[async_trait] 145 | impl HyperliquidSigner for PrivySigner { 146 | async fn sign_hash(&self, hash: B256) -> Result { 147 | // Convert hash to hex string with 0x prefix 148 | let hash_hex = format!("0x{}", hex::encode(hash)); 149 | 150 | // Use secp256k1_sign for raw hash signing 151 | let body = json!({ 152 | "method": "secp256k1_sign", 153 | "params": { 154 | "hash": hash_hex 155 | } 156 | }); 157 | 158 | let resp: SignResponse = self.rpc(body) 159 | .await 160 | .map_err(|e| SignerError::SigningFailed(e.to_string()))?; 161 | 162 | // Parse the signature string (0x-prefixed hex) 163 | let sig_hex = resp.data.signature 164 | .strip_prefix("0x") 165 | .unwrap_or(&resp.data.signature); 166 | 167 | let sig_bytes = hex::decode(sig_hex) 168 | .map_err(|e| SignerError::SigningFailed(format!("Invalid hex signature: {}", e)))?; 169 | 170 | if sig_bytes.len() != 65 { 171 | return Err(SignerError::SigningFailed( 172 | format!("Invalid signature length: expected 65, got {}", sig_bytes.len()) 173 | )); 174 | } 175 | 176 | // Extract r, s, v from the signature bytes 177 | let mut r_bytes = [0u8; 32]; 178 | let mut s_bytes = [0u8; 32]; 179 | r_bytes.copy_from_slice(&sig_bytes[0..32]); 180 | s_bytes.copy_from_slice(&sig_bytes[32..64]); 181 | let v = sig_bytes[64]; 182 | 183 | // Convert v to EIP-155 format if needed 184 | let v = if v < 27 { 185 | v + 27 186 | } else { 187 | v 188 | }; 189 | 190 | Ok(HyperliquidSignature { 191 | r: alloy::primitives::U256::from_be_bytes(r_bytes), 192 | s: alloy::primitives::U256::from_be_bytes(s_bytes), 193 | v: v as u64, 194 | }) 195 | } 196 | 197 | fn address(&self) -> Address { 198 | self.address 199 | } 200 | } 201 | 202 | #[cfg(test)] 203 | mod tests { 204 | use super::*; 205 | use alloy::primitives::address; 206 | 207 | #[test] 208 | fn test_privy_signer_creation() { 209 | // This test would require actual Privy credentials 210 | // For now, just test that missing env vars return appropriate errors 211 | let result = PrivySigner::new( 212 | "test-wallet-id".to_string(), 213 | address!("0000000000000000000000000000000000000000"), 214 | ); 215 | 216 | match result { 217 | Err(PrivyError::MissingEnvVar(var)) => { 218 | assert!(var == "PRIVY_APP_ID" || var == "PRIVY_SECRET"); 219 | } 220 | _ => panic!("Expected MissingEnvVar error"), 221 | } 222 | } 223 | } -------------------------------------------------------------------------------- /src/signers/signer.rs: -------------------------------------------------------------------------------- 1 | use alloy::{ 2 | primitives::{Address, B256, Parity, U256}, 3 | signers::Signer, 4 | }; 5 | use async_trait::async_trait; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct HyperliquidSignature { 9 | pub r: U256, 10 | pub s: U256, 11 | pub v: u64, 12 | } 13 | 14 | #[async_trait] 15 | pub trait HyperliquidSigner: Send + Sync { 16 | /// Sign a hash and return the signature 17 | async fn sign_hash(&self, hash: B256) -> Result; 18 | 19 | /// Get the address of this signer 20 | fn address(&self) -> Address; 21 | } 22 | 23 | #[derive(Debug, thiserror::Error)] 24 | pub enum SignerError { 25 | #[error("signing failed: {0}")] 26 | SigningFailed(String), 27 | 28 | #[error("signer unavailable")] 29 | Unavailable, 30 | } 31 | 32 | pub struct AlloySigner { 33 | pub inner: S, 34 | } 35 | 36 | // Direct implementation for PrivateKeySigner 37 | #[async_trait] 38 | impl HyperliquidSigner for alloy::signers::local::PrivateKeySigner { 39 | async fn sign_hash(&self, hash: B256) -> Result { 40 | let sig = ::sign_hash(self, &hash) 41 | .await 42 | .map_err(|e| SignerError::SigningFailed(e.to_string()))?; 43 | 44 | // Convert Parity to v value (27 or 28) 45 | let v = match sig.v() { 46 | Parity::Eip155(v) => v, 47 | Parity::NonEip155(true) => 28, 48 | Parity::NonEip155(false) => 27, 49 | Parity::Parity(true) => 28, 50 | Parity::Parity(false) => 27, 51 | }; 52 | 53 | Ok(HyperliquidSignature { 54 | r: sig.r(), 55 | s: sig.s(), 56 | v, 57 | }) 58 | } 59 | 60 | fn address(&self) -> Address { 61 | self.address() 62 | } 63 | } 64 | 65 | #[async_trait] 66 | impl HyperliquidSigner for AlloySigner 67 | where 68 | S: Signer + Send + Sync, 69 | { 70 | async fn sign_hash(&self, hash: B256) -> Result { 71 | let sig = self 72 | .inner 73 | .sign_hash(&hash) 74 | .await 75 | .map_err(|e| SignerError::SigningFailed(e.to_string()))?; 76 | 77 | // Convert Parity to v value (27 or 28) 78 | let v = match sig.v() { 79 | Parity::Eip155(v) => v, 80 | Parity::NonEip155(true) => 28, 81 | Parity::NonEip155(false) => 27, 82 | Parity::Parity(true) => 28, 83 | Parity::Parity(false) => 27, 84 | }; 85 | 86 | Ok(HyperliquidSignature { 87 | r: sig.r(), 88 | s: sig.s(), 89 | v, 90 | }) 91 | } 92 | 93 | fn address(&self) -> Address { 94 | self.inner.address() 95 | } 96 | } 97 | 98 | #[cfg(test)] 99 | mod tests { 100 | use alloy::{primitives::b256, signers::local::PrivateKeySigner}; 101 | 102 | use super::*; 103 | use crate::types::{Agent, HyperliquidAction, UsdSend, Withdraw}; 104 | 105 | fn get_test_signer() -> AlloySigner { 106 | let private_key = 107 | "e908f86dbb4d55ac876378565aafeabc187f6690f046459397b17d9b9a19688e"; 108 | let signer = private_key.parse::().unwrap(); 109 | AlloySigner { inner: signer } 110 | } 111 | 112 | #[tokio::test] 113 | async fn test_sign_l1_action() -> Result<(), Box> { 114 | let signer = get_test_signer(); 115 | let connection_id = 116 | b256!("de6c4037798a4434ca03cd05f00e3b803126221375cd1e7eaaaf041768be06eb"); 117 | 118 | // Create Agent action 119 | let agent = Agent { 120 | source: "a".to_string(), 121 | connection_id, 122 | }; 123 | 124 | // L1 actions use the Exchange domain with chain ID 1337 - provided by l1_action! macro 125 | let domain = agent.domain(); 126 | 127 | // Use the action's eip712_signing_hash method which handles everything 128 | let signing_hash = agent.eip712_signing_hash(&domain); 129 | 130 | let mainnet_sig = signer.sign_hash(signing_hash).await?; 131 | 132 | let expected_mainnet = "fa8a41f6a3fa728206df80801a83bcbfbab08649cd34d9c0bfba7c7b2f99340f53a00226604567b98a1492803190d65a201d6805e5831b7044f17fd530aec7841c"; 133 | let actual = format!( 134 | "{:064x}{:064x}{:02x}", 135 | mainnet_sig.r, mainnet_sig.s, mainnet_sig.v 136 | ); 137 | 138 | assert_eq!(actual, expected_mainnet); 139 | 140 | // Test testnet signature with source "b" 141 | let agent_testnet = Agent { 142 | source: "b".to_string(), 143 | connection_id, 144 | }; 145 | 146 | let testnet_hash = agent_testnet.eip712_signing_hash(&agent_testnet.domain()); 147 | let testnet_sig = signer.sign_hash(testnet_hash).await?; 148 | 149 | let expected_testnet = "1713c0fc661b792a50e8ffdd59b637b1ed172d9a3aa4d801d9d88646710fb74b33959f4d075a7ccbec9f2374a6da21ffa4448d58d0413a0d335775f680a881431c"; 150 | let actual_testnet = format!( 151 | "{:064x}{:064x}{:02x}", 152 | testnet_sig.r, testnet_sig.s, testnet_sig.v 153 | ); 154 | 155 | assert_eq!(actual_testnet, expected_testnet); 156 | 157 | Ok(()) 158 | } 159 | 160 | #[tokio::test] 161 | async fn test_sign_usd_transfer_action() -> Result<(), Box> { 162 | let signer = get_test_signer(); 163 | 164 | // Create UsdSend action 165 | let usd_send = UsdSend { 166 | signature_chain_id: 421614, 167 | hyperliquid_chain: "Testnet".to_string(), 168 | destination: "0x0D1d9635D0640821d15e323ac8AdADfA9c111414".to_string(), 169 | amount: "1".to_string(), 170 | time: 1690393044548, 171 | }; 172 | 173 | // Use the action's own domain method which uses signature_chain_id 174 | let domain = usd_send.domain(); 175 | 176 | // Use the action's eip712_signing_hash method 177 | let signing_hash = usd_send.eip712_signing_hash(&domain); 178 | 179 | let sig = signer.sign_hash(signing_hash).await?; 180 | 181 | let expected = "214d507bbdaebba52fa60928f904a8b2df73673e3baba6133d66fe846c7ef70451e82453a6d8db124e7ed6e60fa00d4b7c46e4d96cb2bd61fd81b6e8953cc9d21b"; 182 | let actual = format!("{:064x}{:064x}{:02x}", sig.r, sig.s, sig.v); 183 | 184 | assert_eq!(actual, expected); 185 | 186 | Ok(()) 187 | } 188 | 189 | #[tokio::test] 190 | async fn test_sign_withdraw_action() -> Result<(), Box> { 191 | let signer = get_test_signer(); 192 | 193 | // Create Withdraw action 194 | let withdraw = Withdraw { 195 | signature_chain_id: 421614, 196 | hyperliquid_chain: "Testnet".to_string(), 197 | destination: "0x0D1d9635D0640821d15e323ac8AdADfA9c111414".to_string(), 198 | amount: "1".to_string(), 199 | time: 1690393044548, 200 | }; 201 | 202 | // Use the action's own domain method which uses signature_chain_id 203 | let domain = withdraw.domain(); 204 | 205 | // Use the action's eip712_signing_hash method 206 | let signing_hash = withdraw.eip712_signing_hash(&domain); 207 | 208 | let sig = signer.sign_hash(signing_hash).await?; 209 | 210 | let expected = "b3172e33d2262dac2b4cb135ce3c167fda55dafa6c62213564ab728b9f9ba76b769a938e9f6d603dae7154c83bf5a4c3ebab81779dc2db25463a3ed663c82ae41c"; 211 | let actual = format!("{:064x}{:064x}{:02x}", sig.r, sig.s, sig.v); 212 | 213 | assert_eq!(actual, expected); 214 | 215 | Ok(()) 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/providers/agent.rs: -------------------------------------------------------------------------------- 1 | //! Agent wallet management with automatic rotation and safety features 2 | 3 | use std::sync::Arc; 4 | use std::time::{Duration, Instant}; 5 | use alloy::primitives::Address; 6 | use alloy::signers::local::PrivateKeySigner; 7 | use tokio::sync::RwLock; 8 | use crate::{ 9 | signers::HyperliquidSigner, 10 | errors::HyperliquidError, 11 | providers::nonce::NonceManager, 12 | Network, 13 | }; 14 | 15 | /// Agent wallet with lifecycle tracking 16 | #[derive(Clone)] 17 | pub struct AgentWallet { 18 | /// Agent's address 19 | pub address: Address, 20 | /// Agent's signer 21 | pub signer: PrivateKeySigner, 22 | /// When this agent was created 23 | pub created_at: Instant, 24 | /// Dedicated nonce manager for this agent 25 | pub nonce_manager: Arc, 26 | /// Current status 27 | pub status: AgentStatus, 28 | } 29 | 30 | #[derive(Clone, Debug, PartialEq)] 31 | pub enum AgentStatus { 32 | /// Agent is active and healthy 33 | Active, 34 | /// Agent is marked for rotation 35 | PendingRotation, 36 | /// Agent has been deregistered 37 | Deregistered, 38 | } 39 | 40 | impl AgentWallet { 41 | /// Create a new agent wallet 42 | pub fn new(signer: PrivateKeySigner) -> Self { 43 | Self { 44 | address: signer.address(), 45 | signer, 46 | created_at: Instant::now(), 47 | nonce_manager: Arc::new(NonceManager::new(false)), // No isolation within agent 48 | status: AgentStatus::Active, 49 | } 50 | } 51 | 52 | /// Check if agent should be rotated based on TTL 53 | pub fn should_rotate(&self, ttl: Duration) -> bool { 54 | match self.status { 55 | AgentStatus::Active => self.created_at.elapsed() > ttl, 56 | AgentStatus::PendingRotation | AgentStatus::Deregistered => true, 57 | } 58 | } 59 | 60 | /// Get next nonce for this agent 61 | pub fn next_nonce(&self) -> u64 { 62 | self.nonce_manager.next_nonce(None) 63 | } 64 | } 65 | 66 | /// Configuration for agent management 67 | #[derive(Clone, Debug)] 68 | pub struct AgentConfig { 69 | /// Time before rotating an agent 70 | pub ttl: Duration, 71 | /// Check agent health at this interval 72 | pub health_check_interval: Duration, 73 | /// Rotate agents proactively before expiry 74 | pub proactive_rotation_buffer: Duration, 75 | } 76 | 77 | impl Default for AgentConfig { 78 | fn default() -> Self { 79 | Self { 80 | ttl: Duration::from_secs(23 * 60 * 60), // Rotate daily 81 | health_check_interval: Duration::from_secs(300), // Check every 5 min 82 | proactive_rotation_buffer: Duration::from_secs(60 * 60), // Rotate 1hr before expiry 83 | } 84 | } 85 | } 86 | 87 | /// Manages agent lifecycle with automatic rotation 88 | pub struct AgentManager { 89 | /// Master signer that approves agents 90 | master_signer: S, 91 | /// Currently active agents by name 92 | agents: Arc>>, 93 | /// Configuration 94 | config: AgentConfig, 95 | /// Network for agent operations 96 | network: Network, 97 | } 98 | 99 | impl AgentManager { 100 | /// Create a new agent manager 101 | pub fn new(master_signer: S, config: AgentConfig, network: Network) -> Self { 102 | Self { 103 | master_signer, 104 | agents: Arc::new(RwLock::new(std::collections::HashMap::new())), 105 | config, 106 | network, 107 | } 108 | } 109 | 110 | /// Get or create an agent, rotating if necessary 111 | pub async fn get_or_rotate_agent(&self, name: &str) -> Result { 112 | let mut agents = self.agents.write().await; 113 | 114 | // Check if we have an active agent 115 | if let Some(agent) = agents.get(name) { 116 | let effective_ttl = self.config.ttl.saturating_sub(self.config.proactive_rotation_buffer); 117 | 118 | if !agent.should_rotate(effective_ttl) { 119 | return Ok(agent.clone()); 120 | } 121 | 122 | // Mark for rotation 123 | let mut agent_mut = agent.clone(); 124 | agent_mut.status = AgentStatus::PendingRotation; 125 | agents.insert(name.to_string(), agent_mut); 126 | } 127 | 128 | // Create new agent 129 | let new_agent = self.create_new_agent(name).await?; 130 | agents.insert(name.to_string(), new_agent.clone()); 131 | 132 | Ok(new_agent) 133 | } 134 | 135 | /// Create and approve a new agent 136 | async fn create_new_agent(&self, name: &str) -> Result { 137 | // Generate new key for agent 138 | let agent_signer = PrivateKeySigner::random(); 139 | let agent_wallet = AgentWallet::new(agent_signer.clone()); 140 | 141 | // We need to approve this agent using the exchange provider 142 | // This is a bit circular, but we'll handle it carefully 143 | self.approve_agent_internal(agent_wallet.address, Some(name.to_string())).await?; 144 | 145 | Ok(agent_wallet) 146 | } 147 | 148 | /// Internal method to approve agent (will use exchange provider) 149 | async fn approve_agent_internal(&self, agent_address: Address, name: Option) -> Result<(), HyperliquidError> { 150 | use crate::providers::RawExchangeProvider; 151 | 152 | // Create a temporary raw provider just for agent approval 153 | let raw_provider = match self.network { 154 | Network::Mainnet => RawExchangeProvider::mainnet(self.master_signer.clone()), 155 | Network::Testnet => RawExchangeProvider::testnet(self.master_signer.clone()), 156 | }; 157 | 158 | // Approve the agent 159 | raw_provider.approve_agent(agent_address, name).await?; 160 | 161 | Ok(()) 162 | } 163 | 164 | /// Get all active agents 165 | pub async fn get_active_agents(&self) -> Vec<(String, AgentWallet)> { 166 | let agents = self.agents.read().await; 167 | agents.iter() 168 | .filter(|(_, agent)| agent.status == AgentStatus::Active) 169 | .map(|(name, agent)| (name.clone(), agent.clone())) 170 | .collect() 171 | } 172 | 173 | /// Mark an agent as deregistered 174 | pub async fn mark_deregistered(&self, name: &str) { 175 | let mut agents = self.agents.write().await; 176 | if let Some(agent) = agents.get_mut(name) { 177 | agent.status = AgentStatus::Deregistered; 178 | } 179 | } 180 | 181 | /// Clean up deregistered agents 182 | pub async fn cleanup_deregistered(&self) { 183 | let mut agents = self.agents.write().await; 184 | agents.retain(|_, agent| agent.status != AgentStatus::Deregistered); 185 | } 186 | } 187 | 188 | #[cfg(test)] 189 | mod tests { 190 | use super::*; 191 | 192 | #[test] 193 | fn test_agent_rotation_check() { 194 | let signer = PrivateKeySigner::random(); 195 | let agent = AgentWallet::new(signer); 196 | 197 | // Should not rotate immediately 198 | assert!(!agent.should_rotate(Duration::from_secs(24 * 60 * 60))); 199 | 200 | // Test with zero duration (should always rotate) 201 | assert!(agent.should_rotate(Duration::ZERO)); 202 | } 203 | 204 | #[test] 205 | fn test_agent_nonce_generation() { 206 | let signer = PrivateKeySigner::random(); 207 | let agent = AgentWallet::new(signer); 208 | 209 | let nonce1 = agent.next_nonce(); 210 | let nonce2 = agent.next_nonce(); 211 | 212 | assert!(nonce2 > nonce1); 213 | } 214 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ferrofluid 2 | 3 | ![Ferrofluid](ferrofluid-background.png) 4 | 5 | A high-performance Rust SDK for the Hyperliquid Protocol, built with a "thin wrapper, maximum control" philosophy. 6 | 7 | [![Crates.io](https://img.shields.io/crates/v/ferrofluid.svg)](https://crates.io/crates/ferrofluid) 8 | 9 | ## Features 10 | 11 | - 🚀 **High Performance**: Uses `simd-json` for 10x faster JSON parsing than `serde_json` 12 | - 🔒 **Type-Safe**: Strongly-typed Rust bindings with compile-time guarantees 13 | - 🎯 **Direct Control**: No hidden retry logic or complex abstractions - you control the flow 14 | - ⚡ **Fast WebSockets**: Built on `fastwebsockets` for 3-4x performance over `tungstenite` 15 | - 🛠️ **Builder Support**: Native support for MEV builders with configurable fees 16 | - 📊 **Complete API Coverage**: Info, Exchange, and WebSocket providers for all endpoints 17 | 18 | ## Installation 19 | 20 | Add this to your `Cargo.toml`: 21 | 22 | ```toml 23 | [dependencies] 24 | ferrofluid = "0.1.0" 25 | ``` 26 | 27 | ## Quick Start 28 | 29 | ### Reading Market Data 30 | 31 | ```rust 32 | use ferrofluid::{InfoProvider, Network}; 33 | 34 | #[tokio::main] 35 | async fn main() -> Result<(), Box> { 36 | let info = InfoProvider::new(Network::Mainnet); 37 | 38 | // Get all mid prices 39 | let mids = info.all_mids().await?; 40 | println!("BTC mid price: {}", mids["BTC"]); 41 | 42 | // Get L2 order book 43 | let book = info.l2_book("ETH").await?; 44 | println!("ETH best bid: {:?}", book.levels[0][0]); 45 | 46 | Ok(()) 47 | } 48 | ``` 49 | 50 | ### Placing Orders 51 | 52 | ```rust 53 | use ferrofluid::{ExchangeProvider, signers::AlloySigner}; 54 | use alloy::signers::local::PrivateKeySigner; 55 | 56 | #[tokio::main] 57 | async fn main() -> Result<(), Box> { 58 | // Setup signer 59 | let signer = PrivateKeySigner::random(); 60 | let hyperliquid_signer = AlloySigner { inner: signer }; 61 | 62 | // Create exchange provider 63 | let exchange = ExchangeProvider::mainnet(hyperliquid_signer); 64 | 65 | // Place an order using the builder pattern 66 | let result = exchange.order(0) // BTC perpetual 67 | .limit_buy("50000", "0.001") 68 | .reduce_only(false) 69 | .send() 70 | .await?; 71 | 72 | println!("Order placed: {:?}", result); 73 | Ok(()) 74 | } 75 | ``` 76 | 77 | ### WebSocket Subscriptions 78 | 79 | ```rust 80 | use ferrofluid::{WsProvider, Network, types::ws::Message}; 81 | 82 | #[tokio::main] 83 | async fn main() -> Result<(), Box> { 84 | let mut ws = WsProvider::connect(Network::Mainnet).await?; 85 | 86 | // Subscribe to BTC order book 87 | let (_id, mut rx) = ws.subscribe_l2_book("BTC").await?; 88 | ws.start_reading().await?; 89 | 90 | // Handle updates 91 | while let Some(msg) = rx.recv().await { 92 | match msg { 93 | Message::L2Book(book) => { 94 | println!("BTC book update: {:?}", book.data.coin); 95 | } 96 | _ => {} 97 | } 98 | } 99 | 100 | Ok(()) 101 | } 102 | ``` 103 | 104 | ### Managed WebSocket with Auto-Reconnect 105 | 106 | For production use, consider the `ManagedWsProvider` which adds automatic reconnection and keep-alive: 107 | 108 | ```rust 109 | use ferrofluid::{ManagedWsProvider, WsConfig, Network}; 110 | use std::time::Duration; 111 | 112 | #[tokio::main] 113 | async fn main() -> Result<(), Box> { 114 | // Configure with custom settings 115 | let config = WsConfig { 116 | ping_interval: Duration::from_secs(30), 117 | auto_reconnect: true, 118 | exponential_backoff: true, 119 | ..Default::default() 120 | }; 121 | 122 | let ws = ManagedWsProvider::connect(Network::Mainnet, config).await?; 123 | 124 | // Subscriptions automatically restore on reconnect 125 | let (_id, mut rx) = ws.subscribe_l2_book("BTC").await?; 126 | ws.start_reading().await?; 127 | 128 | // Your subscriptions survive disconnections! 129 | while let Some(msg) = rx.recv().await { 130 | // Handle messages... 131 | } 132 | 133 | Ok(()) 134 | } 135 | ``` 136 | 137 | ## Examples 138 | 139 | The `examples/` directory contains comprehensive examples: 140 | 141 | - `00_symbols.rs` - Working with pre-defined symbols 142 | - `01_info_types.rs` - Using the Info provider for market data 143 | - `02_info_provider.rs` - Advanced Info provider usage 144 | - `03_exchange_provider.rs` - Placing and managing orders 145 | - `04_websocket.rs` - Real-time WebSocket subscriptions 146 | - `05_builder_orders.rs` - Using MEV builders for orders 147 | - `06_basis_trade.rs` - Example basis trading strategy 148 | - `07_managed_websocket.rs` - WebSocket with auto-reconnect and keep-alive 149 | 150 | Run examples with: 151 | ```bash 152 | cargo run --example 01_info_types 153 | ``` 154 | 155 | ## Architecture 156 | 157 | Ferrofluid follows a modular architecture: 158 | 159 | ``` 160 | ferrofluid/ 161 | ├── providers/ 162 | │ ├── info.rs // Read-only market data (HTTP) 163 | │ ├── exchange.rs // Trading operations (HTTP, requires signer) 164 | │ └── websocket.rs // Real-time subscriptions 165 | ├── types/ 166 | │ ├── actions.rs // EIP-712 signable actions 167 | │ ├── requests.rs // Order, Cancel, Modify structs 168 | │ ├── responses.rs // API response types 169 | │ └── ws.rs // WebSocket message types 170 | └── signers/ 171 | └── signer.rs // HyperliquidSigner trait 172 | ``` 173 | 174 | ## Performance 175 | 176 | Ferrofluid is designed for maximum performance: 177 | 178 | - **JSON Parsing**: Uses `simd-json` for vectorized parsing 179 | - **HTTP Client**: Built on `hyper` + `tower` for connection pooling 180 | - **WebSocket**: Uses `fastwebsockets` for minimal overhead 181 | - **Zero-Copy**: Minimizes allocations where possible 182 | 183 | ## Builder Support 184 | 185 | Native support for MEV builders with configurable fees: 186 | 187 | ```rust 188 | let exchange = ExchangeProvider::mainnet_builder(signer, builder_address); 189 | 190 | // All orders automatically include builder info 191 | let order = exchange.order(0) 192 | .limit_buy("50000", "0.001") 193 | .send() 194 | .await?; 195 | 196 | // Or specify custom builder fee 197 | let result = exchange.place_order_with_builder_fee(&order_request, 10).await?; 198 | ``` 199 | 200 | ## Rate Limiting 201 | 202 | Built-in rate limiter respects Hyperliquid's limits: 203 | 204 | ```rust 205 | // Rate limiting is automatic 206 | let result = info.l2_book("BTC").await?; // Uses 1 weight 207 | let fills = info.user_fills(address).await?; // Uses 2 weight 208 | ``` 209 | 210 | ## Error Handling 211 | 212 | Comprehensive error types with `thiserror`: 213 | 214 | ```rust 215 | match exchange.place_order(&order).await { 216 | Ok(status) => println!("Success: {:?}", status), 217 | Err(HyperliquidError::RateLimited { available, required }) => { 218 | println!("Rate limited: need {} but only {} available", required, available); 219 | } 220 | Err(e) => println!("Error: {}", e), 221 | } 222 | ``` 223 | 224 | ## Testing 225 | 226 | Run the test suite: 227 | 228 | ```bash 229 | cargo test 230 | ``` 231 | 232 | Integration tests against testnet: 233 | ```bash 234 | cargo test --features testnet 235 | ``` 236 | 237 | ## Contributing 238 | 239 | Contributions are welcome! Please feel free to submit a Pull Request. 240 | 241 | ## License 242 | 243 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 244 | 245 | ## Acknowledgments 246 | 247 | Built with high-performance crates from the Rust ecosystem: 248 | - [alloy-rs](https://github.com/alloy-rs/alloy) for Ethereum primitives 249 | - [hyperliquid-rust-sdk](https://github.com/hyperliquid-dex/hyperliquid-rust-sdk/tree/master) 250 | - [hyper](https://hyper.rs/) for HTTP 251 | - [fastwebsockets](https://github.com/littledivy/fastwebsockets) for WebSocket 252 | - [simd-json](https://github.com/simd-lite/simd-json) for JSON parsing 253 | 254 | --- 255 | -------------------------------------------------------------------------------- /src/types/ws.rs: -------------------------------------------------------------------------------- 1 | //! WebSocket message types for Hyperliquid 2 | 3 | use std::collections::HashMap; 4 | 5 | use alloy::primitives::Address; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | // Subscription types 9 | #[derive(Debug, Clone, Serialize, Deserialize)] 10 | #[serde(tag = "type", rename_all = "camelCase")] 11 | pub enum Subscription { 12 | AllMids, 13 | Notification { user: Address }, 14 | WebData2 { user: Address }, 15 | Candle { coin: String, interval: String }, 16 | L2Book { coin: String }, 17 | Trades { coin: String }, 18 | OrderUpdates { user: Address }, 19 | UserEvents { user: Address }, 20 | UserFills { user: Address }, 21 | UserFundings { user: Address }, 22 | UserNonFundingLedgerUpdates { user: Address }, 23 | } 24 | 25 | // Incoming message types 26 | #[derive(Debug, Clone, Deserialize)] 27 | #[serde(tag = "channel", rename_all = "camelCase")] 28 | pub enum Message { 29 | AllMids(AllMids), 30 | Trades(Trades), 31 | L2Book(L2Book), 32 | Candle(Candle), 33 | OrderUpdates(OrderUpdates), 34 | UserFills(UserFills), 35 | UserFundings(UserFundings), 36 | UserNonFundingLedgerUpdates(UserNonFundingLedgerUpdates), 37 | Notification(Notification), 38 | WebData2(WebData2), 39 | User(User), 40 | SubscriptionResponse, 41 | Pong, 42 | } 43 | 44 | // Market data structures 45 | #[derive(Debug, Clone, Deserialize)] 46 | pub struct AllMids { 47 | pub data: AllMidsData, 48 | } 49 | 50 | #[derive(Debug, Clone, Deserialize)] 51 | pub struct AllMidsData { 52 | pub mids: HashMap, 53 | } 54 | 55 | #[derive(Debug, Clone, Deserialize)] 56 | pub struct Trades { 57 | pub data: Vec, 58 | } 59 | 60 | #[derive(Debug, Clone, Deserialize)] 61 | pub struct Trade { 62 | pub coin: String, 63 | pub side: String, 64 | pub px: String, 65 | pub sz: String, 66 | pub time: u64, 67 | pub hash: String, 68 | pub tid: u64, 69 | } 70 | 71 | #[derive(Debug, Clone, Deserialize)] 72 | pub struct L2Book { 73 | pub data: L2BookData, 74 | } 75 | 76 | #[derive(Debug, Clone, Deserialize)] 77 | pub struct L2BookData { 78 | pub coin: String, 79 | pub time: u64, 80 | pub levels: Vec>, 81 | } 82 | 83 | #[derive(Debug, Clone, Deserialize)] 84 | pub struct BookLevel { 85 | pub px: String, 86 | pub sz: String, 87 | pub n: u64, 88 | } 89 | 90 | #[derive(Debug, Clone, Deserialize)] 91 | pub struct Candle { 92 | pub data: CandleData, 93 | } 94 | 95 | #[derive(Debug, Clone, Deserialize)] 96 | pub struct CandleData { 97 | #[serde(rename = "T")] 98 | pub time_close: u64, 99 | #[serde(rename = "c")] 100 | pub close: String, 101 | #[serde(rename = "h")] 102 | pub high: String, 103 | #[serde(rename = "i")] 104 | pub interval: String, 105 | #[serde(rename = "l")] 106 | pub low: String, 107 | #[serde(rename = "n")] 108 | pub num_trades: u64, 109 | #[serde(rename = "o")] 110 | pub open: String, 111 | #[serde(rename = "s")] 112 | pub coin: String, 113 | #[serde(rename = "t")] 114 | pub time_open: u64, 115 | #[serde(rename = "v")] 116 | pub volume: String, 117 | } 118 | 119 | // User event structures 120 | #[derive(Debug, Clone, Deserialize)] 121 | pub struct OrderUpdates { 122 | pub data: Vec, 123 | } 124 | 125 | #[derive(Debug, Clone, Deserialize)] 126 | #[serde(rename_all = "camelCase")] 127 | pub struct OrderUpdate { 128 | pub order: BasicOrder, 129 | pub status: String, 130 | pub status_timestamp: u64, 131 | } 132 | 133 | #[derive(Debug, Clone, Deserialize)] 134 | #[serde(rename_all = "camelCase")] 135 | pub struct BasicOrder { 136 | pub coin: String, 137 | pub side: String, 138 | pub limit_px: String, 139 | pub sz: String, 140 | pub oid: u64, 141 | pub timestamp: u64, 142 | pub orig_sz: String, 143 | pub cloid: Option, 144 | } 145 | 146 | #[derive(Debug, Clone, Deserialize)] 147 | pub struct UserFills { 148 | pub data: UserFillsData, 149 | } 150 | 151 | #[derive(Debug, Clone, Deserialize)] 152 | #[serde(rename_all = "camelCase")] 153 | pub struct UserFillsData { 154 | pub is_snapshot: Option, 155 | pub user: Address, 156 | pub fills: Vec, 157 | } 158 | 159 | #[derive(Debug, Clone, Deserialize)] 160 | #[serde(rename_all = "camelCase")] 161 | pub struct TradeInfo { 162 | pub coin: String, 163 | pub side: String, 164 | pub px: String, 165 | pub sz: String, 166 | pub time: u64, 167 | pub hash: String, 168 | pub start_position: String, 169 | pub dir: String, 170 | pub closed_pnl: String, 171 | pub oid: u64, 172 | pub cloid: Option, 173 | pub crossed: bool, 174 | pub fee: String, 175 | pub fee_token: String, 176 | pub tid: u64, 177 | } 178 | 179 | #[derive(Debug, Clone, Deserialize)] 180 | pub struct UserFundings { 181 | pub data: UserFundingsData, 182 | } 183 | 184 | #[derive(Debug, Clone, Deserialize)] 185 | #[serde(rename_all = "camelCase")] 186 | pub struct UserFundingsData { 187 | pub is_snapshot: Option, 188 | pub user: Address, 189 | pub fundings: Vec, 190 | } 191 | 192 | #[derive(Debug, Clone, Deserialize)] 193 | #[serde(rename_all = "camelCase")] 194 | pub struct UserFunding { 195 | pub time: u64, 196 | pub coin: String, 197 | pub usdc: String, 198 | pub szi: String, 199 | pub funding_rate: String, 200 | } 201 | 202 | #[derive(Debug, Clone, Deserialize)] 203 | pub struct UserNonFundingLedgerUpdates { 204 | pub data: UserNonFundingLedgerUpdatesData, 205 | } 206 | 207 | #[derive(Debug, Clone, Deserialize)] 208 | #[serde(rename_all = "camelCase")] 209 | pub struct UserNonFundingLedgerUpdatesData { 210 | pub is_snapshot: Option, 211 | pub user: Address, 212 | pub non_funding_ledger_updates: Vec, 213 | } 214 | 215 | #[derive(Debug, Clone, Deserialize)] 216 | pub struct LedgerUpdateData { 217 | pub time: u64, 218 | pub hash: String, 219 | pub delta: LedgerUpdate, 220 | } 221 | 222 | #[derive(Debug, Clone, Deserialize)] 223 | #[serde(rename_all = "camelCase")] 224 | #[serde(tag = "type")] 225 | pub enum LedgerUpdate { 226 | Deposit { 227 | usdc: String, 228 | }, 229 | Withdraw { 230 | usdc: String, 231 | nonce: u64, 232 | fee: String, 233 | }, 234 | InternalTransfer { 235 | usdc: String, 236 | user: Address, 237 | destination: Address, 238 | fee: String, 239 | }, 240 | SubAccountTransfer { 241 | usdc: String, 242 | user: Address, 243 | destination: Address, 244 | }, 245 | SpotTransfer { 246 | token: String, 247 | amount: String, 248 | user: Address, 249 | destination: Address, 250 | fee: String, 251 | }, 252 | } 253 | 254 | #[derive(Debug, Clone, Deserialize)] 255 | pub struct Notification { 256 | pub data: NotificationData, 257 | } 258 | 259 | #[derive(Debug, Clone, Deserialize)] 260 | pub struct NotificationData { 261 | pub notification: String, 262 | } 263 | 264 | #[derive(Debug, Clone, Deserialize)] 265 | pub struct WebData2 { 266 | pub data: WebData2Data, 267 | } 268 | 269 | #[derive(Debug, Clone, Deserialize)] 270 | #[serde(rename_all = "camelCase")] 271 | pub struct WebData2Data { 272 | pub user: Address, 273 | } 274 | 275 | #[derive(Debug, Clone, Deserialize)] 276 | pub struct User { 277 | pub data: UserData, 278 | } 279 | 280 | #[derive(Debug, Clone, Deserialize)] 281 | #[serde(rename_all = "camelCase")] 282 | #[serde(untagged)] 283 | pub enum UserData { 284 | Fills(Vec), 285 | Funding(UserFunding), 286 | } 287 | 288 | // WebSocket protocol messages 289 | #[derive(Debug, Serialize)] 290 | pub struct WsRequest { 291 | pub method: &'static str, 292 | #[serde(skip_serializing_if = "Option::is_none")] 293 | pub subscription: Option, 294 | } 295 | 296 | impl WsRequest { 297 | pub fn subscribe(subscription: Subscription) -> Self { 298 | Self { 299 | method: "subscribe", 300 | subscription: Some(subscription), 301 | } 302 | } 303 | 304 | pub fn unsubscribe(subscription: Subscription) -> Self { 305 | Self { 306 | method: "unsubscribe", 307 | subscription: Some(subscription), 308 | } 309 | } 310 | 311 | pub fn ping() -> Self { 312 | Self { 313 | method: "ping", 314 | subscription: None, 315 | } 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /src/providers/batcher.rs: -------------------------------------------------------------------------------- 1 | //! Order batching for high-frequency trading strategies 2 | 3 | use std::sync::Arc; 4 | use std::time::Duration; 5 | use std::pin::Pin; 6 | use std::future::Future; 7 | use tokio::sync::{Mutex, mpsc}; 8 | use tokio::time::interval; 9 | use uuid::Uuid; 10 | use crate::types::requests::{OrderRequest, CancelRequest}; 11 | use crate::types::responses::ExchangeResponseStatus; 12 | use crate::errors::HyperliquidError; 13 | 14 | type BoxFuture = Pin + Send + 'static>>; 15 | 16 | /// Order with metadata for batching 17 | #[derive(Clone)] 18 | pub struct PendingOrder { 19 | pub order: OrderRequest, 20 | pub nonce: u64, 21 | pub id: Uuid, 22 | pub response_tx: mpsc::UnboundedSender>, 23 | } 24 | 25 | /// Cancel with metadata for batching 26 | #[derive(Clone)] 27 | pub struct PendingCancel { 28 | pub cancel: CancelRequest, 29 | pub nonce: u64, 30 | pub id: Uuid, 31 | pub response_tx: mpsc::UnboundedSender>, 32 | } 33 | 34 | /// Order type classification for priority batching 35 | #[derive(Debug, Clone, PartialEq)] 36 | pub enum OrderPriority { 37 | /// Add Liquidity Only orders (highest priority per docs) 38 | ALO, 39 | /// Regular GTC/IOC orders 40 | Regular, 41 | } 42 | 43 | /// Handle returned when submitting to batcher 44 | pub enum OrderHandle { 45 | /// Order submitted to batch, will be sent soon 46 | Pending { 47 | id: Uuid, 48 | rx: mpsc::UnboundedReceiver>, 49 | }, 50 | /// Order executed immediately (when batching disabled) 51 | Immediate(Result), 52 | } 53 | 54 | /// Configuration for order batching 55 | #[derive(Clone, Debug)] 56 | pub struct BatchConfig { 57 | /// Interval between batch submissions 58 | pub interval: Duration, 59 | /// Maximum orders per batch 60 | pub max_batch_size: usize, 61 | /// Separate ALO orders into priority batches 62 | pub prioritize_alo: bool, 63 | /// Maximum time an order can wait in queue 64 | pub max_wait_time: Duration, 65 | } 66 | 67 | impl Default for BatchConfig { 68 | fn default() -> Self { 69 | Self { 70 | interval: Duration::from_millis(100), // 0.1s as recommended 71 | max_batch_size: 100, 72 | prioritize_alo: true, 73 | max_wait_time: Duration::from_millis(500), 74 | } 75 | } 76 | } 77 | 78 | /// Batches orders for efficient submission 79 | pub struct OrderBatcher { 80 | /// Pending orders queue 81 | pending_orders: Arc>>, 82 | /// Pending cancels queue 83 | pending_cancels: Arc>>, 84 | /// Configuration 85 | _config: BatchConfig, 86 | /// Shutdown signal 87 | shutdown_tx: mpsc::Sender<()>, 88 | } 89 | 90 | impl OrderBatcher { 91 | /// Create a new order batcher 92 | pub fn new(config: BatchConfig) -> (Self, BatcherHandle) { 93 | let (shutdown_tx, shutdown_rx) = mpsc::channel(1); 94 | 95 | let batcher = Self { 96 | pending_orders: Arc::new(Mutex::new(Vec::new())), 97 | pending_cancels: Arc::new(Mutex::new(Vec::new())), 98 | _config: config, 99 | shutdown_tx, 100 | }; 101 | 102 | let handle = BatcherHandle { 103 | pending_orders: batcher.pending_orders.clone(), 104 | pending_cancels: batcher.pending_cancels.clone(), 105 | shutdown_rx, 106 | }; 107 | 108 | (batcher, handle) 109 | } 110 | 111 | /// Add an order to the batch queue 112 | pub async fn add_order(&self, order: OrderRequest, nonce: u64) -> OrderHandle { 113 | let id = Uuid::new_v4(); 114 | let (tx, rx) = mpsc::unbounded_channel(); 115 | 116 | let pending = PendingOrder { 117 | order, 118 | nonce, 119 | id, 120 | response_tx: tx, 121 | }; 122 | 123 | self.pending_orders.lock().await.push(pending); 124 | 125 | OrderHandle::Pending { id, rx } 126 | } 127 | 128 | /// Add a cancel to the batch queue 129 | pub async fn add_cancel(&self, cancel: CancelRequest, nonce: u64) -> OrderHandle { 130 | let id = Uuid::new_v4(); 131 | let (tx, rx) = mpsc::unbounded_channel(); 132 | 133 | let pending = PendingCancel { 134 | cancel, 135 | nonce, 136 | id, 137 | response_tx: tx, 138 | }; 139 | 140 | self.pending_cancels.lock().await.push(pending); 141 | 142 | OrderHandle::Pending { id, rx } 143 | } 144 | 145 | /// Shutdown the batcher 146 | pub async fn shutdown(self) { 147 | let _ = self.shutdown_tx.send(()).await; 148 | } 149 | } 150 | 151 | /// Handle for the background batching task 152 | pub struct BatcherHandle { 153 | pending_orders: Arc>>, 154 | pending_cancels: Arc>>, 155 | shutdown_rx: mpsc::Receiver<()>, 156 | } 157 | 158 | impl BatcherHandle { 159 | /// Run the batching loop (should be spawned as a task) 160 | pub async fn run( 161 | mut self, 162 | mut order_executor: F, 163 | mut cancel_executor: G, 164 | ) 165 | where 166 | F: FnMut(Vec) -> BoxFuture>> + Send, 167 | G: FnMut(Vec) -> BoxFuture>> + Send, 168 | { 169 | let mut interval = interval(Duration::from_millis(100)); // Fixed interval for now 170 | 171 | loop { 172 | tokio::select! { 173 | _ = interval.tick() => { 174 | // Process orders 175 | let orders = { 176 | let mut pending = self.pending_orders.lock().await; 177 | std::mem::take(&mut *pending) 178 | }; 179 | 180 | if !orders.is_empty() { 181 | // Separate ALO from regular orders 182 | let (alo_orders, regular_orders): (Vec<_>, Vec<_>) = 183 | orders.into_iter().partition(|o| { 184 | o.order.is_alo() 185 | }); 186 | 187 | // Process ALO orders first (priority) 188 | if !alo_orders.is_empty() { 189 | let results = order_executor(alo_orders.clone()).await; 190 | for (order, result) in alo_orders.into_iter().zip(results) { 191 | let _ = order.response_tx.send(result); 192 | } 193 | } 194 | 195 | // Process regular orders 196 | if !regular_orders.is_empty() { 197 | let results = order_executor(regular_orders.clone()).await; 198 | for (order, result) in regular_orders.into_iter().zip(results) { 199 | let _ = order.response_tx.send(result); 200 | } 201 | } 202 | } 203 | 204 | // Process cancels 205 | let cancels = { 206 | let mut pending = self.pending_cancels.lock().await; 207 | std::mem::take(&mut *pending) 208 | }; 209 | 210 | if !cancels.is_empty() { 211 | let results = cancel_executor(cancels.clone()).await; 212 | for (cancel, result) in cancels.into_iter().zip(results) { 213 | let _ = cancel.response_tx.send(result); 214 | } 215 | } 216 | } 217 | 218 | _ = self.shutdown_rx.recv() => { 219 | // Graceful shutdown 220 | break; 221 | } 222 | } 223 | } 224 | } 225 | } 226 | 227 | impl OrderRequest { 228 | /// Check if this is an ALO order 229 | pub fn is_alo(&self) -> bool { 230 | match &self.order_type { 231 | crate::types::requests::OrderType::Limit(limit) => { 232 | limit.tif.to_lowercase() == "alo" 233 | } 234 | _ => false, 235 | } 236 | } 237 | } 238 | 239 | #[cfg(test)] 240 | mod tests { 241 | use super::*; 242 | use crate::types::requests::{OrderType, Limit}; 243 | 244 | #[tokio::test] 245 | async fn test_order_batching() { 246 | let config = BatchConfig::default(); 247 | let (batcher, _handle) = OrderBatcher::new(config); 248 | 249 | // Create a test order 250 | let order = OrderRequest { 251 | asset: 0, 252 | is_buy: true, 253 | limit_px: "50000".to_string(), 254 | sz: "0.1".to_string(), 255 | reduce_only: false, 256 | order_type: OrderType::Limit(Limit { tif: "Gtc".to_string() }), 257 | cloid: None, 258 | }; 259 | 260 | // Add to batch 261 | let handle = batcher.add_order(order, 123456789).await; 262 | 263 | // Should return pending handle 264 | assert!(matches!(handle, OrderHandle::Pending { .. })); 265 | } 266 | } -------------------------------------------------------------------------------- /src/types/actions.rs: -------------------------------------------------------------------------------- 1 | use alloy::primitives::B256; 2 | use serde; 3 | 4 | use crate::types::requests::{ 5 | BuilderInfo, CancelRequest, CancelRequestCloid, ModifyRequest, OrderRequest, 6 | }; 7 | use crate::l1_action; 8 | 9 | // User Actions (with HyperliquidTransaction: prefix) 10 | 11 | // UsdSend needs custom serialization for signature_chain_id 12 | #[derive(Debug, Clone, serde::Serialize)] 13 | #[serde(rename_all = "camelCase")] 14 | pub struct UsdSend { 15 | #[serde(serialize_with = "serialize_chain_id")] 16 | pub signature_chain_id: u64, 17 | pub hyperliquid_chain: String, 18 | pub destination: String, 19 | pub amount: String, 20 | pub time: u64, 21 | } 22 | 23 | impl crate::types::eip712::HyperliquidAction for UsdSend { 24 | const TYPE_STRING: &'static str = "UsdSend(string hyperliquidChain,string destination,string amount,uint64 time)"; 25 | const USE_PREFIX: bool = true; 26 | 27 | fn chain_id(&self) -> Option { 28 | Some(self.signature_chain_id) 29 | } 30 | 31 | fn encode_data(&self) -> Vec { 32 | use crate::types::eip712::encode_value; 33 | let mut encoded = Vec::new(); 34 | encoded.extend_from_slice(&Self::type_hash()[..]); 35 | encoded.extend_from_slice(&encode_value(&self.hyperliquid_chain)[..]); 36 | encoded.extend_from_slice(&encode_value(&self.destination)[..]); 37 | encoded.extend_from_slice(&encode_value(&self.amount)[..]); 38 | encoded.extend_from_slice(&encode_value(&self.time)[..]); 39 | encoded 40 | } 41 | } 42 | 43 | // Withdraw needs custom serialization for signature_chain_id 44 | #[derive(Debug, Clone, serde::Serialize)] 45 | #[serde(rename_all = "camelCase")] 46 | pub struct Withdraw { 47 | #[serde(serialize_with = "serialize_chain_id")] 48 | pub signature_chain_id: u64, 49 | pub hyperliquid_chain: String, 50 | pub destination: String, 51 | pub amount: String, 52 | pub time: u64, 53 | } 54 | 55 | impl crate::types::eip712::HyperliquidAction for Withdraw { 56 | const TYPE_STRING: &'static str = "Withdraw(string hyperliquidChain,string destination,string amount,uint64 time)"; 57 | const USE_PREFIX: bool = true; 58 | 59 | fn chain_id(&self) -> Option { 60 | Some(self.signature_chain_id) 61 | } 62 | 63 | fn encode_data(&self) -> Vec { 64 | use crate::types::eip712::encode_value; 65 | let mut encoded = Vec::new(); 66 | encoded.extend_from_slice(&Self::type_hash()[..]); 67 | encoded.extend_from_slice(&encode_value(&self.hyperliquid_chain)[..]); 68 | encoded.extend_from_slice(&encode_value(&self.destination)[..]); 69 | encoded.extend_from_slice(&encode_value(&self.amount)[..]); 70 | encoded.extend_from_slice(&encode_value(&self.time)[..]); 71 | encoded 72 | } 73 | } 74 | 75 | // SpotSend needs custom serialization for signature_chain_id 76 | #[derive(Debug, Clone, serde::Serialize)] 77 | #[serde(rename_all = "camelCase")] 78 | pub struct SpotSend { 79 | #[serde(serialize_with = "serialize_chain_id")] 80 | pub signature_chain_id: u64, 81 | pub hyperliquid_chain: String, 82 | pub destination: String, 83 | pub token: String, 84 | pub amount: String, 85 | pub time: u64, 86 | } 87 | 88 | impl crate::types::eip712::HyperliquidAction for SpotSend { 89 | const TYPE_STRING: &'static str = "SpotSend(string hyperliquidChain,string destination,string token,string amount,uint64 time)"; 90 | const USE_PREFIX: bool = true; 91 | 92 | fn chain_id(&self) -> Option { 93 | Some(self.signature_chain_id) 94 | } 95 | 96 | fn encode_data(&self) -> Vec { 97 | use crate::types::eip712::encode_value; 98 | let mut encoded = Vec::new(); 99 | encoded.extend_from_slice(&Self::type_hash()[..]); 100 | encoded.extend_from_slice(&encode_value(&self.hyperliquid_chain)[..]); 101 | encoded.extend_from_slice(&encode_value(&self.destination)[..]); 102 | encoded.extend_from_slice(&encode_value(&self.token)[..]); 103 | encoded.extend_from_slice(&encode_value(&self.amount)[..]); 104 | encoded.extend_from_slice(&encode_value(&self.time)[..]); 105 | encoded 106 | } 107 | } 108 | 109 | // ApproveAgent needs custom serialization for the address field 110 | #[derive(Debug, Clone, serde::Serialize)] 111 | #[serde(rename_all = "camelCase")] 112 | pub struct ApproveAgent { 113 | #[serde(serialize_with = "serialize_chain_id")] 114 | pub signature_chain_id: u64, 115 | pub hyperliquid_chain: String, 116 | #[serde(serialize_with = "serialize_address")] 117 | pub agent_address: alloy::primitives::Address, 118 | pub agent_name: Option, 119 | pub nonce: u64, 120 | } 121 | 122 | pub(crate) fn serialize_address(address: &alloy::primitives::Address, serializer: S) -> Result 123 | where 124 | S: serde::Serializer, 125 | { 126 | serializer.serialize_str(&format!("{:#x}", address)) 127 | } 128 | 129 | pub(crate) fn serialize_chain_id(chain_id: &u64, serializer: S) -> Result 130 | where 131 | S: serde::Serializer, 132 | { 133 | // Serialize as hex string to match SDK format 134 | serializer.serialize_str(&format!("{:#x}", chain_id)) 135 | } 136 | 137 | impl crate::types::eip712::HyperliquidAction for ApproveAgent { 138 | const TYPE_STRING: &'static str = "ApproveAgent(string hyperliquidChain,address agentAddress,string agentName,uint64 nonce)"; 139 | const USE_PREFIX: bool = true; 140 | 141 | fn chain_id(&self) -> Option { 142 | Some(self.signature_chain_id) 143 | } 144 | 145 | fn encode_data(&self) -> Vec { 146 | use crate::types::eip712::encode_value; 147 | let mut encoded = Vec::new(); 148 | encoded.extend_from_slice(&Self::type_hash()[..]); 149 | encoded.extend_from_slice(&encode_value(&self.hyperliquid_chain)[..]); 150 | encoded.extend_from_slice(&encode_value(&self.agent_address)[..]); 151 | // SDK uses unwrap_or_default() for agent_name 152 | let agent_name = self.agent_name.clone().unwrap_or_default(); 153 | encoded.extend_from_slice(&encode_value(&agent_name)[..]); 154 | encoded.extend_from_slice(&encode_value(&self.nonce)[..]); 155 | encoded 156 | } 157 | } 158 | 159 | // ApproveBuilderFee needs custom serialization for signature_chain_id 160 | #[derive(Debug, Clone, serde::Serialize)] 161 | #[serde(rename_all = "camelCase")] 162 | pub struct ApproveBuilderFee { 163 | #[serde(serialize_with = "serialize_chain_id")] 164 | pub signature_chain_id: u64, 165 | pub hyperliquid_chain: String, 166 | pub max_fee_rate: String, 167 | pub builder: String, 168 | pub nonce: u64, 169 | } 170 | 171 | impl crate::types::eip712::HyperliquidAction for ApproveBuilderFee { 172 | const TYPE_STRING: &'static str = "ApproveBuilderFee(string hyperliquidChain,string maxFeeRate,string builder,uint64 nonce)"; 173 | const USE_PREFIX: bool = true; 174 | 175 | fn chain_id(&self) -> Option { 176 | Some(self.signature_chain_id) 177 | } 178 | 179 | fn encode_data(&self) -> Vec { 180 | use crate::types::eip712::encode_value; 181 | let mut encoded = Vec::new(); 182 | encoded.extend_from_slice(&Self::type_hash()[..]); 183 | encoded.extend_from_slice(&encode_value(&self.hyperliquid_chain)[..]); 184 | encoded.extend_from_slice(&encode_value(&self.max_fee_rate)[..]); 185 | encoded.extend_from_slice(&encode_value(&self.builder)[..]); 186 | encoded.extend_from_slice(&encode_value(&self.nonce)[..]); 187 | encoded 188 | } 189 | } 190 | 191 | // L1 Actions (use Exchange domain) 192 | 193 | l1_action! { 194 | /// Agent connection action 195 | struct Agent { 196 | pub source: String, 197 | pub connection_id: B256, 198 | } 199 | => "Agent(string source,bytes32 connectionId)" 200 | => encode(source, connection_id) 201 | } 202 | 203 | // Exchange Actions (these don't need EIP-712 signing but are included for completeness) 204 | 205 | #[derive(Debug, Clone, serde::Serialize)] 206 | #[serde(rename_all = "camelCase")] 207 | pub struct UpdateLeverage { 208 | pub asset: u32, 209 | pub is_cross: bool, 210 | pub leverage: u32, 211 | } 212 | 213 | #[derive(Debug, Clone, serde::Serialize)] 214 | #[serde(rename_all = "camelCase")] 215 | pub struct UpdateIsolatedMargin { 216 | pub asset: u32, 217 | pub is_buy: bool, 218 | pub ntli: i64, 219 | } 220 | 221 | #[derive(Debug, Clone, serde::Serialize)] 222 | #[serde(rename_all = "camelCase")] 223 | pub struct VaultTransfer { 224 | pub vault_address: String, 225 | pub is_deposit: bool, 226 | pub usd: u64, 227 | } 228 | 229 | #[derive(Debug, Clone, serde::Serialize)] 230 | #[serde(rename_all = "camelCase")] 231 | pub struct SpotUser { 232 | pub class_transfer: ClassTransfer, 233 | } 234 | 235 | #[derive(Debug, Clone, serde::Serialize)] 236 | #[serde(rename_all = "camelCase")] 237 | pub struct ClassTransfer { 238 | pub usd_size: u64, 239 | pub to_perp: bool, 240 | } 241 | 242 | #[derive(Debug, Clone, serde::Serialize)] 243 | #[serde(rename_all = "camelCase")] 244 | pub struct SetReferrer { 245 | pub code: String, 246 | } 247 | 248 | // Bulk actions that contain other types 249 | 250 | #[derive(Debug, Clone, serde::Serialize)] 251 | #[serde(rename_all = "camelCase")] 252 | pub struct BulkOrder { 253 | pub orders: Vec, 254 | pub grouping: String, 255 | #[serde(default, skip_serializing_if = "Option::is_none")] 256 | pub builder: Option, 257 | } 258 | 259 | #[derive(Debug, Clone, serde::Serialize)] 260 | #[serde(rename_all = "camelCase")] 261 | pub struct BulkCancel { 262 | pub cancels: Vec, 263 | } 264 | 265 | #[derive(Debug, Clone, serde::Serialize)] 266 | #[serde(rename_all = "camelCase")] 267 | pub struct BulkModify { 268 | pub modifies: Vec, 269 | } 270 | 271 | #[derive(Debug, Clone, serde::Serialize)] 272 | #[serde(rename_all = "camelCase")] 273 | pub struct BulkCancelCloid { 274 | pub cancels: Vec, 275 | } 276 | 277 | // Types are now imported from requests.rs 278 | 279 | // The macros don't handle signature_chain_id, so we need to remove the duplicate trait impls 280 | 281 | #[cfg(test)] 282 | mod tests { 283 | use alloy::primitives::keccak256; 284 | 285 | use super::*; 286 | use crate::types::eip712::HyperliquidAction; 287 | 288 | #[test] 289 | fn test_usd_send_type_hash() { 290 | let expected = keccak256( 291 | "HyperliquidTransaction:UsdSend(string hyperliquidChain,string destination,string amount,uint64 time)", 292 | ); 293 | assert_eq!(UsdSend::type_hash(), expected); 294 | } 295 | 296 | #[test] 297 | fn test_agent_type_hash() { 298 | // L1 actions don't use the HyperliquidTransaction: prefix 299 | let expected = keccak256("Agent(string source,bytes32 connectionId)"); 300 | assert_eq!(Agent::type_hash(), expected); 301 | } 302 | 303 | #[test] 304 | fn test_agent_domain() { 305 | let agent = Agent { 306 | source: "a".to_string(), 307 | connection_id: B256::default(), 308 | }; 309 | 310 | // L1 actions use the Exchange domain 311 | let domain = agent.domain(); 312 | let expected_domain = alloy::sol_types::eip712_domain! { 313 | name: "Exchange", 314 | version: "1", 315 | chain_id: 1337u64, 316 | verifying_contract: alloy::primitives::address!("0000000000000000000000000000000000000000"), 317 | }; 318 | 319 | // Compare domain separators to verify they're the same 320 | assert_eq!(domain.separator(), expected_domain.separator()); 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /src/types/info_types.rs: -------------------------------------------------------------------------------- 1 | use alloy::primitives::Address; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | // ==================== Request Types ==================== 5 | 6 | #[derive(Serialize, Deserialize, Debug, Clone)] 7 | #[serde(rename_all = "camelCase")] 8 | pub struct CandleSnapshotRequest { 9 | pub coin: String, 10 | pub interval: String, 11 | pub start_time: u64, 12 | pub end_time: u64, 13 | } 14 | 15 | // ==================== Common Response Types ==================== 16 | 17 | // Note: AllMids returns HashMap directly, not wrapped 18 | 19 | // ==================== Position & Margin Types ==================== 20 | 21 | #[derive(Serialize, Deserialize, Debug)] 22 | pub struct AssetPosition { 23 | pub position: PositionData, 24 | #[serde(rename = "type")] 25 | pub type_string: String, 26 | } 27 | 28 | #[derive(Serialize, Deserialize, Clone, Debug)] 29 | #[serde(rename_all = "camelCase")] 30 | pub struct BasicOrderInfo { 31 | pub coin: String, 32 | pub side: String, 33 | pub limit_px: String, 34 | pub sz: String, 35 | pub oid: u64, 36 | pub timestamp: u64, 37 | pub trigger_condition: String, 38 | pub is_trigger: bool, 39 | pub trigger_px: String, 40 | pub is_position_tpsl: bool, 41 | pub reduce_only: bool, 42 | pub order_type: String, 43 | pub orig_sz: String, 44 | pub tif: String, 45 | pub cloid: Option, 46 | } 47 | 48 | #[derive(Serialize, Deserialize, Debug)] 49 | pub struct CandlesSnapshotResponse { 50 | #[serde(rename = "t")] 51 | pub time_open: u64, 52 | #[serde(rename = "T")] 53 | pub time_close: u64, 54 | #[serde(rename = "s")] 55 | pub coin: String, 56 | #[serde(rename = "i")] 57 | pub candle_interval: String, 58 | #[serde(rename = "o")] 59 | pub open: String, 60 | #[serde(rename = "c")] 61 | pub close: String, 62 | #[serde(rename = "h")] 63 | pub high: String, 64 | #[serde(rename = "l")] 65 | pub low: String, 66 | #[serde(rename = "v")] 67 | pub vlm: String, 68 | #[serde(rename = "n")] 69 | pub num_trades: u64, 70 | } 71 | 72 | #[derive(Serialize, Deserialize, Debug, Clone)] 73 | #[serde(rename_all = "camelCase")] 74 | pub struct CumulativeFunding { 75 | pub all_time: String, 76 | pub since_open: String, 77 | pub since_change: String, 78 | } 79 | 80 | #[derive(Serialize, Deserialize, Debug)] 81 | #[serde(rename_all = "camelCase")] 82 | pub struct DailyUserVlm { 83 | pub date: String, 84 | pub exchange: String, 85 | pub user_add: String, 86 | pub user_cross: String, 87 | } 88 | 89 | #[derive(Serialize, Deserialize, Debug, Clone)] 90 | #[serde(rename_all = "camelCase")] 91 | pub struct Delta { 92 | #[serde(rename = "type")] 93 | pub type_string: String, 94 | pub coin: String, 95 | pub usdc: String, 96 | pub szi: String, 97 | pub funding_rate: String, 98 | } 99 | 100 | #[derive(Serialize, Deserialize, Debug)] 101 | #[serde(rename_all = "camelCase")] 102 | pub struct FeeSchedule { 103 | pub add: String, 104 | pub cross: String, 105 | pub referral_discount: String, 106 | pub tiers: Tiers, 107 | } 108 | 109 | #[derive(Serialize, Deserialize, Debug)] 110 | #[serde(rename_all = "camelCase")] 111 | pub struct FundingHistoryResponse { 112 | pub coin: String, 113 | pub funding_rate: String, 114 | pub premium: String, 115 | pub time: u64, 116 | } 117 | 118 | #[derive(Serialize, Deserialize, Debug)] 119 | #[serde(rename_all = "camelCase")] 120 | pub struct L2SnapshotResponse { 121 | pub coin: String, 122 | pub levels: Vec>, 123 | pub time: u64, 124 | } 125 | 126 | #[derive(Serialize, Deserialize, Debug, Clone)] 127 | #[serde(rename_all = "camelCase")] 128 | pub struct Level { 129 | pub n: u64, 130 | pub px: String, 131 | pub sz: String, 132 | } 133 | 134 | #[derive(Serialize, Deserialize, Debug, Clone)] 135 | #[serde(rename_all = "camelCase")] 136 | pub struct Leverage { 137 | #[serde(rename = "type")] 138 | pub type_string: String, 139 | pub value: u32, 140 | pub raw_usd: Option, 141 | } 142 | 143 | #[derive(Serialize, Deserialize, Debug)] 144 | #[serde(rename_all = "camelCase")] 145 | pub struct MarginSummary { 146 | pub account_value: String, 147 | pub total_margin_used: String, 148 | pub total_ntl_pos: String, 149 | pub total_raw_usd: String, 150 | } 151 | 152 | #[derive(Serialize, Deserialize, Debug)] 153 | #[serde(rename_all = "camelCase")] 154 | pub struct Mm { 155 | pub add: String, 156 | pub maker_fraction_cutoff: String, 157 | } 158 | 159 | #[derive(Serialize, Deserialize, Debug)] 160 | #[serde(rename_all = "camelCase")] 161 | pub struct OpenOrdersResponse { 162 | pub coin: String, 163 | pub limit_px: String, 164 | pub oid: u64, 165 | pub side: String, 166 | pub sz: String, 167 | pub timestamp: u64, 168 | } 169 | 170 | #[derive(Serialize, Deserialize, Clone, Debug)] 171 | #[serde(rename_all = "camelCase")] 172 | pub struct OrderInfo { 173 | pub order: BasicOrderInfo, 174 | pub status: String, 175 | pub status_timestamp: u64, 176 | } 177 | 178 | #[derive(Serialize, Deserialize, Debug)] 179 | pub struct OrderStatusResponse { 180 | pub status: String, 181 | /// `None` if the order is not found 182 | #[serde(default)] 183 | pub order: Option, 184 | } 185 | 186 | #[derive(Serialize, Deserialize, Debug)] 187 | #[serde(rename_all = "camelCase")] 188 | pub struct PositionData { 189 | pub coin: String, 190 | pub entry_px: Option, 191 | pub leverage: Leverage, 192 | pub liquidation_px: Option, 193 | pub margin_used: String, 194 | pub position_value: String, 195 | pub return_on_equity: String, 196 | pub szi: String, 197 | pub unrealized_pnl: String, 198 | pub max_leverage: u32, 199 | pub cum_funding: CumulativeFunding, 200 | } 201 | 202 | #[derive(Serialize, Deserialize, Debug)] 203 | #[serde(rename_all = "camelCase")] 204 | pub struct RecentTradesResponse { 205 | pub coin: String, 206 | pub side: String, 207 | pub px: String, 208 | pub sz: String, 209 | pub time: u64, 210 | pub hash: String, 211 | } 212 | 213 | #[derive(Serialize, Deserialize, Debug)] 214 | #[serde(rename_all = "camelCase")] 215 | pub struct Referrer { 216 | pub referrer: Address, 217 | pub code: String, 218 | } 219 | 220 | #[derive(Serialize, Deserialize, Debug)] 221 | #[serde(rename_all = "camelCase")] 222 | pub struct ReferralResponse { 223 | pub referred_by: Option, 224 | pub cum_vlm: String, 225 | pub unclaimed_rewards: String, 226 | pub claimed_rewards: String, 227 | pub referrer_state: ReferrerState, 228 | } 229 | 230 | #[derive(Serialize, Deserialize, Debug)] 231 | #[serde(rename_all = "camelCase")] 232 | pub struct ReferrerData { 233 | pub required: String, 234 | } 235 | 236 | #[derive(Serialize, Deserialize, Debug)] 237 | #[serde(rename_all = "camelCase")] 238 | pub struct ReferrerState { 239 | pub stage: String, 240 | pub data: ReferrerData, 241 | } 242 | 243 | #[derive(Serialize, Deserialize, Debug)] 244 | pub struct Tiers { 245 | pub mm: Vec, 246 | pub vip: Vec, 247 | } 248 | 249 | #[derive(Serialize, Deserialize, Debug)] 250 | #[serde(rename_all = "camelCase")] 251 | pub struct UserFeesResponse { 252 | pub active_referral_discount: String, 253 | pub daily_user_vlm: Vec, 254 | pub fee_schedule: FeeSchedule, 255 | pub user_add_rate: String, 256 | pub user_cross_rate: String, 257 | } 258 | 259 | #[derive(Serialize, Deserialize, Debug)] 260 | #[serde(rename_all = "camelCase")] 261 | pub struct UserFillsResponse { 262 | pub closed_pnl: String, 263 | pub coin: String, 264 | pub crossed: bool, 265 | pub dir: String, 266 | pub hash: String, 267 | pub oid: u64, 268 | pub px: String, 269 | pub side: String, 270 | pub start_position: String, 271 | pub sz: String, 272 | pub time: u64, 273 | pub fee: String, 274 | } 275 | 276 | #[derive(Serialize, Deserialize, Debug)] 277 | pub struct UserFundingResponse { 278 | pub time: u64, 279 | pub hash: String, 280 | pub delta: Delta, 281 | } 282 | 283 | #[derive(Serialize, Deserialize, Debug)] 284 | #[serde(rename_all = "camelCase")] 285 | pub struct UserStateResponse { 286 | pub asset_positions: Vec, 287 | pub cross_margin_summary: MarginSummary, 288 | pub margin_summary: MarginSummary, 289 | pub withdrawable: String, 290 | } 291 | 292 | #[derive(Serialize, Deserialize, Debug)] 293 | #[serde(rename_all = "camelCase")] 294 | pub struct UserTokenBalance { 295 | pub coin: String, 296 | pub hold: String, 297 | pub total: String, 298 | pub entry_ntl: String, 299 | } 300 | 301 | #[derive(Serialize, Deserialize, Debug)] 302 | pub struct UserTokenBalanceResponse { 303 | pub balances: Vec, 304 | } 305 | 306 | #[derive(Serialize, Deserialize, Debug)] 307 | #[serde(rename_all = "camelCase")] 308 | pub struct Vip { 309 | pub add: String, 310 | pub cross: String, 311 | pub ntl_cutoff: String, 312 | } 313 | 314 | // ==================== Metadata Types ==================== 315 | 316 | #[derive(Serialize, Deserialize, Debug)] 317 | #[serde(rename_all = "camelCase")] 318 | pub struct Meta { 319 | pub universe: Vec, 320 | } 321 | 322 | #[derive(Serialize, Deserialize, Debug)] 323 | #[serde(rename_all = "camelCase")] 324 | pub struct AssetMeta { 325 | pub name: String, 326 | pub sz_decimals: u32, 327 | pub max_leverage: u32, 328 | #[serde(default)] 329 | pub only_isolated: bool, 330 | #[serde(skip_serializing_if = "Option::is_none")] 331 | pub initial_margin_ratio: Option, 332 | #[serde(skip_serializing_if = "Option::is_none")] 333 | pub maintenance_margin_ratio: Option, 334 | #[serde(skip_serializing_if = "Option::is_none")] 335 | pub margin_table_id: Option, 336 | #[serde(default, skip_serializing_if = "Option::is_none")] 337 | pub is_delisted: Option, 338 | } 339 | 340 | #[derive(Serialize, Deserialize, Debug)] 341 | #[serde(rename_all = "camelCase")] 342 | pub struct SpotMeta { 343 | pub universe: Vec, 344 | pub tokens: Vec, 345 | } 346 | 347 | #[derive(Serialize, Deserialize, Debug)] 348 | #[serde(rename_all = "camelCase")] 349 | pub struct SpotPairMeta { 350 | pub name: String, 351 | pub tokens: [u32; 2], 352 | pub index: u32, 353 | pub is_canonical: bool, 354 | } 355 | 356 | #[derive(Serialize, Deserialize, Debug)] 357 | #[serde(untagged)] 358 | pub enum EvmContract { 359 | String(String), 360 | Object { 361 | address: String, 362 | evm_extra_wei_decimals: i32, 363 | }, 364 | } 365 | 366 | #[derive(Serialize, Deserialize, Debug)] 367 | #[serde(rename_all = "camelCase")] 368 | pub struct TokenMeta { 369 | pub name: String, 370 | pub sz_decimals: u32, 371 | pub wei_decimals: u32, 372 | pub index: u32, 373 | pub token_id: String, // Actually a hex string, not Address 374 | pub is_canonical: bool, 375 | #[serde(skip_serializing_if = "Option::is_none")] 376 | pub full_name: Option, 377 | #[serde(skip_serializing_if = "Option::is_none")] 378 | pub evm_contract: Option, 379 | #[serde(skip_serializing_if = "Option::is_none")] 380 | pub deployer_trading_fee_share: Option, 381 | } 382 | 383 | #[derive(Serialize, Deserialize, Debug)] 384 | #[serde(rename_all = "camelCase")] 385 | pub struct SpotMetaAndAssetCtxs { 386 | pub universe: Vec, 387 | pub tokens: Vec, 388 | pub asset_ctxs: Vec, 389 | } 390 | 391 | #[derive(Serialize, Deserialize, Debug)] 392 | #[serde(rename_all = "camelCase")] 393 | pub struct AssetContext { 394 | pub day_ntl_vlm: String, 395 | pub funding: String, 396 | pub impact_pxs: Vec, 397 | pub mark_px: String, 398 | pub mid_px: String, 399 | pub open_interest: String, 400 | pub oracle_px: String, 401 | pub premium: String, 402 | pub prev_day_px: String, 403 | } 404 | -------------------------------------------------------------------------------- /src/providers/info.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::Mutex; 3 | use std::time::Instant; 4 | 5 | use alloy::primitives::Address; 6 | use http::{Method, Request}; 7 | use http_body_util::{BodyExt, Full}; 8 | use hyper::body::Bytes; 9 | use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; 10 | use hyper_util::client::legacy::{Client, connect::HttpConnector}; 11 | use hyper_util::rt::TokioExecutor; 12 | use serde_json::json; 13 | 14 | use crate::constants::Network; 15 | use crate::errors::HyperliquidError; 16 | use crate::types::info_types::*; 17 | use crate::types::Symbol; 18 | 19 | // Rate limiter implementation 20 | pub struct RateLimiter { 21 | tokens: Mutex, 22 | max_tokens: f64, 23 | refill_rate: f64, 24 | last_refill: Mutex, 25 | } 26 | 27 | impl RateLimiter { 28 | pub fn new(max_tokens: u32, refill_rate: u32) -> Self { 29 | Self { 30 | tokens: Mutex::new(max_tokens as f64), 31 | max_tokens: max_tokens as f64, 32 | refill_rate: refill_rate as f64, 33 | last_refill: Mutex::new(Instant::now()), 34 | } 35 | } 36 | 37 | pub fn check_weight(&self, weight: u32) -> Result<(), HyperliquidError> { 38 | let mut tokens = self.tokens.lock().unwrap(); 39 | let mut last_refill = self.last_refill.lock().unwrap(); 40 | 41 | // Refill tokens based on elapsed time 42 | let now = Instant::now(); 43 | let elapsed = now.duration_since(*last_refill).as_secs_f64(); 44 | let tokens_to_add = elapsed * self.refill_rate; 45 | 46 | *tokens = (*tokens + tokens_to_add).min(self.max_tokens); 47 | *last_refill = now; 48 | 49 | // Check if we have enough tokens 50 | if *tokens >= weight as f64 { 51 | *tokens -= weight as f64; 52 | Ok(()) 53 | } else { 54 | Err(HyperliquidError::RateLimited { 55 | available: *tokens as u32, 56 | required: weight, 57 | }) 58 | } 59 | } 60 | } 61 | 62 | pub struct InfoProvider { 63 | client: Client, Full>, 64 | endpoint: &'static str, 65 | } 66 | 67 | impl InfoProvider { 68 | pub fn mainnet() -> Self { 69 | Self::new(Network::Mainnet) 70 | } 71 | 72 | pub fn testnet() -> Self { 73 | Self::new(Network::Testnet) 74 | } 75 | 76 | pub fn new(network: Network) -> Self { 77 | // Initialize rustls crypto provider if not already set 78 | let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); 79 | 80 | let https = HttpsConnectorBuilder::new() 81 | .with_native_roots() 82 | .expect("TLS initialization failed") 83 | .https_only() 84 | .enable_http1() 85 | .build(); 86 | 87 | let client = Client::builder(TokioExecutor::new()).build(https); 88 | 89 | Self { 90 | client, 91 | endpoint: match network { 92 | Network::Mainnet => "https://api.hyperliquid.xyz/info", 93 | Network::Testnet => "https://api.hyperliquid-testnet.xyz/info", 94 | }, 95 | } 96 | } 97 | 98 | async fn request( 99 | &self, 100 | request_json: serde_json::Value, 101 | ) -> Result 102 | where 103 | T: serde::de::DeserializeOwned, 104 | { 105 | let body_string = serde_json::to_string(&request_json)?; 106 | let body_bytes = Bytes::from(body_string); 107 | 108 | let req = Request::builder() 109 | .method(Method::POST) 110 | .uri(self.endpoint) 111 | .header("Content-Type", "application/json") 112 | .body(Full::new(body_bytes))?; 113 | 114 | let res = self 115 | .client 116 | .request(req) 117 | .await 118 | .map_err(|e| HyperliquidError::Network(e.to_string()))?; 119 | let status = res.status(); 120 | 121 | let body_bytes = res 122 | .collect() 123 | .await 124 | .map_err(|e| HyperliquidError::Network(e.to_string()))? 125 | .to_bytes(); 126 | let body_str = String::from_utf8_lossy(&body_bytes); 127 | 128 | if !status.is_success() { 129 | return Err(HyperliquidError::Http { 130 | status: status.as_u16(), 131 | body: body_str.to_string(), 132 | }); 133 | } 134 | 135 | let mut body_vec = body_bytes.to_vec(); 136 | simd_json::from_slice(&mut body_vec).map_err(|e| e.into()) 137 | } 138 | 139 | // ==================== Simple Direct Methods ==================== 140 | 141 | pub async fn all_mids(&self) -> Result, HyperliquidError> { 142 | let request = json!({ 143 | "type": "allMids" 144 | }); 145 | self.request(request).await 146 | } 147 | 148 | pub async fn user_state( 149 | &self, 150 | user: Address, 151 | ) -> Result { 152 | let request = json!({ 153 | "type": "clearinghouseState", 154 | "user": user 155 | }); 156 | self.request(request).await 157 | } 158 | 159 | pub async fn l2_book( 160 | &self, 161 | coin: impl Into, 162 | ) -> Result { 163 | let symbol = coin.into(); 164 | let request = json!({ 165 | "type": "l2Book", 166 | "coin": symbol.as_str() 167 | }); 168 | self.request(request).await 169 | } 170 | 171 | pub async fn order_status( 172 | &self, 173 | user: Address, 174 | oid: u64, 175 | ) -> Result { 176 | let request = json!({ 177 | "type": "orderStatus", 178 | "user": user, 179 | "oid": oid 180 | }); 181 | self.request(request).await 182 | } 183 | 184 | pub async fn open_orders( 185 | &self, 186 | user: Address, 187 | ) -> Result, HyperliquidError> { 188 | let request = json!({ 189 | "type": "openOrders", 190 | "user": user 191 | }); 192 | self.request(request).await 193 | } 194 | 195 | pub async fn user_fills( 196 | &self, 197 | user: Address, 198 | ) -> Result, HyperliquidError> { 199 | let request = json!({ 200 | "type": "userFills", 201 | "user": user 202 | }); 203 | self.request(request).await 204 | } 205 | 206 | pub async fn user_funding( 207 | &self, 208 | user: Address, 209 | start_time: u64, 210 | end_time: Option, 211 | ) -> Result, HyperliquidError> { 212 | let mut request = json!({ 213 | "type": "userFunding", 214 | "user": user, 215 | "startTime": start_time 216 | }); 217 | 218 | if let Some(end) = end_time { 219 | request["endTime"] = json!(end); 220 | } 221 | 222 | self.request(request).await 223 | } 224 | 225 | pub async fn user_fees( 226 | &self, 227 | user: Address, 228 | ) -> Result { 229 | let request = json!({ 230 | "type": "userFees", 231 | "user": user 232 | }); 233 | self.request(request).await 234 | } 235 | 236 | pub async fn recent_trades( 237 | &self, 238 | coin: impl Into, 239 | ) -> Result, HyperliquidError> { 240 | let symbol = coin.into(); 241 | let request = json!({ 242 | "type": "recentTrades", 243 | "coin": symbol.as_str() 244 | }); 245 | self.request(request).await 246 | } 247 | 248 | pub async fn user_token_balances( 249 | &self, 250 | user: Address, 251 | ) -> Result { 252 | let request = json!({ 253 | "type": "spotClearinghouseState", 254 | "user": user 255 | }); 256 | self.request(request).await 257 | } 258 | 259 | pub async fn referral( 260 | &self, 261 | user: Address, 262 | ) -> Result { 263 | let request = json!({ 264 | "type": "referral", 265 | "user": user 266 | }); 267 | self.request(request).await 268 | } 269 | 270 | pub async fn meta(&self) -> Result { 271 | let request = json!({ 272 | "type": "meta" 273 | }); 274 | self.request(request).await 275 | } 276 | 277 | pub async fn spot_meta(&self) -> Result { 278 | let request = json!({ 279 | "type": "spotMeta" 280 | }); 281 | self.request(request).await 282 | } 283 | 284 | pub async fn spot_meta_and_asset_ctxs( 285 | &self, 286 | ) -> Result { 287 | let request = json!({ 288 | "type": "spotMetaAndAssetCtxs" 289 | }); 290 | self.request(request).await 291 | } 292 | 293 | // ==================== Builder Pattern Methods ==================== 294 | 295 | pub fn candles(&self, coin: impl Into) -> CandlesRequestBuilder { 296 | CandlesRequestBuilder { 297 | provider: self, 298 | coin: coin.into(), 299 | interval: None, 300 | start_time: None, 301 | end_time: None, 302 | } 303 | } 304 | 305 | pub fn funding_history(&self, coin: impl Into) -> FundingHistoryBuilder { 306 | FundingHistoryBuilder { 307 | provider: self, 308 | coin: coin.into(), 309 | start_time: None, 310 | end_time: None, 311 | } 312 | } 313 | } 314 | 315 | // ==================== Request Builders ==================== 316 | 317 | pub struct CandlesRequestBuilder<'a> { 318 | provider: &'a InfoProvider, 319 | coin: Symbol, 320 | interval: Option, 321 | start_time: Option, 322 | end_time: Option, 323 | } 324 | 325 | impl<'a> CandlesRequestBuilder<'a> { 326 | pub fn interval(mut self, interval: impl Into) -> Self { 327 | self.interval = Some(interval.into()); 328 | self 329 | } 330 | 331 | pub fn time_range(mut self, start: u64, end: u64) -> Self { 332 | self.start_time = Some(start); 333 | self.end_time = Some(end); 334 | self 335 | } 336 | 337 | pub fn start_time(mut self, start: u64) -> Self { 338 | self.start_time = Some(start); 339 | self 340 | } 341 | 342 | pub fn end_time(mut self, end: u64) -> Self { 343 | self.end_time = Some(end); 344 | self 345 | } 346 | 347 | pub async fn send(self) -> Result, HyperliquidError> { 348 | let interval = self.interval.ok_or_else(|| { 349 | HyperliquidError::InvalidRequest("interval is required".into()) 350 | })?; 351 | let start_time = self.start_time.ok_or_else(|| { 352 | HyperliquidError::InvalidRequest("start_time is required".into()) 353 | })?; 354 | let end_time = self.end_time.ok_or_else(|| { 355 | HyperliquidError::InvalidRequest("end_time is required".into()) 356 | })?; 357 | 358 | let request = json!({ 359 | "type": "candleSnapshot", 360 | "req": { 361 | "coin": self.coin.as_str(), 362 | "interval": interval, 363 | "startTime": start_time, 364 | "endTime": end_time 365 | } 366 | }); 367 | 368 | self.provider.request(request).await 369 | } 370 | } 371 | 372 | pub struct FundingHistoryBuilder<'a> { 373 | provider: &'a InfoProvider, 374 | coin: Symbol, 375 | start_time: Option, 376 | end_time: Option, 377 | } 378 | 379 | impl<'a> FundingHistoryBuilder<'a> { 380 | pub fn time_range(mut self, start: u64, end: u64) -> Self { 381 | self.start_time = Some(start); 382 | self.end_time = Some(end); 383 | self 384 | } 385 | 386 | pub fn start_time(mut self, start: u64) -> Self { 387 | self.start_time = Some(start); 388 | self 389 | } 390 | 391 | pub fn end_time(mut self, end: u64) -> Self { 392 | self.end_time = Some(end); 393 | self 394 | } 395 | 396 | pub async fn send(self) -> Result, HyperliquidError> { 397 | let start_time = self.start_time.ok_or_else(|| { 398 | HyperliquidError::InvalidRequest("start_time is required".into()) 399 | })?; 400 | 401 | let mut request = json!({ 402 | "type": "fundingHistory", 403 | "coin": self.coin.as_str(), 404 | "startTime": start_time 405 | }); 406 | 407 | if let Some(end) = self.end_time { 408 | request["endTime"] = json!(end); 409 | } 410 | 411 | self.provider.request(request).await 412 | } 413 | } 414 | -------------------------------------------------------------------------------- /src/providers/websocket.rs: -------------------------------------------------------------------------------- 1 | //! WebSocket provider for real-time market data and user events 2 | 3 | use std::sync::{ 4 | Arc, 5 | atomic::{AtomicU32, Ordering}, 6 | }; 7 | 8 | use dashmap::DashMap; 9 | use fastwebsockets::{Frame, OpCode, Role, WebSocket, handshake}; 10 | use http_body_util::Empty; 11 | use hyper::{Request, StatusCode, body::Bytes, header, upgrade::Upgraded}; 12 | use hyper_util::rt::TokioIo; 13 | use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; 14 | 15 | use crate::{ 16 | Network, 17 | errors::HyperliquidError, 18 | types::ws::{Message, Subscription, WsRequest}, 19 | types::Symbol, 20 | }; 21 | 22 | pub type SubscriptionId = u32; 23 | 24 | #[derive(Clone)] 25 | struct SubscriptionHandle { 26 | subscription: Subscription, 27 | tx: UnboundedSender, 28 | } 29 | 30 | /// Raw WebSocket provider for Hyperliquid 31 | /// 32 | /// This is a thin wrapper around fastwebsockets that provides: 33 | /// - Type-safe subscriptions 34 | /// - Simple message routing 35 | /// - No automatic reconnection (user controls retry logic) 36 | pub struct RawWsProvider { 37 | _network: Network, 38 | ws: Option>>, 39 | subscriptions: Arc>, 40 | next_id: Arc, 41 | message_tx: Option>, 42 | task_handle: Option>, 43 | } 44 | 45 | impl RawWsProvider { 46 | /// Connect to Hyperliquid WebSocket 47 | pub async fn connect(network: Network) -> Result { 48 | let url = match network { 49 | Network::Mainnet => "https://api.hyperliquid.xyz/ws", 50 | Network::Testnet => "https://api.hyperliquid-testnet.xyz/ws", 51 | }; 52 | 53 | let ws = Self::establish_connection(url).await?; 54 | let subscriptions = Arc::new(DashMap::new()); 55 | let next_id = Arc::new(AtomicU32::new(1)); 56 | 57 | // Create message routing channel 58 | let (message_tx, message_rx) = mpsc::unbounded_channel(); 59 | 60 | // Spawn message routing task 61 | let subscriptions_clone = subscriptions.clone(); 62 | let task_handle = tokio::spawn(async move { 63 | Self::message_router(message_rx, subscriptions_clone).await; 64 | }); 65 | 66 | Ok(Self { 67 | _network: network, 68 | ws: Some(ws), 69 | subscriptions, 70 | next_id, 71 | message_tx: Some(message_tx), 72 | task_handle: Some(task_handle), 73 | }) 74 | } 75 | 76 | async fn establish_connection( 77 | url: &str, 78 | ) -> Result>, HyperliquidError> { 79 | use hyper_rustls::HttpsConnectorBuilder; 80 | use hyper_util::client::legacy::Client; 81 | 82 | let uri = url 83 | .parse::() 84 | .map_err(|e| HyperliquidError::WebSocket(format!("Invalid URL: {}", e)))?; 85 | 86 | // Create HTTPS connector with proper configuration 87 | let https = HttpsConnectorBuilder::new() 88 | .with_native_roots() 89 | .map_err(|e| { 90 | HyperliquidError::WebSocket(format!("Failed to load native roots: {}", e)) 91 | })? 92 | .https_only() 93 | .enable_http1() 94 | .build(); 95 | 96 | let client = Client::builder(hyper_util::rt::TokioExecutor::new()) 97 | .build::<_, Empty>(https); 98 | 99 | // Create WebSocket upgrade request 100 | let host = uri 101 | .host() 102 | .ok_or_else(|| HyperliquidError::WebSocket("No host in URL".to_string()))?; 103 | 104 | let req = Request::builder() 105 | .method("GET") 106 | .uri(&uri) 107 | .header(header::HOST, host) 108 | .header(header::CONNECTION, "upgrade") 109 | .header(header::UPGRADE, "websocket") 110 | .header(header::SEC_WEBSOCKET_VERSION, "13") 111 | .header(header::SEC_WEBSOCKET_KEY, handshake::generate_key()) 112 | .body(Empty::new()) 113 | .map_err(|e| { 114 | HyperliquidError::WebSocket(format!("Request build failed: {}", e)) 115 | })?; 116 | 117 | let res = client.request(req).await.map_err(|e| { 118 | HyperliquidError::WebSocket(format!("HTTP request failed: {}", e)) 119 | })?; 120 | 121 | if res.status() != StatusCode::SWITCHING_PROTOCOLS { 122 | return Err(HyperliquidError::WebSocket(format!( 123 | "WebSocket upgrade failed: {}", 124 | res.status() 125 | ))); 126 | } 127 | 128 | let upgraded = hyper::upgrade::on(res) 129 | .await 130 | .map_err(|e| HyperliquidError::WebSocket(format!("Upgrade failed: {}", e)))?; 131 | 132 | Ok(WebSocket::after_handshake( 133 | TokioIo::new(upgraded), 134 | Role::Client, 135 | )) 136 | } 137 | 138 | /// Subscribe to L2 order book updates 139 | pub async fn subscribe_l2_book( 140 | &mut self, 141 | coin: impl Into, 142 | ) -> Result<(SubscriptionId, UnboundedReceiver), HyperliquidError> { 143 | let symbol = coin.into(); 144 | let subscription = Subscription::L2Book { 145 | coin: symbol.as_str().to_string(), 146 | }; 147 | self.subscribe(subscription).await 148 | } 149 | 150 | /// Subscribe to trades 151 | pub async fn subscribe_trades( 152 | &mut self, 153 | coin: impl Into, 154 | ) -> Result<(SubscriptionId, UnboundedReceiver), HyperliquidError> { 155 | let symbol = coin.into(); 156 | let subscription = Subscription::Trades { 157 | coin: symbol.as_str().to_string(), 158 | }; 159 | self.subscribe(subscription).await 160 | } 161 | 162 | /// Subscribe to all mid prices 163 | pub async fn subscribe_all_mids( 164 | &mut self, 165 | ) -> Result<(SubscriptionId, UnboundedReceiver), HyperliquidError> { 166 | self.subscribe(Subscription::AllMids).await 167 | } 168 | 169 | /// Generic subscription method 170 | pub async fn subscribe( 171 | &mut self, 172 | subscription: Subscription, 173 | ) -> Result<(SubscriptionId, UnboundedReceiver), HyperliquidError> { 174 | let ws = self 175 | .ws 176 | .as_mut() 177 | .ok_or_else(|| HyperliquidError::WebSocket("Not connected".to_string()))?; 178 | 179 | // Send subscription request 180 | let request = WsRequest::subscribe(subscription.clone()); 181 | let payload = serde_json::to_string(&request) 182 | .map_err(|e| HyperliquidError::Serialize(e.to_string()))?; 183 | 184 | ws.write_frame(Frame::text(payload.into_bytes().into())) 185 | .await 186 | .map_err(|e| { 187 | HyperliquidError::WebSocket(format!("Failed to send subscription: {}", e)) 188 | })?; 189 | 190 | // Create channel for this subscription 191 | let (tx, rx) = mpsc::unbounded_channel(); 192 | let id = self.next_id.fetch_add(1, Ordering::SeqCst); 193 | 194 | self.subscriptions 195 | .insert(id, SubscriptionHandle { subscription, tx }); 196 | 197 | Ok((id, rx)) 198 | } 199 | 200 | /// Unsubscribe from a subscription 201 | pub async fn unsubscribe( 202 | &mut self, 203 | id: SubscriptionId, 204 | ) -> Result<(), HyperliquidError> { 205 | if let Some((_, handle)) = self.subscriptions.remove(&id) { 206 | let ws = self.ws.as_mut().ok_or_else(|| { 207 | HyperliquidError::WebSocket("Not connected".to_string()) 208 | })?; 209 | 210 | let request = WsRequest::unsubscribe(handle.subscription); 211 | let payload = serde_json::to_string(&request) 212 | .map_err(|e| HyperliquidError::Serialize(e.to_string()))?; 213 | 214 | ws.write_frame(Frame::text(payload.into_bytes().into())) 215 | .await 216 | .map_err(|e| { 217 | HyperliquidError::WebSocket(format!( 218 | "Failed to send unsubscribe: {}", 219 | e 220 | )) 221 | })?; 222 | } 223 | 224 | Ok(()) 225 | } 226 | 227 | /// Send a ping to keep connection alive 228 | pub async fn ping(&mut self) -> Result<(), HyperliquidError> { 229 | let ws = self 230 | .ws 231 | .as_mut() 232 | .ok_or_else(|| HyperliquidError::WebSocket("Not connected".to_string()))?; 233 | 234 | let request = WsRequest::ping(); 235 | let payload = serde_json::to_string(&request) 236 | .map_err(|e| HyperliquidError::Serialize(e.to_string()))?; 237 | 238 | ws.write_frame(Frame::text(payload.into_bytes().into())) 239 | .await 240 | .map_err(|e| { 241 | HyperliquidError::WebSocket(format!("Failed to send ping: {}", e)) 242 | })?; 243 | 244 | Ok(()) 245 | } 246 | 247 | /// Check if connected 248 | pub fn is_connected(&self) -> bool { 249 | self.ws.is_some() 250 | } 251 | 252 | /// Start reading messages (must be called after connecting) 253 | pub async fn start_reading(&mut self) -> Result<(), HyperliquidError> { 254 | let mut ws = self 255 | .ws 256 | .take() 257 | .ok_or_else(|| HyperliquidError::WebSocket("Not connected".to_string()))?; 258 | 259 | let message_tx = self.message_tx.clone().ok_or_else(|| { 260 | HyperliquidError::WebSocket("Message channel not initialized".to_string()) 261 | })?; 262 | 263 | tokio::spawn(async move { 264 | while let Ok(frame) = ws.read_frame().await { 265 | match frame.opcode { 266 | OpCode::Text => { 267 | if let Ok(text) = String::from_utf8(frame.payload.to_vec()) { 268 | let _ = message_tx.send(text); 269 | } 270 | } 271 | OpCode::Close => { 272 | break; 273 | } 274 | _ => {} 275 | } 276 | } 277 | }); 278 | 279 | Ok(()) 280 | } 281 | 282 | async fn message_router( 283 | mut rx: UnboundedReceiver, 284 | subscriptions: Arc>, 285 | ) { 286 | while let Some(text) = rx.recv().await { 287 | // Use simd-json for fast parsing 288 | let mut text_bytes = text.into_bytes(); 289 | match simd_json::from_slice::(&mut text_bytes) { 290 | Ok(message) => { 291 | // Route to all active subscriptions 292 | // In a more sophisticated implementation, we'd match by subscription type 293 | for entry in subscriptions.iter() { 294 | let _ = entry.value().tx.send(message.clone()); 295 | } 296 | } 297 | Err(_) => { 298 | // Ignore parse errors 299 | } 300 | } 301 | } 302 | } 303 | } 304 | 305 | impl Drop for RawWsProvider { 306 | fn drop(&mut self) { 307 | // Clean shutdown 308 | if let Some(handle) = self.task_handle.take() { 309 | handle.abort(); 310 | } 311 | } 312 | } 313 | 314 | // ==================== Enhanced WebSocket Provider ==================== 315 | 316 | use std::time::{Duration, Instant}; 317 | use tokio::sync::Mutex; 318 | use tokio::time::sleep; 319 | 320 | /// Configuration for managed WebSocket provider 321 | #[derive(Clone, Debug)] 322 | pub struct WsConfig { 323 | /// Interval between ping messages (0 to disable) 324 | pub ping_interval: Duration, 325 | /// Timeout waiting for pong response 326 | pub pong_timeout: Duration, 327 | /// Enable automatic reconnection 328 | pub auto_reconnect: bool, 329 | /// Initial delay between reconnection attempts 330 | pub reconnect_delay: Duration, 331 | /// Maximum reconnection attempts (None for infinite) 332 | pub max_reconnect_attempts: Option, 333 | /// Use exponential backoff for reconnection delays 334 | pub exponential_backoff: bool, 335 | /// Maximum backoff delay when using exponential backoff 336 | pub max_reconnect_delay: Duration, 337 | } 338 | 339 | impl Default for WsConfig { 340 | fn default() -> Self { 341 | Self { 342 | ping_interval: Duration::from_secs(30), 343 | pong_timeout: Duration::from_secs(5), 344 | auto_reconnect: true, 345 | reconnect_delay: Duration::from_secs(1), 346 | max_reconnect_attempts: None, 347 | exponential_backoff: true, 348 | max_reconnect_delay: Duration::from_secs(60), 349 | } 350 | } 351 | } 352 | 353 | #[derive(Clone)] 354 | struct ManagedSubscription { 355 | subscription: Subscription, 356 | tx: UnboundedSender, 357 | #[allow(dead_code)] 358 | created_at: Instant, // For future use: subscription age tracking 359 | } 360 | 361 | /// Managed WebSocket provider with automatic keep-alive and reconnection 362 | /// 363 | /// This provider builds on top of RawWsProvider to add: 364 | /// - Automatic ping/pong keep-alive 365 | /// - Automatic reconnection with subscription replay 366 | /// - Connection state monitoring 367 | /// - Configurable retry behavior 368 | pub struct ManagedWsProvider { 369 | network: Network, 370 | inner: Arc>>, 371 | subscriptions: Arc>, 372 | config: WsConfig, 373 | next_id: Arc, 374 | } 375 | 376 | impl ManagedWsProvider { 377 | /// Connect with custom configuration 378 | pub async fn connect(network: Network, config: WsConfig) -> Result, HyperliquidError> { 379 | // Create initial connection 380 | let raw_provider = RawWsProvider::connect(network).await?; 381 | 382 | let provider = Arc::new(Self { 383 | network, 384 | inner: Arc::new(Mutex::new(Some(raw_provider))), 385 | subscriptions: Arc::new(DashMap::new()), 386 | config, 387 | next_id: Arc::new(AtomicU32::new(1)), 388 | }); 389 | 390 | // Start keep-alive task if configured 391 | if provider.config.ping_interval > Duration::ZERO { 392 | let provider_clone = provider.clone(); 393 | tokio::spawn(async move { 394 | provider_clone.keepalive_loop().await; 395 | }); 396 | } 397 | 398 | // Start reconnection task if configured 399 | if provider.config.auto_reconnect { 400 | let provider_clone = provider.clone(); 401 | tokio::spawn(async move { 402 | provider_clone.reconnect_loop().await; 403 | }); 404 | } 405 | 406 | Ok(provider) 407 | } 408 | 409 | /// Connect with default configuration 410 | pub async fn connect_with_defaults(network: Network) -> Result, HyperliquidError> { 411 | Self::connect(network, WsConfig::default()).await 412 | } 413 | 414 | /// Check if currently connected 415 | pub async fn is_connected(&self) -> bool { 416 | let inner = self.inner.lock().await; 417 | inner.as_ref().map(|p| p.is_connected()).unwrap_or(false) 418 | } 419 | 420 | /// Get mutable access to the raw provider 421 | pub async fn raw(&self) -> Result>, HyperliquidError> { 422 | Ok(self.inner.lock().await) 423 | } 424 | 425 | /// Subscribe to L2 order book updates with automatic replay on reconnect 426 | pub async fn subscribe_l2_book( 427 | &self, 428 | coin: impl Into, 429 | ) -> Result<(SubscriptionId, UnboundedReceiver), HyperliquidError> { 430 | let symbol = coin.into(); 431 | let subscription = Subscription::L2Book { 432 | coin: symbol.as_str().to_string(), 433 | }; 434 | self.subscribe(subscription).await 435 | } 436 | 437 | /// Subscribe to trades with automatic replay on reconnect 438 | pub async fn subscribe_trades( 439 | &self, 440 | coin: impl Into, 441 | ) -> Result<(SubscriptionId, UnboundedReceiver), HyperliquidError> { 442 | let symbol = coin.into(); 443 | let subscription = Subscription::Trades { 444 | coin: symbol.as_str().to_string(), 445 | }; 446 | self.subscribe(subscription).await 447 | } 448 | 449 | /// Subscribe to all mid prices with automatic replay on reconnect 450 | pub async fn subscribe_all_mids( 451 | &self, 452 | ) -> Result<(SubscriptionId, UnboundedReceiver), HyperliquidError> { 453 | self.subscribe(Subscription::AllMids).await 454 | } 455 | 456 | /// Generic subscription with automatic replay on reconnect 457 | pub async fn subscribe( 458 | &self, 459 | subscription: Subscription, 460 | ) -> Result<(SubscriptionId, UnboundedReceiver), HyperliquidError> { 461 | let mut inner = self.inner.lock().await; 462 | let raw_provider = inner.as_mut().ok_or_else(|| { 463 | HyperliquidError::WebSocket("Not connected".to_string()) 464 | })?; 465 | 466 | // Subscribe using the raw provider 467 | let (_raw_id, rx) = raw_provider.subscribe(subscription.clone()).await?; 468 | 469 | // Generate our own ID for tracking 470 | let managed_id = self.next_id.fetch_add(1, Ordering::SeqCst); 471 | 472 | // Create channel for managed subscription 473 | let (tx, managed_rx) = mpsc::unbounded_channel(); 474 | 475 | // Store subscription for replay 476 | self.subscriptions.insert( 477 | managed_id, 478 | ManagedSubscription { 479 | subscription, 480 | tx: tx.clone(), 481 | created_at: Instant::now(), 482 | }, 483 | ); 484 | 485 | // Forward messages from raw to managed 486 | let subscriptions = self.subscriptions.clone(); 487 | tokio::spawn(async move { 488 | let mut rx = rx; 489 | while let Some(msg) = rx.recv().await { 490 | if let Some(entry) = subscriptions.get(&managed_id) { 491 | let _ = entry.tx.send(msg); 492 | } 493 | } 494 | // Clean up when channel closes 495 | subscriptions.remove(&managed_id); 496 | }); 497 | 498 | Ok((managed_id, managed_rx)) 499 | } 500 | 501 | /// Unsubscribe and stop automatic replay 502 | pub async fn unsubscribe(&self, id: SubscriptionId) -> Result<(), HyperliquidError> { 503 | // Remove from our tracking 504 | self.subscriptions.remove(&id); 505 | 506 | // Note: We can't unsubscribe from the raw provider because we don't 507 | // track the mapping between our IDs and raw IDs. This is fine since 508 | // the subscription will be cleaned up on reconnect anyway. 509 | 510 | Ok(()) 511 | } 512 | 513 | /// Start reading messages (must be called after connecting) 514 | pub async fn start_reading(&self) -> Result<(), HyperliquidError> { 515 | let mut inner = self.inner.lock().await; 516 | let raw_provider = inner.as_mut().ok_or_else(|| { 517 | HyperliquidError::WebSocket("Not connected".to_string()) 518 | })?; 519 | raw_provider.start_reading().await 520 | } 521 | 522 | // Keep-alive loop 523 | async fn keepalive_loop(self: Arc) { 524 | let mut interval = tokio::time::interval(self.config.ping_interval); 525 | 526 | loop { 527 | interval.tick().await; 528 | 529 | let mut inner = self.inner.lock().await; 530 | if let Some(provider) = inner.as_mut() { 531 | if provider.ping().await.is_err() { 532 | // Ping failed, connection might be dead 533 | drop(inner); 534 | self.handle_disconnect().await; 535 | } 536 | } 537 | } 538 | } 539 | 540 | // Reconnection loop 541 | async fn reconnect_loop(self: Arc) { 542 | let mut reconnect_attempts = 0u32; 543 | let mut current_delay = self.config.reconnect_delay; 544 | 545 | loop { 546 | // Wait a bit before checking 547 | sleep(Duration::from_secs(1)).await; 548 | 549 | // Check if we need to reconnect 550 | if !self.is_connected().await { 551 | // Check max attempts 552 | if let Some(max) = self.config.max_reconnect_attempts { 553 | if reconnect_attempts >= max { 554 | eprintln!("Max reconnection attempts ({}) reached", max); 555 | break; 556 | } 557 | } 558 | 559 | println!("Attempting reconnection #{}", reconnect_attempts + 1); 560 | 561 | match RawWsProvider::connect(self.network).await { 562 | Ok(mut new_provider) => { 563 | // Start reading before replaying subscriptions 564 | if let Err(e) = new_provider.start_reading().await { 565 | eprintln!("Failed to start reading after reconnect: {}", e); 566 | continue; 567 | } 568 | 569 | // Replay all subscriptions 570 | let mut replay_errors = 0; 571 | for entry in self.subscriptions.iter() { 572 | if let Err(e) = new_provider.subscribe(entry.subscription.clone()).await { 573 | eprintln!("Failed to replay subscription: {}", e); 574 | replay_errors += 1; 575 | } 576 | } 577 | 578 | if replay_errors == 0 { 579 | // Success! Reset counters 580 | *self.inner.lock().await = Some(new_provider); 581 | reconnect_attempts = 0; 582 | current_delay = self.config.reconnect_delay; 583 | println!("Reconnection successful, {} subscriptions replayed", 584 | self.subscriptions.len()); 585 | } 586 | } 587 | Err(e) => { 588 | eprintln!("Reconnection failed: {}", e); 589 | 590 | // Wait before next attempt 591 | sleep(current_delay).await; 592 | 593 | // Update delay for next attempt 594 | reconnect_attempts += 1; 595 | if self.config.exponential_backoff { 596 | current_delay = std::cmp::min( 597 | current_delay * 2, 598 | self.config.max_reconnect_delay, 599 | ); 600 | } 601 | } 602 | } 603 | } 604 | } 605 | } 606 | 607 | // Handle disconnection 608 | async fn handle_disconnect(&self) { 609 | *self.inner.lock().await = None; 610 | } 611 | } 612 | 613 | // Note: Background tasks (keepalive and reconnect loops) will automatically 614 | // terminate when all Arc references to the provider are dropped, since they 615 | // hold Arc and will exit when is_connected() returns false. 616 | 617 | // Re-export for backwards compatibility 618 | pub use RawWsProvider as WsProvider; 619 | -------------------------------------------------------------------------------- /src/types/symbols.rs: -------------------------------------------------------------------------------- 1 | //! Pre-defined symbols for common Hyperliquid assets 2 | //! 3 | //! For perpetuals, use the coin name directly (e.g., `BTC`, `ETH`) 4 | //! For spot pairs, use the Hyperliquid notation (e.g., `@0` for PURR/USDC) 5 | 6 | use crate::types::symbol::Symbol; 7 | 8 | // ==================== MAINNET ==================== 9 | 10 | // ==================== MAINNET PERPETUALS ==================== 11 | 12 | /// AAVE Perpetual (index: 28) 13 | pub const AAVE: Symbol = Symbol::from_static("AAVE"); 14 | 15 | /// ACE Perpetual (index: 96) 16 | pub const ACE: Symbol = Symbol::from_static("ACE"); 17 | 18 | /// ADA Perpetual (index: 65) 19 | pub const ADA: Symbol = Symbol::from_static("ADA"); 20 | 21 | /// AI Perpetual (index: 115) 22 | pub const AI: Symbol = Symbol::from_static("AI"); 23 | 24 | /// AI16Z Perpetual (index: 166) 25 | pub const AI16Z: Symbol = Symbol::from_static("AI16Z"); 26 | 27 | /// AIXBT Perpetual (index: 167) 28 | pub const AIXBT: Symbol = Symbol::from_static("AIXBT"); 29 | 30 | /// ALGO Perpetual (index: 158) 31 | pub const ALGO: Symbol = Symbol::from_static("ALGO"); 32 | 33 | /// ALT Perpetual (index: 107) 34 | pub const ALT: Symbol = Symbol::from_static("ALT"); 35 | 36 | /// ANIME Perpetual (index: 176) 37 | pub const ANIME: Symbol = Symbol::from_static("ANIME"); 38 | 39 | /// APE Perpetual (index: 8) 40 | pub const APE: Symbol = Symbol::from_static("APE"); 41 | 42 | /// APT Perpetual (index: 27) 43 | pub const APT: Symbol = Symbol::from_static("APT"); 44 | 45 | /// AR Perpetual (index: 117) 46 | pub const AR: Symbol = Symbol::from_static("AR"); 47 | 48 | /// ARB Perpetual (index: 11) 49 | pub const ARB: Symbol = Symbol::from_static("ARB"); 50 | 51 | /// ARK Perpetual (index: 55) 52 | pub const ARK: Symbol = Symbol::from_static("ARK"); 53 | 54 | /// ATOM Perpetual (index: 2) 55 | pub const ATOM: Symbol = Symbol::from_static("ATOM"); 56 | 57 | /// AVAX Perpetual (index: 6) 58 | pub const AVAX: Symbol = Symbol::from_static("AVAX"); 59 | 60 | /// BABY Perpetual (index: 189) 61 | pub const BABY: Symbol = Symbol::from_static("BABY"); 62 | 63 | /// BADGER Perpetual (index: 77) 64 | pub const BADGER: Symbol = Symbol::from_static("BADGER"); 65 | 66 | /// BANANA Perpetual (index: 49) 67 | pub const BANANA: Symbol = Symbol::from_static("BANANA"); 68 | 69 | /// BCH Perpetual (index: 26) 70 | pub const BCH: Symbol = Symbol::from_static("BCH"); 71 | 72 | /// BERA Perpetual (index: 180) 73 | pub const BERA: Symbol = Symbol::from_static("BERA"); 74 | 75 | /// BIGTIME Perpetual (index: 59) 76 | pub const BIGTIME: Symbol = Symbol::from_static("BIGTIME"); 77 | 78 | /// BIO Perpetual (index: 169) 79 | pub const BIO: Symbol = Symbol::from_static("BIO"); 80 | 81 | /// BLAST Perpetual (index: 137) 82 | pub const BLAST: Symbol = Symbol::from_static("BLAST"); 83 | 84 | /// BLUR Perpetual (index: 62) 85 | pub const BLUR: Symbol = Symbol::from_static("BLUR"); 86 | 87 | /// BLZ Perpetual (index: 47) 88 | pub const BLZ: Symbol = Symbol::from_static("BLZ"); 89 | 90 | /// BNB Perpetual (index: 7) 91 | pub const BNB: Symbol = Symbol::from_static("BNB"); 92 | 93 | /// BNT Perpetual (index: 56) 94 | pub const BNT: Symbol = Symbol::from_static("BNT"); 95 | 96 | /// BOME Perpetual (index: 120) 97 | pub const BOME: Symbol = Symbol::from_static("BOME"); 98 | 99 | /// BRETT Perpetual (index: 134) 100 | pub const BRETT: Symbol = Symbol::from_static("BRETT"); 101 | 102 | /// BSV Perpetual (index: 64) 103 | pub const BSV: Symbol = Symbol::from_static("BSV"); 104 | 105 | /// BTC Perpetual (index: 0) 106 | pub const BTC: Symbol = Symbol::from_static("BTC"); 107 | 108 | /// CAKE Perpetual (index: 99) 109 | pub const CAKE: Symbol = Symbol::from_static("CAKE"); 110 | 111 | /// CANTO Perpetual (index: 57) 112 | pub const CANTO: Symbol = Symbol::from_static("CANTO"); 113 | 114 | /// CATI Perpetual (index: 143) 115 | pub const CATI: Symbol = Symbol::from_static("CATI"); 116 | 117 | /// CELO Perpetual (index: 144) 118 | pub const CELO: Symbol = Symbol::from_static("CELO"); 119 | 120 | /// CFX Perpetual (index: 21) 121 | pub const CFX: Symbol = Symbol::from_static("CFX"); 122 | 123 | /// CHILLGUY Perpetual (index: 155) 124 | pub const CHILLGUY: Symbol = Symbol::from_static("CHILLGUY"); 125 | 126 | /// COMP Perpetual (index: 29) 127 | pub const COMP: Symbol = Symbol::from_static("COMP"); 128 | 129 | /// CRV Perpetual (index: 16) 130 | pub const CRV: Symbol = Symbol::from_static("CRV"); 131 | 132 | /// CYBER Perpetual (index: 45) 133 | pub const CYBER: Symbol = Symbol::from_static("CYBER"); 134 | 135 | /// DOGE Perpetual (index: 12) 136 | pub const DOGE: Symbol = Symbol::from_static("DOGE"); 137 | 138 | /// DOOD Perpetual (index: 194) 139 | pub const DOOD: Symbol = Symbol::from_static("DOOD"); 140 | 141 | /// DOT Perpetual (index: 48) 142 | pub const DOT: Symbol = Symbol::from_static("DOT"); 143 | 144 | /// DYDX Perpetual (index: 4) 145 | pub const DYDX: Symbol = Symbol::from_static("DYDX"); 146 | 147 | /// DYM Perpetual (index: 109) 148 | pub const DYM: Symbol = Symbol::from_static("DYM"); 149 | 150 | /// EIGEN Perpetual (index: 130) 151 | pub const EIGEN: Symbol = Symbol::from_static("EIGEN"); 152 | 153 | /// ENA Perpetual (index: 122) 154 | pub const ENA: Symbol = Symbol::from_static("ENA"); 155 | 156 | /// ENS Perpetual (index: 101) 157 | pub const ENS: Symbol = Symbol::from_static("ENS"); 158 | 159 | /// ETC Perpetual (index: 102) 160 | pub const ETC: Symbol = Symbol::from_static("ETC"); 161 | 162 | /// ETH Perpetual (index: 1) 163 | pub const ETH: Symbol = Symbol::from_static("ETH"); 164 | 165 | /// ETHFI Perpetual (index: 121) 166 | pub const ETHFI: Symbol = Symbol::from_static("ETHFI"); 167 | 168 | /// FARTCOIN Perpetual (index: 165) 169 | pub const FARTCOIN: Symbol = Symbol::from_static("FARTCOIN"); 170 | 171 | /// FET Perpetual (index: 72) 172 | pub const FET: Symbol = Symbol::from_static("FET"); 173 | 174 | /// FIL Perpetual (index: 80) 175 | pub const FIL: Symbol = Symbol::from_static("FIL"); 176 | 177 | /// FRIEND Perpetual (index: 43) 178 | pub const FRIEND: Symbol = Symbol::from_static("FRIEND"); 179 | 180 | /// FTM Perpetual (index: 22) 181 | pub const FTM: Symbol = Symbol::from_static("FTM"); 182 | 183 | /// FTT Perpetual (index: 51) 184 | pub const FTT: Symbol = Symbol::from_static("FTT"); 185 | 186 | /// FXS Perpetual (index: 32) 187 | pub const FXS: Symbol = Symbol::from_static("FXS"); 188 | 189 | /// GALA Perpetual (index: 93) 190 | pub const GALA: Symbol = Symbol::from_static("GALA"); 191 | 192 | /// GAS Perpetual (index: 69) 193 | pub const GAS: Symbol = Symbol::from_static("GAS"); 194 | 195 | /// GMT Perpetual (index: 86) 196 | pub const GMT: Symbol = Symbol::from_static("GMT"); 197 | 198 | /// GMX Perpetual (index: 23) 199 | pub const GMX: Symbol = Symbol::from_static("GMX"); 200 | 201 | /// GOAT Perpetual (index: 149) 202 | pub const GOAT: Symbol = Symbol::from_static("GOAT"); 203 | 204 | /// GRASS Perpetual (index: 151) 205 | pub const GRASS: Symbol = Symbol::from_static("GRASS"); 206 | 207 | /// GRIFFAIN Perpetual (index: 170) 208 | pub const GRIFFAIN: Symbol = Symbol::from_static("GRIFFAIN"); 209 | 210 | /// HBAR Perpetual (index: 127) 211 | pub const HBAR: Symbol = Symbol::from_static("HBAR"); 212 | 213 | /// HMSTR Perpetual (index: 145) 214 | pub const HMSTR: Symbol = Symbol::from_static("HMSTR"); 215 | 216 | /// HPOS Perpetual (index: 33) 217 | pub const HPOS: Symbol = Symbol::from_static("HPOS"); 218 | 219 | /// HYPE Perpetual (index: 159) 220 | pub const HYPE: Symbol = Symbol::from_static("HYPE"); 221 | 222 | /// HYPER Perpetual (index: 191) 223 | pub const HYPER: Symbol = Symbol::from_static("HYPER"); 224 | 225 | /// ILV Perpetual (index: 83) 226 | pub const ILV: Symbol = Symbol::from_static("ILV"); 227 | 228 | /// IMX Perpetual (index: 84) 229 | pub const IMX: Symbol = Symbol::from_static("IMX"); 230 | 231 | /// INIT Perpetual (index: 193) 232 | pub const INIT: Symbol = Symbol::from_static("INIT"); 233 | 234 | /// INJ Perpetual (index: 13) 235 | pub const INJ: Symbol = Symbol::from_static("INJ"); 236 | 237 | /// IO Perpetual (index: 135) 238 | pub const IO: Symbol = Symbol::from_static("IO"); 239 | 240 | /// IOTA Perpetual (index: 157) 241 | pub const IOTA: Symbol = Symbol::from_static("IOTA"); 242 | 243 | /// IP Perpetual (index: 183) 244 | pub const IP: Symbol = Symbol::from_static("IP"); 245 | 246 | /// JELLY Perpetual (index: 179) 247 | pub const JELLY: Symbol = Symbol::from_static("JELLY"); 248 | 249 | /// JTO Perpetual (index: 94) 250 | pub const JTO: Symbol = Symbol::from_static("JTO"); 251 | 252 | /// JUP Perpetual (index: 90) 253 | pub const JUP: Symbol = Symbol::from_static("JUP"); 254 | 255 | /// KAITO Perpetual (index: 185) 256 | pub const KAITO: Symbol = Symbol::from_static("KAITO"); 257 | 258 | /// KAS Perpetual (index: 60) 259 | pub const KAS: Symbol = Symbol::from_static("KAS"); 260 | 261 | /// LAUNCHCOIN Perpetual (index: 195) 262 | pub const LAUNCHCOIN: Symbol = Symbol::from_static("LAUNCHCOIN"); 263 | 264 | /// LAYER Perpetual (index: 182) 265 | pub const LAYER: Symbol = Symbol::from_static("LAYER"); 266 | 267 | /// LDO Perpetual (index: 17) 268 | pub const LDO: Symbol = Symbol::from_static("LDO"); 269 | 270 | /// LINK Perpetual (index: 18) 271 | pub const LINK: Symbol = Symbol::from_static("LINK"); 272 | 273 | /// LISTA Perpetual (index: 138) 274 | pub const LISTA: Symbol = Symbol::from_static("LISTA"); 275 | 276 | /// LOOM Perpetual (index: 52) 277 | pub const LOOM: Symbol = Symbol::from_static("LOOM"); 278 | 279 | /// LTC Perpetual (index: 10) 280 | pub const LTC: Symbol = Symbol::from_static("LTC"); 281 | 282 | /// MANTA Perpetual (index: 104) 283 | pub const MANTA: Symbol = Symbol::from_static("MANTA"); 284 | 285 | /// MATIC Perpetual (index: 3) 286 | pub const MATIC: Symbol = Symbol::from_static("MATIC"); 287 | 288 | /// MAV Perpetual (index: 97) 289 | pub const MAV: Symbol = Symbol::from_static("MAV"); 290 | 291 | /// MAVIA Perpetual (index: 110) 292 | pub const MAVIA: Symbol = Symbol::from_static("MAVIA"); 293 | 294 | /// ME Perpetual (index: 160) 295 | pub const ME: Symbol = Symbol::from_static("ME"); 296 | 297 | /// MELANIA Perpetual (index: 175) 298 | pub const MELANIA: Symbol = Symbol::from_static("MELANIA"); 299 | 300 | /// MEME Perpetual (index: 75) 301 | pub const MEME: Symbol = Symbol::from_static("MEME"); 302 | 303 | /// MERL Perpetual (index: 126) 304 | pub const MERL: Symbol = Symbol::from_static("MERL"); 305 | 306 | /// MEW Perpetual (index: 139) 307 | pub const MEW: Symbol = Symbol::from_static("MEW"); 308 | 309 | /// MINA Perpetual (index: 67) 310 | pub const MINA: Symbol = Symbol::from_static("MINA"); 311 | 312 | /// MKR Perpetual (index: 30) 313 | pub const MKR: Symbol = Symbol::from_static("MKR"); 314 | 315 | /// MNT Perpetual (index: 123) 316 | pub const MNT: Symbol = Symbol::from_static("MNT"); 317 | 318 | /// MOODENG Perpetual (index: 150) 319 | pub const MOODENG: Symbol = Symbol::from_static("MOODENG"); 320 | 321 | /// MORPHO Perpetual (index: 173) 322 | pub const MORPHO: Symbol = Symbol::from_static("MORPHO"); 323 | 324 | /// MOVE Perpetual (index: 161) 325 | pub const MOVE: Symbol = Symbol::from_static("MOVE"); 326 | 327 | /// MYRO Perpetual (index: 118) 328 | pub const MYRO: Symbol = Symbol::from_static("MYRO"); 329 | 330 | /// NEAR Perpetual (index: 74) 331 | pub const NEAR: Symbol = Symbol::from_static("NEAR"); 332 | 333 | /// NEIROETH Perpetual (index: 147) 334 | pub const NEIROETH: Symbol = Symbol::from_static("NEIROETH"); 335 | 336 | /// NEO Perpetual (index: 78) 337 | pub const NEO: Symbol = Symbol::from_static("NEO"); 338 | 339 | /// NFTI Perpetual (index: 89) 340 | pub const NFTI: Symbol = Symbol::from_static("NFTI"); 341 | 342 | /// NIL Perpetual (index: 186) 343 | pub const NIL: Symbol = Symbol::from_static("NIL"); 344 | 345 | /// NOT Perpetual (index: 132) 346 | pub const NOT: Symbol = Symbol::from_static("NOT"); 347 | 348 | /// NTRN Perpetual (index: 95) 349 | pub const NTRN: Symbol = Symbol::from_static("NTRN"); 350 | 351 | /// NXPC Perpetual (index: 196) 352 | pub const NXPC: Symbol = Symbol::from_static("NXPC"); 353 | 354 | /// OGN Perpetual (index: 53) 355 | pub const OGN: Symbol = Symbol::from_static("OGN"); 356 | 357 | /// OM Perpetual (index: 184) 358 | pub const OM: Symbol = Symbol::from_static("OM"); 359 | 360 | /// OMNI Perpetual (index: 129) 361 | pub const OMNI: Symbol = Symbol::from_static("OMNI"); 362 | 363 | /// ONDO Perpetual (index: 106) 364 | pub const ONDO: Symbol = Symbol::from_static("ONDO"); 365 | 366 | /// OP Perpetual (index: 9) 367 | pub const OP: Symbol = Symbol::from_static("OP"); 368 | 369 | /// ORBS Perpetual (index: 61) 370 | pub const ORBS: Symbol = Symbol::from_static("ORBS"); 371 | 372 | /// ORDI Perpetual (index: 76) 373 | pub const ORDI: Symbol = Symbol::from_static("ORDI"); 374 | 375 | /// OX Perpetual (index: 42) 376 | pub const OX: Symbol = Symbol::from_static("OX"); 377 | 378 | /// PANDORA Perpetual (index: 112) 379 | pub const PANDORA: Symbol = Symbol::from_static("PANDORA"); 380 | 381 | /// PAXG Perpetual (index: 187) 382 | pub const PAXG: Symbol = Symbol::from_static("PAXG"); 383 | 384 | /// PENDLE Perpetual (index: 70) 385 | pub const PENDLE: Symbol = Symbol::from_static("PENDLE"); 386 | 387 | /// PENGU Perpetual (index: 163) 388 | pub const PENGU: Symbol = Symbol::from_static("PENGU"); 389 | 390 | /// PEOPLE Perpetual (index: 100) 391 | pub const PEOPLE: Symbol = Symbol::from_static("PEOPLE"); 392 | 393 | /// PIXEL Perpetual (index: 114) 394 | pub const PIXEL: Symbol = Symbol::from_static("PIXEL"); 395 | 396 | /// PNUT Perpetual (index: 153) 397 | pub const PNUT: Symbol = Symbol::from_static("PNUT"); 398 | 399 | /// POL Perpetual (index: 142) 400 | pub const POL: Symbol = Symbol::from_static("POL"); 401 | 402 | /// POLYX Perpetual (index: 68) 403 | pub const POLYX: Symbol = Symbol::from_static("POLYX"); 404 | 405 | /// POPCAT Perpetual (index: 128) 406 | pub const POPCAT: Symbol = Symbol::from_static("POPCAT"); 407 | 408 | /// PROMPT Perpetual (index: 188) 409 | pub const PROMPT: Symbol = Symbol::from_static("PROMPT"); 410 | 411 | /// PURR Perpetual (index: 152) 412 | pub const PURR: Symbol = Symbol::from_static("PURR"); 413 | 414 | /// PYTH Perpetual (index: 81) 415 | pub const PYTH: Symbol = Symbol::from_static("PYTH"); 416 | 417 | /// RDNT Perpetual (index: 54) 418 | pub const RDNT: Symbol = Symbol::from_static("RDNT"); 419 | 420 | /// RENDER Perpetual (index: 140) 421 | pub const RENDER: Symbol = Symbol::from_static("RENDER"); 422 | 423 | /// REQ Perpetual (index: 58) 424 | pub const REQ: Symbol = Symbol::from_static("REQ"); 425 | 426 | /// REZ Perpetual (index: 131) 427 | pub const REZ: Symbol = Symbol::from_static("REZ"); 428 | 429 | /// RLB Perpetual (index: 34) 430 | pub const RLB: Symbol = Symbol::from_static("RLB"); 431 | 432 | /// RNDR Perpetual (index: 20) 433 | pub const RNDR: Symbol = Symbol::from_static("RNDR"); 434 | 435 | /// RSR Perpetual (index: 92) 436 | pub const RSR: Symbol = Symbol::from_static("RSR"); 437 | 438 | /// RUNE Perpetual (index: 41) 439 | pub const RUNE: Symbol = Symbol::from_static("RUNE"); 440 | 441 | /// S Perpetual (index: 172) 442 | pub const S: Symbol = Symbol::from_static("S"); 443 | 444 | /// SAGA Perpetual (index: 125) 445 | pub const SAGA: Symbol = Symbol::from_static("SAGA"); 446 | 447 | /// SAND Perpetual (index: 156) 448 | pub const SAND: Symbol = Symbol::from_static("SAND"); 449 | 450 | /// SCR Perpetual (index: 146) 451 | pub const SCR: Symbol = Symbol::from_static("SCR"); 452 | 453 | /// SEI Perpetual (index: 40) 454 | pub const SEI: Symbol = Symbol::from_static("SEI"); 455 | 456 | /// SHIA Perpetual (index: 44) 457 | pub const SHIA: Symbol = Symbol::from_static("SHIA"); 458 | 459 | /// SNX Perpetual (index: 24) 460 | pub const SNX: Symbol = Symbol::from_static("SNX"); 461 | 462 | /// SOL Perpetual (index: 5) 463 | pub const SOL: Symbol = Symbol::from_static("SOL"); 464 | 465 | /// SOPH Perpetual (index: 197) 466 | pub const SOPH: Symbol = Symbol::from_static("SOPH"); 467 | 468 | /// SPX Perpetual (index: 171) 469 | pub const SPX: Symbol = Symbol::from_static("SPX"); 470 | 471 | /// STG Perpetual (index: 71) 472 | pub const STG: Symbol = Symbol::from_static("STG"); 473 | 474 | /// STRAX Perpetual (index: 73) 475 | pub const STRAX: Symbol = Symbol::from_static("STRAX"); 476 | 477 | /// STRK Perpetual (index: 113) 478 | pub const STRK: Symbol = Symbol::from_static("STRK"); 479 | 480 | /// STX Perpetual (index: 19) 481 | pub const STX: Symbol = Symbol::from_static("STX"); 482 | 483 | /// SUI Perpetual (index: 14) 484 | pub const SUI: Symbol = Symbol::from_static("SUI"); 485 | 486 | /// SUPER Perpetual (index: 87) 487 | pub const SUPER: Symbol = Symbol::from_static("SUPER"); 488 | 489 | /// SUSHI Perpetual (index: 82) 490 | pub const SUSHI: Symbol = Symbol::from_static("SUSHI"); 491 | 492 | /// TAO Perpetual (index: 116) 493 | pub const TAO: Symbol = Symbol::from_static("TAO"); 494 | 495 | /// TIA Perpetual (index: 63) 496 | pub const TIA: Symbol = Symbol::from_static("TIA"); 497 | 498 | /// TNSR Perpetual (index: 124) 499 | pub const TNSR: Symbol = Symbol::from_static("TNSR"); 500 | 501 | /// TON Perpetual (index: 66) 502 | pub const TON: Symbol = Symbol::from_static("TON"); 503 | 504 | /// TRB Perpetual (index: 50) 505 | pub const TRB: Symbol = Symbol::from_static("TRB"); 506 | 507 | /// TRUMP Perpetual (index: 174) 508 | pub const TRUMP: Symbol = Symbol::from_static("TRUMP"); 509 | 510 | /// TRX Perpetual (index: 37) 511 | pub const TRX: Symbol = Symbol::from_static("TRX"); 512 | 513 | /// TST Perpetual (index: 181) 514 | pub const TST: Symbol = Symbol::from_static("TST"); 515 | 516 | /// TURBO Perpetual (index: 133) 517 | pub const TURBO: Symbol = Symbol::from_static("TURBO"); 518 | 519 | /// UMA Perpetual (index: 105) 520 | pub const UMA: Symbol = Symbol::from_static("UMA"); 521 | 522 | /// UNI Perpetual (index: 39) 523 | pub const UNI: Symbol = Symbol::from_static("UNI"); 524 | 525 | /// UNIBOT Perpetual (index: 35) 526 | pub const UNIBOT: Symbol = Symbol::from_static("UNIBOT"); 527 | 528 | /// USTC Perpetual (index: 88) 529 | pub const USTC: Symbol = Symbol::from_static("USTC"); 530 | 531 | /// USUAL Perpetual (index: 164) 532 | pub const USUAL: Symbol = Symbol::from_static("USUAL"); 533 | 534 | /// VINE Perpetual (index: 177) 535 | pub const VINE: Symbol = Symbol::from_static("VINE"); 536 | 537 | /// VIRTUAL Perpetual (index: 162) 538 | pub const VIRTUAL: Symbol = Symbol::from_static("VIRTUAL"); 539 | 540 | /// VVV Perpetual (index: 178) 541 | pub const VVV: Symbol = Symbol::from_static("VVV"); 542 | 543 | /// W Perpetual (index: 111) 544 | pub const W: Symbol = Symbol::from_static("W"); 545 | 546 | /// WCT Perpetual (index: 190) 547 | pub const WCT: Symbol = Symbol::from_static("WCT"); 548 | 549 | /// WIF Perpetual (index: 98) 550 | pub const WIF: Symbol = Symbol::from_static("WIF"); 551 | 552 | /// WLD Perpetual (index: 31) 553 | pub const WLD: Symbol = Symbol::from_static("WLD"); 554 | 555 | /// XAI Perpetual (index: 103) 556 | pub const XAI: Symbol = Symbol::from_static("XAI"); 557 | 558 | /// XLM Perpetual (index: 154) 559 | pub const XLM: Symbol = Symbol::from_static("XLM"); 560 | 561 | /// XRP Perpetual (index: 25) 562 | pub const XRP: Symbol = Symbol::from_static("XRP"); 563 | 564 | /// YGG Perpetual (index: 36) 565 | pub const YGG: Symbol = Symbol::from_static("YGG"); 566 | 567 | /// ZEN Perpetual (index: 79) 568 | pub const ZEN: Symbol = Symbol::from_static("ZEN"); 569 | 570 | /// ZEREBRO Perpetual (index: 168) 571 | pub const ZEREBRO: Symbol = Symbol::from_static("ZEREBRO"); 572 | 573 | /// ZETA Perpetual (index: 108) 574 | pub const ZETA: Symbol = Symbol::from_static("ZETA"); 575 | 576 | /// ZK Perpetual (index: 136) 577 | pub const ZK: Symbol = Symbol::from_static("ZK"); 578 | 579 | /// ZORA Perpetual (index: 192) 580 | pub const ZORA: Symbol = Symbol::from_static("ZORA"); 581 | 582 | /// ZRO Perpetual (index: 46) 583 | pub const ZRO: Symbol = Symbol::from_static("ZRO"); 584 | 585 | /// kBONK Perpetual (index: 85) 586 | pub const KBONK: Symbol = Symbol::from_static("kBONK"); 587 | 588 | /// kDOGS Perpetual (index: 141) 589 | pub const KDOGS: Symbol = Symbol::from_static("kDOGS"); 590 | 591 | /// kFLOKI Perpetual (index: 119) 592 | pub const KFLOKI: Symbol = Symbol::from_static("kFLOKI"); 593 | 594 | /// kLUNC Perpetual (index: 91) 595 | pub const KLUNC: Symbol = Symbol::from_static("kLUNC"); 596 | 597 | /// kNEIRO Perpetual (index: 148) 598 | pub const KNEIRO: Symbol = Symbol::from_static("kNEIRO"); 599 | 600 | /// kPEPE Perpetual (index: 15) 601 | pub const KPEPE: Symbol = Symbol::from_static("kPEPE"); 602 | 603 | /// kSHIB Perpetual (index: 38) 604 | pub const KSHIB: Symbol = Symbol::from_static("kSHIB"); 605 | 606 | // ==================== MAINNET SPOT PAIRS ==================== 607 | 608 | /// ADHD/USDC Spot (index: 40, @40) 609 | pub const ADHD_USDC: Symbol = Symbol::from_static("@40"); 610 | 611 | /// ANON/USDC Spot (index: 166, @166) 612 | pub const ANON_USDC: Symbol = Symbol::from_static("@166"); 613 | 614 | /// ANSEM/USDC Spot (index: 18, @18) 615 | pub const ANSEM_USDC: Symbol = Symbol::from_static("@18"); 616 | 617 | /// ANT/USDC Spot (index: 55, @55) 618 | pub const ANT_USDC: Symbol = Symbol::from_static("@55"); 619 | 620 | /// ARI/USDC Spot (index: 53, @53) 621 | pub const ARI_USDC: Symbol = Symbol::from_static("@53"); 622 | 623 | /// ASI/USDC Spot (index: 36, @36) 624 | pub const ASI_USDC: Symbol = Symbol::from_static("@36"); 625 | 626 | /// ATEHUN/USDC Spot (index: 51, @51) 627 | pub const ATEHUN_USDC: Symbol = Symbol::from_static("@51"); 628 | 629 | /// AUTIST/USDC Spot (index: 93, @93) 630 | pub const AUTIST_USDC: Symbol = Symbol::from_static("@93"); 631 | 632 | /// BAGS/USDC Spot (index: 17, @17) 633 | pub const BAGS_USDC: Symbol = Symbol::from_static("@17"); 634 | 635 | /// BEATS/USDC Spot (index: 128, @128) 636 | pub const BEATS_USDC: Symbol = Symbol::from_static("@128"); 637 | 638 | /// BERA/USDC Spot (index: 115, @115) 639 | pub const BERA_USDC: Symbol = Symbol::from_static("@115"); 640 | 641 | /// BID/USDC Spot (index: 33, @33) 642 | pub const BID_USDC: Symbol = Symbol::from_static("@33"); 643 | 644 | /// BIGBEN/USDC Spot (index: 25, @25) 645 | pub const BIGBEN_USDC: Symbol = Symbol::from_static("@25"); 646 | 647 | /// BOZO/USDC Spot (index: 76, @76) 648 | pub const BOZO_USDC: Symbol = Symbol::from_static("@76"); 649 | 650 | /// BUBZ/USDC Spot (index: 117, @117) 651 | pub const BUBZ_USDC: Symbol = Symbol::from_static("@117"); 652 | 653 | /// BUDDY/USDC Spot (index: 155, @155) 654 | pub const BUDDY_USDC: Symbol = Symbol::from_static("@155"); 655 | 656 | /// BUSSY/USDC Spot (index: 81, @81) 657 | pub const BUSSY_USDC: Symbol = Symbol::from_static("@81"); 658 | 659 | /// CAPPY/USDC Spot (index: 7, @7) 660 | pub const CAPPY_USDC: Symbol = Symbol::from_static("@7"); 661 | 662 | /// CAT/USDC Spot (index: 124, @124) 663 | pub const CAT_USDC: Symbol = Symbol::from_static("@124"); 664 | 665 | /// CATBAL/USDC Spot (index: 59, @59) 666 | pub const CATBAL_USDC: Symbol = Symbol::from_static("@59"); 667 | 668 | /// CATNIP/USDC Spot (index: 26, @26) 669 | pub const CATNIP_USDC: Symbol = Symbol::from_static("@26"); 670 | 671 | /// CHEF/USDC Spot (index: 106, @106) 672 | pub const CHEF_USDC: Symbol = Symbol::from_static("@106"); 673 | 674 | /// CHINA/USDC Spot (index: 68, @68) 675 | pub const CHINA_USDC: Symbol = Symbol::from_static("@68"); 676 | 677 | /// CINDY/USDC Spot (index: 67, @67) 678 | pub const CINDY_USDC: Symbol = Symbol::from_static("@67"); 679 | 680 | /// COOK/USDC Spot (index: 164, @164) 681 | pub const COOK_USDC: Symbol = Symbol::from_static("@164"); 682 | 683 | /// COPE/USDC Spot (index: 102, @102) 684 | pub const COPE_USDC: Symbol = Symbol::from_static("@102"); 685 | 686 | /// COZY/USDC Spot (index: 52, @52) 687 | pub const COZY_USDC: Symbol = Symbol::from_static("@52"); 688 | 689 | /// CZ/USDC Spot (index: 16, @16) 690 | pub const CZ_USDC: Symbol = Symbol::from_static("@16"); 691 | 692 | /// DEFIN/USDC Spot (index: 143, @143) 693 | pub const DEFIN_USDC: Symbol = Symbol::from_static("@143"); 694 | 695 | /// DEPIN/USDC Spot (index: 126, @126) 696 | pub const DEPIN_USDC: Symbol = Symbol::from_static("@126"); 697 | 698 | /// DIABLO/USDC Spot (index: 159, @159) 699 | pub const DIABLO_USDC: Symbol = Symbol::from_static("@159"); 700 | 701 | /// DROP/USDC Spot (index: 46, @46) 702 | pub const DROP_USDC: Symbol = Symbol::from_static("@46"); 703 | 704 | /// EARTH/USDC Spot (index: 97, @97) 705 | pub const EARTH_USDC: Symbol = Symbol::from_static("@97"); 706 | 707 | /// FARM/USDC Spot (index: 121, @121) 708 | pub const FARM_USDC: Symbol = Symbol::from_static("@121"); 709 | 710 | /// FARMED/USDC Spot (index: 30, @30) 711 | pub const FARMED_USDC: Symbol = Symbol::from_static("@30"); 712 | 713 | /// FATCAT/USDC Spot (index: 82, @82) 714 | pub const FATCAT_USDC: Symbol = Symbol::from_static("@82"); 715 | 716 | /// FEIT/USDC Spot (index: 89, @89) 717 | pub const FEIT_USDC: Symbol = Symbol::from_static("@89"); 718 | 719 | /// FEUSD/USDC Spot (index: 149, @149) 720 | pub const FEUSD_USDC: Symbol = Symbol::from_static("@149"); 721 | 722 | /// FLASK/USDC Spot (index: 122, @122) 723 | pub const FLASK_USDC: Symbol = Symbol::from_static("@122"); 724 | 725 | /// FLY/USDC Spot (index: 135, @135) 726 | pub const FLY_USDC: Symbol = Symbol::from_static("@135"); 727 | 728 | /// FRAC/USDC Spot (index: 50, @50) 729 | pub const FRAC_USDC: Symbol = Symbol::from_static("@50"); 730 | 731 | /// FRCT/USDC Spot (index: 167, @167) 732 | pub const FRCT_USDC: Symbol = Symbol::from_static("@167"); 733 | 734 | /// FRIED/USDC Spot (index: 70, @70) 735 | pub const FRIED_USDC: Symbol = Symbol::from_static("@70"); 736 | 737 | /// FRUDO/USDC Spot (index: 90, @90) 738 | pub const FRUDO_USDC: Symbol = Symbol::from_static("@90"); 739 | 740 | /// FUCKY/USDC Spot (index: 15, @15) 741 | pub const FUCKY_USDC: Symbol = Symbol::from_static("@15"); 742 | 743 | /// FUN/USDC Spot (index: 41, @41) 744 | pub const FUN_USDC: Symbol = Symbol::from_static("@41"); 745 | 746 | /// FUND/USDC Spot (index: 158, @158) 747 | pub const FUND_USDC: Symbol = Symbol::from_static("@158"); 748 | 749 | /// G/USDC Spot (index: 75, @75) 750 | pub const G_USDC: Symbol = Symbol::from_static("@75"); 751 | 752 | /// GENESY/USDC Spot (index: 116, @116) 753 | pub const GENESY_USDC: Symbol = Symbol::from_static("@116"); 754 | 755 | /// GMEOW/USDC Spot (index: 10, @10) 756 | pub const GMEOW_USDC: Symbol = Symbol::from_static("@10"); 757 | 758 | /// GOD/USDC Spot (index: 139, @139) 759 | pub const GOD_USDC: Symbol = Symbol::from_static("@139"); 760 | 761 | /// GPT/USDC Spot (index: 31, @31) 762 | pub const GPT_USDC: Symbol = Symbol::from_static("@31"); 763 | 764 | /// GUESS/USDC Spot (index: 61, @61) 765 | pub const GUESS_USDC: Symbol = Symbol::from_static("@61"); 766 | 767 | /// GUP/USDC Spot (index: 29, @29) 768 | pub const GUP_USDC: Symbol = Symbol::from_static("@29"); 769 | 770 | /// H/USDC Spot (index: 131, @131) 771 | pub const H_USDC: Symbol = Symbol::from_static("@131"); 772 | 773 | /// HAPPY/USDC Spot (index: 22, @22) 774 | pub const HAPPY_USDC: Symbol = Symbol::from_static("@22"); 775 | 776 | /// HBOOST/USDC Spot (index: 27, @27) 777 | pub const HBOOST_USDC: Symbol = Symbol::from_static("@27"); 778 | 779 | /// HEAD/USDC Spot (index: 141, @141) 780 | pub const HEAD_USDC: Symbol = Symbol::from_static("@141"); 781 | 782 | /// HFUN/USDC Spot (index: 1, @1) 783 | pub const HFUN_USDC: Symbol = Symbol::from_static("@1"); 784 | 785 | /// HGOD/USDC Spot (index: 95, @95) 786 | pub const HGOD_USDC: Symbol = Symbol::from_static("@95"); 787 | 788 | /// HODL/USDC Spot (index: 34, @34) 789 | pub const HODL_USDC: Symbol = Symbol::from_static("@34"); 790 | 791 | /// HOLD/USDC Spot (index: 113, @113) 792 | pub const HOLD_USDC: Symbol = Symbol::from_static("@113"); 793 | 794 | /// HOP/USDC Spot (index: 100, @100) 795 | pub const HOP_USDC: Symbol = Symbol::from_static("@100"); 796 | 797 | /// HOPE/USDC Spot (index: 80, @80) 798 | pub const HOPE_USDC: Symbol = Symbol::from_static("@80"); 799 | 800 | /// HORSY/USDC Spot (index: 174, @174) 801 | pub const HORSY_USDC: Symbol = Symbol::from_static("@174"); 802 | 803 | /// HPEPE/USDC Spot (index: 44, @44) 804 | pub const HPEPE_USDC: Symbol = Symbol::from_static("@44"); 805 | 806 | /// HPUMP/USDC Spot (index: 64, @64) 807 | pub const HPUMP_USDC: Symbol = Symbol::from_static("@64"); 808 | 809 | /// HPYH/USDC Spot (index: 103, @103) 810 | pub const HPYH_USDC: Symbol = Symbol::from_static("@103"); 811 | 812 | /// HWTR/USDC Spot (index: 138, @138) 813 | pub const HWTR_USDC: Symbol = Symbol::from_static("@138"); 814 | 815 | /// HYENA/USDC Spot (index: 125, @125) 816 | pub const HYENA_USDC: Symbol = Symbol::from_static("@125"); 817 | 818 | /// HYPE/USDC Spot (index: 105, @105) 819 | pub const HYPE_USDC: Symbol = Symbol::from_static("@105"); 820 | 821 | /// ILIENS/USDC Spot (index: 14, @14) 822 | pub const ILIENS_USDC: Symbol = Symbol::from_static("@14"); 823 | 824 | /// JEET/USDC Spot (index: 45, @45) 825 | pub const JEET_USDC: Symbol = Symbol::from_static("@45"); 826 | 827 | /// JEFF/USDC Spot (index: 4, @4) 828 | pub const JEFF_USDC: Symbol = Symbol::from_static("@4"); 829 | 830 | /// JPEG/USDC Spot (index: 144, @144) 831 | pub const JPEG_USDC: Symbol = Symbol::from_static("@144"); 832 | 833 | /// KOBE/USDC Spot (index: 21, @21) 834 | pub const KOBE_USDC: Symbol = Symbol::from_static("@21"); 835 | 836 | /// LADY/USDC Spot (index: 42, @42) 837 | pub const LADY_USDC: Symbol = Symbol::from_static("@42"); 838 | 839 | /// LAUNCH/USDC Spot (index: 120, @120) 840 | pub const LAUNCH_USDC: Symbol = Symbol::from_static("@120"); 841 | 842 | /// LICK/USDC Spot (index: 2, @2) 843 | pub const LICK_USDC: Symbol = Symbol::from_static("@2"); 844 | 845 | /// LIQD/USDC Spot (index: 130, @130) 846 | pub const LIQD_USDC: Symbol = Symbol::from_static("@130"); 847 | 848 | /// LIQUID/USDC Spot (index: 96, @96) 849 | pub const LIQUID_USDC: Symbol = Symbol::from_static("@96"); 850 | 851 | /// LORA/USDC Spot (index: 58, @58) 852 | pub const LORA_USDC: Symbol = Symbol::from_static("@58"); 853 | 854 | /// LQNA/USDC Spot (index: 85, @85) 855 | pub const LQNA_USDC: Symbol = Symbol::from_static("@85"); 856 | 857 | /// LUCKY/USDC Spot (index: 101, @101) 858 | pub const LUCKY_USDC: Symbol = Symbol::from_static("@101"); 859 | 860 | /// MAGA/USDC Spot (index: 94, @94) 861 | pub const MAGA_USDC: Symbol = Symbol::from_static("@94"); 862 | 863 | /// MANLET/USDC Spot (index: 3, @3) 864 | pub const MANLET_USDC: Symbol = Symbol::from_static("@3"); 865 | 866 | /// MAXI/USDC Spot (index: 62, @62) 867 | pub const MAXI_USDC: Symbol = Symbol::from_static("@62"); 868 | 869 | /// MBAPPE/USDC Spot (index: 47, @47) 870 | pub const MBAPPE_USDC: Symbol = Symbol::from_static("@47"); 871 | 872 | /// MEOW/USDC Spot (index: 110, @110) 873 | pub const MEOW_USDC: Symbol = Symbol::from_static("@110"); 874 | 875 | /// MOG/USDC Spot (index: 43, @43) 876 | pub const MOG_USDC: Symbol = Symbol::from_static("@43"); 877 | 878 | /// MON/USDC Spot (index: 127, @127) 879 | pub const MON_USDC: Symbol = Symbol::from_static("@127"); 880 | 881 | /// MONAD/USDC Spot (index: 79, @79) 882 | pub const MONAD_USDC: Symbol = Symbol::from_static("@79"); 883 | 884 | /// MUNCH/USDC Spot (index: 114, @114) 885 | pub const MUNCH_USDC: Symbol = Symbol::from_static("@114"); 886 | 887 | /// NASDAQ/USDC Spot (index: 86, @86) 888 | pub const NASDAQ_USDC: Symbol = Symbol::from_static("@86"); 889 | 890 | /// NEIRO/USDC Spot (index: 111, @111) 891 | pub const NEIRO_USDC: Symbol = Symbol::from_static("@111"); 892 | 893 | /// NFT/USDC Spot (index: 56, @56) 894 | pub const NFT_USDC: Symbol = Symbol::from_static("@56"); 895 | 896 | /// NIGGO/USDC Spot (index: 99, @99) 897 | pub const NIGGO_USDC: Symbol = Symbol::from_static("@99"); 898 | 899 | /// NMTD/USDC Spot (index: 63, @63) 900 | pub const NMTD_USDC: Symbol = Symbol::from_static("@63"); 901 | 902 | /// NOCEX/USDC Spot (index: 71, @71) 903 | pub const NOCEX_USDC: Symbol = Symbol::from_static("@71"); 904 | 905 | /// OMNIX/USDC Spot (index: 73, @73) 906 | pub const OMNIX_USDC: Symbol = Symbol::from_static("@73"); 907 | 908 | /// ORA/USDC Spot (index: 129, @129) 909 | pub const ORA_USDC: Symbol = Symbol::from_static("@129"); 910 | 911 | /// OTTI/USDC Spot (index: 171, @171) 912 | pub const OTTI_USDC: Symbol = Symbol::from_static("@171"); 913 | 914 | /// PANDA/USDC Spot (index: 38, @38) 915 | pub const PANDA_USDC: Symbol = Symbol::from_static("@38"); 916 | 917 | /// PEAR/USDC Spot (index: 112, @112) 918 | pub const PEAR_USDC: Symbol = Symbol::from_static("@112"); 919 | 920 | /// PEG/USDC Spot (index: 162, @162) 921 | pub const PEG_USDC: Symbol = Symbol::from_static("@162"); 922 | 923 | /// PENIS/USDC Spot (index: 160, @160) 924 | pub const PENIS_USDC: Symbol = Symbol::from_static("@160"); 925 | 926 | /// PEPE/USDC Spot (index: 11, @11) 927 | pub const PEPE_USDC: Symbol = Symbol::from_static("@11"); 928 | 929 | /// PERP/USDC Spot (index: 168, @168) 930 | pub const PERP_USDC: Symbol = Symbol::from_static("@168"); 931 | 932 | /// PICKL/USDC Spot (index: 118, @118) 933 | pub const PICKL_USDC: Symbol = Symbol::from_static("@118"); 934 | 935 | /// PIGEON/USDC Spot (index: 65, @65) 936 | pub const PIGEON_USDC: Symbol = Symbol::from_static("@65"); 937 | 938 | /// PILL/USDC Spot (index: 39, @39) 939 | pub const PILL_USDC: Symbol = Symbol::from_static("@39"); 940 | 941 | /// PIP/USDC Spot (index: 84, @84) 942 | pub const PIP_USDC: Symbol = Symbol::from_static("@84"); 943 | 944 | /// POINTS/USDC Spot (index: 8, @8) 945 | pub const POINTS_USDC: Symbol = Symbol::from_static("@8"); 946 | 947 | /// PRFI/USDC Spot (index: 156, @156) 948 | pub const PRFI_USDC: Symbol = Symbol::from_static("@156"); 949 | 950 | /// PUMP/USDC Spot (index: 20, @20) 951 | pub const PUMP_USDC: Symbol = Symbol::from_static("@20"); 952 | 953 | /// PURR/USDC Spot (index: 0, @0) 954 | pub const PURR_USDC: Symbol = Symbol::from_static("@0"); 955 | 956 | /// PURRO/USDC Spot (index: 169, @169) 957 | pub const PURRO_USDC: Symbol = Symbol::from_static("@169"); 958 | 959 | /// PURRPS/USDC Spot (index: 32, @32) 960 | pub const PURRPS_USDC: Symbol = Symbol::from_static("@32"); 961 | 962 | /// QUANT/USDC Spot (index: 150, @150) 963 | pub const QUANT_USDC: Symbol = Symbol::from_static("@150"); 964 | 965 | /// RAGE/USDC Spot (index: 49, @49) 966 | pub const RAGE_USDC: Symbol = Symbol::from_static("@49"); 967 | 968 | /// RANK/USDC Spot (index: 72, @72) 969 | pub const RANK_USDC: Symbol = Symbol::from_static("@72"); 970 | 971 | /// RAT/USDC Spot (index: 152, @152) 972 | pub const RAT_USDC: Symbol = Symbol::from_static("@152"); 973 | 974 | /// RETARD/USDC Spot (index: 109, @109) 975 | pub const RETARD_USDC: Symbol = Symbol::from_static("@109"); 976 | 977 | /// RICH/USDC Spot (index: 57, @57) 978 | pub const RICH_USDC: Symbol = Symbol::from_static("@57"); 979 | 980 | /// RIP/USDC Spot (index: 74, @74) 981 | pub const RIP_USDC: Symbol = Symbol::from_static("@74"); 982 | 983 | /// RISE/USDC Spot (index: 66, @66) 984 | pub const RISE_USDC: Symbol = Symbol::from_static("@66"); 985 | 986 | /// RUB/USDC Spot (index: 165, @165) 987 | pub const RUB_USDC: Symbol = Symbol::from_static("@165"); 988 | 989 | /// RUG/USDC Spot (index: 13, @13) 990 | pub const RUG_USDC: Symbol = Symbol::from_static("@13"); 991 | 992 | /// SCHIZO/USDC Spot (index: 23, @23) 993 | pub const SCHIZO_USDC: Symbol = Symbol::from_static("@23"); 994 | 995 | /// SELL/USDC Spot (index: 24, @24) 996 | pub const SELL_USDC: Symbol = Symbol::from_static("@24"); 997 | 998 | /// SENT/USDC Spot (index: 133, @133) 999 | pub const SENT_USDC: Symbol = Symbol::from_static("@133"); 1000 | 1001 | /// SHEEP/USDC Spot (index: 119, @119) 1002 | pub const SHEEP_USDC: Symbol = Symbol::from_static("@119"); 1003 | 1004 | /// SHOE/USDC Spot (index: 78, @78) 1005 | pub const SHOE_USDC: Symbol = Symbol::from_static("@78"); 1006 | 1007 | /// SHREK/USDC Spot (index: 83, @83) 1008 | pub const SHREK_USDC: Symbol = Symbol::from_static("@83"); 1009 | 1010 | /// SIX/USDC Spot (index: 5, @5) 1011 | pub const SIX_USDC: Symbol = Symbol::from_static("@5"); 1012 | 1013 | /// SOLV/USDC Spot (index: 134, @134) 1014 | pub const SOLV_USDC: Symbol = Symbol::from_static("@134"); 1015 | 1016 | /// SOVRN/USDC Spot (index: 137, @137) 1017 | pub const SOVRN_USDC: Symbol = Symbol::from_static("@137"); 1018 | 1019 | /// SPH/USDC Spot (index: 77, @77) 1020 | pub const SPH_USDC: Symbol = Symbol::from_static("@77"); 1021 | 1022 | /// STACK/USDC Spot (index: 69, @69) 1023 | pub const STACK_USDC: Symbol = Symbol::from_static("@69"); 1024 | 1025 | /// STAR/USDC Spot (index: 132, @132) 1026 | pub const STAR_USDC: Symbol = Symbol::from_static("@132"); 1027 | 1028 | /// STEEL/USDC Spot (index: 108, @108) 1029 | pub const STEEL_USDC: Symbol = Symbol::from_static("@108"); 1030 | 1031 | /// STRICT/USDC Spot (index: 92, @92) 1032 | pub const STRICT_USDC: Symbol = Symbol::from_static("@92"); 1033 | 1034 | /// SUCKY/USDC Spot (index: 28, @28) 1035 | pub const SUCKY_USDC: Symbol = Symbol::from_static("@28"); 1036 | 1037 | /// SYLVI/USDC Spot (index: 88, @88) 1038 | pub const SYLVI_USDC: Symbol = Symbol::from_static("@88"); 1039 | 1040 | /// TATE/USDC Spot (index: 19, @19) 1041 | pub const TATE_USDC: Symbol = Symbol::from_static("@19"); 1042 | 1043 | /// TEST/USDC Spot (index: 48, @48) 1044 | pub const TEST_USDC: Symbol = Symbol::from_static("@48"); 1045 | 1046 | /// TILT/USDC Spot (index: 153, @153) 1047 | pub const TILT_USDC: Symbol = Symbol::from_static("@153"); 1048 | 1049 | /// TIME/USDC Spot (index: 136, @136) 1050 | pub const TIME_USDC: Symbol = Symbol::from_static("@136"); 1051 | 1052 | /// TJIF/USDC Spot (index: 60, @60) 1053 | pub const TJIF_USDC: Symbol = Symbol::from_static("@60"); 1054 | 1055 | /// TREND/USDC Spot (index: 154, @154) 1056 | pub const TREND_USDC: Symbol = Symbol::from_static("@154"); 1057 | 1058 | /// TRUMP/USDC Spot (index: 9, @9) 1059 | pub const TRUMP_USDC: Symbol = Symbol::from_static("@9"); 1060 | 1061 | /// UBTC/USDC Spot (index: 140, @140) 1062 | pub const UBTC_USDC: Symbol = Symbol::from_static("@140"); 1063 | 1064 | /// UETH/USDC Spot (index: 147, @147) 1065 | pub const UETH_USDC: Symbol = Symbol::from_static("@147"); 1066 | 1067 | /// UFART/USDC Spot (index: 157, @157) 1068 | pub const UFART_USDC: Symbol = Symbol::from_static("@157"); 1069 | 1070 | /// UP/USDC Spot (index: 98, @98) 1071 | pub const UP_USDC: Symbol = Symbol::from_static("@98"); 1072 | 1073 | /// USDE/USDC Spot (index: 146, @146) 1074 | pub const USDE_USDC: Symbol = Symbol::from_static("@146"); 1075 | 1076 | /// USDHL/USDC Spot (index: 172, @172) 1077 | pub const USDHL_USDC: Symbol = Symbol::from_static("@172"); 1078 | 1079 | /// USDT0/USDC Spot (index: 161, @161) 1080 | pub const USDT0_USDC: Symbol = Symbol::from_static("@161"); 1081 | 1082 | /// USDXL/USDC Spot (index: 148, @148) 1083 | pub const USDXL_USDC: Symbol = Symbol::from_static("@148"); 1084 | 1085 | /// USH/USDC Spot (index: 163, @163) 1086 | pub const USH_USDC: Symbol = Symbol::from_static("@163"); 1087 | 1088 | /// USOL/USDC Spot (index: 151, @151) 1089 | pub const USOL_USDC: Symbol = Symbol::from_static("@151"); 1090 | 1091 | /// USR/USDC Spot (index: 170, @170) 1092 | pub const USR_USDC: Symbol = Symbol::from_static("@170"); 1093 | 1094 | /// VAPOR/USDC Spot (index: 37, @37) 1095 | pub const VAPOR_USDC: Symbol = Symbol::from_static("@37"); 1096 | 1097 | /// VAULT/USDC Spot (index: 123, @123) 1098 | pub const VAULT_USDC: Symbol = Symbol::from_static("@123"); 1099 | 1100 | /// VEGAS/USDC Spot (index: 35, @35) 1101 | pub const VEGAS_USDC: Symbol = Symbol::from_static("@35"); 1102 | 1103 | /// VIZN/USDC Spot (index: 91, @91) 1104 | pub const VIZN_USDC: Symbol = Symbol::from_static("@91"); 1105 | 1106 | /// VORTX/USDC Spot (index: 142, @142) 1107 | pub const VORTX_USDC: Symbol = Symbol::from_static("@142"); 1108 | 1109 | /// WAGMI/USDC Spot (index: 6, @6) 1110 | pub const WAGMI_USDC: Symbol = Symbol::from_static("@6"); 1111 | 1112 | /// WASH/USDC Spot (index: 54, @54) 1113 | pub const WASH_USDC: Symbol = Symbol::from_static("@54"); 1114 | 1115 | /// WHYPI/USDC Spot (index: 145, @145) 1116 | pub const WHYPI_USDC: Symbol = Symbol::from_static("@145"); 1117 | 1118 | /// WOW/USDC Spot (index: 107, @107) 1119 | pub const WOW_USDC: Symbol = Symbol::from_static("@107"); 1120 | 1121 | /// XAUT0/USDC Spot (index: 173, @173) 1122 | pub const XAUT0_USDC: Symbol = Symbol::from_static("@173"); 1123 | 1124 | /// XULIAN/USDC Spot (index: 12, @12) 1125 | pub const XULIAN_USDC: Symbol = Symbol::from_static("@12"); 1126 | 1127 | /// YAP/USDC Spot (index: 104, @104) 1128 | pub const YAP_USDC: Symbol = Symbol::from_static("@104"); 1129 | 1130 | /// YEETI/USDC Spot (index: 87, @87) 1131 | pub const YEETI_USDC: Symbol = Symbol::from_static("@87"); 1132 | 1133 | // ==================== TESTNET ==================== 1134 | // Only major assets included for testnet development 1135 | 1136 | // ==================== TESTNET PERPETUALS ==================== 1137 | 1138 | /// APT Perpetual (testnet, index: 1) 1139 | pub const TEST_APT: Symbol = Symbol::from_static("APT"); 1140 | 1141 | /// ARB Perpetual (testnet, index: 13) 1142 | pub const TEST_ARB: Symbol = Symbol::from_static("ARB"); 1143 | 1144 | /// ATOM Perpetual (testnet, index: 2) 1145 | pub const TEST_ATOM: Symbol = Symbol::from_static("ATOM"); 1146 | 1147 | /// AVAX Perpetual (testnet, index: 7) 1148 | pub const TEST_AVAX: Symbol = Symbol::from_static("AVAX"); 1149 | 1150 | /// BNB Perpetual (testnet, index: 6) 1151 | pub const TEST_BNB: Symbol = Symbol::from_static("BNB"); 1152 | 1153 | /// BTC Perpetual (testnet, index: 3) 1154 | pub const TEST_BTC: Symbol = Symbol::from_static("BTC"); 1155 | 1156 | /// ETH Perpetual (testnet, index: 4) 1157 | pub const TEST_ETH: Symbol = Symbol::from_static("ETH"); 1158 | 1159 | /// MATIC Perpetual (testnet, index: 5) 1160 | pub const TEST_MATIC: Symbol = Symbol::from_static("MATIC"); 1161 | 1162 | /// OP Perpetual (testnet, index: 11) 1163 | pub const TEST_OP: Symbol = Symbol::from_static("OP"); 1164 | 1165 | /// SOL Perpetual (testnet, index: 0) 1166 | pub const TEST_SOL: Symbol = Symbol::from_static("SOL"); 1167 | 1168 | /// SUI Perpetual (testnet, index: 25) 1169 | pub const TEST_SUI: Symbol = Symbol::from_static("SUI"); 1170 | 1171 | // ==================== TESTNET SPOT PAIRS ==================== 1172 | 1173 | /// BTC/USDC Spot (testnet, index: 35, @35) 1174 | pub const TEST_BTC_USDC: Symbol = Symbol::from_static("@35"); 1175 | 1176 | // ==================== HELPERS ==================== 1177 | 1178 | /// USDC - convenience constant for the quote currency 1179 | /// Note: This is not a tradeable symbol itself, but useful for clarity 1180 | pub const USDC: Symbol = Symbol::from_static("USDC"); 1181 | 1182 | /// Create a new symbol at runtime (for assets not yet in the SDK) 1183 | /// 1184 | /// # Example 1185 | /// ``` 1186 | /// use ferrofluid::types::symbols::symbol; 1187 | /// 1188 | /// let new_coin = symbol("NEWCOIN"); 1189 | /// let new_spot = symbol("@999"); 1190 | /// ``` 1191 | pub fn symbol(s: impl Into) -> Symbol { 1192 | Symbol::from(s.into()) 1193 | } 1194 | 1195 | // ==================== PRELUDE ==================== 1196 | 1197 | /// Commonly used symbols for easy importing 1198 | /// 1199 | /// # Example 1200 | /// ``` 1201 | /// use ferrofluid::types::symbols::prelude::*; 1202 | /// 1203 | /// // Now you can use BTC, ETH, etc. directly 1204 | /// assert_eq!(BTC.as_str(), "BTC"); 1205 | /// assert_eq!(HYPE_USDC.as_str(), "@105"); 1206 | /// 1207 | /// // Create runtime symbols 1208 | /// let new_coin = symbol("NEWCOIN"); 1209 | /// assert_eq!(new_coin.as_str(), "NEWCOIN"); 1210 | /// ``` 1211 | pub mod prelude { 1212 | pub use super::{ 1213 | // Popular alts 1214 | APT, 1215 | ARB, 1216 | AVAX, 1217 | BNB, 1218 | // Major perpetuals 1219 | BTC, 1220 | DOGE, 1221 | 1222 | ETH, 1223 | // Hyperliquid native 1224 | HYPE, 1225 | // Major spot pairs 1226 | HYPE_USDC, 1227 | INJ, 1228 | KPEPE, 1229 | 1230 | MATIC, 1231 | OP, 1232 | PURR, 1233 | 1234 | PURR_USDC, 1235 | 1236 | SEI, 1237 | SOL, 1238 | SUI, 1239 | // Testnet symbols 1240 | TEST_BTC, 1241 | TEST_ETH, 1242 | TEST_SOL, 1243 | 1244 | TIA, 1245 | // Common quote currency 1246 | USDC, 1247 | 1248 | WIF, 1249 | // Runtime symbol creation 1250 | symbol, 1251 | }; 1252 | // Re-export Symbol type for convenience 1253 | pub use crate::types::symbol::Symbol; 1254 | } 1255 | 1256 | #[cfg(test)] 1257 | mod tests { 1258 | use super::*; 1259 | 1260 | #[test] 1261 | fn test_predefined_symbols() { 1262 | assert_eq!(BTC.as_str(), "BTC"); 1263 | assert!(BTC.is_perp()); 1264 | 1265 | assert_eq!(HYPE_USDC.as_str(), "@105"); 1266 | assert!(HYPE_USDC.is_spot()); 1267 | } 1268 | 1269 | #[test] 1270 | fn test_runtime_symbol_creation() { 1271 | let new_perp = symbol("NEWCOIN"); 1272 | assert_eq!(new_perp.as_str(), "NEWCOIN"); 1273 | assert!(new_perp.is_perp()); 1274 | 1275 | let new_spot = symbol("@999"); 1276 | assert_eq!(new_spot.as_str(), "@999"); 1277 | assert!(new_spot.is_spot()); 1278 | } 1279 | 1280 | #[test] 1281 | fn test_prelude_imports() { 1282 | // Test that prelude symbols work 1283 | use crate::types::symbols::prelude::*; 1284 | 1285 | assert_eq!(BTC.as_str(), "BTC"); 1286 | assert_eq!(ETH.as_str(), "ETH"); 1287 | assert_eq!(HYPE_USDC.as_str(), "@105"); 1288 | 1289 | // Test runtime creation through prelude 1290 | let custom = symbol("CUSTOM"); 1291 | assert_eq!(custom.as_str(), "CUSTOM"); 1292 | } 1293 | } 1294 | --------------------------------------------------------------------------------