Skip to main content

xrpl_wasm_stdlib/core/ledger_objects/
traits.rs

1use crate::core::ledger_objects::{current_ledger_object, ledger_object};
2use crate::core::types::account_id::AccountID;
3use crate::core::types::amount::Amount;
4use crate::core::types::blob::{CONDITION_BLOB_SIZE, ConditionBlob, StandardBlob};
5use crate::core::types::contract_data::{ContractData, XRPL_CONTRACT_DATA_SIZE};
6use crate::core::types::uint::{Hash128, Hash256};
7
8/// This module provides traits for interacting with XRP Ledger objects.
9///
10/// It defines common interfaces for accessing and manipulating different types of ledger objects,
11/// particularly focusing on Escrow objects. The traits provide methods to get and set various
12/// fields of ledger objects, with separate traits for current ledger objects and general ledger objects.
13use crate::host::error_codes::{match_result_code, match_result_code_optional};
14use crate::host::{Error, get_current_ledger_obj_field, get_ledger_obj_field, update_data};
15use crate::host::{Result, Result::Err, Result::Ok};
16use crate::sfield;
17
18/// Trait providing access to common fields present in all ledger objects.
19///
20/// This trait defines methods to access standard fields that are common across
21/// different types of ledger objects in the XRP Ledger.
22pub trait LedgerObjectCommonFields {
23    // NOTE: `get_ledger_index()` is not in this trait because `sfLedgerIndex` is not actually a field on a ledger
24    // object (it's a synthetic field that maps to the `index` field, which is the unique ID of an object in the
25    // ledger's state tree). See https://github.com/XRPLF/rippled/issues/3649 for more context.
26
27    /// Returns the slot number (register number) where the ledger object is stored.
28    ///
29    /// This number is used to identify and access the specific ledger object
30    /// when retrieving or modifying its fields.
31    ///
32    /// # Returns
33    ///
34    /// The slot number as an i32 value
35    fn get_slot_num(&self) -> i32;
36
37    /// Retrieves the flags field of the ledger object.
38    ///
39    /// # Arguments
40    ///
41    /// * `register_num` - The register number where the ledger object is stored
42    ///
43    /// # Returns
44    ///
45    /// The flags as a u32 value
46    fn get_flags(&self) -> Result<u32> {
47        ledger_object::get_field(self.get_slot_num(), sfield::Flags)
48    }
49
50    /// Retrieves the ledger entry type of the object.
51    ///
52    /// The value 0x0075, mapped to the string Escrow, indicates that this is an Escrow entry.
53    ///
54    /// # Returns
55    ///
56    /// The ledger entry type as a u16 value
57    fn get_ledger_entry_type(&self) -> Result<u16> {
58        ledger_object::get_field(self.get_slot_num(), sfield::LedgerEntryType)
59    }
60}
61
62/// Trait providing access to common fields in the current ledger object.
63///
64/// This trait defines methods to access standard fields that are common across
65/// different types of ledger objects, specifically for the current ledger object
66/// being processed.
67pub trait CurrentLedgerObjectCommonFields {
68    // NOTE: `get_ledger_index()` is not in this trait because `sfLedgerIndex` is not actually a field on a ledger
69    // object (it's a synthetic field that maps to the `index` field, which is the unique ID of an object in the
70    // ledger's state tree). See https://github.com/XRPLF/rippled/issues/3649 for more context.
71
72    /// Retrieves the flags field of the current ledger object.
73    ///
74    /// # Returns
75    ///
76    /// The flags as a u32 value
77    fn get_flags(&self) -> Result<u32> {
78        current_ledger_object::get_field(sfield::Flags)
79    }
80
81    /// Retrieves the ledger entry type of the current ledger object.
82    ///
83    /// The value 0x0075, mapped to the string Escrow, indicates that this is an Escrow entry.
84    ///
85    /// # Returns
86    ///
87    /// The ledger entry type as a u16 value
88    fn get_ledger_entry_type(&self) -> Result<u16> {
89        current_ledger_object::get_field(sfield::LedgerEntryType)
90    }
91}
92
93/// Trait providing access to fields specific to Escrow objects in the current ledger.
94///
95/// This trait extends `CurrentLedgerObjectCommonFields` and provides methods to access
96/// fields that are specific to Escrow objects in the current ledger being processed.
97pub trait CurrentEscrowFields: CurrentLedgerObjectCommonFields {
98    /// The address of the owner (sender) of this escrow. This is the account that provided the XRP
99    /// and gets it back if the escrow is canceled.
100    fn get_account(&self) -> Result<AccountID> {
101        current_ledger_object::get_field(sfield::Account)
102    }
103
104    /// The amount currently held in the escrow (could be XRP, IOU, or MPT).
105    fn get_amount(&self) -> Result<Amount> {
106        current_ledger_object::get_field(sfield::Amount)
107    }
108
109    /// The escrow can be canceled if and only if this field is present and the time it specifies
110    /// has passed. Specifically, this is specified as seconds since the Ripple Epoch and it
111    /// "has passed" if it's earlier than the close time of the previous validated ledger.
112    fn get_cancel_after(&self) -> Result<Option<u32>> {
113        current_ledger_object::get_field_optional(sfield::CancelAfter)
114    }
115
116    /// A PREIMAGE-SHA-256 crypto-condition in full crypto-condition format. If present, the EscrowFinish
117    /// transaction must contain a fulfillment that satisfies this condition.
118    fn get_condition(&self) -> Result<Option<ConditionBlob>> {
119        let mut buffer = ConditionBlob::new();
120
121        let result_code = unsafe {
122            get_current_ledger_obj_field(
123                sfield::Condition.into(),
124                buffer.data.as_mut_ptr(),
125                buffer.capacity(),
126            )
127        };
128
129        match_result_code_optional(result_code, || {
130            buffer.len = result_code as usize;
131            (result_code > 0).then_some(buffer)
132        })
133    }
134
135    /// The destination address where the XRP is paid if the escrow is successful.
136    fn get_destination(&self) -> Result<AccountID> {
137        current_ledger_object::get_field(sfield::Destination)
138    }
139
140    /// A hint indicating which page of the destination's owner directory links to this object, in
141    /// case the directory consists of multiple pages. Omitted on escrows created before enabling the fix1523 amendment.
142    fn get_destination_node(&self) -> Result<Option<u64>> {
143        current_ledger_object::get_field_optional(sfield::DestinationNode)
144    }
145
146    /// An arbitrary tag to further specify the destination for this escrow, such as a hosted
147    /// recipient at the destination address.
148    fn get_destination_tag(&self) -> Result<Option<u32>> {
149        current_ledger_object::get_field_optional(sfield::DestinationTag)
150    }
151
152    /// The time, in seconds since the Ripple Epoch, after which this escrow can be finished. Any
153    /// EscrowFinish transaction before this time fails. (Specifically, this is compared with the
154    /// close time of the previous validated ledger.)
155    fn get_finish_after(&self) -> Result<Option<u32>> {
156        current_ledger_object::get_field_optional(sfield::FinishAfter)
157    }
158
159    // TODO: Implement this function.
160    // /// The value 0x0075, mapped to the string Escrow, indicates that this is an Escrow entry.
161    // fn get_ledger_entry_type(&self) -> Result<LedgerEntryType> {
162    //     return Ok(LedgerEntryType::Escrow);
163    // }
164
165    /// A hint indicating which page of the sender's owner directory links to this entry, in case
166    /// the directory consists of multiple pages.
167    fn get_owner_node(&self) -> Result<u64> {
168        current_ledger_object::get_field(sfield::OwnerNode)
169    }
170
171    /// The identifying hash of the transaction that most recently modified this entry.
172    fn get_previous_txn_id(&self) -> Result<Hash256> {
173        current_ledger_object::get_field(sfield::PreviousTxnID)
174    }
175
176    /// The index of the ledger that contains the transaction that most recently modified this
177    /// entry.
178    fn get_previous_txn_lgr_seq(&self) -> Result<u32> {
179        current_ledger_object::get_field(sfield::PreviousTxnLgrSeq)
180    }
181
182    /// An arbitrary tag to further specify the source for this escrow, such as a hosted recipient
183    /// at the owner's address.
184    fn get_source_tag(&self) -> Result<Option<u32>> {
185        current_ledger_object::get_field_optional(sfield::SourceTag)
186    }
187
188    /// The WASM code that is executing.
189    fn get_finish_function(&self) -> Result<Option<StandardBlob>> {
190        current_ledger_object::get_field_optional(sfield::FinishFunction)
191    }
192
193    /// Retrieves the contract `data` from the current escrow object.
194    ///
195    /// This function fetches the `data` field from the current ledger object and returns it as a
196    /// ContractData structure. The data is read into a fixed-size buffer of XRPL_CONTRACT_DATA_SIZE.
197    ///
198    /// # Returns
199    ///
200    /// Returns a `Result<ContractData>` where:
201    /// * `Ok(ContractData)` - Contains the retrieved data and its actual length
202    /// * `Err(Error)` - If the retrieval operation failed
203    fn get_data(&self) -> Result<ContractData> {
204        let mut data: [u8; XRPL_CONTRACT_DATA_SIZE] = [0; XRPL_CONTRACT_DATA_SIZE];
205
206        let result_code = unsafe {
207            get_current_ledger_obj_field(sfield::Data.into(), data.as_mut_ptr(), data.len())
208        };
209
210        match result_code {
211            code if code >= 0 => Ok(ContractData {
212                data,
213                len: code as usize,
214            }),
215            code => Err(Error::from_code(code)),
216        }
217    }
218
219    /// Updates the contract data in the current escrow object.
220    ///
221    /// # Arguments
222    ///
223    /// * `data` - The contract data to update
224    ///
225    /// # Returns
226    ///
227    /// Returns a `Result<()>` where:
228    /// * `Ok(())` - The data was successfully updated
229    /// * `Err(Error)` - If the update operation failed
230    fn update_current_escrow_data(data: ContractData) -> Result<()> {
231        // TODO: Make sure rippled always deletes any existing data bytes in rippled, and sets the new
232        // length to be `data.len` (e.g., if the developer writes 2 bytes, then that's the new
233        // length and any old bytes are lost).
234        let result_code = unsafe { update_data(data.data.as_ptr(), data.len) };
235        match_result_code(result_code, || ())
236    }
237}
238
239/// Trait providing access to fields specific to Escrow objects in any ledger.
240///
241/// This trait extends `LedgerObjectCommonFields` and provides methods to access
242/// fields that are specific to Escrow objects in any ledger, not just the current one.
243/// Each method requires a register number to identify which ledger object to access.
244pub trait EscrowFields: LedgerObjectCommonFields {
245    /// The address of the owner (sender) of this escrow. This is the account that provided the XRP
246    /// and gets it back if the escrow is canceled.
247    fn get_account(&self) -> Result<AccountID> {
248        ledger_object::get_field(self.get_slot_num(), sfield::Account)
249    }
250
251    /// The amount of XRP, in drops, currently held in the escrow.
252    fn get_amount(&self) -> Result<Amount> {
253        // Create a buffer large enough for any Amount type
254        const BUFFER_SIZE: usize = 48usize;
255        let mut buffer = [0u8; BUFFER_SIZE];
256
257        let result_code = unsafe {
258            get_ledger_obj_field(
259                self.get_slot_num(),
260                sfield::Amount.into(),
261                buffer.as_mut_ptr(),
262                buffer.len(),
263            )
264        };
265
266        match_result_code(result_code, || Amount::from(buffer))
267    }
268
269    /// The escrow can be canceled if and only if this field is present and the time it specifies
270    /// has passed. Specifically, this is specified as seconds since the Ripple Epoch and it
271    /// "has passed" if it's earlier than the close time of the previous validated ledger.
272    fn get_cancel_after(&self) -> Result<Option<u32>> {
273        ledger_object::get_field_optional(self.get_slot_num(), sfield::CancelAfter)
274    }
275
276    /// A PREIMAGE-SHA-256 crypto-condition in full crypto-condition format. If present, the EscrowFinish
277    /// transaction must contain a fulfillment that satisfies this condition.
278    fn get_condition(&self) -> Result<Option<ConditionBlob>> {
279        let mut buffer = [0u8; CONDITION_BLOB_SIZE];
280
281        let result_code = unsafe {
282            get_ledger_obj_field(
283                self.get_slot_num(),
284                sfield::Condition.into(),
285                buffer.as_mut_ptr(),
286                buffer.len(),
287            )
288        };
289
290        match_result_code_optional(result_code, || {
291            if result_code > 0 {
292                let blob = ConditionBlob {
293                    data: buffer,
294                    len: result_code as usize,
295                };
296                Some(blob)
297            } else {
298                None
299            }
300        })
301    }
302
303    /// The destination address where the XRP is paid if the escrow is successful.
304    fn get_destination(&self) -> Result<AccountID> {
305        ledger_object::get_field(self.get_slot_num(), sfield::Destination)
306    }
307
308    /// A hint indicating which page of the destination's owner directory links to this object, in
309    /// case the directory consists of multiple pages. Omitted on escrows created before enabling the fix1523 amendment.
310    fn get_destination_node(&self) -> Result<Option<u64>> {
311        ledger_object::get_field_optional(self.get_slot_num(), sfield::DestinationNode)
312    }
313
314    /// An arbitrary tag to further specify the destination for this escrow, such as a hosted
315    /// recipient at the destination address.
316    fn get_destination_tag(&self) -> Result<Option<u32>> {
317        ledger_object::get_field_optional(self.get_slot_num(), sfield::DestinationTag)
318    }
319
320    /// The time, in seconds since the Ripple Epoch, after which this escrow can be finished. Any
321    /// EscrowFinish transaction before this time fails. (Specifically, this is compared with the
322    /// close time of the previous validated ledger.)
323    fn get_finish_after(&self) -> Result<Option<u32>> {
324        ledger_object::get_field_optional(self.get_slot_num(), sfield::FinishAfter)
325    }
326
327    // TODO: Implement this function.
328    // /// The value 0x0075, mapped to the string Escrow, indicates that this is an Escrow entry.
329    // fn get_ledger_entry_type(&self) -> Result<LedgerEntryType> {
330    //     return Ok(LedgerEntryType::Escrow);
331    // }
332
333    /// A hint indicating which page of the sender's owner directory links to this entry, in case
334    /// the directory consists of multiple pages.
335    fn get_owner_node(&self) -> Result<u64> {
336        ledger_object::get_field(self.get_slot_num(), sfield::OwnerNode)
337    }
338
339    /// The identifying hash of the transaction that most recently modified this entry.
340    fn get_previous_txn_id(&self) -> Result<Hash256> {
341        ledger_object::get_field(self.get_slot_num(), sfield::PreviousTxnID)
342    }
343
344    /// The index of the ledger that contains the transaction that most recently modified this
345    /// entry.
346    fn get_previous_txn_lgr_seq(&self) -> Result<u32> {
347        ledger_object::get_field(self.get_slot_num(), sfield::PreviousTxnLgrSeq)
348    }
349
350    /// An arbitrary tag to further specify the source for this escrow, such as a hosted recipient
351    /// at the owner's address.
352    fn get_source_tag(&self) -> Result<Option<u32>> {
353        ledger_object::get_field_optional(self.get_slot_num(), sfield::SourceTag)
354    }
355
356    /// The WASM code that is executing.
357    fn get_finish_function(&self) -> Result<Option<StandardBlob>> {
358        ledger_object::get_field_optional(self.get_slot_num(), sfield::FinishFunction)
359    }
360
361    /// Retrieves the contract data from the specified ledger object.
362    ///
363    /// This function fetches the `data` field from the ledger object at the specified register
364    /// and returns it as a ContractData structure. The data is read into a fixed-size buffer
365    /// of XRPL_CONTRACT_DATA_SIZE.
366    ///
367    /// # Arguments
368    ///
369    /// * `register_num` - The register number where the ledger object is stored
370    ///
371    /// # Returns
372    ///
373    /// Returns a `Result<ContractData>` where:
374    /// * `Ok(ContractData)` - Contains the retrieved data and its actual length
375    /// * `Err(Error)` - If the retrieval operation failed
376    fn get_data(&self) -> Result<ContractData> {
377        let mut data: [u8; XRPL_CONTRACT_DATA_SIZE] = [0; XRPL_CONTRACT_DATA_SIZE];
378
379        let result_code = unsafe {
380            get_ledger_obj_field(
381                self.get_slot_num(),
382                sfield::Data.into(),
383                data.as_mut_ptr(),
384                data.len(),
385            )
386        };
387
388        match result_code {
389            code if code >= 0 => Ok(ContractData {
390                data,
391                len: code as usize,
392            }),
393            code => Err(Error::from_code(code)),
394        }
395    }
396}
397
398/// Trait providing access to fields specific to AccountRoot objects in any ledger.
399///
400/// This trait extends `LedgerObjectCommonFields` and provides methods to access
401/// fields that are specific to Escrow objects in any ledger, not just the current one.
402/// Each method requires a register number to identify which ledger object to access.
403pub trait AccountFields: LedgerObjectCommonFields {
404    /// The identifying address of the account.
405    fn get_account(&self) -> Result<AccountID> {
406        ledger_object::get_field(self.get_slot_num(), sfield::Account)
407    }
408
409    /// AccountTxnID field for the account.
410    fn account_txn_id(&self) -> Result<Option<Hash256>> {
411        ledger_object::get_field_optional(self.get_slot_num(), sfield::AccountTxnID)
412    }
413
414    /// The ledger entry ID of the corresponding AMM ledger entry. Set during account creation; cannot be modified.
415    /// If present, indicates that this is a special AMM AccountRoot; always omitted on non-AMM accounts.
416    /// (Added by the AMM amendment)
417    fn amm_id(&self) -> Result<Option<Hash256>> {
418        ledger_object::get_field_optional(self.get_slot_num(), sfield::AMMID)
419    }
420
421    /// The account's current XRP balance in drops.
422    fn balance(&self) -> Result<Option<Amount>> {
423        ledger_object::get_field_optional(self.get_slot_num(), sfield::Balance)
424    }
425
426    /// How many total of this account's issued non-fungible tokens have been burned.
427    /// This number is always equal or less than MintedNFTokens.
428    fn burned_nf_tokens(&self) -> Result<Option<u32>> {
429        ledger_object::get_field_optional(self.get_slot_num(), sfield::BurnedNFTokens)
430    }
431
432    /// A domain associated with this account. In JSON, this is the hexadecimal for the ASCII representation of the
433    /// domain. Cannot be more than 256 bytes in length.
434    fn domain(&self) -> Result<Option<StandardBlob>> {
435        ledger_object::get_field_optional(self.get_slot_num(), sfield::Domain)
436    }
437
438    /// The MD5 hash of an email address. Clients can use this to look up an avatar through services such as Gravatar.
439    fn email_hash(&self) -> Result<Option<Hash128>> {
440        ledger_object::get_field_optional(self.get_slot_num(), sfield::EmailHash)
441    }
442
443    /// The account's Sequence Number at the time it minted its first non-fungible-token.
444    /// (Added by the fixNFTokenRemint amendment)
445    fn first_nf_token_sequence(&self) -> Result<Option<u32>> {
446        ledger_object::get_field_optional(self.get_slot_num(), sfield::FirstNFTokenSequence)
447    }
448
449    /// The value 0x0061, mapped to the string AccountRoot, indicates that this is an AccountRoot object.
450    fn ledger_entry_type(&self) -> Result<u16> {
451        ledger_object::get_field(self.get_slot_num(), sfield::LedgerEntryType)
452    }
453
454    /// A public key that may be used to send encrypted messages to this account. In JSON, uses hexadecimal.
455    /// Must be exactly 33 bytes, with the first byte indicating the key type: 0x02 or 0x03 for secp256k1 keys,
456    /// 0xED for Ed25519 keys.
457    fn message_key(&self) -> Result<Option<StandardBlob>> {
458        ledger_object::get_field_optional(self.get_slot_num(), sfield::MessageKey)
459    }
460
461    /// How many total non-fungible tokens have been minted by and on behalf of this account.
462    /// (Added by the NonFungibleTokensV1_1 amendment)
463    fn minted_nf_tokens(&self) -> Result<Option<u32>> {
464        ledger_object::get_field_optional(self.get_slot_num(), sfield::MintedNFTokens)
465    }
466
467    /// Another account that can mint non-fungible tokens on behalf of this account.
468    /// (Added by the NonFungibleTokensV1_1 amendment)
469    fn nf_token_minter(&self) -> Result<Option<AccountID>> {
470        ledger_object::get_field_optional(self.get_slot_num(), sfield::NFTokenMinter)
471    }
472
473    /// The number of objects this account owns in the ledger, which contributes to its owner reserve.
474    fn owner_count(&self) -> Result<u32> {
475        ledger_object::get_field(self.get_slot_num(), sfield::OwnerCount)
476    }
477
478    /// The identifying hash of the transaction that most recently modified this object.
479    fn previous_txn_id(&self) -> Result<Hash256> {
480        ledger_object::get_field(self.get_slot_num(), sfield::PreviousTxnID)
481    }
482
483    /// The index of the ledger that contains the transaction that most recently modified this object.
484    fn previous_txn_lgr_seq(&self) -> Result<u32> {
485        ledger_object::get_field(self.get_slot_num(), sfield::PreviousTxnLgrSeq)
486    }
487
488    /// The address of a key pair that can be used to sign transactions for this account instead of the master key.
489    /// Use a SetRegularKey transaction to change this value.
490    fn regular_key(&self) -> Result<Option<AccountID>> {
491        ledger_object::get_field_optional(self.get_slot_num(), sfield::RegularKey)
492    }
493
494    /// The sequence number of the next valid transaction for this account.
495    fn sequence(&self) -> Result<u32> {
496        ledger_object::get_field(self.get_slot_num(), sfield::Sequence)
497    }
498
499    /// How many Tickets this account owns in the ledger. This is updated automatically to ensure that
500    /// the account stays within the hard limit of 250 Tickets at a time. This field is omitted if the account has zero
501    /// Tickets. (Added by the TicketBatch amendment.)
502    fn ticket_count(&self) -> Result<Option<u32>> {
503        ledger_object::get_field_optional(self.get_slot_num(), sfield::TicketCount)
504    }
505
506    /// How many significant digits to use for exchange rates of Offers involving currencies issued by this address.
507    /// Valid values are 3 to 15, inclusive. (Added by the TickSize amendment.)
508    fn tick_size(&self) -> Result<Option<u8>> {
509        ledger_object::get_field_optional(self.get_slot_num(), sfield::TickSize)
510    }
511
512    /// A transfer fee to charge other users for sending currency issued by this account to each other.
513    fn transfer_rate(&self) -> Result<Option<u32>> {
514        ledger_object::get_field_optional(self.get_slot_num(), sfield::TransferRate)
515    }
516
517    /// An arbitrary 256-bit value that users can set.
518    fn wallet_locator(&self) -> Result<Option<Hash256>> {
519        ledger_object::get_field_optional(self.get_slot_num(), sfield::WalletLocator)
520    }
521}
522
523#[cfg(test)]
524mod tests {
525    use super::*;
526    use crate::core::ledger_objects::LedgerObjectFieldGetter;
527    use crate::core::ledger_objects::account_root::AccountRoot;
528    use crate::host::error_codes::{FIELD_NOT_FOUND, INTERNAL_ERROR, INVALID_FIELD};
529    use crate::host::host_bindings_trait::MockHostBindings;
530    use crate::sfield::SField;
531    use mockall::predicate::{always, eq};
532
533    // ========================================
534    // Test helper functions
535    // ========================================
536
537    /// Helper to set up a mock expectation for get_current_ledger_obj_field
538    ///
539    /// Sets up a mock expectation that will match calls with:
540    /// - field: The SField with the specified CODE
541    /// - size: The expected buffer size
542    /// - times: How many times this expectation should be matched
543    ///
544    /// When a test fails, mockall will show which parameter didn't match.
545    fn expect_current_field<
546        T: LedgerObjectFieldGetter + Send + std::fmt::Debug + PartialEq + 'static,
547        const CODE: i32,
548    >(
549        mock: &mut MockHostBindings,
550        _field: SField<T, CODE>,
551        size: usize,
552        times: usize,
553    ) {
554        mock.expect_get_current_ledger_obj_field()
555            .with(eq(CODE), always(), eq(size))
556            .times(times)
557            .returning(move |_, _, _| size as i32);
558    }
559
560    /// Helper to set up a mock expectation for get_ledger_obj_field
561    ///
562    /// Sets up a mock expectation that will match calls with:
563    /// - slot: The ledger object slot number
564    /// - field: The SField with the specified CODE
565    /// - size: The expected buffer size
566    /// - times: How many times this expectation should be matched
567    ///
568    /// When a test fails, mockall will show which parameter didn't match.
569    fn expect_ledger_field<
570        T: LedgerObjectFieldGetter + Send + std::fmt::Debug + PartialEq + 'static,
571        const CODE: i32,
572    >(
573        mock: &mut MockHostBindings,
574        slot: i32,
575        _field: SField<T, CODE>,
576        size: usize,
577        times: usize,
578    ) {
579        mock.expect_get_ledger_obj_field()
580            .with(eq(slot), eq(CODE), always(), eq(size))
581            .times(times)
582            .returning(move |_, _, _, _| size as i32);
583    }
584
585    mod ledger_object_common_fields {
586        use super::*;
587        use crate::host::setup_mock;
588
589        #[test]
590        fn test_mandatory_fields_return_ok() {
591            let mut mock = MockHostBindings::new();
592
593            // get_flags
594            expect_ledger_field(&mut mock, 1, sfield::Flags, 4, 1);
595            // get_ledger_entry_type
596            expect_ledger_field(&mut mock, 1, sfield::LedgerEntryType, 2, 1);
597
598            let _guard = setup_mock(mock);
599
600            let account = AccountRoot { slot_num: 1 };
601
602            // All mandatory fields should return Ok
603            assert!(account.get_flags().is_ok());
604            assert!(account.get_ledger_entry_type().is_ok());
605        }
606
607        #[test]
608        fn test_mandatory_fields_return_error_on_internal_error() {
609            let mut mock = MockHostBindings::new();
610
611            // get_flags with INTERNAL_ERROR
612            mock.expect_get_ledger_obj_field()
613                .with(eq(1), eq(sfield::Flags), always(), eq(4))
614                .times(1)
615                .returning(|_, _, _, _| INTERNAL_ERROR);
616
617            let _guard = setup_mock(mock);
618
619            let account = AccountRoot { slot_num: 1 };
620            let result = account.get_flags();
621
622            assert!(result.is_err());
623            assert_eq!(result.err().unwrap().code(), INTERNAL_ERROR);
624        }
625
626        #[test]
627        fn test_get_ledger_entry_type_returns_error_on_internal_error() {
628            let mut mock = MockHostBindings::new();
629
630            mock.expect_get_ledger_obj_field()
631                .with(eq(1), eq(sfield::LedgerEntryType), always(), eq(2))
632                .times(1)
633                .returning(|_, _, _, _| INTERNAL_ERROR);
634
635            let _guard = setup_mock(mock);
636
637            let account = AccountRoot { slot_num: 1 };
638            let result = account.get_ledger_entry_type();
639
640            assert!(result.is_err());
641            assert_eq!(result.err().unwrap().code(), INTERNAL_ERROR);
642        }
643
644        #[test]
645        fn test_mandatory_fields_return_error_on_invalid_field() {
646            let mut mock = MockHostBindings::new();
647
648            // get_flags with INVALID_FIELD
649            mock.expect_get_ledger_obj_field()
650                .with(eq(1), eq(sfield::Flags), always(), eq(4))
651                .times(1)
652                .returning(|_, _, _, _| INVALID_FIELD);
653
654            let _guard = setup_mock(mock);
655
656            let account = AccountRoot { slot_num: 1 };
657            let result = account.get_flags();
658
659            assert!(result.is_err());
660            assert_eq!(result.err().unwrap().code(), INVALID_FIELD);
661        }
662    }
663
664    mod account_fields {
665        use super::*;
666        use crate::host::setup_mock;
667
668        #[test]
669        fn test_mandatory_fields_return_ok() {
670            let mut mock = MockHostBindings::new();
671
672            // get_account
673            expect_ledger_field(&mut mock, 1, sfield::Account, 20, 1);
674            // owner_count
675            expect_ledger_field(&mut mock, 1, sfield::OwnerCount, 4, 1);
676            // previous_txn_id
677            expect_ledger_field(&mut mock, 1, sfield::PreviousTxnID, 32, 1);
678            // previous_txn_lgr_seq
679            expect_ledger_field(&mut mock, 1, sfield::PreviousTxnLgrSeq, 4, 1);
680            // sequence
681            expect_ledger_field(&mut mock, 1, sfield::Sequence, 4, 1);
682            // ledger_entry_type
683            expect_ledger_field(&mut mock, 1, sfield::LedgerEntryType, 2, 1);
684
685            let _guard = setup_mock(mock);
686
687            let account = AccountRoot { slot_num: 1 };
688
689            // All mandatory fields should return Ok
690            assert!(account.get_account().is_ok());
691            assert!(account.owner_count().is_ok());
692            assert!(account.previous_txn_id().is_ok());
693            assert!(account.previous_txn_lgr_seq().is_ok());
694            assert!(account.sequence().is_ok());
695            assert!(account.ledger_entry_type().is_ok());
696        }
697
698        #[test]
699        fn test_optional_fields_return_some() {
700            let mut mock = MockHostBindings::new();
701
702            // account_txn_id
703            expect_ledger_field(&mut mock, 1, sfield::AccountTxnID, 32, 1);
704            // amm_id
705            expect_ledger_field(&mut mock, 1, sfield::AMMID, 32, 1);
706            // balance
707            expect_ledger_field(&mut mock, 1, sfield::Balance, 48, 1);
708            // burned_nf_tokens
709            expect_ledger_field(&mut mock, 1, sfield::BurnedNFTokens, 4, 1);
710            // domain - StandardBlob uses 1024 bytes
711            expect_ledger_field(&mut mock, 1, sfield::Domain, 1024, 1);
712            // email_hash
713            expect_ledger_field(&mut mock, 1, sfield::EmailHash, 16, 1);
714            // first_nf_token_sequence
715            expect_ledger_field(&mut mock, 1, sfield::FirstNFTokenSequence, 4, 1);
716            // message_key - StandardBlob uses 1024 bytes
717            expect_ledger_field(&mut mock, 1, sfield::MessageKey, 1024, 1);
718            // minted_nf_tokens
719            expect_ledger_field(&mut mock, 1, sfield::MintedNFTokens, 4, 1);
720            // nf_token_minter
721            expect_ledger_field(&mut mock, 1, sfield::NFTokenMinter, 20, 1);
722            // regular_key
723            expect_ledger_field(&mut mock, 1, sfield::RegularKey, 20, 1);
724            // ticket_count
725            expect_ledger_field(&mut mock, 1, sfield::TicketCount, 4, 1);
726            // tick_size
727            expect_ledger_field(&mut mock, 1, sfield::TickSize, 1, 1);
728            // transfer_rate
729            expect_ledger_field(&mut mock, 1, sfield::TransferRate, 4, 1);
730            // wallet_locator
731            expect_ledger_field(&mut mock, 1, sfield::WalletLocator, 32, 1);
732
733            let _guard = setup_mock(mock);
734
735            let account = AccountRoot { slot_num: 1 };
736
737            // All optional fields should return Ok(Some(...))
738            assert!(account.account_txn_id().unwrap().is_some());
739            assert!(account.amm_id().unwrap().is_some());
740            assert!(account.balance().unwrap().is_some());
741            assert!(account.burned_nf_tokens().unwrap().is_some());
742            assert!(account.domain().unwrap().is_some());
743            assert!(account.email_hash().unwrap().is_some());
744            assert!(account.first_nf_token_sequence().unwrap().is_some());
745            assert!(account.message_key().unwrap().is_some());
746            assert!(account.minted_nf_tokens().unwrap().is_some());
747            assert!(account.nf_token_minter().unwrap().is_some());
748            assert!(account.regular_key().unwrap().is_some());
749            assert!(account.ticket_count().unwrap().is_some());
750            assert!(account.tick_size().unwrap().is_some());
751            assert!(account.transfer_rate().unwrap().is_some());
752            assert!(account.wallet_locator().unwrap().is_some());
753        }
754
755        #[test]
756        fn test_optional_fields_return_none_when_field_not_found() {
757            let mut mock = MockHostBindings::new();
758
759            // account_txn_id
760            mock.expect_get_ledger_obj_field()
761                .with(eq(1), eq(sfield::AccountTxnID), always(), eq(32))
762                .times(1)
763                .returning(|_, _, _, _| FIELD_NOT_FOUND);
764            // amm_id
765            mock.expect_get_ledger_obj_field()
766                .with(eq(1), eq(sfield::AMMID), always(), eq(32))
767                .times(1)
768                .returning(|_, _, _, _| FIELD_NOT_FOUND);
769            // balance - variable size field, returns 0 for empty (Some with len=0)
770            mock.expect_get_ledger_obj_field()
771                .with(eq(1), eq(sfield::Balance), always(), eq(48))
772                .times(1)
773                .returning(|_, _, _, _| 0);
774            // burned_nf_tokens
775            mock.expect_get_ledger_obj_field()
776                .with(eq(1), eq(sfield::BurnedNFTokens), always(), eq(4))
777                .times(1)
778                .returning(|_, _, _, _| FIELD_NOT_FOUND);
779            // domain - variable size field, returns 0 for empty (Some with len=0) - StandardBlob uses 1024 bytes
780            mock.expect_get_ledger_obj_field()
781                .with(eq(1), eq(sfield::Domain), always(), eq(1024))
782                .times(1)
783                .returning(|_, _, _, _| 0);
784            // email_hash
785            mock.expect_get_ledger_obj_field()
786                .with(eq(1), eq(sfield::EmailHash), always(), eq(16))
787                .times(1)
788                .returning(|_, _, _, _| FIELD_NOT_FOUND);
789            // first_nf_token_sequence
790            mock.expect_get_ledger_obj_field()
791                .with(eq(1), eq(sfield::FirstNFTokenSequence), always(), eq(4))
792                .times(1)
793                .returning(|_, _, _, _| FIELD_NOT_FOUND);
794            // message_key - variable size field, returns 0 for empty (Some with len=0) - StandardBlob uses 1024 bytes
795            mock.expect_get_ledger_obj_field()
796                .with(eq(1), eq(sfield::MessageKey), always(), eq(1024))
797                .times(1)
798                .returning(|_, _, _, _| 0);
799            // minted_nf_tokens
800            mock.expect_get_ledger_obj_field()
801                .with(eq(1), eq(sfield::MintedNFTokens), always(), eq(4))
802                .times(1)
803                .returning(|_, _, _, _| FIELD_NOT_FOUND);
804            // nf_token_minter
805            mock.expect_get_ledger_obj_field()
806                .with(eq(1), eq(sfield::NFTokenMinter), always(), eq(20))
807                .times(1)
808                .returning(|_, _, _, _| FIELD_NOT_FOUND);
809            // regular_key
810            mock.expect_get_ledger_obj_field()
811                .with(eq(1), eq(sfield::RegularKey), always(), eq(20))
812                .times(1)
813                .returning(|_, _, _, _| FIELD_NOT_FOUND);
814            // ticket_count
815            mock.expect_get_ledger_obj_field()
816                .with(eq(1), eq(sfield::TicketCount), always(), eq(4))
817                .times(1)
818                .returning(|_, _, _, _| FIELD_NOT_FOUND);
819            // tick_size
820            mock.expect_get_ledger_obj_field()
821                .with(eq(1), eq(sfield::TickSize), always(), eq(1))
822                .times(1)
823                .returning(|_, _, _, _| FIELD_NOT_FOUND);
824            // transfer_rate
825            mock.expect_get_ledger_obj_field()
826                .with(eq(1), eq(sfield::TransferRate), always(), eq(4))
827                .times(1)
828                .returning(|_, _, _, _| FIELD_NOT_FOUND);
829            // wallet_locator
830            mock.expect_get_ledger_obj_field()
831                .with(eq(1), eq(sfield::WalletLocator), always(), eq(32))
832                .times(1)
833                .returning(|_, _, _, _| FIELD_NOT_FOUND);
834
835            let _guard = setup_mock(mock);
836
837            let account = AccountRoot { slot_num: 1 };
838
839            // Fixed-size optional fields should return Ok(None) when FIELD_NOT_FOUND
840            assert!(account.account_txn_id().unwrap().is_none());
841            assert!(account.amm_id().unwrap().is_none());
842            assert!(account.burned_nf_tokens().unwrap().is_none());
843            assert!(account.email_hash().unwrap().is_none());
844            assert!(account.first_nf_token_sequence().unwrap().is_none());
845            assert!(account.minted_nf_tokens().unwrap().is_none());
846            assert!(account.nf_token_minter().unwrap().is_none());
847            assert!(account.regular_key().unwrap().is_none());
848            assert!(account.ticket_count().unwrap().is_none());
849            assert!(account.tick_size().unwrap().is_none());
850            assert!(account.transfer_rate().unwrap().is_none());
851            assert!(account.wallet_locator().unwrap().is_none());
852
853            // Variable-size optional fields return Some with len=0 when not found
854            // (they cannot distinguish between "not present" and "present with 0 bytes")
855            let balance = account.balance().unwrap();
856            assert!(balance.is_some());
857            let domain = account.domain().unwrap();
858            assert!(domain.is_some());
859            assert_eq!(domain.unwrap().len, 0);
860            let message_key = account.message_key().unwrap();
861            assert!(message_key.is_some());
862            assert_eq!(message_key.unwrap().len, 0);
863        }
864
865        #[test]
866        fn test_mandatory_fields_return_error_on_internal_error() {
867            let mut mock = MockHostBindings::new();
868
869            // get_account with INTERNAL_ERROR
870            mock.expect_get_ledger_obj_field()
871                .with(eq(1), eq(sfield::Account), always(), eq(20))
872                .times(1)
873                .returning(|_, _, _, _| INTERNAL_ERROR);
874
875            let _guard = setup_mock(mock);
876
877            let account = AccountRoot { slot_num: 1 };
878            let result = account.get_account();
879
880            assert!(result.is_err());
881            assert_eq!(result.err().unwrap().code(), INTERNAL_ERROR);
882        }
883
884        #[test]
885        fn test_mandatory_fields_return_error_on_invalid_field() {
886            let mut mock = MockHostBindings::new();
887
888            // get_account with INVALID_FIELD
889            mock.expect_get_ledger_obj_field()
890                .with(eq(1), eq(sfield::Account), always(), eq(20))
891                .times(1)
892                .returning(|_, _, _, _| INVALID_FIELD);
893
894            let _guard = setup_mock(mock);
895
896            let account = AccountRoot { slot_num: 1 };
897            let result = account.get_account();
898
899            assert!(result.is_err());
900            assert_eq!(result.err().unwrap().code(), INVALID_FIELD);
901        }
902    }
903
904    mod current_ledger_object_common_fields {
905        use super::*;
906        use crate::core::ledger_objects::current_escrow::CurrentEscrow;
907        use crate::host::setup_mock;
908
909        #[test]
910        fn test_mandatory_fields_return_ok() {
911            let mut mock = MockHostBindings::new();
912
913            // get_flags
914            expect_current_field(&mut mock, sfield::Flags, 4, 1);
915            // get_ledger_entry_type
916            expect_current_field(&mut mock, sfield::LedgerEntryType, 2, 1);
917
918            let _guard = setup_mock(mock);
919
920            let escrow = CurrentEscrow;
921
922            // All mandatory fields should return Ok
923            assert!(escrow.get_flags().is_ok());
924            assert!(escrow.get_ledger_entry_type().is_ok());
925        }
926
927        #[test]
928        fn test_mandatory_fields_return_error_on_internal_error() {
929            let mut mock = MockHostBindings::new();
930
931            // get_flags with INTERNAL_ERROR
932            mock.expect_get_current_ledger_obj_field()
933                .with(eq(sfield::Flags), always(), eq(4))
934                .times(1)
935                .returning(|_, _, _| INTERNAL_ERROR);
936
937            let _guard = setup_mock(mock);
938
939            let escrow = CurrentEscrow;
940            let result = escrow.get_flags();
941
942            assert!(result.is_err());
943            assert_eq!(result.err().unwrap().code(), INTERNAL_ERROR);
944        }
945
946        #[test]
947        fn test_get_ledger_entry_type_returns_error_on_internal_error() {
948            let mut mock = MockHostBindings::new();
949
950            mock.expect_get_current_ledger_obj_field()
951                .with(eq(sfield::LedgerEntryType), always(), eq(2))
952                .times(1)
953                .returning(|_, _, _| INTERNAL_ERROR);
954
955            let _guard = setup_mock(mock);
956
957            let escrow = CurrentEscrow;
958            let result = escrow.get_ledger_entry_type();
959
960            assert!(result.is_err());
961            assert_eq!(result.err().unwrap().code(), INTERNAL_ERROR);
962        }
963
964        #[test]
965        fn test_mandatory_fields_return_error_on_invalid_field() {
966            let mut mock = MockHostBindings::new();
967
968            // get_flags with INVALID_FIELD
969            mock.expect_get_current_ledger_obj_field()
970                .with(eq(sfield::Flags), always(), eq(4))
971                .times(1)
972                .returning(|_, _, _| INVALID_FIELD);
973
974            let _guard = setup_mock(mock);
975
976            let escrow = CurrentEscrow;
977            let result = escrow.get_flags();
978
979            assert!(result.is_err());
980            assert_eq!(result.err().unwrap().code(), INVALID_FIELD);
981        }
982    }
983
984    mod current_escrow_fields {
985        use super::*;
986        use crate::core::ledger_objects::current_escrow::CurrentEscrow;
987        use crate::host::setup_mock;
988
989        #[test]
990        fn test_mandatory_fields_return_ok() {
991            let mut mock = MockHostBindings::new();
992
993            // get_account
994            expect_current_field(&mut mock, sfield::Account, 20, 1);
995            // get_amount
996            expect_current_field(&mut mock, sfield::Amount, 48, 1);
997            // get_destination
998            expect_current_field(&mut mock, sfield::Destination, 20, 1);
999            // get_owner_node
1000            expect_current_field(&mut mock, sfield::OwnerNode, 8, 1);
1001            // get_previous_txn_id
1002            expect_current_field(&mut mock, sfield::PreviousTxnID, 32, 1);
1003            // get_previous_txn_lgr_seq
1004            expect_current_field(&mut mock, sfield::PreviousTxnLgrSeq, 4, 1);
1005            // get_data (mandatory for escrow)
1006            expect_current_field(&mut mock, sfield::Data, 4096, 1);
1007
1008            let _guard = setup_mock(mock);
1009
1010            let escrow = CurrentEscrow;
1011
1012            // All mandatory fields should return Ok
1013            assert!(escrow.get_account().is_ok());
1014            assert!(escrow.get_amount().is_ok());
1015            assert!(escrow.get_destination().is_ok());
1016            assert!(escrow.get_owner_node().is_ok());
1017            assert!(escrow.get_previous_txn_id().is_ok());
1018            assert!(escrow.get_previous_txn_lgr_seq().is_ok());
1019            assert!(escrow.get_data().is_ok());
1020        }
1021
1022        #[test]
1023        fn test_optional_fields_return_some() {
1024            let mut mock = MockHostBindings::new();
1025
1026            // get_cancel_after
1027            expect_current_field(&mut mock, sfield::CancelAfter, 4, 1);
1028            // get_condition
1029            expect_current_field(&mut mock, sfield::Condition, 128, 1);
1030            // get_destination_node
1031            expect_current_field(&mut mock, sfield::DestinationNode, 8, 1);
1032            // get_destination_tag
1033            expect_current_field(&mut mock, sfield::DestinationTag, 4, 1);
1034            // get_finish_after
1035            expect_current_field(&mut mock, sfield::FinishAfter, 4, 1);
1036            // get_source_tag
1037            expect_current_field(&mut mock, sfield::SourceTag, 4, 1);
1038            // get_finish_function - StandardBlob uses 1024 bytes
1039            expect_current_field(&mut mock, sfield::FinishFunction, 1024, 1);
1040
1041            let _guard = setup_mock(mock);
1042
1043            let escrow = CurrentEscrow;
1044
1045            // All optional fields should return Ok(Some(...))
1046            assert!(escrow.get_cancel_after().unwrap().is_some());
1047            assert!(escrow.get_condition().unwrap().is_some());
1048            assert!(escrow.get_destination_node().unwrap().is_some());
1049            assert!(escrow.get_destination_tag().unwrap().is_some());
1050            assert!(escrow.get_finish_after().unwrap().is_some());
1051            assert!(escrow.get_source_tag().unwrap().is_some());
1052            assert!(escrow.get_finish_function().unwrap().is_some());
1053        }
1054
1055        #[test]
1056        fn test_optional_fields_return_none_when_field_not_found() {
1057            let mut mock = MockHostBindings::new();
1058
1059            // get_cancel_after
1060            mock.expect_get_current_ledger_obj_field()
1061                .with(eq(sfield::CancelAfter), always(), eq(4))
1062                .times(1)
1063                .returning(|_, _, _| FIELD_NOT_FOUND);
1064            // get_condition - returns 0 for None
1065            mock.expect_get_current_ledger_obj_field()
1066                .with(eq(sfield::Condition), always(), eq(128))
1067                .times(1)
1068                .returning(|_, _, _| 0);
1069            // get_destination_node
1070            mock.expect_get_current_ledger_obj_field()
1071                .with(eq(sfield::DestinationNode), always(), eq(8))
1072                .times(1)
1073                .returning(|_, _, _| FIELD_NOT_FOUND);
1074            // get_destination_tag
1075            mock.expect_get_current_ledger_obj_field()
1076                .with(eq(sfield::DestinationTag), always(), eq(4))
1077                .times(1)
1078                .returning(|_, _, _| FIELD_NOT_FOUND);
1079            // get_finish_after
1080            mock.expect_get_current_ledger_obj_field()
1081                .with(eq(sfield::FinishAfter), always(), eq(4))
1082                .times(1)
1083                .returning(|_, _, _| FIELD_NOT_FOUND);
1084            // get_source_tag
1085            mock.expect_get_current_ledger_obj_field()
1086                .with(eq(sfield::SourceTag), always(), eq(4))
1087                .times(1)
1088                .returning(|_, _, _| FIELD_NOT_FOUND);
1089            // get_finish_function - variable size field, returns 0 for empty (Some with len=0) - StandardBlob uses 1024 bytes
1090            mock.expect_get_current_ledger_obj_field()
1091                .with(eq(sfield::FinishFunction), always(), eq(1024))
1092                .times(1)
1093                .returning(|_, _, _| 0);
1094
1095            let _guard = setup_mock(mock);
1096
1097            let escrow = CurrentEscrow;
1098
1099            // Fixed-size optional fields should return Ok(None) when FIELD_NOT_FOUND
1100            assert!(escrow.get_cancel_after().unwrap().is_none());
1101            assert!(escrow.get_condition().unwrap().is_none());
1102            assert!(escrow.get_destination_node().unwrap().is_none());
1103            assert!(escrow.get_destination_tag().unwrap().is_none());
1104            assert!(escrow.get_finish_after().unwrap().is_none());
1105            assert!(escrow.get_source_tag().unwrap().is_none());
1106
1107            // Variable-size optional fields return Some with len=0 when not found
1108            let finish_function = escrow.get_finish_function().unwrap();
1109            assert!(finish_function.is_some());
1110            assert_eq!(finish_function.unwrap().len, 0);
1111        }
1112
1113        #[test]
1114        fn test_mandatory_fields_return_error_on_internal_error() {
1115            let mut mock = MockHostBindings::new();
1116
1117            // get_account with INTERNAL_ERROR
1118            mock.expect_get_current_ledger_obj_field()
1119                .with(eq(sfield::Account), always(), eq(20))
1120                .times(1)
1121                .returning(|_, _, _| INTERNAL_ERROR);
1122
1123            let _guard = setup_mock(mock);
1124
1125            let escrow = CurrentEscrow;
1126            let result = escrow.get_account();
1127
1128            assert!(result.is_err());
1129            assert_eq!(result.err().unwrap().code(), INTERNAL_ERROR);
1130        }
1131
1132        #[test]
1133        fn test_get_data_returns_error_on_internal_error() {
1134            let mut mock = MockHostBindings::new();
1135
1136            mock.expect_get_current_ledger_obj_field()
1137                .with(eq(sfield::Data), always(), eq(4096))
1138                .times(1)
1139                .returning(|_, _, _| INTERNAL_ERROR);
1140
1141            let _guard = setup_mock(mock);
1142
1143            let escrow = CurrentEscrow;
1144            let result = escrow.get_data();
1145
1146            assert!(result.is_err());
1147            assert_eq!(result.err().unwrap().code(), INTERNAL_ERROR);
1148        }
1149
1150        #[test]
1151        fn test_mandatory_fields_return_error_on_invalid_field() {
1152            let mut mock = MockHostBindings::new();
1153
1154            // get_account with INVALID_FIELD
1155            mock.expect_get_current_ledger_obj_field()
1156                .with(eq(sfield::Account), always(), eq(20))
1157                .times(1)
1158                .returning(|_, _, _| INVALID_FIELD);
1159
1160            let _guard = setup_mock(mock);
1161
1162            let escrow = CurrentEscrow;
1163            let result = escrow.get_account();
1164
1165            assert!(result.is_err());
1166            assert_eq!(result.err().unwrap().code(), INVALID_FIELD);
1167        }
1168    }
1169
1170    mod escrow_fields {
1171        use super::*;
1172        use crate::core::ledger_objects::escrow::Escrow;
1173        use crate::host::setup_mock;
1174
1175        #[test]
1176        fn test_mandatory_fields_return_ok() {
1177            let mut mock = MockHostBindings::new();
1178
1179            // get_account
1180            expect_ledger_field(&mut mock, 1, sfield::Account, 20, 1);
1181            // get_amount
1182            expect_ledger_field(&mut mock, 1, sfield::Amount, 48, 1);
1183            // get_destination
1184            expect_ledger_field(&mut mock, 1, sfield::Destination, 20, 1);
1185            // get_owner_node
1186            expect_ledger_field(&mut mock, 1, sfield::OwnerNode, 8, 1);
1187            // get_previous_txn_id
1188            expect_ledger_field(&mut mock, 1, sfield::PreviousTxnID, 32, 1);
1189            // get_previous_txn_lgr_seq
1190            expect_ledger_field(&mut mock, 1, sfield::PreviousTxnLgrSeq, 4, 1);
1191            // get_data (mandatory for escrow)
1192            expect_ledger_field(&mut mock, 1, sfield::Data, 4096, 1);
1193
1194            let _guard = setup_mock(mock);
1195
1196            let escrow = Escrow { slot_num: 1 };
1197
1198            // All mandatory fields should return Ok
1199            assert!(escrow.get_account().is_ok());
1200            assert!(escrow.get_amount().is_ok());
1201            assert!(escrow.get_destination().is_ok());
1202            assert!(escrow.get_owner_node().is_ok());
1203            assert!(escrow.get_previous_txn_id().is_ok());
1204            assert!(escrow.get_previous_txn_lgr_seq().is_ok());
1205            assert!(escrow.get_data().is_ok());
1206        }
1207
1208        #[test]
1209        fn test_optional_fields_return_some() {
1210            let mut mock = MockHostBindings::new();
1211
1212            // get_cancel_after
1213            expect_ledger_field(&mut mock, 1, sfield::CancelAfter, 4, 1);
1214            // get_condition
1215            expect_ledger_field(&mut mock, 1, sfield::Condition, 128, 1);
1216            // get_destination_node
1217            expect_ledger_field(&mut mock, 1, sfield::DestinationNode, 8, 1);
1218            // get_destination_tag
1219            expect_ledger_field(&mut mock, 1, sfield::DestinationTag, 4, 1);
1220            // get_finish_after
1221            expect_ledger_field(&mut mock, 1, sfield::FinishAfter, 4, 1);
1222            // get_source_tag
1223            expect_ledger_field(&mut mock, 1, sfield::SourceTag, 4, 1);
1224            // get_finish_function - StandardBlob uses 1024 bytes
1225            expect_ledger_field(&mut mock, 1, sfield::FinishFunction, 1024, 1);
1226
1227            let _guard = setup_mock(mock);
1228
1229            let escrow = Escrow { slot_num: 1 };
1230
1231            // All optional fields should return Ok(Some(...))
1232            assert!(escrow.get_cancel_after().unwrap().is_some());
1233            assert!(escrow.get_condition().unwrap().is_some());
1234            assert!(escrow.get_destination_node().unwrap().is_some());
1235            assert!(escrow.get_destination_tag().unwrap().is_some());
1236            assert!(escrow.get_finish_after().unwrap().is_some());
1237            assert!(escrow.get_source_tag().unwrap().is_some());
1238            assert!(escrow.get_finish_function().unwrap().is_some());
1239        }
1240
1241        #[test]
1242        fn test_optional_fields_return_none_when_field_not_found() {
1243            let mut mock = MockHostBindings::new();
1244
1245            // get_cancel_after
1246            mock.expect_get_ledger_obj_field()
1247                .with(eq(1), eq(sfield::CancelAfter), always(), eq(4))
1248                .times(1)
1249                .returning(|_, _, _, _| FIELD_NOT_FOUND);
1250            // get_condition - returns 0 for None
1251            mock.expect_get_ledger_obj_field()
1252                .with(eq(1), eq(sfield::Condition), always(), eq(128))
1253                .times(1)
1254                .returning(|_, _, _, _| 0);
1255            // get_destination_node
1256            mock.expect_get_ledger_obj_field()
1257                .with(eq(1), eq(sfield::DestinationNode), always(), eq(8))
1258                .times(1)
1259                .returning(|_, _, _, _| FIELD_NOT_FOUND);
1260            // get_destination_tag
1261            mock.expect_get_ledger_obj_field()
1262                .with(eq(1), eq(sfield::DestinationTag), always(), eq(4))
1263                .times(1)
1264                .returning(|_, _, _, _| FIELD_NOT_FOUND);
1265            // get_finish_after
1266            mock.expect_get_ledger_obj_field()
1267                .with(eq(1), eq(sfield::FinishAfter), always(), eq(4))
1268                .times(1)
1269                .returning(|_, _, _, _| FIELD_NOT_FOUND);
1270            // get_source_tag
1271            mock.expect_get_ledger_obj_field()
1272                .with(eq(1), eq(sfield::SourceTag), always(), eq(4))
1273                .times(1)
1274                .returning(|_, _, _, _| FIELD_NOT_FOUND);
1275            // get_finish_function - variable size field, returns 0 for empty (Some with len=0) - StandardBlob uses 1024 bytes
1276            mock.expect_get_ledger_obj_field()
1277                .with(eq(1), eq(sfield::FinishFunction), always(), eq(1024))
1278                .times(1)
1279                .returning(|_, _, _, _| 0);
1280
1281            let _guard = setup_mock(mock);
1282
1283            let escrow = Escrow { slot_num: 1 };
1284
1285            // Fixed-size optional fields should return Ok(None) when FIELD_NOT_FOUND
1286            assert!(escrow.get_cancel_after().unwrap().is_none());
1287            assert!(escrow.get_condition().unwrap().is_none());
1288            assert!(escrow.get_destination_node().unwrap().is_none());
1289            assert!(escrow.get_destination_tag().unwrap().is_none());
1290            assert!(escrow.get_finish_after().unwrap().is_none());
1291            assert!(escrow.get_source_tag().unwrap().is_none());
1292
1293            // Variable-size optional fields return Some with len=0 when not found
1294            let finish_function = escrow.get_finish_function().unwrap();
1295            assert!(finish_function.is_some());
1296            assert_eq!(finish_function.unwrap().len, 0);
1297        }
1298
1299        #[test]
1300        fn test_mandatory_fields_return_error_on_internal_error() {
1301            let mut mock = MockHostBindings::new();
1302
1303            // get_account with INTERNAL_ERROR
1304            mock.expect_get_ledger_obj_field()
1305                .with(eq(1), eq(sfield::Account), always(), eq(20))
1306                .times(1)
1307                .returning(|_, _, _, _| INTERNAL_ERROR);
1308
1309            let _guard = setup_mock(mock);
1310
1311            let escrow = Escrow { slot_num: 1 };
1312            let result = escrow.get_account();
1313
1314            assert!(result.is_err());
1315            assert_eq!(result.err().unwrap().code(), INTERNAL_ERROR);
1316        }
1317
1318        #[test]
1319        fn test_get_data_returns_error_on_internal_error() {
1320            let mut mock = MockHostBindings::new();
1321
1322            mock.expect_get_ledger_obj_field()
1323                .with(eq(1), eq(sfield::Data), always(), eq(4096))
1324                .times(1)
1325                .returning(|_, _, _, _| INTERNAL_ERROR);
1326
1327            let _guard = setup_mock(mock);
1328
1329            let escrow = Escrow { slot_num: 1 };
1330            let result = escrow.get_data();
1331
1332            assert!(result.is_err());
1333            assert_eq!(result.err().unwrap().code(), INTERNAL_ERROR);
1334        }
1335
1336        #[test]
1337        fn test_mandatory_fields_return_error_on_invalid_field() {
1338            let mut mock = MockHostBindings::new();
1339
1340            // get_account with INVALID_FIELD
1341            mock.expect_get_ledger_obj_field()
1342                .with(eq(1), eq(sfield::Account), always(), eq(20))
1343                .times(1)
1344                .returning(|_, _, _, _| INVALID_FIELD);
1345
1346            let _guard = setup_mock(mock);
1347
1348            let escrow = Escrow { slot_num: 1 };
1349            let result = escrow.get_account();
1350
1351            assert!(result.is_err());
1352            assert_eq!(result.err().unwrap().code(), INVALID_FIELD);
1353        }
1354    }
1355}