diff --git a/CHANGELOG.md b/CHANGELOG.md index b0b8030..b90d330 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,14 +4,17 @@ ### Added - WebSocket streaming support for real-time market data -- Subscription APIs for orderbook, trades, candles, and user events -- CLI binary for terminal-based queries (`--features=cli`) -- CLI commands for market data and account management +- Adds complete support for Subscription API +- CLI binary for terminal-based queries (`--features cli`) +- CLI commands for order placement and cancellation - Network selection via `--network` flag (mainnet/testnet) - Environment-based authentication for CLI via `HL_PRIVATE_KEY` ### Changed - Project status: WebSocket and CLI marked as complete +- Removes subscription command line arguments +- Fixes rust_decimal::Decimal serde deserialization for response types. +- Updates AllMids from 'type' to 'struct' to support correct response format. ## [0.1.0] - 2025-12-10 diff --git a/Cargo.lock b/Cargo.lock index bd5179d..a6496d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3309,6 +3309,7 @@ dependencies = [ "rust_decimal", "serde", "serde_json", + "serde_with", "thiserror 1.0.69", "tokio", "tokio-test", diff --git a/Cargo.toml b/Cargo.toml index e0ce08e..bf0dd14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ futures-util = { version = "0.3.31", features = ["sink"] } once_cell = "1.19" reqwest = { version = "=0.12.23", features = ["json", "rustls-tls"] } rmp-serde = "1.3.0" +serde_with = "3" rust_decimal = { version = "1.35", features = ["serde", "serde-float", "serde-str", "serde-with-str"] } serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", features = ["preserve_order"] } diff --git a/README.md b/README.md index 18a6798..e277fa3 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,13 @@ More concretely, our principles are: 1. **Type Safety & Financial Precision**: Every API response, order type, and market data structure is modeled in Rust's type system to catch invalid requests at compile time. We use `rust_decimal` for all financial calculations to eliminate floating-point errors. While network and API errors remain runtime concerns, the library prevents entire classes of invalid requests before they're sent. -2. **Correctness Over Speed**: rhyperliquid prioritizes getting trading operations right. This means proper EIP-712 signing implementation, correct msgpack serialization for action hashing, and careful handling of Hyperliquid's WebSocket heartbeat protocol. We optimize client-side performance where it matters, but understand that network latency to Hyperliquid's servers will dominate round-trip times. +2. **Correctness Over Speed**: rhyperliquid prioritizes getting trading operations right. This means proper EIP-712 signing implementation, correct msgpack serialization for action hashing, and careful handling of Hyperliquid's WebSocket heartbeat protocol. -3. **Modularity**: The crate is structured as composable components. Whether you need just the REST client for backtesting, WebSocket streams for live data, or the full CLI for manual trading, you can depend on only what you need. All public APIs are documented with examples showing real-world usage patterns. +3. **Modularity**: The crate is structured as composable components. Whether you need just the REST client for backtesting, WebSocket streams for live data, or the CLI for manual trading. All public APIs are documented with examples showing real-world usage patterns. 4. **Developer Experience**: We believe trading infrastructure should be approachable. The library provides builder patterns for configuration, comprehensive error messages that explain what went wrong, and examples organized by user workflows rather than individual functions. Both library users and CLI users should find the interface intuitive. -5. **Battle-Tested Foundations**: By leveraging proven crates (tokio for async, reqwest for HTTP, Alloy for Ethereum cryptography), we build on solid foundations rather than reinventing implementations. Our authentication follows Hyperliquid's exact specifications for both testnet and mainnet environments. +5. **Battle-Tested Foundations**: By leveraging proven crates (tokio for async, reqwest for HTTP, Alloy for Ethereum cryptography), we build on solid foundations rather than reinventing implementations. Our authentication follows Hyperliquid's specifications for both testnet and mainnet environments. 6. **Open & Extensible**: rhyperliquid is free open source software licensed under Apache/MIT. This enables anyone to build proprietary strategies, modify the client for their needs, or integrate it into larger systems without licensing concerns. We welcome contributions that improve reliability, add features, or enhance documentation. @@ -59,7 +59,7 @@ async fn main() -> Result<(), Box> { ### CLI Usage -The CLI provides quick access to market data and account information from your terminal. +The CLI provides quick access to market data and account information from your terminal. Below you can find a list of all supported CLI commands. rhyperliquid intentionally only supports place_order and cancel_order (by order id) from the CLI. Other Exchange API commands will be added by request or open source contribution. ```bash # Run CLI commands cargo run --bin cli --features=cli -- [OPTIONS] @@ -90,39 +90,130 @@ cargo run --bin cli --features=cli -- candle-snapshot \ cargo run --bin cli --features=cli -- vault-details --vault-address 0x... ``` -#### CLI Options - -**Global Flags:** -- `--network ` - Network to connect to (default: mainnet) -- `--allow-signer-key-env` - Allow reading private key from `HL_PRIVATE_KEY` environment variable - -**Available Commands:** -- `all-mids` - Get mid prices for all assets -- `open-orders` - Get user's open orders -- `frontend-open-orders` - Get frontend-formatted open orders -- `user-fills` - Get user's fill history -- `user-fills-by-time` - Get fills within time range -- `user-rate-limit` - Check user's rate limit status -- `order-status` - Get status of specific order -- `l2-book` - Get L2 orderbook snapshot -- `candle-snapshot` - Get historical candle data -- `historical-orders` - Get user's historical orders -- `sub-accounts` - Get user's sub-accounts -- `vault-details` - Get vault information -- `user-vault-equities` - Get user's vault equity -- `user-role` - Get user's role information -- `portfolio` - Get user's portfolio -- `referral` - Get user's referral information -- `user-fees` - Get user's fee information +### WebSocket Subscriptions + +Subscribe to real-time data feeds using the WebSocket interface: +```bash +# Subscribe to open orders updates (requires HL_PRIVATE_KEY env var) +export HL_PRIVATE_KEY=your_private_key_here +RUST_LOG=info cargo run --bin cli --features=cli -- \ + --subscriptions true \ + subscribe-open-orders \ + --user 0xYourAddress + +# Subscribe to order book updates +cargo run --bin cli --features=cli -- \ + --subscriptions true \ + subscribe-l2-book \ + --coin BTC + +# Subscribe to all mid prices +cargo run --bin cli --features=cli -- \ + --subscriptions true \ + subscribe-all-mids + +# Subscribe to trades +cargo run --bin cli --features=cli -- \ + --subscriptions true \ + subscribe-trades \ + --coin ETH +``` + +WebSocket subscriptions stream live updates until interrupted with `Ctrl+C`. + +## CLI Reference + +### Global Options + +| Flag | Description | Default | +|------|-------------|---------| +| `--network ` | Network to connect to | `mainnet` | +| `--allow-signer-key-env` | Allow reading private key from `HL_PRIVATE_KEY` | `false` | +| `--subscriptions ` | Enable WebSocket subscription mode | `false` | + +### Commands + +#### Exchange API + +| Command | Description | +|---------|-------------| +| `order` | Place an order | +| `cancel` | Cancel an order by its order id | + +#### Market Data + +| Command | Description | +|---------|-------------| +| `all-mids` | Get mid prices for all assets | +| `l2-book` | Get L2 orderbook snapshot | +| `candle-snapshot` | Get historical candle data | + +#### Account & Portfolio + +| Command | Description | +|---------|-------------| +| `open-orders` | Get user's open orders | +| `frontend-open-orders` | Get frontend-formatted open orders | +| `user-fills` | Get user's fill history | +| `user-fills-by-time` | Get fills within time range | +| `user-rate-limit` | Check user's rate limit status | +| `order-status` | Get status of specific order | +| `historical-orders` | Get user's historical orders | +| `portfolio` | Get user's portfolio | +| `user-fees` | Get user's fee information | +| `sub-accounts` | Get user's sub-accounts | +| `user-role` | Get user's role information | +| `referral` | Get user's referral information | + +#### Vault Operations + +| Command | Description | +|---------|-------------| +| `vault-details` | Get vault information | +| `user-vault-equities` | Get user's vault equity | + +#### WebSocket Subscriptions + +**Market Data Streams** + +| Command | Description | +|---------|-------------| +| `subscribe-all-mids` | Stream all mid prices | +| `subscribe-l2-book` | Stream orderbook updates | +| `subscribe-candle-snapshot` | Stream candle updates | +| `subscribe-active-asset-ctx` | Stream active asset context | +| `subscribe-bbo` | Stream best bid/offer updates | + +**Account & Trading Streams** + +| Command | Description | +|---------|-------------| +| `subscribe-open-orders` | Stream open orders updates | +| `subscribe-user-events` | Stream user trading events | +| `subscribe-user-fills` | Stream user fill updates | +| `subscribe-user-funding` | Stream user funding updates | +| `subscribe-user-non-funding-ledger-updates` | Stream non-funding ledger updates | +| `subscribe-active-asset-data` | Stream active asset data for user | + +**Advanced Streams** + +| Command | Description | +|---------|-------------| +| `subscribe-notifications` | Stream user notifications | +| `subscribe-web-data3` | Stream web data updates | +| `subscribe-twap-states` | Stream TWAP order states | +| `subscribe-clearinghouse-state` | Stream clearinghouse state | +| `subscribe-user-twap-slice-fills` | Stream TWAP slice fills | +| `subscribe-user-twap-history` | Stream TWAP order history | Use `--help` on any command for detailed parameter information: ```bash cargo run --bin cli --features=cli -- l2-book --help ``` -## Trading +## Trading Examples -For order placement and cancellation examples, see [`examples/basic_order.rs`](examples/basic_order.rs). +Comprehensive examples demonstrating real trading workflows: For account transfer examples, see [`examples/account_transfer.rs`](examples/account_transfer.rs). @@ -139,26 +230,22 @@ For TWAP order placement see [`examples/twap_order.rs`](examples/twap_order.rs). Add to your `Cargo.toml`: ```toml [dependencies] -rhyperliquid = "0.1" +rhyperliquid = "0.2" tokio = { version = "1.41", features = ["full"] } ``` ### CLI Installation ```bash -# Install from source with CLI support -cargo install --path . --features=cli --bin cli +# Install from crates.io +cargo install rhyperliquid --features=cli -# Or clone and build +# Or install from source git clone https://github.com/elijahhampton/rhyperliquid.git cd rhyperliquid -cargo build --release --features=cli --bin cli - -# Binary will be at target/release/cli +cargo install --path . --features=cli --bin cli ``` -### From Source - -Clone and build the repository: +### Building from Source ```bash # Clone the repository git clone https://github.com/elijahhampton/rhyperliquid.git @@ -177,13 +264,30 @@ cargo test --all-features cargo doc --open ``` +## Stability and API Guarantees + +This crate is under active development. + +- **Breaking changes** may occur between minor versions (0.1 → 0.2) +- **Public API** is subject to refinement based on usage feedback +- **Testnet testing** is strongly recommended before mainnet use + +## Documentation + +- [API Documentation](https://docs.rs/rhyperliquid) +- [Hyperliquid Official Docs](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api) +- [Examples](./examples) + ## Getting Help -If you have any questions, first see if the answer to your question can be found in the [Hyperliquid Docs](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api). -If the answer is not there: +If you have questions: -- Open a discussion with your question, or -- Open an issue with the bug +1. Check the [Hyperliquid API Documentation](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api) +2. Search existing [GitHub Issues](https://github.com/elijahhampton/rhyperliquid/issues) +3. Open a new [Discussion](https://github.com/elijahhampton/rhyperliquid/discussions) for questions +4. Open an [Issue](https://github.com/elijahhampton/rhyperliquid/issues/new) for bugs + +## Requirements ### Minimum Supported Rust Version (MSRV) @@ -195,3 +299,18 @@ rustc --version # Update if needed rustup update stable ``` + +## License + +Licensed under either of: + +- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +## Contributing + +Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/assets/rhyperliquid.png b/assets/rhyperliquid.png new file mode 100644 index 0000000..8ffaf5f Binary files /dev/null and b/assets/rhyperliquid.png differ diff --git a/examples/advanced_order.rs b/examples/advanced_order.rs index c859ee8..9d466ae 100644 --- a/examples/advanced_order.rs +++ b/examples/advanced_order.rs @@ -35,7 +35,7 @@ async fn main() -> Result<(), Box> { // Get current price let all_mids = info_api.all_mids(None).await?; - let doge_price = all_mids.get(&asset_id).ok_or(HyperliquidError::Internal( + let doge_price = all_mids.0.get(&asset_id).ok_or(HyperliquidError::Internal( "Missing asset in universe".to_string(), ))?; let price_decimal = Decimal::from_str(&doge_price.to_string())?; diff --git a/examples/aligned_quote_token_status.rs b/examples/aligned_quote_token_status.rs index 67879e1..1e2e3ee 100644 --- a/examples/aligned_quote_token_status.rs +++ b/examples/aligned_quote_token_status.rs @@ -1,17 +1,12 @@ #![allow(clippy::all)] -use rhyperliquid::{ - example_helpers::{testnet_client, user}, - init_tracing::init_tracing, -}; +use rhyperliquid::{example_helpers::testnet_client, init_tracing::init_tracing}; #[tokio::main] async fn main() -> Result<(), Box> { init_tracing(); let hyperliquid = testnet_client()?; - let user = user(); - - let status = hyperliquid.info().aligned_quote_token_status(&user).await?; + let status = hyperliquid.info().aligned_quote_token_status(0).await?; tracing::info!("{:?}", status); diff --git a/examples/basic_order.rs b/examples/basic_order.rs index 741a7bf..8f78a0e 100644 --- a/examples/basic_order.rs +++ b/examples/basic_order.rs @@ -37,7 +37,7 @@ async fn main() -> Result<(), Box> { let asset_id = format!("@{}", idx); let all_mids = info_api.all_mids(None).await?; - let doge_price = all_mids.get(&asset_id).ok_or(HyperliquidError::Internal( + let doge_price = all_mids.0.get(&asset_id).ok_or(HyperliquidError::Internal( "Missing asset in universe".to_string(), ))?; diff --git a/examples/leverage_position.rs b/examples/leverage_position.rs index 5de884b..55d12c3 100644 --- a/examples/leverage_position.rs +++ b/examples/leverage_position.rs @@ -36,7 +36,7 @@ async fn main() -> Result<(), Box> { let btc_ctx = &asset_ctxs .get(asset_idx) .ok_or(HyperliquidError::Internal("Asset not found".to_string()))?; - let mark_price = Decimal::from_str(&btc_ctx.mark_px)?; + let mark_price = btc_ctx.mark_px; tracing::info!("BTC mark price: ${}", mark_price); diff --git a/examples/spot_market_data.rs b/examples/spot_market_data.rs index f7ce66c..f195e54 100644 --- a/examples/spot_market_data.rs +++ b/examples/spot_market_data.rs @@ -23,17 +23,17 @@ async fn main() -> Result<(), Box> { .await?; tracing::info!("{:?}", spot_deploy_action_information); - let spot_pair_deploy_auction_information = hyperliquid - .info() - .spot_pair_deploy_auction_information() - .await?; - tracing::info!("{:?}", spot_pair_deploy_auction_information); - - let token_information = hyperliquid - .info() - .token_information("0x00000000000000000000000000000000") - .await?; - tracing::info!("{:?}", token_information); + // let spot_pair_deploy_auction_information = hyperliquid + // .info() + // .spot_pair_deploy_auction_information() + // .await?; + // tracing::info!("{:?}", spot_pair_deploy_auction_information); + + // let token_information = hyperliquid + // .info() + // .token_information("0x00000000000000000000000000000000") + // .await?; + // tracing::info!("{:?}", token_information); Ok(()) } diff --git a/examples/subscriptions.rs b/examples/subscriptions.rs index 7cb3fb3..9769b98 100644 --- a/examples/subscriptions.rs +++ b/examples/subscriptions.rs @@ -13,7 +13,8 @@ async fn main() -> Result<(), Box> { let user = user(); subs.subscribe_all_mids(None).await?; - subs.subscribe_candle("BTC", "5m".to_string()).await?; + subs.subscribe_candle_snapshot("BTC", "5m".to_string()) + .await?; subs.subscribe_l2_book("BTC", None, None).await?; subs.subscribe_trades("BTC").await?; subs.subscribe_notifications(user.clone()).await?; diff --git a/examples/twap_order.rs b/examples/twap_order.rs index 8f751e3..3a9798a 100644 --- a/examples/twap_order.rs +++ b/examples/twap_order.rs @@ -34,7 +34,7 @@ async fn main() -> Result<(), Box> { // Get current price to calculate size let all_mids = info_api.all_mids(None).await?; let default_decimal = Decimal::new(0, 0); - let doge_price = all_mids.get(&asset_id).unwrap_or(&default_decimal); + let doge_price = all_mids.0.get(&asset_id).unwrap_or(&default_decimal); let price_decimal = Decimal::from_str(&doge_price.to_string())?; // Calculate size: $50 notional / price diff --git a/src/api/exchange.rs b/src/api/exchange.rs index ad7fa64..cdcbd43 100644 --- a/src/api/exchange.rs +++ b/src/api/exchange.rs @@ -94,7 +94,7 @@ impl<'client> ExchangeApi<'client> { order: OrderRequest, grouping: Grouping, builder: Option, - vault_address: Option
, + vault_address: Option, expires_after: Option, ) -> Result { self.bulk_orders(vec![order], grouping, builder, vault_address, expires_after) @@ -113,7 +113,7 @@ impl<'client> ExchangeApi<'client> { orders: Vec, grouping: Grouping, builder: Option, - vault_address: Option
, + vault_address: Option, expires_after: Option, ) -> Result { let signer = self @@ -130,7 +130,14 @@ impl<'client> ExchangeApi<'client> { builder, }; - let sig = sign_l1_action(&signer, &action, vault_address, nonce, expires_after, false)?; + let sig = sign_l1_action( + &signer, + &action, + vault_address.clone(), + nonce, + expires_after, + false, + )?; let signature = Eip712Signature { r: format!("0x{:x}", sig.r()), @@ -171,7 +178,7 @@ impl<'client> ExchangeApi<'client> { pub async fn cancel_order( &self, cancel: CancelRequest, - vault_address: Option
, + vault_address: Option, expires_after: Option, ) -> Result { self.bulk_cancel_orders(vec![cancel], vault_address, expires_after) @@ -188,7 +195,7 @@ impl<'client> ExchangeApi<'client> { pub async fn bulk_cancel_orders( &self, cancels: Vec, - vault_address: Option
, + vault_address: Option, expires_after: Option, ) -> Result { let signer = self @@ -203,7 +210,14 @@ impl<'client> ExchangeApi<'client> { let nonce = current_time_millis(); - let sig = sign_l1_action(&signer, &action, vault_address, nonce, expires_after, false)?; + let sig = sign_l1_action( + &signer, + &action, + vault_address.clone(), + nonce, + expires_after, + false, + )?; let signature = Eip712Signature { r: format!("0x{:x}", sig.r()), @@ -247,7 +261,7 @@ impl<'client> ExchangeApi<'client> { pub async fn schedule_cancel( &self, time: Option, - vault_address: Option
, + vault_address: Option, expires_after: Option, ) -> Result { let signer = self @@ -265,7 +279,7 @@ impl<'client> ExchangeApi<'client> { let sig = sign_l1_action( &signer, &action, - vault_address, + vault_address.clone(), nonce, expires_after, self.client.is_mainnet(), @@ -311,7 +325,7 @@ impl<'client> ExchangeApi<'client> { pub async fn modify_an_order( &self, request: ModifyRequest, - vault_address: Option
, + vault_address: Option, expires_after: Option, ) -> Result { let signer = self @@ -330,7 +344,7 @@ impl<'client> ExchangeApi<'client> { let sig = sign_l1_action( &signer, &action, - vault_address, + vault_address.clone(), nonce, expires_after, self.client.is_mainnet(), @@ -375,7 +389,7 @@ impl<'client> ExchangeApi<'client> { pub async fn modify_multiple_orders( &self, requests: Vec, - vault_address: Option
, + vault_address: Option, expires_after: Option, ) -> Result { let signer = self @@ -393,7 +407,7 @@ impl<'client> ExchangeApi<'client> { let sig = sign_l1_action( &signer, &action, - vault_address, + vault_address.clone(), nonce, expires_after, self.client.is_mainnet(), @@ -442,7 +456,7 @@ impl<'client> ExchangeApi<'client> { asset: u32, is_buy: bool, ntli: u32, - vault_address: Option
, + vault_address: Option, expires_after: Option, ) -> Result { let signer = self @@ -462,7 +476,7 @@ impl<'client> ExchangeApi<'client> { let sig = sign_l1_action( &signer, &action, - vault_address, + vault_address.clone(), nonce, expires_after, self.client.is_mainnet(), @@ -511,7 +525,7 @@ impl<'client> ExchangeApi<'client> { asset: u32, is_cross: bool, leverage: u32, - vault_address: Option
, + vault_address: Option, expires_after: Option, ) -> Result { let signer = self @@ -531,7 +545,7 @@ impl<'client> ExchangeApi<'client> { let sig = sign_l1_action( &signer, &action, - vault_address, + vault_address.clone(), nonce, expires_after, self.client.is_mainnet(), @@ -724,7 +738,7 @@ impl<'client> ExchangeApi<'client> { destination_dex: Address, token: String, amount: String, - from_sub_account: Option
, + from_sub_account: Option, ) -> Result { let signer = self .client @@ -733,7 +747,7 @@ impl<'client> ExchangeApi<'client> { let nonce = current_time_millis(); - let from_sub_account_param = from_sub_account.map_or_else(String::new, |v| v.to_string()); + let from_sub_account_param = from_sub_account.unwrap_or_default(); let action = SendAssetAction { type_: "sendAsset".to_string(), @@ -904,7 +918,7 @@ impl<'client> ExchangeApi<'client> { /// * `expires_after` - Optional expiration timestamp in milliseconds pub async fn deposit_or_withdraw_from_a_vault( &self, - vault_address: Address, + vault_address: String, is_deposit: bool, usd_amount: u64, expires_after: Option, @@ -916,7 +930,7 @@ impl<'client> ExchangeApi<'client> { let action = VaultTransferAction { type_: "vaultTransfer".to_string(), - vault_address: vault_address.to_string(), + vault_address: vault_address.clone(), is_deposit, usd: usd_amount, }; @@ -1064,7 +1078,7 @@ impl<'client> ExchangeApi<'client> { pub async fn place_twap_order( &self, request: TwapRequest, - vault_address: Option
, + vault_address: Option, expires_after: Option, ) -> Result { let signer = self @@ -1082,7 +1096,7 @@ impl<'client> ExchangeApi<'client> { let sig = sign_l1_action( &signer, &action, - vault_address, + vault_address.clone(), nonce, expires_after, self.client.is_mainnet(), @@ -1129,7 +1143,7 @@ impl<'client> ExchangeApi<'client> { &self, asset: usize, twap_id: u32, - vault_address: Option
, + vault_address: Option, expires_after: Option, ) -> Result { let signer = self @@ -1148,7 +1162,7 @@ impl<'client> ExchangeApi<'client> { let sig = sign_l1_action( &signer, &action, - vault_address, + vault_address.clone(), nonce, expires_after, self.client.is_mainnet(), diff --git a/src/api/info.rs b/src/api/info.rs index 2620276..d96a433 100644 --- a/src/api/info.rs +++ b/src/api/info.rs @@ -78,7 +78,10 @@ impl<'client> InfoApi<'client> { "dex": json!(dex_param) }); - self.post(payload).await + let all_mids: AllMids = self.post(payload).await?; + let map = all_mids.0; + + Ok(AllMids(map)) } /// Retrieves a user's open orders. @@ -569,10 +572,13 @@ impl<'client> InfoApi<'client> { self.post(payload).await } - pub async fn aligned_quote_token_status(&self, user: &str) -> Result { + pub async fn aligned_quote_token_status( + &self, + token: u32, + ) -> Result> { let payload = json!({ "type": "alignedQuoteTokenInfo", - "user": user + "token": token }); self.post(payload).await diff --git a/src/api/subscription/mod.rs b/src/api/subscription/mod.rs index de20205..5845388 100644 --- a/src/api/subscription/mod.rs +++ b/src/api/subscription/mod.rs @@ -1,4 +1,3 @@ -mod sender; mod ws; pub use ws::{StreamMessage, SubscriptionClient, SubscriptionConfig}; diff --git a/src/api/subscription/sender.rs b/src/api/subscription/sender.rs deleted file mode 100644 index f79a301..0000000 --- a/src/api/subscription/sender.rs +++ /dev/null @@ -1,70 +0,0 @@ -use crate::types::ws::{ - WsActiveAssetCtx, WsActiveAssetData, WsAllMids, WsBbo, WsBook, WsCandle, WsClearinghouseState, - WsNotification, WsOpenOrders, WsTrade, WsTwapStates, WsUserEvent, WsUserFills, WsUserFundings, - WsUserNonFundingLedgerUpdate, WsUserTwapHistory, WsUserTwapSliceFills, WsWebData3, -}; -use tokio::sync::broadcast::Sender; - -/// A mapping of subscription identifiers to sender channels and subscription -/// parameters. -#[derive(Clone, Default)] -pub struct StreamSenders { - pub(crate) all_mids: (Option>, Option), - pub(crate) candle: (Option>, Option, Option), - pub(crate) trades: ( - Option>, - Option, - Option, - Option, - ), - pub(crate) l2book: ( - Option>, - Option, - Option, - Option, - ), - pub(crate) notifications: (Option>, Option), - pub(crate) webdata3: (Option>, Option), - pub(crate) twap_states: (Option>, Option), - pub(crate) clearinghouse_state: (Option>, Option), - pub(crate) open_orders: (Option>, Option), - pub(crate) user_events: (Option>, Option), - pub(crate) user_fills: (Option>, Option), - pub(crate) user_funding: (Option>, Option), - pub(crate) user_non_funding_ledger_updates: - (Option>, Option), - pub(crate) active_asset_ctx: (Option>, Option), - pub(crate) active_asset_data: ( - Option>, - Option, - Option, - ), - pub(crate) user_twap_slice_fills: (Option>, Option), - pub(crate) user_twap_history: (Option>, Option), - pub(crate) bbo: (Option>, Option), -} - -impl StreamSenders { - pub fn new() -> Self { - Self { - all_mids: (None, None), - candle: (None, None, None), - trades: (None, None, None, None), - l2book: (None, None, None, None), - notifications: (None, None), - webdata3: (None, None), - twap_states: (None, None), - clearinghouse_state: (None, None), - open_orders: (None, None), - user_events: (None, None), - user_fills: (None, None), - user_funding: (None, None), - user_non_funding_ledger_updates: (None, None), - active_asset_ctx: (None, None), - active_asset_data: (None, None, None), - user_twap_slice_fills: (None, None), - user_twap_history: (None, None), - bbo: (None, None), - } - } -} diff --git a/src/api/subscription/ws.rs b/src/api/subscription/ws.rs index 04bc320..9840b0e 100644 --- a/src/api/subscription/ws.rs +++ b/src/api/subscription/ws.rs @@ -52,8 +52,8 @@ pub enum SubscriptionSpec { }, L2Book { coin: String, - n_sig_figs: Option, - mantissa: Option, + n_sig_figs: Option, + mantissa: Option, }, Trades { coin: String, @@ -514,7 +514,7 @@ impl SubscriptionClient { /// /// # Returns /// A bounded `tokio::sync::broadcast::Sender` - pub async fn subscribe_candle( + pub async fn subscribe_candle_snapshot( &mut self, coin: impl Into + Serialize + Clone, interval: String, @@ -564,8 +564,8 @@ impl SubscriptionClient { pub async fn subscribe_l2_book( &mut self, coin: impl Into + Serialize, - n_sig_figs: Option, - mantissa: Option, + n_sig_figs: Option, + mantissa: Option, ) -> Result<()> { let coin = coin.into(); let spec = SubscriptionSpec::L2Book { diff --git a/src/bin/cli.rs b/src/bin/cli.rs index e4571d3..25d7932 100644 --- a/src/bin/cli.rs +++ b/src/bin/cli.rs @@ -1,32 +1,27 @@ #![allow(unused_imports, clippy::too_many_lines)] -use alloy::signers::local::LocalSigner; +use alloy::{hex, signers::local::LocalSigner}; +use anyhow::anyhow; use clap::{Parser, Subcommand}; use rhyperliquid::{ cli::{Cli, Commands}, init_tracing::init_tracing, - types::info::user::CandleSnapshotRequest, + types::{info::user::CandleSnapshotRequest, ws::SubscriptionResponse}, HyperliquidClientBuilder, }; use std::env; #[tokio::main] async fn main() -> Result<(), Box> { - #[cfg(feature = "cli")] init_tracing(); - #[cfg(feature = "cli")] let cli = Cli::parse(); #[allow(clippy::expect_used)] - #[cfg(feature = "cli")] - let signer = env::var("HL_PRIVATE_KEY").expect("HL_PRIVATE_KEY env var is missing"); - - #[cfg(feature = "cli")] let mut hyperliquid = &mut HyperliquidClientBuilder::new(); // Check if the user provided a network, default to testnet - #[cfg(feature = "cli")] if let Some(network) = cli.network { + let network: String = network; match network.to_lowercase().as_str() { "testnet" => { hyperliquid = hyperliquid.testnet(); @@ -40,34 +35,43 @@ async fn main() -> Result<(), Box> { hyperliquid = hyperliquid.testnet(); } + // Enable subscriptions API + hyperliquid = hyperliquid.with_subscriptions(); + // Check if user provides permission to check env var for signer key - #[cfg(feature = "cli")] if let Some(signer_permission) = cli.allow_signer_key_env { if signer_permission { - hyperliquid = hyperliquid.with_wallet(LocalSigner::from_slice(signer.as_bytes())?); + let signer_str = std::env::var("HL_PRIVATE_KEY")?; + + let key_hex = signer_str.strip_prefix("0x").unwrap_or(&signer_str); + let key_bytes = hex::decode(key_hex)?; + + hyperliquid = hyperliquid.with_wallet(LocalSigner::from_slice(&key_bytes)?); } } - #[cfg(feature = "cli")] let client = hyperliquid.build()?; - #[cfg(feature = "cli")] let info_api = &client.info(); + let exchange_api = &client.exchange(); + let mut subs = client.subscriptions().await?; - #[cfg(feature = "cli")] match cli.command { Commands::AllMids { dex } => { let all_mids = info_api.all_mids(dex).await?; tracing::info!("{:?}", all_mids); + return Ok(()); } Commands::OpenOrders { user, dex } => { let open_orders = info_api.open_orders(&user, dex.as_deref()).await?; tracing::info!("{:?}", open_orders); + return Ok(()); } Commands::FrontendOpenOrders { user, dex } => { let frontend_open_orders = info_api .open_orders_with_additional_info(&user, dex.as_deref()) .await?; tracing::info!("{:?}", frontend_open_orders); + return Ok(()); } Commands::UserFills { user, @@ -75,6 +79,7 @@ async fn main() -> Result<(), Box> { } => { let user_fills = info_api.fills(&user, aggregate_by_time).await?; tracing::info!({"{:?}", user_fills}); + return Ok(()); } Commands::UserFillsByTime { user, @@ -86,16 +91,19 @@ async fn main() -> Result<(), Box> { .fills_by_time(&user, start_time, end_time, aggregate_by_time) .await?; tracing::info!("{:?}", fills_by_time); + return Ok(()); } Commands::UserRateLimit { user } => { let user_rate_limit = info_api.rate_limits(&user).await?; tracing::info!("{:?}", user_rate_limit); + return Ok(()); } Commands::OrderStatus { user, oid } => { let order_status = info_api .order_status(&user, rhyperliquid::types::info::OrderId::Numeric(oid)) .await?; tracing::info!("{:?}", order_status); + return Ok(()); } Commands::L2Book { coin, @@ -106,6 +114,7 @@ async fn main() -> Result<(), Box> { .l2_book_snapshot(&coin, n_sig_figs, mantissa) .await?; tracing::info!("{:?}", l2_book); + return Ok(()); } Commands::CandleSnapshot { coin, @@ -123,10 +132,12 @@ async fn main() -> Result<(), Box> { .await?; tracing::info!("{:?}", snapshot); + return Ok(()); } Commands::HistoricalOrders { user } => { let historical_orders = info_api.historical_orders(&user).await?; tracing::info!("{:?}", historical_orders); + return Ok(()); } Commands::SubAccounts { user } => { let sub_accounts = info_api.subaccounts(&user).await?; @@ -135,6 +146,7 @@ async fn main() -> Result<(), Box> { } else { tracing::info!("User {:?} does not have subaccounts.", user); } + return Ok(()); } Commands::VaultDetails { vault_address, @@ -144,28 +156,197 @@ async fn main() -> Result<(), Box> { .vault_details(&vault_address, user.as_deref()) .await?; tracing::info!("{:?}", vault_details); + return Ok(()); } Commands::UserVaultEquities { user } => { let equities = info_api.vault_deposits(&user).await?; tracing::info!("{:?}", equities); + return Ok(()); } Commands::UserRole { user } => { let role = info_api.role(&user).await?; tracing::info!("{:?}", role); + return Ok(()); } Commands::Portfolio { user } => { let portfolio = info_api.portfolio(&user).await?; tracing::info!("{:?}", portfolio); + return Ok(()); } Commands::Referral { user } => { let referral = info_api.portfolio(&user).await?; tracing::info!("{:?}", referral); + return Ok(()); } Commands::UserFees { user } => { let user_fees = info_api.fees(&user).await?; tracing::info!("{:?}", user_fees); + return Ok(()); + } + Commands::Order(cmd_args) => { + let meta = info_api.spot_metadata().await?; + + let asset_idx = meta + .tokens + .iter() + .position(|asset| asset.name == cmd_args.coin()) + .ok_or_else(|| anyhow!("Unknown coin: {}", cmd_args.coin()))?; + + let (order, grouping, builder, vault, expires) = + cmd_args.build(u32::try_from(asset_idx).expect("index conversion to fit into u32")); + + exchange_api + .place_order(order, grouping, builder, vault, expires) + .await?; + } + Commands::Cancel(cmd_args) => { + let meta = info_api.spot_metadata().await?; + + let asset_idx = meta + .tokens + .iter() + .position(|asset| asset.name == cmd_args.coin()) + .ok_or_else(|| anyhow!("Unknown coin: {}", cmd_args.coin()))?; + + let (cancel_req, vault_address, expires_after) = + cmd_args.build(u32::try_from(asset_idx).expect("index conversion to fit into u32")); + + exchange_api + .cancel_order(cancel_req, vault_address, expires_after) + .await?; + } + Commands::SubscribeOpenOrders { user } => { + subs.subscribe_open_orders(user).await?; + } + Commands::SubscribeAllMids { dex } => { + subs.subscribe_all_mids(dex).await?; + } + Commands::SubscribeL2Book { + coin, + n_sig_figs, + mantissa, + } => { + subs.subscribe_l2_book(coin, n_sig_figs, mantissa).await?; + } + Commands::SubscribeCandleSnapshot { coin, interval } => { + subs.subscribe_candle_snapshot(coin, interval).await?; + } + Commands::SubscribeNotifications { user } => { + subs.subscribe_notifications(user).await?; + } + Commands::SubscribeWebData3 { user } => { + subs.subscribe_webdata3(user).await?; + } + Commands::SubscribeTwapStates { user } => { + subs.subscribe_twap_states(user).await?; + } + Commands::SubscribeClearinghouseState { user } => { + subs.subscribe_clearinghouse_state(user).await?; + } + Commands::SubscribeUserEvents { user } => { + subs.subscribe_user_events(user).await?; + } + Commands::SubscribeUserFills { user } => { + subs.subscribe_user_fills(user).await?; + } + Commands::SubscribeUserFunding { user } => { + subs.subscribe_user_funding(user).await?; + } + Commands::SubscribeUserNonFundingLedgerUpdates { user } => { + subs.subscribe_user_non_funding_ledger_updates(user).await?; + } + Commands::SubscribeActiveAssetCtx { coin } => { + subs.subscribe_active_asset_ctx(coin).await?; + } + Commands::SubscribeActiveAssetData { user, coin } => { + subs.subscribe_active_asset_data(user, coin).await?; + } + Commands::SubscribeUserTwapSliceFills { user } => { + subs.subscribe_user_twap_slice_fills(user).await?; + } + Commands::SubscribeUserTwapHistory { user } => { + subs.subscribe_user_twap_history(user).await?; + } + Commands::SubscribeBbo { user } => { + subs.subscribe_bbo(user).await?; } } - Ok(()) + let mut events = subs.events; + + loop { + match events.recv().await? { + SubscriptionResponse::AllMids(ws_all_mids) => { + tracing::info!("{}", serde_json::to_string_pretty(&ws_all_mids)?); + } + SubscriptionResponse::L2Book(ws_l2_book) => { + tracing::info!("{}", serde_json::to_string_pretty(&ws_l2_book)?); + } + SubscriptionResponse::Candle(ws_candle) => { + tracing::info!("{}", serde_json::to_string_pretty(&ws_candle)?); + } + SubscriptionResponse::Notification(ws_notification) => { + tracing::info!("{}", serde_json::to_string_pretty(&ws_notification)?); + } + SubscriptionResponse::WebData3(ws_webdata3) => { + tracing::info!("{}", serde_json::to_string_pretty(&ws_webdata3)?); + } + SubscriptionResponse::TwapStates(ws_twap_states) => { + tracing::info!("{}", serde_json::to_string_pretty(&ws_twap_states)?); + } + SubscriptionResponse::ClearinghouseState(ws_clearinghouse_state) => { + tracing::info!("{}", serde_json::to_string_pretty(&ws_clearinghouse_state)?); + } + SubscriptionResponse::OpenOrders(ws_open_orders) => { + tracing::info!("{}", serde_json::to_string_pretty(&ws_open_orders)?); + } + SubscriptionResponse::UserEvents(ws_user_events) => { + tracing::info!("{}", serde_json::to_string_pretty(&ws_user_events)?); + } + SubscriptionResponse::UserFills(ws_fills) => { + tracing::info!("{}", serde_json::to_string_pretty(&ws_fills)?); + } + SubscriptionResponse::UserFundings(ws_fundings) => { + tracing::info!("{}", serde_json::to_string_pretty(&ws_fundings)?); + } + SubscriptionResponse::UserNonFundingLedgerUpdates( + ws_user_non_funding_ledger_updates, + ) => { + tracing::info!( + "{}", + serde_json::to_string_pretty(&ws_user_non_funding_ledger_updates)? + ); + } + SubscriptionResponse::ActiveAssetCtx(ws_active_asset_ctx) => { + tracing::info!("{}", serde_json::to_string_pretty(&ws_active_asset_ctx)?); + } + SubscriptionResponse::ActiveAssetData(ws_active_asset_data) => { + tracing::info!("{}", serde_json::to_string_pretty(&ws_active_asset_data)?); + } + SubscriptionResponse::UserTwapSliceFills(ws_user_twap_slice_fills) => { + tracing::info!( + "{}", + serde_json::to_string_pretty(&ws_user_twap_slice_fills)? + ); + } + SubscriptionResponse::UserTwapHistory(ws_user_twap_history) => { + tracing::info!("{}", serde_json::to_string_pretty(&ws_user_twap_history)?); + } + SubscriptionResponse::Bbo(ws_bbo) => { + tracing::info!("{}", serde_json::to_string_pretty(&ws_bbo)?); + } + SubscriptionResponse::Error(ws_error_response) => { + tracing::info!("{:?}", serde_json::to_string_pretty(&ws_error_response)); + } + SubscriptionResponse::SubscriptionResponse(subscription_confirmation) => { + tracing::info!("Subscription confirmed {:?}", subscription_confirmation); + } + SubscriptionResponse::Pong => { + tracing::info!("Received `pong` response from the server"); + } + SubscriptionResponse::Trades(ws_trades) => { + tracing::info!("{:?}", ws_trades); + } + } + } } diff --git a/src/cli/arguments.rs b/src/cli/arguments.rs new file mode 100644 index 0000000..4b6e651 --- /dev/null +++ b/src/cli/arguments.rs @@ -0,0 +1,173 @@ +use crate::cli::OrderRequest; +use crate::types::exchange::order::OrderSide; +use crate::types::exchange::{ + Builder, CancelRequest, Grouping, LimitOrder, Tif, Tpsl, TriggerOrder, +}; +#[allow(unused_imports)] +use clap::{Args, Command, Parser, ValueEnum}; +use serde::{Deserialize, Serialize}; + +/// CLI specific order type argument for order request. +#[derive(ValueEnum, Clone, Serialize, Deserialize)] +pub enum OrderTypeArg { + #[serde(rename = "limit")] + Limit, + #[serde(rename = "trigger")] + Trigger, +} + +/// Command for placing an order +#[derive(Args)] +pub struct OrderCmd { + coin: String, + #[arg(value_enum)] + side: OrderSide, + size: String, + + // Order type (limit vs trigger) + #[arg(long, default_value = "limit")] + #[arg(value_enum)] + order_type: OrderTypeArg, + + // Limit order fields + #[arg(long, required_if_eq("order_type", "limit"))] + price: Option, + + #[arg(long, default_value = "gtc")] + #[arg(value_enum)] + tif: Tif, + + // Trigger order fields + #[arg(long, required_if_eq("order_type", "trigger"))] + trigger_price: Option, + + #[arg(long, requires = "trigger_price")] + #[arg(value_enum)] + tpsl: Option, + + #[arg(long)] + trigger_market: bool, + + // Optional flags + #[arg(long)] + reduce_only: bool, + + #[arg(long)] + cloid: Option, + + // Function params + #[arg(long, default_value = "na")] + #[arg(value_enum)] + grouping: Grouping, + + #[arg(long)] + vault_address: Option, + + #[arg(long)] + expires_after: Option, + + // Builder fee + #[arg(long)] + builder_address: Option, + + #[arg(long, requires = "builder_address")] + builder_fee: Option, +} + +impl OrderCmd { + /// Builds the order request using the `asset_idx` and + /// the command line arguments. + pub fn build( + self, + asset_idx: u32, + ) -> ( + OrderRequest, + Grouping, + Option, + Option, + Option, + ) { + use crate::types::exchange::{OrderRequest, OrderType}; + + let order_type = match self.order_type { + OrderTypeArg::Limit => OrderType::Limit(LimitOrder { tif: self.tif }), + OrderTypeArg::Trigger => OrderType::Trigger(TriggerOrder { + is_market: self.trigger_market, + trigger_px: self + .trigger_price + .clone() + .expect("--trigger price required for trigger orders"), + tpsl: self.tpsl.expect("--tpsl required for trigger orders"), + }), + }; + + let builder = self.builder_address.map(|b| Builder { + b, + f: self.builder_fee.expect(""), + }); + + let price = match self.order_type { + OrderTypeArg::Limit => self.price.ok_or("--price required for limit orders"), + OrderTypeArg::Trigger => Ok(self.trigger_price.clone().unwrap_or_default()), + }; + + let order = OrderRequest { + a: asset_idx, + b: matches!(self.side, OrderSide::Buy), + p: price.expect("--price required for limit orders"), + s: self.size, + r: self.reduce_only, + t: order_type, + c: self.cloid, + }; + + ( + order, + self.grouping, + builder, + self.vault_address, + self.expires_after, + ) + } + + pub fn coin(&self) -> &str { + &self.coin + } +} + +/// Cmd arguments for a cancel order request +#[derive(Args)] +pub struct CancelOrderByOidCmd { + /// Name of coin + coin: String, + + /// Order id + o: u64, + + /// Asset index + #[arg(long)] + a: Option, + + #[arg(long)] + vault_address: Option, + + #[arg(long)] + expires_after: Option, +} + +impl CancelOrderByOidCmd { + /// Builds the order request using the `asset_idx` and + /// the command line arguments. + pub fn build(self, asset_idx: u32) -> (CancelRequest, Option, Option) { + let cancel_req = CancelRequest { + a: self.a.unwrap_or(asset_idx), + o: self.o, + }; + + (cancel_req, self.vault_address, self.expires_after) + } + + pub fn coin(&self) -> &str { + &self.coin + } +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index d461700..03298a4 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,11 +1,23 @@ +mod arguments; + +pub use arguments::{CancelOrderByOidCmd, OrderCmd, OrderTypeArg}; + +#[allow(unused_imports)] +use crate::types::exchange::OrderRequest; use clap::{Parser, Subcommand}; #[derive(Subcommand)] pub enum Commands { + Order(OrderCmd), + Cancel(CancelOrderByOidCmd), AllMids { #[arg(short, long)] dex: Option, }, + SubscribeAllMids { + #[arg(long)] + dex: Option, + }, OpenOrders { #[arg(short, long)] user: String, @@ -52,6 +64,14 @@ pub enum Commands { #[arg(short, long)] mantissa: Option, }, + SubscribeL2Book { + #[arg(short, long)] + coin: String, + #[arg(short, long)] + n_sig_figs: Option, + #[arg(short, long)] + mantissa: Option, + }, CandleSnapshot { #[arg(short, long)] coin: String, @@ -62,6 +82,12 @@ pub enum Commands { #[arg(short, long)] end_time: u64, }, + SubscribeCandleSnapshot { + #[arg(short, long)] + coin: String, + #[arg(short, long)] + interval: String, + }, HistoricalOrders { #[arg(short, long)] user: String, @@ -96,13 +122,73 @@ pub enum Commands { #[arg(short, long)] user: String, }, + SubscribeNotifications { + #[arg(short, long)] + user: String, + }, + SubscribeWebData3 { + #[arg(short, long)] + user: String, + }, + SubscribeTwapStates { + #[arg(short, long)] + user: String, + }, + SubscribeClearinghouseState { + #[arg(short, long)] + user: String, + }, + SubscribeOpenOrders { + #[arg(short, long)] + user: String, + }, + SubscribeUserEvents { + #[arg(short, long)] + user: String, + }, + SubscribeUserFills { + #[arg(short, long)] + user: String, + }, + SubscribeUserFunding { + #[arg(short, long)] + user: String, + }, + SubscribeUserNonFundingLedgerUpdates { + #[arg(short, long)] + user: String, + }, + SubscribeActiveAssetCtx { + #[arg(short, long)] + coin: String, + }, + SubscribeActiveAssetData { + #[arg(short, long)] + user: String, + #[arg(short, long)] + coin: String, + }, + SubscribeUserTwapSliceFills { + #[arg(short, long)] + user: String, + }, + SubscribeUserTwapHistory { + #[arg(short, long)] + user: String, + }, + SubscribeBbo { + #[arg(short, long)] + user: String, + }, } #[derive(Parser)] pub struct Cli { + /// Picks a specific network, i.e. 'testnet' or 'mainnet' #[arg(short, long)] pub network: Option, + /// Allows CLI to use `HL_PRIVATE_KEY` env var in configuration (requires `HL_PRIVATE_KEY` end var) #[arg(short, long)] pub allow_signer_key_env: Option, diff --git a/src/init_tracing.rs b/src/init_tracing.rs index f9cbdf3..4bb279a 100644 --- a/src/init_tracing.rs +++ b/src/init_tracing.rs @@ -1,6 +1,6 @@ #[allow(dead_code)] pub fn init_tracing() { tracing_subscriber::fmt() - .with_max_level(tracing::Level::TRACE) + .with_max_level(tracing::Level::DEBUG) .init(); } diff --git a/src/signature/sign.rs b/src/signature/sign.rs index e7c391e..4c99b61 100644 --- a/src/signature/sign.rs +++ b/src/signature/sign.rs @@ -45,7 +45,7 @@ where pub fn sign_l1_action( wallet: &PrivateKeySigner, action: &impl Serialize, - vault_address: Option
, + vault_address: Option, nonce: u64, expires_after: Option, is_mainnet: bool, @@ -114,7 +114,7 @@ pub fn sign_l1_action( /// Returns the resulting `B256` hash. fn action_hash( action: &impl Serialize, - vault_address: Option
, + vault_address: Option, nonce: u64, expires_after: Option, ) -> Result { @@ -124,7 +124,7 @@ fn action_hash( if let Some(addr) = vault_address { data.push(0x01); - data.extend_from_slice(addr.as_slice()); + data.extend_from_slice(addr.as_bytes()); } else { data.push(0x00); } diff --git a/src/types/exchange/mod.rs b/src/types/exchange/mod.rs index bbb8ae8..05158f2 100644 --- a/src/types/exchange/mod.rs +++ b/src/types/exchange/mod.rs @@ -8,8 +8,8 @@ pub use order::{ BatchModifyAction, Builder, CDepositAction, CWithdrawAction, CancelAction, CancelByCloidAction, CancelByCloidRequest, CancelRequest, Cloid, Grouping, LimitOrder, ModifyAction, ModifyRequest, NoopAction, OrderAction, OrderRequest, OrderType, ReserveRequestWeightAction, - ScheduleCancelAction, SendAssetAction, SpotSendAction, Tif, TokenDelegateAction, TriggerOrder, - TwapCancelAction, TwapOrderAction, TwapRequest, UpdateIsolatedMarginAction, + ScheduleCancelAction, SendAssetAction, SpotSendAction, Tif, TokenDelegateAction, Tpsl, + TriggerOrder, TwapCancelAction, TwapOrderAction, TwapRequest, UpdateIsolatedMarginAction, UpdateLeverageAction, UsdClassTransferAction, UsdSendAction, UserDexAbstractionAction, ValidatorL1StreamAction, VaultTransferAction, WithdrawAction, }; diff --git a/src/types/exchange/order.rs b/src/types/exchange/order.rs index 86f78b0..b92f5ad 100644 --- a/src/types/exchange/order.rs +++ b/src/types/exchange/order.rs @@ -1,5 +1,6 @@ use crate::types::serialize::serialize_chain_id_as_hex; use crate::{signature::eip712::Eip712, types::info::perpetual::AssetInfo}; +use clap::ValueEnum; /// Request and response types for the exchange endpoint used to interact /// with and trade on the Hyperliquid chain. @@ -27,7 +28,7 @@ fn eip_712_domain(chain_id: u64) -> Eip712Domain { pub type Cloid = String; /// Time-in-force for limit orders -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(ValueEnum, Debug, Clone, Serialize, Deserialize)] pub enum Tif { /// Add liquidity only (post only) #[serde(rename = "Alo")] @@ -57,7 +58,7 @@ pub struct TriggerOrder { pub tpsl: Tpsl, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(ValueEnum, Debug, Clone, Serialize, Deserialize)] pub enum Tpsl { #[serde(rename = "tp")] Tp, @@ -75,6 +76,15 @@ pub enum OrderType { Trigger(TriggerOrder), } +/// Order side, i.e. buy or sell for the order +#[derive(ValueEnum, Clone, Serialize, Deserialize)] +pub enum OrderSide { + #[serde(rename = "buy")] + Buy, + #[serde(rename = "sell")] + Sell, +} + /// Builder fee configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Builder { @@ -133,7 +143,7 @@ impl OrderRequest { } /// Grouping type for orders -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(ValueEnum, Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum Grouping { #[serde(rename = "na")] diff --git a/src/types/info/perpetual.rs b/src/types/info/perpetual.rs index 5a33ece..da3c768 100644 --- a/src/types/info/perpetual.rs +++ b/src/types/info/perpetual.rs @@ -1,3 +1,4 @@ +use rust_decimal::Decimal; /// Response types for the info endpoints that are specific to perpetuals. /// Additional information for endpoint responses can be found /// here: `` @@ -61,15 +62,23 @@ pub struct PerpetualsMetadata { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AssetContext { - pub day_ntl_vlm: String, - pub funding: String, + #[serde(with = "rust_decimal::serde::str")] + pub day_ntl_vlm: Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub funding: Decimal, pub impact_pxs: Option<[String; 2]>, - pub mark_px: String, - pub mid_px: Option, - pub open_interest: String, - pub oracle_px: String, - pub premium: Option, - pub prev_day_px: String, + #[serde(with = "rust_decimal::serde::str")] + pub mark_px: Decimal, + #[serde(with = "rust_decimal::serde::str_option")] + pub mid_px: Option, + #[serde(with = "rust_decimal::serde::str")] + pub open_interest: Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub oracle_px: Decimal, + #[serde(with = "rust_decimal::serde::str_option")] + pub premium: Option, + #[serde(with = "rust_decimal::serde::str")] + pub prev_day_px: Decimal, } pub type MetaAndAssetContexts = (PerpetualsMetadata, Vec); @@ -209,23 +218,28 @@ pub struct ActiveAssetData { pub leverage: Leverage, pub max_trade_szs: [String; 2], pub available_to_trade: [String; 2], - pub mark_px: String, + #[serde(with = "rust_decimal::serde::str")] + pub mark_px: Decimal, } /// Response type for POST /info with type "perpDexLimits" #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PerpDexLimits { - pub total_oi_cap: String, - pub oi_sz_cap_per_perp: String, - pub max_transfer_ntl: String, + #[serde(with = "rust_decimal::serde::str")] + pub total_oi_cap: Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub oi_sz_cap_per_perp: Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub max_transfer_ntl: Decimal, /// Array of [coin, cap] pairs - pub coin_to_oi_cap: Vec<(String, String)>, + pub coin_to_oi_cap: Vec<(Decimal, Decimal)>, } /// Response type for POST /info with type "perpDexStatus" #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PerpDexStatus { - pub total_net_deposit: String, + #[serde(with = "rust_decimal::serde::str")] + pub total_net_deposit: Decimal, } diff --git a/src/types/info/spot.rs b/src/types/info/spot.rs index 05ae0c1..f33cd7b 100644 --- a/src/types/info/spot.rs +++ b/src/types/info/spot.rs @@ -1,19 +1,19 @@ -use rust_decimal::Decimal; /// Response types for the info endpoints that are specific to spot. /// Additional information for endpoint responses can be found /// here: `` +use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct EvmContract { - address: String, + pub address: String, #[serde( default, alias = "evm_extra_wei_decimals", alias = "evmExtraWeiDecimals" )] - evm_extra_wei_decimals: i64, + pub evm_extra_wei_decimals: i64, } /// Response type for POST /info with type "spotMeta" @@ -28,6 +28,7 @@ pub struct SpotToken { pub is_canonical: bool, pub evm_contract: Option, pub full_name: Option, + #[serde(with = "rust_decimal::serde::str")] pub deployer_trading_fee_share: Decimal, } @@ -51,9 +52,13 @@ pub struct SpotMetadata { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SpotAssetContext { + #[serde(with = "rust_decimal::serde::str")] pub day_ntl_vlm: Decimal, + #[serde(with = "rust_decimal::serde::str_option")] pub mark_px: Option, + #[serde(with = "rust_decimal::serde::str_option")] pub mid_px: Option, + #[serde(with = "rust_decimal::serde::str_option")] pub prev_day_px: Option, } @@ -65,8 +70,11 @@ pub type SpotMetaAndAssetContexts = (SpotMetadata, Vec); pub struct SpotBalance { pub coin: String, pub token: i64, + #[serde(with = "rust_decimal::serde::str")] pub hold: Decimal, + #[serde(with = "rust_decimal::serde::str")] pub total: Decimal, + #[serde(with = "rust_decimal::serde::str")] pub entry_ntl: Decimal, } @@ -106,8 +114,11 @@ pub struct DeployState { pub struct GasAuction { pub start_time_seconds: u64, pub duration_seconds: u64, + #[serde(with = "rust_decimal::serde::str")] pub start_gas: Decimal, + #[serde(with = "rust_decimal::serde::str_option")] pub current_gas: Option, + #[serde(with = "rust_decimal::serde::str")] pub end_gas: Decimal, } diff --git a/src/types/info/user.rs b/src/types/info/user.rs index 0ce9880..0049b90 100644 --- a/src/types/info/user.rs +++ b/src/types/info/user.rs @@ -5,10 +5,13 @@ use rust_decimal::{self, Decimal}; use serde::de::Error as _; use serde::{Deserialize, Deserializer, Serialize}; +use serde_with::{serde_as, DisplayFromStr}; use std::collections::HashMap; /// Response to "allMids" request type. -pub type AllMids = HashMap; +#[serde_as] +#[derive(Debug, Deserialize)] +pub struct AllMids(#[serde_as(as = "HashMap<_, DisplayFromStr>")] pub HashMap); /// Request for "openOrders" request type. #[derive(Debug, Serialize)] @@ -26,10 +29,11 @@ pub struct OpenOrdersRequest { #[derive(Debug, Deserialize)] pub struct OpenOrder { pub coin: String, - #[serde(rename = "limitPx")] + #[serde(rename = "limitPx", with = "rust_decimal::serde::str")] pub limit_px: Decimal, pub oid: u64, pub side: String, + #[serde(with = "rust_decimal::serde::str")] pub sz: Decimal, pub timestamp: u64, } @@ -56,21 +60,22 @@ pub struct FrontendOpenOrder { pub is_position_tpsl: bool, #[serde(rename = "isTrigger")] pub is_trigger: bool, - #[serde(rename = "limitPx")] + #[serde(rename = "limitPx", with = "rust_decimal::serde::str")] pub limit_px: Decimal, pub oid: u64, #[serde(rename = "orderType")] pub order_type: String, - #[serde(rename = "origSz")] + #[serde(rename = "origSz", with = "rust_decimal::serde::str")] pub orig_sz: Decimal, #[serde(rename = "reduceOnly")] pub reduce_only: bool, pub side: String, + #[serde(with = "rust_decimal::serde::str")] pub sz: Decimal, pub timestamp: u64, #[serde(rename = "triggerCondition")] pub trigger_condition: String, - #[serde(rename = "triggerPx")] + #[serde(rename = "triggerPx", with = "rust_decimal::serde::str")] pub trigger_px: Decimal, } @@ -94,7 +99,7 @@ pub struct UserFillsRequest { #[derive(Debug, Deserialize)] pub struct PerpetualFill { - #[serde(rename = "closedPnl")] + #[serde(rename = "closedPnl", with = "rust_decimal::serde::str")] pub closed_pnl: Decimal, /// Refer to `` for more information /// on how asset IDs work. @@ -103,13 +108,16 @@ pub struct PerpetualFill { pub dir: String, pub hash: String, pub oid: u64, + #[serde(with = "rust_decimal::serde::str")] pub px: Decimal, pub side: String, - #[serde(rename = "startPosition")] + #[serde(rename = "startPosition", with = "rust_decimal::serde::str")] pub start_position: Decimal, + #[serde(with = "rust_decimal::serde::str")] pub sz: Decimal, pub time: u64, // The total fee, inclusive of builderFee. + #[serde(with = "rust_decimal::serde::str")] pub fee: Decimal, #[serde(rename = "feeToken")] pub fee_token: String, @@ -137,17 +145,21 @@ pub struct SpotFill { /// Refer to `` for more information /// on how asset IDs work. pub coin: String, + #[serde(with = "rust_decimal::serde::str")] pub px: Decimal, + #[serde(with = "rust_decimal::serde::str")] pub sz: Decimal, pub side: String, pub time: u64, - #[serde(rename = "startPosition")] + #[serde(rename = "startPosition", with = "rust_decimal::serde::str")] pub start_position: Decimal, pub dir: String, + #[serde(with = "rust_decimal::serde::str")] pub closed_pnl: Decimal, pub hash: String, pub oid: u64, pub crossed: bool, + #[serde(with = "rust_decimal::serde::str")] pub fee: Decimal, pub tid: u64, #[serde(rename = "feeToken")] @@ -201,7 +213,7 @@ pub struct UserRateLimitsRequest { /// Response for "userRateLimit" request type. #[derive(Debug, Deserialize)] pub struct UserRateLimits { - #[serde(rename = "cumVlm")] + #[serde(rename = "cumVlm", with = "rust_decimal::serde::str")] pub cum_vlm: Decimal, /// max(0, `cumulative_used` minus reserved) #[serde(rename = "nRequestsUsed")] @@ -254,17 +266,19 @@ pub enum OrderWithStatus { /// Represents a Bid or Ask in the [`L2BookSnapshot`]. #[derive(Debug, Serialize, Deserialize)] pub struct BidOrAsk { - px: rust_decimal::Decimal, - sz: rust_decimal::Decimal, - n: u64, + #[serde(with = "rust_decimal::serde::str")] + pub px: rust_decimal::Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub sz: rust_decimal::Decimal, + pub n: u64, } /// Response for request with type "l2Book" #[derive(Debug, Serialize, Deserialize)] pub struct L2BookSnapshot { - coin: String, - time: u64, - levels: (Vec, Vec), + pub coin: String, + pub time: u64, + pub levels: (Vec, Vec), } #[derive(Debug, Serialize, Deserialize)] @@ -280,16 +294,16 @@ pub struct CandleSnapshotRequest { #[allow(nonstandard_style)] #[derive(Debug, Serialize, Deserialize)] pub struct Candle { - T: u64, - c: String, - h: String, - i: String, - l: String, - n: u64, - o: String, - s: String, - t: u64, - v: String, + pub T: u64, + pub c: String, + pub h: String, + pub i: String, + pub l: String, + pub n: u64, + pub o: String, + pub s: String, + pub t: u64, + pub v: String, } pub type CandleSnapshot = Vec; @@ -323,8 +337,9 @@ pub struct Children; pub struct Order { pub coin: String, pub side: String, - #[serde(rename = "limitPx")] + #[serde(rename = "limitPx", with = "rust_decimal::serde::str")] pub limit_px: Decimal, + #[serde(with = "rust_decimal::serde::str")] pub sz: Decimal, pub oid: u64, pub timestamp: u64, @@ -332,7 +347,7 @@ pub struct Order { pub trigger_condition: String, #[serde(rename = "isTrigger")] pub is_trigger: bool, - #[serde(rename = "triggerPx")] + #[serde(rename = "triggerPx", with = "rust_decimal::serde::str")] pub trigger_px: Decimal, pub children: Vec, #[serde(rename = "isPositionTpsl")] @@ -341,7 +356,7 @@ pub struct Order { pub reduce_only: bool, #[serde(rename = "orderType")] pub order_type: String, - #[serde(rename = "origSz")] + #[serde(rename = "origSz", with = "rust_decimal::serde::str")] pub orig_sz: Decimal, pub tif: String, pub cloid: Option, @@ -360,19 +375,22 @@ pub type UserHistoricalOrders = Vec; #[derive(Debug, Deserialize)] pub struct SliceFill { - #[serde(rename = "closedPnl")] + #[serde(rename = "closedPnl", with = "rust_decimal::serde::str")] pub closed_pnl: Decimal, pub coin: String, pub crossed: bool, pub dir: String, pub hash: String, pub oid: u64, + #[serde(with = "rust_decimal::serde::str")] pub px: Decimal, pub side: String, - #[serde(rename = "startPosition")] + #[serde(rename = "startPosition", with = "rust_decimal::serde::str")] pub start_position: Decimal, + #[serde(with = "rust_decimal::serde::str")] pub sz: Decimal, pub time: u64, + #[serde(with = "rust_decimal::serde::str")] pub fee: Decimal, #[serde(rename = "feeToken")] pub fee_token: String, @@ -395,8 +413,12 @@ pub struct ClearinghouseState { pub margin_summary: MarginSummary, #[serde(rename = "crossMarginSummary")] pub cross_margin_summary: MarginSummary, - #[serde(rename = "crossMaintenanceMarginUsed")] + #[serde( + rename = "crossMaintenanceMarginUsed", + with = "rust_decimal::serde::str" + )] pub cross_maintenance_margin_used: Decimal, + #[serde(with = "rust_decimal::serde::str")] pub withdrawable: Decimal, #[serde(rename = "assetPositions")] pub asset_positions: Vec, @@ -405,13 +427,13 @@ pub struct ClearinghouseState { #[derive(Debug, Serialize, Deserialize)] pub struct MarginSummary { - #[serde(rename = "accountValue")] + #[serde(rename = "accountValue", with = "rust_decimal::serde::str")] pub account_value: Decimal, - #[serde(rename = "totalNtlPos")] + #[serde(rename = "totalNtlPos", with = "rust_decimal::serde::str")] pub total_ntl_pos: Decimal, - #[serde(rename = "totalRawUsd")] + #[serde(rename = "totalRawUsd", with = "rust_decimal::serde::str")] pub total_raw_usd: Decimal, - #[serde(rename = "totalMarginUsed")] + #[serde(rename = "totalMarginUsed", with = "rust_decimal::serde::str")] pub total_margin_used: Decimal, } @@ -427,14 +449,21 @@ pub struct AssetPosition { #[serde(rename_all = "camelCase")] pub struct Position { pub coin: String, - pub szi: String, + #[serde(with = "rust_decimal::serde::str")] + pub szi: Decimal, pub leverage: Leverage, - pub entry_px: String, - pub position_value: String, - pub unrealized_pnl: String, - pub return_on_equity: String, - pub liquidation_px: String, - pub margin_used: String, + #[serde(with = "rust_decimal::serde::str")] + pub entry_px: Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub position_value: Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub unrealized_pnl: Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub return_on_equity: Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub liquidation_px: Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub margin_used: Decimal, pub max_leverage: u32, pub cum_funding: CumFunding, } @@ -451,9 +480,12 @@ pub struct Leverage { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CumFunding { - pub all_time: String, - pub since_open: String, - pub since_change: String, + #[serde(with = "rust_decimal::serde::str")] + pub all_time: Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub since_open: Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub since_change: Decimal, } #[derive(Debug, Serialize, Deserialize)] @@ -465,9 +497,11 @@ pub struct SpotState { pub struct SpotBalance { pub coin: String, pub token: u64, + #[serde(with = "rust_decimal::serde::str")] pub total: Decimal, + #[serde(with = "rust_decimal::serde::str")] pub hold: Decimal, - #[serde(rename = "entryNtl")] + #[serde(rename = "entryNtl", with = "rust_decimal::serde::str")] pub entry_ntl: Decimal, } @@ -505,7 +539,10 @@ pub struct PortfolioEntry( ); #[derive(Debug, Serialize, Deserialize)] -pub struct HistoryPoint(pub u64, pub Decimal); +pub struct HistoryPoint( + pub u64, + #[serde(with = "rust_decimal::serde::str")] pub Decimal, +); #[derive(Debug, Serialize, Deserialize)] pub struct FollowerState; @@ -513,10 +550,11 @@ pub struct FollowerState; #[derive(Debug, Serialize, Deserialize)] pub struct Follower { pub user: String, - #[serde(rename = "vaultEquity")] + #[serde(rename = "vaultEquity", with = "rust_decimal::serde::str")] pub vault_equity: Decimal, + #[serde(with = "rust_decimal::serde::str")] pub pnl: Decimal, - #[serde(rename = "allTimePnl")] + #[serde(rename = "allTimePnl", with = "rust_decimal::serde::str")] pub all_time_pnl: Decimal, #[serde(rename = "daysFollowing")] pub days_following: u64, @@ -548,6 +586,7 @@ pub struct VaultDetails { pub leader: String, pub description: String, pub portfolio: Vec, + #[serde(with = "rust_decimal::serde::str")] pub apr: Decimal, #[serde(rename = "followerState")] pub follower_state: Option, // null in example @@ -581,6 +620,7 @@ pub struct UserRequest { pub struct VaultDeposit { #[serde(rename = "vaultAddress")] pub vault_address: String, + #[serde(with = "rust_decimal::serde::str")] pub equity: Decimal, } @@ -600,6 +640,7 @@ pub struct HistoryEntry { pub account_value_history: Vec, #[serde(rename = "pnlHistory")] pub pnl_history: Vec, + #[serde(with = "rust_decimal::serde::str")] pub vlm: Decimal, } @@ -627,13 +668,13 @@ pub enum TokenStateEntry { #[derive(Debug, Serialize, Deserialize)] pub struct TokenState { - #[serde(rename = "cumVlm")] + #[serde(rename = "cumVlm", with = "rust_decimal::serde::str")] pub cum_vlm: Decimal, - #[serde(rename = "unclaimedRewards")] + #[serde(rename = "unclaimedRewards", with = "rust_decimal::serde::str")] pub unclaimed_rewards: Decimal, - #[serde(rename = "claimedRewards")] + #[serde(rename = "claimedRewards", with = "rust_decimal::serde::str")] pub claimed_rewards: Decimal, - #[serde(rename = "builderRewards")] + #[serde(rename = "builderRewards", with = "rust_decimal::serde::str")] pub builder_rewards: Decimal, } @@ -652,17 +693,24 @@ pub enum ReferrerData { referral_states: Vec, }, NeedToTrade { + #[serde(with = "rust_decimal::serde::str")] required: Decimal, }, } #[derive(Debug, Serialize, Deserialize)] pub struct ReferralState { - #[serde(rename = "cumVlm")] + #[serde(rename = "cumVlm", with = "rust_decimal::serde::str")] pub cum_vlm: Decimal, - #[serde(rename = "cumRewardedFeesSinceReferred")] + #[serde( + rename = "cumRewardedFeesSinceReferred", + with = "rust_decimal::serde::str" + )] pub cum_rewarded_fees_since_referred: Decimal, - #[serde(rename = "cumFeesRewardedToReferrer")] + #[serde( + rename = "cumFeesRewardedToReferrer", + with = "rust_decimal::serde::str" + )] pub cum_fees_rewarded_to_referrer: Decimal, #[serde(rename = "timeJoined")] pub time_joined: u64, @@ -677,13 +725,13 @@ pub struct RewardHistory; // empty array type pub struct UserReferralInformation { #[serde(rename = "referredBy")] pub referred_by: Option, - #[serde(rename = "cumVlm")] + #[serde(rename = "cumVlm", with = "rust_decimal::serde::str")] pub cum_vlm: Decimal, // USDC Only - #[serde(rename = "unclaimedRewards")] + #[serde(rename = "unclaimedRewards", with = "rust_decimal::serde::str")] pub unclaimed_rewards: Decimal, // USDC Only - #[serde(rename = "claimedRewards")] + #[serde(rename = "claimedRewards", with = "rust_decimal::serde::str")] pub claimed_rewards: Decimal, // USDC Only - #[serde(rename = "builderRewards")] + #[serde(rename = "builderRewards", with = "rust_decimal::serde::str")] pub builder_rewards: Decimal, // USDC Only #[serde(rename = "tokenToState")] pub token_to_state: Vec, @@ -696,23 +744,26 @@ pub struct UserReferralInformation { #[derive(Debug, Serialize, Deserialize)] pub struct DailyUserVlm { pub date: String, - #[serde(rename = "userCross")] + #[serde(rename = "userCross", with = "rust_decimal::serde::str")] pub user_cross: Decimal, - #[serde(rename = "userAdd")] + #[serde(rename = "userAdd", with = "rust_decimal::serde::str")] pub user_add: Decimal, + #[serde(with = "rust_decimal::serde::str")] pub exchange: Decimal, } #[derive(Debug, Serialize, Deserialize)] pub struct FeeSchedule { + #[serde(with = "rust_decimal::serde::str")] pub cross: Decimal, + #[serde(with = "rust_decimal::serde::str")] pub add: Decimal, - #[serde(rename = "spotCross")] + #[serde(rename = "spotCross", with = "rust_decimal::serde::str")] pub spot_cross: Decimal, - #[serde(rename = "spotAdd")] + #[serde(rename = "spotAdd", with = "rust_decimal::serde::str")] pub spot_add: Decimal, pub tiers: FeeTiers, - #[serde(rename = "referralDiscount")] + #[serde(rename = "referralDiscount", with = "rust_decimal::serde::str")] pub referral_discount: Decimal, #[serde(rename = "stakingDiscountTiers")] pub staking_discount_tiers: Vec, @@ -726,20 +777,23 @@ pub struct FeeTiers { #[derive(Debug, Serialize, Deserialize)] pub struct VipTier { - #[serde(rename = "ntlCutoff")] + #[serde(rename = "ntlCutoff", with = "rust_decimal::serde::str")] pub ntl_cutoff: Decimal, + #[serde(with = "rust_decimal::serde::str")] pub cross: Decimal, + #[serde(with = "rust_decimal::serde::str")] pub add: Decimal, - #[serde(rename = "spotCross")] + #[serde(rename = "spotCross", with = "rust_decimal::serde::str")] pub spot_cross: Decimal, - #[serde(rename = "spotAdd")] + #[serde(rename = "spotAdd", with = "rust_decimal::serde::str")] pub spot_add: Decimal, } #[derive(Debug, Serialize, Deserialize)] pub struct MmTier { - #[serde(rename = "makerFractionCutoff")] + #[serde(rename = "makerFractionCutoff", with = "rust_decimal::serde::str")] pub maker_fraction_cutoff: Decimal, + #[serde(with = "rust_decimal::serde::str")] pub add: Decimal, } @@ -753,8 +807,9 @@ pub struct StakingLink { #[derive(Debug, Serialize, Deserialize)] pub struct StakingDiscount { - #[serde(rename = "bpsOfMaxSupply")] + #[serde(rename = "bpsOfMaxSupply", with = "rust_decimal::serde::str")] pub bps_of_max_supply: Decimal, + #[serde(with = "rust_decimal::serde::str")] pub discount: Decimal, } @@ -765,20 +820,24 @@ pub struct UserFees { pub daily_user_vlm: Vec, #[serde(rename = "feeSchedule")] pub fee_schedule: FeeSchedule, - #[serde(rename = "userCrossRate")] + #[serde(rename = "userCrossRate", with = "rust_decimal::serde::str")] pub user_cross_rate: Decimal, - #[serde(rename = "userAddRate")] + #[serde(rename = "userAddRate", with = "rust_decimal::serde::str")] pub user_add_rate: Decimal, - #[serde(rename = "userSpotCrossRate")] + #[serde(rename = "userSpotCrossRate", with = "rust_decimal::serde::str")] pub user_spot_cross_rate: Decimal, - #[serde(rename = "userSpotAddRate")] + #[serde(rename = "userSpotAddRate", with = "rust_decimal::serde::str")] pub user_spot_add_rate: Decimal, - #[serde(rename = "activeReferralDiscount")] + #[serde(rename = "activeReferralDiscount", with = "rust_decimal::serde::str")] pub active_referral_discount: Decimal, + #[serde(with = "rust_decimal::serde::str_option")] pub trial: Option, - #[serde(rename = "feeTrialEscrow")] + #[serde(rename = "feeTrialEscrow", with = "rust_decimal::serde::str")] pub fee_trial_escrow: Decimal, - #[serde(rename = "nextTrialAvailableTimestamp")] + #[serde( + rename = "nextTrialAvailableTimestamp", + with = "rust_decimal::serde::str_option" + )] pub next_trial_available_timestamp: Option, #[serde(rename = "stakingLink")] pub staking_link: Option, @@ -800,17 +859,20 @@ pub type UserStakingDelegations = Vec; /// Response for the "delegatorSummary" request type. #[derive(Debug, Deserialize)] pub struct UserStakingSummary { + #[serde(with = "rust_decimal::serde::str")] pub delegated: Decimal, + #[serde(with = "rust_decimal::serde::str")] pub undelegated: Decimal, - #[serde(rename = "totalPendingWithdrawal")] + #[serde(rename = "totalPendingWithdrawal", with = "rust_decimal::serde::str")] pub total_pending_withdrawal: Decimal, #[serde(rename = "nPendingWithdrawals")] - pub n_pending_withdrawals: Decimal, + pub n_pending_withdrawals: u32, } #[derive(Debug, Deserialize)] pub struct Delegate { pub validator: String, + #[serde(with = "rust_decimal::serde::str")] pub amount: Decimal, #[serde(rename = "isUndelegate")] pub is_undelegate: bool, diff --git a/src/types/ws.rs b/src/types/ws.rs index 59db9df..f2fc74f 100644 --- a/src/types/ws.rs +++ b/src/types/ws.rs @@ -6,6 +6,7 @@ use std::collections::HashMap; use std::primitive::str; use crate::types::serialize::decimal_array; +use serde_with::serde_as; /// WebSocket trade data #[derive(Debug, Clone, Serialize, Deserialize)] @@ -65,10 +66,8 @@ pub struct WsNotification { /// All mid prices #[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct WsAllMids { - pub mids: HashMap, -} +#[serde_as] +pub struct WsAllMids(#[serde_as(as = "HashMap<_, DisplayFromStr>")] pub HashMap); /// Candlestick data #[derive(Debug, Clone, Serialize, Deserialize)]