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}