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 {
38 mantissa: 0,
39 scale: 0,
40 };
41
42 /// Multiplicative identity: `1 × 10^0`.
43 pub const ONE: Self = Self {
44 mantissa: 1,
45 scale: 0,
46 };
47
48 /// Maximum representable value: `i128::MAX × 10^0`.
49 pub const MAX: Self = Self {
50 mantissa: i128::MAX,
51 scale: 0,
52 };
53
54 /// Minimum representable value: `i128::MIN × 10^0`.
55 pub const MIN: Self = Self {
56 mantissa: i128::MIN,
57 scale: 0,
58 };
59
60 /// Negative one: `-1 × 10^0`.
61 pub const NEG_ONE: Self = Self {
62 mantissa: -1,
63 scale: 0,
64 };
65
66 /// `100 × 10^0` — useful for percentage arithmetic.
67 pub const HUNDRED: Self = Self {
68 mantissa: 100,
69 scale: 0,
70 };
71
72 /// `10_000 × 10^0` — basis-points denominator.
73 pub const TEN_THOUSAND: Self = Self {
74 mantissa: 10_000,
75 scale: 0,
76 };
77
78 /// Construct a `Decimal` from a raw mantissa and scale.
79 ///
80 /// Returns `Err(ArithmeticError::ScaleExceeded)` if `scale > MAX_SCALE`.
81 #[inline]
82 pub fn new(mantissa: i128, scale: u8) -> Result<Self, ArithmeticError> {
83 if scale > MAX_SCALE {
84 return Err(ArithmeticError::ScaleExceeded);
85 }
86 Ok(Self { mantissa, scale })
87 }
88
89 /// Construct without validating scale. Caller must ensure `scale <= MAX_SCALE`.
90 ///
91 /// # Safety (logical)
92 /// This is not `unsafe` in the Rust sense, but violating the scale invariant
93 /// will cause incorrect arithmetic results. Prefer `Decimal::new`.
94 #[inline]
95 pub(crate) const fn new_unchecked(mantissa: i128, scale: u8) -> Self {
96 Self { mantissa, scale }
97 }
98
99 /// Returns the raw mantissa.
100 #[inline]
101 pub fn mantissa(self) -> i128 {
102 self.mantissa
103 }
104
105 /// Returns the scale (number of decimal places).
106 #[inline]
107 pub fn scale(self) -> u8 {
108 self.scale
109 }
110
111 /// Returns `true` if the value is exactly zero.
112 #[inline]
113 pub fn is_zero(self) -> bool {
114 self.mantissa == 0
115 }
116
117 /// Returns `true` if the value is strictly positive.
118 #[inline]
119 pub fn is_positive(self) -> bool {
120 self.mantissa > 0
121 }
122
123 /// Returns `true` if the value is strictly negative.
124 #[inline]
125 pub fn is_negative(self) -> bool {
126 self.mantissa < 0
127 }
128}