Skip to main content

xrpl_wasm_stdlib/core/ledger_objects/
traits.rs

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