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 HYPE_WEI_DECIMALS: u32 = 8;
38
39const QN_SUPPORTED_INFO_TYPES: &[&str] = &[
41 "meta",
42 "spotMeta",
43 "clearinghouseState",
44 "spotClearinghouseState",
45 "openOrders",
46 "exchangeStatus",
47 "frontendOpenOrders",
48 "liquidatable",
49 "activeAssetData",
50 "maxMarketOrderNtls",
51 "vaultSummaries",
52 "userVaultEquities",
53 "leadingVaults",
54 "extraAgents",
55 "subAccounts",
56 "userFees",
57 "userRateLimit",
58 "spotDeployState",
59 "perpDeployAuctionStatus",
60 "delegations",
61 "delegatorSummary",
62 "maxBuilderFee",
63 "userToMultiSigSigners",
64 "userRole",
65 "perpsAtOpenInterestCap",
66 "validatorL1Votes",
67 "marginTable",
68 "perpDexs",
69 "webData2",
70 "outcomeMeta",
71];
72
73fn parse_outcome_description(description: &str) -> HashMap<String, String> {
74 description
75 .split('|')
76 .filter_map(|part| {
77 let (key, value) = part.split_once(':')?;
78 Some((key.to_string(), value.to_string()))
79 })
80 .collect()
81}
82
83fn format_prediction_expiry(expiry: &str) -> String {
84 if expiry.len() != 13 || expiry.as_bytes().get(8) != Some(&b'-') {
85 return expiry.to_string();
86 }
87 format!(
88 "{}-{}-{}T{}:{}:00Z",
89 &expiry[0..4],
90 &expiry[4..6],
91 &expiry[6..8],
92 &expiry[9..11],
93 &expiry[11..13]
94 )
95}
96
97fn prediction_title(fields: &HashMap<String, String>) -> String {
98 let underlying = fields.get("underlying").map(String::as_str).unwrap_or("Outcome");
99 match (fields.get("targetPrice"), fields.get("expiry")) {
100 (Some(target_price), Some(expiry)) => {
101 format!("{} above {} on {}", underlying, target_price, format_prediction_expiry(expiry))
102 }
103 (Some(target_price), None) => format!("{} above {}", underlying, target_price),
104 _ => underlying.to_string(),
105 }
106}
107
108fn prediction_slug(value: &str) -> String {
109 let mut out = String::new();
110 let mut last_dash = false;
111 for ch in value.to_lowercase().chars() {
112 if ch.is_ascii_alphanumeric() {
113 out.push(ch);
114 last_dash = false;
115 } else if !last_dash && !out.is_empty() {
116 out.push('-');
117 last_dash = true;
118 }
119 }
120 out.trim_matches('-').to_string()
121}
122
123fn app_style_prediction_slug(fields: &HashMap<String, String>, side: Option<&str>) -> Option<String> {
126 let underlying = fields.get("underlying")?;
127 let target_price = fields.get("targetPrice")?;
128 let expiry = fields.get("expiry")?;
129 if expiry.len() != 13 {
130 return None;
131 }
132 let month = match &expiry[4..6] {
133 "01" => "jan",
134 "02" => "feb",
135 "03" => "mar",
136 "04" => "apr",
137 "05" => "may",
138 "06" => "jun",
139 "07" => "jul",
140 "08" => "aug",
141 "09" => "sep",
142 "10" => "oct",
143 "11" => "nov",
144 "12" => "dec",
145 _ => return None,
146 };
147 let mut parts = vec![underlying.as_str(), "above", target_price.as_str()];
148 if let Some(side) = side {
149 parts.push(side);
150 }
151 parts.extend([month, &expiry[6..8], &expiry[9..13]]);
152 Some(prediction_slug(&parts.join("-")))
153}
154
155fn is_prediction_asset(asset: &str) -> bool {
156 if let Some(rest) = asset.strip_prefix('#').or_else(|| asset.strip_prefix('+')) {
157 return !rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit());
158 }
159 asset.parse::<usize>().map(|id| id >= 100_000_000).unwrap_or(false)
160}
161
162fn prediction_symbol(asset: &str) -> String {
163 if let Some(rest) = asset.strip_prefix('+') {
164 return format!("#{}", rest);
165 }
166 if let Ok(id) = asset.parse::<usize>() {
167 if id >= 100_000_000 {
168 return format!("#{}", id - 100_000_000);
169 }
170 }
171 asset.to_string()
172}
173
174fn prediction_asset_id(asset: &str) -> Option<usize> {
175 let rest = asset.strip_prefix('#').or_else(|| asset.strip_prefix('+'))?;
176 Some(100_000_000 + rest.parse::<usize>().ok()?)
177}
178
179#[derive(Debug, Clone)]
185pub struct AssetInfo {
186 pub index: usize,
187 pub name: String,
188 pub sz_decimals: u8,
189 pub is_spot: bool,
190}
191
192#[derive(Debug, Default)]
194pub struct MetadataCache {
195 assets: RwLock<HashMap<String, AssetInfo>>,
196 assets_by_index: RwLock<HashMap<usize, AssetInfo>>,
197 dexes: RwLock<Vec<String>>,
198 last_update: RwLock<Option<SystemTime>>,
199}
200
201impl MetadataCache {
202 pub fn get_asset(&self, name: &str) -> Option<AssetInfo> {
204 self.assets.read().get(name).cloned()
205 }
206
207 pub fn get_asset_by_index(&self, index: usize) -> Option<AssetInfo> {
209 self.assets_by_index.read().get(&index).cloned()
210 }
211
212 pub fn resolve_asset(&self, name: &str) -> Option<usize> {
214 self.assets.read().get(name).map(|a| a.index)
215 }
216
217 pub fn get_dexes(&self) -> Vec<String> {
219 self.dexes.read().clone()
220 }
221
222 pub fn is_valid(&self) -> bool {
224 if let Some(last) = *self.last_update.read() {
225 if let Ok(elapsed) = last.elapsed() {
226 return elapsed.as_secs() < METADATA_CACHE_TTL_SECS;
227 }
228 }
229 false
230 }
231
232 pub fn update(&self, meta: &Value, spot_meta: Option<&Value>, dexes: &[String]) {
234 let mut assets = HashMap::new();
235 let mut assets_by_index = HashMap::new();
236
237 if let Some(universe) = meta.get("universe").and_then(|u| u.as_array()) {
239 for (i, asset) in universe.iter().enumerate() {
240 if let Some(name) = asset.get("name").and_then(|n| n.as_str()) {
241 let sz_decimals = asset
242 .get("szDecimals")
243 .and_then(|d| d.as_u64())
244 .unwrap_or(8) as u8;
245
246 let info = AssetInfo {
247 index: i,
248 name: name.to_string(),
249 sz_decimals,
250 is_spot: false,
251 };
252 assets.insert(name.to_string(), info.clone());
253 assets_by_index.insert(i, info);
254 }
255 }
256 }
257
258 if let Some(spot) = spot_meta {
260 if let Some(tokens) = spot.get("tokens").and_then(|t| t.as_array()) {
261 for token in tokens {
262 if let (Some(name), Some(index)) = (
263 token.get("name").and_then(|n| n.as_str()),
264 token.get("index").and_then(|i| i.as_u64()),
265 ) {
266 let sz_decimals = token
267 .get("szDecimals")
268 .and_then(|d| d.as_u64())
269 .unwrap_or(8) as u8;
270
271 let info = AssetInfo {
272 index: index as usize,
273 name: name.to_string(),
274 sz_decimals,
275 is_spot: true,
276 };
277 assets.insert(name.to_string(), info.clone());
278 assets_by_index.insert(index as usize, info);
279 }
280 }
281 }
282 }
283
284 *self.assets.write() = assets;
285 *self.assets_by_index.write() = assets_by_index;
286 *self.dexes.write() = dexes.to_vec();
287 *self.last_update.write() = Some(SystemTime::now());
288 }
289
290 pub fn update_outcomes(&self, outcome_meta: &Value) {
292 let mut assets = self.assets.write();
293 let mut assets_by_index = self.assets_by_index.write();
294
295 if let Some(outcomes) = outcome_meta.get("outcomes").and_then(|o| o.as_array()) {
296 for outcome in outcomes {
297 let Some(outcome_id) = outcome.get("outcome").and_then(|o| o.as_u64()) else {
298 continue;
299 };
300 let side_count = outcome
301 .get("sideSpecs")
302 .and_then(|s| s.as_array())
303 .map(Vec::len)
304 .unwrap_or(0);
305 for side_index in 0..side_count {
306 let encoding = outcome_id as usize * 10 + side_index;
307 let name = format!("#{}", encoding);
308 let info = AssetInfo {
309 index: 100_000_000 + encoding,
310 name: name.clone(),
311 sz_decimals: 0,
312 is_spot: false,
313 };
314 assets.insert(name, info.clone());
315 assets_by_index.insert(info.index, info);
316 }
317 }
318 }
319 }
320}
321
322#[derive(Debug, Clone)]
328pub struct EndpointInfo {
329 pub base: String,
331 pub token: Option<String>,
333 pub is_quicknode: bool,
335}
336
337impl EndpointInfo {
338 pub fn parse(url: &str) -> Self {
345 let parsed = url::Url::parse(url).ok();
346
347 if let Some(parsed) = parsed {
348 let base = format!("{}://{}", parsed.scheme(), parsed.host_str().unwrap_or(""));
349 let is_quicknode = parsed.host_str().map(|h| h.contains("quiknode.pro")).unwrap_or(false);
350
351 let path_parts: Vec<&str> = parsed.path()
353 .trim_matches('/')
354 .split('/')
355 .filter(|p| !p.is_empty())
356 .collect();
357
358 let token = path_parts.iter()
360 .find(|&part| !KNOWN_PATHS.contains(part))
361 .map(|s| s.to_string());
362
363 Self { base, token, is_quicknode }
364 } else {
365 Self {
367 base: url.to_string(),
368 token: None,
369 is_quicknode: url.contains("quiknode.pro"),
370 }
371 }
372 }
373
374 pub fn build_url(&self, suffix: &str) -> String {
376 if let Some(ref token) = self.token {
377 format!("{}/{}/{}", self.base, token, suffix)
378 } else {
379 format!("{}/{}", self.base, suffix)
380 }
381 }
382
383 pub fn build_ws_url(&self) -> String {
385 let ws_base = self.base.replace("https://", "wss://").replace("http://", "ws://");
386 if let Some(ref token) = self.token {
387 format!("{}/{}/hypercore/ws", ws_base, token)
388 } else {
389 format!("{}/ws", ws_base)
390 }
391 }
392
393 pub fn build_grpc_url(&self) -> String {
395 if let Some(ref token) = self.token {
397 let grpc_base = self.base.replace(":443", "").replace("https://", "");
398 format!("https://{}:10000/{}", grpc_base, token)
399 } else {
400 self.base.replace(":443", ":10000")
401 }
402 }
403}
404
405pub struct HyperliquidSDKInner {
407 pub(crate) http_client: Client,
408 pub(crate) signer: Option<PrivateKeySigner>,
409 pub(crate) address: Option<Address>,
410 pub(crate) chain: Chain,
411 pub(crate) endpoint: Option<String>,
412 pub(crate) endpoint_info: Option<EndpointInfo>,
413 pub(crate) slippage: f64,
414 pub(crate) metadata: MetadataCache,
415 pub(crate) mid_prices: DashMap<String, f64>,
416}
417
418impl std::fmt::Debug for HyperliquidSDKInner {
419 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
420 f.debug_struct("HyperliquidSDKInner")
421 .field("address", &self.address)
422 .field("chain", &self.chain)
423 .field("endpoint", &self.endpoint)
424 .field("slippage", &self.slippage)
425 .finish_non_exhaustive()
426 }
427}
428
429const DEFAULT_EXCHANGE_URL: &str = "https://send.hyperliquidapi.com/exchange";
431
432impl HyperliquidSDKInner {
433 fn exchange_url(&self) -> String {
439 DEFAULT_EXCHANGE_URL.to_string()
440 }
441
442 fn info_url(&self, query_type: &str) -> String {
444 if let Some(ref info) = self.endpoint_info {
445 if info.is_quicknode && QN_SUPPORTED_INFO_TYPES.contains(&query_type) {
447 return info.build_url("info");
448 }
449 }
450 DEFAULT_WORKER_INFO_URL.to_string()
452 }
453
454 pub fn hypercore_url(&self) -> String {
456 if let Some(ref info) = self.endpoint_info {
457 if info.is_quicknode {
458 return info.build_url("hypercore");
459 }
460 }
461 HL_INFO_URL.to_string()
463 }
464
465 pub fn evm_url(&self, use_nanoreth: bool) -> String {
467 if let Some(ref info) = self.endpoint_info {
468 if info.is_quicknode {
469 let suffix = if use_nanoreth { "nanoreth" } else { "evm" };
470 return info.build_url(suffix);
471 }
472 }
473 match self.chain {
475 Chain::Mainnet => "https://rpc.hyperliquid.xyz/evm".to_string(),
476 Chain::Testnet => "https://rpc.hyperliquid-testnet.xyz/evm".to_string(),
477 }
478 }
479
480 pub fn ws_url(&self) -> String {
482 if let Some(ref info) = self.endpoint_info {
483 return info.build_ws_url();
484 }
485 "wss://api.hyperliquid.xyz/ws".to_string()
487 }
488
489 pub fn grpc_url(&self) -> String {
491 if let Some(ref info) = self.endpoint_info {
492 if info.is_quicknode {
493 return info.build_grpc_url();
494 }
495 }
496 String::new()
498 }
499
500 pub async fn query_info(&self, body: &Value) -> Result<Value> {
502 let query_type = body.get("type").and_then(|t| t.as_str()).unwrap_or("");
503 let url = self.info_url(query_type);
504
505 let response = self
506 .http_client
507 .post(&url)
508 .json(body)
509 .send()
510 .await?;
511
512 let status = response.status();
513 let text = response.text().await?;
514
515 if !status.is_success() {
516 return Err(Error::NetworkError(format!(
517 "Info endpoint returned {}: {}",
518 status, text
519 )));
520 }
521
522 serde_json::from_str(&text).map_err(|e| Error::JsonError(e.to_string()))
523 }
524
525 pub async fn build_action(&self, action: &Value, slippage: Option<f64>) -> Result<BuildResponse> {
527 self.build_action_with_priority(action, slippage, None).await
528 }
529
530 pub async fn build_action_with_priority(
532 &self,
533 action: &Value,
534 slippage: Option<f64>,
535 priority_fee: Option<u64>,
536 ) -> Result<BuildResponse> {
537 let url = self.exchange_url();
538
539 let mut body = json!({ "action": action });
540 if let Some(priority_fee) = priority_fee {
541 body["priorityFee"] = json!(priority_fee);
542 }
543 if let Some(s) = slippage {
544 if !s.is_finite() || s <= 0.0 {
545 return Err(Error::ValidationError(
546 "Slippage must be a positive finite number".to_string(),
547 ));
548 }
549 body["slippage"] = json!(s);
550 }
551
552 let response = self
553 .http_client
554 .post(url)
555 .json(&body)
556 .send()
557 .await?;
558
559 let status = response.status();
560 let text = response.text().await?;
561
562 if !status.is_success() {
563 return Err(Error::NetworkError(format!(
564 "Build request failed {}: {}",
565 status, text
566 )));
567 }
568
569 let result: Value = serde_json::from_str(&text)?;
570
571 if let Some(error) = result.get("error") {
573 return Err(Error::from_api_error(
574 error.as_str().unwrap_or("Unknown error"),
575 ));
576 }
577
578 Ok(BuildResponse {
579 hash: result
580 .get("hash")
581 .and_then(|h| h.as_str())
582 .unwrap_or("")
583 .to_string(),
584 nonce: result.get("nonce").and_then(|n| n.as_u64()).unwrap_or(0),
585 action: result.get("action").cloned().unwrap_or(action.clone()),
586 })
587 }
588
589 pub async fn send_action(
591 &self,
592 action: &Value,
593 nonce: u64,
594 signature: &Signature,
595 ) -> Result<Value> {
596 let url = self.exchange_url();
597
598 let body = json!({
599 "action": action,
600 "nonce": nonce,
601 "signature": signature,
602 });
603
604 let response = self
605 .http_client
606 .post(url)
607 .json(&body)
608 .send()
609 .await?;
610
611 let status = response.status();
612 let text = response.text().await?;
613
614 if !status.is_success() {
615 return Err(Error::NetworkError(format!(
616 "Send request failed {}: {}",
617 status, text
618 )));
619 }
620
621 let result: Value = serde_json::from_str(&text)?;
622
623 if let Some(hl_status) = result.get("status") {
625 if hl_status.as_str() == Some("err") {
626 if let Some(response) = result.get("response") {
627 let raw = response.as_str()
628 .map(|s| s.to_string())
629 .unwrap_or_else(|| response.to_string());
630 return Err(Error::from_api_error(&raw));
631 }
632 }
633 }
634
635 Ok(result)
636 }
637
638 pub async fn build_sign_send(&self, action: &Value, slippage: Option<f64>) -> Result<Value> {
644 self.build_sign_send_with_priority(action, slippage, None).await
645 }
646
647 pub async fn build_sign_send_with_priority(
648 &self,
649 action: &Value,
650 slippage: Option<f64>,
651 priority_fee: Option<u64>,
652 ) -> Result<Value> {
653 let signer = self
654 .signer
655 .as_ref()
656 .ok_or_else(|| Error::ConfigError("No private key configured".to_string()))?;
657
658 let effective_slippage = slippage.or_else(|| {
660 if self.slippage > 0.0 {
661 Some(self.slippage)
662 } else {
663 None
664 }
665 });
666
667 let build_result = self
669 .build_action_with_priority(action, effective_slippage, priority_fee)
670 .await?;
671
672 let hash_bytes = hex::decode(build_result.hash.trim_start_matches("0x"))
674 .map_err(|e| Error::SigningError(format!("Invalid hash: {}", e)))?;
675
676 let hash = alloy::primitives::B256::from_slice(&hash_bytes);
677 let signature = sign_hash(signer, hash).await?;
678
679 self.send_action(&build_result.action, build_result.nonce, &signature)
681 .await
682 }
683
684 pub async fn refresh_metadata(&self) -> Result<()> {
686 let meta = self.query_info(&json!({"type": "meta"})).await?;
688
689 let spot_meta = self.query_info(&json!({"type": "spotMeta"})).await.ok();
691
692 let dexes_result = self.query_info(&json!({"type": "perpDexs"})).await.ok();
694 let dexes: Vec<String> = dexes_result
695 .and_then(|v| {
696 v.as_array().map(|arr| {
697 arr.iter()
698 .filter_map(|d| d.get("name").and_then(|n| n.as_str()).map(|s| s.to_string()))
699 .collect()
700 })
701 })
702 .unwrap_or_default();
703
704 self.metadata.update(&meta, spot_meta.as_ref(), &dexes);
705
706 if let Ok(outcome_meta) = self.query_info(&json!({"type": "outcomeMeta"})).await {
707 self.metadata.update_outcomes(&outcome_meta);
708 }
709
710 Ok(())
711 }
712
713 pub async fn fetch_all_mids(&self) -> Result<HashMap<String, f64>> {
715 let result = self.query_info(&json!({"type": "allMids"})).await?;
716
717 let mut mids = HashMap::new();
718 if let Some(obj) = result.as_object() {
719 for (coin, price_val) in obj {
720 let price_str = price_val.as_str().unwrap_or("");
721 if let Ok(price) = price_str.parse::<f64>() {
722 mids.insert(coin.clone(), price);
723 self.mid_prices.insert(coin.clone(), price);
724 }
725 }
726 }
727
728 for dex in self.metadata.get_dexes() {
730 if let Ok(dex_result) = self.query_info(&json!({"type": "allMids", "dex": dex})).await {
731 if let Some(obj) = dex_result.as_object() {
732 for (coin, price_val) in obj {
733 let price_str = price_val.as_str().unwrap_or("");
734 if let Ok(price) = price_str.parse::<f64>() {
735 mids.insert(coin.clone(), price);
736 self.mid_prices.insert(coin.clone(), price);
737 }
738 }
739 }
740 }
741 }
742
743 Ok(mids)
744 }
745
746 pub async fn get_mid_price(&self, asset: &str) -> Result<f64> {
748 let asset = prediction_symbol(asset);
749 if let Some(price) = self.mid_prices.get(&asset) {
750 return Ok(*price);
751 }
752
753 let mids = self.fetch_all_mids().await?;
755 mids.get(&asset)
756 .copied()
757 .ok_or_else(|| Error::ValidationError(format!("No price found for {}", asset)))
758 }
759
760 pub fn resolve_asset(&self, name: &str) -> Option<usize> {
762 if let Some(id) = prediction_asset_id(name) {
763 return Some(id);
764 }
765 if let Ok(id) = name.parse::<usize>() {
766 return Some(id);
767 }
768 self.metadata.resolve_asset(name)
769 }
770
771 pub async fn cancel_by_oid(&self, oid: u64, asset: &str) -> Result<Value> {
773 let asset_index = self
774 .resolve_asset(asset)
775 .ok_or_else(|| Error::ValidationError(format!("Unknown asset: {}", asset)))?;
776
777 let action = json!({
778 "type": "cancel",
779 "cancels": [{
780 "a": asset_index,
781 "o": oid,
782 }]
783 });
784
785 self.build_sign_send(&action, None).await
786 }
787
788 pub async fn modify_by_oid(
790 &self,
791 oid: u64,
792 asset: &str,
793 side: Side,
794 price: Decimal,
795 size: Decimal,
796 ) -> Result<PlacedOrder> {
797 let asset_index = self
798 .resolve_asset(asset)
799 .ok_or_else(|| Error::ValidationError(format!("Unknown asset: {}", asset)))?;
800
801 let action = json!({
802 "type": "batchModify",
803 "modifies": [{
804 "oid": oid,
805 "order": {
806 "a": asset_index,
807 "b": side.is_buy(),
808 "p": price.normalize().to_string(),
809 "s": size.normalize().to_string(),
810 "r": false,
811 "t": {"limit": {"tif": "Gtc"}},
812 "c": "0x00000000000000000000000000000000",
813 }
814 }]
815 });
816
817 let response = self.build_sign_send(&action, None).await?;
818
819 Ok(PlacedOrder::from_response(
820 response,
821 asset.to_string(),
822 side,
823 size,
824 Some(price),
825 None,
826 ))
827 }
828}
829
830#[derive(Debug)]
832pub struct BuildResponse {
833 pub hash: String,
834 pub nonce: u64,
835 pub action: Value,
836}
837
838fn hype_to_wei(amount_hype: f64) -> Result<u64> {
839 if !amount_hype.is_finite() || amount_hype <= 0.0 {
840 return Err(Error::ValidationError(
841 "HYPE amount must be positive".to_string(),
842 ));
843 }
844 let wei = decimal_amount_to_wei(&amount_hype.to_string())?;
845 if wei == 0 {
846 return Err(Error::ValidationError(
847 "HYPE amount is too small; minimum unit is 0.00000001 HYPE".to_string(),
848 ));
849 }
850 Ok(wei)
851}
852
853fn decimal_amount_to_wei(raw: &str) -> Result<u64> {
854 let amount = raw.trim();
855 let parts: Vec<&str> = amount.split('.').collect();
856 if amount.is_empty() || parts.len() > 2 || !all_decimal_digits(parts[0]) {
857 return Err(Error::ValidationError(
858 "HYPE amount must be positive".to_string(),
859 ));
860 }
861
862 let mut frac = String::new();
863 if parts.len() == 2 {
864 if !all_decimal_digits(parts[1]) {
865 return Err(Error::ValidationError(
866 "HYPE amount must be positive".to_string(),
867 ));
868 }
869 frac.push_str(parts[1]);
870 }
871 let decimals = HYPE_WEI_DECIMALS as usize;
872 if frac.len() > decimals {
873 frac.truncate(decimals);
874 }
875 while frac.len() < decimals {
876 frac.push('0');
877 }
878
879 let whole_wei = parts[0]
880 .parse::<u64>()
881 .map_err(|_| Error::ValidationError("HYPE amount is too large".to_string()))?;
882 let frac_wei = frac
883 .parse::<u64>()
884 .map_err(|_| Error::ValidationError("HYPE amount must be positive".to_string()))?;
885
886 let scale = 10u64.pow(HYPE_WEI_DECIMALS);
887 whole_wei
888 .checked_mul(scale)
889 .and_then(|wei| wei.checked_add(frac_wei))
890 .ok_or_else(|| Error::ValidationError("HYPE amount is too large".to_string()))
891}
892
893fn all_decimal_digits(s: &str) -> bool {
894 !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit())
895}
896
897#[cfg(test)]
898mod client_tests {
899 use super::*;
900
901 #[test]
902 fn hype_to_wei_uses_decimal_string_arithmetic() {
903 assert_eq!(hype_to_wei(0.001).unwrap(), 100_000);
904 assert_eq!(hype_to_wei(0.58).unwrap(), 58_000_000);
905 assert_eq!(hype_to_wei(0.00000001).unwrap(), 1);
906 assert_eq!(hype_to_wei(1.234567891).unwrap(), 123_456_789);
907 assert_eq!(hype_to_wei(1.0).unwrap(), 100_000_000);
908 assert!(hype_to_wei(0.000000001).is_err());
909 }
910}
911
912#[derive(Default)]
918pub struct HyperliquidSDKBuilder {
919 endpoint: Option<String>,
920 private_key: Option<String>,
921 testnet: bool,
922 auto_approve: bool,
923 max_fee: String,
924 slippage: f64,
925 timeout: Duration,
926}
927
928impl HyperliquidSDKBuilder {
929 pub fn new() -> Self {
931 Self {
932 endpoint: None,
933 private_key: None,
934 testnet: false,
935 auto_approve: true,
936 max_fee: "1%".to_string(),
937 slippage: DEFAULT_SLIPPAGE,
938 timeout: Duration::from_secs(DEFAULT_TIMEOUT_SECS),
939 }
940 }
941
942 pub fn endpoint(mut self, endpoint: impl Into<String>) -> Self {
944 self.endpoint = Some(endpoint.into());
945 self
946 }
947
948 pub fn private_key(mut self, key: impl Into<String>) -> Self {
950 self.private_key = Some(key.into());
951 self
952 }
953
954 pub fn testnet(mut self, testnet: bool) -> Self {
956 self.testnet = testnet;
957 self
958 }
959
960 pub fn auto_approve(mut self, auto: bool) -> Self {
962 self.auto_approve = auto;
963 self
964 }
965
966 pub fn max_fee(mut self, fee: impl Into<String>) -> Self {
968 self.max_fee = fee.into();
969 self
970 }
971
972 pub fn slippage(mut self, slippage: f64) -> Self {
974 self.slippage = slippage;
975 self
976 }
977
978 pub fn timeout(mut self, timeout: Duration) -> Self {
980 self.timeout = timeout;
981 self
982 }
983
984 pub async fn build(self) -> Result<HyperliquidSDK> {
986 let private_key = self
988 .private_key
989 .or_else(|| std::env::var("PRIVATE_KEY").ok());
990
991 let (signer, address) = if let Some(key) = private_key {
993 let key = key.trim_start_matches("0x");
994 let signer = PrivateKeySigner::from_str(key)?;
995 let address = signer.address();
996 (Some(signer), Some(address))
997 } else {
998 (None, None)
999 };
1000
1001 let http_client = Client::builder()
1003 .timeout(self.timeout)
1004 .build()
1005 .map_err(|e| Error::ConfigError(format!("Failed to create HTTP client: {}", e)))?;
1006
1007 let chain = if self.testnet {
1008 Chain::Testnet
1009 } else {
1010 Chain::Mainnet
1011 };
1012
1013 let endpoint_info = self.endpoint.as_ref().map(|ep| EndpointInfo::parse(ep));
1015
1016 let inner = Arc::new(HyperliquidSDKInner {
1017 http_client,
1018 signer,
1019 address,
1020 chain,
1021 endpoint: self.endpoint,
1022 endpoint_info,
1023 slippage: self.slippage,
1024 metadata: MetadataCache::default(),
1025 mid_prices: DashMap::new(),
1026 });
1027
1028 if let Err(e) = inner.refresh_metadata().await {
1030 tracing::warn!("Failed to fetch initial metadata: {}", e);
1031 }
1032
1033 Ok(HyperliquidSDK {
1034 inner,
1035 auto_approve: self.auto_approve,
1036 max_fee: self.max_fee,
1037 })
1038 }
1039}
1040
1041pub struct HyperliquidSDK {
1047 inner: Arc<HyperliquidSDKInner>,
1048 #[allow(dead_code)]
1049 auto_approve: bool,
1050 max_fee: String,
1051}
1052
1053impl HyperliquidSDK {
1054 pub fn new() -> HyperliquidSDKBuilder {
1056 HyperliquidSDKBuilder::new()
1057 }
1058
1059 pub fn address(&self) -> Option<Address> {
1061 self.inner.address
1062 }
1063
1064 pub fn chain(&self) -> Chain {
1066 self.inner.chain
1067 }
1068
1069 pub fn info(&self) -> crate::info::Info {
1075 crate::info::Info::new(self.inner.clone())
1076 }
1077
1078 pub fn core(&self) -> crate::hypercore::HyperCore {
1080 crate::hypercore::HyperCore::new(self.inner.clone())
1081 }
1082
1083 pub fn evm(&self) -> crate::evm::EVM {
1085 crate::evm::EVM::new(self.inner.clone())
1086 }
1087
1088 pub fn stream(&self) -> crate::stream::Stream {
1090 crate::stream::Stream::new(self.inner.endpoint.clone())
1091 }
1092
1093 pub fn grpc(&self) -> crate::grpc::GRPCStream {
1095 crate::grpc::GRPCStream::new(self.inner.endpoint.clone())
1096 }
1097
1098 pub fn evm_stream(&self) -> crate::evm_stream::EVMStream {
1100 crate::evm_stream::EVMStream::new(self.inner.endpoint.clone())
1101 }
1102
1103 pub async fn markets(&self) -> Result<Value> {
1109 self.inner.query_info(&json!({"type": "meta"})).await
1110 }
1111
1112 pub async fn prediction_markets(&self) -> Result<Vec<PredictionMarket>> {
1114 let outcome_meta = self.inner.query_info(&json!({"type": "outcomeMeta"})).await?;
1115 let mids = self.inner.fetch_all_mids().await?;
1116 let mut markets = Vec::new();
1117
1118 let Some(outcomes) = outcome_meta.get("outcomes").and_then(|o| o.as_array()) else {
1119 return Ok(markets);
1120 };
1121
1122 for outcome in outcomes {
1123 let Some(outcome_id) = outcome.get("outcome").and_then(|o| o.as_u64()) else {
1124 continue;
1125 };
1126 let description = outcome
1127 .get("description")
1128 .and_then(|d| d.as_str())
1129 .unwrap_or_default()
1130 .to_string();
1131 let fields = parse_outcome_description(&description);
1132 let title = prediction_title(&fields);
1133
1134 let mut sides = Vec::new();
1135 if let Some(side_specs) = outcome.get("sideSpecs").and_then(|s| s.as_array()) {
1136 for (side_index, side_spec) in side_specs.iter().enumerate() {
1137 let encoding = outcome_id as usize * 10 + side_index;
1138 let symbol = format!("#{}", encoding);
1139 sides.push(PredictionSide {
1140 outcome: outcome_id,
1141 side: side_index,
1142 name: side_spec
1143 .get("name")
1144 .and_then(|n| n.as_str())
1145 .unwrap_or_default()
1146 .to_string(),
1147 symbol: symbol.clone(),
1148 token: format!("+{}", encoding),
1149 asset_id: 100_000_000 + encoding,
1150 mid: mids.get(&symbol).map(|m| m.to_string()),
1151 sz_decimals: 0,
1152 supports_priority_fee: false,
1153 });
1154 }
1155 }
1156
1157 if sides.len() < 2 {
1158 continue;
1159 }
1160
1161 let slug = app_style_prediction_slug(&fields, None)
1162 .unwrap_or_else(|| prediction_slug(&title));
1163 let mut aliases = vec![prediction_slug(&title)];
1164 for side in sides.iter().take(2) {
1165 if let Some(alias) = app_style_prediction_slug(&fields, Some(&side.name)) {
1166 aliases.push(alias);
1167 }
1168 }
1169
1170 markets.push(PredictionMarket {
1171 outcome: outcome_id,
1172 name: outcome
1173 .get("name")
1174 .and_then(|n| n.as_str())
1175 .unwrap_or_default()
1176 .to_string(),
1177 description,
1178 title: title.clone(),
1179 slug,
1180 underlying: fields.get("underlying").cloned(),
1181 target_price: fields.get("targetPrice").cloned(),
1182 expiry: fields.get("expiry").map(|e| format_prediction_expiry(e)),
1183 period: fields.get("period").cloned(),
1184 collateral: "USDH".to_string(),
1185 min_order_value: "10".to_string(),
1186 aliases,
1187 yes: sides[0].clone(),
1188 no: sides[1].clone(),
1189 sides,
1190 });
1191 }
1192
1193 Ok(markets)
1194 }
1195
1196 pub async fn predictions(&self) -> Result<Vec<PredictionMarket>> {
1198 self.prediction_markets().await
1199 }
1200
1201 pub async fn prediction_market(&self, filter: PredictionMarketFilter) -> Result<PredictionMarket> {
1203 let markets = self.prediction_markets().await?;
1204 markets
1205 .into_iter()
1206 .find(|market| {
1207 if let Some(query) = &filter.query {
1208 if !market.matches(query) {
1209 return false;
1210 }
1211 }
1212 if let Some(underlying) = &filter.underlying {
1213 if market.underlying.as_deref().unwrap_or_default().to_lowercase() != underlying.to_lowercase() {
1214 return false;
1215 }
1216 }
1217 if let Some(target_price) = &filter.target_price {
1218 if market.target_price.as_deref() != Some(target_price.as_str()) {
1219 return false;
1220 }
1221 }
1222 if let Some(expiry) = &filter.expiry {
1223 let formatted = format_prediction_expiry(expiry);
1224 if market.expiry.as_deref() != Some(expiry.as_str())
1225 && market.expiry.as_deref() != Some(formatted.as_str())
1226 {
1227 return false;
1228 }
1229 }
1230 true
1231 })
1232 .ok_or_else(|| Error::ValidationError(
1233 "No matching prediction market found. Call sdk.prediction_markets() to list active HIP-4 markets.".to_string(),
1234 ))
1235 }
1236
1237 pub async fn prediction_sides(&self) -> Result<Vec<PredictionSide>> {
1239 Ok(self
1240 .prediction_markets()
1241 .await?
1242 .into_iter()
1243 .flat_map(|market| market.sides)
1244 .collect())
1245 }
1246
1247 pub async fn dexes(&self) -> Result<Value> {
1249 self.inner.query_info(&json!({"type": "perpDexs"})).await
1250 }
1251
1252 pub async fn open_orders(&self) -> Result<Value> {
1254 let address = self
1255 .inner
1256 .address
1257 .ok_or_else(|| Error::ConfigError("No address configured".to_string()))?;
1258
1259 self.inner
1260 .query_info(&json!({
1261 "type": "openOrders",
1262 "user": format!("{:?}", address),
1263 }))
1264 .await
1265 }
1266
1267 pub async fn order_status(&self, oid: u64, dex: Option<&str>) -> Result<Value> {
1269 let address = self
1270 .inner
1271 .address
1272 .ok_or_else(|| Error::ConfigError("No address configured".to_string()))?;
1273
1274 let mut req = json!({
1275 "type": "orderStatus",
1276 "user": format!("{:?}", address),
1277 "oid": oid,
1278 });
1279
1280 if let Some(d) = dex {
1281 req["dex"] = json!(d);
1282 }
1283
1284 self.inner.query_info(&req).await
1285 }
1286
1287 pub async fn market_buy(&self, asset: impl Into<String>) -> MarketOrderBuilder {
1293 MarketOrderBuilder::new(self.inner.clone(), asset.into(), Side::Buy)
1294 }
1295
1296 pub async fn market_sell(&self, asset: impl Into<String>) -> MarketOrderBuilder {
1298 MarketOrderBuilder::new(self.inner.clone(), asset.into(), Side::Sell)
1299 }
1300
1301 pub async fn buy(
1303 &self,
1304 asset: impl Into<String>,
1305 size: f64,
1306 price: f64,
1307 tif: TIF,
1308 ) -> Result<PlacedOrder> {
1309 let asset = asset.into();
1310 self.place_order(&asset, Side::Buy, size, Some(price), tif, false, false, false, None, None)
1311 .await
1312 }
1313
1314 pub async fn sell(
1316 &self,
1317 asset: impl Into<String>,
1318 size: f64,
1319 price: f64,
1320 tif: TIF,
1321 ) -> Result<PlacedOrder> {
1322 let asset = asset.into();
1323 self.place_order(&asset, Side::Sell, size, Some(price), tif, false, false, false, None, None)
1324 .await
1325 }
1326
1327 pub async fn order(&self, order: Order) -> Result<PlacedOrder> {
1329 order.validate()?;
1330
1331 let asset = order.get_asset();
1332 let side = order.get_side();
1333 let tif = order.get_tif();
1334
1335 let size = if let Some(s) = order.get_size() {
1337 s
1338 } else if let Some(notional) = order.get_notional() {
1339 let mid = self.inner.get_mid_price(asset).await?;
1340 Decimal::from_f64_retain(notional.to_string().parse::<f64>().unwrap_or(0.0) / mid)
1341 .unwrap_or_default()
1342 } else {
1343 return Err(Error::ValidationError(
1344 "Order must have size or notional".to_string(),
1345 ));
1346 };
1347
1348 let is_market = order.is_market();
1351 let price = if is_market {
1352 None } else {
1354 order
1355 .get_price()
1356 .map(|p| p.to_string().parse::<f64>().unwrap_or(0.0))
1357 };
1358
1359 self.place_order(
1360 asset,
1361 side,
1362 size.to_string().parse::<f64>().unwrap_or(0.0),
1363 price,
1364 if is_market { TIF::Market } else { tif },
1365 order.is_reduce_only(),
1366 is_market,
1367 order.get_notional().is_some() && order.get_size().is_none(),
1368 None, order.get_priority_fee(),
1370 )
1371 .await
1372 }
1373
1374 pub async fn trigger_order(&self, order: TriggerOrder) -> Result<PlacedOrder> {
1376 order.validate()?;
1377
1378 let asset = order.get_asset();
1379 let asset_index = self
1380 .inner
1381 .resolve_asset(asset)
1382 .ok_or_else(|| Error::ValidationError(format!("Unknown asset: {}", asset)))?;
1383
1384 let sz_decimals = self.inner.metadata.get_asset(asset)
1386 .map(|a| a.sz_decimals)
1387 .unwrap_or(5) as u32;
1388
1389 let trigger_px = order
1390 .get_trigger_price()
1391 .ok_or_else(|| Error::ValidationError("Trigger price required".to_string()))?;
1392
1393 let size = order
1394 .get_size()
1395 .ok_or_else(|| Error::ValidationError("Size required".to_string()))?;
1396
1397 let size_rounded = size.round_dp(sz_decimals);
1399
1400 let limit_px = if order.is_market() {
1402 let mid = self.inner.get_mid_price(asset).await?;
1403 let slippage = self.inner.slippage;
1404 let price = if order.get_side().is_buy() {
1405 mid * (1.0 + slippage)
1406 } else {
1407 mid * (1.0 - slippage)
1408 };
1409 Decimal::from_f64_retain(price.round()).unwrap_or_default()
1410 } else {
1411 order.get_limit_price().unwrap_or(trigger_px).round()
1412 };
1413
1414 let trigger_px_rounded = trigger_px.round();
1416
1417 let cloid = {
1419 let now = std::time::SystemTime::now()
1420 .duration_since(std::time::UNIX_EPOCH)
1421 .unwrap_or_default();
1422 let nanos = now.as_nanos() as u64;
1423 let hi = nanos.wrapping_mul(0x517cc1b727220a95);
1424 format!("0x{:016x}{:016x}", nanos, hi)
1425 };
1426
1427 let action = json!({
1428 "type": "order",
1429 "orders": [{
1430 "a": asset_index,
1431 "b": order.get_side().is_buy(),
1432 "p": limit_px.normalize().to_string(),
1433 "s": size_rounded.normalize().to_string(),
1434 "r": order.is_reduce_only(),
1435 "t": {
1436 "trigger": {
1437 "isMarket": order.is_market(),
1438 "triggerPx": trigger_px_rounded.normalize().to_string(),
1439 "tpsl": order.get_tpsl().to_string(),
1440 }
1441 },
1442 "c": cloid,
1443 }],
1444 "grouping": "na",
1445 });
1446
1447 let response = self.inner.build_sign_send(&action, None).await?;
1448
1449 Ok(PlacedOrder::from_response(
1450 response,
1451 asset.to_string(),
1452 order.get_side(),
1453 size,
1454 Some(limit_px),
1455 Some(self.inner.clone()),
1456 ))
1457 }
1458
1459 pub async fn stop_loss(
1461 &self,
1462 asset: &str,
1463 size: f64,
1464 trigger_price: f64,
1465 ) -> Result<PlacedOrder> {
1466 self.trigger_order(
1467 TriggerOrder::stop_loss(asset)
1468 .size(size)
1469 .trigger_price(trigger_price)
1470 .market(),
1471 )
1472 .await
1473 }
1474
1475 pub async fn take_profit(
1477 &self,
1478 asset: &str,
1479 size: f64,
1480 trigger_price: f64,
1481 ) -> Result<PlacedOrder> {
1482 self.trigger_order(
1483 TriggerOrder::take_profit(asset)
1484 .size(size)
1485 .trigger_price(trigger_price)
1486 .market(),
1487 )
1488 .await
1489 }
1490
1491 async fn place_order(
1497 &self,
1498 asset: &str,
1499 side: Side,
1500 size: f64,
1501 price: Option<f64>,
1502 tif: TIF,
1503 reduce_only: bool,
1504 is_market: bool,
1505 size_from_notional: bool,
1506 slippage: Option<f64>,
1507 priority_fee: Option<u64>,
1508 ) -> Result<PlacedOrder> {
1509 if is_prediction_asset(asset) && priority_fee.is_some() {
1510 return Err(Error::ValidationError(
1511 "priority_fee is not supported for HIP-4 prediction markets. Omit priority_fee when trading market.yes, market.no, or # markets.".to_string(),
1512 ));
1513 }
1514
1515 let sz_decimals = if is_prediction_asset(asset) {
1517 0
1518 } else {
1519 self.inner
1520 .metadata
1521 .get_asset(asset)
1522 .map(|a| a.sz_decimals)
1523 .unwrap_or(5)
1524 } as i32;
1525
1526 let size_rounded = if is_prediction_asset(asset) {
1528 size.ceil()
1529 } else {
1530 (size * 10f64.powi(sz_decimals)).round() / 10f64.powi(sz_decimals)
1531 };
1532 if is_prediction_asset(asset)
1533 && !size_from_notional
1534 && (size_rounded - size).abs() > f64::EPSILON
1535 {
1536 return Err(Error::ValidationError(
1537 "HIP-4 prediction market size must be a whole number of contracts".to_string(),
1538 ));
1539 }
1540
1541 if side.is_buy() && is_prediction_asset(asset) {
1542 let px = match price {
1543 Some(px) => px,
1544 None if is_market => self.inner.get_mid_price(asset).await?,
1545 None => 0.0,
1546 };
1547 if px > 0.0 && size_rounded * px < 10.0 {
1548 return Err(Error::ValidationError(
1549 "HIP-4 prediction market orders must have minimum value of 10 USDH. Increase size or price, or call sdk.buy_usdh(...) before trading.".to_string(),
1550 ));
1551 }
1552 }
1553
1554 let (action, effective_slippage) = if is_market {
1555 let mut order_spec = json!({
1557 "asset": asset,
1558 "side": if side.is_buy() { "buy" } else { "sell" },
1559 "size": format!("{}", size_rounded),
1560 "tif": "market",
1561 });
1562 if reduce_only {
1563 order_spec["reduceOnly"] = json!(true);
1564 }
1565 let action = json!({
1566 "type": "order",
1567 "orders": [order_spec],
1568 });
1569 (action, slippage)
1570 } else {
1571 let asset_index = self
1573 .inner
1574 .resolve_asset(asset)
1575 .ok_or_else(|| Error::ValidationError(format!("Unknown asset: {}", asset)))?;
1576
1577 let resolved_price = match price {
1578 Some(px) if is_prediction_asset(asset) => px,
1579 Some(px) => px.round(),
1580 None => 0.0,
1581 };
1582
1583 let tif_wire = match tif {
1584 TIF::Ioc => "Ioc",
1585 TIF::Gtc => "Gtc",
1586 TIF::Alo => "Alo",
1587 TIF::Market => "Ioc",
1588 };
1589
1590 let cloid = {
1592 let now = std::time::SystemTime::now()
1593 .duration_since(std::time::UNIX_EPOCH)
1594 .unwrap_or_default();
1595 let nanos = now.as_nanos() as u64;
1596 let hi = nanos.wrapping_mul(0x517cc1b727220a95);
1597 format!("0x{:016x}{:016x}", nanos, hi)
1598 };
1599
1600 let action = json!({
1601 "type": "order",
1602 "orders": [{
1603 "a": asset_index,
1604 "b": side.is_buy(),
1605 "p": format!("{}", resolved_price),
1606 "s": format!("{}", size_rounded),
1607 "r": reduce_only,
1608 "t": {"limit": {"tif": tif_wire}},
1609 "c": cloid,
1610 }],
1611 "grouping": "na",
1612 });
1613 (action, None) };
1615
1616 let response = self
1617 .inner
1618 .build_sign_send_with_priority(&action, effective_slippage, priority_fee)
1619 .await?;
1620
1621 Ok(PlacedOrder::from_response(
1622 response,
1623 asset.to_string(),
1624 side,
1625 Decimal::from_f64_retain(size_rounded).unwrap_or_default(),
1626 price.map(|p| Decimal::from_f64_retain(p).unwrap_or_default()),
1627 Some(self.inner.clone()),
1628 ))
1629 }
1630
1631 pub async fn modify(
1639 &self,
1640 oid: u64,
1641 asset: &str,
1642 is_buy: bool,
1643 size: f64,
1644 price: f64,
1645 tif: TIF,
1646 reduce_only: bool,
1647 cloid: Option<&str>,
1648 ) -> Result<PlacedOrder> {
1649 let asset_idx = self
1650 .inner
1651 .metadata
1652 .resolve_asset(asset)
1653 .ok_or_else(|| Error::ValidationError(format!("Unknown asset: {}", asset)))?;
1654
1655 let sz_decimals = self.inner.metadata.get_asset(asset)
1656 .map(|a| a.sz_decimals)
1657 .unwrap_or(8) as i32;
1658 let size_rounded = (size * 10f64.powi(sz_decimals)).round() / 10f64.powi(sz_decimals);
1659
1660 let order_type = match tif {
1661 TIF::Gtc => json!({"limit": {"tif": "Gtc"}}),
1662 TIF::Ioc | TIF::Market => json!({"limit": {"tif": "Ioc"}}),
1663 TIF::Alo => json!({"limit": {"tif": "Alo"}}),
1664 };
1665
1666 let cloid_val = cloid
1667 .map(|s| s.to_string())
1668 .unwrap_or_else(|| {
1669 let now = std::time::SystemTime::now()
1670 .duration_since(std::time::UNIX_EPOCH)
1671 .unwrap_or_default();
1672 let nanos = now.as_nanos() as u64;
1673 let hi = nanos.wrapping_mul(0x517cc1b727220a95);
1674 format!("0x{:016x}{:016x}", nanos, hi)
1675 });
1676
1677 let action = json!({
1678 "type": "batchModify",
1679 "modifies": [{
1680 "oid": oid,
1681 "order": {
1682 "a": asset_idx,
1683 "b": is_buy,
1684 "p": format!("{:.8}", price).trim_end_matches('0').trim_end_matches('.'),
1685 "s": format!("{:.8}", size_rounded).trim_end_matches('0').trim_end_matches('.'),
1686 "r": reduce_only,
1687 "t": order_type,
1688 "c": cloid_val,
1689 }
1690 }]
1691 });
1692
1693 let response = self.inner.build_sign_send(&action, None).await?;
1694
1695 Ok(PlacedOrder::from_response(
1696 response,
1697 asset.to_string(),
1698 if is_buy { Side::Buy } else { Side::Sell },
1699 Decimal::from_f64_retain(size_rounded).unwrap_or_default(),
1700 Some(Decimal::from_f64_retain(price).unwrap_or_default()),
1701 Some(self.inner.clone()),
1702 ))
1703 }
1704
1705 pub async fn cancel(&self, oid: u64, asset: &str) -> Result<Value> {
1707 self.inner.cancel_by_oid(oid, asset).await
1708 }
1709
1710 pub async fn cancel_all(&self, asset: Option<&str>) -> Result<Value> {
1712 if self.inner.address.is_none() {
1714 return Err(Error::ConfigError("No address configured".to_string()));
1715 }
1716
1717 let open_orders = self.open_orders().await?;
1719
1720 let cancels: Vec<Value> = open_orders
1721 .as_array()
1722 .unwrap_or(&vec![])
1723 .iter()
1724 .filter(|order| {
1725 if let Some(asset) = asset {
1726 order.get("coin").and_then(|c| c.as_str()) == Some(asset)
1727 } else {
1728 true
1729 }
1730 })
1731 .filter_map(|order| {
1732 let oid = order.get("oid").and_then(|o| o.as_u64())?;
1733 let coin = order.get("coin").and_then(|c| c.as_str())?;
1734 let asset_index = self.inner.resolve_asset(coin)?;
1735 Some(json!({"a": asset_index, "o": oid}))
1736 })
1737 .collect();
1738
1739 if cancels.is_empty() {
1740 return Ok(json!({"status": "ok", "message": "No orders to cancel"}));
1741 }
1742
1743 let action = json!({
1744 "type": "cancel",
1745 "cancels": cancels,
1746 });
1747
1748 self.inner.build_sign_send(&action, None).await
1749 }
1750
1751 pub async fn close_position(&self, asset: &str, slippage: Option<f64>) -> Result<PlacedOrder> {
1757 let address = self
1758 .inner
1759 .address
1760 .ok_or_else(|| Error::ConfigError("No address configured".to_string()))?;
1761
1762 let action = json!({
1763 "type": "closePosition",
1764 "asset": asset,
1765 "user": format!("{:?}", address),
1766 });
1767
1768 let response = self.inner.build_sign_send(&action, slippage).await?;
1769
1770 Ok(PlacedOrder::from_response(
1773 response,
1774 asset.to_string(),
1775 Side::Sell, Decimal::ZERO, None,
1778 Some(self.inner.clone()),
1779 ))
1780 }
1781
1782 pub async fn update_leverage(
1788 &self,
1789 asset: &str,
1790 leverage: i32,
1791 is_cross: bool,
1792 ) -> Result<Value> {
1793 let asset_index = self
1794 .inner
1795 .resolve_asset(asset)
1796 .ok_or_else(|| Error::ValidationError(format!("Unknown asset: {}", asset)))?;
1797
1798 let action = json!({
1799 "type": "updateLeverage",
1800 "asset": asset_index,
1801 "isCross": is_cross,
1802 "leverage": leverage,
1803 });
1804
1805 self.inner.build_sign_send(&action, None).await
1806 }
1807
1808 pub async fn update_isolated_margin(
1810 &self,
1811 asset: &str,
1812 is_buy: bool,
1813 amount_usd: f64,
1814 ) -> Result<Value> {
1815 let asset_index = self
1816 .inner
1817 .resolve_asset(asset)
1818 .ok_or_else(|| Error::ValidationError(format!("Unknown asset: {}", asset)))?;
1819
1820 let action = json!({
1821 "type": "updateIsolatedMargin",
1822 "asset": asset_index,
1823 "isBuy": is_buy,
1824 "ntli": (amount_usd * 1_000_000.0) as i64, });
1826
1827 self.inner.build_sign_send(&action, None).await
1828 }
1829
1830 pub async fn twap_order(
1836 &self,
1837 asset: &str,
1838 size: f64,
1839 is_buy: bool,
1840 duration_minutes: i64,
1841 reduce_only: bool,
1842 randomize: bool,
1843 ) -> Result<Value> {
1844 let action = json!({
1845 "type": "twapOrder",
1846 "twap": {
1847 "a": asset,
1848 "b": is_buy,
1849 "s": format!("{}", size),
1850 "r": reduce_only,
1851 "m": duration_minutes,
1852 "t": randomize,
1853 }
1854 });
1855
1856 self.inner.build_sign_send(&action, None).await
1857 }
1858
1859 pub async fn twap_cancel(&self, asset: &str, twap_id: i64) -> Result<Value> {
1861 let action = json!({
1862 "type": "twapCancel",
1863 "a": asset,
1864 "t": twap_id,
1865 });
1866
1867 self.inner.build_sign_send(&action, None).await
1868 }
1869
1870 pub async fn transfer_usd(&self, destination: &str, amount: f64) -> Result<Value> {
1876 let time = SystemTime::now()
1877 .duration_since(UNIX_EPOCH)
1878 .unwrap()
1879 .as_millis() as u64;
1880
1881 let action = json!({
1882 "type": "usdSend",
1883 "hyperliquidChain": self.inner.chain.to_string(),
1884 "signatureChainId": self.inner.chain.signature_chain_id(),
1885 "destination": destination,
1886 "amount": format!("{}", amount),
1887 "time": time,
1888 });
1889
1890 self.inner.build_sign_send(&action, None).await
1891 }
1892
1893 pub async fn transfer_spot(
1895 &self,
1896 token: &str,
1897 destination: &str,
1898 amount: f64,
1899 ) -> Result<Value> {
1900 let time = SystemTime::now()
1901 .duration_since(UNIX_EPOCH)
1902 .unwrap()
1903 .as_millis() as u64;
1904
1905 let action = json!({
1906 "type": "spotSend",
1907 "hyperliquidChain": self.inner.chain.to_string(),
1908 "signatureChainId": self.inner.chain.signature_chain_id(),
1909 "token": token,
1910 "destination": destination,
1911 "amount": format!("{}", amount),
1912 "time": time,
1913 });
1914
1915 self.inner.build_sign_send(&action, None).await
1916 }
1917
1918 pub async fn withdraw(&self, amount: f64, destination: Option<&str>) -> Result<Value> {
1920 let time = SystemTime::now()
1921 .duration_since(UNIX_EPOCH)
1922 .unwrap()
1923 .as_millis() as u64;
1924
1925 let dest = destination
1926 .map(|s| s.to_string())
1927 .or_else(|| self.inner.address.map(|a| format!("{:?}", a)))
1928 .ok_or_else(|| Error::ConfigError("No destination address".to_string()))?;
1929
1930 let action = json!({
1931 "type": "withdraw3",
1932 "hyperliquidChain": self.inner.chain.to_string(),
1933 "signatureChainId": self.inner.chain.signature_chain_id(),
1934 "destination": dest,
1935 "amount": format!("{}", amount),
1936 "time": time,
1937 });
1938
1939 self.inner.build_sign_send(&action, None).await
1940 }
1941
1942 pub async fn transfer_spot_to_perp(&self, amount: f64) -> Result<Value> {
1944 let nonce = SystemTime::now()
1945 .duration_since(UNIX_EPOCH)
1946 .unwrap()
1947 .as_millis() as u64;
1948
1949 let action = json!({
1950 "type": "usdClassTransfer",
1951 "hyperliquidChain": self.inner.chain.to_string(),
1952 "signatureChainId": self.inner.chain.signature_chain_id(),
1953 "amount": format!("{}", amount),
1954 "toPerp": true,
1955 "nonce": nonce,
1956 });
1957
1958 self.inner.build_sign_send(&action, None).await
1959 }
1960
1961 pub async fn transfer_perp_to_spot(&self, amount: f64) -> Result<Value> {
1963 let nonce = SystemTime::now()
1964 .duration_since(UNIX_EPOCH)
1965 .unwrap()
1966 .as_millis() as u64;
1967
1968 let action = json!({
1969 "type": "usdClassTransfer",
1970 "hyperliquidChain": self.inner.chain.to_string(),
1971 "signatureChainId": self.inner.chain.signature_chain_id(),
1972 "amount": format!("{}", amount),
1973 "toPerp": false,
1974 "nonce": nonce,
1975 });
1976
1977 self.inner.build_sign_send(&action, None).await
1978 }
1979
1980 pub async fn vault_deposit(&self, vault_address: &str, amount: f64) -> Result<Value> {
1986 let action = json!({
1987 "type": "vaultTransfer",
1988 "vaultAddress": vault_address,
1989 "isDeposit": true,
1990 "usd": amount,
1991 });
1992
1993 self.inner.build_sign_send(&action, None).await
1994 }
1995
1996 pub async fn vault_withdraw(&self, vault_address: &str, amount: f64) -> Result<Value> {
1998 let action = json!({
1999 "type": "vaultTransfer",
2000 "vaultAddress": vault_address,
2001 "isDeposit": false,
2002 "usd": amount,
2003 });
2004
2005 self.inner.build_sign_send(&action, None).await
2006 }
2007
2008 pub async fn buy_usdh(&self, amount_usdc: f64) -> Result<PlacedOrder> {
2010 self.market_buy("@230").await.notional(amount_usdc).await
2011 }
2012
2013 pub async fn sell_usdh(&self, amount_usdh: f64) -> Result<PlacedOrder> {
2015 self.market_sell("@230").await.size(amount_usdh).await
2016 }
2017
2018 pub async fn stake(&self, amount_tokens: f64) -> Result<Value> {
2024 let nonce = SystemTime::now()
2025 .duration_since(UNIX_EPOCH)
2026 .unwrap()
2027 .as_millis() as u64;
2028
2029 let wei = hype_to_wei(amount_tokens)?;
2030
2031 let action = json!({
2032 "type": "cDeposit",
2033 "wei": wei,
2034 "nonce": nonce,
2035 });
2036
2037 self.inner.build_sign_send(&action, None).await
2038 }
2039
2040 pub async fn fund_priority_fees(&self, amount_hype: f64) -> Result<Value> {
2042 self.stake(amount_hype).await
2043 }
2044
2045 pub async fn unstake(&self, amount_tokens: f64) -> Result<Value> {
2047 let nonce = SystemTime::now()
2048 .duration_since(UNIX_EPOCH)
2049 .unwrap()
2050 .as_millis() as u64;
2051
2052 let wei = hype_to_wei(amount_tokens)?;
2053
2054 let action = json!({
2055 "type": "cWithdraw",
2056 "wei": wei,
2057 "nonce": nonce,
2058 });
2059
2060 self.inner.build_sign_send(&action, None).await
2061 }
2062
2063 pub async fn delegate(&self, validator: &str, amount_tokens: f64) -> Result<Value> {
2065 let nonce = SystemTime::now()
2066 .duration_since(UNIX_EPOCH)
2067 .unwrap()
2068 .as_millis() as u64;
2069
2070 let wei = hype_to_wei(amount_tokens)?;
2071
2072 let action = json!({
2073 "type": "tokenDelegate",
2074 "validator": validator,
2075 "isUndelegate": false,
2076 "wei": wei,
2077 "nonce": nonce,
2078 });
2079
2080 self.inner.build_sign_send(&action, None).await
2081 }
2082
2083 pub async fn undelegate(&self, validator: &str, amount_tokens: f64) -> Result<Value> {
2085 let nonce = SystemTime::now()
2086 .duration_since(UNIX_EPOCH)
2087 .unwrap()
2088 .as_millis() as u64;
2089
2090 let wei = hype_to_wei(amount_tokens)?;
2091
2092 let action = json!({
2093 "type": "tokenDelegate",
2094 "validator": validator,
2095 "isUndelegate": true,
2096 "wei": wei,
2097 "nonce": nonce,
2098 });
2099
2100 self.inner.build_sign_send(&action, None).await
2101 }
2102
2103 pub async fn approve_builder_fee(&self, max_fee: Option<&str>) -> Result<Value> {
2109 let fee = max_fee.unwrap_or(&self.max_fee);
2110
2111 let action = json!({
2112 "type": "approveBuilderFee",
2113 "maxFeeRate": fee,
2114 });
2115
2116 self.inner.build_sign_send(&action, None).await
2117 }
2118
2119 pub async fn revoke_builder_fee(&self) -> Result<Value> {
2121 self.approve_builder_fee(Some("0%")).await
2122 }
2123
2124 pub async fn approval_status(&self) -> Result<Value> {
2126 let address = self
2127 .inner
2128 .address
2129 .ok_or_else(|| Error::ConfigError("No address configured".to_string()))?;
2130
2131 let url = format!("{}/approval", DEFAULT_WORKER_URL);
2133
2134 let response = self
2135 .inner
2136 .http_client
2137 .post(&url)
2138 .json(&json!({"user": format!("{:?}", address)}))
2139 .send()
2140 .await?;
2141
2142 let text = response.text().await?;
2143 serde_json::from_str(&text).map_err(|e| Error::JsonError(e.to_string()))
2144 }
2145
2146 pub async fn reserve_request_weight(&self, weight: i32) -> Result<Value> {
2152 let action = json!({
2153 "type": "reserveRequestWeight",
2154 "weight": weight,
2155 });
2156
2157 self.inner.build_sign_send(&action, None).await
2158 }
2159
2160 pub async fn noop(&self) -> Result<Value> {
2162 let action = json!({"type": "noop"});
2163 self.inner.build_sign_send(&action, None).await
2164 }
2165
2166 pub async fn preflight(
2168 &self,
2169 asset: &str,
2170 side: Side,
2171 price: f64,
2172 size: f64,
2173 ) -> Result<Value> {
2174 let url = format!("{}/preflight", DEFAULT_WORKER_URL);
2175
2176 let body = json!({
2177 "asset": asset,
2178 "side": side.to_string(),
2179 "price": price,
2180 "size": size,
2181 });
2182
2183 let response = self
2184 .inner
2185 .http_client
2186 .post(&url)
2187 .json(&body)
2188 .send()
2189 .await?;
2190
2191 let text = response.text().await?;
2192 serde_json::from_str(&text).map_err(|e| Error::JsonError(e.to_string()))
2193 }
2194
2195 pub async fn approve_agent(
2201 &self,
2202 agent_address: &str,
2203 name: Option<&str>,
2204 ) -> Result<Value> {
2205 let nonce = SystemTime::now()
2206 .duration_since(UNIX_EPOCH)
2207 .unwrap()
2208 .as_millis() as u64;
2209
2210 let action = json!({
2211 "type": "approveAgent",
2212 "hyperliquidChain": self.inner.chain.as_str(),
2213 "signatureChainId": self.inner.chain.signature_chain_id(),
2214 "agentAddress": agent_address,
2215 "agentName": name,
2216 "nonce": nonce,
2217 });
2218
2219 self.inner.build_sign_send(&action, None).await
2220 }
2221
2222 pub async fn set_abstraction(&self, mode: &str, user: Option<&str>) -> Result<Value> {
2230 let address = self
2231 .inner
2232 .address
2233 .ok_or_else(|| Error::ConfigError("No address configured".to_string()))?;
2234
2235 let addr_string = format!("{:?}", address);
2236 let user_addr = user.unwrap_or(&addr_string);
2237 let nonce = SystemTime::now()
2238 .duration_since(UNIX_EPOCH)
2239 .unwrap()
2240 .as_millis() as u64;
2241
2242 let action = json!({
2243 "type": "userSetAbstraction",
2244 "hyperliquidChain": self.inner.chain.as_str(),
2245 "signatureChainId": self.inner.chain.signature_chain_id(),
2246 "user": user_addr,
2247 "abstraction": mode,
2248 "nonce": nonce,
2249 });
2250
2251 self.inner.build_sign_send(&action, None).await
2252 }
2253
2254 pub async fn agent_set_abstraction(&self, mode: &str) -> Result<Value> {
2256 let short_mode = match mode {
2258 "disabled" | "i" => "i",
2259 "unifiedAccount" | "u" => "u",
2260 "portfolioMargin" | "p" => "p",
2261 _ => {
2262 return Err(Error::ValidationError(format!(
2263 "Invalid mode: {}. Use 'disabled', 'unifiedAccount', or 'portfolioMargin'",
2264 mode
2265 )))
2266 }
2267 };
2268
2269 let action = json!({
2270 "type": "agentSetAbstraction",
2271 "abstraction": short_mode,
2272 });
2273
2274 self.inner.build_sign_send(&action, None).await
2275 }
2276
2277 pub async fn send_asset(
2283 &self,
2284 token: &str,
2285 amount: f64,
2286 destination: &str,
2287 source_dex: Option<&str>,
2288 destination_dex: Option<&str>,
2289 from_sub_account: Option<&str>,
2290 ) -> Result<Value> {
2291 let nonce = SystemTime::now()
2292 .duration_since(UNIX_EPOCH)
2293 .unwrap()
2294 .as_millis() as u64;
2295
2296 let action = json!({
2297 "type": "sendAsset",
2298 "hyperliquidChain": self.inner.chain.as_str(),
2299 "signatureChainId": self.inner.chain.signature_chain_id(),
2300 "destination": destination,
2301 "sourceDex": source_dex.unwrap_or(""),
2302 "destinationDex": destination_dex.unwrap_or(""),
2303 "token": token,
2304 "amount": amount.to_string(),
2305 "fromSubAccount": from_sub_account.unwrap_or(""),
2306 "nonce": nonce,
2307 });
2308
2309 self.inner.build_sign_send(&action, None).await
2310 }
2311
2312 pub async fn send_to_evm_with_data(
2314 &self,
2315 token: &str,
2316 amount: f64,
2317 destination: &str,
2318 data: &str,
2319 source_dex: &str,
2320 destination_chain_id: u32,
2321 gas_limit: u64,
2322 ) -> Result<Value> {
2323 let nonce = SystemTime::now()
2324 .duration_since(UNIX_EPOCH)
2325 .unwrap()
2326 .as_millis() as u64;
2327
2328 let action = json!({
2329 "type": "sendToEvmWithData",
2330 "hyperliquidChain": self.inner.chain.as_str(),
2331 "signatureChainId": self.inner.chain.signature_chain_id(),
2332 "token": token,
2333 "amount": amount.to_string(),
2334 "sourceDex": source_dex,
2335 "destinationRecipient": destination,
2336 "addressEncoding": "hex",
2337 "destinationChainId": destination_chain_id,
2338 "gasLimit": gas_limit,
2339 "data": data,
2340 "nonce": nonce,
2341 });
2342
2343 self.inner.build_sign_send(&action, None).await
2344 }
2345
2346 pub async fn top_up_isolated_only_margin(
2352 &self,
2353 asset: &str,
2354 leverage: f64,
2355 ) -> Result<Value> {
2356 let asset_idx = self
2357 .inner
2358 .metadata
2359 .resolve_asset(asset)
2360 .ok_or_else(|| Error::ValidationError(format!("Unknown asset: {}", asset)))?;
2361
2362 let action = json!({
2363 "type": "topUpIsolatedOnlyMargin",
2364 "asset": asset_idx,
2365 "leverage": leverage.to_string(),
2366 });
2367
2368 self.inner.build_sign_send(&action, None).await
2369 }
2370
2371 pub async fn validator_l1_stream(&self, risk_free_rate: &str) -> Result<Value> {
2377 let action = json!({
2378 "type": "validatorL1Stream",
2379 "riskFreeRate": risk_free_rate,
2380 });
2381
2382 self.inner.build_sign_send(&action, None).await
2383 }
2384
2385 pub async fn cancel_by_cloid(&self, cloid: &str, asset: &str) -> Result<Value> {
2391 let asset_idx = self
2392 .inner
2393 .metadata
2394 .resolve_asset(asset)
2395 .ok_or_else(|| Error::ValidationError(format!("Unknown asset: {}", asset)))?;
2396
2397 let action = json!({
2398 "type": "cancelByCloid",
2399 "cancels": [{"asset": asset_idx, "cloid": cloid}],
2400 });
2401
2402 self.inner.build_sign_send(&action, None).await
2403 }
2404
2405 pub async fn schedule_cancel(&self, time_ms: Option<u64>) -> Result<Value> {
2407 let mut action = json!({"type": "scheduleCancel"});
2408 if let Some(t) = time_ms {
2409 action["time"] = json!(t);
2410 }
2411 self.inner.build_sign_send(&action, None).await
2412 }
2413
2414 pub async fn get_mid(&self, asset: impl Into<String>) -> Result<f64> {
2422 let asset = asset.into();
2423 self.inner.get_mid_price(&asset).await
2424 }
2425
2426 pub async fn refresh_markets(&self) -> Result<()> {
2428 self.inner.refresh_metadata().await
2429 }
2430}
2431
2432pub struct MarketOrderBuilder {
2438 inner: Arc<HyperliquidSDKInner>,
2439 asset: String,
2440 side: Side,
2441 size: Option<f64>,
2442 notional: Option<f64>,
2443 slippage: Option<f64>,
2444 reduce_only: bool,
2445 priority_fee: Option<u64>,
2446}
2447
2448impl MarketOrderBuilder {
2449 fn new(inner: Arc<HyperliquidSDKInner>, asset: String, side: Side) -> Self {
2450 Self {
2451 inner,
2452 asset,
2453 side,
2454 size: None,
2455 notional: None,
2456 slippage: None,
2457 reduce_only: false,
2458 priority_fee: None,
2459 }
2460 }
2461
2462 pub fn size(mut self, size: f64) -> Self {
2464 self.size = Some(size);
2465 self
2466 }
2467
2468 pub fn notional(mut self, notional: f64) -> Self {
2470 self.notional = Some(notional);
2471 self
2472 }
2473
2474 pub fn slippage(mut self, slippage: f64) -> Self {
2478 self.slippage = Some(slippage);
2479 self
2480 }
2481
2482 pub fn reduce_only(mut self) -> Self {
2484 self.reduce_only = true;
2485 self
2486 }
2487
2488 pub fn priority_fee(mut self, p: u64) -> Self {
2493 self.priority_fee = Some(p);
2494 self
2495 }
2496
2497 pub async fn execute(self) -> Result<PlacedOrder> {
2502 if is_prediction_asset(&self.asset) && self.priority_fee.is_some() {
2503 return Err(Error::ValidationError(
2504 "priority_fee is not supported for HIP-4 prediction markets. Omit priority_fee when trading market.yes, market.no, or # markets.".to_string(),
2505 ));
2506 }
2507
2508 let sz_decimals = if is_prediction_asset(&self.asset) {
2510 0
2511 } else {
2512 self.inner
2513 .metadata
2514 .get_asset(&self.asset)
2515 .map(|a| a.sz_decimals)
2516 .unwrap_or(5)
2517 } as i32;
2518
2519 let size = if let Some(s) = self.size {
2520 s
2521 } else if let Some(notional) = self.notional {
2522 let mid = self.inner.get_mid_price(&self.asset).await?;
2523 notional / mid
2524 } else {
2525 return Err(Error::ValidationError(
2526 "Market order must have size or notional".to_string(),
2527 ));
2528 };
2529
2530 let size_rounded = if is_prediction_asset(&self.asset) {
2532 size.ceil()
2533 } else {
2534 (size * 10f64.powi(sz_decimals)).round() / 10f64.powi(sz_decimals)
2535 };
2536 if is_prediction_asset(&self.asset)
2537 && self.notional.is_none()
2538 && (size_rounded - size).abs() > f64::EPSILON
2539 {
2540 return Err(Error::ValidationError(
2541 "HIP-4 prediction market size must be a whole number of contracts".to_string(),
2542 ));
2543 }
2544 if self.side.is_buy() && is_prediction_asset(&self.asset) {
2545 let mid = self.inner.get_mid_price(&self.asset).await.ok();
2546 if mid.is_some_and(|mid| mid > 0.0 && size_rounded * mid < 10.0) {
2547 return Err(Error::ValidationError(
2548 "HIP-4 prediction market orders must have minimum value of 10 USDH. Increase size or price, or call sdk.buy_usdh(...) before trading.".to_string(),
2549 ));
2550 }
2551 }
2552
2553 let mut order_spec = json!({
2555 "asset": self.asset,
2556 "side": if self.side.is_buy() { "buy" } else { "sell" },
2557 "size": format!("{}", size_rounded),
2558 "tif": "market",
2559 });
2560 if self.reduce_only {
2561 order_spec["reduceOnly"] = json!(true);
2562 }
2563 let action = json!({
2564 "type": "order",
2565 "orders": [order_spec],
2566 });
2567
2568 let response = self
2569 .inner
2570 .build_sign_send_with_priority(&action, self.slippage, self.priority_fee)
2571 .await?;
2572
2573 Ok(PlacedOrder::from_response(
2574 response,
2575 self.asset,
2576 self.side,
2577 Decimal::from_f64_retain(size_rounded).unwrap_or_default(),
2578 None,
2579 Some(self.inner),
2580 ))
2581 }
2582}
2583
2584impl std::future::IntoFuture for MarketOrderBuilder {
2586 type Output = Result<PlacedOrder>;
2587 type IntoFuture = std::pin::Pin<Box<dyn std::future::Future<Output = Self::Output> + Send>>;
2588
2589 fn into_future(self) -> Self::IntoFuture {
2590 Box::pin(self.execute())
2591 }
2592}