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
50/// What an asset id names.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
52pub enum AssetKind {
53    /// A perp on the main dex: its index in `meta.universe`.
54    Perp,
55    /// A spot pair: `10000 + pair index`.
56    Spot,
57    /// A perp on a HIP-3 dex: `100000 + dex * 10000 + index`.
58    Hip3Perp,
59    /// A HIP-4 outcome side: `100000000 + 10 * outcome + side`.
60    Outcome,
61}
62
63impl AssetKind {
64    pub const ALL: [AssetKind; 4] = [AssetKind::Perp, AssetKind::Spot, AssetKind::Hip3Perp, AssetKind::Outcome];
65
66    #[must_use]
67    pub const fn of(id: u32) -> AssetKind {
68        if id >= OUTCOME_ASSET_BASE {
69            AssetKind::Outcome
70        } else if id >= HIP3_ASSET_BASE {
71            AssetKind::Hip3Perp
72        } else if id >= SPOT_ASSET_BASE {
73            AssetKind::Spot
74        } else {
75            AssetKind::Perp
76        }
77    }
78
79    /// The most a builder may take on an order of this kind: 0.1% on a perp,
80    /// main dex or HIP-3, and 1% on spot. HIP-4 takes builder codes as spot
81    /// does, per Hyperliquid's HIP-4 page.
82    #[must_use]
83    pub const fn builder_cap(self) -> BuilderFee {
84        match self {
85            AssetKind::Perp | AssetKind::Hip3Perp => PERP_BUILDER_CAP,
86            AssetKind::Spot => SPOT_BUILDER_CAP,
87            AssetKind::Outcome => OUTCOME_BUILDER_CAP,
88        }
89    }
90
91    /// The kind that holds an action over these assets to its builder cap:
92    /// the one with the lowest, so one perp leg holds the whole action to the
93    /// perp cap. `None` for no assets.
94    #[must_use]
95    pub fn strictest(assets: impl IntoIterator<Item = Asset>) -> Option<AssetKind> {
96        assets.into_iter().map(Asset::kind).reduce(|kept, next| match next.builder_cap() < kept.builder_cap() {
97            true => next,
98            false => kept,
99        })
100    }
101
102    /// `a perp`, `an outcome`: one order of this kind, in a sentence.
103    #[must_use]
104    pub const fn one(self) -> &'static str {
105        match self {
106            AssetKind::Perp => "a perp",
107            AssetKind::Spot => "a spot",
108            AssetKind::Hip3Perp => "a HIP-3 perp",
109            AssetKind::Outcome => "an outcome",
110        }
111    }
112}
113
114/// An asset id. Made from the index it is counted from, so a pair index is
115/// never sent where an asset id belongs; an index past its range is refused
116/// rather than spilling into the next kind's ids.
117#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
118pub struct Asset(u32);
119
120impl Asset {
121    /// An id as the venue wrote it, in an order status or a fill.
122    #[must_use]
123    pub const fn from_id(id: u32) -> Asset {
124        Asset(id)
125    }
126
127    #[must_use]
128    pub const fn id(self) -> u32 {
129        self.0
130    }
131
132    #[must_use]
133    pub const fn kind(self) -> AssetKind {
134        AssetKind::of(self.0)
135    }
136
137    /// A perp on the main dex, by its index in `meta.universe`.
138    #[must_use]
139    pub const fn perp(index: u32) -> Option<Asset> {
140        match index < SPOT_ASSET_BASE {
141            true => Some(Asset(index)),
142            false => None,
143        }
144    }
145
146    /// A spot pair, by the pair's own `index` in `spotMeta.universe`, which
147    /// is not a token's index.
148    #[must_use]
149    pub const fn spot(pair: u32) -> Option<Asset> {
150        match pair < HIP3_ASSET_BASE - SPOT_ASSET_BASE {
151            true => Some(Asset(SPOT_ASSET_BASE + pair)),
152            false => None,
153        }
154    }
155
156    /// A perp on a HIP-3 dex: `dex` its place in `perpDexs` (1 or more; the
157    /// main dex at 0 is [`Asset::perp`]), `index` its place in that dex's
158    /// `meta.universe`.
159    #[must_use]
160    pub const fn hip3(dex: u32, index: u32) -> Option<Asset> {
161        if dex == 0 || index >= HIP3_DEX_SPAN {
162            return None;
163        }
164        let Some(offset) = dex.checked_mul(HIP3_DEX_SPAN) else {
165            return None;
166        };
167        let Some(start) = HIP3_ASSET_BASE.checked_add(offset) else {
168            return None;
169        };
170        match start.checked_add(index) {
171            Some(id) if id < OUTCOME_ASSET_BASE => Some(Asset(id)),
172            _ => None,
173        }
174    }
175
176    /// A HIP-3 perp's dex place in `perpDexs` and its index in that dex's
177    /// `meta`: [`Asset::hip3`] taken apart. `None` for any other kind of id.
178    #[must_use]
179    pub const fn hip3_parts(self) -> Option<(u32, u32)> {
180        if !matches!(self.kind(), AssetKind::Hip3Perp) {
181            return None;
182        }
183        let offset = self.0 - HIP3_ASSET_BASE;
184        match offset / HIP3_DEX_SPAN {
185            0 => None,
186            place => Some((place, offset % HIP3_DEX_SPAN)),
187        }
188    }
189
190    /// A perp by its dex's place in `perpDexs`: the main dex at 0, else HIP-3.
191    #[must_use]
192    pub const fn perp_on(dex: u32, index: u32) -> Option<Asset> {
193        match dex {
194            0 => Asset::perp(index),
195            dex => Asset::hip3(dex, index),
196        }
197    }
198
199    /// A HIP-4 outcome side: `100000000 + 10 * outcome + side`.
200    #[must_use]
201    pub const fn outcome(outcome: u32, side: OutcomeSide) -> Option<Asset> {
202        let Some(tens) = outcome.checked_mul(10) else {
203            return None;
204        };
205        match OUTCOME_ASSET_BASE.checked_add(tens + side.digit()) {
206            Some(id) => Some(Asset(id)),
207            None => None,
208        }
209    }
210
211    /// The outcome and side an outcome id names; `None` for any other kind.
212    #[must_use]
213    pub const fn outcome_side(self) -> Option<(u32, OutcomeSide)> {
214        if self.0 < OUTCOME_ASSET_BASE {
215            return None;
216        }
217        let encoding = self.0 - OUTCOME_ASSET_BASE;
218        let side = match encoding % 10 {
219            0 => OutcomeSide::Yes,
220            1 => OutcomeSide::No,
221            _ => return None,
222        };
223        Some((encoding / 10, side))
224    }
225}
226
227impl fmt::Display for Asset {
228    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229        write!(f, "{}", self.0)
230    }
231}
232
233/// An asset goes on the wire as its plain id.
234impl From<Asset> for Value {
235    fn from(asset: Asset) -> Value {
236        Value::Uint(u64::from(asset.0))
237    }
238}
239
240#[cfg(test)]
241#[path = "tests/asset.rs"]
242mod tests;