Skip to main content

xrpl_common_stdlib/types/
amount.rs

1use crate::fields::decoder::{FieldDecoder, FromCurrentTx, FromLedger};
2use crate::host;
3use crate::host::Error::InvalidParams;
4use crate::host::Result::{Err, Ok};
5use crate::types::account_id::AccountID;
6use crate::types::currency::Currency;
7use crate::types::decode_error::DecodeError;
8use crate::types::iou_number::IOUNumber;
9use crate::types::mpt_id::MptId;
10
11pub const AMOUNT_SIZE: usize = 48;
12
13/// A zero-cost abstraction for XRPL tokens. Tokens conform to the following binary layout:
14///
15/// ```markdown
16///              ┌────────────────────────────────────────────────────────────────────────────┐
17///              │                       XRP Amount (64 bits / 8 bytes)                       │
18///              ├────────────────────────────────────────────────────────────────────────────┤
19///              │                     ┌────────────────────────────────────────────────────┐ │
20///              │ ┌─┐┌─┐┌─┐ ┌─┬─┬─┬─┐ │ ┌────────────────────────────────────────────────┐ │ │
21///              │ │0││1││0│ │0│0│0│0│ │ │                      ...                       │ │ │
22///              │ └─┘└─┘└─┘ └─┴─┴─┴─┘ │ └────────────────────────────────────────────────┘ │ │
23///              │  ▲  ▲  ▲       ▲    │              Integer Drops (57 bits)               │ │
24///              │  │  │  │       │    └────────────────────────────────────────────────────┘ │
25///          ┌───┼──┘  │  └─────┐ └────────────────┐                                          │
26///          │   └─────┼────────┼──────────────────┼──────────────────────────────────────────┘
27///          │         │        │                  │
28/// ┌────────────────┐ │ ┌─────────────┐ ┌──────────────────┐
29/// │    Type Bit    │ │ │ Is MPT Bit  │ │     Reserved     │
30/// │(0=XRP/MPT;1=IOU│ │ │(1=MPT/0=XRP)│ └──────────────────┘
31/// └────────────────┘ │ └─────────────┘
32///           ┌────────────────┐
33///           │    Sign bit    │
34///           │(1 for positive)│
35///           └────────────────┘
36///
37///              ┌────────────────────────────────────────────────────────────────────────────┐
38///              │                       MPT Amount (264-bits/33-bytes)                       │
39///              ├────────────────────────────────────────────────────────────────────────────┤
40///              │                       ┌──────────┐ ┌────────────┐ ┌────────────────┐       │
41///              │ ┌─┐┌─┐┌─┐ ┌─┬─┬─┬─┬─┐ │┌────────┐│ │ ┌────────┐ │ │   ┌────────┐   │       │
42///              │ │0││1││1│ │0│0│0│0│0│ ││  ...   ││ │ │  ...   │ │ │   │  ...   │   │       │
43///              │ └─┘└─┘└─┘ └─┴─┴─┴─┴─┘ │└────────┘│ │ └────────┘ │ │   └────────┘   │       │
44///              │  ▲  ▲  ▲       ▲      │  Amount  │ │Sequence Num│ │Issuer AccountID│       │
45///              │  │  │  │       │      │(64 bits) │ │ (32 bits)  │ │   (160 bits)   │       │
46///          ┌───┼──┘  │  └────┐  │      └──────────┘ └────────────┘ └────────────────┘       │
47///          │   └─────┼───────┼──┼───────────────────────────────────────────────────────────┘
48///          │         │       │  └───────────────┐
49/// ┌─────────────────┐│┌─────────────┐           │
50/// │    Type Bit     │││ Is MPT Bit  │           │
51/// │(0=XRP/MPT;1=IOU)│││(1=MPT/0=XRP)│           │
52/// └─────────────────┘│└─────────────┘           │
53///           ┌────────────────┐        ┌──────────────────┐
54///           │    Sign bit    │        │     Reserved     │
55///           │(1 for positive)│        └──────────────────┘
56///           └────────────────┘
57///
58///
59///             ┌────────────────────────────────────────────────────────────────────────────────┐
60///             │                         IOU Amount (384-bits/48-bytes)                         │
61///             ├────────────────────────────────────────────────────────────────────────────────┤
62///             │       ┌─────────────────┐  ┌──────────────┐ ┌──────────────┐ ┌────────────────┐│
63///             │ ┌─┐┌─┐│┌─┬─┬─┬─┬─┬─┬─┬─┐│  │┌────────────┐│ │  ┌────────┐  │ │   ┌───────┐    ││
64///             │ │1││1│││0│0│0│0│0│0│0│0││  ││    ...     ││ │  │  ...   │  │ │   │  ...  │    ││
65///             │ └─┘└─┘│└─┴─┴─┴─┴─┴─┴─┴─┘│  │└────────────┘│ │  └────────┘  │ │   └───────┘    ││
66///             │  ▲  ▲ │Exponent (8 Bits)│  │Mantissa Bits │ │Currency Code │ │Issuer AccountID││
67///             │  │  │ └─────────────────┘  │  (54 Bits)   │ │  (160 bits)  │ │   (160 bits)   ││
68///             │  │  └────────────────┐     └──────────────┘ └──────────────┘ └────────────────┘│
69///             │  │                   │                                                         │
70///             └──┴───────────────────┴─────────────────────────────────────────────────────────┘
71///      ┌──────────────────┐┌──────────────────┐
72///      │     Type Bit     ││     Sign bit     │
73///      │(0=XRP/MPT;1=IOU) ││ (1 for positive) │
74///      └──────────────────┘└──────────────────┘
75/// ```
76///
77/// ## Derived Traits
78///
79/// - `PartialEq, Eq`: Enable comparisons and use in collections
80/// - `Debug, Clone`: Standard traits for development and consistency
81///
82/// Note: `Copy` is intentionally not derived due to the enum's size (48 bytes).
83#[derive(Debug, Clone, PartialEq, Eq)]
84#[repr(C)]
85pub enum Amount {
86    XRP {
87        // amount: Amount::XRP,
88        /// Design decision note: Per the pattern in `Amount`, we considered having this be an
89        /// unsigned u64 and adding an `is_positve` boolean to this variant. However, we decided to
90        /// break that pattern and instead use an i64 here for two reasons. First, this allows
91        /// simple math like `add`, `sub`, etc. to be performed in WASM without having to check for
92        /// negative values. Second, the total supply of XRP is capped at 100B XRP (100B * 1M Drops),
93        /// which fits just fine into an i64.
94        num_drops: i64,
95    },
96    IOU {
97        // amount: Amount::IOU,
98        amount: IOUNumber,
99        issuer: AccountID,
100        currency: Currency,
101    },
102    MPT {
103        // amount: MptAmount,
104        num_units: u64,
105        is_positive: bool, // not expected, but just in case.
106        mpt_id: MptId,
107    },
108}
109
110const MASK_57_BIT: u64 = 0x01FFFFFFFFFFFFFFu64;
111
112impl Amount {
113    /// Converts a Amount to STAmount bytes format.
114    ///
115    /// All Amount types return a 48-byte array for consistency with the XRPL STAmount format.
116    /// The format follows the XRPL binary layout:
117    /// - XRP: Raw drop amount with sign bit in first 8 bytes + 40 bytes padding
118    /// - MPT: Flag byte (0b_0110_0000) in byte 0, raw amount in bytes 1-9, MptId in bytes 9-33 + 15 bytes padding
119    /// - IOU: IOUNumber in first 8 bytes, Currency in bytes 8-28, AccountID in bytes 28-48
120    ///
121    /// Returns a tuple of (bytes, length) where length is always 48.
122    pub fn to_stamount_bytes(&self) -> ([u8; AMOUNT_SIZE], usize) {
123        let mut bytes = [0u8; AMOUNT_SIZE];
124
125        match self {
126            Amount::XRP { num_drops } => {
127                // For tracing, XRP encodes the drop amount with the sign bit
128                // Bit 6 is set to 1 for positive amounts, 0 for negative
129                let abs_drops = num_drops.unsigned_abs();
130                let mut value = abs_drops;
131                if *num_drops >= 0 {
132                    value |= 0x4000000000000000u64; // Set bit 6 for positive
133                }
134                bytes[0..8].copy_from_slice(&value.to_be_bytes());
135                // Remaining 40 bytes stay as zeros (padding)
136            }
137
138            Amount::MPT {
139                num_units,
140                is_positive,
141                mpt_id,
142            } => {
143                // MPT format for tracing: flag byte + amount + mpt_id
144                let mut control_byte = 0u8;
145
146                // Set the sign bit (bit 6)
147                if *is_positive {
148                    control_byte |= 0x40; // Set bit 6
149                }
150
151                // Set the is-MPT bit (bit 5)
152                control_byte |= 0x20; // Set bit 5
153
154                // Type bit (bit 7) is 0 for XRP/MPT - already 0
155                // Reserved bits (bits 4-0) are 0 - already 0
156
157                bytes[0] = control_byte;
158                bytes[1..9].copy_from_slice(&num_units.to_be_bytes());
159                bytes[9..33].copy_from_slice(mpt_id.as_bytes());
160                // Remaining 15 bytes stay as zeros (padding)
161            }
162
163            Amount::IOU {
164                amount,
165                issuer,
166                currency,
167            } => {
168                // IOU format for tracing: opaque float + currency + issuer
169                bytes[0..8].copy_from_slice(&amount.0);
170                bytes[8..28].copy_from_slice(currency.as_bytes());
171                bytes[28..48].copy_from_slice(&issuer.0);
172                // No padding needed - uses all 48 bytes
173            }
174        }
175
176        (bytes, AMOUNT_SIZE)
177    }
178
179    /// Parses a Amount from a byte array.
180    ///
181    /// The byte array can be one of three formats:
182    /// - XRP: 8 bytes
183    /// - MPT: 33 bytes
184    /// - IOU: 48 bytes
185    ///
186    /// Returns `Err(InvalidParams)` if the byte array is not a valid Amount.
187    pub fn from_bytes(bytes: &[u8]) -> host::Result<Self> {
188        // TODO: Move to trait!
189
190        if bytes.len() != 48 {
191            return Err(InvalidParams);
192        }
193
194        let byte0 = bytes[0]; // Get the first byte for flag extraction
195
196        // Extract flags using bitwise operations
197        let is_iou = byte0 & 0x80 == 0x80; // Bit 7 (Most Significant Bit)
198        let is_xrp_or_mpt = !is_iou;
199        let is_xrp: bool = byte0 & 0x20 == 0x00; // Bit 5 (only used if type_bit is 0)
200
201        let is_positive: bool = byte0 & 0x40 == 0x40; // Bit 6
202
203        if is_xrp_or_mpt {
204            if is_xrp {
205                // Only the first 8 bytes are meaningful; the rest is padding.
206
207                let mut amount_bytes = [0u8; 8];
208                amount_bytes.copy_from_slice(&bytes[0..8]);
209
210                // For XRP, we need to handle the first byte specially to mask out the flag bits
211                // and then use the remaining 7 bytes as is.
212                let num_drops_abs = u64::from_be_bytes(amount_bytes) & MASK_57_BIT;
213
214                let amount = Amount::XRP {
215                    num_drops: match is_positive {
216                        true => num_drops_abs as i64,
217                        false => -(num_drops_abs as i64),
218                    },
219                };
220
221                Ok(amount)
222            }
223            // is_mpt
224            else {
225                // Only the first 33 bytes are meaningful; the rest is padding.
226
227                // MPT amount: [0/type][1/sign][1/is-mpt][5/reserved][64/value]
228                let mut num_units_bytes = [0u8; 8];
229                // Skip the first MPT byte, which is control bytes. Grab the next 8 for the u64
230                num_units_bytes.copy_from_slice(&bytes[1..9]);
231                let num_units = u64::from_be_bytes(num_units_bytes);
232
233                // Parse the MptId from the remaining bytes
234                let mut mpt_id_bytes = [0u8; 24];
235                mpt_id_bytes.copy_from_slice(&bytes[9..33]);
236                let mpt_id = MptId::from(mpt_id_bytes);
237
238                let amount = Amount::MPT {
239                    num_units,
240                    is_positive,
241                    mpt_id,
242                };
243
244                Ok(amount)
245            }
246        }
247        // is_iou
248        else {
249            // IOU amounts are 48 bytes
250
251            // IOU amount: [1/type][1/sign][8/exponent][54/mantissa]
252            let iou_number_bytes: [u8; 8] = bytes[0..8].try_into().unwrap();
253            let iou_number: IOUNumber = iou_number_bytes.into();
254
255            // Parse the Currency from the next 20 bytes
256            let mut currency_bytes = [0u8; 20];
257            currency_bytes.copy_from_slice(&bytes[8..28]);
258            let currency = Currency::from(currency_bytes);
259
260            // Parse the AccountID from the last 20 bytes
261            let mut issuer_bytes = [0u8; 20];
262            issuer_bytes.copy_from_slice(&bytes[28..48]);
263            let issuer = AccountID::from(issuer_bytes);
264
265            let amount = Amount::IOU {
266                amount: iou_number,
267                issuer,
268                currency,
269            };
270
271            Ok(amount)
272        }
273    }
274}
275
276/// `FieldDecoder` for XRPL amount values. The host writes a variable number of bytes into the
277/// fixed `AMOUNT_SIZE` buffer — 8 for XRP, 33 for MPT, 48 for IOU — with the remainder left as
278/// `empty_buffer()`'s zero-padding, which is exactly the shape `Amount::from_bytes` wants, so it
279/// reads the buffer in place with no re-slice or re-copy.
280impl FieldDecoder for Amount {
281    type Buffer = [u8; AMOUNT_SIZE];
282
283    #[inline]
284    fn empty_buffer() -> Self::Buffer {
285        [0u8; AMOUNT_SIZE]
286    }
287
288    #[inline]
289    fn decode(buf: Self::Buffer, bytes_written: usize) -> core::result::Result<Self, DecodeError> {
290        // Unlike a fixed-size type, `Amount`'s variant is self-describing via the flag bits in
291        // byte 0, present regardless of how many bytes were written, so `bytes_written` can't be
292        // used to *pick* the variant. Parse the (zero-padded) buffer in place first, then confirm
293        // the host wrote exactly the number of bytes XRPL's wire format fixes for that variant
294        // (8 XRP / 33 MPT / 48 IOU). Trusting a `bytes_written` inconsistent with the parsed
295        // variant would mean silently accepting a truncated or malformed host response as a valid
296        // (but wrong) value rather than surfacing it as a decode error.
297        let amount = Amount::from_bytes(&buf).ok().ok_or(DecodeError)?;
298        let expected_len = match amount {
299            Amount::XRP { .. } => 8,
300            Amount::MPT { .. } => 33,
301            Amount::IOU { .. } => AMOUNT_SIZE,
302        };
303        if bytes_written != expected_len {
304            return core::result::Result::Err(DecodeError);
305        }
306        core::result::Result::Ok(amount)
307    }
308}
309
310impl FromCurrentTx for Amount {}
311impl FromLedger for Amount {}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316    use crate::types::iou_number::IOUNumber;
317
318    #[test]
319    fn test_parse_xrp_amount() {
320        // Create a test XRP amount byte array
321        // XRP amount: [0/type][1/sign][0/is-mpt][4/reserved][57/value]
322        // First byte: 0b0100_0000 (0x40)
323        // Value: 1,000,000 (0xF4240 in hex)
324        let mut bytes = [0u8; 48];
325        bytes[0] = 0x40; // XRP positive flag
326        bytes[1..8].copy_from_slice(&1_000_000u64.to_be_bytes()[1..8]);
327
328        // Parse the Amount
329        let amount = Amount::from_bytes(&bytes).unwrap();
330
331        // Verify it's an XRP amount with the correct value
332        match amount {
333            Amount::XRP { num_drops } => {
334                assert_eq!(num_drops, 1_000_000);
335            }
336            _ => panic!("Expected Amount::XRP"),
337        }
338    }
339
340    #[test]
341    fn test_parse_mpt_amount() {
342        // Create a test MPT amount byte array
343        // MPT amount: [0/type][1/sign][1/is-mpt][5/reserved][64/value][32/sequence][160/issuer]
344        // First byte: 0b0110_0000 (0x60)
345        const VALUE: u64 = 500_000; // 8 bytes
346        const SEQUENCE_NUM: u32 = 12345; // 4 bytes
347        const ISSUER_BYTES: [u8; 20] = [1u8; 20]; // 20 bytes
348
349        let mut bytes = [0u8; 48];
350
351        // Set the amount bytes
352        bytes[0] = 0x60; // MPT positive flag
353        bytes[1..9].copy_from_slice(&VALUE.to_be_bytes());
354
355        // Set the MptId bytes
356        bytes[9..13].copy_from_slice(&SEQUENCE_NUM.to_be_bytes());
357        // Set the Issuer bytes.
358        bytes[13..33].copy_from_slice(&ISSUER_BYTES);
359
360        // Parse the Amount
361        let amount = Amount::from_bytes(&bytes).unwrap();
362
363        // Verify it's an MPT amount with the correct values
364        match amount {
365            Amount::MPT {
366                num_units,
367                is_positive,
368                mpt_id,
369            } => {
370                assert_eq!(num_units, VALUE);
371                assert!(is_positive);
372                assert_eq!(mpt_id.get_sequence_num(), SEQUENCE_NUM);
373                assert_eq!(mpt_id.get_issuer(), AccountID::from(ISSUER_BYTES));
374            }
375            _ => panic!("Expected Amount::MPT"),
376        }
377    }
378
379    #[test]
380    fn test_parse_iou_amount() {
381        // IOU with exponent = 5, mantissa = 12345
382        const EXPONENT: u8 = 5; // 1 byte
383        const MANTISSA: u64 = 12345; // 57 bits (so need or 8 bytes)
384
385        // First byte: 0b1100_0000 (0xC0, flags for IOU positive)
386        // For exponent 5:
387        // - We need to set the last 6 bits of the first byte and first 2 bits of the second byte
388        // - 5 = 0b00000101, so we need 0b000001 in the last 6 bits of first byte
389        // - and 0b01 in the first 2 bits of second byte
390
391        // Create the input bytes
392        let mut input = [0u8; 9];
393        // Set the first byte: IOU positive flag (0xC0) with exponent bits
394        input[0] = 0xC0 | ((EXPONENT >> 2) & 0x3F); // 5 >> 2 = 1, so this is 0xC1
395
396        // Set the second byte: first 2 bits for exponent, rest will be part of mantissa
397        input[1] = (EXPONENT & 0x03) << 6; // 5 & 0x03 = 1, 1 << 6 = 0x40
398
399        let mantissa_bytes = MANTISSA.to_be_bytes();
400
401        // Copy the mantissa bytes to the input array, preserving the exponent bits in input[1]
402        // The mantissa starts from the last 6 bits of input[1], then goes for 6 more bytes.
403        input[1] |= mantissa_bytes[0] & 0x3F; // Keep first 2 bits for exponent, set last 6 bits from mantissa
404        input[2] = mantissa_bytes[1];
405        input[3] = mantissa_bytes[2];
406        input[4] = mantissa_bytes[3];
407        input[5] = mantissa_bytes[4];
408        input[6] = mantissa_bytes[5];
409        input[7] = mantissa_bytes[6];
410        // input[8] = mantissa_bytes[7]; // <-- Not necessary.
411
412        let mut eight_input_bytes: [u8; 8] = [0u8; 8];
413        eight_input_bytes.copy_from_slice(&input[..8]);
414
415        /////////////////
416        // Add the rest of the Amount Fields
417        /////////////////
418
419        // Create a test IOU amount byte array
420        // IOU amount: [1/type][1/sign][8/exponent][54/mantissa][160/currency][160/issuer]
421        // First byte: 0b1100_0000 (0xC0)
422
423        let mut bytes = [0u8; 48];
424
425        bytes[0..8].copy_from_slice(&eight_input_bytes[0..8]);
426
427        // Set the currency code bytes
428        const CURRENCY_BYTES: [u8; 20] = [2u8; 20]; // 20 bytes
429        bytes[8..28].copy_from_slice(&CURRENCY_BYTES);
430
431        // Set the issuer bytes
432        const ISSUER_BYTES: [u8; 20] = [3u8; 20]; // 20 bytes
433        bytes[28..48].copy_from_slice(&ISSUER_BYTES);
434
435        // Parse the Amount
436        let amount = Amount::from_bytes(&bytes).unwrap();
437
438        // Verify it's an IOU amount with the correct values
439        match amount {
440            Amount::IOU {
441                amount,
442                issuer,
443                currency,
444            } => {
445                assert_eq!(amount, IOUNumber(eight_input_bytes));
446                assert_eq!(issuer, AccountID::from(ISSUER_BYTES));
447                assert_eq!(currency, Currency::from(CURRENCY_BYTES));
448            }
449            _ => panic!("Expected Amount::IOU"),
450        }
451    }
452
453    #[test]
454    fn test_parse_invalid_amount() {
455        // A byte array whose length is not 48 is a caller/input error, reported as
456        // `InvalidParams` (not an internal invariant trip).
457        let expected = InvalidParams as i32;
458
459        // Test with an empty byte array
460        assert_eq!(Amount::from_bytes(&[]).err().unwrap().code(), expected);
461
462        // Test with a byte array that's too short for XRP
463        assert_eq!(
464            Amount::from_bytes(&[0x40, 0, 0]).err().unwrap().code(),
465            expected
466        );
467
468        // Test with a byte array that's too short for MPT
469        let mut mpt_bytes = [0u8; 20];
470        mpt_bytes[0] = 0x60; // MPT positive flag
471        assert_eq!(
472            Amount::from_bytes(&mpt_bytes).err().unwrap().code(),
473            expected
474        );
475
476        // Test with a byte array that's too short for IOU
477        let mut iou_bytes = [0u8; 30];
478        iou_bytes[0] = 0xC0; // IOU positive flag
479        assert_eq!(
480            Amount::from_bytes(&iou_bytes).err().unwrap().code(),
481            expected
482        );
483
484        // Test with an invalid type bit pattern
485        assert_eq!(
486            Amount::from_bytes(&[0xA0, 0, 0, 0, 0, 0, 0, 0])
487                .err()
488                .unwrap()
489                .code(),
490            expected
491        );
492    }
493
494    #[test]
495    fn test_round_trip_xrp_positive() {
496        // Test positive XRP amount
497        let original = Amount::XRP {
498            num_drops: 1_000_000,
499        };
500
501        // Create the expected byte layout for XRP
502        // XRP format: [0/type][1/sign][0/is-mpt][4/reserved][57/value]
503        let mut expected_bytes = [0u8; 48];
504        expected_bytes[0] = 0x40; // Positive XRP flag (0b0100_0000)
505        expected_bytes[1..8].copy_from_slice(&1_000_000u64.to_be_bytes()[1..8]);
506
507        // Test from_bytes -> to_bytes round trip
508        let parsed = Amount::from_bytes(&expected_bytes).unwrap();
509        assert_eq!(parsed, original);
510
511        // Test to_stamount_bytes format (should include sign bit for positive)
512        let (stamount_bytes, len) = original.to_stamount_bytes();
513        assert_eq!(len, 48);
514        let expected_value = 1_000_000u64 | 0x4000000000000000u64; // Add positive sign bit
515        assert_eq!(&stamount_bytes[0..8], &expected_value.to_be_bytes());
516        // Remaining bytes should be zero padding
517        assert_eq!(&stamount_bytes[8..48], &[0u8; 40]);
518    }
519
520    #[test]
521    fn test_round_trip_xrp_negative() {
522        // Test negative XRP amount
523        let original = Amount::XRP {
524            num_drops: -500_000,
525        };
526
527        // Create the expected byte layout for negative XRP
528        // XRP format: [0/type][0/sign][0/is-mpt][4/reserved][57/value]
529        let mut expected_bytes = [0u8; 48];
530        expected_bytes[0] = 0x00; // Negative XRP flag (0b0000_0000)
531        expected_bytes[1..8].copy_from_slice(&500_000u64.to_be_bytes()[1..8]);
532
533        // Test from_bytes -> to_bytes round trip
534        let parsed = Amount::from_bytes(&expected_bytes).unwrap();
535        assert_eq!(parsed, original);
536
537        // Test to_stamount_bytes format (should NOT include sign bit for negative)
538        let (stamount_bytes, len) = original.to_stamount_bytes();
539        assert_eq!(len, 48);
540        assert_eq!(&stamount_bytes[0..8], &500_000u64.to_be_bytes());
541        // Remaining bytes should be zero padding
542        assert_eq!(&stamount_bytes[8..48], &[0u8; 40]);
543    }
544
545    #[test]
546    fn test_round_trip_mpt_positive() {
547        // Test positive MPT amount
548        const VALUE: u64 = 750_000;
549        const SEQUENCE_NUM: u32 = 54321;
550        const ISSUER_BYTES: [u8; 20] = [0xAB; 20];
551
552        let issuer = AccountID::from(ISSUER_BYTES);
553        let mpt_id = MptId::new(SEQUENCE_NUM, issuer);
554        let original = Amount::MPT {
555            num_units: VALUE,
556            is_positive: true,
557            mpt_id,
558        };
559
560        // Create the expected byte layout for positive MPT
561        // MPT format: [0/type][1/sign][1/is-mpt][5/reserved][64/value][32/sequence][160/issuer]
562        let mut expected_bytes = [0u8; 48];
563        expected_bytes[0] = 0x60; // Positive MPT flag (0b0110_0000)
564        expected_bytes[1..9].copy_from_slice(&VALUE.to_be_bytes());
565        expected_bytes[9..13].copy_from_slice(&SEQUENCE_NUM.to_be_bytes());
566        expected_bytes[13..33].copy_from_slice(&ISSUER_BYTES);
567
568        // Test from_bytes -> to_bytes round trip
569        let parsed = Amount::from_bytes(&expected_bytes).unwrap();
570        assert_eq!(parsed, original);
571
572        // Test to_stamount_bytes format
573        let (stamount_bytes, len) = original.to_stamount_bytes();
574        assert_eq!(len, 48);
575        assert_eq!(stamount_bytes[0], 0x60); // Flag byte
576        assert_eq!(&stamount_bytes[1..9], &VALUE.to_be_bytes()); // Amount
577        assert_eq!(&stamount_bytes[9..33], mpt_id.as_bytes()); // MptId
578        // Remaining bytes should be zero padding
579        assert_eq!(&stamount_bytes[33..48], &[0u8; 15]);
580    }
581
582    #[test]
583    fn test_round_trip_mpt_negative() {
584        // Test negative MPT amount
585        const VALUE: u64 = 250_000;
586        const SEQUENCE_NUM: u32 = 98765;
587        const ISSUER_BYTES: [u8; 20] = [0xCD; 20];
588
589        let issuer = AccountID::from(ISSUER_BYTES);
590        let mpt_id = MptId::new(SEQUENCE_NUM, issuer);
591        let original = Amount::MPT {
592            num_units: VALUE,
593            is_positive: false,
594            mpt_id,
595        };
596
597        // Create the expected byte layout for negative MPT
598        // MPT format: [0/type][0/sign][1/is-mpt][5/reserved][64/value][32/sequence][160/issuer]
599        let mut expected_bytes = [0u8; 48];
600        expected_bytes[0] = 0x20; // Negative MPT flag (0b0010_0000)
601        expected_bytes[1..9].copy_from_slice(&VALUE.to_be_bytes());
602        expected_bytes[9..13].copy_from_slice(&SEQUENCE_NUM.to_be_bytes());
603        expected_bytes[13..33].copy_from_slice(&ISSUER_BYTES);
604
605        // Test from_bytes -> to_bytes round trip
606        let parsed = Amount::from_bytes(&expected_bytes).unwrap();
607        assert_eq!(parsed, original);
608
609        // Test to_stamount_bytes format
610        let (stamount_bytes, len) = original.to_stamount_bytes();
611        assert_eq!(len, 48);
612        assert_eq!(stamount_bytes[0], 0x20); // Flag byte (negative)
613        assert_eq!(&stamount_bytes[1..9], &VALUE.to_be_bytes()); // Amount
614        assert_eq!(&stamount_bytes[9..33], mpt_id.as_bytes()); // MptId
615        // Remaining bytes should be zero padding
616        assert_eq!(&stamount_bytes[33..48], &[0u8; 15]);
617    }
618
619    #[test]
620    fn test_round_trip_iou_positive() {
621        // Test positive IOU amount
622        const EXPONENT: u8 = 7;
623        const MANTISSA: u64 = 98765;
624        const CURRENCY_BYTES: [u8; 20] = [0xEF; 20];
625        const ISSUER_BYTES: [u8; 20] = [0x12; 20];
626
627        // Create the OpaqueFloat bytes manually
628        // IOU format: [1/type][1/sign][8/exponent][54/mantissa]
629        let mut iou_number_bytes = [0u8; 8];
630
631        // First byte: IOU positive flag (0xC0) with exponent bits
632        iou_number_bytes[0] = 0xC0 | ((EXPONENT >> 2) & 0x3F);
633
634        // Second byte: first 2 bits for exponent, rest will be part of mantissa
635        iou_number_bytes[1] = (EXPONENT & 0x03) << 6;
636
637        let mantissa_bytes = MANTISSA.to_be_bytes();
638
639        // Copy the mantissa bytes, preserving the exponent bits in iou_number_bytes[1]
640        iou_number_bytes[1] |= mantissa_bytes[0] & 0x3F;
641        iou_number_bytes[2] = mantissa_bytes[1];
642        iou_number_bytes[3] = mantissa_bytes[2];
643        iou_number_bytes[4] = mantissa_bytes[3];
644        iou_number_bytes[5] = mantissa_bytes[4];
645        iou_number_bytes[6] = mantissa_bytes[5];
646        iou_number_bytes[7] = mantissa_bytes[6];
647
648        let original = Amount::IOU {
649            amount: IOUNumber(iou_number_bytes),
650            issuer: AccountID::from(ISSUER_BYTES),
651            currency: Currency::from(CURRENCY_BYTES),
652        };
653
654        // Create the expected byte layout for IOU
655        // IOU format: [1/type][1/sign][8/exponent][54/mantissa][160/currency][160/issuer]
656        let mut expected_bytes = [0u8; 48];
657        expected_bytes[0..8].copy_from_slice(&iou_number_bytes);
658        expected_bytes[8..28].copy_from_slice(&CURRENCY_BYTES);
659        expected_bytes[28..48].copy_from_slice(&ISSUER_BYTES);
660
661        // Test from_bytes -> to_bytes round trip
662        let parsed = Amount::from_bytes(&expected_bytes).unwrap();
663        assert_eq!(parsed, original);
664
665        // Test to_stamount_bytes format
666        let (stamount_bytes, len) = original.to_stamount_bytes();
667        assert_eq!(len, 48);
668        assert_eq!(&stamount_bytes[0..8], &iou_number_bytes); // IOUNumber
669        assert_eq!(&stamount_bytes[8..28], &CURRENCY_BYTES); // Currency
670        assert_eq!(&stamount_bytes[28..48], &ISSUER_BYTES); // Issuer
671        // No padding for IOU - uses all 48 bytes
672    }
673
674    #[test]
675    fn test_round_trip_iou_negative() {
676        // Test negative IOU amount
677        const EXPONENT: u8 = 3;
678        const MANTISSA: u64 = 12345;
679        const CURRENCY_BYTES: [u8; 20] = [0x34; 20];
680        const ISSUER_BYTES: [u8; 20] = [0x56; 20];
681
682        // Create the IOUNumber bytes manually for negative amount
683        // IOU format: [1/type][0/sign][8/exponent][54/mantissa]
684        let mut iou_number_bytes = [0u8; 8];
685
686        // First byte: IOU negative flag (0x80) with exponent bits
687        iou_number_bytes[0] = 0x80 | ((EXPONENT >> 2) & 0x3F);
688
689        // Second byte: first 2 bits for exponent, rest will be part of mantissa
690        iou_number_bytes[1] = (EXPONENT & 0x03) << 6;
691
692        let mantissa_bytes = MANTISSA.to_be_bytes();
693
694        // Copy the mantissa bytes, preserving the exponent bits in iou_number_bytes[1]
695        iou_number_bytes[1] |= mantissa_bytes[0] & 0x3F;
696        iou_number_bytes[2] = mantissa_bytes[1];
697        iou_number_bytes[3] = mantissa_bytes[2];
698        iou_number_bytes[4] = mantissa_bytes[3];
699        iou_number_bytes[5] = mantissa_bytes[4];
700        iou_number_bytes[6] = mantissa_bytes[5];
701        iou_number_bytes[7] = mantissa_bytes[6];
702
703        let original = Amount::IOU {
704            amount: IOUNumber(iou_number_bytes),
705            issuer: AccountID::from(ISSUER_BYTES),
706            currency: Currency::from(CURRENCY_BYTES),
707        };
708
709        // Create the expected byte layout for negative IOU
710        // IOU format: [1/type][0/sign][8/exponent][54/mantissa][160/currency][160/issuer]
711        let mut expected_bytes = [0u8; 48];
712        expected_bytes[0..8].copy_from_slice(&iou_number_bytes);
713        expected_bytes[8..28].copy_from_slice(&CURRENCY_BYTES);
714        expected_bytes[28..48].copy_from_slice(&ISSUER_BYTES);
715
716        // Test from_bytes -> to_bytes round trip
717        let parsed = Amount::from_bytes(&expected_bytes).unwrap();
718        assert_eq!(parsed, original);
719
720        // Test to_stamount_bytes format
721        let (stamount_bytes, len) = original.to_stamount_bytes();
722        assert_eq!(len, 48);
723        assert_eq!(&stamount_bytes[0..8], &iou_number_bytes); // IOUNumber
724        assert_eq!(&stamount_bytes[8..28], &CURRENCY_BYTES); // Currency
725        assert_eq!(&stamount_bytes[28..48], &ISSUER_BYTES); // Issuer
726        // No padding for IOU - uses all 48 bytes
727    }
728
729    #[test]
730    fn test_round_trip_edge_cases() {
731        // Test XRP with maximum value that fits in 57 bits
732        let max_57_bit_value = MASK_57_BIT as i64;
733        let max_xrp = Amount::XRP {
734            num_drops: max_57_bit_value,
735        };
736        let mut max_xrp_bytes = [0u8; 48];
737
738        // Create the full 64-bit value with flag bits
739        let full_value = (max_57_bit_value as u64) | 0x4000000000000000u64; // Add positive flag
740        max_xrp_bytes[0..8].copy_from_slice(&full_value.to_be_bytes());
741
742        let parsed_max_xrp = Amount::from_bytes(&max_xrp_bytes).unwrap();
743        assert_eq!(parsed_max_xrp, max_xrp);
744
745        // Test XRP with maximum negative value that fits in 57 bits
746        let min_xrp = Amount::XRP {
747            num_drops: -max_57_bit_value,
748        };
749        let mut min_xrp_bytes = [0u8; 48];
750
751        // Create the full 64-bit value without positive flag (negative)
752        let full_value = max_57_bit_value as u64; // No positive flag = negative
753        min_xrp_bytes[0..8].copy_from_slice(&full_value.to_be_bytes());
754
755        let parsed_min_xrp = Amount::from_bytes(&min_xrp_bytes).unwrap();
756        assert_eq!(parsed_min_xrp, min_xrp);
757
758        // Test XRP with zero value
759        let zero_xrp = Amount::XRP { num_drops: 0 };
760        let mut zero_xrp_bytes = [0u8; 48];
761        zero_xrp_bytes[0] = 0x40; // Positive flag (zero is considered positive)
762
763        let parsed_zero_xrp = Amount::from_bytes(&zero_xrp_bytes).unwrap();
764        assert_eq!(parsed_zero_xrp, zero_xrp);
765
766        // Test that values larger than 57 bits get properly masked during parsing
767        let large_value = i64::MAX;
768        let expected_masked_value = (large_value as u64 & MASK_57_BIT) as i64;
769        let large_xrp = Amount::XRP {
770            num_drops: expected_masked_value,
771        };
772
773        let mut large_xrp_bytes = [0u8; 48];
774        // Create the full 64-bit value with XRP positive flag and the large value
775        let masked_value = (large_value as u64) & MASK_57_BIT;
776        let full_value = masked_value | 0x4000000000000000u64; // Add positive flag (bit 62)
777        large_xrp_bytes[0..8].copy_from_slice(&full_value.to_be_bytes());
778
779        let parsed_large_xrp = Amount::from_bytes(&large_xrp_bytes).unwrap();
780        assert_eq!(parsed_large_xrp, large_xrp);
781    }
782}