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}