Skip to main content

df_hyperliquid_core/utils/
asset.rs

1//! Asset ids: the integer an order names its market by. Ids run perps on the
2//! main dex below 10000, spot pairs from 10000, HIP-3 perps from 100000 and
3//! HIP-4 outcomes from 100000000, so an id alone says what it names, and so
4//! which builder cap holds it.
5
6use std::fmt;
7
8use crate::trade::builder::fee::{BuilderFee, OUTCOME_BUILDER_CAP, PERP_BUILDER_CAP, SPOT_BUILDER_CAP};
9use crate::utils::sign::msgpack::Value;
10
11/// Where spot pairs begin: `10000 + pair index`.
12pub const SPOT_ASSET_BASE: u32 = 10_000;
13
14/// Where HIP-3 perps begin: `100000 + dex * 10000 + index`, `dex` the dex's
15/// place in `perpDexs`, where the main dex is 0.
16pub const HIP3_ASSET_BASE: u32 = 100_000;
17
18/// How many ids each HIP-3 dex spans.
19pub const HIP3_DEX_SPAN: u32 = 10_000;
20
21/// Where HIP-4 outcomes begin: `100000000 + 10 * outcome + side`.
22pub const OUTCOME_ASSET_BASE: u32 = 100_000_000;
23
24/// A HIP-4 outcome's side, the last digit of its encoding.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
26pub enum OutcomeSide {
27    /// The first side, `0`: the one a settle fraction pays.
28    Yes,
29    No,
30}
31
32impl OutcomeSide {
33    #[must_use]
34    pub const fn digit(self) -> u32 {
35        match self {
36            OutcomeSide::Yes => 0,
37            OutcomeSide::No => 1,
38        }
39    }
40
41    #[must_use]
42    pub const fn other(self) -> OutcomeSide {
43        match self {
44            OutcomeSide::Yes => OutcomeSide::No,
45            OutcomeSide::No => OutcomeSide::Yes,
46        }
47    }
48
49    /// `yes` or `no`, as serde writes it.
50    #[must_use]
51    pub const fn as_str(self) -> &'static str {
52        match self {
53            OutcomeSide::Yes => "yes",
54            OutcomeSide::No => "no",
55        }
56    }
57}
58
59impl serde::Serialize for OutcomeSide {
60    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
61        serializer.serialize_str(self.as_str())
62    }
63}
64
65impl<'de> serde::Deserialize<'de> for OutcomeSide {
66    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
67        match String::deserialize(deserializer)?.as_str() {
68            "yes" => Ok(OutcomeSide::Yes),
69            "no" => Ok(OutcomeSide::No),
70            other => Err(serde::de::Error::custom(format!("`{other}` is not an outcome side; expected yes or no"))),
71        }
72    }
73}
74
75/// What an asset id names.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
77pub enum AssetKind {
78    /// A perp on the main dex: its index in `meta.universe`.
79    Perp,
80    /// A spot pair: `10000 + pair index`.
81    Spot,
82    /// A perp on a HIP-3 dex: `100000 + dex * 10000 + index`.
83    Hip3Perp,
84    /// A HIP-4 outcome side: `100000000 + 10 * outcome + side`.
85    Outcome,
86}
87
88impl AssetKind {
89    pub const ALL: [AssetKind; 4] = [AssetKind::Perp, AssetKind::Spot, AssetKind::Hip3Perp, AssetKind::Outcome];
90
91    #[must_use]
92    pub const fn of(id: u32) -> AssetKind {
93        if id >= OUTCOME_ASSET_BASE {
94            AssetKind::Outcome
95        } else if id >= HIP3_ASSET_BASE {
96            AssetKind::Hip3Perp
97        } else if id >= SPOT_ASSET_BASE {
98            AssetKind::Spot
99        } else {
100            AssetKind::Perp
101        }
102    }
103
104    /// The most a builder may take on an order of this kind: 0.1% on a perp,
105    /// main dex or HIP-3, and 1% on spot. HIP-4 takes builder codes as spot
106    /// does, per Hyperliquid's HIP-4 page.
107    #[must_use]
108    pub const fn builder_cap(self) -> BuilderFee {
109        match self {
110            AssetKind::Perp | AssetKind::Hip3Perp => PERP_BUILDER_CAP,
111            AssetKind::Spot => SPOT_BUILDER_CAP,
112            AssetKind::Outcome => OUTCOME_BUILDER_CAP,
113        }
114    }
115
116    /// The kind that holds an action over these assets to its builder cap:
117    /// the one with the lowest, so one perp leg holds the whole action to the
118    /// perp cap. `None` for no assets.
119    #[must_use]
120    pub fn strictest(assets: impl IntoIterator<Item = Asset>) -> Option<AssetKind> {
121        assets.into_iter().map(Asset::kind).reduce(|kept, next| match next.builder_cap() < kept.builder_cap() {
122            true => next,
123            false => kept,
124        })
125    }
126
127    /// `a perp`, `an outcome`: one order of this kind, in a sentence.
128    #[must_use]
129    pub const fn one(self) -> &'static str {
130        match self {
131            AssetKind::Perp => "a perp",
132            AssetKind::Spot => "a spot",
133            AssetKind::Hip3Perp => "a HIP-3 perp",
134            AssetKind::Outcome => "an outcome",
135        }
136    }
137}
138
139/// An asset id. Made from the index it is counted from, so a pair index is
140/// never sent where an asset id belongs; an index past its range is refused
141/// rather than spilling into the next kind's ids.
142#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
143pub struct Asset(u32);
144
145impl Asset {
146    /// An id as the venue wrote it, in an order status or a fill.
147    #[must_use]
148    pub const fn from_id(id: u32) -> Asset {
149        Asset(id)
150    }
151
152    #[must_use]
153    pub const fn id(self) -> u32 {
154        self.0
155    }
156
157    #[must_use]
158    pub const fn kind(self) -> AssetKind {
159        AssetKind::of(self.0)
160    }
161
162    /// A perp on the main dex, by its index in `meta.universe`.
163    #[must_use]
164    pub const fn perp(index: u32) -> Option<Asset> {
165        match index < SPOT_ASSET_BASE {
166            true => Some(Asset(index)),
167            false => None,
168        }
169    }
170
171    /// A spot pair, by the pair's own `index` in `spotMeta.universe`, which
172    /// is not a token's index.
173    #[must_use]
174    pub const fn spot(pair: u32) -> Option<Asset> {
175        match pair < HIP3_ASSET_BASE - SPOT_ASSET_BASE {
176            true => Some(Asset(SPOT_ASSET_BASE + pair)),
177            false => None,
178        }
179    }
180
181    /// A perp on a HIP-3 dex: `dex` its place in `perpDexs` (1 or more; the
182    /// main dex at 0 is [`Asset::perp`]), `index` its place in that dex's
183    /// `meta.universe`.
184    #[must_use]
185    pub const fn hip3(dex: u32, index: u32) -> Option<Asset> {
186        if dex == 0 || index >= HIP3_DEX_SPAN {
187            return None;
188        }
189        let Some(offset) = dex.checked_mul(HIP3_DEX_SPAN) else {
190            return None;
191        };
192        let Some(start) = HIP3_ASSET_BASE.checked_add(offset) else {
193            return None;
194        };
195        match start.checked_add(index) {
196            Some(id) if id < OUTCOME_ASSET_BASE => Some(Asset(id)),
197            _ => None,
198        }
199    }
200
201    /// A HIP-3 perp's dex place in `perpDexs` and its index in that dex's
202    /// `meta`: [`Asset::hip3`] taken apart. `None` for any other kind of id.
203    #[must_use]
204    pub const fn hip3_parts(self) -> Option<(u32, u32)> {
205        if !matches!(self.kind(), AssetKind::Hip3Perp) {
206            return None;
207        }
208        let offset = self.0 - HIP3_ASSET_BASE;
209        match offset / HIP3_DEX_SPAN {
210            0 => None,
211            place => Some((place, offset % HIP3_DEX_SPAN)),
212        }
213    }
214
215    /// A perp by its dex's place in `perpDexs`: the main dex at 0, else HIP-3.
216    #[must_use]
217    pub const fn perp_on(dex: u32, index: u32) -> Option<Asset> {
218        match dex {
219            0 => Asset::perp(index),
220            dex => Asset::hip3(dex, index),
221        }
222    }
223
224    /// A HIP-4 outcome side: `100000000 + 10 * outcome + side`.
225    #[must_use]
226    pub const fn outcome(outcome: u32, side: OutcomeSide) -> Option<Asset> {
227        let Some(tens) = outcome.checked_mul(10) else {
228            return None;
229        };
230        match OUTCOME_ASSET_BASE.checked_add(tens + side.digit()) {
231            Some(id) => Some(Asset(id)),
232            None => None,
233        }
234    }
235
236    /// The outcome and side an outcome id names; `None` for any other kind.
237    #[must_use]
238    pub const fn outcome_side(self) -> Option<(u32, OutcomeSide)> {
239        if self.0 < OUTCOME_ASSET_BASE {
240            return None;
241        }
242        let encoding = self.0 - OUTCOME_ASSET_BASE;
243        let side = match encoding % 10 {
244            0 => OutcomeSide::Yes,
245            1 => OutcomeSide::No,
246            _ => return None,
247        };
248        Some((encoding / 10, side))
249    }
250}
251
252impl fmt::Display for Asset {
253    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254        write!(f, "{}", self.0)
255    }
256}
257
258/// An asset goes on the wire as its plain id.
259impl From<Asset> for Value {
260    fn from(asset: Asset) -> Value {
261        Value::Uint(u64::from(asset.0))
262    }
263}
264
265#[cfg(test)]
266#[path = "tests/asset.rs"]
267mod tests;