Skip to main content

df_hyperliquid_core/utils/
units.rs

1//! Amounts an action carries as whole integers: USDC in millionths
2//! (`vaultTransfer`, `subAccountTransfer`), HYPE in 1e8 (`cDeposit`,
3//! `tokenDelegate`). The integer is what is signed, so it is made here, once.
4
5use crate::utils::dec::{Dec, Round};
6
7/// USDC's decimals on the perp side and on Arbitrum: millionths.
8pub const USDC_DECIMALS: u32 = 6;
9
10/// HYPE on HyperCore counts in 1e8: what `wei` means in staking and delegation.
11pub const HYPE_DECIMALS: u32 = 8;
12
13#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
14pub enum BadUnits {
15    #[error("{0} is not a positive amount")]
16    NotPositive(Dec),
17    #[error("{value} has more than {places} decimal places")]
18    TooFine { value: Dec, places: u32 },
19    #[error("{0} is too large")]
20    TooLarge(Dec),
21}
22
23/// `value` as a count of `10^-places`, refused rather than rounded when it is
24/// finer than that: what was typed is what is signed.
25pub fn exact_units(value: Dec, places: u32) -> Result<u64, BadUnits> {
26    if value.places() > places {
27        return Err(BadUnits::TooFine { value, places });
28    }
29    cut_units(value, places)
30}
31
32/// `value` cut toward zero to a count of `10^-places`: for a computed figure
33/// (a whole balance, say), so it never asks for more than there is.
34pub fn cut_units(value: Dec, places: u32) -> Result<u64, BadUnits> {
35    if !value.is_positive() {
36        return Err(BadUnits::NotPositive(value));
37    }
38    let units = value.units_at(places, Round::Down).ok_or(BadUnits::TooLarge(value))?;
39    match u64::try_from(units) {
40        Ok(0) => Err(BadUnits::NotPositive(value)),
41        Ok(units) => Ok(units),
42        Err(_) => Err(BadUnits::TooLarge(value)),
43    }
44}
45
46#[cfg(test)]
47#[path = "tests/units.rs"]
48mod tests;