use crate::signature::sign_typed_data;
use crate::{
exchange::{
actions::{
ApproveAgent, BulkCancel, BulkModify, BulkOrder, SetReferrer, UpdateIsolatedMargin,
UpdateLeverage, UsdSend,
},
cancel::{CancelRequest, CancelRequestCloid},
modify::{ClientModifyRequest, ModifyRequest},
ClientCancelRequest, ClientOrderRequest,
},
helpers::{generate_random_key, next_nonce, uuid_to_hex_string},
info::info_client::InfoClient,
meta::Meta,
prelude::*,
req::HttpClient,
signature::sign_l1_action,
BaseUrl, BulkCancelCloid, Error, ExchangeResponseStatus,
};
use crate::{ClassTransfer, SpotSend, SpotUser, VaultTransfer, Withdraw3};
use ethers::{
abi::AbiEncode,
signers::{LocalWallet, Signer},
types::{Signature, H160, H256},
};
use log::debug;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use super::cancel::ClientCancelRequestCloid;
use super::order::{MarketCloseParams, MarketOrderParams};
use super::{ClientLimit, ClientOrder};
pub struct ExchangeClient {
pub http_client: HttpClient,
pub wallet: LocalWallet,
pub meta: Meta,
pub vault_address: Option<H160>,
pub coin_to_asset: HashMap<String, u32>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ExchangePayload {
action: serde_json::Value,
signature: Signature,
nonce: u64,
vault_address: Option<H160>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
#[serde(rename_all = "camelCase")]
pub enum Actions {
UsdSend(UsdSend),
UpdateLeverage(UpdateLeverage),
UpdateIsolatedMargin(UpdateIsolatedMargin),
Order(BulkOrder),
Cancel(BulkCancel),
CancelByCloid(BulkCancelCloid),
#[serde(rename = "batchModify")]
Modify(BulkModify),
ApproveAgent(ApproveAgent),
Withdraw3(Withdraw3),
SpotUser(SpotUser),
VaultTransfer(VaultTransfer),
SpotSend(SpotSend),
SetReferrer(SetReferrer),
}
impl Actions {
fn hash(&self, timestamp: u64, vault_address: Option<H160>) -> Result<H256> {
let mut bytes =
rmp_serde::to_vec_named(self).map_err(|e| Error::RmpParse(e.to_string()))?;
bytes.extend(timestamp.to_be_bytes());
if let Some(vault_address) = vault_address {
bytes.push(1);
bytes.extend(vault_address.to_fixed_bytes());
} else {
bytes.push(0);
}
Ok(H256(ethers::utils::keccak256(bytes)))
}
}
impl ExchangeClient {
pub async fn new(
client: Option<Client>,
wallet: LocalWallet,
base_url: Option<BaseUrl>,
meta: Option<Meta>,
vault_address: Option<H160>,
) -> Result<ExchangeClient> {
let client = client.unwrap_or_default();
let base_url = base_url.unwrap_or(BaseUrl::Mainnet);
let info = InfoClient::new(None, Some(base_url)).await?;
let meta = if let Some(meta) = meta {
meta
} else {
info.meta().await?
};
let mut coin_to_asset = HashMap::new();
for (asset_ind, asset) in meta.universe.iter().enumerate() {
coin_to_asset.insert(asset.name.clone(), asset_ind as u32);
}
coin_to_asset = info
.spot_meta()
.await?
.add_pair_and_name_to_index_map(coin_to_asset);
Ok(ExchangeClient {
wallet,
meta,
vault_address,
http_client: HttpClient {
client,
base_url: base_url.get_url(),
},
coin_to_asset,
})
}
async fn post(
&self,
action: serde_json::Value,
signature: Signature,
nonce: u64,
) -> Result<ExchangeResponseStatus> {
let exchange_payload = ExchangePayload {
action,
signature,
nonce,
vault_address: self.vault_address,
};
let res = serde_json::to_string(&exchange_payload)
.map_err(|e| Error::JsonParse(e.to_string()))?;
debug!("Sending request {res:?}");
serde_json::from_str(
&self
.http_client
.post("/exchange", res)
.await
.map_err(|e| Error::JsonParse(e.to_string()))?,
)
.map_err(|e| Error::JsonParse(e.to_string()))
}
pub async fn usdc_transfer(
&self,
amount: &str,
destination: &str,
wallet: Option<&LocalWallet>,
) -> Result<ExchangeResponseStatus> {
let wallet = wallet.unwrap_or(&self.wallet);
let hyperliquid_chain = if self.http_client.is_mainnet() {
"Mainnet".to_string()
} else {
"Testnet".to_string()
};
let timestamp = next_nonce();
let usd_send = UsdSend {
signature_chain_id: 421614.into(),
hyperliquid_chain,
destination: destination.to_string(),
amount: amount.to_string(),
time: timestamp,
};
let signature = sign_typed_data(&usd_send, wallet)?;
let action = serde_json::to_value(Actions::UsdSend(usd_send))
.map_err(|e| Error::JsonParse(e.to_string()))?;
self.post(action, signature, timestamp).await
}
pub async fn class_transfer(
&self,
usdc: f64,
to_perp: bool,
wallet: Option<&LocalWallet>,
) -> Result<ExchangeResponseStatus> {
let usdc = (usdc * 1e6).round() as u64;
let wallet = wallet.unwrap_or(&self.wallet);
let timestamp = next_nonce();
let action = Actions::SpotUser(SpotUser {
class_transfer: ClassTransfer { usdc, to_perp },
});
let connection_id = action.hash(timestamp, self.vault_address)?;
let action = serde_json::to_value(&action).map_err(|e| Error::JsonParse(e.to_string()))?;
let is_mainnet = self.http_client.is_mainnet();
let signature = sign_l1_action(wallet, connection_id, is_mainnet)?;
self.post(action, signature, timestamp).await
}
pub async fn vault_transfer(
&self,
is_deposit: bool,
usd: String,
vault_address: Option<H160>,
wallet: Option<&LocalWallet>,
) -> Result<ExchangeResponseStatus> {
let vault_address = self
.vault_address
.or(vault_address)
.ok_or_else(|| Error::VaultAddressNotFound)?;
let wallet = wallet.unwrap_or(&self.wallet);
let timestamp = next_nonce();
let action = Actions::VaultTransfer(VaultTransfer {
vault_address,
is_deposit,
usd,
});
let connection_id = action.hash(timestamp, self.vault_address)?;
let action = serde_json::to_value(&action).map_err(|e| Error::JsonParse(e.to_string()))?;
let is_mainnet = self.http_client.is_mainnet();
let signature = sign_l1_action(wallet, connection_id, is_mainnet)?;
self.post(action, signature, timestamp).await
}
pub async fn market_open(
&self,
params: MarketOrderParams<'_>,
) -> Result<ExchangeResponseStatus> {
let slippage = params.slippage.unwrap_or(0.05); let (px, sz_decimals) = self
.calculate_slippage_price(params.asset, params.is_buy, slippage, params.px)
.await?;
let order = ClientOrderRequest {
asset: params.asset.to_string(),
is_buy: params.is_buy,
reduce_only: false,
limit_px: px,
sz: round_to_decimals(params.sz, sz_decimals),
cloid: params.cloid,
order_type: ClientOrder::Limit(ClientLimit {
tif: "Ioc".to_string(),
}),
};
self.order(order, params.wallet).await
}
pub async fn market_close(
&self,
params: MarketCloseParams<'_>,
) -> Result<ExchangeResponseStatus> {
let slippage = params.slippage.unwrap_or(0.05); let wallet = params.wallet.unwrap_or(&self.wallet);
let base_url = match self.http_client.base_url.as_str() {
"https://api.hyperliquid.xyz" => BaseUrl::Mainnet,
"https://api.hyperliquid-testnet.xyz" => BaseUrl::Testnet,
_ => return Err(Error::GenericRequest("Invalid base URL".to_string())),
};
let info_client = InfoClient::new(None, Some(base_url)).await?;
let user_state = info_client.user_state(wallet.address()).await?;
let position = user_state
.asset_positions
.iter()
.find(|p| p.position.coin == params.asset)
.ok_or_else(|| Error::AssetNotFound)?;
let szi = position
.position
.szi
.parse::<f64>()
.map_err(|_| Error::FloatStringParse)?;
let (px, sz_decimals) = self
.calculate_slippage_price(params.asset, szi < 0.0, slippage, params.px)
.await?;
let sz = round_to_decimals(params.sz.unwrap_or_else(|| szi.abs()), sz_decimals);
let order = ClientOrderRequest {
asset: params.asset.to_string(),
is_buy: szi < 0.0,
reduce_only: true,
limit_px: px,
sz,
cloid: params.cloid,
order_type: ClientOrder::Limit(ClientLimit {
tif: "Ioc".to_string(),
}),
};
self.order(order, Some(wallet)).await
}
async fn calculate_slippage_price(
&self,
asset: &str,
is_buy: bool,
slippage: f64,
px: Option<f64>,
) -> Result<(f64, u32)> {
let base_url = match self.http_client.base_url.as_str() {
"https://api.hyperliquid.xyz" => BaseUrl::Mainnet,
"https://api.hyperliquid-testnet.xyz" => BaseUrl::Testnet,
_ => return Err(Error::GenericRequest("Invalid base URL".to_string())),
};
let info_client = InfoClient::new(None, Some(base_url)).await?;
let meta = info_client.meta().await?;
let asset_meta = meta
.universe
.iter()
.find(|a| a.name == asset)
.ok_or_else(|| Error::AssetNotFound)?;
let sz_decimals = asset_meta.sz_decimals;
let max_decimals: u32 = if self.coin_to_asset[asset] < 10000 {
6
} else {
8
};
let price_decimals = max_decimals.saturating_sub(sz_decimals);
let px = if let Some(px) = px {
px
} else {
let all_mids = info_client.all_mids().await?;
all_mids
.get(asset)
.ok_or_else(|| Error::AssetNotFound)?
.parse::<f64>()
.map_err(|_| Error::FloatStringParse)?
};
debug!("px before slippage: {px:?}");
let slippage_factor = if is_buy {
1.0 + slippage
} else {
1.0 - slippage
};
let px = px * slippage_factor;
let px = round_to_significant_and_decimal(px, 5, price_decimals);
debug!("px after slippage: {px:?}");
Ok((px, sz_decimals))
}
pub async fn order(
&self,
order: ClientOrderRequest,
wallet: Option<&LocalWallet>,
) -> Result<ExchangeResponseStatus> {
self.bulk_order(vec![order], wallet).await
}
pub async fn bulk_order(
&self,
orders: Vec<ClientOrderRequest>,
wallet: Option<&LocalWallet>,
) -> Result<ExchangeResponseStatus> {
let wallet = wallet.unwrap_or(&self.wallet);
let timestamp = next_nonce();
let mut transformed_orders = Vec::new();
for order in orders {
transformed_orders.push(order.convert(&self.coin_to_asset)?);
}
let action = Actions::Order(BulkOrder {
orders: transformed_orders,
grouping: "na".to_string(),
});
let connection_id = action.hash(timestamp, self.vault_address)?;
let action = serde_json::to_value(&action).map_err(|e| Error::JsonParse(e.to_string()))?;
let is_mainnet = self.http_client.is_mainnet();
let signature = sign_l1_action(wallet, connection_id, is_mainnet)?;
self.post(action, signature, timestamp).await
}
pub async fn cancel(
&self,
cancel: ClientCancelRequest,
wallet: Option<&LocalWallet>,
) -> Result<ExchangeResponseStatus> {
self.bulk_cancel(vec![cancel], wallet).await
}
pub async fn bulk_cancel(
&self,
cancels: Vec<ClientCancelRequest>,
wallet: Option<&LocalWallet>,
) -> Result<ExchangeResponseStatus> {
let wallet = wallet.unwrap_or(&self.wallet);
let timestamp = next_nonce();
let mut transformed_cancels = Vec::new();
for cancel in cancels.into_iter() {
let &asset = self
.coin_to_asset
.get(&cancel.asset)
.ok_or(Error::AssetNotFound)?;
transformed_cancels.push(CancelRequest {
asset,
oid: cancel.oid,
});
}
let action = Actions::Cancel(BulkCancel {
cancels: transformed_cancels,
});
let connection_id = action.hash(timestamp, self.vault_address)?;
let action = serde_json::to_value(&action).map_err(|e| Error::JsonParse(e.to_string()))?;
let is_mainnet = self.http_client.is_mainnet();
let signature = sign_l1_action(wallet, connection_id, is_mainnet)?;
self.post(action, signature, timestamp).await
}
pub async fn modify(
&self,
modify: ClientModifyRequest,
wallet: Option<&LocalWallet>,
) -> Result<ExchangeResponseStatus> {
self.bulk_modify(vec![modify], wallet).await
}
pub async fn bulk_modify(
&self,
modifies: Vec<ClientModifyRequest>,
wallet: Option<&LocalWallet>,
) -> Result<ExchangeResponseStatus> {
let wallet = wallet.unwrap_or(&self.wallet);
let timestamp = next_nonce();
let mut transformed_modifies = Vec::new();
for modify in modifies.into_iter() {
transformed_modifies.push(ModifyRequest {
oid: modify.oid,
order: modify.order.convert(&self.coin_to_asset)?,
});
}
let action = Actions::Modify(BulkModify {
modifies: transformed_modifies,
});
let connection_id = action.hash(timestamp, self.vault_address)?;
let action = serde_json::to_value(&action).map_err(|e| Error::JsonParse(e.to_string()))?;
let is_mainnet = self.http_client.is_mainnet();
let signature = sign_l1_action(wallet, connection_id, is_mainnet)?;
self.post(action, signature, timestamp).await
}
pub async fn cancel_by_cloid(
&self,
cancel: ClientCancelRequestCloid,
wallet: Option<&LocalWallet>,
) -> Result<ExchangeResponseStatus> {
self.bulk_cancel_by_cloid(vec![cancel], wallet).await
}
pub async fn bulk_cancel_by_cloid(
&self,
cancels: Vec<ClientCancelRequestCloid>,
wallet: Option<&LocalWallet>,
) -> Result<ExchangeResponseStatus> {
let wallet = wallet.unwrap_or(&self.wallet);
let timestamp = next_nonce();
let mut transformed_cancels: Vec<CancelRequestCloid> = Vec::new();
for cancel in cancels.into_iter() {
let &asset = self
.coin_to_asset
.get(&cancel.asset)
.ok_or(Error::AssetNotFound)?;
transformed_cancels.push(CancelRequestCloid {
asset,
cloid: uuid_to_hex_string(cancel.cloid),
});
}
let action = Actions::CancelByCloid(BulkCancelCloid {
cancels: transformed_cancels,
});
let connection_id = action.hash(timestamp, self.vault_address)?;
let action = serde_json::to_value(&action).map_err(|e| Error::JsonParse(e.to_string()))?;
let is_mainnet = self.http_client.is_mainnet();
let signature = sign_l1_action(wallet, connection_id, is_mainnet)?;
self.post(action, signature, timestamp).await
}
pub async fn update_leverage(
&self,
leverage: u32,
coin: &str,
is_cross: bool,
wallet: Option<&LocalWallet>,
) -> Result<ExchangeResponseStatus> {
let wallet = wallet.unwrap_or(&self.wallet);
let timestamp = next_nonce();
let &asset_index = self.coin_to_asset.get(coin).ok_or(Error::AssetNotFound)?;
let action = Actions::UpdateLeverage(UpdateLeverage {
asset: asset_index,
is_cross,
leverage,
});
let connection_id = action.hash(timestamp, self.vault_address)?;
let action = serde_json::to_value(&action).map_err(|e| Error::JsonParse(e.to_string()))?;
let is_mainnet = self.http_client.is_mainnet();
let signature = sign_l1_action(wallet, connection_id, is_mainnet)?;
self.post(action, signature, timestamp).await
}
pub async fn update_isolated_margin(
&self,
amount: f64,
coin: &str,
wallet: Option<&LocalWallet>,
) -> Result<ExchangeResponseStatus> {
let wallet = wallet.unwrap_or(&self.wallet);
let amount = (amount * 1_000_000.0).round() as i64;
let timestamp = next_nonce();
let &asset_index = self.coin_to_asset.get(coin).ok_or(Error::AssetNotFound)?;
let action = Actions::UpdateIsolatedMargin(UpdateIsolatedMargin {
asset: asset_index,
is_buy: true,
ntli: amount,
});
let connection_id = action.hash(timestamp, self.vault_address)?;
let action = serde_json::to_value(&action).map_err(|e| Error::JsonParse(e.to_string()))?;
let is_mainnet = self.http_client.is_mainnet();
let signature = sign_l1_action(wallet, connection_id, is_mainnet)?;
self.post(action, signature, timestamp).await
}
pub async fn approve_agent(
&self,
wallet: Option<&LocalWallet>,
) -> Result<(String, ExchangeResponseStatus)> {
let wallet = wallet.unwrap_or(&self.wallet);
let key = H256::from(generate_random_key()?).encode_hex()[2..].to_string();
let address = key
.parse::<LocalWallet>()
.map_err(|e| Error::PrivateKeyParse(e.to_string()))?
.address();
let hyperliquid_chain = if self.http_client.is_mainnet() {
"Mainnet".to_string()
} else {
"Testnet".to_string()
};
let nonce = next_nonce();
let approve_agent = ApproveAgent {
signature_chain_id: 421614.into(),
hyperliquid_chain,
agent_address: address,
agent_name: None,
nonce,
};
let signature = sign_typed_data(&approve_agent, wallet)?;
let action = serde_json::to_value(Actions::ApproveAgent(approve_agent))
.map_err(|e| Error::JsonParse(e.to_string()))?;
Ok((key, self.post(action, signature, nonce).await?))
}
pub async fn withdraw_from_bridge(
&self,
amount: &str,
destination: &str,
wallet: Option<&LocalWallet>,
) -> Result<ExchangeResponseStatus> {
let wallet = wallet.unwrap_or(&self.wallet);
let hyperliquid_chain = if self.http_client.is_mainnet() {
"Mainnet".to_string()
} else {
"Testnet".to_string()
};
let timestamp = next_nonce();
let withdraw = Withdraw3 {
signature_chain_id: 421614.into(),
hyperliquid_chain,
destination: destination.to_string(),
amount: amount.to_string(),
time: timestamp,
};
let signature = sign_typed_data(&withdraw, wallet)?;
let action = serde_json::to_value(Actions::Withdraw3(withdraw))
.map_err(|e| Error::JsonParse(e.to_string()))?;
self.post(action, signature, timestamp).await
}
pub async fn spot_transfer(
&self,
amount: &str,
destination: &str,
token: &str,
wallet: Option<&LocalWallet>,
) -> Result<ExchangeResponseStatus> {
let wallet = wallet.unwrap_or(&self.wallet);
let hyperliquid_chain = if self.http_client.is_mainnet() {
"Mainnet".to_string()
} else {
"Testnet".to_string()
};
let timestamp = next_nonce();
let spot_send = SpotSend {
signature_chain_id: 421614.into(),
hyperliquid_chain,
destination: destination.to_string(),
amount: amount.to_string(),
time: timestamp,
token: token.to_string(),
};
let signature = sign_typed_data(&spot_send, wallet)?;
let action = serde_json::to_value(Actions::SpotSend(spot_send))
.map_err(|e| Error::JsonParse(e.to_string()))?;
self.post(action, signature, timestamp).await
}
pub async fn set_referrer(
&self,
code: String,
wallet: Option<&LocalWallet>,
) -> Result<ExchangeResponseStatus> {
let wallet = wallet.unwrap_or(&self.wallet);
let timestamp = next_nonce();
let action = Actions::SetReferrer(SetReferrer { code });
let connection_id = action.hash(timestamp, self.vault_address)?;
let action = serde_json::to_value(&action).map_err(|e| Error::JsonParse(e.to_string()))?;
let is_mainnet = self.http_client.is_mainnet();
let signature = sign_l1_action(wallet, connection_id, is_mainnet)?;
self.post(action, signature, timestamp).await
}
}
fn round_to_decimals(value: f64, decimals: u32) -> f64 {
let factor = 10f64.powi(decimals as i32);
(value * factor).round() / factor
}
fn round_to_significant_and_decimal(value: f64, sig_figs: u32, max_decimals: u32) -> f64 {
let abs_value = value.abs();
let magnitude = abs_value.log10().floor() as i32;
let scale = 10f64.powi(sig_figs as i32 - magnitude - 1);
let rounded = (abs_value * scale).round() / scale;
round_to_decimals(rounded.copysign(value), max_decimals)
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use super::*;
use crate::{
exchange::order::{Limit, OrderRequest, Trigger},
Order,
};
fn get_wallet() -> Result<LocalWallet> {
let priv_key = "e908f86dbb4d55ac876378565aafeabc187f6690f046459397b17d9b9a19688e";
priv_key
.parse::<LocalWallet>()
.map_err(|e| Error::Wallet(e.to_string()))
}
#[test]
fn test_limit_order_action_hashing() -> Result<()> {
let wallet = get_wallet()?;
let action = Actions::Order(BulkOrder {
orders: vec![OrderRequest {
asset: 1,
is_buy: true,
limit_px: "2000.0".to_string(),
sz: "3.5".to_string(),
reduce_only: false,
order_type: Order::Limit(Limit {
tif: "Ioc".to_string(),
}),
cloid: None,
}],
grouping: "na".to_string(),
});
let connection_id = action.hash(1583838, None)?;
let signature = sign_l1_action(&wallet, connection_id, true)?;
assert_eq!(signature.to_string(), "77957e58e70f43b6b68581f2dc42011fc384538a2e5b7bf42d5b936f19fbb67360721a8598727230f67080efee48c812a6a4442013fd3b0eed509171bef9f23f1c");
let signature = sign_l1_action(&wallet, connection_id, false)?;
assert_eq!(signature.to_string(), "cd0925372ff1ed499e54883e9a6205ecfadec748f80ec463fe2f84f1209648776377961965cb7b12414186b1ea291e95fd512722427efcbcfb3b0b2bcd4d79d01c");
Ok(())
}
#[test]
fn test_limit_order_action_hashing_with_cloid() -> Result<()> {
let cloid = uuid::Uuid::from_str("1e60610f-0b3d-4205-97c8-8c1fed2ad5ee")
.map_err(|_e| uuid::Uuid::new_v4());
let wallet = get_wallet()?;
let action = Actions::Order(BulkOrder {
orders: vec![OrderRequest {
asset: 1,
is_buy: true,
limit_px: "2000.0".to_string(),
sz: "3.5".to_string(),
reduce_only: false,
order_type: Order::Limit(Limit {
tif: "Ioc".to_string(),
}),
cloid: Some(uuid_to_hex_string(cloid.unwrap())),
}],
grouping: "na".to_string(),
});
let connection_id = action.hash(1583838, None)?;
let signature = sign_l1_action(&wallet, connection_id, true)?;
assert_eq!(signature.to_string(), "d3e894092eb27098077145714630a77bbe3836120ee29df7d935d8510b03a08f456de5ec1be82aa65fc6ecda9ef928b0445e212517a98858cfaa251c4cd7552b1c");
let signature = sign_l1_action(&wallet, connection_id, false)?;
assert_eq!(signature.to_string(), "3768349dbb22a7fd770fc9fc50c7b5124a7da342ea579b309f58002ceae49b4357badc7909770919c45d850aabb08474ff2b7b3204ae5b66d9f7375582981f111c");
Ok(())
}
#[test]
fn test_tpsl_order_action_hashing() -> Result<()> {
for (tpsl, mainnet_signature, testnet_signature) in [
(
"tp",
"b91e5011dff15e4b4a40753730bda44972132e7b75641f3cac58b66159534a170d422ee1ac3c7a7a2e11e298108a2d6b8da8612caceaeeb3e571de3b2dfda9e41b",
"6df38b609904d0d4439884756b8f366f22b3a081801dbdd23f279094a2299fac6424cb0cdc48c3706aeaa368f81959e91059205403d3afd23a55983f710aee871b"
),
(
"sl",
"8456d2ace666fce1bee1084b00e9620fb20e810368841e9d4dd80eb29014611a0843416e51b1529c22dd2fc28f7ff8f6443875635c72011f60b62cbb8ce90e2d1c",
"eb5bdb52297c1d19da45458758bd569dcb24c07e5c7bd52cf76600fd92fdd8213e661e21899c985421ec018a9ee7f3790e7b7d723a9932b7b5adcd7def5354601c"
)
] {
let wallet = get_wallet()?;
let action = Actions::Order(BulkOrder {
orders: vec![
OrderRequest {
asset: 1,
is_buy: true,
limit_px: "2000.0".to_string(),
sz: "3.5".to_string(),
reduce_only: false,
order_type: Order::Trigger(Trigger {
trigger_px: "2000.0".to_string(),
is_market: true,
tpsl: tpsl.to_string(),
}),
cloid: None,
}
],
grouping: "na".to_string(),
});
let connection_id = action.hash(1583838, None)?;
let signature = sign_l1_action(&wallet, connection_id, true)?;
assert_eq!(signature.to_string(), mainnet_signature);
let signature = sign_l1_action(&wallet, connection_id, false)?;
assert_eq!(signature.to_string(), testnet_signature);
}
Ok(())
}
#[test]
fn test_cancel_action_hashing() -> Result<()> {
let wallet = get_wallet()?;
let action = Actions::Cancel(BulkCancel {
cancels: vec![CancelRequest {
asset: 1,
oid: 82382,
}],
});
let connection_id = action.hash(1583838, None)?;
let signature = sign_l1_action(&wallet, connection_id, true)?;
assert_eq!(signature.to_string(), "02f76cc5b16e0810152fa0e14e7b219f49c361e3325f771544c6f54e157bf9fa17ed0afc11a98596be85d5cd9f86600aad515337318f7ab346e5ccc1b03425d51b");
let signature = sign_l1_action(&wallet, connection_id, false)?;
assert_eq!(signature.to_string(), "6ffebadfd48067663390962539fbde76cfa36f53be65abe2ab72c9db6d0db44457720db9d7c4860f142a484f070c84eb4b9694c3a617c83f0d698a27e55fd5e01c");
Ok(())
}
}