fermat_core/decimal.rs
1//! Core `Decimal` type definition and constants.
2
3use crate::error::ArithmeticError;
4
5/// Maximum allowed scale (decimal places). Fixed at 28 to stay within `i128` range.
6///
7/// At scale 28 the mantissa unit is `10^-28`, giving sub-yocto precision.
8pub const MAX_SCALE: u8 = 28;
9
10/// Conventional scale for USDC (6 decimal places).
11pub const USDC_SCALE: u8 = 6;
12
13/// Conventional scale for SOL lamports (9 decimal places).
14pub const SOL_SCALE: u8 = 9;
15
16/// Conventional scale for percentage values expressed as basis points (4 d.p.).
17pub const BPS_SCALE: u8 = 4;
18
19/// A 128-bit signed fixed-point decimal.
20///
21/// ```text
22/// value = mantissa × 10^(-scale)
23/// ```
24///
25/// - `mantissa`: signed coefficient stored as `i128`
26/// - `scale`: number of decimal places in `[0, MAX_SCALE]`
27///
28/// On-chain Borsh encoding: 17 bytes (16-byte LE mantissa + 1-byte scale).
29#[derive(Clone, Copy, Default, PartialEq, Eq, Hash)]
30pub struct Decimal {
31 pub(crate) mantissa: i128,
32 pub(crate) scale: u8,
33}
34
35impl Decimal {
36 /// Additive identity: `0 × 10^0`.
37 pub const ZERO: Self = Self { mantissa: 0, scale: 0 };
38
39 /// Multiplicative identity: `1 × 10^0`.
40 pub const ONE: Self = Self { mantissa: 1, scale: 0 };
41
42 /// Maximum representable value: `i128::MAX × 10^0`.
43 pub const MAX: Self = Self { mantissa: i128::MAX, scale: 0 };
44
45 /// Minimum representable value: `i128::MIN × 10^0`.
46 pub const MIN: Self = Self { mantissa: i128::MIN, scale: 0 };
47
48 /// Negative one: `-1 × 10^0`.
49 pub const NEG_ONE: Self = Self { mantissa: -1, scale: 0 };
50
51 /// `100 × 10^0` — useful for percentage arithmetic.
52 pub const HUNDRED: Self = Self { mantissa: 100, scale: 0 };
53
54 /// `10_000 × 10^0` — basis-points denominator.
55 pub const TEN_THOUSAND: Self = Self { mantissa: 10_000, scale: 0 };
56
57 /// Construct a `Decimal` from a raw mantissa and scale.
58 ///
59 /// Returns `Err(ArithmeticError::ScaleExceeded)` if `scale > MAX_SCALE`.
60 #[inline]
61 pub fn new(mantissa: i128, scale: u8) -> Result<Self, ArithmeticError> {
62 if scale > MAX_SCALE {
63 return Err(ArithmeticError::ScaleExceeded);
64 }
65 Ok(Self { mantissa, scale })
66 }
67
68 /// Construct without validating scale. Caller must ensure `scale <= MAX_SCALE`.
69 ///
70 /// # Safety (logical)
71 /// This is not `unsafe` in the Rust sense, but violating the scale invariant
72 /// will cause incorrect arithmetic results. Prefer `Decimal::new`.
73 #[inline]
74 pub(crate) const fn new_unchecked(mantissa: i128, scale: u8) -> Self {
75 Self { mantissa, scale }
76 }
77
78 /// Returns the raw mantissa.
79 #[inline]
80 pub fn mantissa(self) -> i128 {
81 self.mantissa
82 }
83
84 /// Returns the scale (number of decimal places).
85 #[inline]
86 pub fn scale(self) -> u8 {
87 self.scale
88 }
89
90 /// Returns `true` if the value is exactly zero.
91 #[inline]
92 pub fn is_zero(self) -> bool {
93 self.mantissa == 0
94 }
95
96 /// Returns `true` if the value is strictly positive.
97 #[inline]
98 pub fn is_positive(self) -> bool {
99 self.mantissa > 0
100 }
101
102 /// Returns `true` if the value is strictly negative.
103 #[inline]
104 pub fn is_negative(self) -> bool {
105 self.mantissa < 0
106 }
107}