Skip to main content

xrpl_wasm_stdlib/core/current_tx/
mod.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//! ## Field Types Supported
16//!
17//! - **AccountID**: 20-byte account identifiers
18//! - **u32**: 32-bit unsigned integers
19//! - **Hash256**: 256-bit cryptographic hashes
20//! - **PublicKey**: 33-byte public keys
21//! - **Blob**: Variable-length binary data
22//!
23//! ## Optional vs Required Fields
24//!
25//! The module provides both optional and required variants for field retrieval:
26//!
27//! - **Required variants** (e.g., `get_u32_field`): Return an error if the field is missing
28//! - **Optional variants** (e.g., `get_optional_u32_field`): Return `None` if the field is missing
29//!
30//! ## Error Handling
31//!
32//! All functions return `Result<T>` or `Result<Option<T>>` types that encapsulate
33//! the custom error handling required for the XRPL Programmability environment.
34//!
35//! ## Safety Considerations
36//!
37//! - All functions use fixed-size buffers appropriate for their data types
38//! - Buffer sizes are validated against expected field sizes
39//! - Unsafe operations are contained within the low-level host function calls
40//! - Memory safety is ensured through proper buffer management
41//! - Field codes are validated by the underlying host functions
42//!
43//! ## Performance Notes
44//!
45//! - All functions are marked `#[inline]` to minimize call overhead
46//! - Buffer allocations are stack-based and have minimal cost
47//! - Host function calls are the primary performance bottleneck
48//!
49//! Concrete transaction wrappers (e.g., `EscrowFinish`) live in their respective
50//! companion crates (`xrpl-escrow-stdlib` for escrow flows).
51
52pub mod traits;
53
54use crate::host::error_codes::{
55    match_result_code_with_expected_bytes, match_result_code_with_expected_bytes_optional,
56};
57use crate::host::{Result, get_tx_field};
58use crate::sfield::SField;
59
60/// Trait for types that can be retrieved from current transaction fields.
61///
62/// This trait provides a unified interface for retrieving typed data from the current
63/// XRPL transaction being processed, replacing the previous collection of type-specific
64/// functions with a generic, type-safe approach.
65///
66/// ## Supported Types
67///
68/// The following types implement this trait:
69/// - `u32` - 32-bit unsigned integers for sequence numbers, flags, timestamps
70/// - `AccountID` - 20-byte account identifiers for transaction participants
71/// - `Amount` - XRP amounts and token amounts for transaction values
72/// - `Hash256` - 256-bit hashes for transaction IDs and references
73/// - `PublicKey` - 33-byte compressed public keys for cryptographic operations
74/// - `Blob<N>` - Variable-length binary data (generic over buffer size `N`)
75///
76/// ## Usage Patterns
77///
78/// ```rust,no_run
79/// use xrpl_wasm_stdlib::core::current_tx::{get_field, get_field_optional};
80/// use xrpl_wasm_stdlib::core::types::account_id::AccountID;
81/// use xrpl_wasm_stdlib::core::types::amount::Amount;
82/// use xrpl_wasm_stdlib::sfield;
83/// # fn example() {
84///   // Get required fields from the current transaction
85///   let account: AccountID = get_field(sfield::Account).unwrap();
86///   let sequence: u32 = get_field(sfield::Sequence).unwrap();
87///   let fee: Amount = get_field(sfield::Fee).unwrap();
88///
89///   // Get optional fields from the current transaction
90///   let flags: Option<u32> = get_field_optional(sfield::Flags).unwrap();
91/// # }
92/// ```
93///
94/// ## Error Handling
95///
96/// - Required field methods return `Result<T>` and error if the field is missing
97/// - Optional field methods return `Result<Option<T>>` and return `None` if the field is missing
98/// - All methods return appropriate errors for buffer size mismatches or other retrieval failures
99///
100/// ## Transaction Context
101///
102/// This trait operates on the "current transaction" - the transaction currently being
103/// processed in the XRPL Programmability environment. The transaction context is
104/// established by the XRPL host environment before calling into WASM code.
105///
106/// ## Safety Considerations
107///
108/// - All implementations use appropriately sized buffers for their data types
109/// - Buffer sizes are validated against expected field sizes where applicable
110/// - Unsafe operations are contained within the host function calls
111/// - Transaction field access is validated by the host environment
112pub trait CurrentTxFieldGetter: Sized {
113    /// Get a required field from the current transaction.
114    ///
115    /// This method retrieves a field that must be present in the transaction.
116    /// If the field is missing, an error is returned.
117    ///
118    /// # Arguments
119    ///
120    /// * `field` - The SField identifying which field to retrieve
121    ///
122    /// # Returns
123    ///
124    /// Returns a `Result<Self>` where:
125    /// * `Ok(Self)` - The field value for the specified field
126    /// * `Err(Error::FieldNotFound)` - If the field is not present in the transaction
127    /// * `Err(Error)` - If the field cannot be retrieved or has unexpected size
128    fn get_from_current_tx<const CODE: i32>(field: SField<Self, CODE>) -> Result<Self>;
129
130    /// Get an optional field from the current transaction.
131    ///
132    /// This method retrieves a field that may or may not be present in the transaction.
133    /// If the field is missing, `None` is returned rather than an error.
134    ///
135    /// # Arguments
136    ///
137    /// * `field` - The SField identifying which field to retrieve
138    ///
139    /// # Returns
140    ///
141    /// Returns a `Result<Option<Self>>` where:
142    /// * `Ok(Some(Self))` - The field value for the specified field
143    /// * `Ok(None)` - If the field is not present in the transaction (i.e., result_code == FIELD_NOT_FOUND)
144    /// * `Err(Error)` - If the field cannot be retrieved or has unexpected size
145    fn get_from_current_tx_optional<const CODE: i32>(
146        field: SField<Self, CODE>,
147    ) -> Result<Option<Self>>;
148}
149
150/// Trait for types that can be retrieved as fixed-size fields from transactions.
151///
152/// This trait enables a generic implementation of `CurrentTxFieldGetter` for all fixed-size
153/// unsigned integer types (u8, u16, u32, u64). Types implementing this trait must
154/// have a known, constant size in bytes.
155///
156/// # Implementing Types
157///
158/// - `u8` - 1 byte
159/// - `u16` - 2 bytes
160/// - `u32` - 4 bytes
161/// - `u64` - 8 bytes
162trait FixedSizeFieldType: Sized {
163    /// The size of this type in bytes
164    const SIZE: usize;
165}
166
167impl FixedSizeFieldType for u8 {
168    const SIZE: usize = 1;
169}
170
171impl FixedSizeFieldType for u16 {
172    const SIZE: usize = 2;
173}
174
175impl FixedSizeFieldType for u32 {
176    const SIZE: usize = 4;
177}
178
179impl FixedSizeFieldType for u64 {
180    const SIZE: usize = 8;
181}
182
183/// Generic implementation of `CurrentTxFieldGetter` for all fixed-size unsigned integer types.
184///
185/// This single implementation handles u8, u16, u32, and u64 by leveraging the
186/// `FixedSizeFieldType` trait. The implementation:
187/// - Allocates a buffer of the appropriate size
188/// - Calls the host function to retrieve the field
189/// - Validates that the returned byte count matches the expected size
190/// - Converts the buffer to the target type
191///
192/// # Buffer Management
193///
194/// Uses `MaybeUninit` for efficient stack allocation without initialization overhead.
195/// The buffer size is determined at compile-time via the `SIZE` constant.
196impl<T: FixedSizeFieldType> CurrentTxFieldGetter for T {
197    #[inline]
198    fn get_from_current_tx<const CODE: i32>(field: SField<Self, CODE>) -> Result<Self> {
199        let mut value = core::mem::MaybeUninit::<T>::uninit();
200        let result_code =
201            unsafe { get_tx_field(i32::from(field), value.as_mut_ptr().cast(), T::SIZE) };
202        match_result_code_with_expected_bytes(result_code, T::SIZE, || unsafe {
203            value.assume_init()
204        })
205    }
206
207    #[inline]
208    fn get_from_current_tx_optional<const CODE: i32>(
209        field: SField<Self, CODE>,
210    ) -> Result<Option<Self>> {
211        let mut value = core::mem::MaybeUninit::<T>::uninit();
212        let result_code =
213            unsafe { get_tx_field(i32::from(field), value.as_mut_ptr().cast(), T::SIZE) };
214        match_result_code_with_expected_bytes_optional(result_code, T::SIZE, || {
215            Some(unsafe { value.assume_init() })
216        })
217    }
218}
219
220/// Retrieves a field from the current transaction using an SField constant.
221///
222/// # Arguments
223///
224/// * `field` - An SField constant that encodes both the field code and expected type
225///
226/// # Returns
227///
228/// Returns a `Result<T>` where:
229/// * `Ok(T)` - The field value for the specified field
230/// * `Err(Error)` - If the field cannot be retrieved or has unexpected size
231///
232/// # Example
233///
234/// ```rust,no_run
235/// use xrpl_wasm_stdlib::core::current_tx::get_field;
236/// use xrpl_wasm_stdlib::sfield;
237///
238/// // Type is automatically inferred from the SField constant
239/// let sequence = get_field(sfield::Sequence).unwrap();  // u32
240/// let account = get_field(sfield::Account).unwrap();  // AccountID
241/// ```
242#[inline]
243pub fn get_field<T: CurrentTxFieldGetter, const CODE: i32>(field: SField<T, CODE>) -> Result<T> {
244    T::get_from_current_tx(field)
245}
246
247/// Retrieves an optionally present field from the current transaction using an SField constant.
248///
249/// # Arguments
250///
251/// * `field` - An SField constant that encodes both the field code and expected type
252///
253/// # Returns
254///
255/// Returns a `Result<Option<T>>` where:
256/// * `Ok(Some(T))` - The field value for the specified field
257/// * `Ok(None)` - If the field is not present (i.e., result_code == FIELD_NOT_FOUND)
258/// * `Err(Error)` - If the field cannot be retrieved or has unexpected size
259///
260/// # Example
261///
262/// ```rust,no_run
263/// use xrpl_wasm_stdlib::core::current_tx::get_field_optional;
264/// use xrpl_wasm_stdlib::sfield;
265///
266/// // Type is automatically inferred from the SField constant
267/// let flags = get_field_optional(sfield::Flags).unwrap();  // Option<u32>
268/// let source_tag = get_field_optional(sfield::SourceTag).unwrap();  // Option<u32>
269/// ```
270#[inline]
271pub fn get_field_optional<T: CurrentTxFieldGetter, const CODE: i32>(
272    field: SField<T, CODE>,
273) -> Result<Option<T>> {
274    T::get_from_current_tx_optional(field)
275}
276
277#[cfg(test)]
278mod tests {
279    use super::{CurrentTxFieldGetter, get_field, get_field_optional};
280    use crate::core::types::account_id::{ACCOUNT_ID_SIZE, AccountID};
281    use crate::core::types::amount::{AMOUNT_SIZE, Amount};
282    use crate::core::types::blob::{Blob, DEFAULT_BLOB_SIZE, PUBLIC_KEY_BLOB_SIZE, PublicKeyBlob};
283    use crate::core::types::transaction_type::TransactionType;
284    use crate::core::types::uint::{HASH256_SIZE, Hash256};
285    use crate::host::error_codes::{FIELD_NOT_FOUND, INTERNAL_ERROR};
286    use crate::host::host_bindings_trait::MockHostBindings;
287    use crate::host::setup_mock;
288    use crate::sfield;
289    use mockall::predicate::{always, eq};
290
291    fn expect_tx_field(mock: &mut MockHostBindings, field_code: i32, size: usize, times: usize) {
292        mock.expect_get_tx_field()
293            .with(eq(field_code), always(), eq(size))
294            .times(times)
295            .returning(move |_, _, _| size as i32);
296    }
297
298    fn expect_tx_field_not_found(mock: &mut MockHostBindings, field_code: i32, size: usize) {
299        mock.expect_get_tx_field()
300            .with(eq(field_code), always(), eq(size))
301            .times(1)
302            .returning(|_, _, _| FIELD_NOT_FOUND);
303    }
304
305    // One fixed-size (u32) and one variable-size (AccountID) type are sufficient here;
306    // success-path coverage for all supported types lives in the per-type getter tests above.
307    #[test]
308    fn test_optional_field_getter_returns_some_when_field_present() {
309        let mut mock = MockHostBindings::new();
310
311        expect_tx_field(&mut mock, sfield::SourceTag.into(), 4, 1);
312        expect_tx_field(&mut mock, sfield::Destination.into(), ACCOUNT_ID_SIZE, 1);
313
314        let _guard = setup_mock(mock);
315
316        let result = u32::get_from_current_tx_optional(sfield::SourceTag);
317        assert!(result.is_ok());
318        assert!(result.unwrap().is_some());
319
320        let result = AccountID::get_from_current_tx_optional(sfield::Destination);
321        assert!(result.is_ok());
322        assert!(result.unwrap().is_some());
323    }
324
325    #[test]
326    fn test_optional_field_getter_returns_none_when_field_not_found() {
327        let mut mock = MockHostBindings::new();
328
329        expect_tx_field_not_found(&mut mock, sfield::SourceTag.into(), 4);
330        expect_tx_field_not_found(&mut mock, sfield::Destination.into(), ACCOUNT_ID_SIZE);
331
332        let _guard = setup_mock(mock);
333
334        let result = u32::get_from_current_tx_optional(sfield::SourceTag);
335        assert!(result.is_ok());
336        assert!(result.unwrap().is_none());
337
338        let result = AccountID::get_from_current_tx_optional(sfield::Destination);
339        assert!(result.is_ok());
340        assert!(result.unwrap().is_none());
341    }
342
343    #[test]
344    fn test_required_field_getter_returns_err_when_field_not_found() {
345        let mut mock = MockHostBindings::new();
346
347        expect_tx_field_not_found(&mut mock, sfield::Sequence.into(), 4);
348
349        let _guard = setup_mock(mock);
350
351        assert!(u32::get_from_current_tx(sfield::Sequence).is_err());
352    }
353
354    #[test]
355    #[should_panic]
356    fn test_field_getter_panics_on_size_mismatch() {
357        let mut mock = MockHostBindings::new();
358        mock.expect_get_tx_field()
359            .with(eq::<i32>(sfield::Sequence.into()), always(), eq(4))
360            .times(1)
361            .returning(|_, _, _| 2); // host returns fewer bytes than expected
362
363        let _guard = setup_mock(mock);
364
365        let _ = u32::get_from_current_tx(sfield::Sequence);
366    }
367
368    // get_field / get_field_optional are thin wrappers over get_from_current_tx / get_from_current_tx_optional,
369    // so exercising u32 and AccountID here is sufficient; per-type coverage lives in the getter tests above.
370    #[test]
371    fn test_get_field_and_get_field_optional_convenience_fns() {
372        let mut mock = MockHostBindings::new();
373
374        expect_tx_field(&mut mock, sfield::Sequence.into(), 4, 1);
375        expect_tx_field(&mut mock, sfield::Account.into(), ACCOUNT_ID_SIZE, 1);
376        expect_tx_field(&mut mock, sfield::SourceTag.into(), 4, 1);
377
378        let _guard = setup_mock(mock);
379
380        assert!(get_field::<u32, _>(sfield::Sequence).is_ok());
381        assert!(get_field::<AccountID, _>(sfield::Account).is_ok());
382
383        let result = get_field_optional::<u32, _>(sfield::SourceTag);
384        assert!(result.is_ok());
385        assert!(result.unwrap().is_some());
386    }
387
388    #[test]
389    fn test_get_field_returns_err_on_internal_error() {
390        let mut mock = MockHostBindings::new();
391        mock.expect_get_tx_field()
392            .with(eq::<i32>(sfield::Flags.into()), always(), eq(4))
393            .times(1)
394            .returning(|_, _, _| INTERNAL_ERROR);
395
396        let _guard = setup_mock(mock);
397
398        assert!(get_field::<u32, _>(sfield::Flags).is_err());
399    }
400
401    #[test]
402    fn test_u8_field_getter() {
403        let mut mock = MockHostBindings::new();
404        expect_tx_field(&mut mock, sfield::Generic.into(), 1, 1);
405        let _guard = setup_mock(mock);
406        assert!(u8::get_from_current_tx(sfield::Generic).is_ok());
407    }
408
409    #[test]
410    fn test_u16_field_getter() {
411        let mut mock = MockHostBindings::new();
412        expect_tx_field(&mut mock, sfield::SignerWeight.into(), 2, 1);
413        let _guard = setup_mock(mock);
414        assert!(u16::get_from_current_tx(sfield::SignerWeight).is_ok());
415    }
416
417    #[test]
418    fn test_u64_field_getter() {
419        let mut mock = MockHostBindings::new();
420        expect_tx_field(&mut mock, sfield::IndexNext.into(), 8, 1);
421        let _guard = setup_mock(mock);
422        assert!(u64::get_from_current_tx(sfield::IndexNext).is_ok());
423    }
424
425    #[test]
426    fn test_account_id_field_getter() {
427        let mut mock = MockHostBindings::new();
428        expect_tx_field(&mut mock, sfield::Account.into(), ACCOUNT_ID_SIZE, 1);
429        let _guard = setup_mock(mock);
430        assert!(AccountID::get_from_current_tx(sfield::Account).is_ok());
431    }
432
433    #[test]
434    fn test_hash256_field_getter() {
435        let mut mock = MockHostBindings::new();
436        expect_tx_field(&mut mock, sfield::PreviousTxnID.into(), HASH256_SIZE, 1);
437        let _guard = setup_mock(mock);
438        assert!(Hash256::get_from_current_tx(sfield::PreviousTxnID).is_ok());
439    }
440
441    #[test]
442    fn test_amount_field_getter() {
443        let mut mock = MockHostBindings::new();
444        expect_tx_field(&mut mock, sfield::Fee.into(), AMOUNT_SIZE, 1);
445        let _guard = setup_mock(mock);
446        assert!(Amount::get_from_current_tx(sfield::Fee).is_ok());
447    }
448
449    #[test]
450    fn test_public_key_blob_field_getter() {
451        let mut mock = MockHostBindings::new();
452        expect_tx_field(
453            &mut mock,
454            sfield::SigningPubKey.into(),
455            PUBLIC_KEY_BLOB_SIZE,
456            1,
457        );
458        let _guard = setup_mock(mock);
459        assert!(PublicKeyBlob::get_from_current_tx(sfield::SigningPubKey).is_ok());
460    }
461
462    #[test]
463    fn test_transaction_type_field_getter() {
464        let mut mock = MockHostBindings::new();
465        expect_tx_field(&mut mock, sfield::TransactionType.into(), 2, 1);
466        let _guard = setup_mock(mock);
467        assert!(TransactionType::get_from_current_tx(sfield::TransactionType).is_ok());
468    }
469
470    #[test]
471    fn test_blob_field_getter() {
472        let mut mock = MockHostBindings::new();
473        expect_tx_field(&mut mock, sfield::MemoData.into(), DEFAULT_BLOB_SIZE, 1);
474        let _guard = setup_mock(mock);
475        assert!(Blob::<DEFAULT_BLOB_SIZE>::get_from_current_tx(sfield::MemoData).is_ok());
476    }
477
478    #[test]
479    fn test_get_tx_field_pipeline_routes_bytes_to_amount() {
480        let mut mock = MockHostBindings::new();
481        mock.expect_get_tx_field()
482            .with(eq::<i32>(sfield::Amount.into()), always(), eq(AMOUNT_SIZE))
483            .times(1)
484            .returning(|_, buf, size| {
485                let slice = unsafe { core::slice::from_raw_parts_mut(buf, size) };
486                slice.fill(0);
487                let mut be = 1000u64.to_be_bytes();
488                be[0] |= 0x40;
489                slice[0..8].copy_from_slice(&be);
490                8
491            });
492
493        let _guard = setup_mock(mock);
494
495        let amount = Amount::get_from_current_tx(sfield::Amount).unwrap();
496        assert!(matches!(amount, Amount::XRP { num_drops: 1000 }));
497    }
498}