Skip to main content

bullet_exchange_interface/decimals/
positive_decimal.rs

1use std::io::Read;
2use std::ops::{Add, Div, Mul, Neg, Sub};
3use std::str::FromStr;
4
5use borsh::{BorshDeserialize, BorshSerialize};
6use rust_decimal::{Decimal, MathematicalOps};
7use serde::{Deserialize, Serialize};
8
9#[cfg(feature = "schema")]
10use super::SurrogateDecimal;
11use super::{FixedPositiveDecimal, RoundingMode, TryDecimalOps};
12use crate::error::{ArithmeticError, ArithmeticOperation, ConfigError};
13
14#[derive(
15    BorshSerialize,
16    Clone,
17    Copy,
18    Debug,
19    Default,
20    Deserialize,
21    Eq,
22    Hash,
23    Ord,
24    PartialEq,
25    PartialOrd,
26    Serialize,
27)]
28#[cfg_attr(feature = "schema", derive(sov_universal_wallet::UniversalWallet))]
29#[serde(into = "Decimal", try_from = "Decimal")]
30pub struct PositiveDecimal(
31    #[cfg_attr(feature = "schema", sov_wallet(as_ty = "SurrogateDecimal"))] Decimal,
32);
33
34impl std::fmt::Display for PositiveDecimal {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        write!(f, "{}", self.0)
37    }
38}
39
40impl FromStr for PositiveDecimal {
41    type Err = ConfigError;
42
43    fn from_str(s: &str) -> Result<Self, <Self as FromStr>::Err> {
44        let decimal = Decimal::try_from_str(s)?;
45        Self::try_from(decimal)
46    }
47}
48
49impl From<u8> for PositiveDecimal {
50    fn from(value: u8) -> Self {
51        Self(Decimal::from(value))
52    }
53}
54
55impl From<u16> for PositiveDecimal {
56    fn from(value: u16) -> Self {
57        Self(Decimal::from(value))
58    }
59}
60
61impl From<u32> for PositiveDecimal {
62    fn from(value: u32) -> Self {
63        Self(Decimal::from(value))
64    }
65}
66
67impl From<u64> for PositiveDecimal {
68    fn from(value: u64) -> Self {
69        Self(Decimal::from(value))
70    }
71}
72
73impl TryFrom<Decimal> for PositiveDecimal {
74    type Error = ConfigError;
75
76    fn try_from(value: Decimal) -> Result<Self, ConfigError> {
77        Self::new(value).ok_or_else(|| ConfigError::FailedToParseInput {
78            input: value.to_string(),
79            reason: format!(
80                "Provided decimal value for {value} cannot be converted to the underlying type (PositiveDecimal).",
81            ),
82        })
83    }
84}
85
86impl From<PositiveDecimal> for Decimal {
87    #[inline]
88    fn from(value: PositiveDecimal) -> Self {
89        value.0
90    }
91}
92
93impl BorshDeserialize for PositiveDecimal {
94    fn deserialize_reader<R: Read>(reader: &mut R) -> std::io::Result<Self> {
95        let decimal = Decimal::deserialize_reader(reader)?;
96        Self::new(decimal).ok_or_else(|| {
97            std::io::Error::new(
98                std::io::ErrorKind::InvalidData,
99                format!("PositiveDecimal cannot be negative: {decimal}"),
100            )
101        })
102    }
103}
104
105impl schemars::JsonSchema for PositiveDecimal {
106    fn schema_name() -> String {
107        "PositiveDecimal".to_string()
108    }
109
110    fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
111        Decimal::json_schema(generator)
112    }
113}
114
115impl TryDecimalOps for PositiveDecimal {
116    #[inline]
117    fn try_from_str(value: &str) -> Result<Self, ConfigError> {
118        Self::from_str(value)
119    }
120
121    #[inline]
122    fn try_add(self, v: impl Into<Decimal>) -> Result<Self, ArithmeticError> {
123        let v = v.into();
124
125        self.as_dec().checked_add(v).and_then(Self::new).ok_or_else(|| {
126            ArithmeticError::DecimalFailed {
127                operation: ArithmeticOperation::Addition,
128                left: self.as_dec(),
129                right: v,
130            }
131        })
132    }
133
134    #[inline]
135    fn try_sub(self, v: impl Into<Decimal>) -> Result<Self, ArithmeticError> {
136        let v = v.into();
137
138        // be able to find errors in tests
139        #[cfg(test)]
140        if self.as_dec().lt(&v) {
141            panic!("Unintended subtraction for PositiveDecimal: {} - {}", self, v);
142        }
143
144        self.as_dec().checked_sub(v).and_then(Self::new).ok_or_else(|| {
145            ArithmeticError::DecimalFailed {
146                operation: ArithmeticOperation::Subtraction,
147                left: self.as_dec(),
148                right: v,
149            }
150        })
151    }
152
153    #[inline]
154    fn try_mul(self, v: impl Into<Decimal>) -> Result<Self, ArithmeticError> {
155        let v = v.into();
156        if v.is_sign_negative() {
157            return Err(ArithmeticError::DecimalFailed {
158                operation: ArithmeticOperation::Multiplication,
159                left: self.as_dec(),
160                right: v,
161            });
162        }
163        self.as_dec().checked_mul(v).and_then(Self::new).ok_or_else(|| {
164            ArithmeticError::DecimalFailed {
165                operation: ArithmeticOperation::Multiplication,
166                left: self.as_dec(),
167                right: v,
168            }
169        })
170    }
171
172    #[inline]
173    fn try_div(self, v: impl Into<Decimal>) -> Result<Self, ArithmeticError> {
174        let v = v.into();
175        if v.is_sign_negative() {
176            return Err(ArithmeticError::DecimalFailed {
177                operation: ArithmeticOperation::Division,
178                left: self.as_dec(),
179                right: v,
180            });
181        }
182        self.as_dec().checked_div(v).and_then(Self::new).ok_or_else(|| {
183            ArithmeticError::DecimalFailed {
184                operation: ArithmeticOperation::Division,
185                left: self.as_dec(),
186                right: v,
187            }
188        })
189    }
190
191    #[inline]
192    fn try_exp(self) -> Result<Self, ArithmeticError> {
193        self.as_dec().checked_exp().and_then(Self::new).ok_or_else(|| {
194            ArithmeticError::DecimalFailed {
195                operation: ArithmeticOperation::Exponentiation,
196                left: self.as_dec(),
197                right: Decimal::ZERO,
198            }
199        })
200    }
201}
202
203impl PositiveDecimal {
204    /// The smallest positive value that can be represented by PositiveDecimal.
205    pub const ZERO: PositiveDecimal = PositiveDecimal(Decimal::ZERO);
206
207    pub const ONE: PositiveDecimal = PositiveDecimal(Decimal::ONE);
208
209    pub const TWO: PositiveDecimal = PositiveDecimal(Decimal::TWO);
210
211    pub const TEN: PositiveDecimal = PositiveDecimal(Decimal::TEN);
212
213    pub const ONE_HUNDRED: PositiveDecimal = PositiveDecimal(Decimal::ONE_HUNDRED);
214
215    pub const MAX: PositiveDecimal = PositiveDecimal(Decimal::MAX);
216
217    #[inline]
218    pub fn new(value: Decimal) -> Option<Self> {
219        if value.is_sign_positive() { Some(PositiveDecimal(value)) } else { None }
220    }
221
222    #[inline]
223    pub fn to_fixed(&self, rounding_mode: RoundingMode) -> FixedPositiveDecimal {
224        FixedPositiveDecimal::new(*self, rounding_mode)
225    }
226
227    #[inline]
228    pub fn as_dec(&self) -> Decimal {
229        self.0
230    }
231
232    #[inline]
233    pub fn is_zero(&self) -> bool {
234        self.0.is_zero()
235    }
236
237    #[inline]
238    pub fn fract(&self) -> Decimal {
239        self.0.fract()
240    }
241
242    #[inline]
243    pub fn try_pow_i64(&self, v: i64) -> Result<PositiveDecimal, ArithmeticError> {
244        self.as_dec().checked_powi(v).and_then(Self::new).ok_or_else(|| {
245            ArithmeticError::DecimalFailed {
246                operation: ArithmeticOperation::Exponentiation,
247                left: self.as_dec(),
248                right: Decimal::from(v),
249            }
250        })
251    }
252
253    #[inline]
254    pub fn try_with_precision(&self, precision: i64) -> Result<PositiveDecimal, ArithmeticError> {
255        let scaling_factor = Self::TEN.try_pow_i64(precision)?;
256        self.try_mul(scaling_factor)
257    }
258
259    /// Performs a saturating subtraction for PositiveDecimal, this is for intentional saturating
260    /// subtractions hence no warnings We don't return a result because we know the saturating
261    /// subtraction result will always be non-negative
262    #[inline]
263    pub fn saturating_sub(self, v: impl Into<Decimal>) -> Self {
264        let v = v.into();
265        if self.as_dec().lt(&v) {
266            Self::ZERO
267        } else {
268            // Safe because:
269            // 1. We checked self >= v above
270            // 2. Decimal subtraction can only fail on overflow
271            // 3. When subtracting a smaller number from a larger number, overflow is impossible
272            Self(self.as_dec().sub(v))
273        }
274    }
275}
276
277impl Neg for PositiveDecimal {
278    type Output = Decimal;
279
280    fn neg(self) -> Decimal {
281        self.as_dec().neg()
282    }
283}
284
285// These are mostly just for conveniece for tests, do not use in production
286impl Add for PositiveDecimal {
287    type Output = Self;
288
289    fn add(self, other: Self) -> Self::Output {
290        Self(self.as_dec().add(other.as_dec()))
291    }
292}
293
294// Can potentially be negative, so return a Decimal
295impl Sub for PositiveDecimal {
296    type Output = Decimal;
297
298    fn sub(self, other: Self) -> Self::Output {
299        self.as_dec().sub(other.as_dec())
300    }
301}
302
303impl Mul for PositiveDecimal {
304    type Output = Self;
305
306    fn mul(self, other: Self) -> Self::Output {
307        Self(self.as_dec().mul(other.as_dec()))
308    }
309}
310
311impl Div for PositiveDecimal {
312    type Output = Self;
313
314    fn div(self, other: Self) -> Self::Output {
315        Self(self.as_dec().div(other.as_dec()))
316    }
317}
318
319#[cfg(test)]
320mod tests {
321    use borsh::to_vec;
322
323    use super::*;
324
325    #[test]
326    fn serde_rejects_negative_decimal() {
327        let json = r#""-123.45""#;
328        let result = serde_json::from_str::<PositiveDecimal>(json);
329        assert!(result.is_err(), "Should reject negative decimal via serde");
330    }
331
332    #[test]
333    fn serde_accepts_positive_decimal() {
334        let json = r#""123.45""#;
335        let result = serde_json::from_str::<PositiveDecimal>(json);
336        assert!(result.is_ok(), "Should accept positive decimal via serde");
337        assert_eq!(result.unwrap().to_string(), "123.45");
338    }
339
340    #[test]
341    fn serde_accepts_zero() {
342        let json = r#""0""#;
343        let result = serde_json::from_str::<PositiveDecimal>(json);
344        assert!(result.is_ok(), "Should accept zero via serde");
345    }
346
347    #[test]
348    fn borsh_rejects_negative_decimal() {
349        // Serialize a negative Decimal directly, then try to deserialize as PositiveDecimal
350        let negative = Decimal::new(-12345, 2); // -123.45
351        let bytes = to_vec(&negative).expect("serialize negative decimal");
352
353        let result = PositiveDecimal::try_from_slice(&bytes);
354        assert!(result.is_err(), "Should reject negative decimal via borsh");
355    }
356
357    #[test]
358    fn borsh_accepts_positive_decimal() {
359        let positive = Decimal::new(12345, 2); // 123.45
360        let bytes = to_vec(&positive).expect("serialize positive decimal");
361
362        let result = PositiveDecimal::try_from_slice(&bytes);
363        assert!(result.is_ok(), "Should accept positive decimal via borsh");
364        assert_eq!(result.unwrap().to_string(), "123.45");
365    }
366
367    #[test]
368    fn borsh_roundtrip_preserves_value() {
369        let original = PositiveDecimal::new(Decimal::new(12345, 2)).unwrap();
370        let bytes = to_vec(&original).expect("serialize");
371        let restored = PositiveDecimal::try_from_slice(&bytes).expect("deserialize");
372        assert_eq!(original, restored);
373    }
374}