1use crate::signature::sign_typed_data;
2use crate::{
3 exchange::{
4 actions::{
5 ApproveAgent, BulkCancel, BulkModify, BulkOrder, SetReferrer, UpdateIsolatedMargin,
6 UpdateLeverage, UsdSend,
7 },
8 cancel::{CancelRequest, CancelRequestCloid},
9 modify::{ClientModifyRequest, ModifyRequest},
10 ClientCancelRequest, ClientOrderRequest,
11 },
12 helpers::{generate_random_key, next_nonce, uuid_to_hex_string},
13 info::info_client::InfoClient,
14 meta::Meta,
15 prelude::*,
16 req::HttpClient,
17 signature::sign_l1_action,
18 BaseUrl, BulkCancelCloid, Error, ExchangeResponseStatus,
19};
20use crate::{ClassTransfer, SpotSend, SpotUser, VaultTransfer, Withdraw3};
21use ethers::{
22 abi::AbiEncode,
23 signers::{LocalWallet, Signer},
24 types::{Signature, H160, H256},
25};
26use log::debug;
27use reqwest::Client;
28use serde::{Deserialize, Serialize};
29use std::collections::HashMap;
30
31use super::cancel::ClientCancelRequestCloid;
32use super::order::{MarketCloseParams, MarketOrderParams, OrderRequest};
33use super::{ClientLimit, ClientOrder};
34
35pub struct ExchangeClient {
36 pub http_client: HttpClient,
37 pub wallet: LocalWallet,
38 pub meta: Meta,
39 pub vault_address: Option<H160>,
40 pub coin_to_asset: HashMap<String, u32>,
41}
42
43#[derive(Serialize, Deserialize)]
44#[serde(rename_all = "camelCase")]
45struct ExchangePayload {
46 action: serde_json::Value,
47 signature: Signature,
48 nonce: u64,
49 vault_address: Option<H160>,
50}
51
52#[derive(Serialize, Deserialize, Debug, Clone)]
53#[serde(tag = "type")]
54#[serde(rename_all = "camelCase")]
55pub enum Actions {
56 UsdSend(UsdSend),
57 UpdateLeverage(UpdateLeverage),
58 UpdateIsolatedMargin(UpdateIsolatedMargin),
59 Order(BulkOrder),
60 Cancel(BulkCancel),
61 CancelByCloid(BulkCancelCloid),
62 #[serde(rename = "batchModify")]
63 Modify(BulkModify),
64 ApproveAgent(ApproveAgent),
65 Withdraw3(Withdraw3),
66 SpotUser(SpotUser),
67 VaultTransfer(VaultTransfer),
68 SpotSend(SpotSend),
69 SetReferrer(SetReferrer),
70}
71
72impl Actions {
73 fn hash(&self, timestamp: u64, vault_address: Option<H160>) -> Result<H256> {
74 let mut bytes =
75 rmp_serde::to_vec_named(self).map_err(|e| Error::RmpParse(e.to_string()))?;
76 bytes.extend(timestamp.to_be_bytes());
77 if let Some(vault_address) = vault_address {
78 bytes.push(1);
79 bytes.extend(vault_address.to_fixed_bytes());
80 } else {
81 bytes.push(0);
82 }
83 Ok(H256(ethers::utils::keccak256(bytes)))
84 }
85}
86
87impl ExchangeClient {
88 pub async fn new(
89 client: Option<Client>,
90 wallet: LocalWallet,
91 base_url: Option<BaseUrl>,
92 meta: Option<Meta>,
93 vault_address: Option<H160>,
94 ) -> Result<ExchangeClient> {
95 let client = client.unwrap_or_default();
96 let base_url = base_url.unwrap_or(BaseUrl::Mainnet);
97
98 let info = InfoClient::new(None, Some(base_url)).await?;
99 let meta = if let Some(meta) = meta {
100 meta
101 } else {
102 info.meta().await?
103 };
104
105 let mut coin_to_asset = HashMap::new();
106 for (asset_ind, asset) in meta.universe.iter().enumerate() {
107 coin_to_asset.insert(asset.name.clone(), asset_ind as u32);
108 }
109
110 coin_to_asset = info
111 .spot_meta()
112 .await?
113 .add_pair_and_name_to_index_map(coin_to_asset);
114
115 Ok(ExchangeClient {
116 wallet,
117 meta,
118 vault_address,
119 http_client: HttpClient {
120 client,
121 base_url: base_url.get_url(),
122 },
123 coin_to_asset,
124 })
125 }
126
127 async fn post(
128 &self,
129 action: serde_json::Value,
130 signature: Signature,
131 nonce: u64,
132 ) -> Result<ExchangeResponseStatus> {
133 let exchange_payload = ExchangePayload {
134 action,
135 signature,
136 nonce,
137 vault_address: self.vault_address,
138 };
139 let res = serde_json::to_string(&exchange_payload)
140 .map_err(|e| Error::JsonParse(e.to_string()))?;
141 debug!("Sending request {res:?}");
142
143 serde_json::from_str(
144 &self
145 .http_client
146 .post("/exchange", res)
147 .await
148 .map_err(|e| Error::JsonParse(e.to_string()))?,
149 )
150 .map_err(|e| Error::JsonParse(e.to_string()))
151 }
152
153 pub async fn usdc_transfer(
154 &self,
155 amount: &str,
156 destination: &str,
157 wallet: Option<&LocalWallet>,
158 ) -> Result<ExchangeResponseStatus> {
159 let wallet = wallet.unwrap_or(&self.wallet);
160 let hyperliquid_chain = if self.http_client.is_mainnet() {
161 "Mainnet".to_string()
162 } else {
163 "Testnet".to_string()
164 };
165
166 let timestamp = next_nonce();
167 let usd_send = UsdSend {
168 signature_chain_id: 421614.into(),
169 hyperliquid_chain,
170 destination: destination.to_string(),
171 amount: amount.to_string(),
172 time: timestamp,
173 };
174 let signature = sign_typed_data(&usd_send, wallet)?;
175 let action = serde_json::to_value(Actions::UsdSend(usd_send))
176 .map_err(|e| Error::JsonParse(e.to_string()))?;
177
178 self.post(action, signature, timestamp).await
179 }
180
181 pub async fn class_transfer(
182 &self,
183 usdc: f64,
184 to_perp: bool,
185 wallet: Option<&LocalWallet>,
186 ) -> Result<ExchangeResponseStatus> {
187 let usdc = (usdc * 1e6).round() as u64;
189 let wallet = wallet.unwrap_or(&self.wallet);
190
191 let timestamp = next_nonce();
192
193 let action = Actions::SpotUser(SpotUser {
194 class_transfer: ClassTransfer { usdc, to_perp },
195 });
196 let connection_id = action.hash(timestamp, self.vault_address)?;
197 let action = serde_json::to_value(&action).map_err(|e| Error::JsonParse(e.to_string()))?;
198 let is_mainnet = self.http_client.is_mainnet();
199 let signature = sign_l1_action(wallet, connection_id, is_mainnet)?;
200
201 self.post(action, signature, timestamp).await
202 }
203
204 pub async fn vault_transfer(
205 &self,
206 is_deposit: bool,
207 usd: String,
208 vault_address: Option<H160>,
209 wallet: Option<&LocalWallet>,
210 ) -> Result<ExchangeResponseStatus> {
211 let vault_address = self
212 .vault_address
213 .or(vault_address)
214 .ok_or_else(|| Error::VaultAddressNotFound)?;
215 let wallet = wallet.unwrap_or(&self.wallet);
216
217 let timestamp = next_nonce();
218
219 let action = Actions::VaultTransfer(VaultTransfer {
220 vault_address,
221 is_deposit,
222 usd,
223 });
224 let connection_id = action.hash(timestamp, self.vault_address)?;
225 let action = serde_json::to_value(&action).map_err(|e| Error::JsonParse(e.to_string()))?;
226 let is_mainnet = self.http_client.is_mainnet();
227 let signature = sign_l1_action(wallet, connection_id, is_mainnet)?;
228
229 self.post(action, signature, timestamp).await
230 }
231
232 pub async fn market_open(
233 &self,
234 params: MarketOrderParams<'_>,
235 ) -> Result<ExchangeResponseStatus> {
236 let slippage = params.slippage.unwrap_or(0.05); let (px, sz_decimals) = self
238 .calculate_slippage_price(params.asset, params.is_buy, slippage, params.px)
239 .await?;
240
241 let order = ClientOrderRequest {
242 asset: params.asset.to_string(),
243 is_buy: params.is_buy,
244 reduce_only: false,
245 limit_px: px,
246 sz: round_to_decimals(params.sz, sz_decimals),
247 cloid: params.cloid,
248 order_type: ClientOrder::Limit(ClientLimit {
249 tif: "Ioc".to_string(),
250 }),
251 };
252
253 self.order(order, params.wallet).await
254 }
255
256 pub async fn market_close(
257 &self,
258 params: MarketCloseParams<'_>,
259 ) -> Result<ExchangeResponseStatus> {
260 let slippage = params.slippage.unwrap_or(0.05); let wallet = params.wallet.unwrap_or(&self.wallet);
262
263 let base_url = match self.http_client.base_url.as_str() {
264 "https://api.hyperliquid.xyz" => BaseUrl::Mainnet,
265 "https://api.hyperliquid-testnet.xyz" => BaseUrl::Testnet,
266 _ => return Err(Error::GenericRequest("Invalid base URL".to_string())),
267 };
268 let info_client = InfoClient::new(None, Some(base_url)).await?;
269 let user_state = info_client.user_state(wallet.address()).await?;
270
271 let position = user_state
272 .asset_positions
273 .iter()
274 .find(|p| p.position.coin == params.asset)
275 .ok_or_else(|| Error::AssetNotFound)?;
276
277 let szi = position
278 .position
279 .szi
280 .parse::<f64>()
281 .map_err(|_| Error::FloatStringParse)?;
282
283 let (px, sz_decimals) = self
284 .calculate_slippage_price(params.asset, szi < 0.0, slippage, params.px)
285 .await?;
286
287 let sz = round_to_decimals(params.sz.unwrap_or_else(|| szi.abs()), sz_decimals);
288
289 let order = ClientOrderRequest {
290 asset: params.asset.to_string(),
291 is_buy: szi < 0.0,
292 reduce_only: true,
293 limit_px: px,
294 sz,
295 cloid: params.cloid,
296 order_type: ClientOrder::Limit(ClientLimit {
297 tif: "Ioc".to_string(),
298 }),
299 };
300
301 self.order(order, Some(wallet)).await
302 }
303
304 async fn calculate_slippage_price(
305 &self,
306 asset: &str,
307 is_buy: bool,
308 slippage: f64,
309 px: Option<f64>,
310 ) -> Result<(f64, u32)> {
311 let base_url = match self.http_client.base_url.as_str() {
312 "https://api.hyperliquid.xyz" => BaseUrl::Mainnet,
313 "https://api.hyperliquid-testnet.xyz" => BaseUrl::Testnet,
314 _ => return Err(Error::GenericRequest("Invalid base URL".to_string())),
315 };
316 let info_client = InfoClient::new(None, Some(base_url)).await?;
317 let meta = info_client.meta().await?;
318
319 let asset_meta = meta
320 .universe
321 .iter()
322 .find(|a| a.name == asset)
323 .ok_or_else(|| Error::AssetNotFound)?;
324
325 let sz_decimals = asset_meta.sz_decimals;
326 let max_decimals: u32 = if self.coin_to_asset[asset] < 10000 {
327 6
328 } else {
329 8
330 };
331 let price_decimals = max_decimals.saturating_sub(sz_decimals);
332
333 let px = if let Some(px) = px {
334 px
335 } else {
336 let all_mids = info_client.all_mids().await?;
337 all_mids
338 .get(asset)
339 .ok_or_else(|| Error::AssetNotFound)?
340 .parse::<f64>()
341 .map_err(|_| Error::FloatStringParse)?
342 };
343
344 debug!("px before slippage: {px:?}");
345 let slippage_factor = if is_buy {
346 1.0 + slippage
347 } else {
348 1.0 - slippage
349 };
350 let px = px * slippage_factor;
351
352 let px = round_to_significant_and_decimal(px, 5, price_decimals);
354
355 debug!("px after slippage: {px:?}");
356 Ok((px, sz_decimals))
357 }
358
359 pub async fn order(
360 &self,
361 order: ClientOrderRequest,
362 wallet: Option<&LocalWallet>,
363 ) -> Result<ExchangeResponseStatus> {
364 self.bulk_order(vec![order], wallet).await
365 }
366
367 pub async fn bulk_order_prepared(
368 &self,
369 orders: Vec<OrderRequest>,
370 wallet: Option<&LocalWallet>,
371 ) -> Result<ExchangeResponseStatus> {
372 let wallet = wallet.unwrap_or(&self.wallet);
373 let timestamp = next_nonce();
374
375 let action = Actions::Order(BulkOrder {
376 orders,
377 grouping: "na".to_string(),
378 });
379 let connection_id = action.hash(timestamp, self.vault_address)?;
380 let action = serde_json::to_value(&action).map_err(|e| Error::JsonParse(e.to_string()))?;
381
382 let is_mainnet = self.http_client.is_mainnet();
383 let signature = sign_l1_action(wallet, connection_id, is_mainnet)?;
384 self.post(action, signature, timestamp).await
385 }
386
387 pub async fn bulk_order(
388 &self,
389 orders: Vec<ClientOrderRequest>,
390 wallet: Option<&LocalWallet>,
391 ) -> Result<ExchangeResponseStatus> {
392 let mut transformed_orders = Vec::new();
393
394 for order in orders {
395 transformed_orders.push(order.convert(&self.coin_to_asset)?);
396 }
397
398 self.bulk_order_prepared(transformed_orders, wallet).await
399 }
400
401 pub async fn cancel(
402 &self,
403 cancel: ClientCancelRequest,
404 wallet: Option<&LocalWallet>,
405 ) -> Result<ExchangeResponseStatus> {
406 self.bulk_cancel(vec![cancel], wallet).await
407 }
408
409 pub async fn bulk_cancel(
410 &self,
411 cancels: Vec<ClientCancelRequest>,
412 wallet: Option<&LocalWallet>,
413 ) -> Result<ExchangeResponseStatus> {
414 let wallet = wallet.unwrap_or(&self.wallet);
415 let timestamp = next_nonce();
416
417 let mut transformed_cancels = Vec::new();
418 for cancel in cancels.into_iter() {
419 let &asset = self
420 .coin_to_asset
421 .get(&cancel.asset)
422 .ok_or(Error::AssetNotFound)?;
423 transformed_cancels.push(CancelRequest {
424 asset,
425 oid: cancel.oid,
426 });
427 }
428
429 let action = Actions::Cancel(BulkCancel {
430 cancels: transformed_cancels,
431 });
432 let connection_id = action.hash(timestamp, self.vault_address)?;
433
434 let action = serde_json::to_value(&action).map_err(|e| Error::JsonParse(e.to_string()))?;
435 let is_mainnet = self.http_client.is_mainnet();
436 let signature = sign_l1_action(wallet, connection_id, is_mainnet)?;
437
438 self.post(action, signature, timestamp).await
439 }
440
441 pub async fn modify(
442 &self,
443 modify: ClientModifyRequest,
444 wallet: Option<&LocalWallet>,
445 ) -> Result<ExchangeResponseStatus> {
446 self.bulk_modify(vec![modify], wallet).await
447 }
448
449 pub async fn bulk_modify_prepared(
450 &self,
451 modifies: Vec<ModifyRequest>,
452 wallet: Option<&LocalWallet>,
453 ) -> Result<ExchangeResponseStatus> {
454 let wallet = wallet.unwrap_or(&self.wallet);
455 let timestamp = next_nonce();
456
457 let action = Actions::Modify(BulkModify { modifies });
458 let connection_id = action.hash(timestamp, self.vault_address)?;
459
460 let action = serde_json::to_value(&action).map_err(|e| Error::JsonParse(e.to_string()))?;
461 let is_mainnet = self.http_client.is_mainnet();
462 let signature = sign_l1_action(wallet, connection_id, is_mainnet)?;
463
464 self.post(action, signature, timestamp).await
465 }
466
467 pub async fn bulk_modify(
468 &self,
469 modifies: Vec<ClientModifyRequest>,
470 wallet: Option<&LocalWallet>,
471 ) -> Result<ExchangeResponseStatus> {
472 let mut transformed_modifies = Vec::new();
473 for modify in modifies.into_iter() {
474 transformed_modifies.push(ModifyRequest {
475 oid: modify.oid,
476 order: modify.order.convert(&self.coin_to_asset)?,
477 });
478 }
479
480 self.bulk_modify_prepared(transformed_modifies, wallet)
481 .await
482 }
483
484 pub async fn cancel_by_cloid(
485 &self,
486 cancel: ClientCancelRequestCloid,
487 wallet: Option<&LocalWallet>,
488 ) -> Result<ExchangeResponseStatus> {
489 self.bulk_cancel_by_cloid(vec![cancel], wallet).await
490 }
491
492 pub async fn bulk_cancel_by_cloid(
493 &self,
494 cancels: Vec<ClientCancelRequestCloid>,
495 wallet: Option<&LocalWallet>,
496 ) -> Result<ExchangeResponseStatus> {
497 let wallet = wallet.unwrap_or(&self.wallet);
498 let timestamp = next_nonce();
499
500 let mut transformed_cancels: Vec<CancelRequestCloid> = Vec::new();
501 for cancel in cancels.into_iter() {
502 let &asset = self
503 .coin_to_asset
504 .get(&cancel.asset)
505 .ok_or(Error::AssetNotFound)?;
506 transformed_cancels.push(CancelRequestCloid {
507 asset,
508 cloid: uuid_to_hex_string(cancel.cloid),
509 });
510 }
511
512 let action = Actions::CancelByCloid(BulkCancelCloid {
513 cancels: transformed_cancels,
514 });
515
516 let connection_id = action.hash(timestamp, self.vault_address)?;
517 let action = serde_json::to_value(&action).map_err(|e| Error::JsonParse(e.to_string()))?;
518 let is_mainnet = self.http_client.is_mainnet();
519 let signature = sign_l1_action(wallet, connection_id, is_mainnet)?;
520
521 self.post(action, signature, timestamp).await
522 }
523
524 pub async fn update_leverage(
525 &self,
526 leverage: u32,
527 coin: &str,
528 is_cross: bool,
529 wallet: Option<&LocalWallet>,
530 ) -> Result<ExchangeResponseStatus> {
531 let wallet = wallet.unwrap_or(&self.wallet);
532
533 let timestamp = next_nonce();
534
535 let &asset_index = self.coin_to_asset.get(coin).ok_or(Error::AssetNotFound)?;
536 let action = Actions::UpdateLeverage(UpdateLeverage {
537 asset: asset_index,
538 is_cross,
539 leverage,
540 });
541 let connection_id = action.hash(timestamp, self.vault_address)?;
542 let action = serde_json::to_value(&action).map_err(|e| Error::JsonParse(e.to_string()))?;
543 let is_mainnet = self.http_client.is_mainnet();
544 let signature = sign_l1_action(wallet, connection_id, is_mainnet)?;
545
546 self.post(action, signature, timestamp).await
547 }
548
549 pub async fn update_isolated_margin(
550 &self,
551 amount: f64,
552 coin: &str,
553 wallet: Option<&LocalWallet>,
554 ) -> Result<ExchangeResponseStatus> {
555 let wallet = wallet.unwrap_or(&self.wallet);
556
557 let amount = (amount * 1_000_000.0).round() as i64;
558 let timestamp = next_nonce();
559
560 let &asset_index = self.coin_to_asset.get(coin).ok_or(Error::AssetNotFound)?;
561 let action = Actions::UpdateIsolatedMargin(UpdateIsolatedMargin {
562 asset: asset_index,
563 is_buy: true,
564 ntli: amount,
565 });
566 let connection_id = action.hash(timestamp, self.vault_address)?;
567 let action = serde_json::to_value(&action).map_err(|e| Error::JsonParse(e.to_string()))?;
568 let is_mainnet = self.http_client.is_mainnet();
569 let signature = sign_l1_action(wallet, connection_id, is_mainnet)?;
570
571 self.post(action, signature, timestamp).await
572 }
573
574 pub async fn approve_agent(
575 &self,
576 wallet: Option<&LocalWallet>,
577 ) -> Result<(String, ExchangeResponseStatus)> {
578 let wallet = wallet.unwrap_or(&self.wallet);
579 let key = H256::from(generate_random_key()?).encode_hex()[2..].to_string();
580
581 let address = key
582 .parse::<LocalWallet>()
583 .map_err(|e| Error::PrivateKeyParse(e.to_string()))?
584 .address();
585
586 let hyperliquid_chain = if self.http_client.is_mainnet() {
587 "Mainnet".to_string()
588 } else {
589 "Testnet".to_string()
590 };
591
592 let nonce = next_nonce();
593 let approve_agent = ApproveAgent {
594 signature_chain_id: 421614.into(),
595 hyperliquid_chain,
596 agent_address: address,
597 agent_name: None,
598 nonce,
599 };
600 let signature = sign_typed_data(&approve_agent, wallet)?;
601 let action = serde_json::to_value(Actions::ApproveAgent(approve_agent))
602 .map_err(|e| Error::JsonParse(e.to_string()))?;
603 Ok((key, self.post(action, signature, nonce).await?))
604 }
605
606 pub async fn withdraw_from_bridge(
607 &self,
608 amount: &str,
609 destination: &str,
610 wallet: Option<&LocalWallet>,
611 ) -> Result<ExchangeResponseStatus> {
612 let wallet = wallet.unwrap_or(&self.wallet);
613 let hyperliquid_chain = if self.http_client.is_mainnet() {
614 "Mainnet".to_string()
615 } else {
616 "Testnet".to_string()
617 };
618
619 let timestamp = next_nonce();
620 let withdraw = Withdraw3 {
621 signature_chain_id: 421614.into(),
622 hyperliquid_chain,
623 destination: destination.to_string(),
624 amount: amount.to_string(),
625 time: timestamp,
626 };
627 let signature = sign_typed_data(&withdraw, wallet)?;
628 let action = serde_json::to_value(Actions::Withdraw3(withdraw))
629 .map_err(|e| Error::JsonParse(e.to_string()))?;
630
631 self.post(action, signature, timestamp).await
632 }
633
634 pub async fn spot_transfer(
635 &self,
636 amount: &str,
637 destination: &str,
638 token: &str,
639 wallet: Option<&LocalWallet>,
640 ) -> Result<ExchangeResponseStatus> {
641 let wallet = wallet.unwrap_or(&self.wallet);
642 let hyperliquid_chain = if self.http_client.is_mainnet() {
643 "Mainnet".to_string()
644 } else {
645 "Testnet".to_string()
646 };
647
648 let timestamp = next_nonce();
649 let spot_send = SpotSend {
650 signature_chain_id: 421614.into(),
651 hyperliquid_chain,
652 destination: destination.to_string(),
653 amount: amount.to_string(),
654 time: timestamp,
655 token: token.to_string(),
656 };
657 let signature = sign_typed_data(&spot_send, wallet)?;
658 let action = serde_json::to_value(Actions::SpotSend(spot_send))
659 .map_err(|e| Error::JsonParse(e.to_string()))?;
660
661 self.post(action, signature, timestamp).await
662 }
663
664 pub async fn set_referrer(
665 &self,
666 code: String,
667 wallet: Option<&LocalWallet>,
668 ) -> Result<ExchangeResponseStatus> {
669 let wallet = wallet.unwrap_or(&self.wallet);
670 let timestamp = next_nonce();
671
672 let action = Actions::SetReferrer(SetReferrer { code });
673
674 let connection_id = action.hash(timestamp, self.vault_address)?;
675 let action = serde_json::to_value(&action).map_err(|e| Error::JsonParse(e.to_string()))?;
676
677 let is_mainnet = self.http_client.is_mainnet();
678 let signature = sign_l1_action(wallet, connection_id, is_mainnet)?;
679 self.post(action, signature, timestamp).await
680 }
681}
682
683fn round_to_decimals(value: f64, decimals: u32) -> f64 {
684 let factor = 10f64.powi(decimals as i32);
685 (value * factor).round() / factor
686}
687
688fn round_to_significant_and_decimal(value: f64, sig_figs: u32, max_decimals: u32) -> f64 {
689 let abs_value = value.abs();
690 let magnitude = abs_value.log10().floor() as i32;
691 let scale = 10f64.powi(sig_figs as i32 - magnitude - 1);
692 let rounded = (abs_value * scale).round() / scale;
693 round_to_decimals(rounded.copysign(value), max_decimals)
694}
695
696#[cfg(test)]
697mod tests {
698 use std::str::FromStr;
699
700 use super::*;
701 use crate::{
702 exchange::order::{Limit, OrderRequest, Trigger},
703 Order,
704 };
705
706 fn get_wallet() -> Result<LocalWallet> {
707 let priv_key = "e908f86dbb4d55ac876378565aafeabc187f6690f046459397b17d9b9a19688e";
708 priv_key
709 .parse::<LocalWallet>()
710 .map_err(|e| Error::Wallet(e.to_string()))
711 }
712
713 #[test]
714 fn test_limit_order_action_hashing() -> Result<()> {
715 let wallet = get_wallet()?;
716 let action = Actions::Order(BulkOrder {
717 orders: vec![OrderRequest {
718 asset: 1,
719 is_buy: true,
720 limit_px: "2000.0".to_string(),
721 sz: "3.5".to_string(),
722 reduce_only: false,
723 order_type: Order::Limit(Limit {
724 tif: "Ioc".to_string(),
725 }),
726 cloid: None,
727 }],
728 grouping: "na".to_string(),
729 });
730 let connection_id = action.hash(1583838, None)?;
731
732 let signature = sign_l1_action(&wallet, connection_id, true)?;
733 assert_eq!(signature.to_string(), "77957e58e70f43b6b68581f2dc42011fc384538a2e5b7bf42d5b936f19fbb67360721a8598727230f67080efee48c812a6a4442013fd3b0eed509171bef9f23f1c");
734
735 let signature = sign_l1_action(&wallet, connection_id, false)?;
736 assert_eq!(signature.to_string(), "cd0925372ff1ed499e54883e9a6205ecfadec748f80ec463fe2f84f1209648776377961965cb7b12414186b1ea291e95fd512722427efcbcfb3b0b2bcd4d79d01c");
737
738 Ok(())
739 }
740
741 #[test]
742 fn test_limit_order_action_hashing_with_cloid() -> Result<()> {
743 let cloid = uuid::Uuid::from_str("1e60610f-0b3d-4205-97c8-8c1fed2ad5ee")
744 .map_err(|_e| uuid::Uuid::new_v4());
745 let wallet = get_wallet()?;
746 let action = Actions::Order(BulkOrder {
747 orders: vec![OrderRequest {
748 asset: 1,
749 is_buy: true,
750 limit_px: "2000.0".to_string(),
751 sz: "3.5".to_string(),
752 reduce_only: false,
753 order_type: Order::Limit(Limit {
754 tif: "Ioc".to_string(),
755 }),
756 cloid: Some(uuid_to_hex_string(cloid.unwrap())),
757 }],
758 grouping: "na".to_string(),
759 });
760 let connection_id = action.hash(1583838, None)?;
761
762 let signature = sign_l1_action(&wallet, connection_id, true)?;
763 assert_eq!(signature.to_string(), "d3e894092eb27098077145714630a77bbe3836120ee29df7d935d8510b03a08f456de5ec1be82aa65fc6ecda9ef928b0445e212517a98858cfaa251c4cd7552b1c");
764
765 let signature = sign_l1_action(&wallet, connection_id, false)?;
766 assert_eq!(signature.to_string(), "3768349dbb22a7fd770fc9fc50c7b5124a7da342ea579b309f58002ceae49b4357badc7909770919c45d850aabb08474ff2b7b3204ae5b66d9f7375582981f111c");
767
768 Ok(())
769 }
770
771 #[test]
772 fn test_tpsl_order_action_hashing() -> Result<()> {
773 for (tpsl, mainnet_signature, testnet_signature) in [
774 (
775 "tp",
776 "b91e5011dff15e4b4a40753730bda44972132e7b75641f3cac58b66159534a170d422ee1ac3c7a7a2e11e298108a2d6b8da8612caceaeeb3e571de3b2dfda9e41b",
777 "6df38b609904d0d4439884756b8f366f22b3a081801dbdd23f279094a2299fac6424cb0cdc48c3706aeaa368f81959e91059205403d3afd23a55983f710aee871b"
778 ),
779 (
780 "sl",
781 "8456d2ace666fce1bee1084b00e9620fb20e810368841e9d4dd80eb29014611a0843416e51b1529c22dd2fc28f7ff8f6443875635c72011f60b62cbb8ce90e2d1c",
782 "eb5bdb52297c1d19da45458758bd569dcb24c07e5c7bd52cf76600fd92fdd8213e661e21899c985421ec018a9ee7f3790e7b7d723a9932b7b5adcd7def5354601c"
783 )
784 ] {
785 let wallet = get_wallet()?;
786 let action = Actions::Order(BulkOrder {
787 orders: vec![
788 OrderRequest {
789 asset: 1,
790 is_buy: true,
791 limit_px: "2000.0".to_string(),
792 sz: "3.5".to_string(),
793 reduce_only: false,
794 order_type: Order::Trigger(Trigger {
795 trigger_px: "2000.0".to_string(),
796 is_market: true,
797 tpsl: tpsl.to_string(),
798 }),
799 cloid: None,
800 }
801 ],
802 grouping: "na".to_string(),
803 });
804 let connection_id = action.hash(1583838, None)?;
805
806 let signature = sign_l1_action(&wallet, connection_id, true)?;
807 assert_eq!(signature.to_string(), mainnet_signature);
808
809 let signature = sign_l1_action(&wallet, connection_id, false)?;
810 assert_eq!(signature.to_string(), testnet_signature);
811 }
812 Ok(())
813 }
814
815 #[test]
816 fn test_cancel_action_hashing() -> Result<()> {
817 let wallet = get_wallet()?;
818 let action = Actions::Cancel(BulkCancel {
819 cancels: vec![CancelRequest {
820 asset: 1,
821 oid: 82382,
822 }],
823 });
824 let connection_id = action.hash(1583838, None)?;
825
826 let signature = sign_l1_action(&wallet, connection_id, true)?;
827 assert_eq!(signature.to_string(), "02f76cc5b16e0810152fa0e14e7b219f49c361e3325f771544c6f54e157bf9fa17ed0afc11a98596be85d5cd9f86600aad515337318f7ab346e5ccc1b03425d51b");
828
829 let signature = sign_l1_action(&wallet, connection_id, false)?;
830 assert_eq!(signature.to_string(), "6ffebadfd48067663390962539fbde76cfa36f53be65abe2ab72c9db6d0db44457720db9d7c4860f142a484f070c84eb4b9694c3a617c83f0d698a27e55fd5e01c");
831
832 Ok(())
833 }
834}