Skip to main content

xrpl_common_stdlib/fields/
current_tx.rs

1//! # Current Transaction Retrieval Module
2//!
3//! This module provides utilities for retrieving typed fields from the current XRPL transaction
4//! within the context of XRPL Programmability. It offers a safe, type-safe
5//! interface over the low-level host functions for accessing transaction data, such as from an
6//! `EscrowFinish` transaction.
7//!
8//! ## Overview
9//!
10//! When processing XRPL transactions in a permissionless programmability environment, you often
11//! need to extract specific fields like account IDs, hashes, public keys, and other data. This
12//! module provides convenient wrapper functions that handle the low-level buffer management
13//! and error handling required to safely retrieve these fields.
14//!
15//! `get_field` and `get_field_optional` are generic over any type implementing
16//! [`crate::fields::decoder::FromCurrentTx`] — see [`crate::fields::decoder`] for how a type
17//! opts into that.
18//!
19//! ## Optional vs Required Fields
20//!
21//! - **Required** (`get_field`): Returns an error if the field is missing.
22//! - **Optional** (`get_field_optional`): Returns `Ok(None)` if the field is missing.
23//!
24//! Concrete transaction wrappers (e.g., `EscrowFinish`) live in their respective
25//! companion crates (`xrpl-escrow-stdlib` for escrow flows).
26
27use crate::fields::decoder::{FromCurrentTx, decode_host_result};
28use crate::host::{Error, Result, tx_field};
29use crate::sfield::SField;
30use crate::types::blob::Blob;
31use core::mem::MaybeUninit;
32
33/// Retrieves a field from the current transaction using an SField constant.
34///
35/// # Arguments
36///
37/// * `field` - An SField constant that encodes both the field code and expected type
38///
39/// # Returns
40///
41/// Returns a `Result<T>` where:
42/// * `Ok(T)` - The field value for the specified field
43/// * `Err(Error)` - If the field cannot be retrieved, has unexpected size, or fails to decode
44///
45/// # Example
46///
47/// ```rust,no_run
48/// use xrpl_common_stdlib::fields::current_tx::get_field;
49/// use xrpl_common_stdlib::sfield;
50///
51/// // Type is automatically inferred from the SField constant
52/// let sequence = get_field(sfield::Sequence).unwrap();  // u32
53/// let account = get_field(sfield::Account).unwrap();  // AccountID
54/// ```
55#[inline]
56pub fn get_field<T: FromCurrentTx, const CODE: i32>(_: SField<T, CODE>) -> Result<T> {
57    let mut buf = T::empty_buffer();
58    let n = {
59        let slice = buf.as_mut();
60        unsafe { tx_field(CODE, slice.as_mut_ptr(), slice.len()) }
61    };
62    decode_host_result::<T>(buf, n)
63}
64
65/// Retrieves an optionally present field from the current transaction using an SField constant.
66///
67/// # Arguments
68///
69/// * `field` - An SField constant that encodes both the field code and expected type
70///
71/// # Returns
72///
73/// Returns a `Result<Option<T>>` where:
74/// * `Ok(Some(T))` - The field value for the specified field
75/// * `Ok(None)` - If the field is not present (i.e., result_code == FIELD_NOT_FOUND)
76/// * `Err(Error)` - If the field cannot be retrieved, has unexpected size, or fails to decode
77///
78/// # Example
79///
80/// ```rust,no_run
81/// use xrpl_common_stdlib::fields::current_tx::get_field_optional;
82/// use xrpl_common_stdlib::sfield;
83///
84/// // Type is automatically inferred from the SField constant
85/// let flags = get_field_optional(sfield::Flags).unwrap();  // Option<u32>
86/// let source_tag = get_field_optional(sfield::SourceTag).unwrap();  // Option<u32>
87/// ```
88#[inline]
89pub fn get_field_optional<T: FromCurrentTx, const CODE: i32>(
90    field: SField<T, CODE>,
91) -> Result<Option<T>> {
92    match get_field(field) {
93        Result::Ok(value) => Result::Ok(Some(value)),
94        Result::Err(Error::FieldNotFound) => Result::Ok(None),
95        Result::Err(e) => Result::Err(e),
96    }
97}
98
99// --- `Blob<N>`-specific accessors ------------------------------------------------------------
100//
101// See the matching comment in `fields::ledger_obj` for the full rationale — this is the same
102// zero-copy accessor, mirrored here for reading `Blob<N>` fields off the current transaction
103// instead of a cached ledger object. In short: the generic `get_field`/`get_field_optional`
104// above allocate their own scratch buffer and then reconstruct `T` from it, which for large
105// `Blob<N>` values (e.g. `WasmBlob`, N = 4096) compiles to a real, measured ~4092-byte `memcpy`
106// under this crate's size-optimized (`opt-level = "s"`) release profile. These functions instead
107// have the host write straight into the returned `Blob<N>`'s own `data` field, so no such
108// reconstruction — and no such copy — ever happens. Deliberately not folded into the generic
109// path, which stays untouched for every other (small, copy-is-negligible) field type.
110
111/// Retrieves a `Blob<N>` field from the current transaction, writing the host's bytes directly
112/// into the returned `Blob<N>`'s own storage. See the module comment block above for why
113/// `Blob<N>` gets a dedicated accessor instead of using [`get_field`].
114#[inline]
115pub fn get_blob_field<const N: usize, const CODE: i32>(
116    _: SField<Blob<N>, CODE>,
117) -> Result<Blob<N>> {
118    let mut blob = MaybeUninit::<Blob<N>>::uninit();
119    // SAFETY: `data_ptr` points at the `data` field inside `blob`'s own (uninitialized) storage;
120    // `blob` outlives this pointer and no other reference to it exists yet.
121    let data_ptr = unsafe { core::ptr::addr_of_mut!((*blob.as_mut_ptr()).data) } as *mut u8;
122    // Zero the destination *before* the host call: a result code of 0 is a legitimate "empty
123    // field" success (not an error), and `Blob::data` is `pub`, so callers may read past `len`
124    // directly. Zeroing in place (rather than in a separate scratch buffer) costs the same one
125    // `memset` the old code paid anyway, just at the final address instead of a temporary one.
126    unsafe { core::ptr::write_bytes(data_ptr, 0u8, N) };
127    let n = unsafe { tx_field(CODE, data_ptr, N) };
128    if n < 0 {
129        return Result::Err(Error::from_code(n));
130    }
131    if n as usize > N {
132        // A conformant host never reports writing more bytes than the buffer holds.
133        return Result::Err(Error::PointerOutOfBounds);
134    }
135    // SAFETY: `data` was fully zeroed above and then (partially) overwritten by the host, so
136    // every byte is initialized; `len` is set right before `assume_init`, completing the value.
137    unsafe {
138        core::ptr::addr_of_mut!((*blob.as_mut_ptr()).len).write(n as usize);
139        Result::Ok(blob.assume_init())
140    }
141}
142
143/// Optional variant of [`get_blob_field`]: returns `Ok(None)` if the field is not present,
144/// otherwise behaves identically (including the direct-write zero-copy behavior). Implemented
145/// in terms of [`get_blob_field`] itself rather than duplicating its body — `Error::FieldNotFound`
146/// round-trips exactly through `Error::from_code`/`.code()` (both are just `FIELD_NOT_FOUND`),
147/// so translating that one error case into `None` is all this needs to do.
148#[inline]
149pub fn get_blob_field_optional<const N: usize, const CODE: i32>(
150    field: SField<Blob<N>, CODE>,
151) -> Result<Option<Blob<N>>> {
152    match get_blob_field(field) {
153        Result::Ok(blob) => Result::Ok(Some(blob)),
154        Result::Err(Error::FieldNotFound) => Result::Ok(None),
155        Result::Err(e) => Result::Err(e),
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::{get_blob_field, get_blob_field_optional, get_field, get_field_optional};
162    use crate::fields::decoder::FieldDecoder;
163    use crate::host::error_codes::{FIELD_NOT_FOUND, SOME_ERROR};
164    use crate::host::host_bindings_trait::MockHostBindings;
165    use crate::host::setup_mock;
166    use crate::sfield;
167    use crate::types::account_id::{ACCOUNT_ID_SIZE, AccountID};
168    use crate::types::number::Number;
169    use mockall::predicate::{always, eq};
170
171    fn expect_tx_field(mock: &mut MockHostBindings, field_code: i32, size: usize, times: usize) {
172        mock.expect_tx_field()
173            .with(eq(field_code), always(), eq(size))
174            .times(times)
175            .returning(move |_, _, _| size as i32);
176    }
177
178    #[test]
179    fn test_get_field_success() {
180        let mut mock = MockHostBindings::new();
181        expect_tx_field(&mut mock, sfield::Sequence.into(), 4, 1);
182        expect_tx_field(&mut mock, sfield::Account.into(), ACCOUNT_ID_SIZE, 1);
183        let _guard = setup_mock(mock);
184
185        assert!(get_field::<u32, _>(sfield::Sequence).is_ok());
186        assert!(get_field::<AccountID, _>(sfield::Account).is_ok());
187    }
188
189    #[test]
190    fn test_get_field_decodes_stnumber_field() {
191        // An `STI_NUMBER` field is 12 bytes; asking for one also exercises `Number`'s buffer size
192        // and its `FromCurrentTx` marker.
193        let mut mock = MockHostBindings::new();
194        expect_tx_field(&mut mock, sfield::PeriodicPayment.into(), 12, 1);
195        let _guard = setup_mock(mock);
196
197        assert_eq!(
198            get_field(sfield::PeriodicPayment).unwrap(),
199            Number::from([0u8; 12])
200        );
201    }
202
203    #[test]
204    fn test_get_field_optional_returns_none_on_field_not_found() {
205        let mut mock = MockHostBindings::new();
206        mock.expect_tx_field()
207            .with(eq::<i32>(sfield::SourceTag.into()), always(), eq(4))
208            .times(1)
209            .returning(|_, _, _| FIELD_NOT_FOUND);
210        let _guard = setup_mock(mock);
211
212        let result = get_field_optional::<u32, _>(sfield::SourceTag);
213        assert!(result.is_ok());
214        assert!(result.unwrap().is_none());
215    }
216
217    #[test]
218    fn test_get_field_optional_returns_some_when_present() {
219        let mut mock = MockHostBindings::new();
220        expect_tx_field(&mut mock, sfield::SourceTag.into(), 4, 1);
221        let _guard = setup_mock(mock);
222
223        let result = get_field_optional::<u32, _>(sfield::SourceTag);
224        assert!(result.is_ok());
225        assert!(result.unwrap().is_some());
226    }
227
228    #[test]
229    fn test_get_field_returns_decode_error_on_byte_mismatch() {
230        // u32's FieldDecoder requires exactly 4 bytes; a shorter write fails the length check
231        // and surfaces as InvalidDecoding.
232        let mut mock = MockHostBindings::new();
233        mock.expect_tx_field()
234            .with(eq::<i32>(sfield::Sequence.into()), always(), eq(4))
235            .times(1)
236            .returning(|_, _, _| 3);
237        let _guard = setup_mock(mock);
238
239        let result = get_field::<u32, _>(sfield::Sequence);
240        assert!(result.is_err());
241        assert_eq!(
242            result.err().unwrap().code(),
243            crate::host::Error::InvalidDecoding.code()
244        );
245    }
246
247    #[test]
248    fn test_get_field_returns_err_on_internal_error() {
249        let mut mock = MockHostBindings::new();
250        mock.expect_tx_field()
251            .with(eq::<i32>(sfield::Flags.into()), always(), eq(4))
252            .times(1)
253            .returning(|_, _, _| SOME_ERROR);
254        let _guard = setup_mock(mock);
255
256        let result = get_field::<u32, _>(sfield::Flags);
257        assert!(result.is_err());
258        assert_eq!(result.err().unwrap().code(), SOME_ERROR);
259    }
260
261    #[test]
262    fn test_u16_decodes_little_endian_host_bytes() {
263        let result = u16::decode([0x02, 0x01], 2);
264        assert_eq!(result.unwrap(), 0x0102u16);
265    }
266
267    #[test]
268    fn test_u32_decodes_little_endian_host_bytes() {
269        let result = u32::decode([0x04, 0x03, 0x02, 0x01], 4);
270        assert_eq!(result.unwrap(), 0x01020304u32);
271    }
272
273    #[test]
274    fn test_u64_decodes_little_endian_host_bytes() {
275        let result = u64::decode([0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01], 8);
276        assert_eq!(result.unwrap(), 0x0102030405060708u64);
277    }
278
279    #[test]
280    fn test_get_field_returns_err_when_host_reports_oversized_write() {
281        // A conformant host can't write past the buffer it was handed; a positive count larger
282        // than the buffer is reported as PointerOutOfBounds.
283        let mut mock = MockHostBindings::new();
284        mock.expect_tx_field()
285            .with(eq::<i32>(sfield::Sequence.into()), always(), eq(4))
286            .times(1)
287            .returning(|_, _, _| 8); // claims 8 bytes into a 4-byte u32 buffer
288        let _guard = setup_mock(mock);
289
290        let result = get_field::<u32, _>(sfield::Sequence);
291        assert!(result.is_err());
292        assert_eq!(
293            result.err().unwrap().code(),
294            crate::host::Error::PointerOutOfBounds.code()
295        );
296    }
297
298    #[test]
299    fn test_get_blob_field_writes_bytes_directly_into_blob_data() {
300        let mut mock = MockHostBindings::new();
301        mock.expect_tx_field()
302            .with(eq::<i32>(sfield::PublicKey.into()), always(), eq(33))
303            .times(1)
304            .returning(|_, buf, size| {
305                // Simulate the host writing 33 bytes of non-zero data.
306                let slice = unsafe { core::slice::from_raw_parts_mut(buf, size) };
307                slice.fill(0xAB);
308                size as i32
309            });
310        let _guard = setup_mock(mock);
311
312        let blob = get_blob_field(sfield::PublicKey).unwrap();
313        assert_eq!(blob.len(), 33);
314        assert!(blob.as_slice().iter().all(|&b| b == 0xAB));
315    }
316
317    #[test]
318    fn test_get_blob_field_zeroes_tail_when_host_writes_fewer_bytes() {
319        // A short write (e.g. an empty/undersized field) must leave the tail zeroed, not
320        // uninitialized -- `Blob::data` is `pub`, so callers may read past `len` directly.
321        let mut mock = MockHostBindings::new();
322        mock.expect_tx_field()
323            .with(eq::<i32>(sfield::PublicKey.into()), always(), eq(33))
324            .times(1)
325            .returning(|_, buf, _size| {
326                let slice = unsafe { core::slice::from_raw_parts_mut(buf, 10) };
327                slice.fill(0xFF);
328                10
329            });
330        let _guard = setup_mock(mock);
331
332        let blob = get_blob_field(sfield::PublicKey).unwrap();
333        assert_eq!(blob.len(), 10);
334        assert_eq!(blob.data[9], 0xFF);
335        assert_eq!(blob.data[10], 0);
336        assert_eq!(blob.data[32], 0);
337    }
338
339    #[test]
340    fn test_get_blob_field_returns_err_on_internal_error() {
341        let mut mock = MockHostBindings::new();
342        mock.expect_tx_field()
343            .with(eq::<i32>(sfield::PublicKey.into()), always(), eq(33))
344            .times(1)
345            .returning(|_, _, _| SOME_ERROR);
346        let _guard = setup_mock(mock);
347
348        let result = get_blob_field(sfield::PublicKey);
349        assert!(result.is_err());
350        assert_eq!(result.err().unwrap().code(), SOME_ERROR);
351    }
352
353    #[test]
354    fn test_get_blob_field_returns_err_when_host_reports_oversized_write() {
355        let mut mock = MockHostBindings::new();
356        mock.expect_tx_field()
357            .with(eq::<i32>(sfield::PublicKey.into()), always(), eq(33))
358            .times(1)
359            .returning(|_, _, _| 34); // claims 34 bytes into a 33-byte buffer
360        let _guard = setup_mock(mock);
361
362        let result = get_blob_field(sfield::PublicKey);
363        assert!(result.is_err());
364        assert_eq!(
365            result.err().unwrap().code(),
366            crate::host::Error::PointerOutOfBounds.code()
367        );
368    }
369
370    #[test]
371    fn test_get_blob_field_optional_returns_none_on_field_not_found() {
372        let mut mock = MockHostBindings::new();
373        mock.expect_tx_field()
374            .with(eq::<i32>(sfield::PublicKey.into()), always(), eq(33))
375            .times(1)
376            .returning(|_, _, _| FIELD_NOT_FOUND);
377        let _guard = setup_mock(mock);
378
379        let result = get_blob_field_optional(sfield::PublicKey);
380        assert!(result.is_ok());
381        assert!(result.unwrap().is_none());
382    }
383
384    #[test]
385    fn test_get_blob_field_optional_returns_some_when_present() {
386        let mut mock = MockHostBindings::new();
387        mock.expect_tx_field()
388            .with(eq::<i32>(sfield::PublicKey.into()), always(), eq(33))
389            .times(1)
390            .returning(|_, _, _| 33);
391        let _guard = setup_mock(mock);
392
393        let result = get_blob_field_optional(sfield::PublicKey);
394        assert!(result.is_ok());
395        assert!(result.unwrap().is_some());
396    }
397}