Skip to main content

xrpl_common_stdlib/types/
number.rs

1use crate::fields::decoder::{FieldDecoder, FromCurrentTx, FromLedger, decode_exact};
2use crate::host;
3use crate::host::error_codes::match_result_code_with_expected_bytes;
4use crate::host::{Error, Result, RoundingMode};
5use crate::types::decode_error::DecodeError;
6
7/// The number of bytes in the serialized STNumber (float) representation.
8const NUMBER_SIZE: usize = 12;
9
10/// An opaque XRPL `STNumber` value: a decimal float represented as `mantissa × 10^exponent`.
11///
12/// The wire format is 12 bytes — an 8-byte big-endian `i64` mantissa followed by a 4-byte
13/// big-endian `i32` exponent. This is the representation every `float_*` host function consumes
14/// and produces, and it is distinct from the 8-byte
15/// [`IOUNumber`](crate::types::iou_number::IOUNumber) value carried inside an IOU `STAmount`.
16///
17/// # Important
18///
19/// This type is intentionally opaque: arithmetic and conversions MUST go through the host, which
20/// delegates to rippled's `Number` class so results stay exactly consensus-compatible. The bytes
21/// are only ever produced by the host, so callers cannot construct an out-of-range value.
22///
23/// `PartialEq`/`Eq` compare the raw bytes. The host canonicalizes every value it emits, so bytewise
24/// equality matches semantic equality for host-produced values.
25#[derive(Debug, Clone, PartialEq, Eq)]
26#[repr(C)]
27pub struct Number([u8; NUMBER_SIZE]);
28
29impl Number {
30    /// The value `0`.
31    pub const ZERO: Number = Number([0u8; NUMBER_SIZE]);
32
33    /// The value `1` (mantissa = 1,000,000,000,000,000,000, exponent = -18).
34    pub const ONE: Number = Number([
35        0x0D, 0xE0, 0xB6, 0xB3, 0xA7, 0x64, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xEE,
36    ]);
37
38    /// The value `-1` (mantissa = -1,000,000,000,000,000,000, exponent = -18).
39    pub const NEGATIVE_ONE: Number = Number([
40        0xF2, 0x1F, 0x49, 0x4C, 0x58, 0x9C, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xEE,
41    ]);
42
43    /// Converts a signed integer to a `Number`.
44    pub fn from_int(value: i64) -> Result<Number> {
45        let mut out = [0u8; NUMBER_SIZE];
46        let rescode = unsafe {
47            host::float_from_int(
48                value,
49                out.as_mut_ptr(),
50                NUMBER_SIZE,
51                RoundingMode::ToNearest.into(),
52            )
53        };
54        match_result_code_with_expected_bytes(rescode, NUMBER_SIZE, || Number(out))
55    }
56
57    /// Converts an unsigned integer to a `Number`.
58    pub fn from_uint(value: u64) -> Result<Number> {
59        // The host reads the unsigned value from a little-endian byte buffer (native order on the
60        // little-endian WASM target).
61        let value_bytes = value.to_le_bytes();
62        let mut out = [0u8; NUMBER_SIZE];
63        let rescode = unsafe {
64            host::float_from_uint(
65                value_bytes.as_ptr(),
66                value_bytes.len(),
67                out.as_mut_ptr(),
68                NUMBER_SIZE,
69                RoundingMode::ToNearest.into(),
70            )
71        };
72        match_result_code_with_expected_bytes(rescode, NUMBER_SIZE, || Number(out))
73    }
74
75    /// Constructs a `Number` from an explicit mantissa and exponent (`mantissa × 10^exponent`).
76    pub fn from_mant_exp(mantissa: i64, exponent: i32) -> Result<Number> {
77        let mut out = [0u8; NUMBER_SIZE];
78        let rescode = unsafe {
79            host::float_from_mant_exp(
80                mantissa,
81                exponent,
82                out.as_mut_ptr(),
83                NUMBER_SIZE,
84                RoundingMode::ToNearest.into(),
85            )
86        };
87        match_result_code_with_expected_bytes(rescode, NUMBER_SIZE, || Number(out))
88    }
89
90    /// Converts a serialized `STAmount` (e.g. from an amount field) to a `Number`.
91    pub fn from_stamount(bytes: &[u8]) -> Result<Number> {
92        let mut out = [0u8; NUMBER_SIZE];
93        let rescode = unsafe {
94            host::float_from_stamount(
95                bytes.as_ptr(),
96                bytes.len(),
97                out.as_mut_ptr(),
98                NUMBER_SIZE,
99                RoundingMode::ToNearest.into(),
100            )
101        };
102        match_result_code_with_expected_bytes(rescode, NUMBER_SIZE, || Number(out))
103    }
104
105    /// Converts a serialized `STNumber` (12-byte) to a `Number`.
106    pub fn from_stnumber(bytes: &[u8]) -> Result<Number> {
107        let mut out = [0u8; NUMBER_SIZE];
108        let rescode = unsafe {
109            host::float_from_stnumber(
110                bytes.as_ptr(),
111                bytes.len(),
112                out.as_mut_ptr(),
113                NUMBER_SIZE,
114                RoundingMode::ToNearest.into(),
115            )
116        };
117        match_result_code_with_expected_bytes(rescode, NUMBER_SIZE, || Number(out))
118    }
119
120    /// Converts this `Number` to a signed integer, rounding to nearest.
121    pub fn to_int(&self) -> Result<i64> {
122        let mut int_bytes = [0u8; 8];
123        let rescode = unsafe {
124            host::float_to_int(
125                self.0.as_ptr(),
126                self.0.len(),
127                int_bytes.as_mut_ptr(),
128                int_bytes.len(),
129                RoundingMode::ToNearest.into(),
130            )
131        };
132        match_result_code_with_expected_bytes(rescode, 8, || i64::from_le_bytes(int_bytes))
133    }
134
135    /// Decomposes this `Number` into its `(mantissa, exponent)` components.
136    ///
137    /// No rounding is applied — the value is already rounded/canonical.
138    pub fn to_mant_exp(&self) -> Result<(i64, i32)> {
139        let mut mant_bytes = [0u8; 8];
140        let mut exp_bytes = [0u8; 4];
141        let rescode = unsafe {
142            host::float_to_mant_exp(
143                self.0.as_ptr(),
144                self.0.len(),
145                mant_bytes.as_mut_ptr(),
146                mant_bytes.len(),
147                exp_bytes.as_mut_ptr(),
148                exp_bytes.len(),
149            )
150        };
151        match_result_code_with_expected_bytes(rescode, 8, || {
152            (
153                i64::from_le_bytes(mant_bytes),
154                i32::from_le_bytes(exp_bytes),
155            )
156        })
157    }
158
159    /// Compares this `Number` to another via the host, backing the [`Ord`]/[`PartialOrd`] impls.
160    fn compare_via_host(&self, other: &Number) -> Result<core::cmp::Ordering> {
161        let rescode = unsafe {
162            host::float_cmp(
163                self.0.as_ptr(),
164                self.0.len(),
165                other.0.as_ptr(),
166                other.0.len(),
167            )
168        };
169        match rescode {
170            0 => Result::Ok(core::cmp::Ordering::Equal),
171            1 => Result::Ok(core::cmp::Ordering::Greater),
172            2 => Result::Ok(core::cmp::Ordering::Less),
173            _ => Result::Err(Error::from_code(rescode)),
174        }
175    }
176
177    /// Returns `self + other`, rounding per `rounding`.
178    pub fn add(&self, other: &Number, rounding: RoundingMode) -> Result<Number> {
179        let mut out = [0u8; NUMBER_SIZE];
180        let rescode = unsafe {
181            host::float_add(
182                self.0.as_ptr(),
183                self.0.len(),
184                other.0.as_ptr(),
185                other.0.len(),
186                out.as_mut_ptr(),
187                NUMBER_SIZE,
188                rounding.into(),
189            )
190        };
191        match_result_code_with_expected_bytes(rescode, NUMBER_SIZE, || Number(out))
192    }
193
194    /// Returns `self - other`, rounding per `rounding`.
195    pub fn subtract(&self, other: &Number, rounding: RoundingMode) -> Result<Number> {
196        let mut out = [0u8; NUMBER_SIZE];
197        let rescode = unsafe {
198            host::float_sub(
199                self.0.as_ptr(),
200                self.0.len(),
201                other.0.as_ptr(),
202                other.0.len(),
203                out.as_mut_ptr(),
204                NUMBER_SIZE,
205                rounding.into(),
206            )
207        };
208        match_result_code_with_expected_bytes(rescode, NUMBER_SIZE, || Number(out))
209    }
210
211    /// Returns `self * other`, rounding per `rounding`.
212    pub fn multiply(&self, other: &Number, rounding: RoundingMode) -> Result<Number> {
213        let mut out = [0u8; NUMBER_SIZE];
214        let rescode = unsafe {
215            host::float_mult(
216                self.0.as_ptr(),
217                self.0.len(),
218                other.0.as_ptr(),
219                other.0.len(),
220                out.as_mut_ptr(),
221                NUMBER_SIZE,
222                rounding.into(),
223            )
224        };
225        match_result_code_with_expected_bytes(rescode, NUMBER_SIZE, || Number(out))
226    }
227
228    /// Returns `self / other`, rounding per `rounding`.
229    pub fn divide(&self, other: &Number, rounding: RoundingMode) -> Result<Number> {
230        let mut out = [0u8; NUMBER_SIZE];
231        let rescode = unsafe {
232            host::float_div(
233                self.0.as_ptr(),
234                self.0.len(),
235                other.0.as_ptr(),
236                other.0.len(),
237                out.as_mut_ptr(),
238                NUMBER_SIZE,
239                rounding.into(),
240            )
241        };
242        match_result_code_with_expected_bytes(rescode, NUMBER_SIZE, || Number(out))
243    }
244
245    /// Returns `self` raised to the integer power `n`, rounding per `rounding`.
246    pub fn pow(&self, n: i32, rounding: RoundingMode) -> Result<Number> {
247        let mut out = [0u8; NUMBER_SIZE];
248        let rescode = unsafe {
249            host::float_pow(
250                self.0.as_ptr(),
251                self.0.len(),
252                n,
253                out.as_mut_ptr(),
254                NUMBER_SIZE,
255                rounding.into(),
256            )
257        };
258        match_result_code_with_expected_bytes(rescode, NUMBER_SIZE, || Number(out))
259    }
260}
261
262impl From<[u8; NUMBER_SIZE]> for Number {
263    fn from(value: [u8; NUMBER_SIZE]) -> Self {
264        Number(value)
265    }
266}
267
268/// `FieldDecoder` for XRPL `STNumber` fields: decodes the 12-byte serialized value, failing if the
269/// host wrote a different number of bytes.
270///
271/// The bytes are taken as-is rather than routed back through `float_from_stnumber`: the host's float
272/// representation *is* the `STNumber` serialization (rippled's WASM host builds it with
273/// `STNumber::add`), so a value read off a transaction or ledger entry is already a canonical
274/// host-produced `Number`.
275impl FieldDecoder for Number {
276    type Buffer = [u8; NUMBER_SIZE];
277
278    #[inline]
279    fn empty_buffer() -> Self::Buffer {
280        [0u8; NUMBER_SIZE]
281    }
282
283    #[inline]
284    fn decode(buf: Self::Buffer, bytes_written: usize) -> core::result::Result<Self, DecodeError> {
285        decode_exact(buf, bytes_written)
286    }
287}
288
289impl FromCurrentTx for Number {}
290impl FromLedger for Number {}
291
292impl PartialOrd for Number {
293    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
294        Some(self.cmp(other))
295    }
296}
297
298impl Ord for Number {
299    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
300        // Comparison delegates to rippled's `Number` via the host. A well-formed `Number` (the only
301        // kind the host produces) always compares cleanly, so this cannot fail in practice.
302        self.compare_via_host(other).unwrap()
303    }
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309    use crate::host::host_bindings_trait::MockHostBindings;
310    use crate::host::setup_mock;
311
312    // An arbitrary well-formed 12-byte value used to stand in for a host-produced float.
313    const SAMPLE: [u8; NUMBER_SIZE] = [
314        0xD4, 0x91, 0xC3, 0x79, 0x37, 0xE0, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
315    ];
316
317    #[test]
318    fn test_from_int_success() {
319        let mut mock = MockHostBindings::new();
320        mock.expect_float_from_int()
321            .times(1)
322            .returning(|_, out, out_len, _| {
323                unsafe { out.copy_from_nonoverlapping(SAMPLE.as_ptr(), NUMBER_SIZE) }
324                out_len as i32
325            });
326        let _guard = setup_mock(mock);
327
328        assert_eq!(Number::from_int(42).unwrap(), Number(SAMPLE));
329    }
330
331    #[test]
332    fn test_from_int_host_error() {
333        let mut mock = MockHostBindings::new();
334        mock.expect_float_from_int()
335            .times(1)
336            .returning(|_, _, _, _| -19); // INVALID_FLOAT_INPUT
337        let _guard = setup_mock(mock);
338
339        assert!(Number::from_int(0).is_err());
340    }
341
342    #[test]
343    fn test_from_uint_success() {
344        let mut mock = MockHostBindings::new();
345        mock.expect_float_from_uint()
346            .times(1)
347            .returning(|_, _, out, out_len, _| {
348                unsafe { out.copy_from_nonoverlapping(SAMPLE.as_ptr(), NUMBER_SIZE) }
349                out_len as i32
350            });
351        let _guard = setup_mock(mock);
352
353        assert_eq!(Number::from_uint(42).unwrap(), Number(SAMPLE));
354    }
355
356    #[test]
357    fn test_from_mant_exp_success() {
358        let mut mock = MockHostBindings::new();
359        mock.expect_float_from_mant_exp()
360            .times(1)
361            .returning(|_, _, out, out_len, _| {
362                unsafe { out.copy_from_nonoverlapping(SAMPLE.as_ptr(), NUMBER_SIZE) }
363                out_len as i32
364            });
365        let _guard = setup_mock(mock);
366
367        assert_eq!(
368            Number::from_mant_exp(5_000_000_000_000_000, -15).unwrap(),
369            Number(SAMPLE)
370        );
371    }
372
373    #[test]
374    fn test_float_from_stamount_success() {
375        let mut mock = MockHostBindings::new();
376        mock.expect_float_from_stamount()
377            .times(1)
378            .returning(|_, _, out, out_len, _| {
379                unsafe { out.copy_from_nonoverlapping(SAMPLE.as_ptr(), NUMBER_SIZE) }
380                out_len as i32
381            });
382        let _guard = setup_mock(mock);
383
384        assert_eq!(Number::from_stamount(&[0u8; 48]).unwrap(), Number(SAMPLE));
385    }
386
387    #[test]
388    fn test_float_from_stnumber_success() {
389        let mut mock = MockHostBindings::new();
390        mock.expect_float_from_stnumber()
391            .times(1)
392            .returning(|_, _, out, out_len, _| {
393                unsafe { out.copy_from_nonoverlapping(SAMPLE.as_ptr(), NUMBER_SIZE) }
394                out_len as i32
395            });
396        let _guard = setup_mock(mock);
397
398        assert_eq!(
399            Number::from_stnumber(&[0u8; NUMBER_SIZE]).unwrap(),
400            Number(SAMPLE)
401        );
402    }
403
404    #[test]
405    fn test_float_to_int_success() {
406        let mut mock = MockHostBindings::new();
407        mock.expect_float_to_int()
408            .times(1)
409            .returning(|_, _, out, out_len, _| {
410                unsafe { out.copy_from_nonoverlapping(42i64.to_le_bytes().as_ptr(), 8) }
411                out_len as i32
412            });
413        let _guard = setup_mock(mock);
414
415        assert_eq!(Number(SAMPLE).to_int().unwrap(), 42);
416    }
417
418    #[test]
419    fn test_float_to_mant_exp_success() {
420        let mut mock = MockHostBindings::new();
421        mock.expect_float_to_mant_exp()
422            .times(1)
423            .returning(|_, _, mant, mant_len, exp, _| {
424                unsafe {
425                    mant.copy_from_nonoverlapping(123i64.to_le_bytes().as_ptr(), 8);
426                    exp.copy_from_nonoverlapping(5i32.to_le_bytes().as_ptr(), 4);
427                }
428                mant_len as i32
429            });
430        let _guard = setup_mock(mock);
431
432        assert_eq!(Number(SAMPLE).to_mant_exp().unwrap(), (123, 5));
433    }
434
435    #[test]
436    fn test_compare_orderings() {
437        for (code, expected) in [
438            (0, core::cmp::Ordering::Equal),
439            (1, core::cmp::Ordering::Greater),
440            (2, core::cmp::Ordering::Less),
441        ] {
442            let mut mock = MockHostBindings::new();
443            mock.expect_float_cmp()
444                .times(1)
445                .returning(move |_, _, _, _| code);
446            let _guard = setup_mock(mock);
447
448            assert_eq!(Number(SAMPLE).cmp(&Number(SAMPLE)), expected);
449        }
450    }
451
452    #[test]
453    fn test_compare_via_host_error() {
454        let mut mock = MockHostBindings::new();
455        mock.expect_float_cmp().times(1).returning(|_, _, _, _| -19);
456        let _guard = setup_mock(mock);
457
458        assert!(Number(SAMPLE).compare_via_host(&Number(SAMPLE)).is_err());
459    }
460
461    #[test]
462    fn test_zero_is_all_zeros() {
463        assert_eq!(Number::ZERO, Number([0u8; NUMBER_SIZE]));
464    }
465
466    // A second distinct 12-byte value, for the two-operand arithmetic mocks.
467    const OTHER: [u8; NUMBER_SIZE] = [
468        0xD4, 0x83, 0x8D, 0x7E, 0xA4, 0xC6, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
469    ];
470
471    #[test]
472    fn test_float_add_success() {
473        let mut mock = MockHostBindings::new();
474        mock.expect_float_add()
475            .times(1)
476            .returning(|_, _, _, _, out, out_len, _| {
477                unsafe { out.copy_from_nonoverlapping(SAMPLE.as_ptr(), NUMBER_SIZE) }
478                out_len as i32
479            });
480        let _guard = setup_mock(mock);
481
482        assert_eq!(
483            Number(SAMPLE)
484                .add(&Number(OTHER), RoundingMode::ToNearest)
485                .unwrap(),
486            Number(SAMPLE)
487        );
488    }
489
490    #[test]
491    fn test_float_sub_success() {
492        let mut mock = MockHostBindings::new();
493        mock.expect_float_sub()
494            .times(1)
495            .returning(|_, _, _, _, out, out_len, _| {
496                unsafe { out.copy_from_nonoverlapping(SAMPLE.as_ptr(), NUMBER_SIZE) }
497                out_len as i32
498            });
499        let _guard = setup_mock(mock);
500
501        assert_eq!(
502            Number(SAMPLE)
503                .subtract(&Number(OTHER), RoundingMode::ToNearest)
504                .unwrap(),
505            Number(SAMPLE)
506        );
507    }
508
509    #[test]
510    fn test_float_mult_success() {
511        let mut mock = MockHostBindings::new();
512        mock.expect_float_mult()
513            .times(1)
514            .returning(|_, _, _, _, out, out_len, _| {
515                unsafe { out.copy_from_nonoverlapping(SAMPLE.as_ptr(), NUMBER_SIZE) }
516                out_len as i32
517            });
518        let _guard = setup_mock(mock);
519
520        assert_eq!(
521            Number(SAMPLE)
522                .multiply(&Number(OTHER), RoundingMode::ToNearest)
523                .unwrap(),
524            Number(SAMPLE)
525        );
526    }
527
528    #[test]
529    fn test_float_div_success() {
530        let mut mock = MockHostBindings::new();
531        mock.expect_float_div()
532            .times(1)
533            .returning(|_, _, _, _, out, out_len, _| {
534                unsafe { out.copy_from_nonoverlapping(SAMPLE.as_ptr(), NUMBER_SIZE) }
535                out_len as i32
536            });
537        let _guard = setup_mock(mock);
538
539        assert_eq!(
540            Number(SAMPLE)
541                .divide(&Number(OTHER), RoundingMode::ToNearest)
542                .unwrap(),
543            Number(SAMPLE)
544        );
545    }
546
547    #[test]
548    fn test_float_div_host_error() {
549        let mut mock = MockHostBindings::new();
550        mock.expect_float_div()
551            .times(1)
552            .returning(|_, _, _, _, _, _, _| -20); // INVALID_FLOAT_COMPUTATION (e.g. divide by zero)
553        let _guard = setup_mock(mock);
554
555        assert!(
556            Number(SAMPLE)
557                .divide(&Number(OTHER), RoundingMode::ToNearest)
558                .is_err()
559        );
560    }
561
562    #[test]
563    fn test_float_pow_success() {
564        let mut mock = MockHostBindings::new();
565        mock.expect_float_pow()
566            .times(1)
567            .returning(|_, _, _, out, out_len, _| {
568                unsafe { out.copy_from_nonoverlapping(SAMPLE.as_ptr(), NUMBER_SIZE) }
569                out_len as i32
570            });
571        let _guard = setup_mock(mock);
572
573        assert_eq!(
574            Number(SAMPLE).pow(2, RoundingMode::ToNearest).unwrap(),
575            Number(SAMPLE)
576        );
577    }
578    // Test-only
579    // Builds the 12-byte `STNumber` wire value for a mantissa/exponent pair the way rippled's
580    // `STNumber::add` does: an 8-byte big-endian `i64` followed by a 4-byte big-endian `i32`.
581    fn wire(mantissa: i64, exponent: i32) -> [u8; NUMBER_SIZE] {
582        let mut bytes = [0u8; NUMBER_SIZE];
583        bytes[0..8].copy_from_slice(&mantissa.to_be_bytes());
584        bytes[8..12].copy_from_slice(&exponent.to_be_bytes());
585        bytes
586    }
587
588    #[test]
589    fn test_decode_matches_declared_constants() {
590        assert_eq!(
591            Number::decode(wire(0, 0), NUMBER_SIZE).unwrap(),
592            Number::ZERO
593        );
594        assert_eq!(
595            Number::decode(wire(1_000_000_000_000_000_000, -18), NUMBER_SIZE).unwrap(),
596            Number::ONE
597        );
598        assert_eq!(
599            Number::decode(wire(-1_000_000_000_000_000_000, -18), NUMBER_SIZE).unwrap(),
600            Number::NEGATIVE_ONE
601        );
602    }
603
604    #[test]
605    fn test_decode_roundtrips_wire_bytes() {
606        // Negative mantissa with a positive exponent, a positive mantissa with a negative one, and
607        // both extremes, to cover the sign bit of each half of the buffer.
608        for (mantissa, exponent) in [
609            (-7_654_321i64, 12i32),
610            (123i64, -7i32),
611            (i64::MIN, i32::MIN),
612            (i64::MAX, i32::MAX),
613        ] {
614            let bytes = wire(mantissa, exponent);
615            assert_eq!(Number::decode(bytes, NUMBER_SIZE).unwrap(), Number(bytes));
616        }
617    }
618
619    #[test]
620    fn test_decode_rejects_short_write() {
621        assert_eq!(Number::decode(wire(1, 0), 11), Err(DecodeError));
622        assert_eq!(Number::decode(wire(1, 0), 0), Err(DecodeError));
623    }
624
625    #[test]
626    fn test_empty_buffer_is_number_sized() {
627        assert_eq!(<Number as FieldDecoder>::empty_buffer(), [0u8; NUMBER_SIZE]);
628    }
629}