use super::{HyperLiquid, error};
use ccxt_core::{Error, Result};
use serde_json::{Map, Value};
pub struct HyperliquidSignedRequestBuilder<'a> {
hyperliquid: &'a HyperLiquid,
action: Value,
nonce: Option<u64>,
}
impl<'a> HyperliquidSignedRequestBuilder<'a> {
pub fn new(hyperliquid: &'a HyperLiquid, action: Value) -> Self {
Self {
hyperliquid,
action,
nonce: None,
}
}
pub fn nonce(mut self, nonce: u64) -> Self {
self.nonce = Some(nonce);
self
}
pub async fn execute(self) -> Result<Value> {
let auth = self
.hyperliquid
.auth()
.ok_or_else(|| Error::authentication("Private key required for exchange actions"))?;
let nonce = self.nonce.unwrap_or_else(|| get_current_nonce());
let is_mainnet = !self.hyperliquid.options().testnet;
let signature = auth.sign_l1_action(&self.action, nonce, is_mainnet)?;
let mut signature_map = Map::new();
signature_map.insert("r".to_string(), Value::String(format!("0x{}", signature.r)));
signature_map.insert("s".to_string(), Value::String(format!("0x{}", signature.s)));
signature_map.insert("v".to_string(), Value::Number(signature.v.into()));
let mut body_map = Map::new();
body_map.insert("action".to_string(), self.action);
body_map.insert("nonce".to_string(), Value::Number(nonce.into()));
body_map.insert("signature".to_string(), Value::Object(signature_map));
if let Some(vault_address) = &self.hyperliquid.options().vault_address {
body_map.insert(
"vaultAddress".to_string(),
Value::String(format!("0x{}", hex::encode(vault_address))),
);
}
let body = Value::Object(body_map);
let urls = self.hyperliquid.urls();
let url = format!("{}/exchange", urls.rest);
tracing::debug!("HyperLiquid signed action request: {:?}", body);
let response = self
.hyperliquid
.base()
.http_client
.post(&url, None, Some(body))
.await?;
if error::is_error_response(&response) {
return Err(error::parse_error(&response));
}
Ok(response)
}
}
fn get_current_nonce() -> u64 {
chrono::Utc::now().timestamp_millis() as u64
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
const TEST_PRIVATE_KEY: &str =
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
#[test]
fn test_builder_construction() {
let hyperliquid = HyperLiquid::builder().testnet(true).build().unwrap();
let action = json!({"type": "order", "orders": [], "grouping": "na"});
let builder = HyperliquidSignedRequestBuilder::new(&hyperliquid, action.clone());
assert_eq!(builder.action, action);
assert!(builder.nonce.is_none());
}
#[test]
fn test_builder_with_nonce() {
let hyperliquid = HyperLiquid::builder().testnet(true).build().unwrap();
let action = json!({"type": "order", "orders": [], "grouping": "na"});
let builder =
HyperliquidSignedRequestBuilder::new(&hyperliquid, action).nonce(1234567890000);
assert_eq!(builder.nonce, Some(1234567890000));
}
#[test]
fn test_builder_method_chaining() {
let hyperliquid = HyperLiquid::builder().testnet(true).build().unwrap();
let action = json!({"type": "cancel", "cancels": []});
let builder =
HyperliquidSignedRequestBuilder::new(&hyperliquid, action.clone()).nonce(9999999999999);
assert_eq!(builder.action, action);
assert_eq!(builder.nonce, Some(9999999999999));
}
#[test]
fn test_get_current_nonce() {
let nonce1 = get_current_nonce();
std::thread::sleep(std::time::Duration::from_millis(10));
let nonce2 = get_current_nonce();
assert!(nonce2 > nonce1);
assert!(nonce1 > 1577836800000); }
#[test]
fn test_builder_with_authenticated_exchange() {
let hyperliquid = HyperLiquid::builder()
.private_key(TEST_PRIVATE_KEY)
.testnet(true)
.build()
.unwrap();
let action = json!({"type": "order", "orders": [], "grouping": "na"});
let builder = HyperliquidSignedRequestBuilder::new(&hyperliquid, action);
assert!(builder.hyperliquid.auth().is_some());
}
#[test]
fn test_builder_without_authentication() {
let hyperliquid = HyperLiquid::builder().testnet(true).build().unwrap();
let action = json!({"type": "order", "orders": [], "grouping": "na"});
let builder = HyperliquidSignedRequestBuilder::new(&hyperliquid, action);
assert!(builder.hyperliquid.auth().is_none());
}
#[tokio::test]
async fn test_execute_without_credentials_returns_error() {
let hyperliquid = HyperLiquid::builder().testnet(true).build().unwrap();
let action = json!({"type": "order", "orders": [], "grouping": "na"});
let result = hyperliquid.signed_action(action).execute().await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("Private key required"));
}
#[test]
fn test_different_action_types() {
let hyperliquid = HyperLiquid::builder().testnet(true).build().unwrap();
let order_action = json!({
"type": "order",
"orders": [{"a": 0, "b": true, "p": "50000", "s": "0.001", "r": false, "t": {"limit": {"tif": "Gtc"}}}],
"grouping": "na"
});
let builder = HyperliquidSignedRequestBuilder::new(&hyperliquid, order_action.clone());
assert_eq!(builder.action["type"], "order");
let cancel_action = json!({
"type": "cancel",
"cancels": [{"a": 0, "o": 12345}]
});
let builder = HyperliquidSignedRequestBuilder::new(&hyperliquid, cancel_action.clone());
assert_eq!(builder.action["type"], "cancel");
let leverage_action = json!({
"type": "updateLeverage",
"asset": 0,
"isCross": true,
"leverage": 10
});
let builder = HyperliquidSignedRequestBuilder::new(&hyperliquid, leverage_action.clone());
assert_eq!(builder.action["type"], "updateLeverage");
}
}