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