use std::fmt::Display;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct HyperliquidAssetId(pub u32);
const HIP_1_SPOT_BASE: u32 = 10_000;
const HIP_3_BUILDER_PERP_BASE: u32 = 100_000;
const HIP_4_OUTCOME_BASE: u32 = 100_000_000;
impl HyperliquidAssetId {
pub fn perp(index: u32) -> Self {
Self(index)
}
pub fn spot(index: u32) -> Self {
Self(HIP_1_SPOT_BASE + index)
}
pub fn builder_perp(dex_index: u32, meta_index: u32) -> Self {
Self(HIP_3_BUILDER_PERP_BASE + dex_index * 10_000 + meta_index)
}
pub fn outcome(outcome: u32, side: u8) -> Self {
assert!(side <= 1, "outcome side must be 0 or 1, received {side}");
Self(HIP_4_OUTCOME_BASE + 10 * outcome + u32::from(side))
}
pub fn from_outcome_encoding(encoding: u32) -> Option<Self> {
let raw = HIP_4_OUTCOME_BASE.checked_add(encoding)?;
let asset_id = Self(raw);
asset_id.is_outcome().then_some(asset_id)
}
pub fn is_perp(self) -> bool {
self.0 < HIP_1_SPOT_BASE
}
pub fn is_spot(self) -> bool {
self.0 >= HIP_1_SPOT_BASE && self.0 < HIP_3_BUILDER_PERP_BASE
}
pub fn is_builder_perp(self) -> bool {
self.0 >= HIP_3_BUILDER_PERP_BASE && self.0 < HIP_4_OUTCOME_BASE
}
pub fn is_outcome(self) -> bool {
self.0 >= HIP_4_OUTCOME_BASE && (self.0 - HIP_4_OUTCOME_BASE) % 10 <= 1
}
pub fn base_index(self) -> u32 {
if self.is_outcome() {
self.0 - HIP_4_OUTCOME_BASE
} else if self.is_builder_perp() {
(self.0 - HIP_3_BUILDER_PERP_BASE) % 10_000
} else if self.is_spot() {
self.0 - HIP_1_SPOT_BASE
} else {
self.0
}
}
pub fn outcome_index(self) -> Option<u32> {
self.outcome_encoding().map(|encoding| encoding / 10)
}
pub fn outcome_side(self) -> Option<u8> {
self.outcome_encoding()
.map(|encoding| (encoding % 10) as u8)
}
pub fn outcome_encoding(self) -> Option<u32> {
self.is_outcome().then(|| self.0 - HIP_4_OUTCOME_BASE)
}
pub fn to_raw(self) -> u32 {
self.0
}
}
impl Display for HyperliquidAssetId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
fn test_asset_id_perp() {
let asset_id = HyperliquidAssetId::perp(7);
assert_eq!(asset_id.to_raw(), 7);
assert!(asset_id.is_perp());
assert!(!asset_id.is_spot());
assert!(!asset_id.is_builder_perp());
assert!(!asset_id.is_outcome());
assert_eq!(asset_id.base_index(), 7);
}
#[rstest]
fn test_asset_id_spot() {
let asset_id = HyperliquidAssetId::spot(7);
assert_eq!(asset_id.to_raw(), 10_007);
assert!(!asset_id.is_perp());
assert!(asset_id.is_spot());
assert!(!asset_id.is_builder_perp());
assert!(!asset_id.is_outcome());
assert_eq!(asset_id.base_index(), 7);
}
#[rstest]
fn test_asset_id_builder_perp() {
let asset_id = HyperliquidAssetId::builder_perp(1, 7);
assert_eq!(asset_id.to_raw(), 110_007);
assert!(!asset_id.is_perp());
assert!(!asset_id.is_spot());
assert!(asset_id.is_builder_perp());
assert!(!asset_id.is_outcome());
assert_eq!(asset_id.base_index(), 7);
}
#[rstest]
fn test_asset_id_outcome() {
let asset_id = HyperliquidAssetId::outcome(1, 0);
assert_eq!(asset_id.to_raw(), 100_000_010);
assert!(!asset_id.is_perp());
assert!(!asset_id.is_spot());
assert!(!asset_id.is_builder_perp());
assert!(asset_id.is_outcome());
assert_eq!(asset_id.base_index(), 10);
assert_eq!(asset_id.outcome_encoding(), Some(10));
assert_eq!(asset_id.outcome_index(), Some(1));
assert_eq!(asset_id.outcome_side(), Some(0));
}
#[rstest]
fn test_asset_id_outcome_side_one() {
let asset_id = HyperliquidAssetId::outcome(3, 1);
assert_eq!(asset_id.to_raw(), 100_000_031);
assert!(asset_id.is_outcome());
assert_eq!(asset_id.outcome_encoding(), Some(31));
assert_eq!(asset_id.outcome_index(), Some(3));
assert_eq!(asset_id.outcome_side(), Some(1));
}
#[rstest]
fn test_asset_id_from_outcome_encoding() {
let asset_id = HyperliquidAssetId::from_outcome_encoding(10).unwrap();
assert_eq!(asset_id.to_raw(), 100_000_010);
assert_eq!(asset_id.outcome_index(), Some(1));
assert_eq!(asset_id.outcome_side(), Some(0));
}
#[rstest]
fn test_asset_id_from_outcome_encoding_rejects_invalid_side() {
assert_eq!(HyperliquidAssetId::from_outcome_encoding(12), None);
}
#[rstest]
fn test_asset_id_from_outcome_encoding_rejects_overflow() {
assert_eq!(HyperliquidAssetId::from_outcome_encoding(u32::MAX), None);
}
#[rstest]
#[should_panic(expected = "outcome side must be 0 or 1")]
fn test_asset_id_outcome_invalid_side() {
let _ = HyperliquidAssetId::outcome(0, 2);
}
#[rstest]
fn test_asset_id_outcome_accessors_non_outcome() {
let perp = HyperliquidAssetId::perp(7);
assert_eq!(perp.outcome_index(), None);
assert_eq!(perp.outcome_side(), None);
let spot = HyperliquidAssetId::spot(7);
assert_eq!(spot.outcome_index(), None);
assert_eq!(spot.outcome_side(), None);
let builder = HyperliquidAssetId::builder_perp(1, 7);
assert_eq!(builder.outcome_index(), None);
assert_eq!(builder.outcome_side(), None);
}
#[rstest]
fn test_asset_id_outcome_invalid_side_digit_not_outcome() {
for side_digit in 2..=9u32 {
let raw = HyperliquidAssetId(100_000_000 + side_digit);
assert!(!raw.is_outcome(), "side digit {side_digit} must reject");
assert_eq!(raw.outcome_index(), None);
assert_eq!(raw.outcome_side(), None);
}
}
#[rstest]
fn test_asset_id_ranges_mutually_exclusive() {
let high_builder = HyperliquidAssetId(99_999_999);
assert!(high_builder.is_builder_perp());
assert!(!high_builder.is_outcome());
let low_outcome = HyperliquidAssetId(100_000_000);
assert!(!low_outcome.is_builder_perp());
assert!(low_outcome.is_outcome());
}
}