xrpl_types/types/
amount.rs

1use crate::alloc::{format, string::ToString};
2use crate::{AccountId, CurrencyCode, Error};
3use core::fmt::Debug;
4
5/// Amount of XRP or issued token. See <https://xrpl.org/currency-formats.html#specifying-currency-amounts>
6/// and <https://xrpl.org/serialization.html#amount-fields>
7#[derive(Debug, Eq, PartialEq, Clone, Copy)]
8pub enum Amount {
9    Issued(IssuedAmount),
10    Drops(DropsAmount),
11}
12
13impl Amount {
14    pub fn drops(drops: u64) -> Result<Self, Error> {
15        Ok(Self::Drops(DropsAmount::from_drops(drops)?))
16    }
17
18    pub fn issued(
19        value: IssuedValue,
20        currency: CurrencyCode,
21        issuer: AccountId,
22    ) -> Result<Self, Error> {
23        Ok(Self::Issued(IssuedAmount::from_issued_value(
24            value, currency, issuer,
25        )?))
26    }
27
28    pub fn is_drops(&self) -> bool {
29        matches!(self, Amount::Drops(_))
30    }
31
32    pub fn is_issued(&self) -> bool {
33        matches!(self, Amount::Issued(_))
34    }
35}
36
37/// Amount of XRP in drops, see <https://xrpl.org/currency-formats.html#xrp-amounts>
38/// and <https://xrpl.org/serialization.html#amount-fields>
39#[derive(Debug, Eq, PartialEq, Clone, Copy)]
40// tuple element is private since it is validated when the DropsAmount value is created
41pub struct DropsAmount(u64);
42
43impl DropsAmount {
44    pub fn from_drops(drops: u64) -> Result<Self, Error> {
45        if drops & (0b11 << 62) != 0 {
46            return Err(Error::OutOfRange(
47                "Drop amounts cannot use the two must significant bits".to_string(),
48            ));
49        }
50        Ok(Self(drops))
51    }
52
53    /// Amount of XRP in drops
54    pub fn drops(&self) -> u64 {
55        self.0
56    }
57}
58
59/// Amount of issued token. See <https://xrpl.org/currency-formats.html#token-amounts>
60/// and <https://xrpl.org/serialization.html#amount-fields>
61#[derive(Debug, Eq, PartialEq, Clone, Copy)]
62pub struct IssuedAmount {
63    // fields are private since it is validated when the IssuedAmount value is created
64    value: IssuedValue,
65    currency: CurrencyCode,
66    issuer: AccountId,
67}
68
69impl IssuedAmount {
70    pub fn from_issued_value(
71        value: IssuedValue,
72        currency: CurrencyCode,
73        issuer: AccountId,
74    ) -> Result<Self, Error> {
75        if currency.is_xrp() {
76            return Err(Error::InvalidData(
77                "Issued amount cannot have XRP currency code".to_string(),
78            ));
79        }
80        Ok(Self {
81            value,
82            currency,
83            issuer,
84        })
85    }
86
87    /// Decimal representation of token amount, see <https://xrpl.org/serialization.html#amount-fields>
88    pub fn value(&self) -> IssuedValue {
89        self.value
90    }
91
92    /// Currency code, see <https://xrpl.org/serialization.html#amount-fields>
93    pub fn currency(&self) -> CurrencyCode {
94        self.currency
95    }
96
97    /// Issuer of token, see <https://xrpl.org/serialization.html#amount-fields>
98    pub fn issuer(&self) -> AccountId {
99        self.issuer
100    }
101}
102
103/// The value of issued amount, see <https://xrpl.org/serialization.html#token-amount-format>
104#[derive(Debug, Eq, PartialEq, Clone, Copy)]
105pub struct IssuedValue {
106    // fields are private since it is validated when the IssuedValue value is created
107    mantissa: i64,
108    exponent: i8,
109}
110
111impl IssuedValue {
112    /// Creates value from given mantissa and exponent. The created value will be normalized
113    /// according to <https://xrpl.org/serialization.html#token-amount-format>. If the value
114    /// cannot be represented, an error is returned.
115    pub fn from_mantissa_exponent(mantissa: i64, exponent: i8) -> Result<Self, Error> {
116        Self { mantissa, exponent }.normalize()
117    }
118
119    /// The value zero
120    pub fn zero() -> Self {
121        Self {
122            mantissa: 0,
123            exponent: 0,
124        }
125    }
126
127    /// Signed and normalized mantissa, see <https://xrpl.org/serialization.html#token-amount-format>
128    pub fn mantissa(&self) -> i64 {
129        self.mantissa
130    }
131
132    /// Normalized exponent, see <https://xrpl.org/serialization.html#token-amount-format>
133    pub fn exponent(&self) -> i8 {
134        self.exponent
135    }
136
137    /// Normalizes value into the ranges specified at <https://xrpl.org/serialization.html#token-amount-format>
138    fn normalize(self) -> Result<Self, Error> {
139        // rippled implementation: https://github.com/seelabs/rippled/blob/cecc0ad75849a1d50cc573188ad301ca65519a5b/src/ripple/protocol/impl/IOUAmount.cpp#L38
140
141        const MANTISSA_MIN: i64 = 1000000000000000;
142        const MANTISSA_MAX: i64 = 9999999999999999;
143        const EXPONENT_MIN: i8 = -96;
144        const EXPONENT_MAX: i8 = 80;
145
146        let mut exponent = self.exponent;
147        let (mut mantissa, negative) = match self.mantissa {
148            0 => {
149                return Ok(Self::zero());
150            }
151            1.. => (self.mantissa, false),
152            ..=-1 => (
153                self.mantissa.checked_neg().ok_or_else(|| {
154                    Error::OutOfRange("Specified mantissa cannot be i64::MIN".to_string())
155                })?,
156                true,
157            ),
158        };
159
160        while mantissa < MANTISSA_MIN && exponent > EXPONENT_MIN {
161            mantissa *= 10;
162            exponent -= 1;
163        }
164
165        while mantissa > MANTISSA_MAX && exponent < EXPONENT_MAX {
166            mantissa /= 10;
167            exponent += 1;
168        }
169
170        if mantissa > MANTISSA_MAX || exponent > EXPONENT_MAX {
171            return Err(Error::OutOfRange(format!(
172                "Issued value too big to be normalized: {:?}",
173                self
174            )));
175        }
176
177        if mantissa < MANTISSA_MIN || exponent < EXPONENT_MIN {
178            return Ok(Self::zero());
179        }
180
181        if negative {
182            mantissa = -mantissa;
183        }
184
185        Ok(Self { mantissa, exponent })
186    }
187}
188
189#[cfg(test)]
190mod test {
191    use super::*;
192    use ascii::AsciiChar;
193    use assert_matches::assert_matches;
194
195    #[test]
196    fn test_drops_amount() {
197        let amount = DropsAmount::from_drops(0).unwrap();
198        assert_eq!(amount.drops(), 0);
199        let amount = DropsAmount::from_drops(10000).unwrap();
200        assert_eq!(amount.drops(), 10000);
201        // test max value
202        let amount = DropsAmount::from_drops(u64::MAX >> 2).unwrap();
203        assert_eq!(amount.drops(), u64::MAX >> 2);
204    }
205
206    /// We cannot use the two first bits, those values are out of range
207    /// <https://xrpl.org/serialization.html#amount-fields>
208    #[test]
209    fn test_drops_amount_out_of_range() {
210        let result = DropsAmount::from_drops(1 << 62);
211        assert_matches!(result, Err(Error::OutOfRange(message)) => {
212            assert!(message.contains("Drop amounts cannot use the two must significant bits"), "message: {}", message);
213        });
214        let result = DropsAmount::from_drops(1 << 63);
215        assert_matches!(result, Err(Error::OutOfRange(message)) => {
216            assert!(message.contains("Drop amounts cannot use the two must significant bits"), "message: {}", message);
217        });
218    }
219
220    /// Issued amount with XRP currency code is not valid
221    #[test]
222    fn test_issued_amount_xrp() {
223        let result = IssuedAmount::from_issued_value(
224            IssuedValue::from_mantissa_exponent(1, 0).unwrap(),
225            CurrencyCode::xrp(),
226            AccountId::from_address("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn").unwrap(),
227        );
228        assert_matches!(result, Err(Error::InvalidData(message)) => {
229            assert!(message.contains("Issued amount cannot have XRP currency code"), "message: {}", message);
230        });
231    }
232
233    /// Test we can created issued amount with standard currency code
234    #[test]
235    fn test_issued_amount_standard() {
236        IssuedAmount::from_issued_value(
237            IssuedValue::from_mantissa_exponent(1, 0).unwrap(),
238            CurrencyCode::standard([AsciiChar::U, AsciiChar::S, AsciiChar::D]).unwrap(),
239            AccountId::from_address("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn").unwrap(),
240        )
241        .unwrap();
242    }
243
244    /// Test we can created issued amount with non-standard currency code
245    #[test]
246    fn test_issued_amount_non_standard() {
247        IssuedAmount::from_issued_value(
248            IssuedValue::from_mantissa_exponent(1, 0).unwrap(),
249            CurrencyCode::non_standard([
250                0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
251                0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
252            ])
253            .unwrap(),
254            AccountId::from_address("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn").unwrap(),
255        )
256        .unwrap();
257    }
258
259    #[test]
260    fn test_issued_value_zero() {
261        let value = IssuedValue::from_mantissa_exponent(0, 0).unwrap();
262        assert_eq!(value.mantissa(), 0);
263        assert_eq!(value.exponent(), 0);
264    }
265
266    #[test]
267    fn test_issued_value_one() {
268        let value = IssuedValue::from_mantissa_exponent(1, 0).unwrap();
269        assert_eq!(value.mantissa(), 1_000_000_000_000_000);
270        assert_eq!(value.exponent(), -15);
271    }
272
273    #[test]
274    fn test_issued_value_minus_one() {
275        let value = IssuedValue::from_mantissa_exponent(-1, 0).unwrap();
276        assert_eq!(value.mantissa(), -1_000_000_000_000_000);
277        assert_eq!(value.exponent(), -15);
278    }
279
280    /// Exponent is always zero for the value zero
281    #[test]
282    fn test_issued_value_zero_normalize_exponent() {
283        let value = IssuedValue::from_mantissa_exponent(0, 10).unwrap();
284        assert_eq!(value.mantissa(), 0);
285        assert_eq!(value.exponent(), 0);
286    }
287
288    /// Test mantissa is scaled up to the normalized range
289    #[test]
290    fn test_issued_value_scale_up() {
291        let value = IssuedValue::from_mantissa_exponent(123, 0).unwrap();
292        assert_eq!(value.mantissa(), 1_230_000_000_000_000);
293        assert_eq!(value.exponent(), -13);
294    }
295
296    /// Test mantissa is scaled down to the normalized range
297    #[test]
298    fn test_issued_value_scale_down() {
299        let value = IssuedValue::from_mantissa_exponent(1_230_000_000_000_000_000, 0).unwrap();
300        assert_eq!(value.mantissa(), 1_230_000_000_000_000);
301        assert_eq!(value.exponent(), 3);
302    }
303
304    /// Test negative value
305    #[test]
306    fn test_issued_value_negative() {
307        let value = IssuedValue::from_mantissa_exponent(-123, 0).unwrap();
308        assert_eq!(value.mantissa(), -1_230_000_000_000_000);
309        assert_eq!(value.exponent(), -13);
310    }
311
312    /// Test hitting the mantissa min value when scaling up
313    #[test]
314    fn test_issued_value_mantissa_min_scale_up() {
315        let value = IssuedValue::from_mantissa_exponent(1, 0).unwrap();
316        assert_eq!(value.mantissa(), 1_000_000_000_000_000);
317        assert_eq!(value.exponent(), -15);
318    }
319
320    /// Test hitting the mantissa min value when scaling down
321    #[test]
322    fn test_issued_value_mantissa_min_scale_down() {
323        let value = IssuedValue::from_mantissa_exponent(1_000_000_000_000_000_000, 0).unwrap();
324        assert_eq!(value.mantissa(), 1_000_000_000_000_000);
325        assert_eq!(value.exponent(), 3);
326    }
327
328    /// Test hitting the mantissa max value when scaling down
329    #[test]
330    fn test_issued_value_mantissa_max_scale_down() {
331        let value = IssuedValue::from_mantissa_exponent(999_999_999_999_999_900, 0).unwrap();
332        assert_eq!(value.mantissa(), 9_999_999_999_999_999);
333        assert_eq!(value.exponent(), 2);
334    }
335
336    /// Test hitting exponent max value when scaling down mantissa
337    #[test]
338    fn test_issued_value_exponent_max() {
339        let value = IssuedValue::from_mantissa_exponent(1_230_000_000_000_000_000, 77).unwrap();
340        assert_eq!(value.mantissa(), 1_230_000_000_000_000);
341        assert_eq!(value.exponent(), 80);
342    }
343
344    /// Test going over exponent max value when scaling down mantissa
345    #[test]
346    fn test_issued_value_out_of_range_too_big_mantissa() {
347        let result = IssuedValue::from_mantissa_exponent(1_000_000_000_000_000_000, 78);
348        assert_matches!(result, Err(Error::OutOfRange(message)) => {
349            assert!(message.contains("Issued value too big to be normalized"), "message: {}", message);
350        });
351    }
352
353    /// Test going over exponent max value
354    #[test]
355    fn test_issued_value_out_of_range_too_big_exponent() {
356        let result = IssuedValue::from_mantissa_exponent(1_000_000_000_000_000, 81);
357        assert_matches!(result, Err(Error::OutOfRange(message)) => {
358            assert!(message.contains("Issued value too big to be normalized"), "message: {}", message);
359        });
360    }
361
362    /// Test hitting exponent min value when scaling up mantissa
363    #[test]
364    fn test_issued_value_exponent_min() {
365        let value = IssuedValue::from_mantissa_exponent(123_000_000_000, -92).unwrap();
366        assert_eq!(value.mantissa(), 1_230_000_000_000_000);
367        assert_eq!(value.exponent(), -96);
368    }
369
370    /// Test going under exponent min value when scaling up mantissa. This
371    /// should result in value zero
372    #[test]
373    fn test_issued_value_non_zero_normalized_to_zero_mantissa_too_small() {
374        let value = IssuedValue::from_mantissa_exponent(123_000_000_000, -93).unwrap();
375        assert_eq!(value.mantissa(), 0);
376        assert_eq!(value.exponent(), 0);
377    }
378
379    /// Test going under exponent min value when scaling up mantissa. This
380    /// should result in value zero
381    #[test]
382    fn test_issued_value_non_zero_normalized_to_zero_exponent_too_small() {
383        let value = IssuedValue::from_mantissa_exponent(1_230_000_000_000_000, -97).unwrap();
384        assert_eq!(value.mantissa(), 0);
385        assert_eq!(value.exponent(), 0);
386    }
387
388    /// Test mantissa value that is `i64::MIN`
389    #[test]
390    fn test_issued_value_mantissa_i64_min() {
391        let result = IssuedValue::from_mantissa_exponent(i64::MIN, 0);
392        assert_matches!(result, Err(Error::OutOfRange(message)) => {
393            assert!(message.contains("Specified mantissa cannot be i64::MIN"), "message: {}", message);
394        });
395    }
396
397    #[test]
398    fn test_amount_drops() {
399        let amount = Amount::drops(0).unwrap();
400        assert!(amount.is_drops());
401    }
402
403    #[test]
404    fn test_amount_issued() {
405        let amount = Amount::issued(
406            IssuedValue::from_mantissa_exponent(1, 0).unwrap(),
407            CurrencyCode::standard([AsciiChar::U, AsciiChar::S, AsciiChar::D]).unwrap(),
408            AccountId::from_address("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn").unwrap(),
409        )
410        .unwrap();
411        assert!(amount.is_issued());
412    }
413}