Skip to main content

xrpl_escrow_stdlib/ledger_objects/
traits.rs

1//! Escrow-specific ledger-object field accessor traits.
2
3use xrpl_common_stdlib::core::ledger_objects::current_ledger_object;
4use xrpl_common_stdlib::core::ledger_objects::traits::CurrentLedgerObjectCommonFields;
5use xrpl_common_stdlib::core::types::account_id::AccountID;
6use xrpl_common_stdlib::core::types::amount::Amount;
7use xrpl_common_stdlib::core::types::blob::{ConditionBlob, WasmBlob};
8use xrpl_common_stdlib::core::types::contract_data::{ContractData, XRPL_CONTRACT_DATA_SIZE};
9use xrpl_common_stdlib::core::types::uint::Hash256;
10use xrpl_common_stdlib::host::error_codes::{match_result_code, match_result_code_optional};
11use xrpl_common_stdlib::host::{Error, get_current_ledger_obj_field, update_data};
12use xrpl_common_stdlib::host::{Result, Result::Err, Result::Ok};
13use xrpl_common_stdlib::sfield;
14
15/// Trait providing access to fields specific to Escrow objects in the current ledger.
16///
17/// This trait extends `CurrentLedgerObjectCommonFields` and provides methods to access
18/// fields that are specific to Escrow objects in the current ledger being processed.
19pub trait CurrentEscrowFields: CurrentLedgerObjectCommonFields {
20    /// The address of the owner (sender) of this escrow. This is the account that provided the XRP
21    /// and gets it back if the escrow is canceled.
22    fn get_account(&self) -> Result<AccountID> {
23        current_ledger_object::get_field(sfield::Account)
24    }
25
26    /// The amount currently held in the escrow (could be XRP, IOU, or MPT).
27    fn get_amount(&self) -> Result<Amount> {
28        current_ledger_object::get_field(sfield::Amount)
29    }
30
31    /// The escrow can be canceled if and only if this field is present and the time it specifies
32    /// has passed. Specifically, this is specified as seconds since the Ripple Epoch and it
33    /// "has passed" if it's earlier than the close time of the previous validated ledger.
34    fn get_cancel_after(&self) -> Result<Option<u32>> {
35        current_ledger_object::get_field_optional(sfield::CancelAfter)
36    }
37
38    /// A PREIMAGE-SHA-256 crypto-condition in full crypto-condition format. If present, the EscrowFinish
39    /// transaction must contain a fulfillment that satisfies this condition.
40    fn get_condition(&self) -> Result<Option<ConditionBlob>> {
41        let mut buffer = ConditionBlob::new();
42        let result_code = unsafe {
43            get_current_ledger_obj_field(
44                sfield::Condition.into(),
45                buffer.data.as_mut_ptr(),
46                buffer.capacity(),
47            )
48        };
49        match_result_code_optional(result_code, || {
50            buffer.len = result_code as usize;
51            (result_code > 0).then_some(buffer)
52        })
53    }
54
55    /// The destination address where the XRP is paid if the escrow is successful.
56    fn get_destination(&self) -> Result<AccountID> {
57        current_ledger_object::get_field(sfield::Destination)
58    }
59
60    /// A hint indicating which page of the destination's owner directory links to this object, in
61    /// case the directory consists of multiple pages. Omitted on escrows created before enabling the fix1523 amendment.
62    fn get_destination_node(&self) -> Result<Option<u64>> {
63        current_ledger_object::get_field_optional(sfield::DestinationNode)
64    }
65
66    /// An arbitrary tag to further specify the destination for this escrow, such as a hosted
67    /// recipient at the destination address.
68    fn get_destination_tag(&self) -> Result<Option<u32>> {
69        current_ledger_object::get_field_optional(sfield::DestinationTag)
70    }
71
72    /// The time, in seconds since the Ripple Epoch, after which this escrow can be finished. Any
73    /// EscrowFinish transaction before this time fails. (Specifically, this is compared with the
74    /// close time of the previous validated ledger.)
75    fn get_finish_after(&self) -> Result<Option<u32>> {
76        current_ledger_object::get_field_optional(sfield::FinishAfter)
77    }
78
79    /// A hint indicating which page of the sender's owner directory links to this entry, in case
80    /// the directory consists of multiple pages.
81    fn get_owner_node(&self) -> Result<u64> {
82        current_ledger_object::get_field(sfield::OwnerNode)
83    }
84
85    /// The identifying hash of the transaction that most recently modified this entry.
86    fn get_previous_txn_id(&self) -> Result<Hash256> {
87        current_ledger_object::get_field(sfield::PreviousTxnID)
88    }
89
90    /// The index of the ledger that contains the transaction that most recently modified this
91    /// entry.
92    fn get_previous_txn_lgr_seq(&self) -> Result<u32> {
93        current_ledger_object::get_field(sfield::PreviousTxnLgrSeq)
94    }
95
96    /// An arbitrary tag to further specify the source for this escrow, such as a hosted recipient
97    /// at the owner's address.
98    fn get_source_tag(&self) -> Result<Option<u32>> {
99        current_ledger_object::get_field_optional(sfield::SourceTag)
100    }
101
102    /// The WASM code that is executing.
103    fn get_finish_function(&self) -> Result<Option<WasmBlob>> {
104        current_ledger_object::get_field_optional(sfield::FinishFunction)
105    }
106
107    /// Retrieves the contract `data` from the current escrow object.
108    ///
109    /// This function fetches the `data` field from the current ledger object and returns it as a
110    /// ContractData structure. The data is read into a fixed-size buffer of XRPL_CONTRACT_DATA_SIZE.
111    ///
112    /// # Returns
113    ///
114    /// Returns a `Result<ContractData>` where:
115    /// * `Ok(ContractData)` - Contains the retrieved data and its actual length
116    /// * `Err(Error)` - If the retrieval operation failed
117    fn get_data(&self) -> Result<ContractData> {
118        let mut data: [u8; XRPL_CONTRACT_DATA_SIZE] = [0; XRPL_CONTRACT_DATA_SIZE];
119
120        let result_code = unsafe {
121            get_current_ledger_obj_field(sfield::Data.into(), data.as_mut_ptr(), data.len())
122        };
123
124        match result_code {
125            code if code >= 0 => Ok(ContractData {
126                data,
127                len: code as usize,
128            }),
129            code => Err(Error::from_code(code)),
130        }
131    }
132
133    /// Updates the contract data in the current escrow object.
134    ///
135    /// # Arguments
136    ///
137    /// * `data` - The contract data to update
138    ///
139    /// # Returns
140    ///
141    /// Returns a `Result<()>` where:
142    /// * `Ok(())` - The data was successfully updated
143    /// * `Err(Error)` - If the update operation failed
144    fn update_current_escrow_data(data: ContractData) -> Result<()> {
145        // TODO: Make sure rippled always deletes any existing data bytes in rippled, and sets the new
146        // length to be `data.len` (e.g., if the developer writes 2 bytes, then that's the new
147        // length and any old bytes are lost).
148        let result_code = unsafe { update_data(data.data.as_ptr(), data.len) };
149        match_result_code(result_code, || ())
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use mockall::predicate::{always, eq};
157    use xrpl_common_stdlib::core::ledger_objects::LedgerObjectFieldGetter;
158    use xrpl_common_stdlib::host::error_codes::{FIELD_NOT_FOUND, INTERNAL_ERROR, INVALID_FIELD};
159    use xrpl_common_stdlib::host::host_bindings_trait::MockHostBindings;
160    use xrpl_common_stdlib::sfield::SField;
161
162    fn expect_current_field<
163        T: LedgerObjectFieldGetter + Send + std::fmt::Debug + PartialEq + 'static,
164        const CODE: i32,
165    >(
166        mock: &mut MockHostBindings,
167        _field: SField<T, CODE>,
168        size: usize,
169        times: usize,
170    ) {
171        mock.expect_get_current_ledger_obj_field()
172            .with(eq(CODE), always(), eq(size))
173            .times(times)
174            .returning(move |_, _, _| size as i32);
175    }
176
177    mod current_escrow_fields {
178        use super::*;
179        use crate::ledger_objects::current_escrow::CurrentEscrow;
180        use xrpl_common_stdlib::core::types::blob::CONDITION_BLOB_SIZE;
181        use xrpl_common_stdlib::core::types::blob::WASM_BLOB_SIZE;
182        use xrpl_common_stdlib::host::setup_mock;
183
184        #[test]
185        fn test_mandatory_fields_return_ok() {
186            let mut mock = MockHostBindings::new();
187
188            // get_account
189            expect_current_field(&mut mock, sfield::Account, 20, 1);
190            // get_amount
191            expect_current_field(&mut mock, sfield::Amount, 48, 1);
192            // get_destination
193            expect_current_field(&mut mock, sfield::Destination, 20, 1);
194            // get_owner_node
195            expect_current_field(&mut mock, sfield::OwnerNode, 8, 1);
196            // get_previous_txn_id
197            expect_current_field(&mut mock, sfield::PreviousTxnID, 32, 1);
198            // get_previous_txn_lgr_seq
199            expect_current_field(&mut mock, sfield::PreviousTxnLgrSeq, 4, 1);
200            // get_data (mandatory for escrow)
201            expect_current_field(&mut mock, sfield::Data, 4096, 1);
202
203            let _guard = setup_mock(mock);
204
205            let escrow = CurrentEscrow;
206
207            // All mandatory fields should return Ok
208            assert!(escrow.get_account().is_ok());
209            assert!(escrow.get_amount().is_ok());
210            assert!(escrow.get_destination().is_ok());
211            assert!(escrow.get_owner_node().is_ok());
212            assert!(escrow.get_previous_txn_id().is_ok());
213            assert!(escrow.get_previous_txn_lgr_seq().is_ok());
214            assert!(escrow.get_data().is_ok());
215        }
216
217        #[test]
218        fn test_optional_fields_return_some() {
219            let mut mock = MockHostBindings::new();
220
221            // get_cancel_after
222            expect_current_field(&mut mock, sfield::CancelAfter, 4, 1);
223            // get_condition
224            expect_current_field(&mut mock, sfield::Condition, CONDITION_BLOB_SIZE, 1);
225            // get_destination_node
226            expect_current_field(&mut mock, sfield::DestinationNode, 8, 1);
227            // get_destination_tag
228            expect_current_field(&mut mock, sfield::DestinationTag, 4, 1);
229            // get_finish_after
230            expect_current_field(&mut mock, sfield::FinishAfter, 4, 1);
231            // get_source_tag
232            expect_current_field(&mut mock, sfield::SourceTag, 4, 1);
233            // get_finish_function
234            expect_current_field(&mut mock, sfield::FinishFunction, WASM_BLOB_SIZE, 1);
235
236            let _guard = setup_mock(mock);
237
238            let escrow = CurrentEscrow;
239
240            // All optional fields should return Ok(Some(...))
241            assert!(escrow.get_cancel_after().unwrap().is_some());
242            assert!(escrow.get_condition().unwrap().is_some());
243            assert!(escrow.get_destination_node().unwrap().is_some());
244            assert!(escrow.get_destination_tag().unwrap().is_some());
245            assert!(escrow.get_finish_after().unwrap().is_some());
246            assert!(escrow.get_source_tag().unwrap().is_some());
247            assert!(escrow.get_finish_function().unwrap().is_some());
248        }
249
250        #[test]
251        fn test_optional_fields_return_none_when_field_not_found() {
252            let mut mock = MockHostBindings::new();
253
254            // get_cancel_after
255            mock.expect_get_current_ledger_obj_field()
256                .with(eq(sfield::CancelAfter), always(), eq(4))
257                .times(1)
258                .returning(|_, _, _| FIELD_NOT_FOUND);
259            // get_condition - returns 0 for None
260            mock.expect_get_current_ledger_obj_field()
261                .with(eq(sfield::Condition), always(), eq(CONDITION_BLOB_SIZE))
262                .times(1)
263                .returning(|_, _, _| 0);
264            // get_destination_node
265            mock.expect_get_current_ledger_obj_field()
266                .with(eq(sfield::DestinationNode), always(), eq(8))
267                .times(1)
268                .returning(|_, _, _| FIELD_NOT_FOUND);
269            // get_destination_tag
270            mock.expect_get_current_ledger_obj_field()
271                .with(eq(sfield::DestinationTag), always(), eq(4))
272                .times(1)
273                .returning(|_, _, _| FIELD_NOT_FOUND);
274            // get_finish_after
275            mock.expect_get_current_ledger_obj_field()
276                .with(eq(sfield::FinishAfter), always(), eq(4))
277                .times(1)
278                .returning(|_, _, _| FIELD_NOT_FOUND);
279            // get_source_tag
280            mock.expect_get_current_ledger_obj_field()
281                .with(eq(sfield::SourceTag), always(), eq(4))
282                .times(1)
283                .returning(|_, _, _| FIELD_NOT_FOUND);
284            // get_finish_function - variable size field, returns 0 for empty (Some with len=0)
285            mock.expect_get_current_ledger_obj_field()
286                .with(eq(sfield::FinishFunction), always(), eq(WASM_BLOB_SIZE))
287                .times(1)
288                .returning(|_, _, _| 0);
289
290            let _guard = setup_mock(mock);
291
292            let escrow = CurrentEscrow;
293
294            // Fixed-size optional fields should return Ok(None) when FIELD_NOT_FOUND
295            assert!(escrow.get_cancel_after().unwrap().is_none());
296            assert!(escrow.get_condition().unwrap().is_none());
297            assert!(escrow.get_destination_node().unwrap().is_none());
298            assert!(escrow.get_destination_tag().unwrap().is_none());
299            assert!(escrow.get_finish_after().unwrap().is_none());
300            assert!(escrow.get_source_tag().unwrap().is_none());
301
302            // Variable-size optional fields return Some with len=0 when not found
303            let finish_function = escrow.get_finish_function().unwrap();
304            assert!(finish_function.is_some());
305            assert_eq!(finish_function.unwrap().len, 0);
306        }
307
308        #[test]
309        fn test_mandatory_fields_return_error_on_internal_error() {
310            let mut mock = MockHostBindings::new();
311
312            // get_account with INTERNAL_ERROR
313            mock.expect_get_current_ledger_obj_field()
314                .with(eq(sfield::Account), always(), eq(20))
315                .times(1)
316                .returning(|_, _, _| INTERNAL_ERROR);
317
318            let _guard = setup_mock(mock);
319
320            let escrow = CurrentEscrow;
321            let result = escrow.get_account();
322
323            assert!(result.is_err());
324            assert_eq!(result.err().unwrap().code(), INTERNAL_ERROR);
325        }
326
327        #[test]
328        fn test_get_data_returns_error_on_internal_error() {
329            let mut mock = MockHostBindings::new();
330
331            mock.expect_get_current_ledger_obj_field()
332                .with(eq(sfield::Data), always(), eq(4096))
333                .times(1)
334                .returning(|_, _, _| INTERNAL_ERROR);
335
336            let _guard = setup_mock(mock);
337
338            let escrow = CurrentEscrow;
339            let result = escrow.get_data();
340
341            assert!(result.is_err());
342            assert_eq!(result.err().unwrap().code(), INTERNAL_ERROR);
343        }
344
345        #[test]
346        fn test_mandatory_fields_return_error_on_invalid_field() {
347            let mut mock = MockHostBindings::new();
348
349            // get_account with INVALID_FIELD
350            mock.expect_get_current_ledger_obj_field()
351                .with(eq(sfield::Account), always(), eq(20))
352                .times(1)
353                .returning(|_, _, _| INVALID_FIELD);
354
355            let _guard = setup_mock(mock);
356
357            let escrow = CurrentEscrow;
358            let result = escrow.get_account();
359
360            assert!(result.is_err());
361            assert_eq!(result.err().unwrap().code(), INVALID_FIELD);
362        }
363    }
364}