Skip to main content

df_hyperliquid_core/client/evm/u256/
amount.rs

1//! A [`U256`] amount and an exact [`Dec`]: wei to the coin and back at a named
2//! number of decimals, refused rather than rounded.
3
4use crate::client::evm::u256::U256;
5use crate::utils::{Dec, Round};
6
7/// HYPE on HyperEVM and ETH on Arbitrum both have 18 decimals.
8pub const NATIVE_DECIMALS: u32 = 18;
9
10/// Why an amount does not convert between wei and a [`Dec`].
11#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
12pub enum BadAmount {
13    #[error("{0} is not a positive amount")]
14    NotPositive(Dec),
15    #[error("{value} has more than {decimals} decimal places")]
16    TooFine { value: Dec, decimals: u32 },
17    #[error("the amount is larger than this conversion can count")]
18    TooLarge,
19    /// A token counted in more than eighteen places, and an amount that needs
20    /// them: a [`Dec`] keeps eighteen.
21    #[error("{amount} at {decimals} decimals needs more places than a Dec keeps")]
22    Unkept { amount: U256, decimals: u32 },
23}
24
25/// The places a [`Dec`] keeps.
26const DEC_PLACES: u32 = crate::utils::dec::MAX_SCALE;
27
28impl U256 {
29    /// The amount as a decimal of `decimals` places: wei at 18 is the coin.
30    /// Exact, or refused: past eighteen places the digits a [`Dec`] cannot
31    /// keep must be zero.
32    pub fn to_dec(self, decimals: u32) -> Result<Dec, BadAmount> {
33        let (units, scale) = match decimals.checked_sub(DEC_PLACES) {
34            None | Some(0) => (self, decimals),
35            Some(extra) => {
36                let unkept = BadAmount::Unkept { amount: self, decimals };
37                let step = U256::ten_to(extra).ok_or_else(|| unkept.clone())?;
38                let (quotient, rest) = self.div_rem(step).ok_or(BadAmount::TooLarge)?;
39                if !rest.is_zero() {
40                    return Err(unkept);
41                }
42                (quotient, DEC_PLACES)
43            }
44        };
45        let units = units.to_u128().and_then(|units| i128::try_from(units).ok()).ok_or(BadAmount::TooLarge)?;
46        Dec::new(units, scale).ok_or(BadAmount::TooLarge)
47    }
48
49    /// `value` counted in `10^-decimals`: 1.5 at 18 is 1.5e18 wei. A value
50    /// finer than `decimals` is refused, never rounded.
51    pub fn from_dec(value: Dec, decimals: u32) -> Result<U256, BadAmount> {
52        if value.is_negative() {
53            return Err(BadAmount::NotPositive(value));
54        }
55        let places = value.places();
56        if places > decimals {
57            return Err(BadAmount::TooFine { value, decimals });
58        }
59        let units = value
60            .units_at(places, Round::Down)
61            .and_then(|units| u128::try_from(units).ok())
62            .ok_or(BadAmount::TooLarge)?;
63        U256::ten_to(decimals - places)
64            .and_then(|step| U256::from_u128(units).checked_mul(step))
65            .ok_or(BadAmount::TooLarge)
66    }
67}
68
69#[cfg(test)]
70#[path = "tests/amount.rs"]
71mod tests;