use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use crate::core::utils::crypto_evm::{self, EvmWallet};
use crate::core::{
Credentials, ExchangeResult, ExchangeError,
timestamp_millis,
};
struct MsgpackEncoder {
buf: Vec<u8>,
}
impl MsgpackEncoder {
fn new() -> Self {
Self { buf: Vec::with_capacity(256) }
}
fn finish(self) -> Vec<u8> {
self.buf
}
fn write_nil(&mut self) {
self.buf.push(0xc0);
}
fn write_bool(&mut self, v: bool) {
self.buf.push(if v { 0xc3 } else { 0xc2 });
}
fn write_uint(&mut self, v: u64) {
if v <= 0x7f {
self.buf.push(v as u8);
} else if v <= 0xff {
self.buf.push(0xcc);
self.buf.push(v as u8);
} else if v <= 0xffff {
self.buf.push(0xcd);
self.buf.extend_from_slice(&(v as u16).to_be_bytes());
} else if v <= 0xffff_ffff {
self.buf.push(0xce);
self.buf.extend_from_slice(&(v as u32).to_be_bytes());
} else {
self.buf.push(0xcf);
self.buf.extend_from_slice(&v.to_be_bytes());
}
}
fn write_int(&mut self, v: i64) {
if v >= 0 {
self.write_uint(v as u64);
} else if v >= -32 {
self.buf.push(v as u8); } else if v >= -128 {
self.buf.push(0xd0);
self.buf.push(v as u8);
} else if v >= -32768 {
self.buf.push(0xd1);
self.buf.extend_from_slice(&(v as i16).to_be_bytes());
} else if v >= -2_147_483_648 {
self.buf.push(0xd2);
self.buf.extend_from_slice(&(v as i32).to_be_bytes());
} else {
self.buf.push(0xd3);
self.buf.extend_from_slice(&v.to_be_bytes());
}
}
fn write_str(&mut self, s: &str) {
let bytes = s.as_bytes();
let len = bytes.len();
if len <= 31 {
self.buf.push(0xa0 | len as u8);
} else if len <= 0xff {
self.buf.push(0xd9);
self.buf.push(len as u8);
} else if len <= 0xffff {
self.buf.push(0xda);
self.buf.extend_from_slice(&(len as u16).to_be_bytes());
} else {
self.buf.push(0xdb);
self.buf.extend_from_slice(&(len as u32).to_be_bytes());
}
self.buf.extend_from_slice(bytes);
}
fn write_bin(&mut self, data: &[u8]) {
let len = data.len();
if len <= 0xff {
self.buf.push(0xc4);
self.buf.push(len as u8);
} else if len <= 0xffff {
self.buf.push(0xc5);
self.buf.extend_from_slice(&(len as u16).to_be_bytes());
} else {
self.buf.push(0xc6);
self.buf.extend_from_slice(&(len as u32).to_be_bytes());
}
self.buf.extend_from_slice(data);
}
fn begin_map(&mut self, n: usize) {
if n <= 15 {
self.buf.push(0x80 | n as u8);
} else if n <= 0xffff {
self.buf.push(0xde);
self.buf.extend_from_slice(&(n as u16).to_be_bytes());
} else {
self.buf.push(0xdf);
self.buf.extend_from_slice(&(n as u32).to_be_bytes());
}
}
fn begin_array(&mut self, n: usize) {
if n <= 15 {
self.buf.push(0x90 | n as u8);
} else if n <= 0xffff {
self.buf.push(0xdc);
self.buf.extend_from_slice(&(n as u16).to_be_bytes());
} else {
self.buf.push(0xdd);
self.buf.extend_from_slice(&(n as u32).to_be_bytes());
}
}
}
#[derive(Debug, Clone)]
pub struct HlOrder {
pub a: u32,
pub b: bool,
pub p: String,
pub s: String,
pub r: bool,
pub t: HlOrderType,
pub c: Option<String>,
}
#[derive(Debug, Clone)]
pub enum HlOrderType {
Limit { tif: HlTif },
Trigger { trigger_px: String, is_market: bool, tpsl: String },
}
#[derive(Debug, Clone, Copy)]
pub enum HlTif {
Gtc,
Alo, Ioc,
Fok,
}
impl HlTif {
pub fn as_str(&self) -> &'static str {
match self {
Self::Gtc => "Gtc",
Self::Alo => "Alo",
Self::Ioc => "Ioc",
Self::Fok => "Fok",
}
}
}
pub fn msgpack_order_action(orders: &[HlOrder], grouping: &str) -> Vec<u8> {
let mut enc = MsgpackEncoder::new();
enc.begin_map(3);
enc.write_str("grouping");
enc.write_str(grouping);
enc.write_str("orders");
enc.begin_array(orders.len());
for order in orders {
msgpack_order(&mut enc, order);
}
enc.write_str("type");
enc.write_str("order");
enc.finish()
}
fn msgpack_order(enc: &mut MsgpackEncoder, order: &HlOrder) {
let n = if order.c.is_some() { 7 } else { 6 };
enc.begin_map(n);
enc.write_str("a");
enc.write_uint(order.a as u64);
enc.write_str("b");
enc.write_bool(order.b);
if let Some(ref cloid) = order.c {
enc.write_str("c");
let hex_str = cloid.trim_start_matches("0x");
if let Ok(bytes) = hex::decode(hex_str) {
enc.write_bin(&bytes);
} else {
enc.write_nil();
}
}
enc.write_str("p");
enc.write_str(&order.p);
enc.write_str("r");
enc.write_bool(order.r);
enc.write_str("s");
enc.write_str(&order.s);
enc.write_str("t");
msgpack_order_type(enc, &order.t);
}
fn msgpack_order_type(enc: &mut MsgpackEncoder, ot: &HlOrderType) {
match ot {
HlOrderType::Limit { tif } => {
enc.begin_map(1);
enc.write_str("limit");
enc.begin_map(1);
enc.write_str("tif");
enc.write_str(tif.as_str());
}
HlOrderType::Trigger { trigger_px, is_market, tpsl } => {
enc.begin_map(1);
enc.write_str("trigger");
enc.begin_map(3);
enc.write_str("isMarket");
enc.write_bool(*is_market);
enc.write_str("tpsl");
enc.write_str(tpsl);
enc.write_str("triggerPx");
enc.write_str(trigger_px);
}
}
}
pub fn msgpack_cancel_action(cancels: &[(u32, u64)]) -> Vec<u8> {
let mut enc = MsgpackEncoder::new();
enc.begin_map(2);
enc.write_str("cancels");
enc.begin_array(cancels.len());
for &(asset, oid) in cancels {
enc.begin_map(2);
enc.write_str("a");
enc.write_uint(asset as u64);
enc.write_str("o");
enc.write_uint(oid);
}
enc.write_str("type");
enc.write_str("cancel");
enc.finish()
}
pub fn msgpack_modify_action(oid: u64, order: &HlOrder) -> Vec<u8> {
let mut enc = MsgpackEncoder::new();
enc.begin_map(3);
enc.write_str("oid");
enc.write_uint(oid);
enc.write_str("order");
msgpack_order(&mut enc, order);
enc.write_str("type");
enc.write_str("modify");
enc.finish()
}
pub fn msgpack_batch_modify_action(modifies: &[(u64, &HlOrder)]) -> Vec<u8> {
let mut enc = MsgpackEncoder::new();
enc.begin_map(2);
enc.write_str("modifies");
enc.begin_array(modifies.len());
for (oid, order) in modifies {
enc.begin_map(2);
enc.write_str("oid");
enc.write_uint(*oid);
enc.write_str("order");
msgpack_order(&mut enc, order);
}
enc.write_str("type");
enc.write_str("batchModify");
enc.finish()
}
pub fn msgpack_update_leverage_action(asset: u32, is_cross: bool, leverage: u32) -> Vec<u8> {
let mut enc = MsgpackEncoder::new();
enc.begin_map(4);
enc.write_str("asset");
enc.write_uint(asset as u64);
enc.write_str("isCross");
enc.write_bool(is_cross);
enc.write_str("leverage");
enc.write_uint(leverage as u64);
enc.write_str("type");
enc.write_str("updateLeverage");
enc.finish()
}
pub fn msgpack_update_isolated_margin_action(asset: u32, is_buy: bool, ntli: i64) -> Vec<u8> {
let mut enc = MsgpackEncoder::new();
enc.begin_map(4);
enc.write_str("asset");
enc.write_uint(asset as u64);
enc.write_str("isBuy");
enc.write_bool(is_buy);
enc.write_str("ntli");
enc.write_int(ntli);
enc.write_str("type");
enc.write_str("updateIsolatedMargin");
enc.finish()
}
pub fn msgpack_usd_class_transfer_action(amount: &str, to_perp: bool) -> Vec<u8> {
let mut enc = MsgpackEncoder::new();
enc.begin_map(3);
enc.write_str("amount");
enc.write_str(amount);
enc.write_str("toPerp");
enc.write_bool(to_perp);
enc.write_str("type");
enc.write_str("usdClassTransfer");
enc.finish()
}
pub fn msgpack_twap_action(
asset: u32,
is_buy: bool,
size: &str,
reduce_only: bool,
duration_minutes: u64,
) -> Vec<u8> {
let mut enc = MsgpackEncoder::new();
enc.begin_map(2);
enc.write_str("twap");
enc.begin_map(5);
enc.write_str("a");
enc.write_uint(asset as u64);
enc.write_str("b");
enc.write_bool(is_buy);
enc.write_str("m");
enc.write_uint(duration_minutes);
enc.write_str("r");
enc.write_bool(reduce_only);
enc.write_str("s");
enc.write_str(size);
enc.write_str("type");
enc.write_str("twapOrder");
enc.finish()
}
fn keccak(data: &[u8]) -> [u8; 32] {
crypto_evm::keccak256(data)
}
fn compute_domain_separator(chain_id: u64) -> [u8; 32] {
let type_hash = keccak(
b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
);
let name_hash = keccak(b"Exchange");
let version_hash = keccak(b"1");
let mut chain_id_bytes = [0u8; 32];
chain_id_bytes[24..].copy_from_slice(&chain_id.to_be_bytes());
let verifying_contract = [0u8; 32];
let mut encoded = Vec::with_capacity(5 * 32);
encoded.extend_from_slice(&type_hash);
encoded.extend_from_slice(&name_hash);
encoded.extend_from_slice(&version_hash);
encoded.extend_from_slice(&chain_id_bytes);
encoded.extend_from_slice(&verifying_contract);
keccak(&encoded)
}
fn compute_connection_id(action_bytes: &[u8], nonce: u64, vault_address: Option<&[u8; 20]>) -> [u8; 32] {
let mut data = Vec::with_capacity(action_bytes.len() + 8 + 21);
data.extend_from_slice(action_bytes);
data.extend_from_slice(&nonce.to_be_bytes());
if let Some(vault) = vault_address {
data.push(1u8);
data.extend_from_slice(vault);
} else {
data.push(0u8);
}
keccak(&data)
}
fn compute_agent_hash(connection_id: &[u8; 32]) -> [u8; 32] {
let type_hash = keccak(b"Agent(address source,bytes32 connectionId)");
let phantom_source: [u8; 20] = [
0xa0, 0xb8, 0x69, 0x91, 0xc6, 0x21, 0x8b, 0x36, 0xc1, 0xd1,
0x9d, 0x4a, 0x2e, 0x9e, 0xb0, 0xce, 0x36, 0x06, 0xeb, 0x48,
];
let mut encoded = Vec::with_capacity(3 * 32);
encoded.extend_from_slice(&type_hash);
encoded.extend_from_slice(&[0u8; 12]);
encoded.extend_from_slice(&phantom_source);
encoded.extend_from_slice(connection_id);
keccak(&encoded)
}
fn compute_eip712_hash(domain_separator: &[u8; 32], struct_hash: &[u8; 32]) -> [u8; 32] {
let mut data = Vec::with_capacity(66);
data.extend_from_slice(b"\x19\x01");
data.extend_from_slice(domain_separator);
data.extend_from_slice(struct_hash);
keccak(&data)
}
#[derive(Debug, Clone)]
pub struct SignatureComponents {
pub r: String,
pub s: String,
pub v: u8,
}
#[derive(Clone)]
pub struct HyperliquidAuth {
wallet_address: String,
signer: Arc<EvmWallet>,
nonce_counter: Arc<AtomicU64>,
is_testnet: bool,
}
impl HyperliquidAuth {
pub fn new(credentials: &Credentials) -> ExchangeResult<Self> {
if credentials.api_secret.is_empty() {
return Err(ExchangeError::Auth(
"Hyperliquid requires wallet private key in api_secret field".to_string()
));
}
let wallet = EvmWallet::from_hex(&credentials.api_secret)
.map_err(|e| ExchangeError::Auth(format!("Invalid private key: {}", e)))?;
let wallet_address = if !credentials.api_key.is_empty() {
credentials.api_key.to_lowercase()
} else {
wallet.address_hex()
};
let nonce_counter = Arc::new(AtomicU64::new(timestamp_millis()));
Ok(Self {
wallet_address,
signer: Arc::new(wallet),
nonce_counter,
is_testnet: false,
})
}
pub fn new_with_network(credentials: &Credentials, is_testnet: bool) -> ExchangeResult<Self> {
let mut auth = Self::new(credentials)?;
auth.is_testnet = is_testnet;
Ok(auth)
}
pub fn wallet_address(&self) -> &str {
&self.wallet_address
}
pub fn get_next_nonce(&self) -> u64 {
let now = timestamp_millis();
self.nonce_counter.fetch_max(now, Ordering::SeqCst);
self.nonce_counter.fetch_add(1, Ordering::SeqCst)
}
fn chain_id(&self) -> u64 {
if self.is_testnet { 421614 } else { 42161 }
}
fn sign_hash(&self, hash: [u8; 32]) -> ExchangeResult<SignatureComponents> {
let sig_bytes = self.signer.sign_prehash_recoverable(&hash)
.map_err(|e| ExchangeError::Auth(format!("Signing failed: {}", e)))?;
let r = format!("0x{}", hex::encode(&sig_bytes[..32]));
let s = format!("0x{}", hex::encode(&sig_bytes[32..64]));
let v = sig_bytes[64];
Ok(SignatureComponents { r, s, v })
}
pub fn sign_l1_action(
&self,
action_bytes: &[u8],
nonce: u64,
vault_address: Option<&[u8; 20]>,
) -> ExchangeResult<SignatureComponents> {
let chain_id = self.chain_id();
let domain_separator = compute_domain_separator(chain_id);
let connection_id = compute_connection_id(action_bytes, nonce, vault_address);
let agent_hash = compute_agent_hash(&connection_id);
let final_hash = compute_eip712_hash(&domain_separator, &agent_hash);
self.sign_hash(final_hash)
}
pub fn sign_order_action(
&self,
orders: &[HlOrder],
grouping: &str,
vault_address: Option<&[u8; 20]>,
) -> ExchangeResult<serde_json::Value> {
let nonce = self.get_next_nonce();
let action_bytes = msgpack_order_action(orders, grouping);
let sig = self.sign_l1_action(&action_bytes, nonce, vault_address)?;
let action = build_order_action_json(orders, grouping);
Ok(build_exchange_request(action, nonce, sig, vault_address))
}
pub fn sign_cancel_action(
&self,
cancels: &[(u32, u64)],
vault_address: Option<&[u8; 20]>,
) -> ExchangeResult<serde_json::Value> {
let nonce = self.get_next_nonce();
let action_bytes = msgpack_cancel_action(cancels);
let sig = self.sign_l1_action(&action_bytes, nonce, vault_address)?;
let action = build_cancel_action_json(cancels);
Ok(build_exchange_request(action, nonce, sig, vault_address))
}
pub fn sign_batch_modify_action(
&self,
modifies: &[(u64, &HlOrder)],
vault_address: Option<&[u8; 20]>,
) -> ExchangeResult<serde_json::Value> {
let nonce = self.get_next_nonce();
let action_bytes = msgpack_batch_modify_action(modifies);
let sig = self.sign_l1_action(&action_bytes, nonce, vault_address)?;
let modifies_json: Vec<serde_json::Value> = modifies.iter().map(|(oid, order)| {
let order_type_json = match &order.t {
HlOrderType::Limit { tif } => serde_json::json!({
"limit": { "tif": tif.as_str() }
}),
HlOrderType::Trigger { trigger_px, is_market, tpsl } => serde_json::json!({
"trigger": {
"triggerPx": trigger_px,
"isMarket": is_market,
"tpsl": tpsl,
}
}),
};
let cloid_json = match &order.c {
Some(c) => serde_json::Value::String(c.clone()),
None => serde_json::Value::Null,
};
serde_json::json!({
"oid": oid,
"order": {
"a": order.a,
"b": order.b,
"p": order.p,
"s": order.s,
"r": order.r,
"t": order_type_json,
"c": cloid_json,
}
})
}).collect();
let action = serde_json::json!({
"type": "batchModify",
"modifies": modifies_json,
});
Ok(build_exchange_request(action, nonce, sig, vault_address))
}
pub fn sign_modify_action(
&self,
oid: u64,
order: &HlOrder,
vault_address: Option<&[u8; 20]>,
) -> ExchangeResult<serde_json::Value> {
let nonce = self.get_next_nonce();
let action_bytes = msgpack_modify_action(oid, order);
let sig = self.sign_l1_action(&action_bytes, nonce, vault_address)?;
let order_type_json = match &order.t {
HlOrderType::Limit { tif } => serde_json::json!({
"limit": { "tif": tif.as_str() }
}),
HlOrderType::Trigger { trigger_px, is_market, tpsl } => serde_json::json!({
"trigger": {
"triggerPx": trigger_px,
"isMarket": is_market,
"tpsl": tpsl,
}
}),
};
let cloid_json = match &order.c {
Some(c) => serde_json::Value::String(c.clone()),
None => serde_json::Value::Null,
};
let action = serde_json::json!({
"type": "modify",
"oid": oid,
"order": {
"a": order.a,
"b": order.b,
"p": order.p,
"s": order.s,
"r": order.r,
"t": order_type_json,
"c": cloid_json,
}
});
Ok(build_exchange_request(action, nonce, sig, vault_address))
}
pub fn sign_update_leverage(
&self,
asset: u32,
is_cross: bool,
leverage: u32,
vault_address: Option<&[u8; 20]>,
) -> ExchangeResult<serde_json::Value> {
let nonce = self.get_next_nonce();
let action_bytes = msgpack_update_leverage_action(asset, is_cross, leverage);
let sig = self.sign_l1_action(&action_bytes, nonce, vault_address)?;
let action = serde_json::json!({
"type": "updateLeverage",
"asset": asset,
"isCross": is_cross,
"leverage": leverage,
});
Ok(build_exchange_request(action, nonce, sig, vault_address))
}
pub fn sign_update_isolated_margin(
&self,
asset: u32,
is_buy: bool,
ntli: i64,
vault_address: Option<&[u8; 20]>,
) -> ExchangeResult<serde_json::Value> {
let nonce = self.get_next_nonce();
let action_bytes = msgpack_update_isolated_margin_action(asset, is_buy, ntli);
let sig = self.sign_l1_action(&action_bytes, nonce, vault_address)?;
let action = serde_json::json!({
"type": "updateIsolatedMargin",
"asset": asset,
"isBuy": is_buy,
"ntli": ntli,
});
Ok(build_exchange_request(action, nonce, sig, vault_address))
}
pub fn sign_twap_action(
&self,
asset: u32,
is_buy: bool,
size: &str,
reduce_only: bool,
duration_seconds: u64,
vault_address: Option<&[u8; 20]>,
) -> ExchangeResult<serde_json::Value> {
let nonce = self.get_next_nonce();
let duration_minutes = (duration_seconds / 60).max(5);
let action_bytes = msgpack_twap_action(asset, is_buy, size, reduce_only, duration_minutes);
let sig = self.sign_l1_action(&action_bytes, nonce, vault_address)?;
let action = serde_json::json!({
"type": "twapOrder",
"twap": {
"a": asset,
"b": is_buy,
"s": size,
"r": reduce_only,
"m": duration_minutes,
},
});
Ok(build_exchange_request(action, nonce, sig, vault_address))
}
pub fn sign_usd_class_transfer(
&self,
amount: &str,
to_perp: bool,
vault_address: Option<&[u8; 20]>,
) -> ExchangeResult<serde_json::Value> {
let nonce = self.get_next_nonce();
let action_bytes = msgpack_usd_class_transfer_action(amount, to_perp);
let sig = self.sign_l1_action(&action_bytes, nonce, vault_address)?;
let action = serde_json::json!({
"type": "usdClassTransfer",
"amount": amount,
"toPerp": to_perp,
});
Ok(build_exchange_request(action, nonce, sig, vault_address))
}
pub fn get_headers(&self) -> HashMap<String, String> {
let mut headers = HashMap::new();
headers.insert("Content-Type".to_string(), "application/json".to_string());
headers
}
}
fn build_order_action_json(orders: &[HlOrder], grouping: &str) -> serde_json::Value {
let order_jsons: Vec<serde_json::Value> = orders.iter().map(|o| {
let order_type_json = match &o.t {
HlOrderType::Limit { tif } => serde_json::json!({
"limit": { "tif": tif.as_str() }
}),
HlOrderType::Trigger { trigger_px, is_market, tpsl } => serde_json::json!({
"trigger": {
"triggerPx": trigger_px,
"isMarket": is_market,
"tpsl": tpsl,
}
}),
};
let cloid_json = match &o.c {
Some(c) => serde_json::Value::String(c.clone()),
None => serde_json::Value::Null,
};
serde_json::json!({
"a": o.a,
"b": o.b,
"p": o.p,
"s": o.s,
"r": o.r,
"t": order_type_json,
"c": cloid_json,
})
}).collect();
serde_json::json!({
"type": "order",
"orders": order_jsons,
"grouping": grouping,
})
}
fn build_cancel_action_json(cancels: &[(u32, u64)]) -> serde_json::Value {
let cancels_json: Vec<serde_json::Value> = cancels.iter()
.map(|&(a, o)| serde_json::json!({ "a": a, "o": o }))
.collect();
serde_json::json!({
"type": "cancel",
"cancels": cancels_json,
})
}
fn build_exchange_request(
action: serde_json::Value,
nonce: u64,
sig: SignatureComponents,
vault_address: Option<&[u8; 20]>,
) -> serde_json::Value {
let vault_json = vault_address
.map(|v| serde_json::Value::String(format!("0x{}", hex::encode(v))))
.unwrap_or(serde_json::Value::Null);
serde_json::json!({
"action": action,
"nonce": nonce,
"signature": {
"r": sig.r,
"s": sig.s,
"v": sig.v,
},
"vaultAddress": vault_json,
})
}
pub fn normalize_price(price: f64) -> String {
let s = format!("{:.8}", price);
let s = s.trim_end_matches('0');
if s.ends_with('.') {
format!("{}0", s)
} else {
s.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalize_price() {
assert_eq!(normalize_price(50000.0), "50000.0");
assert_eq!(normalize_price(50000.5), "50000.5");
assert_eq!(normalize_price(0.1), "0.1");
assert_eq!(normalize_price(123.456), "123.456");
}
#[test]
fn test_msgpack_bool() {
let mut enc = MsgpackEncoder::new();
enc.write_bool(true);
enc.write_bool(false);
let buf = enc.finish();
assert_eq!(buf, vec![0xc3, 0xc2]);
}
#[test]
fn test_msgpack_str() {
let mut enc = MsgpackEncoder::new();
enc.write_str("hello");
let buf = enc.finish();
assert_eq!(buf[0], 0xa5); assert_eq!(&buf[1..], b"hello");
}
#[test]
fn test_msgpack_uint() {
let mut enc = MsgpackEncoder::new();
enc.write_uint(0);
enc.write_uint(127);
enc.write_uint(128);
let buf = enc.finish();
assert_eq!(buf[0], 0);
assert_eq!(buf[1], 127);
assert_eq!(buf[2], 0xcc);
assert_eq!(buf[3], 128);
}
#[test]
fn test_keccak_empty() {
let result = keccak(b"");
let expected = hex::decode(
"c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"
).unwrap();
assert_eq!(result.as_slice(), expected.as_slice());
}
}