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) -> Result<BuildResponse> {
388 let url = self.exchange_url();
389
390 let body = json!({ "action": action });
391
392 let response = self
393 .http_client
394 .post(url)
395 .json(&body)
396 .send()
397 .await?;
398
399 let status = response.status();
400 let text = response.text().await?;
401
402 if !status.is_success() {
403 return Err(Error::NetworkError(format!(
404 "Build request failed {}: {}",
405 status, text
406 )));
407 }
408
409 let result: Value = serde_json::from_str(&text)?;
410
411 if let Some(error) = result.get("error") {
413 return Err(Error::from_api_error(
414 error.as_str().unwrap_or("Unknown error"),
415 ));
416 }
417
418 Ok(BuildResponse {
419 hash: result
420 .get("hash")
421 .and_then(|h| h.as_str())
422 .unwrap_or("")
423 .to_string(),
424 nonce: result.get("nonce").and_then(|n| n.as_u64()).unwrap_or(0),
425 action: result.get("action").cloned().unwrap_or(action.clone()),
426 })
427 }
428
429 pub async fn send_action(
431 &self,
432 action: &Value,
433 nonce: u64,
434 signature: &Signature,
435 ) -> Result<Value> {
436 let url = self.exchange_url();
437
438 let body = json!({
439 "action": action,
440 "nonce": nonce,
441 "signature": signature,
442 });
443
444 let response = self
445 .http_client
446 .post(url)
447 .json(&body)
448 .send()
449 .await?;
450
451 let status = response.status();
452 let text = response.text().await?;
453
454 if !status.is_success() {
455 return Err(Error::NetworkError(format!(
456 "Send request failed {}: {}",
457 status, text
458 )));
459 }
460
461 let result: Value = serde_json::from_str(&text)?;
462
463 if let Some(hl_status) = result.get("status") {
465 if hl_status.as_str() == Some("err") {
466 if let Some(response) = result.get("response") {
467 let raw = response.as_str()
468 .map(|s| s.to_string())
469 .unwrap_or_else(|| response.to_string());
470 return Err(Error::from_api_error(&raw));
471 }
472 }
473 }
474
475 Ok(result)
476 }
477
478 pub async fn build_sign_send(&self, action: &Value) -> Result<Value> {
480 let signer = self
481 .signer
482 .as_ref()
483 .ok_or_else(|| Error::ConfigError("No private key configured".to_string()))?;
484
485 let build_result = self.build_action(action).await?;
487
488 let hash_bytes = hex::decode(build_result.hash.trim_start_matches("0x"))
490 .map_err(|e| Error::SigningError(format!("Invalid hash: {}", e)))?;
491
492 let hash = alloy::primitives::B256::from_slice(&hash_bytes);
493 let signature = sign_hash(signer, hash).await?;
494
495 self.send_action(&build_result.action, build_result.nonce, &signature)
497 .await
498 }
499
500 pub async fn refresh_metadata(&self) -> Result<()> {
502 let meta = self.query_info(&json!({"type": "meta"})).await?;
504
505 let spot_meta = self.query_info(&json!({"type": "spotMeta"})).await.ok();
507
508 let dexes_result = self.query_info(&json!({"type": "perpDexs"})).await.ok();
510 let dexes: Vec<String> = dexes_result
511 .and_then(|v| {
512 v.as_array().map(|arr| {
513 arr.iter()
514 .filter_map(|d| d.get("name").and_then(|n| n.as_str()).map(|s| s.to_string()))
515 .collect()
516 })
517 })
518 .unwrap_or_default();
519
520 self.metadata.update(&meta, spot_meta.as_ref(), &dexes);
521
522 Ok(())
523 }
524
525 pub async fn fetch_all_mids(&self) -> Result<HashMap<String, f64>> {
527 let result = self.query_info(&json!({"type": "allMids"})).await?;
528
529 let mut mids = HashMap::new();
530 if let Some(obj) = result.as_object() {
531 for (coin, price_val) in obj {
532 let price_str = price_val.as_str().unwrap_or("");
533 if let Ok(price) = price_str.parse::<f64>() {
534 mids.insert(coin.clone(), price);
535 self.mid_prices.insert(coin.clone(), price);
536 }
537 }
538 }
539
540 for dex in self.metadata.get_dexes() {
542 if let Ok(dex_result) = self.query_info(&json!({"type": "allMids", "dex": dex})).await {
543 if let Some(obj) = dex_result.as_object() {
544 for (coin, price_val) in obj {
545 let price_str = price_val.as_str().unwrap_or("");
546 if let Ok(price) = price_str.parse::<f64>() {
547 mids.insert(coin.clone(), price);
548 self.mid_prices.insert(coin.clone(), price);
549 }
550 }
551 }
552 }
553 }
554
555 Ok(mids)
556 }
557
558 pub async fn get_mid_price(&self, asset: &str) -> Result<f64> {
560 if let Some(price) = self.mid_prices.get(asset) {
561 return Ok(*price);
562 }
563
564 let mids = self.fetch_all_mids().await?;
566 mids.get(asset)
567 .copied()
568 .ok_or_else(|| Error::ValidationError(format!("No price found for {}", asset)))
569 }
570
571 pub fn resolve_asset(&self, name: &str) -> Option<usize> {
573 self.metadata.resolve_asset(name)
574 }
575
576 pub async fn cancel_by_oid(&self, oid: u64, asset: &str) -> Result<Value> {
578 let asset_index = self
579 .resolve_asset(asset)
580 .ok_or_else(|| Error::ValidationError(format!("Unknown asset: {}", asset)))?;
581
582 let action = json!({
583 "type": "cancel",
584 "cancels": [{
585 "a": asset_index,
586 "o": oid,
587 }]
588 });
589
590 self.build_sign_send(&action).await
591 }
592
593 pub async fn modify_by_oid(
595 &self,
596 oid: u64,
597 asset: &str,
598 side: Side,
599 price: Decimal,
600 size: Decimal,
601 ) -> Result<PlacedOrder> {
602 let asset_index = self
603 .resolve_asset(asset)
604 .ok_or_else(|| Error::ValidationError(format!("Unknown asset: {}", asset)))?;
605
606 let action = json!({
607 "type": "batchModify",
608 "modifies": [{
609 "oid": oid,
610 "order": {
611 "a": asset_index,
612 "b": side.is_buy(),
613 "p": price.normalize().to_string(),
614 "s": size.normalize().to_string(),
615 "r": false,
616 "t": {"limit": {"tif": "Gtc"}},
617 "c": "0x00000000000000000000000000000000",
618 }
619 }]
620 });
621
622 let response = self.build_sign_send(&action).await?;
623
624 Ok(PlacedOrder::from_response(
625 response,
626 asset.to_string(),
627 side,
628 size,
629 Some(price),
630 None,
631 ))
632 }
633}
634
635#[derive(Debug)]
637pub struct BuildResponse {
638 pub hash: String,
639 pub nonce: u64,
640 pub action: Value,
641}
642
643#[derive(Default)]
649pub struct HyperliquidSDKBuilder {
650 endpoint: Option<String>,
651 private_key: Option<String>,
652 testnet: bool,
653 auto_approve: bool,
654 max_fee: String,
655 slippage: f64,
656 timeout: Duration,
657}
658
659impl HyperliquidSDKBuilder {
660 pub fn new() -> Self {
662 Self {
663 endpoint: None,
664 private_key: None,
665 testnet: false,
666 auto_approve: true,
667 max_fee: "1%".to_string(),
668 slippage: DEFAULT_SLIPPAGE,
669 timeout: Duration::from_secs(DEFAULT_TIMEOUT_SECS),
670 }
671 }
672
673 pub fn endpoint(mut self, endpoint: impl Into<String>) -> Self {
675 self.endpoint = Some(endpoint.into());
676 self
677 }
678
679 pub fn private_key(mut self, key: impl Into<String>) -> Self {
681 self.private_key = Some(key.into());
682 self
683 }
684
685 pub fn testnet(mut self, testnet: bool) -> Self {
687 self.testnet = testnet;
688 self
689 }
690
691 pub fn auto_approve(mut self, auto: bool) -> Self {
693 self.auto_approve = auto;
694 self
695 }
696
697 pub fn max_fee(mut self, fee: impl Into<String>) -> Self {
699 self.max_fee = fee.into();
700 self
701 }
702
703 pub fn slippage(mut self, slippage: f64) -> Self {
705 self.slippage = slippage;
706 self
707 }
708
709 pub fn timeout(mut self, timeout: Duration) -> Self {
711 self.timeout = timeout;
712 self
713 }
714
715 pub async fn build(self) -> Result<HyperliquidSDK> {
717 let private_key = self
719 .private_key
720 .or_else(|| std::env::var("PRIVATE_KEY").ok());
721
722 let (signer, address) = if let Some(key) = private_key {
724 let key = key.trim_start_matches("0x");
725 let signer = PrivateKeySigner::from_str(key)?;
726 let address = signer.address();
727 (Some(signer), Some(address))
728 } else {
729 (None, None)
730 };
731
732 let http_client = Client::builder()
734 .timeout(self.timeout)
735 .build()
736 .map_err(|e| Error::ConfigError(format!("Failed to create HTTP client: {}", e)))?;
737
738 let chain = if self.testnet {
739 Chain::Testnet
740 } else {
741 Chain::Mainnet
742 };
743
744 let endpoint_info = self.endpoint.as_ref().map(|ep| EndpointInfo::parse(ep));
746
747 let inner = Arc::new(HyperliquidSDKInner {
748 http_client,
749 signer,
750 address,
751 chain,
752 endpoint: self.endpoint,
753 endpoint_info,
754 slippage: self.slippage,
755 metadata: MetadataCache::default(),
756 mid_prices: DashMap::new(),
757 });
758
759 if let Err(e) = inner.refresh_metadata().await {
761 tracing::warn!("Failed to fetch initial metadata: {}", e);
762 }
763
764 Ok(HyperliquidSDK {
765 inner,
766 auto_approve: self.auto_approve,
767 max_fee: self.max_fee,
768 })
769 }
770}
771
772pub struct HyperliquidSDK {
778 inner: Arc<HyperliquidSDKInner>,
779 #[allow(dead_code)]
780 auto_approve: bool,
781 max_fee: String,
782}
783
784impl HyperliquidSDK {
785 pub fn new() -> HyperliquidSDKBuilder {
787 HyperliquidSDKBuilder::new()
788 }
789
790 pub fn address(&self) -> Option<Address> {
792 self.inner.address
793 }
794
795 pub fn chain(&self) -> Chain {
797 self.inner.chain
798 }
799
800 pub fn info(&self) -> crate::info::Info {
806 crate::info::Info::new(self.inner.clone())
807 }
808
809 pub fn core(&self) -> crate::hypercore::HyperCore {
811 crate::hypercore::HyperCore::new(self.inner.clone())
812 }
813
814 pub fn evm(&self) -> crate::evm::EVM {
816 crate::evm::EVM::new(self.inner.clone())
817 }
818
819 pub fn stream(&self) -> crate::stream::Stream {
821 crate::stream::Stream::new(self.inner.endpoint.clone())
822 }
823
824 pub fn grpc(&self) -> crate::grpc::GRPCStream {
826 crate::grpc::GRPCStream::new(self.inner.endpoint.clone())
827 }
828
829 pub fn evm_stream(&self) -> crate::evm_stream::EVMStream {
831 crate::evm_stream::EVMStream::new(self.inner.endpoint.clone())
832 }
833
834 pub async fn markets(&self) -> Result<Value> {
840 self.inner.query_info(&json!({"type": "meta"})).await
841 }
842
843 pub async fn dexes(&self) -> Result<Value> {
845 self.inner.query_info(&json!({"type": "perpDexs"})).await
846 }
847
848 pub async fn open_orders(&self) -> Result<Value> {
850 let address = self
851 .inner
852 .address
853 .ok_or_else(|| Error::ConfigError("No address configured".to_string()))?;
854
855 self.inner
856 .query_info(&json!({
857 "type": "openOrders",
858 "user": format!("{:?}", address),
859 }))
860 .await
861 }
862
863 pub async fn order_status(&self, oid: u64, dex: Option<&str>) -> Result<Value> {
865 let address = self
866 .inner
867 .address
868 .ok_or_else(|| Error::ConfigError("No address configured".to_string()))?;
869
870 let mut req = json!({
871 "type": "orderStatus",
872 "user": format!("{:?}", address),
873 "oid": oid,
874 });
875
876 if let Some(d) = dex {
877 req["dex"] = json!(d);
878 }
879
880 self.inner.query_info(&req).await
881 }
882
883 pub async fn market_buy(&self, asset: &str) -> MarketOrderBuilder {
889 MarketOrderBuilder::new(self.inner.clone(), asset.to_string(), Side::Buy)
890 }
891
892 pub async fn market_sell(&self, asset: &str) -> MarketOrderBuilder {
894 MarketOrderBuilder::new(self.inner.clone(), asset.to_string(), Side::Sell)
895 }
896
897 pub async fn buy(
899 &self,
900 asset: &str,
901 size: f64,
902 price: f64,
903 tif: TIF,
904 ) -> Result<PlacedOrder> {
905 self.place_order(asset, Side::Buy, size, Some(price), tif, false)
906 .await
907 }
908
909 pub async fn sell(
911 &self,
912 asset: &str,
913 size: f64,
914 price: f64,
915 tif: TIF,
916 ) -> Result<PlacedOrder> {
917 self.place_order(asset, Side::Sell, size, Some(price), tif, false)
918 .await
919 }
920
921 pub async fn order(&self, order: Order) -> Result<PlacedOrder> {
923 order.validate()?;
924
925 let asset = order.get_asset();
926 let side = order.get_side();
927 let tif = order.get_tif();
928
929 let size = if let Some(s) = order.get_size() {
931 s
932 } else if let Some(notional) = order.get_notional() {
933 let mid = self.inner.get_mid_price(asset).await?;
934 Decimal::from_f64_retain(notional.to_string().parse::<f64>().unwrap_or(0.0) / mid)
935 .unwrap_or_default()
936 } else {
937 return Err(Error::ValidationError(
938 "Order must have size or notional".to_string(),
939 ));
940 };
941
942 let price = if order.is_market() {
944 let mid = self.inner.get_mid_price(asset).await?;
945 let slippage = self.inner.slippage;
946 let price = if side.is_buy() {
947 mid * (1.0 + slippage)
948 } else {
949 mid * (1.0 - slippage)
950 };
951 Some(price)
952 } else {
953 order
954 .get_price()
955 .map(|p| p.to_string().parse::<f64>().unwrap_or(0.0))
956 };
957
958 self.place_order(
959 asset,
960 side,
961 size.to_string().parse::<f64>().unwrap_or(0.0),
962 price,
963 if order.is_market() { TIF::Ioc } else { tif },
964 order.is_reduce_only(),
965 )
966 .await
967 }
968
969 pub async fn trigger_order(&self, order: TriggerOrder) -> Result<PlacedOrder> {
971 order.validate()?;
972
973 let asset = order.get_asset();
974 let asset_index = self
975 .inner
976 .resolve_asset(asset)
977 .ok_or_else(|| Error::ValidationError(format!("Unknown asset: {}", asset)))?;
978
979 let sz_decimals = self.inner.metadata.get_asset(asset)
981 .map(|a| a.sz_decimals)
982 .unwrap_or(5) as u32;
983
984 let trigger_px = order
985 .get_trigger_price()
986 .ok_or_else(|| Error::ValidationError("Trigger price required".to_string()))?;
987
988 let size = order
989 .get_size()
990 .ok_or_else(|| Error::ValidationError("Size required".to_string()))?;
991
992 let size_rounded = size.round_dp(sz_decimals);
994
995 let limit_px = if order.is_market() {
997 let mid = self.inner.get_mid_price(asset).await?;
998 let slippage = self.inner.slippage;
999 let price = if order.get_side().is_buy() {
1000 mid * (1.0 + slippage)
1001 } else {
1002 mid * (1.0 - slippage)
1003 };
1004 Decimal::from_f64_retain(price.round()).unwrap_or_default()
1005 } else {
1006 order.get_limit_price().unwrap_or(trigger_px).round()
1007 };
1008
1009 let trigger_px_rounded = trigger_px.round();
1011
1012 let cloid = {
1014 let now = std::time::SystemTime::now()
1015 .duration_since(std::time::UNIX_EPOCH)
1016 .unwrap_or_default();
1017 let nanos = now.as_nanos() as u64;
1018 let hi = nanos.wrapping_mul(0x517cc1b727220a95);
1019 format!("0x{:016x}{:016x}", nanos, hi)
1020 };
1021
1022 let action = json!({
1023 "type": "order",
1024 "orders": [{
1025 "a": asset_index,
1026 "b": order.get_side().is_buy(),
1027 "p": limit_px.normalize().to_string(),
1028 "s": size_rounded.normalize().to_string(),
1029 "r": order.is_reduce_only(),
1030 "t": {
1031 "trigger": {
1032 "isMarket": order.is_market(),
1033 "triggerPx": trigger_px_rounded.normalize().to_string(),
1034 "tpsl": order.get_tpsl().to_string(),
1035 }
1036 },
1037 "c": cloid,
1038 }],
1039 "grouping": "na",
1040 });
1041
1042 let response = self.inner.build_sign_send(&action).await?;
1043
1044 Ok(PlacedOrder::from_response(
1045 response,
1046 asset.to_string(),
1047 order.get_side(),
1048 size,
1049 Some(limit_px),
1050 Some(self.inner.clone()),
1051 ))
1052 }
1053
1054 pub async fn stop_loss(
1056 &self,
1057 asset: &str,
1058 size: f64,
1059 trigger_price: f64,
1060 ) -> Result<PlacedOrder> {
1061 self.trigger_order(
1062 TriggerOrder::stop_loss(asset)
1063 .size(size)
1064 .trigger_price(trigger_price)
1065 .market(),
1066 )
1067 .await
1068 }
1069
1070 pub async fn take_profit(
1072 &self,
1073 asset: &str,
1074 size: f64,
1075 trigger_price: f64,
1076 ) -> Result<PlacedOrder> {
1077 self.trigger_order(
1078 TriggerOrder::take_profit(asset)
1079 .size(size)
1080 .trigger_price(trigger_price)
1081 .market(),
1082 )
1083 .await
1084 }
1085
1086 async fn place_order(
1088 &self,
1089 asset: &str,
1090 side: Side,
1091 size: f64,
1092 price: Option<f64>,
1093 tif: TIF,
1094 reduce_only: bool,
1095 ) -> Result<PlacedOrder> {
1096 let asset_index = self
1097 .inner
1098 .resolve_asset(asset)
1099 .ok_or_else(|| Error::ValidationError(format!("Unknown asset: {}", asset)))?;
1100
1101 let sz_decimals = self.inner.metadata.get_asset(asset)
1103 .map(|a| a.sz_decimals)
1104 .unwrap_or(5) as i32;
1105
1106 let size_rounded = (size * 10f64.powi(sz_decimals)).round() / 10f64.powi(sz_decimals);
1108
1109 let resolved_price = price.map(|p| p.round()).unwrap_or(0.0);
1111
1112 let tif_wire = match tif {
1113 TIF::Ioc => "Ioc",
1114 TIF::Gtc => "Gtc",
1115 TIF::Alo => "Alo",
1116 TIF::Market => "Ioc",
1117 };
1118
1119 let cloid = {
1121 let now = std::time::SystemTime::now()
1122 .duration_since(std::time::UNIX_EPOCH)
1123 .unwrap_or_default();
1124 let nanos = now.as_nanos() as u64;
1125 let hi = nanos.wrapping_mul(0x517cc1b727220a95);
1126 format!("0x{:016x}{:016x}", nanos, hi)
1127 };
1128
1129 let action = json!({
1130 "type": "order",
1131 "orders": [{
1132 "a": asset_index,
1133 "b": side.is_buy(),
1134 "p": format!("{}", resolved_price),
1135 "s": format!("{}", size_rounded),
1136 "r": reduce_only,
1137 "t": {"limit": {"tif": tif_wire}},
1138 "c": cloid,
1139 }],
1140 "grouping": "na",
1141 });
1142
1143 let response = self.inner.build_sign_send(&action).await?;
1144
1145 Ok(PlacedOrder::from_response(
1146 response,
1147 asset.to_string(),
1148 side,
1149 Decimal::from_f64_retain(size_rounded).unwrap_or_default(),
1150 price.map(|p| Decimal::from_f64_retain(p).unwrap_or_default()),
1151 Some(self.inner.clone()),
1152 ))
1153 }
1154
1155 pub async fn modify(
1163 &self,
1164 oid: u64,
1165 asset: &str,
1166 is_buy: bool,
1167 size: f64,
1168 price: f64,
1169 tif: TIF,
1170 reduce_only: bool,
1171 cloid: Option<&str>,
1172 ) -> Result<PlacedOrder> {
1173 let asset_idx = self
1174 .inner
1175 .metadata
1176 .resolve_asset(asset)
1177 .ok_or_else(|| Error::ValidationError(format!("Unknown asset: {}", asset)))?;
1178
1179 let sz_decimals = self.inner.metadata.get_asset(asset)
1180 .map(|a| a.sz_decimals)
1181 .unwrap_or(8) as i32;
1182 let size_rounded = (size * 10f64.powi(sz_decimals)).round() / 10f64.powi(sz_decimals);
1183
1184 let order_type = match tif {
1185 TIF::Gtc => json!({"limit": {"tif": "Gtc"}}),
1186 TIF::Ioc | TIF::Market => json!({"limit": {"tif": "Ioc"}}),
1187 TIF::Alo => json!({"limit": {"tif": "Alo"}}),
1188 };
1189
1190 let cloid_val = cloid
1191 .map(|s| s.to_string())
1192 .unwrap_or_else(|| {
1193 let now = std::time::SystemTime::now()
1194 .duration_since(std::time::UNIX_EPOCH)
1195 .unwrap_or_default();
1196 let nanos = now.as_nanos() as u64;
1197 let hi = nanos.wrapping_mul(0x517cc1b727220a95);
1198 format!("0x{:016x}{:016x}", nanos, hi)
1199 });
1200
1201 let action = json!({
1202 "type": "batchModify",
1203 "modifies": [{
1204 "oid": oid,
1205 "order": {
1206 "a": asset_idx,
1207 "b": is_buy,
1208 "p": format!("{:.8}", price).trim_end_matches('0').trim_end_matches('.'),
1209 "s": format!("{:.8}", size_rounded).trim_end_matches('0').trim_end_matches('.'),
1210 "r": reduce_only,
1211 "t": order_type,
1212 "c": cloid_val,
1213 }
1214 }]
1215 });
1216
1217 let response = self.inner.build_sign_send(&action).await?;
1218
1219 Ok(PlacedOrder::from_response(
1220 response,
1221 asset.to_string(),
1222 if is_buy { Side::Buy } else { Side::Sell },
1223 Decimal::from_f64_retain(size_rounded).unwrap_or_default(),
1224 Some(Decimal::from_f64_retain(price).unwrap_or_default()),
1225 Some(self.inner.clone()),
1226 ))
1227 }
1228
1229 pub async fn cancel(&self, oid: u64, asset: &str) -> Result<Value> {
1231 self.inner.cancel_by_oid(oid, asset).await
1232 }
1233
1234 pub async fn cancel_all(&self, asset: Option<&str>) -> Result<Value> {
1236 if self.inner.address.is_none() {
1238 return Err(Error::ConfigError("No address configured".to_string()));
1239 }
1240
1241 let open_orders = self.open_orders().await?;
1243
1244 let cancels: Vec<Value> = open_orders
1245 .as_array()
1246 .unwrap_or(&vec![])
1247 .iter()
1248 .filter(|order| {
1249 if let Some(asset) = asset {
1250 order.get("coin").and_then(|c| c.as_str()) == Some(asset)
1251 } else {
1252 true
1253 }
1254 })
1255 .filter_map(|order| {
1256 let oid = order.get("oid").and_then(|o| o.as_u64())?;
1257 let coin = order.get("coin").and_then(|c| c.as_str())?;
1258 let asset_index = self.inner.resolve_asset(coin)?;
1259 Some(json!({"a": asset_index, "o": oid}))
1260 })
1261 .collect();
1262
1263 if cancels.is_empty() {
1264 return Ok(json!({"status": "ok", "message": "No orders to cancel"}));
1265 }
1266
1267 let action = json!({
1268 "type": "cancel",
1269 "cancels": cancels,
1270 });
1271
1272 self.inner.build_sign_send(&action).await
1273 }
1274
1275 pub async fn close_position(&self, asset: &str) -> Result<PlacedOrder> {
1277 let address = self
1278 .inner
1279 .address
1280 .ok_or_else(|| Error::ConfigError("No address configured".to_string()))?;
1281
1282 let state = self
1284 .inner
1285 .query_info(&json!({
1286 "type": "clearinghouseState",
1287 "user": format!("{:?}", address),
1288 }))
1289 .await?;
1290
1291 let positions = state
1293 .get("assetPositions")
1294 .and_then(|p| p.as_array())
1295 .ok_or_else(|| Error::NoPosition {
1296 asset: asset.to_string(),
1297 })?;
1298
1299 let position = positions
1300 .iter()
1301 .find(|p| {
1302 p.get("position")
1303 .and_then(|pos| pos.get("coin"))
1304 .and_then(|c| c.as_str())
1305 == Some(asset)
1306 })
1307 .ok_or_else(|| Error::NoPosition {
1308 asset: asset.to_string(),
1309 })?;
1310
1311 let szi = position
1312 .get("position")
1313 .and_then(|p| p.get("szi"))
1314 .and_then(|s| s.as_str())
1315 .and_then(|s| s.parse::<f64>().ok())
1316 .ok_or_else(|| Error::NoPosition {
1317 asset: asset.to_string(),
1318 })?;
1319
1320 if szi.abs() < 1e-10 {
1321 return Err(Error::NoPosition {
1322 asset: asset.to_string(),
1323 });
1324 }
1325
1326 let side = if szi > 0.0 { Side::Sell } else { Side::Buy };
1328
1329 let mid = self.inner.get_mid_price(asset).await?;
1331 let price = if side.is_buy() {
1332 mid * (1.0 + self.inner.slippage)
1333 } else {
1334 mid * (1.0 - self.inner.slippage)
1335 };
1336
1337 self.place_order(asset, side, szi.abs(), Some(price), TIF::Ioc, true)
1338 .await
1339 }
1340
1341 pub async fn update_leverage(
1347 &self,
1348 asset: &str,
1349 leverage: i32,
1350 is_cross: bool,
1351 ) -> Result<Value> {
1352 let asset_index = self
1353 .inner
1354 .resolve_asset(asset)
1355 .ok_or_else(|| Error::ValidationError(format!("Unknown asset: {}", asset)))?;
1356
1357 let action = json!({
1358 "type": "updateLeverage",
1359 "asset": asset_index,
1360 "isCross": is_cross,
1361 "leverage": leverage,
1362 });
1363
1364 self.inner.build_sign_send(&action).await
1365 }
1366
1367 pub async fn update_isolated_margin(
1369 &self,
1370 asset: &str,
1371 is_buy: bool,
1372 amount_usd: f64,
1373 ) -> Result<Value> {
1374 let asset_index = self
1375 .inner
1376 .resolve_asset(asset)
1377 .ok_or_else(|| Error::ValidationError(format!("Unknown asset: {}", asset)))?;
1378
1379 let action = json!({
1380 "type": "updateIsolatedMargin",
1381 "asset": asset_index,
1382 "isBuy": is_buy,
1383 "ntli": (amount_usd * 1_000_000.0) as i64, });
1385
1386 self.inner.build_sign_send(&action).await
1387 }
1388
1389 pub async fn twap_order(
1395 &self,
1396 asset: &str,
1397 size: f64,
1398 is_buy: bool,
1399 duration_minutes: i64,
1400 reduce_only: bool,
1401 randomize: bool,
1402 ) -> Result<Value> {
1403 let action = json!({
1404 "type": "twapOrder",
1405 "twap": {
1406 "a": asset,
1407 "b": is_buy,
1408 "s": format!("{}", size),
1409 "r": reduce_only,
1410 "m": duration_minutes,
1411 "t": randomize,
1412 }
1413 });
1414
1415 self.inner.build_sign_send(&action).await
1416 }
1417
1418 pub async fn twap_cancel(&self, asset: &str, twap_id: i64) -> Result<Value> {
1420 let action = json!({
1421 "type": "twapCancel",
1422 "a": asset,
1423 "t": twap_id,
1424 });
1425
1426 self.inner.build_sign_send(&action).await
1427 }
1428
1429 pub async fn transfer_usd(&self, destination: &str, amount: f64) -> Result<Value> {
1435 let time = SystemTime::now()
1436 .duration_since(UNIX_EPOCH)
1437 .unwrap()
1438 .as_millis() as u64;
1439
1440 let action = json!({
1441 "type": "usdSend",
1442 "hyperliquidChain": self.inner.chain.to_string(),
1443 "signatureChainId": self.inner.chain.signature_chain_id(),
1444 "destination": destination,
1445 "amount": format!("{}", amount),
1446 "time": time,
1447 });
1448
1449 self.inner.build_sign_send(&action).await
1450 }
1451
1452 pub async fn transfer_spot(
1454 &self,
1455 token: &str,
1456 destination: &str,
1457 amount: f64,
1458 ) -> Result<Value> {
1459 let time = SystemTime::now()
1460 .duration_since(UNIX_EPOCH)
1461 .unwrap()
1462 .as_millis() as u64;
1463
1464 let action = json!({
1465 "type": "spotSend",
1466 "hyperliquidChain": self.inner.chain.to_string(),
1467 "signatureChainId": self.inner.chain.signature_chain_id(),
1468 "token": token,
1469 "destination": destination,
1470 "amount": format!("{}", amount),
1471 "time": time,
1472 });
1473
1474 self.inner.build_sign_send(&action).await
1475 }
1476
1477 pub async fn withdraw(&self, amount: f64, destination: Option<&str>) -> Result<Value> {
1479 let time = SystemTime::now()
1480 .duration_since(UNIX_EPOCH)
1481 .unwrap()
1482 .as_millis() as u64;
1483
1484 let dest = destination
1485 .map(|s| s.to_string())
1486 .or_else(|| self.inner.address.map(|a| format!("{:?}", a)))
1487 .ok_or_else(|| Error::ConfigError("No destination address".to_string()))?;
1488
1489 let action = json!({
1490 "type": "withdraw3",
1491 "hyperliquidChain": self.inner.chain.to_string(),
1492 "signatureChainId": self.inner.chain.signature_chain_id(),
1493 "destination": dest,
1494 "amount": format!("{}", amount),
1495 "time": time,
1496 });
1497
1498 self.inner.build_sign_send(&action).await
1499 }
1500
1501 pub async fn transfer_spot_to_perp(&self, amount: f64) -> Result<Value> {
1503 let nonce = SystemTime::now()
1504 .duration_since(UNIX_EPOCH)
1505 .unwrap()
1506 .as_millis() as u64;
1507
1508 let action = json!({
1509 "type": "usdClassTransfer",
1510 "hyperliquidChain": self.inner.chain.to_string(),
1511 "signatureChainId": self.inner.chain.signature_chain_id(),
1512 "amount": format!("{}", amount),
1513 "toPerp": true,
1514 "nonce": nonce,
1515 });
1516
1517 self.inner.build_sign_send(&action).await
1518 }
1519
1520 pub async fn transfer_perp_to_spot(&self, amount: f64) -> Result<Value> {
1522 let nonce = SystemTime::now()
1523 .duration_since(UNIX_EPOCH)
1524 .unwrap()
1525 .as_millis() as u64;
1526
1527 let action = json!({
1528 "type": "usdClassTransfer",
1529 "hyperliquidChain": self.inner.chain.to_string(),
1530 "signatureChainId": self.inner.chain.signature_chain_id(),
1531 "amount": format!("{}", amount),
1532 "toPerp": false,
1533 "nonce": nonce,
1534 });
1535
1536 self.inner.build_sign_send(&action).await
1537 }
1538
1539 pub async fn vault_deposit(&self, vault_address: &str, amount: f64) -> Result<Value> {
1545 let action = json!({
1546 "type": "vaultTransfer",
1547 "vaultAddress": vault_address,
1548 "isDeposit": true,
1549 "usd": amount,
1550 });
1551
1552 self.inner.build_sign_send(&action).await
1553 }
1554
1555 pub async fn vault_withdraw(&self, vault_address: &str, amount: f64) -> Result<Value> {
1557 let action = json!({
1558 "type": "vaultTransfer",
1559 "vaultAddress": vault_address,
1560 "isDeposit": false,
1561 "usd": amount,
1562 });
1563
1564 self.inner.build_sign_send(&action).await
1565 }
1566
1567 pub async fn stake(&self, amount_tokens: f64) -> Result<Value> {
1573 let nonce = SystemTime::now()
1574 .duration_since(UNIX_EPOCH)
1575 .unwrap()
1576 .as_millis() as u64;
1577
1578 let wei = (amount_tokens * 1e18) as u128;
1579
1580 let action = json!({
1581 "type": "cDeposit",
1582 "hyperliquidChain": self.inner.chain.to_string(),
1583 "signatureChainId": self.inner.chain.signature_chain_id(),
1584 "wei": wei.to_string(),
1585 "nonce": nonce,
1586 });
1587
1588 self.inner.build_sign_send(&action).await
1589 }
1590
1591 pub async fn unstake(&self, amount_tokens: f64) -> Result<Value> {
1593 let nonce = SystemTime::now()
1594 .duration_since(UNIX_EPOCH)
1595 .unwrap()
1596 .as_millis() as u64;
1597
1598 let wei = (amount_tokens * 1e18) as u128;
1599
1600 let action = json!({
1601 "type": "cWithdraw",
1602 "hyperliquidChain": self.inner.chain.to_string(),
1603 "signatureChainId": self.inner.chain.signature_chain_id(),
1604 "wei": wei.to_string(),
1605 "nonce": nonce,
1606 });
1607
1608 self.inner.build_sign_send(&action).await
1609 }
1610
1611 pub async fn delegate(&self, validator: &str, amount_tokens: f64) -> Result<Value> {
1613 let nonce = SystemTime::now()
1614 .duration_since(UNIX_EPOCH)
1615 .unwrap()
1616 .as_millis() as u64;
1617
1618 let wei = (amount_tokens * 1e18) as u128;
1619
1620 let action = json!({
1621 "type": "tokenDelegate",
1622 "hyperliquidChain": self.inner.chain.to_string(),
1623 "signatureChainId": self.inner.chain.signature_chain_id(),
1624 "validator": validator,
1625 "isUndelegate": false,
1626 "wei": wei.to_string(),
1627 "nonce": nonce,
1628 });
1629
1630 self.inner.build_sign_send(&action).await
1631 }
1632
1633 pub async fn undelegate(&self, validator: &str, amount_tokens: f64) -> Result<Value> {
1635 let nonce = SystemTime::now()
1636 .duration_since(UNIX_EPOCH)
1637 .unwrap()
1638 .as_millis() as u64;
1639
1640 let wei = (amount_tokens * 1e18) as u128;
1641
1642 let action = json!({
1643 "type": "tokenDelegate",
1644 "hyperliquidChain": self.inner.chain.to_string(),
1645 "signatureChainId": self.inner.chain.signature_chain_id(),
1646 "validator": validator,
1647 "isUndelegate": true,
1648 "wei": wei.to_string(),
1649 "nonce": nonce,
1650 });
1651
1652 self.inner.build_sign_send(&action).await
1653 }
1654
1655 pub async fn approve_builder_fee(&self, max_fee: Option<&str>) -> Result<Value> {
1661 let fee = max_fee.unwrap_or(&self.max_fee);
1662
1663 let action = json!({
1664 "type": "approveBuilderFee",
1665 "maxFeeRate": fee,
1666 });
1667
1668 self.inner.build_sign_send(&action).await
1669 }
1670
1671 pub async fn revoke_builder_fee(&self) -> Result<Value> {
1673 self.approve_builder_fee(Some("0%")).await
1674 }
1675
1676 pub async fn approval_status(&self) -> Result<Value> {
1678 let address = self
1679 .inner
1680 .address
1681 .ok_or_else(|| Error::ConfigError("No address configured".to_string()))?;
1682
1683 let url = format!("{}/approval", DEFAULT_WORKER_URL);
1685
1686 let response = self
1687 .inner
1688 .http_client
1689 .post(&url)
1690 .json(&json!({"user": format!("{:?}", address)}))
1691 .send()
1692 .await?;
1693
1694 let text = response.text().await?;
1695 serde_json::from_str(&text).map_err(|e| Error::JsonError(e.to_string()))
1696 }
1697
1698 pub async fn reserve_request_weight(&self, weight: i32) -> Result<Value> {
1704 let action = json!({
1705 "type": "reserveRequestWeight",
1706 "weight": weight,
1707 });
1708
1709 self.inner.build_sign_send(&action).await
1710 }
1711
1712 pub async fn noop(&self) -> Result<Value> {
1714 let action = json!({"type": "noop"});
1715 self.inner.build_sign_send(&action).await
1716 }
1717
1718 pub async fn preflight(
1720 &self,
1721 asset: &str,
1722 side: Side,
1723 price: f64,
1724 size: f64,
1725 ) -> Result<Value> {
1726 let url = format!("{}/preflight", DEFAULT_WORKER_URL);
1727
1728 let body = json!({
1729 "asset": asset,
1730 "side": side.to_string(),
1731 "price": price,
1732 "size": size,
1733 });
1734
1735 let response = self
1736 .inner
1737 .http_client
1738 .post(&url)
1739 .json(&body)
1740 .send()
1741 .await?;
1742
1743 let text = response.text().await?;
1744 serde_json::from_str(&text).map_err(|e| Error::JsonError(e.to_string()))
1745 }
1746
1747 pub async fn approve_agent(
1753 &self,
1754 agent_address: &str,
1755 name: Option<&str>,
1756 ) -> Result<Value> {
1757 let nonce = SystemTime::now()
1758 .duration_since(UNIX_EPOCH)
1759 .unwrap()
1760 .as_millis() as u64;
1761
1762 let action = json!({
1763 "type": "approveAgent",
1764 "hyperliquidChain": self.inner.chain.as_str(),
1765 "signatureChainId": self.inner.chain.signature_chain_id(),
1766 "agentAddress": agent_address,
1767 "agentName": name,
1768 "nonce": nonce,
1769 });
1770
1771 self.inner.build_sign_send(&action).await
1772 }
1773
1774 pub async fn set_abstraction(&self, mode: &str, user: Option<&str>) -> Result<Value> {
1782 let address = self
1783 .inner
1784 .address
1785 .ok_or_else(|| Error::ConfigError("No address configured".to_string()))?;
1786
1787 let addr_string = format!("{:?}", address);
1788 let user_addr = user.unwrap_or(&addr_string);
1789 let nonce = SystemTime::now()
1790 .duration_since(UNIX_EPOCH)
1791 .unwrap()
1792 .as_millis() as u64;
1793
1794 let action = json!({
1795 "type": "userSetAbstraction",
1796 "hyperliquidChain": self.inner.chain.as_str(),
1797 "signatureChainId": self.inner.chain.signature_chain_id(),
1798 "user": user_addr,
1799 "abstraction": mode,
1800 "nonce": nonce,
1801 });
1802
1803 self.inner.build_sign_send(&action).await
1804 }
1805
1806 pub async fn agent_set_abstraction(&self, mode: &str) -> Result<Value> {
1808 let short_mode = match mode {
1810 "disabled" | "i" => "i",
1811 "unifiedAccount" | "u" => "u",
1812 "portfolioMargin" | "p" => "p",
1813 _ => {
1814 return Err(Error::ValidationError(format!(
1815 "Invalid mode: {}. Use 'disabled', 'unifiedAccount', or 'portfolioMargin'",
1816 mode
1817 )))
1818 }
1819 };
1820
1821 let action = json!({
1822 "type": "agentSetAbstraction",
1823 "abstraction": short_mode,
1824 });
1825
1826 self.inner.build_sign_send(&action).await
1827 }
1828
1829 pub async fn send_asset(
1835 &self,
1836 token: &str,
1837 amount: f64,
1838 destination: &str,
1839 source_dex: Option<&str>,
1840 destination_dex: Option<&str>,
1841 from_sub_account: Option<&str>,
1842 ) -> Result<Value> {
1843 let nonce = SystemTime::now()
1844 .duration_since(UNIX_EPOCH)
1845 .unwrap()
1846 .as_millis() as u64;
1847
1848 let action = json!({
1849 "type": "sendAsset",
1850 "hyperliquidChain": self.inner.chain.as_str(),
1851 "signatureChainId": self.inner.chain.signature_chain_id(),
1852 "destination": destination,
1853 "sourceDex": source_dex.unwrap_or(""),
1854 "destinationDex": destination_dex.unwrap_or(""),
1855 "token": token,
1856 "amount": amount.to_string(),
1857 "fromSubAccount": from_sub_account.unwrap_or(""),
1858 "nonce": nonce,
1859 });
1860
1861 self.inner.build_sign_send(&action).await
1862 }
1863
1864 pub async fn send_to_evm_with_data(
1866 &self,
1867 token: &str,
1868 amount: f64,
1869 destination: &str,
1870 data: &str,
1871 source_dex: &str,
1872 destination_chain_id: u32,
1873 gas_limit: u64,
1874 ) -> Result<Value> {
1875 let nonce = SystemTime::now()
1876 .duration_since(UNIX_EPOCH)
1877 .unwrap()
1878 .as_millis() as u64;
1879
1880 let action = json!({
1881 "type": "sendToEvmWithData",
1882 "hyperliquidChain": self.inner.chain.as_str(),
1883 "signatureChainId": self.inner.chain.signature_chain_id(),
1884 "token": token,
1885 "amount": amount.to_string(),
1886 "sourceDex": source_dex,
1887 "destinationRecipient": destination,
1888 "addressEncoding": "hex",
1889 "destinationChainId": destination_chain_id,
1890 "gasLimit": gas_limit,
1891 "data": data,
1892 "nonce": nonce,
1893 });
1894
1895 self.inner.build_sign_send(&action).await
1896 }
1897
1898 pub async fn top_up_isolated_only_margin(
1904 &self,
1905 asset: &str,
1906 leverage: f64,
1907 ) -> Result<Value> {
1908 let asset_idx = self
1909 .inner
1910 .metadata
1911 .resolve_asset(asset)
1912 .ok_or_else(|| Error::ValidationError(format!("Unknown asset: {}", asset)))?;
1913
1914 let action = json!({
1915 "type": "topUpIsolatedOnlyMargin",
1916 "asset": asset_idx,
1917 "leverage": leverage.to_string(),
1918 });
1919
1920 self.inner.build_sign_send(&action).await
1921 }
1922
1923 pub async fn validator_l1_stream(&self, risk_free_rate: &str) -> Result<Value> {
1929 let action = json!({
1930 "type": "validatorL1Stream",
1931 "riskFreeRate": risk_free_rate,
1932 });
1933
1934 self.inner.build_sign_send(&action).await
1935 }
1936
1937 pub async fn cancel_by_cloid(&self, cloid: &str, asset: &str) -> Result<Value> {
1943 let asset_idx = self
1944 .inner
1945 .metadata
1946 .resolve_asset(asset)
1947 .ok_or_else(|| Error::ValidationError(format!("Unknown asset: {}", asset)))?;
1948
1949 let action = json!({
1950 "type": "cancelByCloid",
1951 "cancels": [{"asset": asset_idx, "cloid": cloid}],
1952 });
1953
1954 self.inner.build_sign_send(&action).await
1955 }
1956
1957 pub async fn schedule_cancel(&self, time_ms: Option<u64>) -> Result<Value> {
1959 let mut action = json!({"type": "scheduleCancel"});
1960 if let Some(t) = time_ms {
1961 action["time"] = json!(t);
1962 }
1963 self.inner.build_sign_send(&action).await
1964 }
1965
1966 pub async fn get_mid(&self, asset: &str) -> Result<f64> {
1974 self.inner.get_mid_price(asset).await
1975 }
1976
1977 pub async fn refresh_markets(&self) -> Result<()> {
1979 self.inner.refresh_metadata().await
1980 }
1981}
1982
1983pub struct MarketOrderBuilder {
1989 inner: Arc<HyperliquidSDKInner>,
1990 asset: String,
1991 side: Side,
1992 size: Option<f64>,
1993 notional: Option<f64>,
1994}
1995
1996impl MarketOrderBuilder {
1997 fn new(inner: Arc<HyperliquidSDKInner>, asset: String, side: Side) -> Self {
1998 Self {
1999 inner,
2000 asset,
2001 side,
2002 size: None,
2003 notional: None,
2004 }
2005 }
2006
2007 pub fn size(mut self, size: f64) -> Self {
2009 self.size = Some(size);
2010 self
2011 }
2012
2013 pub fn notional(mut self, notional: f64) -> Self {
2015 self.notional = Some(notional);
2016 self
2017 }
2018
2019 pub async fn execute(self) -> Result<PlacedOrder> {
2021 let sz_decimals = self.inner.metadata.get_asset(&self.asset)
2023 .map(|a| a.sz_decimals)
2024 .unwrap_or(5) as i32;
2025
2026 let size = if let Some(s) = self.size {
2027 s
2028 } else if let Some(notional) = self.notional {
2029 let mid = self.inner.get_mid_price(&self.asset).await?;
2030 notional / mid
2031 } else {
2032 return Err(Error::ValidationError(
2033 "Market order must have size or notional".to_string(),
2034 ));
2035 };
2036
2037 let size_rounded = (size * 10f64.powi(sz_decimals)).round() / 10f64.powi(sz_decimals);
2039
2040 let mid = self.inner.get_mid_price(&self.asset).await?;
2042 let price = if self.side.is_buy() {
2043 (mid * (1.0 + self.inner.slippage)).round()
2044 } else {
2045 (mid * (1.0 - self.inner.slippage)).round()
2046 };
2047
2048 let asset_index = self
2049 .inner
2050 .resolve_asset(&self.asset)
2051 .ok_or_else(|| Error::ValidationError(format!("Unknown asset: {}", self.asset)))?;
2052
2053 let cloid = {
2055 let now = std::time::SystemTime::now()
2056 .duration_since(std::time::UNIX_EPOCH)
2057 .unwrap_or_default();
2058 let nanos = now.as_nanos() as u64;
2059 let hi = nanos.wrapping_mul(0x517cc1b727220a95);
2060 format!("0x{:016x}{:016x}", nanos, hi)
2061 };
2062
2063 let action = json!({
2064 "type": "order",
2065 "orders": [{
2066 "a": asset_index,
2067 "b": self.side.is_buy(),
2068 "p": format!("{}", price),
2069 "s": format!("{}", size_rounded),
2070 "r": false,
2071 "t": {"limit": {"tif": "Ioc"}},
2072 "c": cloid,
2073 }],
2074 "grouping": "na",
2075 });
2076
2077 let response = self.inner.build_sign_send(&action).await?;
2078
2079 Ok(PlacedOrder::from_response(
2080 response,
2081 self.asset,
2082 self.side,
2083 Decimal::from_f64_retain(size_rounded).unwrap_or_default(),
2084 Some(Decimal::from_f64_retain(price).unwrap_or_default()),
2085 Some(self.inner),
2086 ))
2087 }
2088}
2089
2090impl std::future::IntoFuture for MarketOrderBuilder {
2092 type Output = Result<PlacedOrder>;
2093 type IntoFuture = std::pin::Pin<Box<dyn std::future::Future<Output = Self::Output> + Send>>;
2094
2095 fn into_future(self) -> Self::IntoFuture {
2096 Box::pin(self.execute())
2097 }
2098}