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