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#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
8pub enum BadUnits {
9    #[error("{0} is not a positive amount")]
10    NotPositive(Dec),
11    #[error("{value} has more than {places} decimal places")]
12    TooFine { value: Dec, places: u32 },
13    #[error("{0} is too large")]
14    TooLarge(Dec),
15}
16
17/// `value` as a count of `10^-places`, refused rather than rounded when it is
18/// finer than that: what was typed is what is signed.
19pub fn exact_units(value: Dec, places: u32) -> Result<u64, BadUnits> {
20    if value.places() > places {
21        return Err(BadUnits::TooFine { value, places });
22    }
23    cut_units(value, places)
24}
25
26/// `value` cut toward zero to a count of `10^-places`: for a computed figure
27/// (a whole balance, say), so it never asks for more than there is.
28pub fn cut_units(value: Dec, places: u32) -> Result<u64, BadUnits> {
29    if !value.is_positive() {
30        return Err(BadUnits::NotPositive(value));
31    }
32    let units = value.units_at(places, Round::Down).ok_or(BadUnits::TooLarge(value))?;
33    match u64::try_from(units) {
34        Ok(0) => Err(BadUnits::NotPositive(value)),
35        Ok(units) => Ok(units),
36        Err(_) => Err(BadUnits::TooLarge(value)),
37    }
38}
39
40#[cfg(test)]
41mod tests {
42    use super::*;
43
44    fn dec(text: &str) -> Dec {
45        Dec::parse(text).unwrap()
46    }
47
48    #[test]
49    fn an_exact_amount_is_counted_and_a_finer_one_refused() {
50        assert_eq!(exact_units(dec("10"), 6), Ok(10_000_000));
51        assert_eq!(exact_units(dec("2.5"), 6), Ok(2_500_000));
52        assert_eq!(exact_units(dec("100"), 8), Ok(10_000_000_000));
53        assert_eq!(exact_units(dec("0.00000001"), 8), Ok(1));
54        assert_eq!(exact_units(dec("1.0000001"), 6), Err(BadUnits::TooFine { value: dec("1.0000001"), places: 6 }));
55        assert_eq!(exact_units(Dec::ZERO, 6), Err(BadUnits::NotPositive(Dec::ZERO)));
56        assert_eq!(exact_units(dec("-1"), 6), Err(BadUnits::NotPositive(dec("-1"))));
57        assert_eq!(exact_units(dec("18446744073709.551616"), 6), Err(BadUnits::TooLarge(dec("18446744073709.551616"))));
58    }
59
60    #[test]
61    fn a_computed_amount_is_cut_toward_zero() {
62        assert_eq!(cut_units(dec("604931.6453119999"), 6), Ok(604_931_645_311));
63        assert_eq!(
64            cut_units(dec("0.0000009"), 6),
65            Err(BadUnits::NotPositive(dec("0.0000009"))),
66            "dust cuts to nothing"
67        );
68    }
69}