Skip to main content

xrpl_escrow_stdlib/ledger_objects/
traits.rs

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