xrpl/utils/
xrpl_conversion.rs

1//! Conversions between XRP drops and native number types.
2
3use crate::utils::exceptions::XRPRangeException;
4use alloc::format;
5use alloc::string::String;
6use alloc::string::ToString;
7use bigdecimal::BigDecimal;
8use regex::Regex;
9use rust_decimal::prelude::*;
10use rust_decimal::Decimal;
11
12use super::exceptions::XRPLUtilsResult;
13
14/// Indivisible unit of XRP
15pub(crate) const _ONE_DROP: Decimal = Decimal::from_parts(1, 0, 0, false, 6);
16
17/// One drop in decimal form.
18pub const ONE_DROP: &str = "0.000001";
19/// 100 billion decimal XRP
20pub const MAX_XRP: u64 = u64::pow(10, 11);
21/// Maximum possible drops of XRP
22pub const MAX_DROPS: u64 = u64::pow(10, 17);
23/// Drops in one XRP
24pub const XRP_DROPS: u64 = 1000000;
25/// Minimum IC exponent
26pub const MIN_IOU_EXPONENT: i32 = -96;
27/// Maximum IC exponent
28pub const MAX_IOU_EXPONENT: i32 = 80;
29/// Maximum IC precision
30pub const MAX_IOU_PRECISION: u8 = 16;
31
32/// Checked remainder. Computes self % other, returning None if overflow occurred.
33fn checked_rem(first: &BigDecimal, second: &BigDecimal) -> Option<BigDecimal> {
34    // If second is zero, return None to avoid division by zero
35    if second.is_zero() {
36        return None;
37    }
38
39    // Perform the division and handle the case where there's no overflow
40    match checked_div(first, second) {
41        Some(div) => {
42            // Get the integer part of the division
43            let int_part = div.with_scale(0); // Truncate to integer
44
45            // Calculate remainder: remainder = first - (int_part * second)
46            let rem = first - &(int_part * second);
47
48            Some(rem)
49        }
50        None => None, // If the division fails, return None (overflow case)
51    }
52}
53
54/// Checked division. Computes self / other, returning None if overflow occurred.
55fn checked_div(first: &BigDecimal, second: &BigDecimal) -> Option<BigDecimal> {
56    // If second is zero, return None to avoid division by zero
57    if second.is_zero() {
58        return None;
59    }
60
61    // Perform the division and return Some(result) if successful
62    Some(first / second)
63}
64
65/// Checked multiplication. Computes self * other, returning None if overflow occurred.
66fn checked_mul(first: &BigDecimal, second: &BigDecimal) -> Option<BigDecimal> {
67    // Perform the multiplication
68    let result = first * second;
69
70    // Since BigDecimal supports arbitrary precision, we don't need to check for overflow.
71    // Simply return the result wrapped in Some.
72    Some(result)
73}
74
75/// TODO Make less bootleg
76/// Get the precision of a number.
77fn _calculate_precision(value: &str) -> XRPLUtilsResult<usize> {
78    let decimal = BigDecimal::from_str(value)?.normalized();
79    let regex = Regex::new("[^1-9]").expect("_calculate_precision");
80
81    if checked_rem(&decimal, &BigDecimal::one()).is_some() {
82        let stripped = regex
83            .replace(&decimal.to_string(), "")
84            .replace(['.', '0'], "");
85        Ok(stripped.len())
86    } else {
87        let quantized = decimal.with_scale(2);
88        let stripped = regex
89            .replace(&quantized.to_string(), "")
90            .replace(['.', '0'], "");
91        Ok(stripped.len())
92    }
93}
94
95/// Ensure that the value after being multiplied by the
96/// exponent does not contain a decimal.
97fn _verify_no_decimal(decimal: BigDecimal) -> XRPLUtilsResult<()> {
98    let (mantissa, scale) = decimal.as_bigint_and_exponent();
99    let decimal = BigDecimal::from_i64(scale).expect("_verify_no_decimal");
100
101    let value: String = if decimal == BigDecimal::zero() {
102        mantissa.to_string()
103    } else {
104        (&decimal * &decimal).to_string()
105        // decimal
106        //     .checked_mul(decimal)
107        //     .unwrap_or(Decimal::ZERO)
108        //     .to_string()
109    };
110
111    if value.contains('.') {
112        Err(XRPRangeException::InvalidValueContainsDecimal.into())
113    } else {
114        Ok(())
115    }
116}
117
118/// Convert a numeric XRP amount to drops of XRP.
119/// Return an equivalent amount in drops of XRP.
120///
121/// # Examples
122///
123/// ## Basic usage
124///
125/// ```
126/// use xrpl::utils::xrp_to_drops;
127/// use xrpl::utils::exceptions::{XRPRangeException, XRPLUtilsException};
128///
129/// let xrp: &str = "100.000001";
130/// let drops: String = "100000001".to_string();
131///
132/// let conversion: Option<String> = match xrp_to_drops(xrp) {
133///     Ok(xrp) => Some(xrp),
134///     Err(e) => match e {
135///         XRPLUtilsException::XRPRangeError(XRPRangeException::InvalidXRPAmountTooLarge { max: _, found: _ }) => None,
136///         XRPLUtilsException::XRPRangeError(XRPRangeException::InvalidXRPAmountTooSmall { min: _, found: _ }) => None,
137///         _ => None,
138///     },
139/// };
140///
141/// assert_eq!(Some(drops), conversion);
142/// ```
143pub fn xrp_to_drops(xrp: &str) -> XRPLUtilsResult<String> {
144    let xrp_d = Decimal::from_str(xrp)?;
145
146    if xrp_d < _ONE_DROP && xrp_d != Decimal::ZERO {
147        Err(XRPRangeException::InvalidXRPAmountTooSmall {
148            min: ONE_DROP.to_string(),
149            found: xrp.to_string(),
150        }
151        .into())
152    } else if xrp_d.gt(&Decimal::new(MAX_XRP as i64, 0)) {
153        Err(XRPRangeException::InvalidXRPAmountTooLarge {
154            max: MAX_XRP,
155            found: xrp.into(),
156        }
157        .into())
158    } else {
159        Ok(format!("{}", (xrp_d / _ONE_DROP).trunc()))
160    }
161}
162
163/// Convert from drops to decimal XRP.
164/// Return an equivalent amount of XRP from drops.
165///
166/// # Examples
167///
168/// ## Basic usage
169///
170/// ```
171/// use xrpl::utils::drops_to_xrp;
172/// use xrpl::utils::exceptions::{XRPRangeException, XRPLUtilsException};
173///
174/// let drops: &str = "100000000";
175/// let xrp: String = "100".to_string();
176///
177/// let conversion: Option<String> = match drops_to_xrp(drops) {
178///     Ok(xrp) => Some(xrp),
179///     Err(e) => match e {
180///         XRPLUtilsException::XRPRangeError(XRPRangeException::InvalidDropsAmountTooLarge { max: _, found: _ }) => None,
181///         _ => None,
182///     },
183/// };
184///
185/// assert_eq!(Some(xrp), conversion);
186/// ```
187pub fn drops_to_xrp(drops: &str) -> XRPLUtilsResult<String> {
188    let drops_d = Decimal::from_str(drops)?;
189    let xrp = drops_d * _ONE_DROP;
190
191    if xrp.gt(&Decimal::new(MAX_XRP as i64, 0)) {
192        Err(XRPRangeException::InvalidDropsAmountTooLarge {
193            max: MAX_XRP.to_string(),
194            found: drops.to_string(),
195        }
196        .into())
197    } else {
198        Ok(xrp.normalize().to_string())
199    }
200}
201
202/// Validate if a provided XRP amount is valid.
203///
204/// # Examples
205///
206/// ## Basic usage
207///
208/// ```
209/// use xrpl::utils::verify_valid_xrp_value;
210/// use xrpl::utils::exceptions::{XRPRangeException, XRPLUtilsException};
211///
212/// let valid: bool = match verify_valid_xrp_value("0.000001") {
213///     Ok(()) => true,
214///     Err(e) => match e {
215///         XRPLUtilsException::XRPRangeError(XRPRangeException::InvalidXRPAmountTooSmall { min: _, found: _ }) => false,
216///         XRPLUtilsException::XRPRangeError(XRPRangeException::InvalidXRPAmountTooLarge { max: _, found: _ }) => false,
217///         _ => false,
218///     },
219/// };
220///
221/// assert!(valid);
222/// ```
223pub fn verify_valid_xrp_value(xrp_value: &str) -> XRPLUtilsResult<()> {
224    let decimal = Decimal::from_str(xrp_value)?;
225    let max = Decimal::new(MAX_DROPS as i64, 0);
226
227    match decimal {
228        xrp if xrp.is_zero() => Ok(()),
229        xrp if xrp.ge(&_ONE_DROP) && xrp.le(&max) => Ok(()),
230        xrp if xrp.lt(&_ONE_DROP) => Err(XRPRangeException::InvalidXRPAmountTooSmall {
231            min: ONE_DROP.to_string(),
232            found: xrp.to_string(),
233        }
234        .into()),
235        xrp if xrp.gt(&max) => Err(XRPRangeException::InvalidDropsAmountTooLarge {
236            max: MAX_XRP.to_string(),
237            found: xrp.to_string(),
238        }
239        .into()),
240        // Should never occur
241        _ => Err(XRPRangeException::InvalidXRPAmount.into()),
242    }
243}
244
245/// Validates the format of an issued currency amount value.
246///
247/// # Examples
248///
249/// ## Basic usage
250///
251/// ```
252/// use xrpl::utils::verify_valid_ic_value;
253/// use xrpl::utils::exceptions::{XRPRangeException, XRPLUtilsException};
254///
255/// let valid: bool = match verify_valid_ic_value("1111111111111111.0") {
256///     Ok(()) => true,
257///     Err(e) => match e {
258///         XRPLUtilsException::XRPRangeError(XRPRangeException::InvalidICPrecisionTooSmall { min: _, found: _ }) => false,
259///         XRPLUtilsException::XRPRangeError(XRPRangeException::InvalidICPrecisionTooLarge { max: _, found: _ }) => false,
260///         _ => false,
261///     },
262/// };
263///
264/// assert!(valid);
265/// ```
266pub fn verify_valid_ic_value(ic_value: &str) -> XRPLUtilsResult<()> {
267    let decimal = BigDecimal::from_str(ic_value)?.normalized();
268    let scale = -(decimal.fractional_digit_count() as i32);
269    let prec = _calculate_precision(ic_value)?;
270
271    match decimal {
272        ic if ic.is_zero() => Ok(()),
273        _ if prec > MAX_IOU_PRECISION as usize || scale > MAX_IOU_EXPONENT => {
274            Err(XRPRangeException::InvalidICPrecisionTooLarge {
275                max: MAX_IOU_EXPONENT,
276                found: scale,
277            }
278            .into())
279        }
280        _ if prec > MAX_IOU_PRECISION as usize || scale < MIN_IOU_EXPONENT => {
281            Err(XRPRangeException::InvalidICPrecisionTooSmall {
282                min: MIN_IOU_EXPONENT,
283                found: scale,
284            }
285            .into())
286        }
287        _ => _verify_no_decimal(decimal),
288    }
289}
290
291#[cfg(test)]
292mod test {
293    use super::*;
294    use crate::alloc::string::ToString;
295    extern crate std;
296
297    #[test]
298    fn test_one_drop_decimal() {
299        assert_eq!(Ok(_ONE_DROP), Decimal::from_str("0.000001"));
300    }
301
302    #[test]
303    fn test_xrp_to_drops() {
304        assert_eq!(
305            Ok((100 * XRP_DROPS + 1).to_string()),
306            xrp_to_drops("100.000001")
307        );
308    }
309
310    #[test]
311    fn test_drops_to_xrp() {
312        assert_eq!(
313            drops_to_xrp("100000001"),
314            Ok(Decimal::new(100000001, 6).to_string())
315        );
316    }
317
318    #[test]
319    fn test_verify_valid_xrp_value() {
320        assert!(verify_valid_xrp_value(ONE_DROP).is_ok());
321        assert!(verify_valid_xrp_value(&MAX_DROPS.to_string()).is_ok());
322        assert!(verify_valid_xrp_value("0.0000001").is_err());
323        assert!(verify_valid_xrp_value("100000000000000001").is_err());
324    }
325
326    #[test]
327    fn test_verify_valid_ic_value() {
328        // { zero, pos, negative } * fractional, large, small
329        let valid = [
330            "0",
331            "0.0",
332            "1",
333            "1.1111",
334            "-1",
335            "-1.1",
336            "1111111111111111.0",
337            "-1111111111111111.0",
338            "0.00000000001",
339            "0.00000000001",
340            "-0.00000000001",
341            "0.001111111111111111",
342            "-0.001111111111111111",
343        ];
344
345        let invalid = ["-0.0011111111111111111"];
346
347        for case in valid {
348            assert!(verify_valid_ic_value(case).is_ok());
349        }
350
351        for case in invalid {
352            assert!(verify_valid_ic_value(case).is_err());
353        }
354    }
355
356    #[test]
357    fn accept_one_xrp() {
358        assert_eq!(xrp_to_drops("1"), Ok("1000000".to_string()));
359    }
360
361    #[test]
362    fn accept_zero_xrp() {
363        assert_eq!(xrp_to_drops("0"), Ok("0".to_string()));
364    }
365
366    #[test]
367    fn accept_min_xrp() {
368        assert_eq!(xrp_to_drops("0.000001"), Ok("1".to_string()));
369    }
370
371    #[test]
372    fn accept_max_xrp() {
373        assert_eq!(
374            xrp_to_drops(&MAX_XRP.to_string()),
375            Ok("100000000000000000".to_string())
376        );
377    }
378
379    #[test]
380    fn accept_too_small_xrp() {
381        assert!(xrp_to_drops("0.0000001").is_err());
382    }
383
384    #[test]
385    fn accept_too_big_xrp() {
386        let xrp = (MAX_XRP + 1).to_string();
387        assert!(xrp_to_drops(&xrp).is_err());
388    }
389
390    #[test]
391    fn accept_one_drop() {
392        assert_eq!(drops_to_xrp("1"), Ok(ONE_DROP.to_string()));
393    }
394
395    #[test]
396    fn accept_zero_drops() {
397        assert_eq!(drops_to_xrp("0"), Ok("0".to_string()));
398    }
399
400    #[test]
401    fn accept_1mil_drops() {
402        assert_eq!(drops_to_xrp("1000000"), Ok(Decimal::new(1, 0).to_string()));
403    }
404
405    #[test]
406    fn accept_max_drops() {
407        assert_eq!(
408            drops_to_xrp(&MAX_DROPS.to_string()),
409            Ok(Decimal::new(MAX_XRP as i64, 0).to_string())
410        );
411    }
412
413    #[test]
414    fn accept_too_big_drops() {
415        assert!(xrp_to_drops(&(MAX_XRP + 1).to_string()).is_err());
416    }
417}