1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
use crate::StellarSdkError;
use core::convert::{AsRef, From, TryFrom, TryInto};
use sp_std::str;

pub const STROOPS_PER_LUMEN: i64 = 10_000_000;
pub struct LumenAmount(pub f64);
pub struct StroopAmount(pub i64);

pub trait IntoAmount {
    fn into_stroop_amount(self, allow_zero: bool) -> Result<i64, StellarSdkError>;
}

impl IntoAmount for StroopAmount {
    fn into_stroop_amount(self, allow_zero: bool) -> Result<i64, StellarSdkError> {
        if allow_zero {
            match self.0 < 0 {
                true => Err(StellarSdkError::AmountNegative),
                false => Ok(self.0),
            }
        } else {
            match self.0 <= 0 {
                true => Err(StellarSdkError::AmountNonPositive),
                false => Ok(self.0),
            }
        }
    }
}

impl IntoAmount for LumenAmount {
    fn into_stroop_amount(self, allow_zero: bool) -> Result<i64, StellarSdkError> {
        let stroop_amount: StroopAmount = self.try_into()?;
        stroop_amount.into_stroop_amount(allow_zero)
    }
}

impl TryFrom<LumenAmount> for StroopAmount {
    type Error = StellarSdkError;

    fn try_from(value: LumenAmount) -> Result<Self, Self::Error> {
        let float_stroops = value.0 * STROOPS_PER_LUMEN as f64;
        if float_stroops > i64::MAX as f64 {
            return Err(StellarSdkError::AmountOverflow);
        }

        Ok(StroopAmount(float_stroops as i64))
    }
}

impl From<StroopAmount> for LumenAmount {
    fn from(value: StroopAmount) -> Self {
        LumenAmount(value.0 as f64 / STROOPS_PER_LUMEN as f64)
    }
}

impl<T: AsRef<[u8]>> IntoAmount for T {
    fn into_stroop_amount(self, allow_zero: bool) -> Result<i64, StellarSdkError> {
        let string = self.as_ref();
        let seperator_position = string.iter().position(|char| *char == b'.');

        let (integer_part, decimals) = match seperator_position {
            Some(seperator_position) => {
                let decimals_length = string.len() - seperator_position - 1;
                if decimals_length > 7 {
                    return Err(StellarSdkError::InvalidAmountString);
                }
                let mut decimals = [b'0'; 7];
                decimals[..decimals_length].copy_from_slice(&string[seperator_position + 1..]);

                (&string[..seperator_position], parse_integer(&decimals)?)
            }
            None => (&string[..], 0),
        };

        let integer_part = parse_integer(integer_part)?;

        let result = match integer_part.checked_mul(STROOPS_PER_LUMEN) {
            Some(result) => result,
            None => return Err(StellarSdkError::AmountOverflow),
        };

        let result = match result.checked_add(decimals) {
            Some(result) => result,
            None => return Err(StellarSdkError::AmountOverflow),
        };

        if result == 0 && !allow_zero {
            return Err(StellarSdkError::AmountNonPositive);
        }

        Ok(result)
    }
}

fn parse_integer(slice: &[u8]) -> Result<i64, StellarSdkError> {
    if !slice.iter().all(|char| (*char as char).is_ascii_digit()) {
        return Err(StellarSdkError::InvalidAmountString);
    }
    let slice = str::from_utf8(slice).unwrap();
    slice
        .parse()
        .map_err(|_| StellarSdkError::InvalidAmountString)
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn parse_lumen_string() {
        assert_eq!("23".into_stroop_amount(true), Ok(230_000_000));
        assert_eq!(
            "922337203685".into_stroop_amount(true),
            Ok(9223372036850_000_000)
        );
        assert_eq!(
            "922337203686".into_stroop_amount(true),
            Err(StellarSdkError::AmountOverflow)
        );

        assert_eq!("0.23".into_stroop_amount(true), Ok(2_300_000));
        assert_eq!("0.232442".into_stroop_amount(true), Ok(2_324_420));
        assert_eq!("14.2324426".into_stroop_amount(true), Ok(142_324_426));
        assert_eq!(
            "14.23244267".into_stroop_amount(true),
            Err(StellarSdkError::InvalidAmountString)
        );

        assert_eq!("420.".into_stroop_amount(true), Ok(4200_000_000));

        // maximal value allowed in Stellar (max value that fits in a i64)
        assert_eq!(
            "922337203685.4775807".into_stroop_amount(true),
            Ok(9223372036854775807)
        );
        assert_eq!(
            "922337203685.4775807".into_stroop_amount(true),
            Ok(i64::MAX)
        );

        // one more stroop and it overflows
        assert_eq!(
            "922337203685.4775808".into_stroop_amount(true),
            Err(StellarSdkError::AmountOverflow)
        );

        assert_eq!(
            ".".into_stroop_amount(true),
            Err(StellarSdkError::InvalidAmountString)
        );
        assert_eq!(
            "".into_stroop_amount(true),
            Err(StellarSdkError::InvalidAmountString)
        );

        assert_eq!(
            "243. 34".into_stroop_amount(true),
            Err(StellarSdkError::InvalidAmountString)
        );
        assert_eq!(
            "243.+34".into_stroop_amount(true),
            Err(StellarSdkError::InvalidAmountString)
        );
        assert_eq!(
            "+243.34".into_stroop_amount(true),
            Err(StellarSdkError::InvalidAmountString)
        );
        assert_eq!(
            "243.34x".into_stroop_amount(true),
            Err(StellarSdkError::InvalidAmountString)
        );
        assert_eq!(
            "24?.34x".into_stroop_amount(true),
            Err(StellarSdkError::InvalidAmountString)
        );
    }
}