use serde::Serialize;
use crate::{
common::enums::{HyperliquidBarInterval, HyperliquidInfoRequestType},
http::models::{
HyperliquidExecBuilderFee, HyperliquidExecCancelByCloidRequest, HyperliquidExecGrouping,
HyperliquidExecModifyOrderRequest, HyperliquidExecPlaceOrderRequest,
},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum ExchangeActionType {
Order,
Cancel,
CancelByCloid,
Modify,
UpdateLeverage,
UpdateIsolatedMargin,
}
impl AsRef<str> for ExchangeActionType {
fn as_ref(&self) -> &str {
match self {
Self::Order => "order",
Self::Cancel => "cancel",
Self::CancelByCloid => "cancelByCloid",
Self::Modify => "modify",
Self::UpdateLeverage => "updateLeverage",
Self::UpdateIsolatedMargin => "updateIsolatedMargin",
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct OrderParams {
pub orders: Vec<HyperliquidExecPlaceOrderRequest>,
pub grouping: HyperliquidExecGrouping,
#[serde(skip_serializing_if = "Option::is_none")]
pub builder: Option<HyperliquidExecBuilderFee>,
}
#[derive(Debug, Clone, Serialize)]
pub struct CancelParams {
pub cancels: Vec<HyperliquidExecCancelByCloidRequest>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ModifyParams {
#[serde(flatten)]
pub request: HyperliquidExecModifyOrderRequest,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateLeverageParams {
pub asset: u32,
pub is_cross: bool,
pub leverage: u32,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateIsolatedMarginParams {
pub asset: u32,
pub is_buy: bool,
pub ntli: i64,
}
#[derive(Debug, Clone, Serialize)]
pub struct L2BookParams {
pub coin: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct UserFillsParams {
pub user: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct OrderStatusParams {
pub user: String,
pub oid: u64,
}
#[derive(Debug, Clone, Serialize)]
pub struct OpenOrdersParams {
pub user: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct ClearinghouseStateParams {
pub user: String,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CandleSnapshotReq {
pub coin: String,
pub interval: HyperliquidBarInterval,
pub start_time: u64,
pub end_time: u64,
}
#[derive(Debug, Clone, Serialize)]
pub struct CandleSnapshotParams {
pub req: CandleSnapshotReq,
}
#[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
pub enum InfoRequestParams {
L2Book(L2BookParams),
UserFills(UserFillsParams),
OrderStatus(OrderStatusParams),
OpenOrders(OpenOrdersParams),
ClearinghouseState(ClearinghouseStateParams),
CandleSnapshot(CandleSnapshotParams),
None,
}
#[derive(Debug, Clone, Serialize)]
pub struct InfoRequest {
#[serde(rename = "type")]
pub request_type: HyperliquidInfoRequestType,
#[serde(flatten)]
pub params: InfoRequestParams,
}
impl InfoRequest {
pub fn meta() -> Self {
Self {
request_type: HyperliquidInfoRequestType::Meta,
params: InfoRequestParams::None,
}
}
pub fn all_perp_metas() -> Self {
Self {
request_type: HyperliquidInfoRequestType::AllPerpMetas,
params: InfoRequestParams::None,
}
}
pub fn spot_meta() -> Self {
Self {
request_type: HyperliquidInfoRequestType::SpotMeta,
params: InfoRequestParams::None,
}
}
pub fn meta_and_asset_ctxs() -> Self {
Self {
request_type: HyperliquidInfoRequestType::MetaAndAssetCtxs,
params: InfoRequestParams::None,
}
}
pub fn spot_meta_and_asset_ctxs() -> Self {
Self {
request_type: HyperliquidInfoRequestType::SpotMetaAndAssetCtxs,
params: InfoRequestParams::None,
}
}
pub fn l2_book(coin: &str) -> Self {
Self {
request_type: HyperliquidInfoRequestType::L2Book,
params: InfoRequestParams::L2Book(L2BookParams {
coin: coin.to_string(),
}),
}
}
pub fn user_fills(user: &str) -> Self {
Self {
request_type: HyperliquidInfoRequestType::UserFills,
params: InfoRequestParams::UserFills(UserFillsParams {
user: user.to_string(),
}),
}
}
pub fn order_status(user: &str, oid: u64) -> Self {
Self {
request_type: HyperliquidInfoRequestType::OrderStatus,
params: InfoRequestParams::OrderStatus(OrderStatusParams {
user: user.to_string(),
oid,
}),
}
}
pub fn open_orders(user: &str) -> Self {
Self {
request_type: HyperliquidInfoRequestType::OpenOrders,
params: InfoRequestParams::OpenOrders(OpenOrdersParams {
user: user.to_string(),
}),
}
}
pub fn frontend_open_orders(user: &str) -> Self {
Self {
request_type: HyperliquidInfoRequestType::FrontendOpenOrders,
params: InfoRequestParams::OpenOrders(OpenOrdersParams {
user: user.to_string(),
}),
}
}
pub fn clearinghouse_state(user: &str) -> Self {
Self {
request_type: HyperliquidInfoRequestType::ClearinghouseState,
params: InfoRequestParams::ClearinghouseState(ClearinghouseStateParams {
user: user.to_string(),
}),
}
}
pub fn user_fees(user: &str) -> Self {
Self {
request_type: HyperliquidInfoRequestType::UserFees,
params: InfoRequestParams::OpenOrders(OpenOrdersParams {
user: user.to_string(),
}),
}
}
pub fn candle_snapshot(
coin: &str,
interval: HyperliquidBarInterval,
start_time: u64,
end_time: u64,
) -> Self {
Self {
request_type: HyperliquidInfoRequestType::CandleSnapshot,
params: InfoRequestParams::CandleSnapshot(CandleSnapshotParams {
req: CandleSnapshotReq {
coin: coin.to_string(),
interval,
start_time,
end_time,
},
}),
}
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
pub enum ExchangeActionParams {
Order(OrderParams),
Cancel(CancelParams),
Modify(ModifyParams),
UpdateLeverage(UpdateLeverageParams),
UpdateIsolatedMargin(UpdateIsolatedMarginParams),
}
#[derive(Debug, Clone, Serialize)]
pub struct ExchangeAction {
#[serde(rename = "type", serialize_with = "serialize_action_type")]
pub action_type: ExchangeActionType,
#[serde(flatten)]
pub params: ExchangeActionParams,
}
fn serialize_action_type<S>(
action_type: &ExchangeActionType,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(action_type.as_ref())
}
impl ExchangeAction {
pub fn order(
orders: Vec<HyperliquidExecPlaceOrderRequest>,
builder: Option<HyperliquidExecBuilderFee>,
) -> Self {
Self {
action_type: ExchangeActionType::Order,
params: ExchangeActionParams::Order(OrderParams {
orders,
grouping: HyperliquidExecGrouping::Na,
builder,
}),
}
}
pub fn cancel(cancels: Vec<HyperliquidExecCancelByCloidRequest>) -> Self {
Self {
action_type: ExchangeActionType::Cancel,
params: ExchangeActionParams::Cancel(CancelParams { cancels }),
}
}
pub fn cancel_by_cloid(cancels: Vec<HyperliquidExecCancelByCloidRequest>) -> Self {
Self {
action_type: ExchangeActionType::CancelByCloid,
params: ExchangeActionParams::Cancel(CancelParams { cancels }),
}
}
pub fn modify(request: HyperliquidExecModifyOrderRequest) -> Self {
Self {
action_type: ExchangeActionType::Modify,
params: ExchangeActionParams::Modify(ModifyParams { request }),
}
}
pub fn update_leverage(asset: u32, is_cross: bool, leverage: u32) -> Self {
Self {
action_type: ExchangeActionType::UpdateLeverage,
params: ExchangeActionParams::UpdateLeverage(UpdateLeverageParams {
asset,
is_cross,
leverage,
}),
}
}
pub fn update_isolated_margin(asset: u32, is_buy: bool, ntli: i64) -> Self {
Self {
action_type: ExchangeActionType::UpdateIsolatedMargin,
params: ExchangeActionParams::UpdateIsolatedMargin(UpdateIsolatedMarginParams {
asset,
is_buy,
ntli,
}),
}
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use rust_decimal::Decimal;
use super::*;
use crate::http::models::{
Cloid, HyperliquidExecCancelByCloidRequest, HyperliquidExecLimitParams,
HyperliquidExecModifyOrderRequest, HyperliquidExecOrderKind,
HyperliquidExecPlaceOrderRequest, HyperliquidExecTif,
};
#[rstest]
fn test_info_request_meta() {
let req = InfoRequest::meta();
assert_eq!(req.request_type, HyperliquidInfoRequestType::Meta);
assert!(matches!(req.params, InfoRequestParams::None));
}
#[rstest]
fn test_info_request_all_perp_metas() {
let req = InfoRequest::all_perp_metas();
assert_eq!(req.request_type, HyperliquidInfoRequestType::AllPerpMetas);
let json = serde_json::to_string(&req).unwrap();
assert!(json.contains(r#""type":"allPerpMetas""#));
}
#[rstest]
fn test_info_request_l2_book() {
let req = InfoRequest::l2_book("BTC");
assert_eq!(req.request_type, HyperliquidInfoRequestType::L2Book);
let json = serde_json::to_string(&req).unwrap();
assert!(json.contains("\"coin\":\"BTC\""));
}
#[rstest]
fn test_exchange_action_order() {
let order = HyperliquidExecPlaceOrderRequest {
asset: 0,
is_buy: true,
price: Decimal::new(50000, 0),
size: Decimal::new(1, 0),
reduce_only: false,
kind: HyperliquidExecOrderKind::Limit {
limit: HyperliquidExecLimitParams {
tif: HyperliquidExecTif::Gtc,
},
},
cloid: None,
};
let action = ExchangeAction::order(vec![order], None);
assert_eq!(action.action_type, ExchangeActionType::Order);
let json = serde_json::to_string(&action).unwrap();
assert!(json.contains("\"orders\""));
}
#[rstest]
fn test_exchange_action_cancel() {
let cancel = HyperliquidExecCancelByCloidRequest {
asset: 0,
cloid: Cloid::from_hex("0x00000000000000000000000000000000").unwrap(),
};
let action = ExchangeAction::cancel(vec![cancel]);
assert_eq!(action.action_type, ExchangeActionType::Cancel);
}
#[rstest]
fn test_exchange_action_serialization() {
let order = HyperliquidExecPlaceOrderRequest {
asset: 0,
is_buy: true,
price: Decimal::new(50000, 0),
size: Decimal::new(1, 0),
reduce_only: false,
kind: HyperliquidExecOrderKind::Limit {
limit: HyperliquidExecLimitParams {
tif: HyperliquidExecTif::Gtc,
},
},
cloid: None,
};
let action = ExchangeAction::order(vec![order], None);
let json = serde_json::to_string(&action).unwrap();
assert!(json.contains(r#""type":"order""#));
assert!(json.contains(r#""orders""#));
assert!(json.contains(r#""grouping":"na""#));
}
#[rstest]
fn test_exchange_action_type_as_ref() {
assert_eq!(ExchangeActionType::Order.as_ref(), "order");
assert_eq!(ExchangeActionType::Cancel.as_ref(), "cancel");
assert_eq!(ExchangeActionType::CancelByCloid.as_ref(), "cancelByCloid");
assert_eq!(ExchangeActionType::Modify.as_ref(), "modify");
assert_eq!(
ExchangeActionType::UpdateLeverage.as_ref(),
"updateLeverage"
);
assert_eq!(
ExchangeActionType::UpdateIsolatedMargin.as_ref(),
"updateIsolatedMargin"
);
}
#[rstest]
fn test_update_leverage_serialization() {
let action = ExchangeAction::update_leverage(1, true, 10);
let json = serde_json::to_string(&action).unwrap();
assert!(json.contains(r#""type":"updateLeverage""#));
assert!(json.contains(r#""asset":1"#));
assert!(json.contains(r#""isCross":true"#));
assert!(json.contains(r#""leverage":10"#));
}
#[rstest]
fn test_update_isolated_margin_serialization() {
let action = ExchangeAction::update_isolated_margin(2, false, 1000);
let json = serde_json::to_string(&action).unwrap();
assert!(json.contains(r#""type":"updateIsolatedMargin""#));
assert!(json.contains(r#""asset":2"#));
assert!(json.contains(r#""isBuy":false"#));
assert!(json.contains(r#""ntli":1000"#));
}
#[rstest]
fn test_cancel_by_cloid_serialization() {
let cancel_request = HyperliquidExecCancelByCloidRequest {
asset: 0,
cloid: Cloid::from_hex("0x00000000000000000000000000000000").unwrap(),
};
let action = ExchangeAction::cancel_by_cloid(vec![cancel_request]);
let json = serde_json::to_string(&action).unwrap();
assert!(json.contains(r#""type":"cancelByCloid""#));
assert!(json.contains(r#""cancels""#));
}
#[rstest]
fn test_modify_serialization() {
let modify_request = HyperliquidExecModifyOrderRequest {
oid: 12345,
order: HyperliquidExecPlaceOrderRequest {
asset: 0,
is_buy: true,
price: Decimal::new(51000, 0),
size: Decimal::new(2, 0),
reduce_only: false,
kind: HyperliquidExecOrderKind::Limit {
limit: HyperliquidExecLimitParams {
tif: HyperliquidExecTif::Gtc,
},
},
cloid: None,
},
};
let action = ExchangeAction::modify(modify_request);
let json = serde_json::to_string(&action).unwrap();
assert!(json.contains(r#""type":"modify""#));
assert!(json.contains(r#""oid":12345"#));
assert!(json.contains(r#""order""#));
}
}