#![allow(clippy::disallowed_methods)]
use proptest::prelude::*;
mod private_key_derivation {
use super::*;
use ccxt_exchanges::hyperliquid::HyperLiquidAuth;
fn valid_private_key_strategy() -> impl Strategy<Value = String> {
prop::collection::vec(any::<u8>(), 32..=32)
.prop_map(|bytes| format!("0x{}", hex::encode(bytes)))
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_valid_key_produces_valid_address(key in valid_private_key_strategy()) {
if let Ok(auth) = HyperLiquidAuth::from_private_key(&key) {
let address = auth.wallet_address();
prop_assert!(
address.starts_with("0x"),
"Address should start with '0x', got: {}",
address
);
prop_assert_eq!(
address.len(),
42,
"Address should be 42 characters, got: {}",
address.len()
);
let hex_part = &address[2..];
prop_assert!(
hex_part.chars().all(|c| c.is_ascii_hexdigit()),
"Address should only contain hex characters, got: {}",
address
);
}
}
#[test]
fn prop_address_derivation_deterministic(key in valid_private_key_strategy()) {
if let Ok(auth1) = HyperLiquidAuth::from_private_key(&key) {
let auth2 = HyperLiquidAuth::from_private_key(&key)
.expect("Second derivation should succeed if first did");
prop_assert_eq!(
auth1.wallet_address(),
auth2.wallet_address(),
"Same private key should always produce same address"
);
}
}
#[test]
fn prop_key_with_and_without_prefix_same(key in valid_private_key_strategy()) {
let key_without_prefix = key.strip_prefix("0x").unwrap_or(&key);
if let Ok(auth_with) = HyperLiquidAuth::from_private_key(&key) {
let auth_without = HyperLiquidAuth::from_private_key(key_without_prefix)
.expect("Key without prefix should work if key with prefix works");
prop_assert_eq!(
auth_with.wallet_address(),
auth_without.wallet_address(),
"Keys with and without 0x prefix should produce same address"
);
}
}
}
}
mod invalid_private_key_rejection {
use super::*;
use ccxt_exchanges::hyperliquid::HyperLiquidAuth;
fn wrong_length_key_strategy() -> impl Strategy<Value = String> {
prop_oneof![
prop::collection::vec(any::<u8>(), 1..32)
.prop_map(|bytes| format!("0x{}", hex::encode(bytes))),
prop::collection::vec(any::<u8>(), 33..65)
.prop_map(|bytes| format!("0x{}", hex::encode(bytes))),
Just("0x".to_string()),
Just("".to_string()),
]
}
fn invalid_hex_strategy() -> impl Strategy<Value = String> {
prop_oneof![
Just("0xGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG".to_string()),
Just("0x123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdefZ".to_string()),
Just("0x 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcde".to_string()),
Just("0x!@#$%^&*()1234567890abcdef1234567890abcdef1234567890abcdef12345".to_string()),
]
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_wrong_length_rejected(key in wrong_length_key_strategy()) {
let result = HyperLiquidAuth::from_private_key(&key);
prop_assert!(
result.is_err(),
"Wrong length key should be rejected: {}",
key
);
}
#[test]
fn prop_invalid_hex_rejected(key in invalid_hex_strategy()) {
let result = HyperLiquidAuth::from_private_key(&key);
prop_assert!(
result.is_err(),
"Invalid hex format should be rejected: {}",
key
);
}
#[test]
fn prop_short_strings_rejected(len in 0usize..10usize) {
let key = "a".repeat(len);
let result = HyperLiquidAuth::from_private_key(&key);
prop_assert!(
result.is_err(),
"Short string should be rejected: {}",
key
);
}
}
}
mod eip712_signature_determinism {
use super::*;
use ccxt_exchanges::hyperliquid::HyperLiquidAuth;
use serde_json::json;
const TEST_PRIVATE_KEY: &str =
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
fn nonce_strategy() -> impl Strategy<Value = u64> {
1577836800000u64..1893456000000u64 }
fn action_type_strategy() -> impl Strategy<Value = String> {
prop_oneof![
Just("order".to_string()),
Just("cancel".to_string()),
Just("cancelByCloid".to_string()),
Just("modifyOrder".to_string()),
Just("updateLeverage".to_string()),
]
}
fn action_payload_strategy() -> impl Strategy<Value = serde_json::Value> {
(action_type_strategy(), 0u32..100u32, any::<bool>()).prop_map(
|(action_type, asset, is_buy)| {
json!({
"type": action_type,
"asset": asset,
"isBuy": is_buy
})
},
)
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_signature_deterministic(
action in action_payload_strategy(),
nonce in nonce_strategy(),
is_mainnet in any::<bool>()
) {
let auth = HyperLiquidAuth::from_private_key(TEST_PRIVATE_KEY)
.expect("Failed to create auth");
let sig1 = auth.sign_l1_action(&action, nonce, is_mainnet)
.expect("First signing should succeed");
let sig2 = auth.sign_l1_action(&action, nonce, is_mainnet)
.expect("Second signing should succeed");
prop_assert_eq!(sig1.r, sig2.r, "R component should be deterministic");
prop_assert_eq!(sig1.s, sig2.s, "S component should be deterministic");
prop_assert_eq!(sig1.v, sig2.v, "V component should be deterministic");
}
#[test]
fn prop_signature_format_valid(
action in action_payload_strategy(),
nonce in nonce_strategy(),
is_mainnet in any::<bool>()
) {
let auth = HyperLiquidAuth::from_private_key(TEST_PRIVATE_KEY)
.expect("Failed to create auth");
let sig = auth.sign_l1_action(&action, nonce, is_mainnet)
.expect("Signing should succeed");
prop_assert_eq!(
sig.r.len(),
64,
"R component should be 64 hex chars, got: {}",
sig.r.len()
);
prop_assert_eq!(
sig.s.len(),
64,
"S component should be 64 hex chars, got: {}",
sig.s.len()
);
prop_assert!(
sig.r.chars().all(|c| c.is_ascii_hexdigit()),
"R should be valid hex"
);
prop_assert!(
sig.s.chars().all(|c| c.is_ascii_hexdigit()),
"S should be valid hex"
);
prop_assert!(
sig.v == 27 || sig.v == 28,
"V should be 27 or 28, got: {}",
sig.v
);
}
#[test]
fn prop_different_nonce_different_signature(
action in action_payload_strategy(),
nonce1 in nonce_strategy(),
nonce2 in nonce_strategy(),
is_mainnet in any::<bool>()
) {
prop_assume!(nonce1 != nonce2);
let auth = HyperLiquidAuth::from_private_key(TEST_PRIVATE_KEY)
.expect("Failed to create auth");
let sig1 = auth.sign_l1_action(&action, nonce1, is_mainnet)
.expect("First signing should succeed");
let sig2 = auth.sign_l1_action(&action, nonce2, is_mainnet)
.expect("Second signing should succeed");
prop_assert!(
sig1.r != sig2.r || sig1.s != sig2.s,
"Different nonces should produce different signatures"
);
}
#[test]
fn prop_signature_to_hex_valid(
action in action_payload_strategy(),
nonce in nonce_strategy(),
is_mainnet in any::<bool>()
) {
let auth = HyperLiquidAuth::from_private_key(TEST_PRIVATE_KEY)
.expect("Failed to create auth");
let sig = auth.sign_l1_action(&action, nonce, is_mainnet)
.expect("Signing should succeed");
let hex = sig.to_hex();
prop_assert!(hex.starts_with("0x"), "Hex should start with 0x");
prop_assert_eq!(
hex.len(),
132,
"Hex signature should be 132 chars, got: {}",
hex.len()
);
}
}
}
mod market_symbol_format {
use super::*;
use ccxt_exchanges::hyperliquid::parser::parse_market;
use serde_json::json;
fn asset_name_strategy() -> impl Strategy<Value = String> {
"[A-Z]{2,10}"
}
fn sz_decimals_strategy() -> impl Strategy<Value = u64> {
0u64..9u64
}
fn market_index_strategy() -> impl Strategy<Value = usize> {
0usize..1000usize
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_market_symbol_format(
asset_name in asset_name_strategy(),
sz_decimals in sz_decimals_strategy(),
index in market_index_strategy()
) {
let data = json!({
"name": asset_name,
"szDecimals": sz_decimals
});
let market = parse_market(&data, index)
.expect("Market parsing should succeed");
let expected_symbol = format!("{}/USDC:USDC", asset_name);
prop_assert_eq!(
market.symbol,
expected_symbol,
"Symbol should match pattern BASE/USDC:USDC"
);
prop_assert_eq!(
market.base,
asset_name,
"Base should be the asset name"
);
prop_assert_eq!(
market.quote,
"USDC",
"Quote should be USDC"
);
prop_assert_eq!(
market.settle,
Some("USDC".to_string()),
"Settle should be USDC"
);
}
#[test]
fn prop_market_is_perpetual(
asset_name in asset_name_strategy(),
sz_decimals in sz_decimals_strategy(),
index in market_index_strategy()
) {
let data = json!({
"name": asset_name,
"szDecimals": sz_decimals
});
let market = parse_market(&data, index)
.expect("Market parsing should succeed");
prop_assert!(market.active, "Market should be active");
prop_assert!(market.margin, "Market should support margin");
prop_assert_eq!(market.contract, Some(true), "Market should be a contract");
prop_assert_eq!(market.linear, Some(true), "Market should be linear");
prop_assert_eq!(market.inverse, Some(false), "Market should not be inverse");
}
#[test]
fn prop_market_id_is_index(
asset_name in asset_name_strategy(),
sz_decimals in sz_decimals_strategy(),
index in market_index_strategy()
) {
let data = json!({
"name": asset_name,
"szDecimals": sz_decimals
});
let market = parse_market(&data, index)
.expect("Market parsing should succeed");
prop_assert_eq!(
market.id,
index.to_string(),
"Market ID should be the index as string"
);
}
}
}
mod ticker_data_completeness {
use super::*;
use ccxt_exchanges::hyperliquid::parser::parse_ticker;
use rust_decimal::Decimal;
use rust_decimal::prelude::FromPrimitive;
fn mid_price_strategy() -> impl Strategy<Value = Decimal> {
(1u64..1000000u64).prop_map(|n| Decimal::from_u64(n).unwrap())
}
fn symbol_strategy() -> impl Strategy<Value = String> {
"[A-Z]{2,10}".prop_map(|s| format!("{}/USDC:USDC", s))
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_ticker_has_last_price(
symbol in symbol_strategy(),
mid_price in mid_price_strategy()
) {
let ticker = parse_ticker(&symbol, mid_price, None)
.expect("Ticker parsing should succeed");
prop_assert!(
ticker.last.is_some(),
"Ticker should have last price"
);
let last = ticker.last.unwrap();
prop_assert!(
last.0 > Decimal::ZERO,
"Last price should be positive"
);
}
#[test]
fn prop_ticker_has_valid_timestamp(
symbol in symbol_strategy(),
mid_price in mid_price_strategy()
) {
let ticker = parse_ticker(&symbol, mid_price, None)
.expect("Ticker parsing should succeed");
prop_assert!(
ticker.timestamp >= 1577836800000,
"Timestamp should be after 2020"
);
prop_assert!(
ticker.timestamp <= 1893456000000,
"Timestamp should be before 2030"
);
}
#[test]
fn prop_ticker_symbol_matches(
symbol in symbol_strategy(),
mid_price in mid_price_strategy()
) {
let ticker = parse_ticker(&symbol, mid_price, None)
.expect("Ticker parsing should succeed");
prop_assert_eq!(
ticker.symbol,
symbol,
"Ticker symbol should match input"
);
}
}
}
mod orderbook_invariants {
use super::*;
use ccxt_exchanges::hyperliquid::parser::parse_orderbook;
use rust_decimal::Decimal;
use serde_json::json;
fn price_strategy() -> impl Strategy<Value = String> {
(1u64..100000u64).prop_map(|n| format!("{}.{}", n / 100, n % 100))
}
fn size_strategy() -> impl Strategy<Value = String> {
(1u64..10000u64).prop_map(|n| format!("{}.{}", n / 1000, n % 1000))
}
fn levels_strategy() -> impl Strategy<Value = Vec<(String, String)>> {
prop::collection::vec((price_strategy(), size_strategy()), 1..20)
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_bids_sorted_descending(bid_levels in levels_strategy()) {
let bids_json: Vec<serde_json::Value> = bid_levels
.iter()
.map(|(px, sz)| json!({"px": px, "sz": sz}))
.collect();
let data = json!({
"levels": [bids_json, []]
});
let orderbook = parse_orderbook(&data, "BTC/USDC:USDC".to_string())
.expect("Orderbook parsing should succeed");
for i in 1..orderbook.bids.len() {
prop_assert!(
orderbook.bids[i - 1].price >= orderbook.bids[i].price,
"Bids should be sorted descending: {:?} >= {:?}",
orderbook.bids[i - 1].price,
orderbook.bids[i].price
);
}
}
#[test]
fn prop_asks_sorted_ascending(ask_levels in levels_strategy()) {
let asks_json: Vec<serde_json::Value> = ask_levels
.iter()
.map(|(px, sz)| json!({"px": px, "sz": sz}))
.collect();
let data = json!({
"levels": [[], asks_json]
});
let orderbook = parse_orderbook(&data, "BTC/USDC:USDC".to_string())
.expect("Orderbook parsing should succeed");
for i in 1..orderbook.asks.len() {
prop_assert!(
orderbook.asks[i - 1].price <= orderbook.asks[i].price,
"Asks should be sorted ascending: {:?} <= {:?}",
orderbook.asks[i - 1].price,
orderbook.asks[i].price
);
}
}
#[test]
fn prop_all_entries_positive(
bid_levels in levels_strategy(),
ask_levels in levels_strategy()
) {
let bids_json: Vec<serde_json::Value> = bid_levels
.iter()
.map(|(px, sz)| json!({"px": px, "sz": sz}))
.collect();
let asks_json: Vec<serde_json::Value> = ask_levels
.iter()
.map(|(px, sz)| json!({"px": px, "sz": sz}))
.collect();
let data = json!({
"levels": [bids_json, asks_json]
});
let orderbook = parse_orderbook(&data, "BTC/USDC:USDC".to_string())
.expect("Orderbook parsing should succeed");
for bid in &orderbook.bids {
prop_assert!(
bid.price.0 > Decimal::ZERO,
"Bid price should be positive"
);
prop_assert!(
bid.amount.0 > Decimal::ZERO,
"Bid amount should be positive"
);
}
for ask in &orderbook.asks {
prop_assert!(
ask.price.0 > Decimal::ZERO,
"Ask price should be positive"
);
prop_assert!(
ask.amount.0 > Decimal::ZERO,
"Ask amount should be positive"
);
}
}
}
}
mod trade_data_completeness {
use super::*;
use ccxt_core::types::OrderSide;
use ccxt_exchanges::hyperliquid::parser::parse_trade;
use rust_decimal::Decimal;
use serde_json::json;
fn timestamp_strategy() -> impl Strategy<Value = i64> {
1577836800000i64..1893456000000i64
}
fn price_strategy() -> impl Strategy<Value = String> {
(1u64..100000u64).prop_map(|n| n.to_string())
}
fn size_strategy() -> impl Strategy<Value = String> {
(1u64..10000u64).prop_map(|n| format!("{}.{}", n / 1000, n % 1000))
}
fn side_strategy() -> impl Strategy<Value = &'static str> {
prop_oneof![Just("B"), Just("A"), Just("buy"), Just("sell"),]
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_trade_has_valid_timestamp(
timestamp in timestamp_strategy(),
price in price_strategy(),
size in size_strategy(),
side in side_strategy()
) {
let data = json!({
"time": timestamp,
"px": price,
"sz": size,
"side": side,
"tid": "123"
});
let trade = parse_trade(&data, None)
.expect("Trade parsing should succeed");
prop_assert_eq!(
trade.timestamp,
timestamp,
"Trade timestamp should match input"
);
}
#[test]
fn prop_trade_has_positive_values(
timestamp in timestamp_strategy(),
price in price_strategy(),
size in size_strategy(),
side in side_strategy()
) {
let data = json!({
"time": timestamp,
"px": price,
"sz": size,
"side": side,
"tid": "123"
});
let trade = parse_trade(&data, None)
.expect("Trade parsing should succeed");
prop_assert!(
trade.price.0 > Decimal::ZERO,
"Trade price should be positive"
);
prop_assert!(
trade.amount.0 > Decimal::ZERO,
"Trade amount should be positive"
);
}
#[test]
fn prop_trade_side_valid(
timestamp in timestamp_strategy(),
price in price_strategy(),
size in size_strategy(),
side in side_strategy()
) {
let data = json!({
"time": timestamp,
"px": price,
"sz": size,
"side": side,
"tid": "123"
});
let trade = parse_trade(&data, None)
.expect("Trade parsing should succeed");
prop_assert!(
trade.side == OrderSide::Buy || trade.side == OrderSide::Sell,
"Trade side should be Buy or Sell"
);
match side {
"B" | "buy" | "Buy" => prop_assert_eq!(trade.side, OrderSide::Buy),
"A" | "sell" | "Sell" => prop_assert_eq!(trade.side, OrderSide::Sell),
_ => {}
}
}
}
}
mod ohlcv_data_validity {
use super::*;
use ccxt_exchanges::hyperliquid::parser::parse_ohlcv;
use rust_decimal::Decimal;
use serde_json::json;
fn timestamp_strategy() -> impl Strategy<Value = i64> {
1577836800000i64..1893456000000i64
}
fn valid_ohlcv_prices_strategy() -> impl Strategy<Value = (String, String, String, String)> {
(
1u64..100000u64,
1u64..100000u64,
1u64..100000u64,
1u64..100000u64,
)
.prop_map(|(a, b, c, d)| {
let mut prices = [a, b, c, d];
prices.sort();
let low = prices[0];
let high = prices[3];
let open = prices[1];
let close = prices[2];
(
open.to_string(),
high.to_string(),
low.to_string(),
close.to_string(),
)
})
}
fn volume_strategy() -> impl Strategy<Value = String> {
(0u64..1000000u64).prop_map(|v| v.to_string())
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_ohlcv_high_valid(
timestamp in timestamp_strategy(),
(open, high, low, close) in valid_ohlcv_prices_strategy(),
volume in volume_strategy()
) {
let data = json!([timestamp, open, high, low, close, volume]);
let ohlcv = parse_ohlcv(&data)
.expect("OHLCV parsing should succeed");
let max_oc = std::cmp::max(ohlcv.open.0, ohlcv.close.0);
prop_assert!(
ohlcv.high.0 >= max_oc,
"High ({}) should be >= max(open, close) ({})",
ohlcv.high.0,
max_oc
);
}
#[test]
fn prop_ohlcv_low_valid(
timestamp in timestamp_strategy(),
(open, high, low, close) in valid_ohlcv_prices_strategy(),
volume in volume_strategy()
) {
let data = json!([timestamp, open, high, low, close, volume]);
let ohlcv = parse_ohlcv(&data)
.expect("OHLCV parsing should succeed");
let min_oc = std::cmp::min(ohlcv.open.0, ohlcv.close.0);
prop_assert!(
ohlcv.low.0 <= min_oc,
"Low ({}) should be <= min(open, close) ({})",
ohlcv.low.0,
min_oc
);
}
#[test]
fn prop_ohlcv_volume_non_negative(
timestamp in timestamp_strategy(),
(open, high, low, close) in valid_ohlcv_prices_strategy(),
volume in volume_strategy()
) {
let data = json!([timestamp, open, high, low, close, volume]);
let ohlcv = parse_ohlcv(&data)
.expect("OHLCV parsing should succeed");
prop_assert!(
ohlcv.volume.0 >= Decimal::ZERO,
"Volume should be non-negative"
);
}
#[test]
fn prop_ohlcv_timestamp_preserved(
timestamp in timestamp_strategy(),
(open, high, low, close) in valid_ohlcv_prices_strategy(),
volume in volume_strategy()
) {
let data = json!([timestamp, open, high, low, close, volume]);
let ohlcv = parse_ohlcv(&data)
.expect("OHLCV parsing should succeed");
prop_assert_eq!(
ohlcv.timestamp,
timestamp,
"Timestamp should be preserved"
);
}
}
}
mod balance_parsing_accuracy {
use super::*;
use ccxt_exchanges::hyperliquid::parser::parse_balance;
use rust_decimal::Decimal;
use serde_json::json;
#[allow(dead_code)]
fn account_value_strategy() -> impl Strategy<Value = String> {
(0u64..1000000u64).prop_map(|v| v.to_string())
}
fn margin_used_strategy() -> impl Strategy<Value = (String, String)> {
(0u64..1000000u64, 0u64..100u64).prop_map(|(account, pct)| {
let margin = account * pct / 100;
(account.to_string(), margin.to_string())
})
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_balance_total_equals_free_plus_used(
(account_value, margin_used) in margin_used_strategy()
) {
let data = json!({
"marginSummary": {
"accountValue": account_value,
"totalMarginUsed": margin_used
}
});
let balance = parse_balance(&data)
.expect("Balance parsing should succeed");
if let Some(usdc) = balance.get("USDC") {
prop_assert_eq!(
usdc.total,
usdc.free + usdc.used,
"Total should equal free + used"
);
}
}
#[test]
fn prop_balance_values_non_negative(
(account_value, margin_used) in margin_used_strategy()
) {
let data = json!({
"marginSummary": {
"accountValue": account_value,
"totalMarginUsed": margin_used
}
});
let balance = parse_balance(&data)
.expect("Balance parsing should succeed");
if let Some(usdc) = balance.get("USDC") {
prop_assert!(
usdc.total >= Decimal::ZERO,
"Total should be non-negative"
);
prop_assert!(
usdc.used >= Decimal::ZERO,
"Used should be non-negative"
);
}
}
}
}
mod error_response_parsing {
use super::*;
use ccxt_exchanges::hyperliquid::error::{is_error_response, parse_error};
use serde_json::json;
fn error_message_strategy() -> impl Strategy<Value = String> {
prop_oneof![
Just("Invalid signature".to_string()),
Just("Insufficient margin".to_string()),
Just("Rate limit exceeded".to_string()),
Just("Order not found".to_string()),
Just("Invalid parameter".to_string()),
"[a-zA-Z0-9 ]{5,50}".prop_map(|s| s.to_string()),
]
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_error_response_identified(msg in error_message_strategy()) {
let response = json!({
"error": msg
});
prop_assert!(
is_error_response(&response),
"Response with 'error' field should be identified as error"
);
}
#[test]
fn prop_status_err_identified(msg in error_message_strategy()) {
let response = json!({
"status": "err",
"response": msg
});
prop_assert!(
is_error_response(&response),
"Response with status 'err' should be identified as error"
);
}
#[test]
fn prop_success_not_error(_dummy in 0u32..100u32) {
let response = json!({
"status": "ok",
"response": {"data": []}
});
prop_assert!(
!is_error_response(&response),
"Success response should not be identified as error"
);
}
#[test]
fn prop_parse_error_produces_valid_error(msg in error_message_strategy()) {
let response = json!({
"error": msg
});
let error = parse_error(&response);
let display = error.to_string();
prop_assert!(
!display.is_empty(),
"Error display should not be empty"
);
}
#[test]
fn prop_insufficient_margin_mapped(_dummy in 0u32..100u32) {
let response = json!({
"error": "Insufficient margin for order"
});
let error = parse_error(&response);
let display = error.to_string().to_lowercase();
prop_assert!(
display.contains("insufficient") || display.contains("balance"),
"Insufficient margin error should be correctly mapped"
);
}
}
}
mod order_parsing_correctness {
use super::*;
use ccxt_core::types::OrderStatus;
use ccxt_exchanges::hyperliquid::parser::{parse_order, parse_order_status};
use serde_json::json;
fn status_strategy() -> impl Strategy<Value = &'static str> {
prop_oneof![
Just("open"),
Just("resting"),
Just("filled"),
Just("canceled"),
Just("cancelled"),
Just("rejected"),
]
}
fn order_id_strategy() -> impl Strategy<Value = u64> {
1u64..1000000u64
}
fn price_strategy() -> impl Strategy<Value = String> {
(1u64..100000u64).prop_map(|n| n.to_string())
}
fn size_strategy() -> impl Strategy<Value = String> {
(1u64..10000u64).prop_map(|n| format!("{}.{}", n / 1000, n % 1000))
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_order_status_mapping(status in status_strategy()) {
let parsed = parse_order_status(status);
match status {
"open" | "resting" => prop_assert_eq!(parsed, OrderStatus::Open),
"filled" => prop_assert_eq!(parsed, OrderStatus::Closed),
"canceled" | "cancelled" => prop_assert_eq!(parsed, OrderStatus::Cancelled),
"rejected" => prop_assert_eq!(parsed, OrderStatus::Rejected),
_ => {}
}
}
#[test]
fn prop_order_id_parsed(
oid in order_id_strategy(),
price in price_strategy(),
size in size_strategy()
) {
let data = json!({
"oid": oid,
"coin": "BTC",
"limitPx": price,
"sz": size,
"side": "B",
"status": "open"
});
let order = parse_order(&data, None)
.expect("Order parsing should succeed");
prop_assert_eq!(
order.id,
oid.to_string(),
"Order ID should be correctly parsed"
);
}
#[test]
fn prop_order_symbol_formatted(
oid in order_id_strategy(),
price in price_strategy(),
size in size_strategy()
) {
let data = json!({
"oid": oid,
"coin": "BTC",
"limitPx": price,
"sz": size,
"side": "B",
"status": "open"
});
let order = parse_order(&data, None)
.expect("Order parsing should succeed");
prop_assert!(
order.symbol.ends_with("/USDC:USDC"),
"Order symbol should end with /USDC:USDC"
);
}
}
}
mod parser_robustness {
use super::*;
use ccxt_exchanges::hyperliquid::parser::{parse_balance, parse_market, parse_ticker};
use rust_decimal::Decimal;
use rust_decimal::prelude::FromPrimitive;
use serde_json::json;
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_market_parser_minimal(
name in "[A-Z]{2,10}",
index in 0usize..1000usize
) {
let data = json!({
"name": name
});
let result = parse_market(&data, index);
prop_assert!(
result.is_ok(),
"Market parser should handle minimal data"
);
}
#[test]
fn prop_ticker_parser_any_price(
symbol in "[A-Z]{2,10}".prop_map(|s| format!("{}/USDC:USDC", s)),
price in 1u64..1000000u64
) {
let mid_price = Decimal::from_u64(price).unwrap();
let result = parse_ticker(&symbol, mid_price, None);
prop_assert!(
result.is_ok(),
"Ticker parser should handle any valid price"
);
}
#[test]
fn prop_balance_parser_empty(_dummy in 0u32..100u32) {
let data = json!({});
let result = parse_balance(&data);
prop_assert!(
result.is_ok(),
"Balance parser should handle empty data"
);
}
#[test]
fn prop_balance_parser_partial(account_value in 0u64..1000000u64) {
let data = json!({
"marginSummary": {
"accountValue": account_value.to_string()
}
});
let result = parse_balance(&data);
prop_assert!(
result.is_ok(),
"Balance parser should handle partial data"
);
}
}
}
mod order_parameter_validation {
use super::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_negative_amount_invalid(amount in -1000.0f64..-0.001f64) {
prop_assert!(
amount < 0.0,
"Negative amounts should be invalid"
);
}
#[test]
fn prop_zero_price_invalid_for_limit(_dummy in 0u32..100u32) {
let price = 0.0f64;
prop_assert!(
price <= 0.0,
"Zero or negative price should be invalid for limit orders"
);
}
#[test]
fn prop_valid_params_accepted(
amount in 0.001f64..1000.0f64,
price in 0.01f64..100000.0f64
) {
prop_assert!(amount > 0.0, "Amount should be positive");
prop_assert!(price > 0.0, "Price should be positive");
}
}
}
mod request_serialization_roundtrip {
use super::*;
use serde_json::json;
fn asset_index_strategy() -> impl Strategy<Value = u32> {
0u32..100u32
}
fn price_strategy() -> impl Strategy<Value = String> {
(1u64..100000u64).prop_map(|n| n.to_string())
}
fn size_strategy() -> impl Strategy<Value = String> {
(1u64..10000u64).prop_map(|n| format!("{}.{}", n / 1000, n % 1000))
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_order_request_roundtrip(
asset in asset_index_strategy(),
is_buy in any::<bool>(),
price in price_strategy(),
size in size_strategy()
) {
let order_request = json!({
"a": asset,
"b": is_buy,
"p": price,
"s": size,
"r": false,
"t": {
"limit": {
"tif": "Gtc"
}
}
});
let serialized = serde_json::to_string(&order_request)
.expect("Serialization should succeed");
let parsed: serde_json::Value = serde_json::from_str(&serialized)
.expect("Parsing should succeed");
prop_assert_eq!(
parsed["a"].as_u64().unwrap() as u32,
asset,
"Asset should round-trip"
);
prop_assert_eq!(
parsed["b"].as_bool().unwrap(),
is_buy,
"isBuy should round-trip"
);
prop_assert_eq!(
parsed["p"].as_str().unwrap(),
price,
"Price should round-trip"
);
prop_assert_eq!(
parsed["s"].as_str().unwrap(),
size,
"Size should round-trip"
);
}
#[test]
fn prop_cancel_request_roundtrip(
asset in asset_index_strategy(),
order_id in 1u64..1000000u64
) {
let cancel_request = json!({
"type": "cancel",
"cancels": [{
"a": asset,
"o": order_id
}]
});
let serialized = serde_json::to_string(&cancel_request)
.expect("Serialization should succeed");
let parsed: serde_json::Value = serde_json::from_str(&serialized)
.expect("Parsing should succeed");
prop_assert_eq!(
parsed["type"].as_str().unwrap(),
"cancel",
"Type should round-trip"
);
let cancel = &parsed["cancels"][0];
prop_assert_eq!(
cancel["a"].as_u64().unwrap() as u32,
asset,
"Asset should round-trip"
);
prop_assert_eq!(
cancel["o"].as_u64().unwrap(),
order_id,
"Order ID should round-trip"
);
}
}
}
mod exchange_trait_consistency {
use super::*;
use ccxt_core::exchange::Exchange;
use ccxt_exchanges::hyperliquid::HyperLiquid;
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_metadata_consistent(_dummy in 0u32..100u32) {
let exchange = HyperLiquid::builder()
.testnet(true)
.build()
.expect("Failed to build HyperLiquid");
let trait_obj: &dyn Exchange = &exchange;
prop_assert_eq!(trait_obj.id(), "hyperliquid");
prop_assert_eq!(trait_obj.name(), "HyperLiquid");
prop_assert_eq!(trait_obj.version(), "1");
prop_assert!(!trait_obj.certified());
prop_assert!(trait_obj.has_websocket());
}
#[test]
fn prop_capabilities_consistent(_dummy in 0u32..100u32) {
let exchange = HyperLiquid::builder()
.testnet(true)
.build()
.expect("Failed to build HyperLiquid");
let trait_obj: &dyn Exchange = &exchange;
let caps = trait_obj.capabilities();
prop_assert!(caps.fetch_markets());
prop_assert!(caps.fetch_ticker());
prop_assert!(caps.fetch_tickers());
prop_assert!(caps.fetch_order_book());
prop_assert!(caps.fetch_trades());
prop_assert!(caps.fetch_ohlcv());
prop_assert!(caps.create_order());
prop_assert!(caps.cancel_order());
prop_assert!(caps.cancel_all_orders());
prop_assert!(caps.fetch_open_orders());
prop_assert!(caps.fetch_balance());
prop_assert!(caps.fetch_positions());
prop_assert!(caps.set_leverage());
prop_assert!(!caps.fetch_currencies());
prop_assert!(!caps.edit_order());
prop_assert!(!caps.fetch_my_trades());
}
#[test]
fn prop_rate_limit_consistent(_dummy in 0u32..100u32) {
let exchange = HyperLiquid::builder()
.testnet(true)
.build()
.expect("Failed to build HyperLiquid");
let trait_obj: &dyn Exchange = &exchange;
prop_assert!(
trait_obj.rate_limit() == 100,
"Rate limit should be 100"
);
}
}
}
mod not_implemented_error_handling {
use super::*;
use ccxt_core::exchange::Exchange;
use ccxt_exchanges::hyperliquid::HyperLiquid;
proptest! {
#![proptest_config(ProptestConfig::with_cases(10))]
#[test]
fn prop_fetch_order_not_implemented(_dummy in 0u32..10u32) {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let exchange = HyperLiquid::builder()
.testnet(true)
.build()
.expect("Failed to build HyperLiquid");
let trait_obj: &dyn Exchange = &exchange;
let result = trait_obj.fetch_order("123", Some("BTC/USDC:USDC")).await;
prop_assert!(result.is_err(), "fetch_order should return error");
let err = result.unwrap_err();
prop_assert!(
err.to_string().to_lowercase().contains("not implemented") ||
err.to_string().to_lowercase().contains("notimplemented"),
"Error should indicate not implemented"
);
Ok(())
})?;
}
#[test]
fn prop_fetch_closed_orders_not_implemented(_dummy in 0u32..10u32) {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let exchange = HyperLiquid::builder()
.testnet(true)
.build()
.expect("Failed to build HyperLiquid");
let trait_obj: &dyn Exchange = &exchange;
let result = trait_obj.fetch_closed_orders(None, None, None).await;
prop_assert!(result.is_err(), "fetch_closed_orders should return error");
let err = result.unwrap_err();
prop_assert!(
err.to_string().to_lowercase().contains("not implemented") ||
err.to_string().to_lowercase().contains("notimplemented"),
"Error should indicate not implemented"
);
Ok(())
})?;
}
#[test]
fn prop_fetch_my_trades_not_implemented(_dummy in 0u32..10u32) {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let exchange = HyperLiquid::builder()
.testnet(true)
.build()
.expect("Failed to build HyperLiquid");
let trait_obj: &dyn Exchange = &exchange;
let result = trait_obj.fetch_my_trades(None, None, None).await;
prop_assert!(result.is_err(), "fetch_my_trades should return error");
let err = result.unwrap_err();
prop_assert!(
err.to_string().to_lowercase().contains("not implemented") ||
err.to_string().to_lowercase().contains("notimplemented"),
"Error should indicate not implemented"
);
Ok(())
})?;
}
}
}