1use alloy::primitives::Address;
6use alloy::signers::local::PrivateKeySigner;
7use dashmap::DashMap;
8use parking_lot::RwLock;
9use reqwest::Client;
10use rust_decimal::Decimal;
11use serde_json::{json, Value};
12use std::collections::HashMap;
13use std::str::FromStr;
14use std::sync::Arc;
15use std::time::{Duration, SystemTime, UNIX_EPOCH};
16
17use crate::error::{Error, Result};
18use crate::order::{Order, PlacedOrder, TriggerOrder};
19use crate::signing::sign_hash;
20use crate::types::*;
21
22const DEFAULT_WORKER_URL: &str = "https://send.hyperliquidapi.com";
27const DEFAULT_WORKER_INFO_URL: &str = "https://send.hyperliquidapi.com/info";
28
29const KNOWN_PATHS: &[&str] = &["info", "hypercore", "evm", "nanoreth", "ws", "send"];
31const HL_INFO_URL: &str = "https://api.hyperliquid.xyz/info";
32#[allow(dead_code)]
33const HL_EXCHANGE_URL: &str = "https://api.hyperliquid.xyz/exchange";
34const DEFAULT_SLIPPAGE: f64 = 0.03; const DEFAULT_TIMEOUT_SECS: u64 = 30;
36const METADATA_CACHE_TTL_SECS: u64 = 300; const QN_SUPPORTED_INFO_TYPES: &[&str] = &[
40 "meta",
41 "spotMeta",
42 "clearinghouseState",
43 "spotClearinghouseState",
44 "openOrders",
45 "exchangeStatus",
46 "frontendOpenOrders",
47 "liquidatable",
48 "activeAssetData",
49 "maxMarketOrderNtls",
50 "vaultSummaries",
51 "userVaultEquities",
52 "leadingVaults",
53 "extraAgents",
54 "subAccounts",
55 "userFees",
56 "userRateLimit",
57 "spotDeployState",
58 "perpDeployAuctionStatus",
59 "delegations",
60 "delegatorSummary",
61 "maxBuilderFee",
62 "userToMultiSigSigners",
63 "userRole",
64 "perpsAtOpenInterestCap",
65 "validatorL1Votes",
66 "marginTable",
67 "perpDexs",
68 "webData2",
69];
70
71#[derive(Debug, Clone)]
77pub struct AssetInfo {
78 pub index: usize,
79 pub name: String,
80 pub sz_decimals: u8,
81 pub is_spot: bool,
82}
83
84#[derive(Debug, Default)]
86pub struct MetadataCache {
87 assets: RwLock<HashMap<String, AssetInfo>>,
88 assets_by_index: RwLock<HashMap<usize, AssetInfo>>,
89 dexes: RwLock<Vec<String>>,
90 last_update: RwLock<Option<SystemTime>>,
91}
92
93impl MetadataCache {
94 pub fn get_asset(&self, name: &str) -> Option<AssetInfo> {
96 self.assets.read().get(name).cloned()
97 }
98
99 pub fn get_asset_by_index(&self, index: usize) -> Option<AssetInfo> {
101 self.assets_by_index.read().get(&index).cloned()
102 }
103
104 pub fn resolve_asset(&self, name: &str) -> Option<usize> {
106 self.assets.read().get(name).map(|a| a.index)
107 }
108
109 pub fn get_dexes(&self) -> Vec<String> {
111 self.dexes.read().clone()
112 }
113
114 pub fn is_valid(&self) -> bool {
116 if let Some(last) = *self.last_update.read() {
117 if let Ok(elapsed) = last.elapsed() {
118 return elapsed.as_secs() < METADATA_CACHE_TTL_SECS;
119 }
120 }
121 false
122 }
123
124 pub fn update(&self, meta: &Value, spot_meta: Option<&Value>, dexes: &[String]) {
126 let mut assets = HashMap::new();
127 let mut assets_by_index = HashMap::new();
128
129 if let Some(universe) = meta.get("universe").and_then(|u| u.as_array()) {
131 for (i, asset) in universe.iter().enumerate() {
132 if let Some(name) = asset.get("name").and_then(|n| n.as_str()) {
133 let sz_decimals = asset
134 .get("szDecimals")
135 .and_then(|d| d.as_u64())
136 .unwrap_or(8) as u8;
137
138 let info = AssetInfo {
139 index: i,
140 name: name.to_string(),
141 sz_decimals,
142 is_spot: false,
143 };
144 assets.insert(name.to_string(), info.clone());
145 assets_by_index.insert(i, info);
146 }
147 }
148 }
149
150 if let Some(spot) = spot_meta {
152 if let Some(tokens) = spot.get("tokens").and_then(|t| t.as_array()) {
153 for token in tokens {
154 if let (Some(name), Some(index)) = (
155 token.get("name").and_then(|n| n.as_str()),
156 token.get("index").and_then(|i| i.as_u64()),
157 ) {
158 let sz_decimals = token
159 .get("szDecimals")
160 .and_then(|d| d.as_u64())
161 .unwrap_or(8) as u8;
162
163 let info = AssetInfo {
164 index: index as usize,
165 name: name.to_string(),
166 sz_decimals,
167 is_spot: true,
168 };
169 assets.insert(name.to_string(), info.clone());
170 assets_by_index.insert(index as usize, info);
171 }
172 }
173 }
174 }
175
176 *self.assets.write() = assets;
177 *self.assets_by_index.write() = assets_by_index;
178 *self.dexes.write() = dexes.to_vec();
179 *self.last_update.write() = Some(SystemTime::now());
180 }
181}
182
183#[derive(Debug, Clone)]
189pub struct EndpointInfo {
190 pub base: String,
192 pub token: Option<String>,
194 pub is_quicknode: bool,
196}
197
198impl EndpointInfo {
199 pub fn parse(url: &str) -> Self {
206 let parsed = url::Url::parse(url).ok();
207
208 if let Some(parsed) = parsed {
209 let base = format!("{}://{}", parsed.scheme(), parsed.host_str().unwrap_or(""));
210 let is_quicknode = parsed.host_str().map(|h| h.contains("quiknode.pro")).unwrap_or(false);
211
212 let path_parts: Vec<&str> = parsed.path()
214 .trim_matches('/')
215 .split('/')
216 .filter(|p| !p.is_empty())
217 .collect();
218
219 let token = path_parts.iter()
221 .find(|&part| !KNOWN_PATHS.contains(part))
222 .map(|s| s.to_string());
223
224 Self { base, token, is_quicknode }
225 } else {
226 Self {
228 base: url.to_string(),
229 token: None,
230 is_quicknode: url.contains("quiknode.pro"),
231 }
232 }
233 }
234
235 pub fn build_url(&self, suffix: &str) -> String {
237 if let Some(ref token) = self.token {
238 format!("{}/{}/{}", self.base, token, suffix)
239 } else {
240 format!("{}/{}", self.base, suffix)
241 }
242 }
243
244 pub fn build_ws_url(&self) -> String {
246 let ws_base = self.base.replace("https://", "wss://").replace("http://", "ws://");
247 if let Some(ref token) = self.token {
248 format!("{}/{}/hypercore/ws", ws_base, token)
249 } else {
250 format!("{}/ws", ws_base)
251 }
252 }
253
254 pub fn build_grpc_url(&self) -> String {
256 if let Some(ref token) = self.token {
258 let grpc_base = self.base.replace(":443", "").replace("https://", "");
259 format!("https://{}:10000/{}", grpc_base, token)
260 } else {
261 self.base.replace(":443", ":10000")
262 }
263 }
264}
265
266pub struct HyperliquidSDKInner {
268 pub(crate) http_client: Client,
269 pub(crate) signer: Option<PrivateKeySigner>,
270 pub(crate) address: Option<Address>,
271 pub(crate) chain: Chain,
272 pub(crate) endpoint: Option<String>,
273 pub(crate) endpoint_info: Option<EndpointInfo>,
274 pub(crate) slippage: f64,
275 pub(crate) metadata: MetadataCache,
276 pub(crate) mid_prices: DashMap<String, f64>,
277}
278
279impl std::fmt::Debug for HyperliquidSDKInner {
280 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
281 f.debug_struct("HyperliquidSDKInner")
282 .field("address", &self.address)
283 .field("chain", &self.chain)
284 .field("endpoint", &self.endpoint)
285 .field("slippage", &self.slippage)
286 .finish_non_exhaustive()
287 }
288}
289
290const DEFAULT_EXCHANGE_URL: &str = "https://send.hyperliquidapi.com/exchange";
292
293impl HyperliquidSDKInner {
294 fn exchange_url(&self) -> String {
300 DEFAULT_EXCHANGE_URL.to_string()
301 }
302
303 fn info_url(&self, query_type: &str) -> String {
305 if let Some(ref info) = self.endpoint_info {
306 if info.is_quicknode && QN_SUPPORTED_INFO_TYPES.contains(&query_type) {
308 return info.build_url("info");
309 }
310 }
311 DEFAULT_WORKER_INFO_URL.to_string()
313 }
314
315 pub fn hypercore_url(&self) -> String {
317 if let Some(ref info) = self.endpoint_info {
318 if info.is_quicknode {
319 return info.build_url("hypercore");
320 }
321 }
322 HL_INFO_URL.to_string()
324 }
325
326 pub fn evm_url(&self, use_nanoreth: bool) -> String {
328 if let Some(ref info) = self.endpoint_info {
329 if info.is_quicknode {
330 let suffix = if use_nanoreth { "nanoreth" } else { "evm" };
331 return info.build_url(suffix);
332 }
333 }
334 match self.chain {
336 Chain::Mainnet => "https://rpc.hyperliquid.xyz/evm".to_string(),
337 Chain::Testnet => "https://rpc.hyperliquid-testnet.xyz/evm".to_string(),
338 }
339 }
340
341 pub fn ws_url(&self) -> String {
343 if let Some(ref info) = self.endpoint_info {
344 return info.build_ws_url();
345 }
346 "wss://api.hyperliquid.xyz/ws".to_string()
348 }
349
350 pub fn grpc_url(&self) -> String {
352 if let Some(ref info) = self.endpoint_info {
353 if info.is_quicknode {
354 return info.build_grpc_url();
355 }
356 }
357 String::new()
359 }
360
361 pub async fn query_info(&self, body: &Value) -> Result<Value> {
363 let query_type = body.get("type").and_then(|t| t.as_str()).unwrap_or("");
364 let url = self.info_url(query_type);
365
366 let response = self
367 .http_client
368 .post(&url)
369 .json(body)
370 .send()
371 .await?;
372
373 let status = response.status();
374 let text = response.text().await?;
375
376 if !status.is_success() {
377 return Err(Error::NetworkError(format!(
378 "Info endpoint returned {}: {}",
379 status, text
380 )));
381 }
382
383 serde_json::from_str(&text).map_err(|e| Error::JsonError(e.to_string()))
384 }
385
386 pub async fn build_action(&self, action: &Value, slippage: Option<f64>) -> Result<BuildResponse> {
388 let url = self.exchange_url();
389
390 let mut body = json!({ "action": action });
391 if let Some(s) = slippage {
392 if !s.is_finite() || s <= 0.0 {
393 return Err(Error::ValidationError(
394 "Slippage must be a positive finite number".to_string(),
395 ));
396 }
397 body["slippage"] = json!(s);
398 }
399
400 let response = self
401 .http_client
402 .post(url)
403 .json(&body)
404 .send()
405 .await?;
406
407 let status = response.status();
408 let text = response.text().await?;
409
410 if !status.is_success() {
411 return Err(Error::NetworkError(format!(
412 "Build request failed {}: {}",
413 status, text
414 )));
415 }
416
417 let result: Value = serde_json::from_str(&text)?;
418
419 if let Some(error) = result.get("error") {
421 return Err(Error::from_api_error(
422 error.as_str().unwrap_or("Unknown error"),
423 ));
424 }
425
426 Ok(BuildResponse {
427 hash: result
428 .get("hash")
429 .and_then(|h| h.as_str())
430 .unwrap_or("")
431 .to_string(),
432 nonce: result.get("nonce").and_then(|n| n.as_u64()).unwrap_or(0),
433 action: result.get("action").cloned().unwrap_or(action.clone()),
434 })
435 }
436
437 pub async fn send_action(
439 &self,
440 action: &Value,
441 nonce: u64,
442 signature: &Signature,
443 ) -> Result<Value> {
444 let url = self.exchange_url();
445
446 let body = json!({
447 "action": action,
448 "nonce": nonce,
449 "signature": signature,
450 });
451
452 let response = self
453 .http_client
454 .post(url)
455 .json(&body)
456 .send()
457 .await?;
458
459 let status = response.status();
460 let text = response.text().await?;
461
462 if !status.is_success() {
463 return Err(Error::NetworkError(format!(
464 "Send request failed {}: {}",
465 status, text
466 )));
467 }
468
469 let result: Value = serde_json::from_str(&text)?;
470
471 if let Some(hl_status) = result.get("status") {
473 if hl_status.as_str() == Some("err") {
474 if let Some(response) = result.get("response") {
475 let raw = response.as_str()
476 .map(|s| s.to_string())
477 .unwrap_or_else(|| response.to_string());
478 return Err(Error::from_api_error(&raw));
479 }
480 }
481 }
482
483 Ok(result)
484 }
485
486 pub async fn build_sign_send(&self, action: &Value, slippage: Option<f64>) -> Result<Value> {
492 let signer = self
493 .signer
494 .as_ref()
495 .ok_or_else(|| Error::ConfigError("No private key configured".to_string()))?;
496
497 let effective_slippage = slippage.or_else(|| {
499 if self.slippage > 0.0 {
500 Some(self.slippage)
501 } else {
502 None
503 }
504 });
505
506 let build_result = self.build_action(action, effective_slippage).await?;
508
509 let hash_bytes = hex::decode(build_result.hash.trim_start_matches("0x"))
511 .map_err(|e| Error::SigningError(format!("Invalid hash: {}", e)))?;
512
513 let hash = alloy::primitives::B256::from_slice(&hash_bytes);
514 let signature = sign_hash(signer, hash).await?;
515
516 self.send_action(&build_result.action, build_result.nonce, &signature)
518 .await
519 }
520
521 pub async fn refresh_metadata(&self) -> Result<()> {
523 let meta = self.query_info(&json!({"type": "meta"})).await?;
525
526 let spot_meta = self.query_info(&json!({"type": "spotMeta"})).await.ok();
528
529 let dexes_result = self.query_info(&json!({"type": "perpDexs"})).await.ok();
531 let dexes: Vec<String> = dexes_result
532 .and_then(|v| {
533 v.as_array().map(|arr| {
534 arr.iter()
535 .filter_map(|d| d.get("name").and_then(|n| n.as_str()).map(|s| s.to_string()))
536 .collect()
537 })
538 })
539 .unwrap_or_default();
540
541 self.metadata.update(&meta, spot_meta.as_ref(), &dexes);
542
543 Ok(())
544 }
545
546 pub async fn fetch_all_mids(&self) -> Result<HashMap<String, f64>> {
548 let result = self.query_info(&json!({"type": "allMids"})).await?;
549
550 let mut mids = HashMap::new();
551 if let Some(obj) = result.as_object() {
552 for (coin, price_val) in obj {
553 let price_str = price_val.as_str().unwrap_or("");
554 if let Ok(price) = price_str.parse::<f64>() {
555 mids.insert(coin.clone(), price);
556 self.mid_prices.insert(coin.clone(), price);
557 }
558 }
559 }
560
561 for dex in self.metadata.get_dexes() {
563 if let Ok(dex_result) = self.query_info(&json!({"type": "allMids", "dex": dex})).await {
564 if let Some(obj) = dex_result.as_object() {
565 for (coin, price_val) in obj {
566 let price_str = price_val.as_str().unwrap_or("");
567 if let Ok(price) = price_str.parse::<f64>() {
568 mids.insert(coin.clone(), price);
569 self.mid_prices.insert(coin.clone(), price);
570 }
571 }
572 }
573 }
574 }
575
576 Ok(mids)
577 }
578
579 pub async fn get_mid_price(&self, asset: &str) -> Result<f64> {
581 if let Some(price) = self.mid_prices.get(asset) {
582 return Ok(*price);
583 }
584
585 let mids = self.fetch_all_mids().await?;
587 mids.get(asset)
588 .copied()
589 .ok_or_else(|| Error::ValidationError(format!("No price found for {}", asset)))
590 }
591
592 pub fn resolve_asset(&self, name: &str) -> Option<usize> {
594 self.metadata.resolve_asset(name)
595 }
596
597 pub async fn cancel_by_oid(&self, oid: u64, asset: &str) -> Result<Value> {
599 let asset_index = self
600 .resolve_asset(asset)
601 .ok_or_else(|| Error::ValidationError(format!("Unknown asset: {}", asset)))?;
602
603 let action = json!({
604 "type": "cancel",
605 "cancels": [{
606 "a": asset_index,
607 "o": oid,
608 }]
609 });
610
611 self.build_sign_send(&action, None).await
612 }
613
614 pub async fn modify_by_oid(
616 &self,
617 oid: u64,
618 asset: &str,
619 side: Side,
620 price: Decimal,
621 size: Decimal,
622 ) -> Result<PlacedOrder> {
623 let asset_index = self
624 .resolve_asset(asset)
625 .ok_or_else(|| Error::ValidationError(format!("Unknown asset: {}", asset)))?;
626
627 let action = json!({
628 "type": "batchModify",
629 "modifies": [{
630 "oid": oid,
631 "order": {
632 "a": asset_index,
633 "b": side.is_buy(),
634 "p": price.normalize().to_string(),
635 "s": size.normalize().to_string(),
636 "r": false,
637 "t": {"limit": {"tif": "Gtc"}},
638 "c": "0x00000000000000000000000000000000",
639 }
640 }]
641 });
642
643 let response = self.build_sign_send(&action, None).await?;
644
645 Ok(PlacedOrder::from_response(
646 response,
647 asset.to_string(),
648 side,
649 size,
650 Some(price),
651 None,
652 ))
653 }
654}
655
656#[derive(Debug)]
658pub struct BuildResponse {
659 pub hash: String,
660 pub nonce: u64,
661 pub action: Value,
662}
663
664#[derive(Default)]
670pub struct HyperliquidSDKBuilder {
671 endpoint: Option<String>,
672 private_key: Option<String>,
673 testnet: bool,
674 auto_approve: bool,
675 max_fee: String,
676 slippage: f64,
677 timeout: Duration,
678}
679
680impl HyperliquidSDKBuilder {
681 pub fn new() -> Self {
683 Self {
684 endpoint: None,
685 private_key: None,
686 testnet: false,
687 auto_approve: true,
688 max_fee: "1%".to_string(),
689 slippage: DEFAULT_SLIPPAGE,
690 timeout: Duration::from_secs(DEFAULT_TIMEOUT_SECS),
691 }
692 }
693
694 pub fn endpoint(mut self, endpoint: impl Into<String>) -> Self {
696 self.endpoint = Some(endpoint.into());
697 self
698 }
699
700 pub fn private_key(mut self, key: impl Into<String>) -> Self {
702 self.private_key = Some(key.into());
703 self
704 }
705
706 pub fn testnet(mut self, testnet: bool) -> Self {
708 self.testnet = testnet;
709 self
710 }
711
712 pub fn auto_approve(mut self, auto: bool) -> Self {
714 self.auto_approve = auto;
715 self
716 }
717
718 pub fn max_fee(mut self, fee: impl Into<String>) -> Self {
720 self.max_fee = fee.into();
721 self
722 }
723
724 pub fn slippage(mut self, slippage: f64) -> Self {
726 self.slippage = slippage;
727 self
728 }
729
730 pub fn timeout(mut self, timeout: Duration) -> Self {
732 self.timeout = timeout;
733 self
734 }
735
736 pub async fn build(self) -> Result<HyperliquidSDK> {
738 let private_key = self
740 .private_key
741 .or_else(|| std::env::var("PRIVATE_KEY").ok());
742
743 let (signer, address) = if let Some(key) = private_key {
745 let key = key.trim_start_matches("0x");
746 let signer = PrivateKeySigner::from_str(key)?;
747 let address = signer.address();
748 (Some(signer), Some(address))
749 } else {
750 (None, None)
751 };
752
753 let http_client = Client::builder()
755 .timeout(self.timeout)
756 .build()
757 .map_err(|e| Error::ConfigError(format!("Failed to create HTTP client: {}", e)))?;
758
759 let chain = if self.testnet {
760 Chain::Testnet
761 } else {
762 Chain::Mainnet
763 };
764
765 let endpoint_info = self.endpoint.as_ref().map(|ep| EndpointInfo::parse(ep));
767
768 let inner = Arc::new(HyperliquidSDKInner {
769 http_client,
770 signer,
771 address,
772 chain,
773 endpoint: self.endpoint,
774 endpoint_info,
775 slippage: self.slippage,
776 metadata: MetadataCache::default(),
777 mid_prices: DashMap::new(),
778 });
779
780 if let Err(e) = inner.refresh_metadata().await {
782 tracing::warn!("Failed to fetch initial metadata: {}", e);
783 }
784
785 Ok(HyperliquidSDK {
786 inner,
787 auto_approve: self.auto_approve,
788 max_fee: self.max_fee,
789 })
790 }
791}
792
793pub struct HyperliquidSDK {
799 inner: Arc<HyperliquidSDKInner>,
800 #[allow(dead_code)]
801 auto_approve: bool,
802 max_fee: String,
803}
804
805impl HyperliquidSDK {
806 pub fn new() -> HyperliquidSDKBuilder {
808 HyperliquidSDKBuilder::new()
809 }
810
811 pub fn address(&self) -> Option<Address> {
813 self.inner.address
814 }
815
816 pub fn chain(&self) -> Chain {
818 self.inner.chain
819 }
820
821 pub fn info(&self) -> crate::info::Info {
827 crate::info::Info::new(self.inner.clone())
828 }
829
830 pub fn core(&self) -> crate::hypercore::HyperCore {
832 crate::hypercore::HyperCore::new(self.inner.clone())
833 }
834
835 pub fn evm(&self) -> crate::evm::EVM {
837 crate::evm::EVM::new(self.inner.clone())
838 }
839
840 pub fn stream(&self) -> crate::stream::Stream {
842 crate::stream::Stream::new(self.inner.endpoint.clone())
843 }
844
845 pub fn grpc(&self) -> crate::grpc::GRPCStream {
847 crate::grpc::GRPCStream::new(self.inner.endpoint.clone())
848 }
849
850 pub fn evm_stream(&self) -> crate::evm_stream::EVMStream {
852 crate::evm_stream::EVMStream::new(self.inner.endpoint.clone())
853 }
854
855 pub async fn markets(&self) -> Result<Value> {
861 self.inner.query_info(&json!({"type": "meta"})).await
862 }
863
864 pub async fn dexes(&self) -> Result<Value> {
866 self.inner.query_info(&json!({"type": "perpDexs"})).await
867 }
868
869 pub async fn open_orders(&self) -> Result<Value> {
871 let address = self
872 .inner
873 .address
874 .ok_or_else(|| Error::ConfigError("No address configured".to_string()))?;
875
876 self.inner
877 .query_info(&json!({
878 "type": "openOrders",
879 "user": format!("{:?}", address),
880 }))
881 .await
882 }
883
884 pub async fn order_status(&self, oid: u64, dex: Option<&str>) -> Result<Value> {
886 let address = self
887 .inner
888 .address
889 .ok_or_else(|| Error::ConfigError("No address configured".to_string()))?;
890
891 let mut req = json!({
892 "type": "orderStatus",
893 "user": format!("{:?}", address),
894 "oid": oid,
895 });
896
897 if let Some(d) = dex {
898 req["dex"] = json!(d);
899 }
900
901 self.inner.query_info(&req).await
902 }
903
904 pub async fn market_buy(&self, asset: &str) -> MarketOrderBuilder {
910 MarketOrderBuilder::new(self.inner.clone(), asset.to_string(), Side::Buy)
911 }
912
913 pub async fn market_sell(&self, asset: &str) -> MarketOrderBuilder {
915 MarketOrderBuilder::new(self.inner.clone(), asset.to_string(), Side::Sell)
916 }
917
918 pub async fn buy(
920 &self,
921 asset: &str,
922 size: f64,
923 price: f64,
924 tif: TIF,
925 ) -> Result<PlacedOrder> {
926 self.place_order(asset, Side::Buy, size, Some(price), tif, false, false, None)
927 .await
928 }
929
930 pub async fn sell(
932 &self,
933 asset: &str,
934 size: f64,
935 price: f64,
936 tif: TIF,
937 ) -> Result<PlacedOrder> {
938 self.place_order(asset, Side::Sell, size, Some(price), tif, false, false, None)
939 .await
940 }
941
942 pub async fn order(&self, order: Order) -> Result<PlacedOrder> {
944 order.validate()?;
945
946 let asset = order.get_asset();
947 let side = order.get_side();
948 let tif = order.get_tif();
949
950 let size = if let Some(s) = order.get_size() {
952 s
953 } else if let Some(notional) = order.get_notional() {
954 let mid = self.inner.get_mid_price(asset).await?;
955 Decimal::from_f64_retain(notional.to_string().parse::<f64>().unwrap_or(0.0) / mid)
956 .unwrap_or_default()
957 } else {
958 return Err(Error::ValidationError(
959 "Order must have size or notional".to_string(),
960 ));
961 };
962
963 let is_market = order.is_market();
966 let price = if is_market {
967 None } else {
969 order
970 .get_price()
971 .map(|p| p.to_string().parse::<f64>().unwrap_or(0.0))
972 };
973
974 self.place_order(
975 asset,
976 side,
977 size.to_string().parse::<f64>().unwrap_or(0.0),
978 price,
979 if is_market { TIF::Market } else { tif },
980 order.is_reduce_only(),
981 is_market,
982 None, )
984 .await
985 }
986
987 pub async fn trigger_order(&self, order: TriggerOrder) -> Result<PlacedOrder> {
989 order.validate()?;
990
991 let asset = order.get_asset();
992 let asset_index = self
993 .inner
994 .resolve_asset(asset)
995 .ok_or_else(|| Error::ValidationError(format!("Unknown asset: {}", asset)))?;
996
997 let sz_decimals = self.inner.metadata.get_asset(asset)
999 .map(|a| a.sz_decimals)
1000 .unwrap_or(5) as u32;
1001
1002 let trigger_px = order
1003 .get_trigger_price()
1004 .ok_or_else(|| Error::ValidationError("Trigger price required".to_string()))?;
1005
1006 let size = order
1007 .get_size()
1008 .ok_or_else(|| Error::ValidationError("Size required".to_string()))?;
1009
1010 let size_rounded = size.round_dp(sz_decimals);
1012
1013 let limit_px = if order.is_market() {
1015 let mid = self.inner.get_mid_price(asset).await?;
1016 let slippage = self.inner.slippage;
1017 let price = if order.get_side().is_buy() {
1018 mid * (1.0 + slippage)
1019 } else {
1020 mid * (1.0 - slippage)
1021 };
1022 Decimal::from_f64_retain(price.round()).unwrap_or_default()
1023 } else {
1024 order.get_limit_price().unwrap_or(trigger_px).round()
1025 };
1026
1027 let trigger_px_rounded = trigger_px.round();
1029
1030 let cloid = {
1032 let now = std::time::SystemTime::now()
1033 .duration_since(std::time::UNIX_EPOCH)
1034 .unwrap_or_default();
1035 let nanos = now.as_nanos() as u64;
1036 let hi = nanos.wrapping_mul(0x517cc1b727220a95);
1037 format!("0x{:016x}{:016x}", nanos, hi)
1038 };
1039
1040 let action = json!({
1041 "type": "order",
1042 "orders": [{
1043 "a": asset_index,
1044 "b": order.get_side().is_buy(),
1045 "p": limit_px.normalize().to_string(),
1046 "s": size_rounded.normalize().to_string(),
1047 "r": order.is_reduce_only(),
1048 "t": {
1049 "trigger": {
1050 "isMarket": order.is_market(),
1051 "triggerPx": trigger_px_rounded.normalize().to_string(),
1052 "tpsl": order.get_tpsl().to_string(),
1053 }
1054 },
1055 "c": cloid,
1056 }],
1057 "grouping": "na",
1058 });
1059
1060 let response = self.inner.build_sign_send(&action, None).await?;
1061
1062 Ok(PlacedOrder::from_response(
1063 response,
1064 asset.to_string(),
1065 order.get_side(),
1066 size,
1067 Some(limit_px),
1068 Some(self.inner.clone()),
1069 ))
1070 }
1071
1072 pub async fn stop_loss(
1074 &self,
1075 asset: &str,
1076 size: f64,
1077 trigger_price: f64,
1078 ) -> Result<PlacedOrder> {
1079 self.trigger_order(
1080 TriggerOrder::stop_loss(asset)
1081 .size(size)
1082 .trigger_price(trigger_price)
1083 .market(),
1084 )
1085 .await
1086 }
1087
1088 pub async fn take_profit(
1090 &self,
1091 asset: &str,
1092 size: f64,
1093 trigger_price: f64,
1094 ) -> Result<PlacedOrder> {
1095 self.trigger_order(
1096 TriggerOrder::take_profit(asset)
1097 .size(size)
1098 .trigger_price(trigger_price)
1099 .market(),
1100 )
1101 .await
1102 }
1103
1104 async fn place_order(
1110 &self,
1111 asset: &str,
1112 side: Side,
1113 size: f64,
1114 price: Option<f64>,
1115 tif: TIF,
1116 reduce_only: bool,
1117 is_market: bool,
1118 slippage: Option<f64>,
1119 ) -> Result<PlacedOrder> {
1120 let sz_decimals = self.inner.metadata.get_asset(asset)
1122 .map(|a| a.sz_decimals)
1123 .unwrap_or(5) as i32;
1124
1125 let size_rounded = (size * 10f64.powi(sz_decimals)).round() / 10f64.powi(sz_decimals);
1127
1128 let (action, effective_slippage) = if is_market {
1129 let mut order_spec = json!({
1131 "asset": asset,
1132 "side": if side.is_buy() { "buy" } else { "sell" },
1133 "size": format!("{}", size_rounded),
1134 "tif": "market",
1135 });
1136 if reduce_only {
1137 order_spec["reduceOnly"] = json!(true);
1138 }
1139 let action = json!({
1140 "type": "order",
1141 "orders": [order_spec],
1142 });
1143 (action, slippage)
1144 } else {
1145 let asset_index = self
1147 .inner
1148 .resolve_asset(asset)
1149 .ok_or_else(|| Error::ValidationError(format!("Unknown asset: {}", asset)))?;
1150
1151 let resolved_price = price.map(|p| p.round()).unwrap_or(0.0);
1152
1153 let tif_wire = match tif {
1154 TIF::Ioc => "Ioc",
1155 TIF::Gtc => "Gtc",
1156 TIF::Alo => "Alo",
1157 TIF::Market => "Ioc",
1158 };
1159
1160 let cloid = {
1162 let now = std::time::SystemTime::now()
1163 .duration_since(std::time::UNIX_EPOCH)
1164 .unwrap_or_default();
1165 let nanos = now.as_nanos() as u64;
1166 let hi = nanos.wrapping_mul(0x517cc1b727220a95);
1167 format!("0x{:016x}{:016x}", nanos, hi)
1168 };
1169
1170 let action = json!({
1171 "type": "order",
1172 "orders": [{
1173 "a": asset_index,
1174 "b": side.is_buy(),
1175 "p": format!("{}", resolved_price),
1176 "s": format!("{}", size_rounded),
1177 "r": reduce_only,
1178 "t": {"limit": {"tif": tif_wire}},
1179 "c": cloid,
1180 }],
1181 "grouping": "na",
1182 });
1183 (action, None) };
1185
1186 let response = self.inner.build_sign_send(&action, effective_slippage).await?;
1187
1188 Ok(PlacedOrder::from_response(
1189 response,
1190 asset.to_string(),
1191 side,
1192 Decimal::from_f64_retain(size_rounded).unwrap_or_default(),
1193 price.map(|p| Decimal::from_f64_retain(p).unwrap_or_default()),
1194 Some(self.inner.clone()),
1195 ))
1196 }
1197
1198 pub async fn modify(
1206 &self,
1207 oid: u64,
1208 asset: &str,
1209 is_buy: bool,
1210 size: f64,
1211 price: f64,
1212 tif: TIF,
1213 reduce_only: bool,
1214 cloid: Option<&str>,
1215 ) -> Result<PlacedOrder> {
1216 let asset_idx = self
1217 .inner
1218 .metadata
1219 .resolve_asset(asset)
1220 .ok_or_else(|| Error::ValidationError(format!("Unknown asset: {}", asset)))?;
1221
1222 let sz_decimals = self.inner.metadata.get_asset(asset)
1223 .map(|a| a.sz_decimals)
1224 .unwrap_or(8) as i32;
1225 let size_rounded = (size * 10f64.powi(sz_decimals)).round() / 10f64.powi(sz_decimals);
1226
1227 let order_type = match tif {
1228 TIF::Gtc => json!({"limit": {"tif": "Gtc"}}),
1229 TIF::Ioc | TIF::Market => json!({"limit": {"tif": "Ioc"}}),
1230 TIF::Alo => json!({"limit": {"tif": "Alo"}}),
1231 };
1232
1233 let cloid_val = cloid
1234 .map(|s| s.to_string())
1235 .unwrap_or_else(|| {
1236 let now = std::time::SystemTime::now()
1237 .duration_since(std::time::UNIX_EPOCH)
1238 .unwrap_or_default();
1239 let nanos = now.as_nanos() as u64;
1240 let hi = nanos.wrapping_mul(0x517cc1b727220a95);
1241 format!("0x{:016x}{:016x}", nanos, hi)
1242 });
1243
1244 let action = json!({
1245 "type": "batchModify",
1246 "modifies": [{
1247 "oid": oid,
1248 "order": {
1249 "a": asset_idx,
1250 "b": is_buy,
1251 "p": format!("{:.8}", price).trim_end_matches('0').trim_end_matches('.'),
1252 "s": format!("{:.8}", size_rounded).trim_end_matches('0').trim_end_matches('.'),
1253 "r": reduce_only,
1254 "t": order_type,
1255 "c": cloid_val,
1256 }
1257 }]
1258 });
1259
1260 let response = self.inner.build_sign_send(&action, None).await?;
1261
1262 Ok(PlacedOrder::from_response(
1263 response,
1264 asset.to_string(),
1265 if is_buy { Side::Buy } else { Side::Sell },
1266 Decimal::from_f64_retain(size_rounded).unwrap_or_default(),
1267 Some(Decimal::from_f64_retain(price).unwrap_or_default()),
1268 Some(self.inner.clone()),
1269 ))
1270 }
1271
1272 pub async fn cancel(&self, oid: u64, asset: &str) -> Result<Value> {
1274 self.inner.cancel_by_oid(oid, asset).await
1275 }
1276
1277 pub async fn cancel_all(&self, asset: Option<&str>) -> Result<Value> {
1279 if self.inner.address.is_none() {
1281 return Err(Error::ConfigError("No address configured".to_string()));
1282 }
1283
1284 let open_orders = self.open_orders().await?;
1286
1287 let cancels: Vec<Value> = open_orders
1288 .as_array()
1289 .unwrap_or(&vec![])
1290 .iter()
1291 .filter(|order| {
1292 if let Some(asset) = asset {
1293 order.get("coin").and_then(|c| c.as_str()) == Some(asset)
1294 } else {
1295 true
1296 }
1297 })
1298 .filter_map(|order| {
1299 let oid = order.get("oid").and_then(|o| o.as_u64())?;
1300 let coin = order.get("coin").and_then(|c| c.as_str())?;
1301 let asset_index = self.inner.resolve_asset(coin)?;
1302 Some(json!({"a": asset_index, "o": oid}))
1303 })
1304 .collect();
1305
1306 if cancels.is_empty() {
1307 return Ok(json!({"status": "ok", "message": "No orders to cancel"}));
1308 }
1309
1310 let action = json!({
1311 "type": "cancel",
1312 "cancels": cancels,
1313 });
1314
1315 self.inner.build_sign_send(&action, None).await
1316 }
1317
1318 pub async fn close_position(&self, asset: &str, slippage: Option<f64>) -> Result<PlacedOrder> {
1324 let address = self
1325 .inner
1326 .address
1327 .ok_or_else(|| Error::ConfigError("No address configured".to_string()))?;
1328
1329 let action = json!({
1330 "type": "closePosition",
1331 "asset": asset,
1332 "user": format!("{:?}", address),
1333 });
1334
1335 let response = self.inner.build_sign_send(&action, slippage).await?;
1336
1337 Ok(PlacedOrder::from_response(
1340 response,
1341 asset.to_string(),
1342 Side::Sell, Decimal::ZERO, None,
1345 Some(self.inner.clone()),
1346 ))
1347 }
1348
1349 pub async fn update_leverage(
1355 &self,
1356 asset: &str,
1357 leverage: i32,
1358 is_cross: bool,
1359 ) -> Result<Value> {
1360 let asset_index = self
1361 .inner
1362 .resolve_asset(asset)
1363 .ok_or_else(|| Error::ValidationError(format!("Unknown asset: {}", asset)))?;
1364
1365 let action = json!({
1366 "type": "updateLeverage",
1367 "asset": asset_index,
1368 "isCross": is_cross,
1369 "leverage": leverage,
1370 });
1371
1372 self.inner.build_sign_send(&action, None).await
1373 }
1374
1375 pub async fn update_isolated_margin(
1377 &self,
1378 asset: &str,
1379 is_buy: bool,
1380 amount_usd: f64,
1381 ) -> Result<Value> {
1382 let asset_index = self
1383 .inner
1384 .resolve_asset(asset)
1385 .ok_or_else(|| Error::ValidationError(format!("Unknown asset: {}", asset)))?;
1386
1387 let action = json!({
1388 "type": "updateIsolatedMargin",
1389 "asset": asset_index,
1390 "isBuy": is_buy,
1391 "ntli": (amount_usd * 1_000_000.0) as i64, });
1393
1394 self.inner.build_sign_send(&action, None).await
1395 }
1396
1397 pub async fn twap_order(
1403 &self,
1404 asset: &str,
1405 size: f64,
1406 is_buy: bool,
1407 duration_minutes: i64,
1408 reduce_only: bool,
1409 randomize: bool,
1410 ) -> Result<Value> {
1411 let action = json!({
1412 "type": "twapOrder",
1413 "twap": {
1414 "a": asset,
1415 "b": is_buy,
1416 "s": format!("{}", size),
1417 "r": reduce_only,
1418 "m": duration_minutes,
1419 "t": randomize,
1420 }
1421 });
1422
1423 self.inner.build_sign_send(&action, None).await
1424 }
1425
1426 pub async fn twap_cancel(&self, asset: &str, twap_id: i64) -> Result<Value> {
1428 let action = json!({
1429 "type": "twapCancel",
1430 "a": asset,
1431 "t": twap_id,
1432 });
1433
1434 self.inner.build_sign_send(&action, None).await
1435 }
1436
1437 pub async fn transfer_usd(&self, destination: &str, amount: f64) -> Result<Value> {
1443 let time = SystemTime::now()
1444 .duration_since(UNIX_EPOCH)
1445 .unwrap()
1446 .as_millis() as u64;
1447
1448 let action = json!({
1449 "type": "usdSend",
1450 "hyperliquidChain": self.inner.chain.to_string(),
1451 "signatureChainId": self.inner.chain.signature_chain_id(),
1452 "destination": destination,
1453 "amount": format!("{}", amount),
1454 "time": time,
1455 });
1456
1457 self.inner.build_sign_send(&action, None).await
1458 }
1459
1460 pub async fn transfer_spot(
1462 &self,
1463 token: &str,
1464 destination: &str,
1465 amount: f64,
1466 ) -> Result<Value> {
1467 let time = SystemTime::now()
1468 .duration_since(UNIX_EPOCH)
1469 .unwrap()
1470 .as_millis() as u64;
1471
1472 let action = json!({
1473 "type": "spotSend",
1474 "hyperliquidChain": self.inner.chain.to_string(),
1475 "signatureChainId": self.inner.chain.signature_chain_id(),
1476 "token": token,
1477 "destination": destination,
1478 "amount": format!("{}", amount),
1479 "time": time,
1480 });
1481
1482 self.inner.build_sign_send(&action, None).await
1483 }
1484
1485 pub async fn withdraw(&self, amount: f64, destination: Option<&str>) -> Result<Value> {
1487 let time = SystemTime::now()
1488 .duration_since(UNIX_EPOCH)
1489 .unwrap()
1490 .as_millis() as u64;
1491
1492 let dest = destination
1493 .map(|s| s.to_string())
1494 .or_else(|| self.inner.address.map(|a| format!("{:?}", a)))
1495 .ok_or_else(|| Error::ConfigError("No destination address".to_string()))?;
1496
1497 let action = json!({
1498 "type": "withdraw3",
1499 "hyperliquidChain": self.inner.chain.to_string(),
1500 "signatureChainId": self.inner.chain.signature_chain_id(),
1501 "destination": dest,
1502 "amount": format!("{}", amount),
1503 "time": time,
1504 });
1505
1506 self.inner.build_sign_send(&action, None).await
1507 }
1508
1509 pub async fn transfer_spot_to_perp(&self, amount: f64) -> Result<Value> {
1511 let nonce = SystemTime::now()
1512 .duration_since(UNIX_EPOCH)
1513 .unwrap()
1514 .as_millis() as u64;
1515
1516 let action = json!({
1517 "type": "usdClassTransfer",
1518 "hyperliquidChain": self.inner.chain.to_string(),
1519 "signatureChainId": self.inner.chain.signature_chain_id(),
1520 "amount": format!("{}", amount),
1521 "toPerp": true,
1522 "nonce": nonce,
1523 });
1524
1525 self.inner.build_sign_send(&action, None).await
1526 }
1527
1528 pub async fn transfer_perp_to_spot(&self, amount: f64) -> Result<Value> {
1530 let nonce = SystemTime::now()
1531 .duration_since(UNIX_EPOCH)
1532 .unwrap()
1533 .as_millis() as u64;
1534
1535 let action = json!({
1536 "type": "usdClassTransfer",
1537 "hyperliquidChain": self.inner.chain.to_string(),
1538 "signatureChainId": self.inner.chain.signature_chain_id(),
1539 "amount": format!("{}", amount),
1540 "toPerp": false,
1541 "nonce": nonce,
1542 });
1543
1544 self.inner.build_sign_send(&action, None).await
1545 }
1546
1547 pub async fn vault_deposit(&self, vault_address: &str, amount: f64) -> Result<Value> {
1553 let action = json!({
1554 "type": "vaultTransfer",
1555 "vaultAddress": vault_address,
1556 "isDeposit": true,
1557 "usd": amount,
1558 });
1559
1560 self.inner.build_sign_send(&action, None).await
1561 }
1562
1563 pub async fn vault_withdraw(&self, vault_address: &str, amount: f64) -> Result<Value> {
1565 let action = json!({
1566 "type": "vaultTransfer",
1567 "vaultAddress": vault_address,
1568 "isDeposit": false,
1569 "usd": amount,
1570 });
1571
1572 self.inner.build_sign_send(&action, None).await
1573 }
1574
1575 pub async fn stake(&self, amount_tokens: f64) -> Result<Value> {
1581 let nonce = SystemTime::now()
1582 .duration_since(UNIX_EPOCH)
1583 .unwrap()
1584 .as_millis() as u64;
1585
1586 let wei = (amount_tokens * 1e18) as u128;
1587
1588 let action = json!({
1589 "type": "cDeposit",
1590 "hyperliquidChain": self.inner.chain.to_string(),
1591 "signatureChainId": self.inner.chain.signature_chain_id(),
1592 "wei": wei.to_string(),
1593 "nonce": nonce,
1594 });
1595
1596 self.inner.build_sign_send(&action, None).await
1597 }
1598
1599 pub async fn unstake(&self, amount_tokens: f64) -> Result<Value> {
1601 let nonce = SystemTime::now()
1602 .duration_since(UNIX_EPOCH)
1603 .unwrap()
1604 .as_millis() as u64;
1605
1606 let wei = (amount_tokens * 1e18) as u128;
1607
1608 let action = json!({
1609 "type": "cWithdraw",
1610 "hyperliquidChain": self.inner.chain.to_string(),
1611 "signatureChainId": self.inner.chain.signature_chain_id(),
1612 "wei": wei.to_string(),
1613 "nonce": nonce,
1614 });
1615
1616 self.inner.build_sign_send(&action, None).await
1617 }
1618
1619 pub async fn delegate(&self, validator: &str, amount_tokens: f64) -> Result<Value> {
1621 let nonce = SystemTime::now()
1622 .duration_since(UNIX_EPOCH)
1623 .unwrap()
1624 .as_millis() as u64;
1625
1626 let wei = (amount_tokens * 1e18) as u128;
1627
1628 let action = json!({
1629 "type": "tokenDelegate",
1630 "hyperliquidChain": self.inner.chain.to_string(),
1631 "signatureChainId": self.inner.chain.signature_chain_id(),
1632 "validator": validator,
1633 "isUndelegate": false,
1634 "wei": wei.to_string(),
1635 "nonce": nonce,
1636 });
1637
1638 self.inner.build_sign_send(&action, None).await
1639 }
1640
1641 pub async fn undelegate(&self, validator: &str, amount_tokens: f64) -> Result<Value> {
1643 let nonce = SystemTime::now()
1644 .duration_since(UNIX_EPOCH)
1645 .unwrap()
1646 .as_millis() as u64;
1647
1648 let wei = (amount_tokens * 1e18) as u128;
1649
1650 let action = json!({
1651 "type": "tokenDelegate",
1652 "hyperliquidChain": self.inner.chain.to_string(),
1653 "signatureChainId": self.inner.chain.signature_chain_id(),
1654 "validator": validator,
1655 "isUndelegate": true,
1656 "wei": wei.to_string(),
1657 "nonce": nonce,
1658 });
1659
1660 self.inner.build_sign_send(&action, None).await
1661 }
1662
1663 pub async fn approve_builder_fee(&self, max_fee: Option<&str>) -> Result<Value> {
1669 let fee = max_fee.unwrap_or(&self.max_fee);
1670
1671 let action = json!({
1672 "type": "approveBuilderFee",
1673 "maxFeeRate": fee,
1674 });
1675
1676 self.inner.build_sign_send(&action, None).await
1677 }
1678
1679 pub async fn revoke_builder_fee(&self) -> Result<Value> {
1681 self.approve_builder_fee(Some("0%")).await
1682 }
1683
1684 pub async fn approval_status(&self) -> Result<Value> {
1686 let address = self
1687 .inner
1688 .address
1689 .ok_or_else(|| Error::ConfigError("No address configured".to_string()))?;
1690
1691 let url = format!("{}/approval", DEFAULT_WORKER_URL);
1693
1694 let response = self
1695 .inner
1696 .http_client
1697 .post(&url)
1698 .json(&json!({"user": format!("{:?}", address)}))
1699 .send()
1700 .await?;
1701
1702 let text = response.text().await?;
1703 serde_json::from_str(&text).map_err(|e| Error::JsonError(e.to_string()))
1704 }
1705
1706 pub async fn reserve_request_weight(&self, weight: i32) -> Result<Value> {
1712 let action = json!({
1713 "type": "reserveRequestWeight",
1714 "weight": weight,
1715 });
1716
1717 self.inner.build_sign_send(&action, None).await
1718 }
1719
1720 pub async fn noop(&self) -> Result<Value> {
1722 let action = json!({"type": "noop"});
1723 self.inner.build_sign_send(&action, None).await
1724 }
1725
1726 pub async fn preflight(
1728 &self,
1729 asset: &str,
1730 side: Side,
1731 price: f64,
1732 size: f64,
1733 ) -> Result<Value> {
1734 let url = format!("{}/preflight", DEFAULT_WORKER_URL);
1735
1736 let body = json!({
1737 "asset": asset,
1738 "side": side.to_string(),
1739 "price": price,
1740 "size": size,
1741 });
1742
1743 let response = self
1744 .inner
1745 .http_client
1746 .post(&url)
1747 .json(&body)
1748 .send()
1749 .await?;
1750
1751 let text = response.text().await?;
1752 serde_json::from_str(&text).map_err(|e| Error::JsonError(e.to_string()))
1753 }
1754
1755 pub async fn approve_agent(
1761 &self,
1762 agent_address: &str,
1763 name: Option<&str>,
1764 ) -> Result<Value> {
1765 let nonce = SystemTime::now()
1766 .duration_since(UNIX_EPOCH)
1767 .unwrap()
1768 .as_millis() as u64;
1769
1770 let action = json!({
1771 "type": "approveAgent",
1772 "hyperliquidChain": self.inner.chain.as_str(),
1773 "signatureChainId": self.inner.chain.signature_chain_id(),
1774 "agentAddress": agent_address,
1775 "agentName": name,
1776 "nonce": nonce,
1777 });
1778
1779 self.inner.build_sign_send(&action, None).await
1780 }
1781
1782 pub async fn set_abstraction(&self, mode: &str, user: Option<&str>) -> Result<Value> {
1790 let address = self
1791 .inner
1792 .address
1793 .ok_or_else(|| Error::ConfigError("No address configured".to_string()))?;
1794
1795 let addr_string = format!("{:?}", address);
1796 let user_addr = user.unwrap_or(&addr_string);
1797 let nonce = SystemTime::now()
1798 .duration_since(UNIX_EPOCH)
1799 .unwrap()
1800 .as_millis() as u64;
1801
1802 let action = json!({
1803 "type": "userSetAbstraction",
1804 "hyperliquidChain": self.inner.chain.as_str(),
1805 "signatureChainId": self.inner.chain.signature_chain_id(),
1806 "user": user_addr,
1807 "abstraction": mode,
1808 "nonce": nonce,
1809 });
1810
1811 self.inner.build_sign_send(&action, None).await
1812 }
1813
1814 pub async fn agent_set_abstraction(&self, mode: &str) -> Result<Value> {
1816 let short_mode = match mode {
1818 "disabled" | "i" => "i",
1819 "unifiedAccount" | "u" => "u",
1820 "portfolioMargin" | "p" => "p",
1821 _ => {
1822 return Err(Error::ValidationError(format!(
1823 "Invalid mode: {}. Use 'disabled', 'unifiedAccount', or 'portfolioMargin'",
1824 mode
1825 )))
1826 }
1827 };
1828
1829 let action = json!({
1830 "type": "agentSetAbstraction",
1831 "abstraction": short_mode,
1832 });
1833
1834 self.inner.build_sign_send(&action, None).await
1835 }
1836
1837 pub async fn send_asset(
1843 &self,
1844 token: &str,
1845 amount: f64,
1846 destination: &str,
1847 source_dex: Option<&str>,
1848 destination_dex: Option<&str>,
1849 from_sub_account: Option<&str>,
1850 ) -> Result<Value> {
1851 let nonce = SystemTime::now()
1852 .duration_since(UNIX_EPOCH)
1853 .unwrap()
1854 .as_millis() as u64;
1855
1856 let action = json!({
1857 "type": "sendAsset",
1858 "hyperliquidChain": self.inner.chain.as_str(),
1859 "signatureChainId": self.inner.chain.signature_chain_id(),
1860 "destination": destination,
1861 "sourceDex": source_dex.unwrap_or(""),
1862 "destinationDex": destination_dex.unwrap_or(""),
1863 "token": token,
1864 "amount": amount.to_string(),
1865 "fromSubAccount": from_sub_account.unwrap_or(""),
1866 "nonce": nonce,
1867 });
1868
1869 self.inner.build_sign_send(&action, None).await
1870 }
1871
1872 pub async fn send_to_evm_with_data(
1874 &self,
1875 token: &str,
1876 amount: f64,
1877 destination: &str,
1878 data: &str,
1879 source_dex: &str,
1880 destination_chain_id: u32,
1881 gas_limit: u64,
1882 ) -> Result<Value> {
1883 let nonce = SystemTime::now()
1884 .duration_since(UNIX_EPOCH)
1885 .unwrap()
1886 .as_millis() as u64;
1887
1888 let action = json!({
1889 "type": "sendToEvmWithData",
1890 "hyperliquidChain": self.inner.chain.as_str(),
1891 "signatureChainId": self.inner.chain.signature_chain_id(),
1892 "token": token,
1893 "amount": amount.to_string(),
1894 "sourceDex": source_dex,
1895 "destinationRecipient": destination,
1896 "addressEncoding": "hex",
1897 "destinationChainId": destination_chain_id,
1898 "gasLimit": gas_limit,
1899 "data": data,
1900 "nonce": nonce,
1901 });
1902
1903 self.inner.build_sign_send(&action, None).await
1904 }
1905
1906 pub async fn top_up_isolated_only_margin(
1912 &self,
1913 asset: &str,
1914 leverage: f64,
1915 ) -> Result<Value> {
1916 let asset_idx = self
1917 .inner
1918 .metadata
1919 .resolve_asset(asset)
1920 .ok_or_else(|| Error::ValidationError(format!("Unknown asset: {}", asset)))?;
1921
1922 let action = json!({
1923 "type": "topUpIsolatedOnlyMargin",
1924 "asset": asset_idx,
1925 "leverage": leverage.to_string(),
1926 });
1927
1928 self.inner.build_sign_send(&action, None).await
1929 }
1930
1931 pub async fn validator_l1_stream(&self, risk_free_rate: &str) -> Result<Value> {
1937 let action = json!({
1938 "type": "validatorL1Stream",
1939 "riskFreeRate": risk_free_rate,
1940 });
1941
1942 self.inner.build_sign_send(&action, None).await
1943 }
1944
1945 pub async fn cancel_by_cloid(&self, cloid: &str, asset: &str) -> Result<Value> {
1951 let asset_idx = self
1952 .inner
1953 .metadata
1954 .resolve_asset(asset)
1955 .ok_or_else(|| Error::ValidationError(format!("Unknown asset: {}", asset)))?;
1956
1957 let action = json!({
1958 "type": "cancelByCloid",
1959 "cancels": [{"asset": asset_idx, "cloid": cloid}],
1960 });
1961
1962 self.inner.build_sign_send(&action, None).await
1963 }
1964
1965 pub async fn schedule_cancel(&self, time_ms: Option<u64>) -> Result<Value> {
1967 let mut action = json!({"type": "scheduleCancel"});
1968 if let Some(t) = time_ms {
1969 action["time"] = json!(t);
1970 }
1971 self.inner.build_sign_send(&action, None).await
1972 }
1973
1974 pub async fn get_mid(&self, asset: &str) -> Result<f64> {
1982 self.inner.get_mid_price(asset).await
1983 }
1984
1985 pub async fn refresh_markets(&self) -> Result<()> {
1987 self.inner.refresh_metadata().await
1988 }
1989}
1990
1991pub struct MarketOrderBuilder {
1997 inner: Arc<HyperliquidSDKInner>,
1998 asset: String,
1999 side: Side,
2000 size: Option<f64>,
2001 notional: Option<f64>,
2002 slippage: Option<f64>,
2003 reduce_only: bool,
2004}
2005
2006impl MarketOrderBuilder {
2007 fn new(inner: Arc<HyperliquidSDKInner>, asset: String, side: Side) -> Self {
2008 Self {
2009 inner,
2010 asset,
2011 side,
2012 size: None,
2013 notional: None,
2014 slippage: None,
2015 reduce_only: false,
2016 }
2017 }
2018
2019 pub fn size(mut self, size: f64) -> Self {
2021 self.size = Some(size);
2022 self
2023 }
2024
2025 pub fn notional(mut self, notional: f64) -> Self {
2027 self.notional = Some(notional);
2028 self
2029 }
2030
2031 pub fn slippage(mut self, slippage: f64) -> Self {
2035 self.slippage = Some(slippage);
2036 self
2037 }
2038
2039 pub fn reduce_only(mut self) -> Self {
2041 self.reduce_only = true;
2042 self
2043 }
2044
2045 pub async fn execute(self) -> Result<PlacedOrder> {
2050 let sz_decimals = self.inner.metadata.get_asset(&self.asset)
2052 .map(|a| a.sz_decimals)
2053 .unwrap_or(5) as i32;
2054
2055 let size = if let Some(s) = self.size {
2056 s
2057 } else if let Some(notional) = self.notional {
2058 let mid = self.inner.get_mid_price(&self.asset).await?;
2059 notional / mid
2060 } else {
2061 return Err(Error::ValidationError(
2062 "Market order must have size or notional".to_string(),
2063 ));
2064 };
2065
2066 let size_rounded = (size * 10f64.powi(sz_decimals)).round() / 10f64.powi(sz_decimals);
2068
2069 let mut order_spec = json!({
2071 "asset": self.asset,
2072 "side": if self.side.is_buy() { "buy" } else { "sell" },
2073 "size": format!("{}", size_rounded),
2074 "tif": "market",
2075 });
2076 if self.reduce_only {
2077 order_spec["reduceOnly"] = json!(true);
2078 }
2079 let action = json!({
2080 "type": "order",
2081 "orders": [order_spec],
2082 });
2083
2084 let response = self.inner.build_sign_send(&action, self.slippage).await?;
2085
2086 Ok(PlacedOrder::from_response(
2087 response,
2088 self.asset,
2089 self.side,
2090 Decimal::from_f64_retain(size_rounded).unwrap_or_default(),
2091 None,
2092 Some(self.inner),
2093 ))
2094 }
2095}
2096
2097impl std::future::IntoFuture for MarketOrderBuilder {
2099 type Output = Result<PlacedOrder>;
2100 type IntoFuture = std::pin::Pin<Box<dyn std::future::Future<Output = Self::Output> + Send>>;
2101
2102 fn into_future(self) -> Self::IntoFuture {
2103 Box::pin(self.execute())
2104 }
2105}