Skip to main content

xrpl_common_stdlib/types/
iou_number.rs

1/// The 8-byte value field of an XRPL fungible token (IOU) amount.
2///
3/// This is the leading 8 bytes of a serialized IOU `STAmount` (the value, without the trailing
4/// currency and issuer). It is **not** an `STAmount`, and it is a different encoding from the
5/// 12-byte `STNumber` used for host arithmetic.
6///
7/// The format is `[Type:1][Sign:1][Exponent:8][Mantissa:54]` bits, big-endian.
8///
9/// # Important
10///
11/// This type is a read-only wire representation. Arithmetic MUST be performed through the host's
12/// `Number` (STNumber) type, which uses rippled's `Number` class to stay consensus-exact. The
13/// accessors below expose the raw encoded fields for inspection only.
14///
15/// # Format Details
16///
17/// - **Type bit** (bit 63): Always 1 for fungible tokens
18/// - **Sign bit** (bit 62): 1 = positive, 0 = negative
19/// - **Exponent** (bits 61-54): 8 bits, biased by 97 (real exponent range -96 to +80)
20/// - **Mantissa** (bits 53-0): 54 bits providing ~16 decimal digits precision
21///
22/// # Special Values
23///
24/// - Zero: `0x8000000000000000` (mantissa is 0)
25/// - Maximum: ~9.999999999999999 × 10^80
26/// - Minimum positive: ~1.0 × 10^-81
27///
28/// ## Derived Traits
29///
30/// - `PartialEq, Eq`: Enable comparisons (bitwise comparison only)
31/// - `Debug, Clone`: Standard traits for development and consistency
32///
33/// **Note**: `PartialEq` and `Eq` perform bitwise comparison only. For semantic comparison of
34/// amounts (e.g., handling different representations of zero), convert to `Number` and compare
35/// there.
36#[derive(Debug, Clone, PartialEq, Eq)]
37#[repr(C)]
38pub struct IOUNumber(pub [u8; 8]);
39
40/// The bias added to the real exponent to produce the 8-bit stored exponent.
41const EXPONENT_BIAS: i32 = 97;
42
43impl IOUNumber {
44    /// Returns `true` if the sign bit marks this value as positive.
45    ///
46    /// Note the sign bit is meaningless for zero; prefer [`IOUNumber::is_zero`] to test for zero.
47    pub fn is_positive(&self) -> bool {
48        // Sign bit is bit 62 -> bit 6 of the first big-endian byte.
49        self.0[0] & 0x40 == 0x40
50    }
51
52    /// Returns `true` if this value is zero (mantissa is 0).
53    pub fn is_zero(&self) -> bool {
54        self.mantissa() == 0
55    }
56
57    /// Returns the real (unbiased) base-10 exponent.
58    ///
59    /// WARNING: prefer the host `Number` type for arithmetic; this exposes the raw encoded field.
60    pub fn exponent(&self) -> i32 {
61        // The 8-bit stored exponent is the low 6 bits of byte 0 followed by the top 2 bits of
62        // byte 1. It is biased by `EXPONENT_BIAS`; subtract to recover the real exponent.
63        let stored = (((self.0[0] & 0x3F) as i32) << 2) | (((self.0[1] & 0xC0) as i32) >> 6);
64        stored - EXPONENT_BIAS
65    }
66
67    /// Returns the 54-bit unsigned mantissa.
68    ///
69    /// WARNING: prefer the host `Number` type for arithmetic; this exposes the raw encoded field.
70    pub fn mantissa(&self) -> u64 {
71        // The 54-bit mantissa is the low 6 bits of byte 1 followed by bytes 2..=7.
72        let top_6 = (self.0[1] & 0x3F) as u64;
73        (top_6 << 48)
74            | ((self.0[2] as u64) << 40)
75            | ((self.0[3] as u64) << 32)
76            | ((self.0[4] as u64) << 24)
77            | ((self.0[5] as u64) << 16)
78            | ((self.0[6] as u64) << 8)
79            | (self.0[7] as u64)
80    }
81}
82
83impl From<[u8; 8]> for IOUNumber {
84    fn from(value: [u8; 8]) -> Self {
85        IOUNumber(value)
86    }
87}
88
89/// The number `1` in XRPL's custom IOU float format.
90pub const FLOAT_ONE: [u8; 8] = [0xD4, 0x83, 0x8D, 0x7E, 0xA4, 0xC6, 0x80, 0x00];
91
92/// The number `-1` in XRPL's custom IOU float format.
93pub const FLOAT_NEGATIVE_ONE: [u8; 8] = [0x94, 0x83, 0x8D, 0x7E, 0xA4, 0xC6, 0x80, 0x00];
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn test_one_decodes() {
101        let one = IOUNumber(FLOAT_ONE);
102        assert!(one.is_positive());
103        assert!(!one.is_zero());
104        // 1 == 1_000_000_000_000_000 × 10^-15
105        assert_eq!(one.exponent(), -15);
106        assert_eq!(one.mantissa(), 1_000_000_000_000_000);
107    }
108
109    #[test]
110    fn test_negative_one_decodes() {
111        let neg_one = IOUNumber(FLOAT_NEGATIVE_ONE);
112        assert!(!neg_one.is_positive());
113        assert!(!neg_one.is_zero());
114        assert_eq!(neg_one.exponent(), -15);
115        assert_eq!(neg_one.mantissa(), 1_000_000_000_000_000);
116    }
117
118    #[test]
119    fn test_zero_is_zero() {
120        // Canonical IOU zero: type bit set, everything else 0.
121        let zero = IOUNumber([0x80, 0, 0, 0, 0, 0, 0, 0]);
122        assert!(zero.is_zero());
123        assert_eq!(zero.mantissa(), 0);
124    }
125
126    #[test]
127    fn test_exponent_mantissa_extraction() {
128        // Construct exponent = 5 (stored 102), mantissa = 12345, positive IOU.
129        const EXPONENT: u8 = 5;
130        const STORED: u8 = EXPONENT + EXPONENT_BIAS as u8; // 102
131        const MANTISSA: u64 = 12345;
132
133        let mut bytes = [0u8; 8];
134        // Type bit (0x80) + sign bit (0x40) + high 6 bits of stored exponent.
135        bytes[0] = 0xC0 | ((STORED >> 2) & 0x3F);
136        // Low 2 bits of stored exponent occupy the top of byte 1.
137        bytes[1] = (STORED & 0x03) << 6;
138
139        // The 54-bit mantissa is stored big-endian in byte 1's low 6 bits followed by bytes 2..=7.
140        bytes[1] |= ((MANTISSA >> 48) & 0x3F) as u8;
141        bytes[2] = ((MANTISSA >> 40) & 0xFF) as u8;
142        bytes[3] = ((MANTISSA >> 32) & 0xFF) as u8;
143        bytes[4] = ((MANTISSA >> 24) & 0xFF) as u8;
144        bytes[5] = ((MANTISSA >> 16) & 0xFF) as u8;
145        bytes[6] = ((MANTISSA >> 8) & 0xFF) as u8;
146        bytes[7] = (MANTISSA & 0xFF) as u8;
147
148        let num = IOUNumber(bytes);
149        assert!(num.is_positive());
150        assert_eq!(num.exponent(), EXPONENT as i32);
151        assert_eq!(num.mantissa(), MANTISSA);
152    }
153}