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()
126            .checked_add(v)
127            .and_then(Self::new)
128            .ok_or_else(|| ArithmeticError::DecimalFailed {
129                operation: ArithmeticOperation::Addition,
130                left: self.as_dec(),
131                right: v,
132            })
133    }
134
135    #[inline]
136    fn try_sub(self, v: impl Into<Decimal>) -> Result<Self, ArithmeticError> {
137        let v = v.into();
138
139        // be able to find errors in tests
140        #[cfg(test)]
141        if self.as_dec().lt(&v) {
142            panic!(
143                "Unintended subtraction for PositiveDecimal: {} - {}",
144                self, v
145            );
146        }
147
148        self.as_dec()
149            .checked_sub(v)
150            .and_then(Self::new)
151            .ok_or_else(|| ArithmeticError::DecimalFailed {
152                operation: ArithmeticOperation::Subtraction,
153                left: self.as_dec(),
154                right: v,
155            })
156    }
157
158    #[inline]
159    fn try_mul(self, v: impl Into<Decimal>) -> Result<Self, ArithmeticError> {
160        let v = v.into();
161        if v.is_sign_negative() {
162            return Err(ArithmeticError::DecimalFailed {
163                operation: ArithmeticOperation::Multiplication,
164                left: self.as_dec(),
165                right: v,
166            });
167        }
168        self.as_dec()
169            .checked_mul(v)
170            .and_then(Self::new)
171            .ok_or_else(|| ArithmeticError::DecimalFailed {
172                operation: ArithmeticOperation::Multiplication,
173                left: self.as_dec(),
174                right: v,
175            })
176    }
177
178    #[inline]
179    fn try_div(self, v: impl Into<Decimal>) -> Result<Self, ArithmeticError> {
180        let v = v.into();
181        if v.is_sign_negative() {
182            return Err(ArithmeticError::DecimalFailed {
183                operation: ArithmeticOperation::Division,
184                left: self.as_dec(),
185                right: v,
186            });
187        }
188        self.as_dec()
189            .checked_div(v)
190            .and_then(Self::new)
191            .ok_or_else(|| ArithmeticError::DecimalFailed {
192                operation: ArithmeticOperation::Division,
193                left: self.as_dec(),
194                right: v,
195            })
196    }
197
198    #[inline]
199    fn try_exp(self) -> Result<Self, ArithmeticError> {
200        self.as_dec()
201            .checked_exp()
202            .and_then(Self::new)
203            .ok_or_else(|| ArithmeticError::DecimalFailed {
204                operation: ArithmeticOperation::Exponentiation,
205                left: self.as_dec(),
206                right: Decimal::ZERO,
207            })
208    }
209}
210
211impl PositiveDecimal {
212    /// The smallest positive value that can be represented by PositiveDecimal.
213    pub const ZERO: PositiveDecimal = PositiveDecimal(Decimal::ZERO);
214
215    pub const ONE: PositiveDecimal = PositiveDecimal(Decimal::ONE);
216
217    pub const TWO: PositiveDecimal = PositiveDecimal(Decimal::TWO);
218
219    pub const TEN: PositiveDecimal = PositiveDecimal(Decimal::TEN);
220
221    pub const ONE_HUNDRED: PositiveDecimal = PositiveDecimal(Decimal::ONE_HUNDRED);
222
223    pub const MAX: PositiveDecimal = PositiveDecimal(Decimal::MAX);
224
225    #[inline]
226    pub fn new(value: Decimal) -> Option<Self> {
227        if value.is_sign_positive() {
228            Some(PositiveDecimal(value))
229        } else {
230            None
231        }
232    }
233
234    #[inline]
235    pub fn to_fixed(&self, rounding_mode: RoundingMode) -> FixedPositiveDecimal {
236        FixedPositiveDecimal::new(*self, rounding_mode)
237    }
238
239    #[inline]
240    pub fn as_dec(&self) -> Decimal {
241        self.0
242    }
243
244    #[inline]
245    pub fn is_zero(&self) -> bool {
246        self.0.is_zero()
247    }
248
249    #[inline]
250    pub fn fract(&self) -> Decimal {
251        self.0.fract()
252    }
253
254    #[inline]
255    pub fn try_pow_i64(&self, v: i64) -> Result<PositiveDecimal, ArithmeticError> {
256        self.as_dec()
257            .checked_powi(v)
258            .and_then(Self::new)
259            .ok_or_else(|| ArithmeticError::DecimalFailed {
260                operation: ArithmeticOperation::Exponentiation,
261                left: self.as_dec(),
262                right: Decimal::from(v),
263            })
264    }
265
266    #[inline]
267    pub fn try_with_precision(&self, precision: i64) -> Result<PositiveDecimal, ArithmeticError> {
268        let scaling_factor = Self::TEN.try_pow_i64(precision)?;
269        self.try_mul(scaling_factor)
270    }
271
272    /// Performs a saturating subtraction for PositiveDecimal, this is for intentional saturating subtractions hence no warnings
273    /// We don't return a result because we know the saturating subtraction result will always be non-negative
274    #[inline]
275    pub fn saturating_sub(self, v: impl Into<Decimal>) -> Self {
276        let v = v.into();
277        if self.as_dec().lt(&v) {
278            Self::ZERO
279        } else {
280            // Safe because:
281            // 1. We checked self >= v above
282            // 2. Decimal subtraction can only fail on overflow
283            // 3. When subtracting a smaller number from a larger number, overflow is impossible
284            Self(self.as_dec().sub(v))
285        }
286    }
287}
288
289impl Neg for PositiveDecimal {
290    type Output = Decimal;
291
292    fn neg(self) -> Decimal {
293        self.as_dec().neg()
294    }
295}
296
297// These are mostly just for conveniece for tests, do not use in production
298impl Add for PositiveDecimal {
299    type Output = Self;
300
301    fn add(self, other: Self) -> Self::Output {
302        Self(self.as_dec().add(other.as_dec()))
303    }
304}
305
306// Can potentially be negative, so return a Decimal
307impl Sub for PositiveDecimal {
308    type Output = Decimal;
309
310    fn sub(self, other: Self) -> Self::Output {
311        self.as_dec().sub(other.as_dec())
312    }
313}
314
315impl Mul for PositiveDecimal {
316    type Output = Self;
317
318    fn mul(self, other: Self) -> Self::Output {
319        Self(self.as_dec().mul(other.as_dec()))
320    }
321}
322
323impl Div for PositiveDecimal {
324    type Output = Self;
325
326    fn div(self, other: Self) -> Self::Output {
327        Self(self.as_dec().div(other.as_dec()))
328    }
329}
330
331#[cfg(test)]
332mod tests {
333    use borsh::to_vec;
334
335    use super::*;
336
337    #[test]
338    fn serde_rejects_negative_decimal() {
339        let json = r#""-123.45""#;
340        let result = serde_json::from_str::<PositiveDecimal>(json);
341        assert!(result.is_err(), "Should reject negative decimal via serde");
342    }
343
344    #[test]
345    fn serde_accepts_positive_decimal() {
346        let json = r#""123.45""#;
347        let result = serde_json::from_str::<PositiveDecimal>(json);
348        assert!(result.is_ok(), "Should accept positive decimal via serde");
349        assert_eq!(result.unwrap().to_string(), "123.45");
350    }
351
352    #[test]
353    fn serde_accepts_zero() {
354        let json = r#""0""#;
355        let result = serde_json::from_str::<PositiveDecimal>(json);
356        assert!(result.is_ok(), "Should accept zero via serde");
357    }
358
359    #[test]
360    fn borsh_rejects_negative_decimal() {
361        // Serialize a negative Decimal directly, then try to deserialize as PositiveDecimal
362        let negative = Decimal::new(-12345, 2); // -123.45
363        let bytes = to_vec(&negative).expect("serialize negative decimal");
364
365        let result = PositiveDecimal::try_from_slice(&bytes);
366        assert!(result.is_err(), "Should reject negative decimal via borsh");
367    }
368
369    #[test]
370    fn borsh_accepts_positive_decimal() {
371        let positive = Decimal::new(12345, 2); // 123.45
372        let bytes = to_vec(&positive).expect("serialize positive decimal");
373
374        let result = PositiveDecimal::try_from_slice(&bytes);
375        assert!(result.is_ok(), "Should accept positive decimal via borsh");
376        assert_eq!(result.unwrap().to_string(), "123.45");
377    }
378
379    #[test]
380    fn borsh_roundtrip_preserves_value() {
381        let original = PositiveDecimal::new(Decimal::new(12345, 2)).unwrap();
382        let bytes = to_vec(&original).expect("serialize");
383        let restored = PositiveDecimal::try_from_slice(&bytes).expect("deserialize");
384        assert_eq!(original, restored);
385    }
386}