Skip to main content

xrpl_wasm_stdlib/core/ledger_objects/
mod.rs

1pub mod account_root;
2pub mod array_object;
3pub mod current_escrow;
4pub mod escrow;
5pub mod traits;
6
7use crate::core::types::uint::{HASH160_SIZE, HASH192_SIZE, Hash160, Hash192};
8use crate::host::error_codes::{
9    match_result_code_with_expected_bytes, match_result_code_with_expected_bytes_optional,
10};
11use crate::host::{Result, get_current_ledger_obj_field, get_ledger_obj_field};
12
13/// Trait for types that can be retrieved from ledger object fields.
14///
15/// This trait provides a unified interface for retrieving typed data from XRPL ledger objects,
16/// replacing the previous collection of type-specific functions with a generic, type-safe approach.
17///
18/// ## Supported Types
19///
20/// The following types implement this trait:
21/// - `u8` - 8-bit unsigned integers (1 byte)
22/// - `u16` - 16-bit unsigned integers (2 bytes)
23/// - `u32` - 32-bit unsigned integers (4 bytes)
24/// - `u64` - 64-bit unsigned integers (8 bytes)
25/// - `AccountID` - 20-byte account identifiers
26/// - `Amount` - XRP amounts and token amounts (variable size, up to 48 bytes)
27/// - `Hash128` - 128-bit cryptographic hashes (16 bytes)
28/// - `Hash256` - 256-bit cryptographic hashes (32 bytes)
29/// - `Blob<N>` - Variable-length binary data (generic over buffer size `N`)
30///
31/// ## Usage Patterns
32///
33/// ```rust,no_run
34/// use xrpl_wasm_stdlib::core::ledger_objects::{ledger_object, current_ledger_object};
35/// use xrpl_wasm_stdlib::core::types::account_id::AccountID;
36/// use xrpl_wasm_stdlib::core::types::amount::Amount;
37/// use xrpl_wasm_stdlib::sfield;
38///
39/// fn example() {
40///   let slot = 0;
41///   // Get a required field from a specific ledger object
42///   let balance = ledger_object::get_field(slot, sfield::Balance.into()).unwrap();
43///   let account = ledger_object::get_field(slot, sfield::Account.into()).unwrap();
44///
45///   // Get an optional field from the current ledger object
46///   let flags = current_ledger_object::get_field_optional(sfield::Flags).unwrap();
47/// }
48/// ```
49///
50/// ## Error Handling
51///
52/// - Required field methods return `Result<T>` and error if the field is missing
53/// - Optional field methods return `Result<Option<T>>` and return `None` if the field is missing
54/// - All methods return appropriate errors for buffer size mismatches or other retrieval failures
55///
56/// ## Safety Considerations
57///
58/// - All implementations use appropriately sized buffers for their data types
59/// - Buffer sizes are validated against expected field sizes where applicable
60/// - Unsafe operations are contained within the host function calls
61pub trait LedgerObjectFieldGetter: Sized {
62    /// Get a required field from the current ledger object.
63    ///
64    /// # Arguments
65    ///
66    /// * `field_code` - The field code identifying which field to retrieve
67    ///
68    /// # Returns
69    ///
70    /// Returns a `Result<Self>` where:
71    /// * `Ok(Self)` - The field value for the specified field
72    /// * `Err(Error)` - If the field cannot be retrieved or has unexpected size
73    fn get_from_current_ledger_obj(field_code: i32) -> Result<Self>;
74
75    /// Get an optional field from the current ledger object.
76    ///
77    /// # Arguments
78    ///
79    /// * `field_code` - The field code identifying which field to retrieve
80    ///
81    /// # Returns
82    ///
83    /// Returns a `Result<Option<Self>>` where:
84    /// * `Ok(Some(Self))` - The field value for the specified field
85    /// * `Ok(None)` - If the field is not present
86    /// * `Err(Error)` - If the field cannot be retrieved or has unexpected size
87    fn get_from_current_ledger_obj_optional(field_code: i32) -> Result<Option<Self>>;
88
89    /// Get a required field from a specific ledger object.
90    ///
91    /// # Arguments
92    ///
93    /// * `register_num` - The register number holding the ledger object
94    /// * `field_code` - The field code identifying which field to retrieve
95    ///
96    /// # Returns
97    ///
98    /// Returns a `Result<Self>` where:
99    /// * `Ok(Self)` - The field value for the specified field
100    /// * `Err(Error)` - If the field cannot be retrieved or has unexpected size
101    fn get_from_ledger_obj(register_num: i32, field_code: i32) -> Result<Self>;
102
103    /// Get an optional field from a specific ledger object.
104    ///
105    /// # Arguments
106    ///
107    /// * `register_num` - The register number holding the ledger object
108    /// * `field_code` - The field code identifying which field to retrieve
109    ///
110    /// # Returns
111    ///
112    /// Returns a `Result<Option<Self>>` where:
113    /// * `Ok(Some(Self))` - The field value for the specified field
114    /// * `Ok(None)` - If the field is not present in the ledger object
115    /// * `Err(Error)` - If the field retrieval operation failed
116    fn get_from_ledger_obj_optional(register_num: i32, field_code: i32) -> Result<Option<Self>>;
117}
118
119/// Trait for types that can be retrieved as fixed-size fields from ledger objects.
120///
121/// This trait enables a generic implementation of `LedgerObjectFieldGetter` for all fixed-size
122/// unsigned integer types (u8, u16, u32, u64). Types implementing this trait must
123/// have a known, constant size in bytes.
124///
125/// # Implementing Types
126///
127/// - `u8` - 1 byte
128/// - `u16` - 2 bytes
129/// - `u32` - 4 bytes
130/// - `u64` - 8 bytes
131trait FixedSizeFieldType: Sized {
132    /// The size of this type in bytes
133    const SIZE: usize;
134}
135
136impl FixedSizeFieldType for u8 {
137    const SIZE: usize = 1;
138}
139
140impl FixedSizeFieldType for u16 {
141    const SIZE: usize = 2;
142}
143
144impl FixedSizeFieldType for u32 {
145    const SIZE: usize = 4;
146}
147
148impl FixedSizeFieldType for u64 {
149    const SIZE: usize = 8;
150}
151
152/// Generic implementation of `LedgerObjectFieldGetter` for all fixed-size unsigned integer types.
153///
154/// This single implementation handles u8, u16, u32, and u64 by leveraging the
155/// `FixedSizeFieldType` trait. The implementation:
156/// - Allocates a buffer of the appropriate size
157/// - Calls the host function to retrieve the field
158/// - Validates that the returned byte count matches the expected size
159/// - Converts the buffer to the target type
160///
161/// # Buffer Management
162///
163/// Uses `MaybeUninit` for efficient stack allocation without initialization overhead.
164/// The buffer size is determined at compile-time via the `SIZE` constant.
165impl<T: FixedSizeFieldType> LedgerObjectFieldGetter for T {
166    #[inline]
167    fn get_from_current_ledger_obj(field_code: i32) -> Result<Self> {
168        let mut value = core::mem::MaybeUninit::<T>::uninit();
169        let result_code =
170            unsafe { get_current_ledger_obj_field(field_code, value.as_mut_ptr().cast(), T::SIZE) };
171        match_result_code_with_expected_bytes(result_code, T::SIZE, || unsafe {
172            value.assume_init()
173        })
174    }
175
176    #[inline]
177    fn get_from_current_ledger_obj_optional(field_code: i32) -> Result<Option<Self>> {
178        let mut value = core::mem::MaybeUninit::<T>::uninit();
179        let result_code =
180            unsafe { get_current_ledger_obj_field(field_code, value.as_mut_ptr().cast(), T::SIZE) };
181        match_result_code_with_expected_bytes_optional(result_code, T::SIZE, || {
182            Some(unsafe { value.assume_init() })
183        })
184    }
185
186    #[inline]
187    fn get_from_ledger_obj(register_num: i32, field_code: i32) -> Result<Self> {
188        let mut value = core::mem::MaybeUninit::<T>::uninit();
189        let result_code = unsafe {
190            get_ledger_obj_field(register_num, field_code, value.as_mut_ptr().cast(), T::SIZE)
191        };
192        match_result_code_with_expected_bytes(result_code, T::SIZE, || unsafe {
193            value.assume_init()
194        })
195    }
196
197    #[inline]
198    fn get_from_ledger_obj_optional(register_num: i32, field_code: i32) -> Result<Option<Self>> {
199        let mut value = core::mem::MaybeUninit::<T>::uninit();
200        let result_code = unsafe {
201            get_ledger_obj_field(register_num, field_code, value.as_mut_ptr().cast(), T::SIZE)
202        };
203        match_result_code_with_expected_bytes_optional(result_code, T::SIZE, || {
204            Some(unsafe { value.assume_init() })
205        })
206    }
207}
208
209/// Implementation of `LedgerObjectFieldGetter` for 160-bit cryptographic hashes.
210///
211/// This implementation handles 20-byte hash fields in XRPL ledger objects.
212/// Hash160 values are used for various cryptographic operations and identifiers.
213///
214/// # Buffer Management
215///
216/// Uses a 20-byte buffer (HASH160_SIZE) and validates that exactly 20 bytes
217/// are returned from the host function to ensure data integrity.
218impl LedgerObjectFieldGetter for Hash160 {
219    #[inline]
220    fn get_from_current_ledger_obj(field_code: i32) -> Result<Self> {
221        let mut buffer = core::mem::MaybeUninit::<[u8; HASH160_SIZE]>::uninit();
222        let result_code = unsafe {
223            get_current_ledger_obj_field(field_code, buffer.as_mut_ptr().cast(), HASH160_SIZE)
224        };
225        match_result_code_with_expected_bytes(result_code, HASH160_SIZE, || {
226            Hash160::from(unsafe { buffer.assume_init() })
227        })
228    }
229
230    #[inline]
231    fn get_from_current_ledger_obj_optional(field_code: i32) -> Result<Option<Self>> {
232        let mut buffer = core::mem::MaybeUninit::<[u8; HASH160_SIZE]>::uninit();
233        let result_code = unsafe {
234            get_current_ledger_obj_field(field_code, buffer.as_mut_ptr().cast(), HASH160_SIZE)
235        };
236        match_result_code_with_expected_bytes_optional(result_code, HASH160_SIZE, || {
237            Some(Hash160::from(unsafe { buffer.assume_init() }))
238        })
239    }
240
241    #[inline]
242    fn get_from_ledger_obj(register_num: i32, field_code: i32) -> Result<Self> {
243        let mut buffer = core::mem::MaybeUninit::<[u8; HASH160_SIZE]>::uninit();
244        let result_code = unsafe {
245            get_ledger_obj_field(
246                register_num,
247                field_code,
248                buffer.as_mut_ptr().cast(),
249                HASH160_SIZE,
250            )
251        };
252        match_result_code_with_expected_bytes(result_code, HASH160_SIZE, || {
253            Hash160::from(unsafe { buffer.assume_init() })
254        })
255    }
256
257    #[inline]
258    fn get_from_ledger_obj_optional(register_num: i32, field_code: i32) -> Result<Option<Self>> {
259        let mut buffer = core::mem::MaybeUninit::<[u8; HASH160_SIZE]>::uninit();
260        let result_code = unsafe {
261            get_ledger_obj_field(
262                register_num,
263                field_code,
264                buffer.as_mut_ptr().cast(),
265                HASH160_SIZE,
266            )
267        };
268        match_result_code_with_expected_bytes_optional(result_code, HASH160_SIZE, || {
269            Some(Hash160::from(unsafe { buffer.assume_init() }))
270        })
271    }
272}
273
274/// Implementation of `LedgerObjectFieldGetter` for 192-bit cryptographic hashes.
275///
276/// This implementation handles 24-byte hash fields in XRPL ledger objects.
277/// Hash192 values are used for various cryptographic operations and identifiers.
278///
279/// # Buffer Management
280///
281/// Uses a 24-byte buffer (HASH192_SIZE) and validates that exactly 24 bytes
282/// are returned from the host function to ensure data integrity.
283impl LedgerObjectFieldGetter for Hash192 {
284    #[inline]
285    fn get_from_current_ledger_obj(field_code: i32) -> Result<Self> {
286        let mut buffer = core::mem::MaybeUninit::<[u8; HASH192_SIZE]>::uninit();
287        let result_code = unsafe {
288            get_current_ledger_obj_field(field_code, buffer.as_mut_ptr().cast(), HASH192_SIZE)
289        };
290        match_result_code_with_expected_bytes(result_code, HASH192_SIZE, || {
291            Hash192::from(unsafe { buffer.assume_init() })
292        })
293    }
294
295    #[inline]
296    fn get_from_current_ledger_obj_optional(field_code: i32) -> Result<Option<Self>> {
297        let mut buffer = core::mem::MaybeUninit::<[u8; HASH192_SIZE]>::uninit();
298        let result_code = unsafe {
299            get_current_ledger_obj_field(field_code, buffer.as_mut_ptr().cast(), HASH192_SIZE)
300        };
301        match_result_code_with_expected_bytes_optional(result_code, HASH192_SIZE, || {
302            Some(Hash192::from(unsafe { buffer.assume_init() }))
303        })
304    }
305
306    #[inline]
307    fn get_from_ledger_obj(register_num: i32, field_code: i32) -> Result<Self> {
308        let mut buffer = core::mem::MaybeUninit::<[u8; HASH192_SIZE]>::uninit();
309        let result_code = unsafe {
310            get_ledger_obj_field(
311                register_num,
312                field_code,
313                buffer.as_mut_ptr().cast(),
314                HASH192_SIZE,
315            )
316        };
317        match_result_code_with_expected_bytes(result_code, HASH192_SIZE, || {
318            Hash192::from(unsafe { buffer.assume_init() })
319        })
320    }
321
322    #[inline]
323    fn get_from_ledger_obj_optional(register_num: i32, field_code: i32) -> Result<Option<Self>> {
324        let mut buffer = core::mem::MaybeUninit::<[u8; HASH192_SIZE]>::uninit();
325        let result_code = unsafe {
326            get_ledger_obj_field(
327                register_num,
328                field_code,
329                buffer.as_mut_ptr().cast(),
330                HASH192_SIZE,
331            )
332        };
333        match_result_code_with_expected_bytes_optional(result_code, HASH192_SIZE, || {
334            Some(Hash192::from(unsafe { buffer.assume_init() }))
335        })
336    }
337}
338
339pub mod current_ledger_object {
340    use super::LedgerObjectFieldGetter;
341    use crate::host::Result;
342    use crate::sfield::SField;
343
344    /// Retrieves a field from the current ledger object.
345    ///
346    /// # Arguments
347    ///
348    /// * `field` - An SField constant that encodes both the field code and expected type
349    ///
350    /// # Returns
351    ///
352    /// Returns a `Result<T>` where:
353    /// * `Ok(T)` - The field value for the specified field
354    /// * `Err(Error)` - If the field cannot be retrieved or has unexpected size
355    ///
356    /// # Example
357    ///
358    /// ```rust,no_run
359    /// use xrpl_wasm_stdlib::core::ledger_objects::current_ledger_object;
360    /// use xrpl_wasm_stdlib::sfield;
361    ///
362    /// // Type is automatically inferred from the SField constant
363    /// let flags = current_ledger_object::get_field(sfield::Flags).unwrap();  // u32
364    /// let balance = current_ledger_object::get_field(sfield::Balance).unwrap();  // u64
365    /// ```
366    #[inline]
367    pub fn get_field<T: LedgerObjectFieldGetter, const CODE: i32>(
368        _field: SField<T, CODE>,
369    ) -> Result<T> {
370        T::get_from_current_ledger_obj(CODE)
371    }
372
373    /// Retrieves an optionally present field from the current ledger object.
374    ///
375    /// # Arguments
376    ///
377    /// * `field` - An SField constant that encodes both the field code and expected type
378    ///
379    /// # Returns
380    ///
381    /// Returns a `Result<Option<T>>` where:
382    /// * `Ok(Some(T))` - The field value for the specified field
383    /// * `Ok(None)` - If the field is not present
384    /// * `Err(Error)` - If the field cannot be retrieved or has unexpected size
385    #[inline]
386    pub fn get_field_optional<T: LedgerObjectFieldGetter, const CODE: i32>(
387        _field: SField<T, CODE>,
388    ) -> Result<Option<T>> {
389        T::get_from_current_ledger_obj_optional(CODE)
390    }
391}
392
393pub mod ledger_object {
394    use super::LedgerObjectFieldGetter;
395    use crate::host::Result;
396    use crate::sfield::SField;
397
398    /// Retrieves a field from a specified ledger object.
399    ///
400    /// # Arguments
401    ///
402    /// * `register_num` - The register number holding the ledger object to look for data in
403    /// * `field` - An SField constant that encodes both the field code and expected type
404    ///
405    /// # Returns
406    ///
407    /// Returns a `Result<T>` where:
408    /// * `Ok(T)` - The field value for the specified field
409    /// * `Err(Error)` - If the field cannot be retrieved or has unexpected size
410    ///
411    /// # Example
412    ///
413    /// ```rust,no_run
414    /// use xrpl_wasm_stdlib::core::ledger_objects::ledger_object;
415    /// use xrpl_wasm_stdlib::sfield;
416    ///
417    /// // Type is automatically inferred from the SField constant
418    /// let balance = ledger_object::get_field(0, sfield::Balance).unwrap();  // u64
419    /// let account = ledger_object::get_field(0, sfield::Account).unwrap();  // AccountID
420    /// ```
421    #[inline]
422    pub fn get_field<T: LedgerObjectFieldGetter, const CODE: i32>(
423        register_num: i32,
424        _field: SField<T, CODE>,
425    ) -> Result<T> {
426        T::get_from_ledger_obj(register_num, CODE)
427    }
428
429    /// Retrieves an optionally present field from a specified ledger object.
430    ///
431    /// # Arguments
432    ///
433    /// * `register_num` - The register number holding the ledger object to look for data in
434    /// * `field` - An SField constant that encodes both the field code and expected type
435    ///
436    /// # Returns
437    ///
438    /// Returns a `Result<Option<T>>` where:
439    /// * `Ok(Some(T))` - The field value for the specified field
440    /// * `Ok(None)` - If the field is not present in the ledger object
441    /// * `Err(Error)` - If the field retrieval operation failed
442    #[inline]
443    pub fn get_field_optional<T: LedgerObjectFieldGetter, const CODE: i32>(
444        register_num: i32,
445        _field: SField<T, CODE>,
446    ) -> Result<Option<T>> {
447        T::get_from_ledger_obj_optional(register_num, CODE)
448    }
449
450    #[cfg(test)]
451    mod tests {
452        use super::*;
453        use crate::core::ledger_objects::{current_ledger_object, ledger_object};
454        use crate::core::types::account_id::{ACCOUNT_ID_SIZE, AccountID};
455        use crate::core::types::amount::{AMOUNT_SIZE, Amount};
456        use crate::core::types::blob::{Blob, DEFAULT_BLOB_SIZE};
457        use crate::core::types::currency::{CURRENCY_SIZE, Currency};
458        use crate::core::types::issue::Issue;
459        use crate::core::types::public_key::PUBLIC_KEY_BUFFER_SIZE;
460        use crate::core::types::uint::{
461            HASH128_SIZE, HASH160_SIZE, HASH192_SIZE, HASH256_SIZE, Hash128, Hash160, Hash192,
462            Hash256,
463        };
464        use crate::host::host_bindings_trait::MockHostBindings;
465        use crate::host::setup_mock;
466        use crate::sfield;
467        use mockall::predicate::{always, eq};
468
469        // ========================================
470        // Test helper functions
471        // ========================================
472
473        /// Helper to set up a mock expectation for get_current_ledger_obj_field.
474        ///
475        /// Zero-fills the output buffer before returning. This is required because
476        /// `get_variable_size_field` and the fixed-size getters allocate the buffer
477        /// via `MaybeUninit` and call `assume_init` after the host call returns —
478        /// leaving the buffer uninitialized would be UB.
479        fn expect_current_field(
480            mock: &mut MockHostBindings,
481            field_code: i32,
482            size: usize,
483            times: usize,
484        ) {
485            mock.expect_get_current_ledger_obj_field()
486                .with(eq(field_code), always(), eq(size))
487                .times(times)
488                .returning(move |_, buf, buf_size| {
489                    unsafe { core::ptr::write_bytes(buf, 0, buf_size) };
490                    size as i32
491                });
492        }
493
494        /// Helper to set up a mock expectation for get_ledger_obj_field.
495        /// Zero-fills the output buffer; see `expect_current_field` for rationale.
496        fn expect_ledger_field(
497            mock: &mut MockHostBindings,
498            slot: i32,
499            field_code: i32,
500            size: usize,
501            times: usize,
502        ) {
503            mock.expect_get_ledger_obj_field()
504                .with(eq(slot), eq(field_code), always(), eq(size))
505                .times(times)
506                .returning(move |_, _, buf, buf_size| {
507                    unsafe { core::ptr::write_bytes(buf, 0, buf_size) };
508                    size as i32
509                });
510        }
511
512        /// Like `expect_current_field`, but the host writes fewer bytes than the
513        /// buffer holds — used for variable-size fields (e.g. `Issue` uses a 40-byte
514        /// buffer but returns 20 bytes for the XRP variant).
515        fn expect_current_field_short(
516            mock: &mut MockHostBindings,
517            field_code: i32,
518            buf_size: usize,
519            returned: i32,
520        ) {
521            mock.expect_get_current_ledger_obj_field()
522                .with(eq(field_code), always(), eq(buf_size))
523                .times(1)
524                .returning(move |_, _, _| returned);
525        }
526
527        /// Like `expect_ledger_field`, but the host writes fewer bytes than the
528        /// buffer holds. See `expect_current_field_short`.
529        fn expect_ledger_field_short(
530            mock: &mut MockHostBindings,
531            slot: i32,
532            field_code: i32,
533            buf_size: usize,
534            returned: i32,
535        ) {
536            mock.expect_get_ledger_obj_field()
537                .with(eq(slot), eq(field_code), always(), eq(buf_size))
538                .times(1)
539                .returning(move |_, _, _, _| returned);
540        }
541
542        // ========================================
543        // Basic smoke tests for LedgerObjectFieldGetter implementations
544        // These tests verify that the trait implementations compile and work with the test host.
545        // Note: The test host returns buffer_len as success, so these only verify basic functionality.
546        // ========================================
547
548        #[test]
549        fn test_field_getter_basic_types() {
550            let mut mock = MockHostBindings::new();
551
552            expect_current_field(&mut mock, sfield::LedgerEntryType.into(), 2, 1);
553            expect_current_field(&mut mock, sfield::Flags.into(), 4, 1);
554            expect_current_field(&mut mock, sfield::Balance.into(), 8, 1);
555
556            let _guard = setup_mock(mock);
557
558            // Test that all basic integer types work
559            assert!(u16::get_from_current_ledger_obj(sfield::LedgerEntryType.into()).is_ok());
560            assert!(u32::get_from_current_ledger_obj(sfield::Flags.into()).is_ok());
561            assert!(u64::get_from_current_ledger_obj(sfield::Balance.into()).is_ok());
562        }
563
564        #[test]
565        fn test_field_getter_xrpl_types() {
566            let mut mock = MockHostBindings::new();
567
568            expect_current_field(&mut mock, sfield::Account.into(), ACCOUNT_ID_SIZE, 1);
569            expect_current_field(&mut mock, sfield::Amount.into(), AMOUNT_SIZE, 1);
570            expect_current_field(&mut mock, sfield::EmailHash.into(), HASH128_SIZE, 1);
571            expect_current_field(&mut mock, sfield::PreviousTxnID.into(), HASH256_SIZE, 1);
572            expect_current_field(&mut mock, sfield::PublicKey.into(), DEFAULT_BLOB_SIZE, 1);
573            expect_current_field(&mut mock, sfield::TakerPaysCurrency.into(), HASH160_SIZE, 1);
574            expect_current_field(&mut mock, sfield::MPTokenIssuanceID.into(), HASH192_SIZE, 1);
575            expect_current_field(&mut mock, sfield::BaseAsset.into(), CURRENCY_SIZE, 1);
576            expect_current_field_short(&mut mock, sfield::Asset.into(), 40, 20);
577
578            let _guard = setup_mock(mock);
579
580            // Test that XRPL-specific types work
581            assert!(AccountID::get_from_current_ledger_obj(sfield::Account.into()).is_ok());
582            assert!(Amount::get_from_current_ledger_obj(sfield::Amount.into()).is_ok());
583            assert!(Hash128::get_from_current_ledger_obj(sfield::EmailHash.into()).is_ok());
584            assert!(Hash256::get_from_current_ledger_obj(sfield::PreviousTxnID.into()).is_ok());
585            assert!(Hash160::get_from_current_ledger_obj(sfield::TakerPaysCurrency.into()).is_ok());
586            assert!(Hash192::get_from_current_ledger_obj(sfield::MPTokenIssuanceID.into()).is_ok());
587            assert!(Currency::get_from_current_ledger_obj(sfield::BaseAsset.into()).is_ok());
588            assert!(Issue::get_from_current_ledger_obj(sfield::Asset.into()).is_ok());
589
590            let blob: Blob<DEFAULT_BLOB_SIZE> =
591                Blob::get_from_current_ledger_obj(sfield::PublicKey.into()).unwrap();
592            // The test host returns buffer length as the result
593            assert_eq!(blob.len, DEFAULT_BLOB_SIZE);
594        }
595
596        #[test]
597        fn test_field_getter_optional_variants() {
598            let mut mock = MockHostBindings::new();
599
600            expect_current_field(&mut mock, sfield::Flags.into(), 4, 1);
601            expect_current_field(&mut mock, sfield::Account.into(), ACCOUNT_ID_SIZE, 1);
602            expect_current_field(&mut mock, sfield::TakerPaysCurrency.into(), HASH160_SIZE, 1);
603            expect_current_field(&mut mock, sfield::MPTokenIssuanceID.into(), HASH192_SIZE, 1);
604            expect_current_field(&mut mock, sfield::Amount.into(), AMOUNT_SIZE, 1);
605            expect_current_field(&mut mock, sfield::BaseAsset.into(), CURRENCY_SIZE, 1);
606            expect_current_field(&mut mock, sfield::EmailHash.into(), HASH128_SIZE, 1);
607            expect_current_field(&mut mock, sfield::PreviousTxnID.into(), HASH256_SIZE, 1);
608            expect_current_field(&mut mock, sfield::PublicKey.into(), DEFAULT_BLOB_SIZE, 1);
609            expect_current_field_short(&mut mock, sfield::Asset.into(), 40, 20);
610
611            let _guard = setup_mock(mock);
612
613            // Test optional field retrieval
614            let result = u32::get_from_current_ledger_obj_optional(sfield::Flags.into());
615            assert!(result.is_ok());
616            assert!(result.unwrap().is_some());
617
618            let result = AccountID::get_from_current_ledger_obj_optional(sfield::Account.into());
619            assert!(result.is_ok());
620            assert!(result.unwrap().is_some());
621
622            let result =
623                Hash160::get_from_current_ledger_obj_optional(sfield::TakerPaysCurrency.into());
624            assert!(result.is_ok());
625            assert!(result.unwrap().is_some());
626
627            let result =
628                Hash192::get_from_current_ledger_obj_optional(sfield::MPTokenIssuanceID.into());
629            assert!(result.is_ok());
630            assert!(result.unwrap().is_some());
631
632            let result = Amount::get_from_current_ledger_obj_optional(sfield::Amount.into());
633            assert!(result.is_ok());
634            assert!(result.unwrap().is_some());
635
636            let result = Currency::get_from_current_ledger_obj_optional(sfield::BaseAsset.into());
637            assert!(result.is_ok());
638            assert!(result.unwrap().is_some());
639
640            let result = Hash128::get_from_current_ledger_obj_optional(sfield::EmailHash.into());
641            assert!(result.is_ok());
642            assert!(result.unwrap().is_some());
643
644            let result =
645                Hash256::get_from_current_ledger_obj_optional(sfield::PreviousTxnID.into());
646            assert!(result.is_ok());
647            assert!(result.unwrap().is_some());
648
649            let result = Blob::<DEFAULT_BLOB_SIZE>::get_from_current_ledger_obj_optional(
650                sfield::PublicKey.into(),
651            );
652            assert!(result.is_ok());
653            assert!(result.unwrap().is_some());
654
655            let result = Issue::get_from_current_ledger_obj_optional(sfield::Asset.into());
656            assert!(result.is_ok());
657            assert!(result.unwrap().is_some());
658        }
659
660        #[test]
661        fn test_field_getter_with_slot() {
662            let mut mock = MockHostBindings::new();
663            let slot = 0;
664
665            expect_ledger_field(&mut mock, slot, sfield::Flags.into(), 4, 1);
666            expect_ledger_field(&mut mock, slot, sfield::Balance.into(), 8, 1);
667            expect_ledger_field(&mut mock, slot, sfield::Account.into(), ACCOUNT_ID_SIZE, 1);
668            expect_ledger_field(
669                &mut mock,
670                slot,
671                sfield::TakerPaysCurrency.into(),
672                HASH160_SIZE,
673                1,
674            );
675            expect_ledger_field(
676                &mut mock,
677                slot,
678                sfield::MPTokenIssuanceID.into(),
679                HASH192_SIZE,
680                1,
681            );
682            expect_ledger_field(&mut mock, slot, sfield::Amount.into(), AMOUNT_SIZE, 1);
683            expect_ledger_field(&mut mock, slot, sfield::BaseAsset.into(), CURRENCY_SIZE, 1);
684            expect_ledger_field(&mut mock, slot, sfield::EmailHash.into(), HASH128_SIZE, 1);
685            expect_ledger_field(
686                &mut mock,
687                slot,
688                sfield::PreviousTxnID.into(),
689                HASH256_SIZE,
690                1,
691            );
692            expect_ledger_field(
693                &mut mock,
694                slot,
695                sfield::PublicKey.into(),
696                DEFAULT_BLOB_SIZE,
697                1,
698            );
699            expect_ledger_field_short(&mut mock, slot, sfield::Asset.into(), 40, 20);
700
701            let _guard = setup_mock(mock);
702
703            // Test ledger object field retrieval with slot numbers
704            assert!(u32::get_from_ledger_obj(slot, sfield::Flags.into()).is_ok());
705            assert!(u64::get_from_ledger_obj(slot, sfield::Balance.into()).is_ok());
706            assert!(AccountID::get_from_ledger_obj(slot, sfield::Account.into()).is_ok());
707            assert!(Hash160::get_from_ledger_obj(slot, sfield::TakerPaysCurrency.into()).is_ok());
708            assert!(Hash192::get_from_ledger_obj(slot, sfield::MPTokenIssuanceID.into()).is_ok());
709            assert!(Amount::get_from_ledger_obj(slot, sfield::Amount.into()).is_ok());
710            assert!(Currency::get_from_ledger_obj(slot, sfield::BaseAsset.into()).is_ok());
711            assert!(Hash128::get_from_ledger_obj(slot, sfield::EmailHash.into()).is_ok());
712            assert!(Hash256::get_from_ledger_obj(slot, sfield::PreviousTxnID.into()).is_ok());
713            assert!(
714                Blob::<DEFAULT_BLOB_SIZE>::get_from_ledger_obj(slot, sfield::PublicKey.into())
715                    .is_ok()
716            );
717            assert!(Issue::get_from_ledger_obj(slot, sfield::Asset.into()).is_ok());
718        }
719
720        #[test]
721        fn test_field_getter_optional_with_slot() {
722            let mut mock = MockHostBindings::new();
723            let slot = 0;
724
725            expect_ledger_field(&mut mock, slot, sfield::Flags.into(), 4, 1);
726            expect_ledger_field(
727                &mut mock,
728                slot,
729                sfield::TakerPaysCurrency.into(),
730                HASH160_SIZE,
731                1,
732            );
733            expect_ledger_field(
734                &mut mock,
735                slot,
736                sfield::MPTokenIssuanceID.into(),
737                HASH192_SIZE,
738                1,
739            );
740            expect_ledger_field(&mut mock, slot, sfield::Amount.into(), AMOUNT_SIZE, 1);
741            expect_ledger_field(&mut mock, slot, sfield::BaseAsset.into(), CURRENCY_SIZE, 1);
742            expect_ledger_field(&mut mock, slot, sfield::EmailHash.into(), HASH128_SIZE, 1);
743            expect_ledger_field(
744                &mut mock,
745                slot,
746                sfield::PreviousTxnID.into(),
747                HASH256_SIZE,
748                1,
749            );
750            expect_ledger_field(
751                &mut mock,
752                slot,
753                sfield::PublicKey.into(),
754                DEFAULT_BLOB_SIZE,
755                1,
756            );
757            expect_ledger_field_short(&mut mock, slot, sfield::Asset.into(), 40, 20);
758
759            let _guard = setup_mock(mock);
760
761            // Test optional field retrieval with slot numbers
762            let result = u32::get_from_ledger_obj_optional(slot, sfield::Flags.into());
763            assert!(result.is_ok());
764            assert!(result.unwrap().is_some());
765
766            let result =
767                Hash160::get_from_ledger_obj_optional(slot, sfield::TakerPaysCurrency.into());
768            assert!(result.is_ok());
769            assert!(result.unwrap().is_some());
770
771            let result =
772                Hash192::get_from_ledger_obj_optional(slot, sfield::MPTokenIssuanceID.into());
773            assert!(result.is_ok());
774            assert!(result.unwrap().is_some());
775            let result = Amount::get_from_ledger_obj_optional(slot, sfield::Amount.into());
776            assert!(result.is_ok());
777            assert!(result.unwrap().is_some());
778
779            let result = Currency::get_from_ledger_obj_optional(slot, sfield::BaseAsset.into());
780            assert!(result.is_ok());
781            assert!(result.unwrap().is_some());
782
783            let result = Hash128::get_from_ledger_obj_optional(slot, sfield::EmailHash.into());
784            assert!(result.is_ok());
785            assert!(result.unwrap().is_some());
786
787            let result = Hash256::get_from_ledger_obj_optional(slot, sfield::PreviousTxnID.into());
788            assert!(result.is_ok());
789            assert!(result.unwrap().is_some());
790
791            let result = Blob::<DEFAULT_BLOB_SIZE>::get_from_ledger_obj_optional(
792                slot,
793                sfield::PublicKey.into(),
794            );
795            assert!(result.is_ok());
796            assert!(result.unwrap().is_some());
797
798            let result = Issue::get_from_ledger_obj_optional(slot, sfield::Asset.into());
799            assert!(result.is_ok());
800            assert!(result.unwrap().is_some());
801        }
802
803        // ========================================
804        // Value-level tests: verify Issue variant detection by populating
805        // the mock buffer with known bytes (not just checking `is_ok()`).
806        // ========================================
807
808        #[test]
809        fn test_issue_decodes_xrp_variant() {
810            let mut mock = MockHostBindings::new();
811            mock.expect_get_current_ledger_obj_field()
812                .with(eq::<i32>(sfield::Asset.into()), always(), eq(40))
813                .times(1)
814                .returning(|_, buf, _| {
815                    // 20 zero bytes → XRP
816                    unsafe { core::ptr::write_bytes(buf, 0, 20) };
817                    20
818                });
819
820            let _guard = setup_mock(mock);
821
822            let issue = Issue::get_from_current_ledger_obj(sfield::Asset.into()).unwrap();
823            assert!(matches!(issue, Issue::XRP(_)));
824        }
825
826        #[test]
827        fn test_issue_decodes_mpt_variant() {
828            let mut mock = MockHostBindings::new();
829            mock.expect_get_current_ledger_obj_field()
830                .with(eq::<i32>(sfield::Asset.into()), always(), eq(40))
831                .times(1)
832                .returning(|_, buf, _| {
833                    // 4 bytes seq=42 (big-endian) + 20 bytes issuer=0xAB → MPT
834                    let slice = unsafe { core::slice::from_raw_parts_mut(buf, 24) };
835                    slice[0..4].copy_from_slice(&42u32.to_be_bytes());
836                    slice[4..24].fill(0xAB);
837                    24
838                });
839
840            let _guard = setup_mock(mock);
841
842            let issue = Issue::get_from_current_ledger_obj(sfield::Asset.into()).unwrap();
843            match issue {
844                Issue::MPT(mpt) => {
845                    assert_eq!(mpt.mpt_id().get_sequence_num(), 42);
846                    assert_eq!(mpt.mpt_id().get_issuer(), AccountID::from([0xAB; 20]));
847                }
848                _ => panic!("expected MPT variant"),
849            }
850        }
851
852        #[test]
853        fn test_issue_decodes_iou_variant() {
854            let mut mock = MockHostBindings::new();
855            mock.expect_get_current_ledger_obj_field()
856                .with(eq::<i32>(sfield::Asset.into()), always(), eq(40))
857                .times(1)
858                .returning(|_, buf, _| {
859                    // 20 bytes currency=0xCC + 20 bytes issuer=0xDD → IOU
860                    let slice = unsafe { core::slice::from_raw_parts_mut(buf, 40) };
861                    slice[0..20].fill(0xCC);
862                    slice[20..40].fill(0xDD);
863                    40
864                });
865
866            let _guard = setup_mock(mock);
867
868            let issue = Issue::get_from_current_ledger_obj(sfield::Asset.into()).unwrap();
869            match issue {
870                Issue::IOU(iou) => {
871                    let bytes = iou.as_bytes();
872                    assert_eq!(&bytes[..20], &[0xCC; 20]);
873                    assert_eq!(&bytes[20..], &[0xDD; 20]);
874                }
875                _ => panic!("expected IOU variant"),
876            }
877        }
878
879        // ========================================
880        // Value-level tests: verify Amount variant detection by populating
881        // the mock buffer with known flag bits + payload (not just `is_ok()`).
882        // The 48-byte buffer is always passed to `Amount::from_bytes` in full,
883        // so the mock must initialize all 48 bytes (MaybeUninit) even though
884        // only a prefix is meaningful per variant.
885        // ========================================
886
887        #[test]
888        fn test_amount_decodes_xrp_variant() {
889            let mut mock = MockHostBindings::new();
890            mock.expect_get_current_ledger_obj_field()
891                .with(eq::<i32>(sfield::Amount.into()), always(), eq(48))
892                .times(1)
893                .returning(|_, buf, size| {
894                    // XRP positive 1000 drops: byte0 = 0x40 (positive bit, XRP type),
895                    // remaining 7 bytes hold the drop amount big-endian.
896                    let slice = unsafe { core::slice::from_raw_parts_mut(buf, size) };
897                    slice.fill(0);
898                    let mut be = 1000u64.to_be_bytes();
899                    be[0] |= 0x40; // set positive flag in top bits
900                    slice[0..8].copy_from_slice(&be);
901                    8
902                });
903
904            let _guard = setup_mock(mock);
905
906            let amount = Amount::get_from_current_ledger_obj(sfield::Amount.into()).unwrap();
907            assert!(matches!(amount, Amount::XRP { num_drops: 1000 }));
908        }
909
910        #[test]
911        fn test_amount_decodes_mpt_variant() {
912            let mut mock = MockHostBindings::new();
913            mock.expect_get_current_ledger_obj_field()
914                .with(eq::<i32>(sfield::Amount.into()), always(), eq(48))
915                .times(1)
916                .returning(|_, buf, size| {
917                    // MPT positive: byte0 bit7=0 (not IOU), bit6=1 (positive), bit5=1 (MPT)
918                    // bytes[1..9]  = num_units big-endian
919                    // bytes[9..33] = MptId (4-byte seq + 20-byte issuer)
920                    let slice = unsafe { core::slice::from_raw_parts_mut(buf, size) };
921                    slice.fill(0);
922                    slice[0] = 0x60;
923                    slice[1..9].copy_from_slice(&100u64.to_be_bytes());
924                    slice[9..13].copy_from_slice(&7u32.to_be_bytes());
925                    slice[13..33].fill(0xAB);
926                    33
927                });
928
929            let _guard = setup_mock(mock);
930
931            let amount = Amount::get_from_current_ledger_obj(sfield::Amount.into()).unwrap();
932            match amount {
933                Amount::MPT {
934                    num_units,
935                    is_positive,
936                    mpt_id,
937                } => {
938                    assert_eq!(num_units, 100);
939                    assert!(is_positive);
940                    assert_eq!(mpt_id.get_sequence_num(), 7);
941                    assert_eq!(mpt_id.get_issuer(), AccountID::from([0xAB; 20]));
942                }
943                _ => panic!("expected MPT variant"),
944            }
945        }
946
947        #[test]
948        fn test_amount_decodes_iou_variant() {
949            let mut mock = MockHostBindings::new();
950            mock.expect_get_current_ledger_obj_field()
951                .with(eq::<i32>(sfield::Amount.into()), always(), eq(48))
952                .times(1)
953                .returning(|_, buf, size| {
954                    // IOU: byte0 bit7=1; bytes[0..8]=OpaqueFloat (opaque, content
955                    // doesn't matter for variant detection), bytes[8..28]=currency,
956                    // bytes[28..48]=issuer.
957                    let slice = unsafe { core::slice::from_raw_parts_mut(buf, size) };
958                    slice.fill(0);
959                    slice[0] = 0x80;
960                    slice[8..28].fill(0xCC);
961                    slice[28..48].fill(0xDD);
962                    48
963                });
964
965            let _guard = setup_mock(mock);
966
967            let amount = Amount::get_from_current_ledger_obj(sfield::Amount.into()).unwrap();
968            match amount {
969                Amount::IOU {
970                    issuer, currency, ..
971                } => {
972                    assert_eq!(issuer, AccountID::from([0xDD; 20]));
973                    assert_eq!(currency, Currency::from([0xCC; 20]));
974                }
975                _ => panic!("expected IOU variant"),
976            }
977        }
978
979        // ========================================
980        // Tests for module-level convenience functions
981        // ========================================
982
983        #[test]
984        fn test_current_ledger_object_module() {
985            let mut mock = MockHostBindings::new();
986
987            expect_current_field(&mut mock, sfield::Flags.into(), 4, 2);
988            expect_current_field(&mut mock, sfield::Account.into(), ACCOUNT_ID_SIZE, 1);
989
990            let _guard = setup_mock(mock);
991
992            // Test the current_ledger_object module's convenience functions
993            assert!(current_ledger_object::get_field(sfield::Flags).is_ok());
994            assert!(current_ledger_object::get_field(sfield::Account).is_ok());
995
996            let result = current_ledger_object::get_field_optional(sfield::Flags);
997            assert!(result.is_ok());
998            assert!(result.unwrap().is_some());
999        }
1000
1001        #[test]
1002        fn test_ledger_object_module() {
1003            let mut mock = MockHostBindings::new();
1004            let slot = 0;
1005
1006            expect_ledger_field(&mut mock, slot, sfield::LedgerEntryType.into(), 2, 1);
1007            expect_ledger_field(&mut mock, slot, sfield::Flags.into(), 4, 2);
1008            expect_ledger_field(&mut mock, slot, sfield::Balance.into(), AMOUNT_SIZE, 1);
1009            expect_ledger_field(&mut mock, slot, sfield::Account.into(), ACCOUNT_ID_SIZE, 1);
1010            expect_ledger_field(&mut mock, slot, sfield::Amount.into(), AMOUNT_SIZE, 1);
1011            expect_ledger_field(&mut mock, slot, sfield::EmailHash.into(), HASH128_SIZE, 1);
1012            expect_ledger_field(
1013                &mut mock,
1014                slot,
1015                sfield::PreviousTxnID.into(),
1016                HASH256_SIZE,
1017                1,
1018            );
1019            expect_ledger_field(
1020                &mut mock,
1021                slot,
1022                sfield::PublicKey.into(),
1023                DEFAULT_BLOB_SIZE,
1024                1,
1025            );
1026
1027            let _guard = setup_mock(mock);
1028
1029            // Test the ledger_object module's convenience functions
1030            assert!(ledger_object::get_field(slot, sfield::LedgerEntryType).is_ok());
1031            assert!(ledger_object::get_field(slot, sfield::Flags).is_ok());
1032            assert!(ledger_object::get_field(slot, sfield::Balance).is_ok());
1033            assert!(ledger_object::get_field(slot, sfield::Account).is_ok());
1034            assert!(ledger_object::get_field(slot, sfield::Amount).is_ok());
1035            assert!(ledger_object::get_field(slot, sfield::EmailHash).is_ok());
1036            assert!(ledger_object::get_field(slot, sfield::PreviousTxnID).is_ok());
1037            assert!(ledger_object::get_field(slot, sfield::PublicKey).is_ok());
1038
1039            let result = ledger_object::get_field_optional(slot, sfield::Flags);
1040            assert!(result.is_ok());
1041            assert!(result.unwrap().is_some());
1042        }
1043
1044        // ========================================
1045        // Type inference and compilation tests
1046        // ========================================
1047
1048        #[test]
1049        fn test_type_inference() {
1050            let mut mock = MockHostBindings::new();
1051            let slot = 0;
1052
1053            expect_ledger_field(&mut mock, slot, sfield::Balance.into(), AMOUNT_SIZE, 1);
1054            expect_ledger_field(&mut mock, slot, sfield::Account.into(), ACCOUNT_ID_SIZE, 1);
1055            expect_ledger_field(&mut mock, slot, sfield::Sequence.into(), 4, 1);
1056            expect_ledger_field(&mut mock, slot, sfield::Flags.into(), 4, 1);
1057
1058            let _guard = setup_mock(mock);
1059
1060            // Verify type inference works with turbofish syntax
1061            let _balance = get_field(slot, sfield::Balance);
1062            let _account = get_field(slot, sfield::Account);
1063
1064            // Verify type inference works with type annotations
1065            let _sequence: Result<u32> = get_field(slot, sfield::Sequence);
1066            let _flags: Result<u32> = get_field(slot, sfield::Flags);
1067        }
1068
1069        // ========================================
1070        // Data size verification tests
1071        // ========================================
1072
1073        #[test]
1074        fn test_type_sizes() {
1075            let mut mock = MockHostBindings::new();
1076
1077            expect_current_field(&mut mock, sfield::EmailHash.into(), HASH128_SIZE, 1);
1078            expect_current_field(&mut mock, sfield::PreviousTxnID.into(), HASH256_SIZE, 1);
1079            expect_current_field(&mut mock, sfield::Account.into(), ACCOUNT_ID_SIZE, 1);
1080            expect_current_field(
1081                &mut mock,
1082                sfield::PublicKey.into(),
1083                PUBLIC_KEY_BUFFER_SIZE,
1084                1,
1085            );
1086
1087            let _guard = setup_mock(mock);
1088
1089            // Verify that returned types have the expected sizes
1090            let hash128 = Hash128::get_from_current_ledger_obj(sfield::EmailHash.into()).unwrap();
1091            assert_eq!(hash128.as_bytes().len(), HASH128_SIZE);
1092
1093            let hash256 =
1094                Hash256::get_from_current_ledger_obj(sfield::PreviousTxnID.into()).unwrap();
1095            assert_eq!(hash256.as_bytes().len(), HASH256_SIZE);
1096
1097            let account = AccountID::get_from_current_ledger_obj(sfield::Account.into()).unwrap();
1098            assert_eq!(account.0.len(), ACCOUNT_ID_SIZE);
1099
1100            let blob: Blob<{ PUBLIC_KEY_BUFFER_SIZE }> =
1101                Blob::get_from_current_ledger_obj(sfield::PublicKey.into()).unwrap();
1102            // In test environment, host returns buffer size as result code
1103            assert_eq!(blob.len, PUBLIC_KEY_BUFFER_SIZE);
1104            assert_eq!(blob.data.len(), PUBLIC_KEY_BUFFER_SIZE);
1105        }
1106
1107        // ========================================
1108        // Array and Object unreachable tests
1109        // These verify that Array and Object types panic when accessed
1110        // through the public field access API, as they should only be
1111        // used for navigation via the Location/Locator system.
1112        // ========================================
1113
1114        #[test]
1115        #[should_panic]
1116        fn test_array_get_field_panics() {
1117            let _ = current_ledger_object::get_field(sfield::Signers);
1118        }
1119
1120        #[test]
1121        #[should_panic]
1122        fn test_array_get_field_optional_panics() {
1123            let _ = current_ledger_object::get_field_optional(sfield::Signers);
1124        }
1125
1126        #[test]
1127        #[should_panic]
1128        fn test_array_get_field_with_slot_panics() {
1129            let _ = ledger_object::get_field(0, sfield::Signers);
1130        }
1131
1132        #[test]
1133        #[should_panic]
1134        fn test_array_get_field_optional_with_slot_panics() {
1135            let _ = ledger_object::get_field_optional(0, sfield::Signers);
1136        }
1137
1138        #[test]
1139        #[should_panic]
1140        fn test_object_get_field_panics() {
1141            let _ = current_ledger_object::get_field(sfield::Memo);
1142        }
1143
1144        #[test]
1145        #[should_panic]
1146        fn test_object_get_field_optional_panics() {
1147            let _ = current_ledger_object::get_field_optional(sfield::Memo);
1148        }
1149
1150        #[test]
1151        #[should_panic]
1152        fn test_object_get_field_with_slot_panics() {
1153            let _ = ledger_object::get_field(0, sfield::Memo);
1154        }
1155
1156        #[test]
1157        #[should_panic]
1158        fn test_object_get_field_optional_with_slot_panics() {
1159            let _ = ledger_object::get_field_optional(0, sfield::Memo);
1160        }
1161    }
1162}