hyperliquid_rust_sdk_abrkn/exchange/
exchange_client.rs

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        // payload expects usdc without decimals
188        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); // Default 5% slippage
237        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); // Default 5% slippage
261        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        // Round to the correct number of decimal places and significant figures
353        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}