1use std::fmt;
14use std::fmt::Display;
15use std::str::FromStr;
16
17use rust_decimal::Decimal;
18use rust_decimal::prelude::FromPrimitive;
19
20fn strip_non_numeric(input: &str) -> String {
22 input
23 .chars()
24 .filter(|c| c.is_ascii_digit() || *c == '.' || *c == '-')
25 .collect()
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub struct MoneyAmount(pub Decimal);
41
42impl MoneyAmount {
43 #[must_use]
48 pub const fn scale(&self) -> u32 {
49 self.0.scale()
50 }
51
52 #[must_use]
56 pub const fn mantissa(&self) -> u128 {
57 self.0.mantissa().unsigned_abs()
58 }
59}
60
61#[derive(Debug, Clone, Copy, thiserror::Error)]
63#[non_exhaustive]
64pub enum MoneyAmountParseError {
65 #[error("Invalid number format")]
67 InvalidFormat,
68 #[error(
70 "Amount must be between {} and {}",
71 constants::MIN_STR,
72 constants::MAX_STR
73 )]
74 OutOfRange,
75 #[error("Negative value is not allowed")]
77 Negative,
78 #[error("Too big of a precision: {money} vs {token} on token")]
80 WrongPrecision {
81 money: u32,
83 token: u32,
85 },
86}
87
88mod constants {
89 use super::Decimal;
90
91 pub(super) const MIN_STR: &str = "0.000000001";
92 pub(super) const MAX_STR: &str = "999999999";
93
94 pub(super) const MIN: Decimal = Decimal::from_parts(1, 0, 0, false, 9);
96 pub(super) const MAX: Decimal = Decimal::from_parts(999_999_999, 0, 0, false, 0);
98}
99
100impl MoneyAmount {
101 pub fn parse(input: &str) -> Result<Self, MoneyAmountParseError> {
114 let cleaned = strip_non_numeric(input);
115
116 let parsed =
117 Decimal::from_str(&cleaned).map_err(|_| MoneyAmountParseError::InvalidFormat)?;
118
119 if parsed.is_sign_negative() {
120 return Err(MoneyAmountParseError::Negative);
121 }
122
123 if parsed < constants::MIN || parsed > constants::MAX {
124 return Err(MoneyAmountParseError::OutOfRange);
125 }
126
127 Ok(Self(parsed))
128 }
129}
130
131impl FromStr for MoneyAmount {
132 type Err = MoneyAmountParseError;
133
134 fn from_str(s: &str) -> Result<Self, Self::Err> {
135 Self::parse(s)
136 }
137}
138
139impl TryFrom<&str> for MoneyAmount {
140 type Error = MoneyAmountParseError;
141
142 fn try_from(value: &str) -> Result<Self, Self::Error> {
143 Self::from_str(value)
144 }
145}
146
147impl From<u128> for MoneyAmount {
148 fn from(value: u128) -> Self {
149 Self(Decimal::from(value))
150 }
151}
152
153impl TryFrom<f64> for MoneyAmount {
154 type Error = MoneyAmountParseError;
155
156 fn try_from(value: f64) -> Result<Self, Self::Error> {
157 let decimal = Decimal::from_f64(value).ok_or(MoneyAmountParseError::OutOfRange)?;
158 if decimal.is_sign_negative() {
159 return Err(MoneyAmountParseError::Negative);
160 }
161 if decimal < constants::MIN || decimal > constants::MAX {
162 return Err(MoneyAmountParseError::OutOfRange);
163 }
164 Ok(Self(decimal))
165 }
166}
167
168impl Display for MoneyAmount {
169 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170 write!(f, "{}", self.0.normalize())
171 }
172}
173
174pub trait ScaleFromMantissa: Sized {
180 fn from_mantissa_scaled(mantissa: u128, scale_diff: u32)
187 -> Result<Self, MoneyAmountParseError>;
188}
189
190impl ScaleFromMantissa for u64 {
191 fn from_mantissa_scaled(
192 mantissa: u128,
193 scale_diff: u32,
194 ) -> Result<Self, MoneyAmountParseError> {
195 let multiplier = 10u64
196 .checked_pow(scale_diff)
197 .ok_or(MoneyAmountParseError::OutOfRange)?;
198 let digits = Self::try_from(mantissa).map_err(|_| MoneyAmountParseError::OutOfRange)?;
199 digits
200 .checked_mul(multiplier)
201 .ok_or(MoneyAmountParseError::OutOfRange)
202 }
203}
204
205impl ScaleFromMantissa for u128 {
206 fn from_mantissa_scaled(
207 mantissa: u128,
208 scale_diff: u32,
209 ) -> Result<Self, MoneyAmountParseError> {
210 let multiplier = 10u128
211 .checked_pow(scale_diff)
212 .ok_or(MoneyAmountParseError::OutOfRange)?;
213 mantissa
214 .checked_mul(multiplier)
215 .ok_or(MoneyAmountParseError::OutOfRange)
216 }
217}
218
219impl MoneyAmount {
220 pub fn to_token_amount<T: ScaleFromMantissa>(
231 self,
232 decimals: u8,
233 ) -> Result<T, MoneyAmountParseError> {
234 let scale = self.scale();
235 let token_scale = u32::from(decimals);
236 if scale > token_scale {
237 return Err(MoneyAmountParseError::WrongPrecision {
238 money: scale,
239 token: token_scale,
240 });
241 }
242 let scale_diff = token_scale - scale;
243 T::from_mantissa_scaled(self.mantissa(), scale_diff)
244 }
245}