substrate_stellar_sdk/
amount.rs

1use crate::StellarSdkError;
2use core::convert::{AsRef, From, TryFrom, TryInto};
3use sp_std::str;
4
5pub const STROOPS_PER_LUMEN: i64 = 10_000_000;
6pub struct LumenAmount(pub f64);
7pub struct StroopAmount(pub i64);
8
9pub trait IntoAmount {
10    fn into_stroop_amount(self, allow_zero: bool) -> Result<i64, StellarSdkError>;
11}
12
13impl IntoAmount for StroopAmount {
14    fn into_stroop_amount(self, allow_zero: bool) -> Result<i64, StellarSdkError> {
15        if allow_zero {
16            match self.0 < 0 {
17                true => Err(StellarSdkError::AmountNegative),
18                false => Ok(self.0),
19            }
20        } else {
21            match self.0 <= 0 {
22                true => Err(StellarSdkError::AmountNonPositive),
23                false => Ok(self.0),
24            }
25        }
26    }
27}
28
29impl IntoAmount for LumenAmount {
30    fn into_stroop_amount(self, allow_zero: bool) -> Result<i64, StellarSdkError> {
31        let stroop_amount: StroopAmount = self.try_into()?;
32        stroop_amount.into_stroop_amount(allow_zero)
33    }
34}
35
36impl TryFrom<LumenAmount> for StroopAmount {
37    type Error = StellarSdkError;
38
39    fn try_from(value: LumenAmount) -> Result<Self, Self::Error> {
40        let float_stroops = value.0 * STROOPS_PER_LUMEN as f64;
41        if float_stroops > i64::MAX as f64 {
42            return Err(StellarSdkError::AmountOverflow)
43        }
44
45        Ok(StroopAmount(float_stroops as i64))
46    }
47}
48
49impl From<StroopAmount> for LumenAmount {
50    fn from(value: StroopAmount) -> Self {
51        LumenAmount(value.0 as f64 / STROOPS_PER_LUMEN as f64)
52    }
53}
54
55impl<T: AsRef<[u8]>> IntoAmount for T {
56    fn into_stroop_amount(self, allow_zero: bool) -> Result<i64, StellarSdkError> {
57        let string = self.as_ref();
58        let seperator_position = string.iter().position(|char| *char == b'.');
59
60        let (integer_part, decimals) = match seperator_position {
61            Some(seperator_position) => {
62                let decimals_length = string.len() - seperator_position - 1;
63                if decimals_length > 7 {
64                    return Err(StellarSdkError::InvalidAmountString)
65                }
66                let mut decimals = [b'0'; 7];
67                decimals[..decimals_length].copy_from_slice(&string[seperator_position + 1..]);
68
69                (&string[..seperator_position], parse_integer(&decimals)?)
70            },
71            None => (&string[..], 0),
72        };
73
74        let integer_part = parse_integer(integer_part)?;
75
76        let result = match integer_part.checked_mul(STROOPS_PER_LUMEN) {
77            Some(result) => result,
78            None => return Err(StellarSdkError::AmountOverflow),
79        };
80
81        let result = match result.checked_add(decimals) {
82            Some(result) => result,
83            None => return Err(StellarSdkError::AmountOverflow),
84        };
85
86        if result == 0 && !allow_zero {
87            return Err(StellarSdkError::AmountNonPositive)
88        }
89
90        Ok(result)
91    }
92}
93
94fn parse_integer(slice: &[u8]) -> Result<i64, StellarSdkError> {
95    if !slice.iter().all(|char| (*char as char).is_ascii_digit()) {
96        return Err(StellarSdkError::InvalidAmountString)
97    }
98    let slice = str::from_utf8(slice).unwrap();
99    slice.parse().map_err(|_| StellarSdkError::InvalidAmountString)
100}
101
102#[cfg(test)]
103mod test {
104    use super::*;
105
106    #[test]
107    fn parse_lumen_string() {
108        assert_eq!("23".into_stroop_amount(true), Ok(230_000_000));
109        assert_eq!("922337203685".into_stroop_amount(true), Ok(9223372036850_000_000));
110        assert_eq!("922337203686".into_stroop_amount(true), Err(StellarSdkError::AmountOverflow));
111
112        assert_eq!("0.23".into_stroop_amount(true), Ok(2_300_000));
113        assert_eq!("0.232442".into_stroop_amount(true), Ok(2_324_420));
114        assert_eq!("14.2324426".into_stroop_amount(true), Ok(142_324_426));
115        assert_eq!("14.23244267".into_stroop_amount(true), Err(StellarSdkError::InvalidAmountString));
116
117        assert_eq!("420.".into_stroop_amount(true), Ok(4200_000_000));
118
119        // maximal value allowed in Stellar (max value that fits in a i64)
120        assert_eq!("922337203685.4775807".into_stroop_amount(true), Ok(9223372036854775807));
121        assert_eq!("922337203685.4775807".into_stroop_amount(true), Ok(i64::MAX));
122
123        // one more stroop and it overflows
124        assert_eq!("922337203685.4775808".into_stroop_amount(true), Err(StellarSdkError::AmountOverflow));
125
126        assert_eq!(".".into_stroop_amount(true), Err(StellarSdkError::InvalidAmountString));
127        assert_eq!("".into_stroop_amount(true), Err(StellarSdkError::InvalidAmountString));
128
129        assert_eq!("243. 34".into_stroop_amount(true), Err(StellarSdkError::InvalidAmountString));
130        assert_eq!("243.+34".into_stroop_amount(true), Err(StellarSdkError::InvalidAmountString));
131        assert_eq!("+243.34".into_stroop_amount(true), Err(StellarSdkError::InvalidAmountString));
132        assert_eq!("243.34x".into_stroop_amount(true), Err(StellarSdkError::InvalidAmountString));
133        assert_eq!("24?.34x".into_stroop_amount(true), Err(StellarSdkError::InvalidAmountString));
134    }
135}