Skip to main content

xrpl_common_stdlib/current_tx/
traits.rs

1//! # Transaction Field Access Traits
2//!
3//! This module defines traits for accessing fields from XRPL transactions in a type-safe manner.
4//! It provides a structured interface for retrieving both common transaction fields (shared across
5//! all transaction types) and transaction-specific fields (unique to particular transaction types).
6//!
7//! ## Overview
8//!
9//! XRPL transactions contain a variety of fields, some mandatory and others optional. This module
10//! organizes field access into logical groups:
11//!
12//! - **Common Fields**: Fields present in all XRPL transactions (Account, Fee, Sequence, etc.)
13//! - **Transaction-Specific Fields**: Fields unique to specific transaction types
14//!
15//! ## Design Philosophy
16//!
17//! The trait-based design provides several benefits:
18//!
19//! - **Type Safety**: Each field is accessed through methods with appropriate return types
20//! - **Composability**: Transaction types can implement multiple traits as needed
21//! - **Zero-Cost Abstraction**: Trait methods compile down to direct host function calls
22//! - **Extensibility**: New transaction types can easily implement the relevant traits
23//!
24//! ## Field Categories
25//!
26//! ### Mandatory vs. Optional Fields
27//!
28//! - **Mandatory fields** return `Result<T>` and will error if missing
29//! - **Optional fields** return `Result<Option<T>>` and return `None` if missing
30//!
31//! ### Field Types
32//!
33//! - **AccountID**: 20-byte account identifiers
34//! - **Hash256**: 256-bit cryptographic hashes
35//! - **Amount**: XRP amounts (with future support for tokens)
36//! - **u32**: 32-bit unsigned integers for sequence numbers, flags, etc.
37//! - **Blob**: Variable-length binary data
38//! - **PublicKey**: 33-byte compressed public keys
39//! - **TransactionType**: Enumerated transaction type identifiers
40
41use crate::current_tx::{get_field, get_field_optional};
42use crate::fields::locator::TxPathBuilder;
43use crate::host::Result;
44use crate::sfield;
45use crate::types::account_id::AccountID;
46use crate::types::amount::Amount;
47use crate::types::blob::SignatureBlob;
48use crate::types::public_key::PublicKey;
49use crate::types::transaction_type::TransactionType;
50use crate::types::uint::Hash256;
51
52/// Trait providing access to common fields present in all XRPL transactions.
53///
54/// ## Implementation Requirements
55///
56/// Types implementing this trait should ensure they are used only in the context of a valid
57/// XRPL transaction. The trait methods assume the current transaction context is properly
58/// established by the XRPL Programmability environment.
59pub trait TransactionCommonFields {
60    /// Starts an inner-field path rooted at the current transaction.
61    ///
62    /// Use this to reach into arrays and inner objects that the flat getters below can't return
63    /// whole (e.g. `Memos[0].MemoData`). Chain [`field`](TxPathBuilder::field) /
64    /// [`index`](TxPathBuilder::index), then [`get::<T>()`](TxPathBuilder::get).
65    ///
66    /// ```no_run
67    /// use xrpl_common_stdlib::current_tx::traits::TransactionCommonFields;
68    /// use xrpl_common_stdlib::sfield;
69    /// # fn demo(tx: &impl TransactionCommonFields) {
70    /// let data = tx.path()
71    ///     .field(sfield::Memos)
72    ///     .index(0)
73    ///     .field(sfield::MemoData)
74    ///     .get::<u32>();
75    /// # let _ = data; }
76    /// ```
77    fn path(&self) -> TxPathBuilder {
78        TxPathBuilder::for_current_tx()
79    }
80
81    /// Retrieves the account field from the current transaction.
82    ///
83    /// This field identifies (Required) The unique address of the account that initiated the
84    /// transaction.
85    ///
86    /// # Returns
87    ///
88    /// Returns a `Result<AccountID>` where:
89    /// * `Ok(AccountID)` - The 20-byte account identifier of the transaction sender
90    /// * `Err(Error)` - If the field cannot be retrieved or has an unexpected size
91    fn get_account(&self) -> Result<AccountID> {
92        get_field(sfield::Account)
93    }
94
95    /// Retrieves the transaction type from the current transaction.
96    ///
97    /// This field specifies the type of transaction. Valid transaction types include:
98    /// Payment, OfferCreate, TrustSet, and many others.
99    ///
100    /// # Returns
101    ///
102    /// Returns a `Result<TransactionType>` where:
103    /// * `Ok(TransactionType)` - An enumerated value representing the transaction type
104    /// * `Err(Error)` - If the field cannot be retrieved or has an unexpected size
105    ///
106    fn get_transaction_type(&self) -> Result<TransactionType> {
107        get_field(sfield::TransactionType)
108    }
109
110    /// Retrieves the gas amount from the current transaction.
111    ///
112    /// This field specifies the maximum computational resources that the transaction is
113    /// allowed to consume during execution in the XRPL Programmability environment.
114    /// It helps prevent runaway computations and ensures network stability.
115    ///
116    /// # Returns
117    ///
118    /// Returns a `Result<u32>` where:
119    /// * `Ok(u32)` - The gas value in platform-defined units
120    /// * `Err(Error)` - If the field cannot be retrieved or has an unexpected size
121    fn get_gas(&self) -> Result<u32> {
122        get_field(sfield::Gas)
123    }
124
125    /// Retrieves the fee amount from the current transaction.
126    ///
127    /// This field specifies the amount of XRP (in drops) that the sender is willing to pay
128    /// as a transaction fee. The fee is consumed regardless of whether the transaction
129    /// succeeds or fails, and higher fees can improve transaction priority during
130    /// network congestion.
131    ///
132    /// # Returns
133    ///
134    /// Returns a `Result<Amount>` where:
135    /// * `Ok(Amount)` - The fee amount as an XRP amount in drops
136    /// * `Err(Error)` - If the field cannot be retrieved or has an unexpected size
137    ///
138    /// # Note
139    ///
140    /// Returns XRP amounts only (for now). Future versions may support other token types
141    /// when the underlying amount handling is enhanced.
142    fn get_fee(&self) -> Result<Amount> {
143        get_field(sfield::Fee)
144    }
145
146    /// Retrieves the sequence number from the current transaction.
147    ///
148    /// This field represents the sequence number of the account sending the transaction. A
149    /// transaction is only valid if the Sequence number is exactly 1 greater than the previous
150    /// transaction from the same account. The special case 0 means the transaction is using a
151    /// Ticket instead (Added by the TicketBatch amendment).
152    ///
153    /// # Returns
154    ///
155    /// Returns a `Result<u32>` where:
156    /// * `Ok(u32)` - The transaction sequence number
157    /// * `Err(Error)` - If the field cannot be retrieved or has an unexpected size
158    ///
159    /// # Note
160    ///
161    /// If the transaction uses tickets instead of sequence numbers, this field may not
162    /// be present. In such cases, use `get_ticket_sequence()` instead.
163    fn get_sequence(&self) -> Result<u32> {
164        get_field(sfield::Sequence)
165    }
166
167    /// Retrieves the account transaction ID from the current transaction.
168    ///
169    /// This optional field contains the hash value identifying another transaction. If provided,
170    /// this transaction is only valid if the sending account's previously sent transaction matches
171    /// the provided hash.
172    ///
173    /// # Returns
174    ///
175    /// Returns a `Result<Option<Hash256>>` where:
176    /// * `Ok(Some(Hash256))` - The hash of the required previous transaction
177    /// * `Ok(None)` - If no previous transaction requirement is specified
178    /// * `Err(Error)` - If an error occurred during field retrieval
179    fn get_account_txn_id(&self) -> Result<Option<Hash256>> {
180        get_field_optional(sfield::AccountTxnID)
181    }
182
183    /// Retrieves the delegate account from the current transaction.
184    ///
185    /// This optional field identifies a delegate account that is sending the transaction on behalf
186    /// of the Account. Requires the PermissionDelegation amendment.
187    ///
188    /// # Returns
189    ///
190    /// Returns a `Result<Option<AccountID>>` where:
191    /// * `Ok(Some(AccountID))` - The 20-byte account identifier of the delegate
192    /// * `Ok(None)` - If no delegate is specified (the `Account` is sending directly)
193    /// * `Err(Error)` - If an error occurred during field retrieval
194    fn get_delegate(&self) -> Result<Option<AccountID>> {
195        get_field_optional(sfield::Delegate)
196    }
197
198    /// Retrieves the `flags` field from the current transaction.
199    ///
200    /// This optional field contains a bitfield of transaction-specific flags that modify
201    /// the transaction's behavior.
202    ///
203    /// # Returns
204    ///
205    /// Returns a `Result<Option<u32>>` where:
206    /// * `Ok(Some(u32))` - The flags bitfield if present
207    /// * `Ok(None)` - If no flags are specified (equivalent to flags = 0)
208    /// * `Err(Error)` - If an error occurred during field retrieval
209    fn get_flags(&self) -> Result<Option<u32>> {
210        get_field_optional(sfield::Flags)
211    }
212
213    /// Retrieves the last ledger sequence from the current transaction.
214    ///
215    /// This optional field specifies the highest ledger index this transaction can appear in.
216    /// Specifying this field places a strict upper limit on how long the transaction can wait to
217    /// be validated or rejected. See Reliable Transaction Submission for more details.
218    ///
219    /// # Returns
220    ///
221    /// Returns a `Result<Option<u32>>` where:
222    /// * `Ok(Some(u32))` - The maximum ledger index for transaction inclusion
223    /// * `Ok(None)` - If no expiration is specified (transaction never expires)
224    /// * `Err(Error)` - If an error occurred during field retrieval
225    fn get_last_ledger_sequence(&self) -> Result<Option<u32>> {
226        get_field_optional(sfield::LastLedgerSequence)
227    }
228
229    /// Retrieves the network ID from the current transaction.
230    ///
231    /// This optional field identifies the network ID of the chain this transaction is intended for.
232    /// MUST BE OMITTED for Mainnet and some test networks. REQUIRED on chains whose network ID is
233    /// 1025 or higher.
234    ///
235    /// # Returns
236    ///
237    /// Returns a `Result<Option<u32>>` where:
238    /// * `Ok(Some(u32))` - The network identifier
239    /// * `Ok(None)` - If no specific network is specified (uses default network)
240    /// * `Err(Error)` - If an error occurred during field retrieval
241    fn get_network_id(&self) -> Result<Option<u32>> {
242        get_field_optional(sfield::NetworkID)
243    }
244
245    /// Retrieves the source tag from the current transaction.
246    ///
247    /// This optional field is an arbitrary integer used to identify the reason for this payment, or
248    /// a sender on whose behalf this transaction is made. Conventionally, a refund should specify
249    /// the initial payment's SourceTag as the refund payment's DestinationTag.
250    ///
251    /// # Returns
252    ///
253    /// Returns a `Result<Option<u32>>` where:
254    /// * `Ok(Some(u32))` - The source tag identifier
255    /// * `Ok(None)` - If no source tag is specified
256    /// * `Err(Error)` - If an error occurred during field retrieval
257    fn get_source_tag(&self) -> Result<Option<u32>> {
258        get_field_optional(sfield::SourceTag)
259    }
260
261    /// Retrieves the signing public key from the current transaction.
262    ///
263    /// This field contains the hex representation of the public key that corresponds to the
264    /// private key used to sign this transaction. If an empty string, this field indicates that a
265    /// multi-signature is present in the Signers field instead.
266    ///
267    /// # Returns
268    ///
269    /// Returns a `Result<Option<PublicKey>>` where:
270    /// * `Ok(Some(PublicKey))` - The 33-byte compressed public key for single-signature transactions
271    /// * `Ok(None)` - Empty SigningPubKey field, indicating a multi-signature transaction
272    /// * `Err(Error)` - If the field cannot be retrieved
273    ///
274    /// # Panics
275    ///
276    /// Panics if the field is present with a length other than 0 or 33 bytes. rippled's
277    /// preflight rejects such transactions before they are applied, so this is an internal
278    /// invariant violation rather than recoverable input.
279    ///
280    /// # Security Note
281    ///
282    /// The presence of this field doesn't guarantee the signature is valid. Instead, this field
283    /// only provides the key claimed to be used for signing. The XRPL network performs signature
284    /// validation before transaction execution.
285    fn get_signing_pub_key(&self) -> Result<Option<PublicKey>> {
286        get_field(sfield::SigningPubKey).and_then(|blob| match blob.len {
287            0 => Result::Ok(None), // Multi-signature transaction
288            33 => Result::Ok(Some(PublicKey::from(blob.data))), // Single-signature transaction
289            // Unreachable in practice (see `# Panics`); fail fast if the invariant breaks.
290            len => panic!("internal invariant violated: SigningPubKey has unexpected length {len} (expected 0 or 33)"),
291        })
292    }
293
294    /// Retrieves the ticket sequence from the current transaction.
295    ///
296    /// This optional field provides the sequence number of the ticket to use in place of a
297    /// Sequence number. If this is provided, Sequence must be 0. Cannot be used with AccountTxnID.
298    ///
299    /// # Returns
300    ///
301    /// Returns a `Result<Option<u32>>` where:
302    /// * `Ok(Some(u32))` - The ticket sequence number if the transaction uses tickets
303    /// * `Ok(None)` - If the transaction uses traditional sequence numbering
304    /// * `Err(Error)` - If an error occurred during field retrieval
305    ///
306    /// # Note
307    ///
308    /// Transactions use either `Sequence` or `TicketSequence`, but not both. Check this
309    /// field when `get_sequence()` fails or when implementing ticket-aware logic.
310    fn get_ticket_sequence(&self) -> Result<Option<u32>> {
311        get_field_optional(sfield::TicketSequence)
312    }
313
314    /// Retrieves the transaction signature from the current transaction.
315    ///
316    /// This mandatory field contains the signature that verifies this transaction as originating
317    /// from the account it says it is from.
318    ///
319    /// Signatures can be either:
320    /// - 64 bytes for EdDSA (Ed25519) signatures
321    /// - 70-72 bytes for ECDSA (secp256k1) signatures
322    ///
323    /// # Returns
324    ///
325    /// Returns a `Result<Signature>` where:
326    /// * `Ok(Signature)` - The transaction signature (up to 72 bytes)
327    /// * `Err(Error)` - If the field cannot be retrieved
328    ///
329    /// # Security Note
330    ///
331    /// The signature is validated by the XRPL network before transaction execution.
332    /// In the programmability context, you can access the signature for logging or
333    /// analysis purposes, but signature validation has already been performed.
334    fn get_txn_signature(&self) -> Result<SignatureBlob> {
335        get_field(sfield::TxnSignature)
336    }
337}
338
339#[cfg(test)]
340mod tests {
341    use crate::current_tx::traits::TransactionCommonFields;
342    use crate::host::host_bindings_trait::MockHostBindings;
343    use crate::sfield::SField;
344    use mockall::predicate::{always, eq};
345
346    /// Minimal concrete type implementing [`TransactionCommonFields`], used to exercise the
347    /// trait's default methods without depending on any transaction-specific wrapper. The
348    /// concrete wrappers (e.g. `EscrowFinish`) now live in the `xrpl-escrow-stdlib` crate, so
349    /// common's own tests use a local stand-in instead.
350    struct TestTransaction;
351    impl TransactionCommonFields for TestTransaction {}
352
353    /// Helper to set up a mock expectation for `tx_field`.
354    fn expect_tx_field<T: Send + std::fmt::Debug + PartialEq + 'static, const CODE: i32>(
355        mock: &mut MockHostBindings,
356        field: SField<T, CODE>,
357        size: usize,
358        times: usize,
359    ) {
360        mock.expect_tx_field()
361            .with(eq(field), always(), eq(size))
362            .times(times)
363            .returning(move |_, _, _| size as i32);
364    }
365
366    #[test]
367    fn path_roots_a_current_tx_builder() {
368        use crate::host::setup_mock;
369        use crate::sfield;
370
371        // `ctx.tx().path()` must build against the current transaction and read through
372        // `tx_inner`. Memos[0] is two 4-byte segments = 8 bytes; the u32 buffer is 4.
373        let mut mock = MockHostBindings::new();
374        mock.expect_tx_inner()
375            .with(always(), eq(8usize), always(), eq(4usize))
376            .times(1)
377            .returning(|_, _, _, _| 4);
378        let _guard = setup_mock(mock);
379
380        let result = TestTransaction
381            .path()
382            .field(sfield::Memos)
383            .index(0)
384            .get::<u32>();
385        assert!(result.is_ok());
386    }
387
388    mod transaction_common_fields {
389
390        mod optional_fields {
391            use crate::current_tx::traits::TransactionCommonFields;
392            use crate::current_tx::traits::tests::TestTransaction;
393            use crate::current_tx::traits::tests::expect_tx_field;
394            use crate::host::error_codes::{FIELD_NOT_FOUND, INVALID_FIELD, SOME_ERROR};
395            use crate::host::host_bindings_trait::MockHostBindings;
396            use crate::host::setup_mock;
397            use crate::sfield;
398            use crate::types::account_id::ACCOUNT_ID_SIZE;
399            use crate::types::uint::HASH256_SIZE;
400            use mockall::predicate::{always, eq};
401
402            #[test]
403            fn test_optional_fields_return_some() {
404                let mut mock = MockHostBindings::new();
405
406                // get_account_txn_id
407                expect_tx_field(&mut mock, sfield::AccountTxnID, HASH256_SIZE, 1);
408                // get_delegate
409                expect_tx_field(&mut mock, sfield::Delegate, ACCOUNT_ID_SIZE, 1);
410                // get_flags
411                expect_tx_field(&mut mock, sfield::Flags, 4, 1);
412                // get_last_ledger_sequence
413                expect_tx_field(&mut mock, sfield::LastLedgerSequence, 4, 1);
414                // get_network_id
415                expect_tx_field(&mut mock, sfield::NetworkID, 4, 1);
416                // get_source_tag
417                expect_tx_field(&mut mock, sfield::SourceTag, 4, 1);
418                // get_ticket_sequence
419                expect_tx_field(&mut mock, sfield::TicketSequence, 4, 1);
420
421                let _guard = setup_mock(mock);
422
423                let tx = TestTransaction;
424
425                // All optional fields should return Ok(Some(...))
426                assert!(tx.get_account_txn_id().unwrap().is_some());
427                assert!(tx.get_delegate().unwrap().is_some());
428                assert!(tx.get_flags().unwrap().is_some());
429                assert!(tx.get_last_ledger_sequence().unwrap().is_some());
430                assert!(tx.get_network_id().unwrap().is_some());
431                assert!(tx.get_source_tag().unwrap().is_some());
432                assert!(tx.get_ticket_sequence().unwrap().is_some());
433            }
434
435            #[test]
436            fn test_optional_fields_return_none_when_field_not_found() {
437                let mut mock = MockHostBindings::new();
438
439                // get_account_txn_id
440                mock.expect_tx_field()
441                    .with(eq(sfield::AccountTxnID), always(), eq(HASH256_SIZE))
442                    .times(1)
443                    .returning(|_, _, _| FIELD_NOT_FOUND);
444                // get_delegate
445                mock.expect_tx_field()
446                    .with(eq(sfield::Delegate), always(), eq(ACCOUNT_ID_SIZE))
447                    .times(1)
448                    .returning(|_, _, _| FIELD_NOT_FOUND);
449                // get_flags
450                mock.expect_tx_field()
451                    .with(eq(sfield::Flags), always(), eq(4))
452                    .times(1)
453                    .returning(|_, _, _| FIELD_NOT_FOUND);
454                // get_last_ledger_sequence
455                mock.expect_tx_field()
456                    .with(eq(sfield::LastLedgerSequence), always(), eq(4))
457                    .times(1)
458                    .returning(|_, _, _| FIELD_NOT_FOUND);
459                // get_network_id
460                mock.expect_tx_field()
461                    .with(eq(sfield::NetworkID), always(), eq(4))
462                    .times(1)
463                    .returning(|_, _, _| FIELD_NOT_FOUND);
464                // get_source_tag
465                mock.expect_tx_field()
466                    .with(eq(sfield::SourceTag), always(), eq(4))
467                    .times(1)
468                    .returning(|_, _, _| FIELD_NOT_FOUND);
469                // get_ticket_sequence
470                mock.expect_tx_field()
471                    .with(eq(sfield::TicketSequence), always(), eq(4))
472                    .times(1)
473                    .returning(|_, _, _| FIELD_NOT_FOUND);
474
475                let _guard = setup_mock(mock);
476
477                let tx = TestTransaction;
478
479                // Fixed-size optional fields should return Ok(None) when FIELD_NOT_FOUND
480                assert!(tx.get_account_txn_id().unwrap().is_none());
481                assert!(tx.get_delegate().unwrap().is_none());
482                assert!(tx.get_flags().unwrap().is_none());
483                assert!(tx.get_last_ledger_sequence().unwrap().is_none());
484                assert!(tx.get_network_id().unwrap().is_none());
485                assert!(tx.get_source_tag().unwrap().is_none());
486                assert!(tx.get_ticket_sequence().unwrap().is_none());
487            }
488
489            #[test]
490            fn test_optional_fields_return_none_when_zero_length() {
491                let mut mock = MockHostBindings::new();
492
493                // get_account_txn_id - returns 0 (zero length)
494                mock.expect_tx_field()
495                    .with(eq(sfield::AccountTxnID), always(), eq(HASH256_SIZE))
496                    .times(1)
497                    .returning(|_, _, _| 0);
498                // get_delegate - returns 0 (zero length)
499                mock.expect_tx_field()
500                    .with(eq(sfield::Delegate), always(), eq(ACCOUNT_ID_SIZE))
501                    .times(1)
502                    .returning(|_, _, _| 0);
503                // get_flags - returns 0 (zero length)
504                mock.expect_tx_field()
505                    .with(eq(sfield::Flags), always(), eq(4))
506                    .times(1)
507                    .returning(|_, _, _| 0);
508                // get_last_ledger_sequence - returns 0 (zero length)
509                mock.expect_tx_field()
510                    .with(eq(sfield::LastLedgerSequence), always(), eq(4))
511                    .times(1)
512                    .returning(|_, _, _| 0);
513                // get_network_id - returns 0 (zero length)
514                mock.expect_tx_field()
515                    .with(eq(sfield::NetworkID), always(), eq(4))
516                    .times(1)
517                    .returning(|_, _, _| 0);
518                // get_source_tag - returns 0 (zero length)
519                mock.expect_tx_field()
520                    .with(eq(sfield::SourceTag), always(), eq(4))
521                    .times(1)
522                    .returning(|_, _, _| 0);
523                // get_ticket_sequence - returns 0 (zero length)
524                mock.expect_tx_field()
525                    .with(eq(sfield::TicketSequence), always(), eq(4))
526                    .times(1)
527                    .returning(|_, _, _| 0);
528
529                let _guard = setup_mock(mock);
530
531                let tx = TestTransaction;
532
533                // Fixed-size optional fields return Err (InvalidDecoding) on a zero-length read:
534                // decode's length check rejects the byte-count mismatch.
535                assert!(tx.get_account_txn_id().is_err());
536                assert!(tx.get_delegate().is_err());
537                assert!(tx.get_flags().is_err());
538                assert!(tx.get_last_ledger_sequence().is_err());
539                assert!(tx.get_network_id().is_err());
540                assert!(tx.get_source_tag().is_err());
541                assert!(tx.get_ticket_sequence().is_err());
542            }
543
544            #[test]
545            fn test_optional_fields_return_error_on_internal_error() {
546                let mut mock = MockHostBindings::new();
547
548                // get_account_txn_id
549                mock.expect_tx_field()
550                    .with(eq(sfield::AccountTxnID), always(), eq(HASH256_SIZE))
551                    .times(1)
552                    .returning(|_, _, _| SOME_ERROR);
553                // get_delegate
554                mock.expect_tx_field()
555                    .with(eq(sfield::Delegate), always(), eq(ACCOUNT_ID_SIZE))
556                    .times(1)
557                    .returning(|_, _, _| SOME_ERROR);
558                // get_flags
559                mock.expect_tx_field()
560                    .with(eq(sfield::Flags), always(), eq(4))
561                    .times(1)
562                    .returning(|_, _, _| SOME_ERROR);
563                // get_last_ledger_sequence
564                mock.expect_tx_field()
565                    .with(eq(sfield::LastLedgerSequence), always(), eq(4))
566                    .times(1)
567                    .returning(|_, _, _| SOME_ERROR);
568                // get_network_id
569                mock.expect_tx_field()
570                    .with(eq(sfield::NetworkID), always(), eq(4))
571                    .times(1)
572                    .returning(|_, _, _| SOME_ERROR);
573                // get_source_tag
574                mock.expect_tx_field()
575                    .with(eq(sfield::SourceTag), always(), eq(4))
576                    .times(1)
577                    .returning(|_, _, _| SOME_ERROR);
578                // get_ticket_sequence
579                mock.expect_tx_field()
580                    .with(eq(sfield::TicketSequence), always(), eq(4))
581                    .times(1)
582                    .returning(|_, _, _| SOME_ERROR);
583
584                let _guard = setup_mock(mock);
585
586                let tx = TestTransaction;
587
588                // Optional fields should return Err on SOME_ERROR
589                let account_txn_id_result = tx.get_account_txn_id();
590                assert!(account_txn_id_result.is_err());
591                assert_eq!(account_txn_id_result.err().unwrap().code(), SOME_ERROR);
592
593                let delegate_result = tx.get_delegate();
594                assert!(delegate_result.is_err());
595                assert_eq!(delegate_result.err().unwrap().code(), SOME_ERROR);
596
597                let flags_result = tx.get_flags();
598                assert!(flags_result.is_err());
599                assert_eq!(flags_result.err().unwrap().code(), SOME_ERROR);
600
601                let last_ledger_seq_result = tx.get_last_ledger_sequence();
602                assert!(last_ledger_seq_result.is_err());
603                assert_eq!(last_ledger_seq_result.err().unwrap().code(), SOME_ERROR);
604
605                let network_id_result = tx.get_network_id();
606                assert!(network_id_result.is_err());
607                assert_eq!(network_id_result.err().unwrap().code(), SOME_ERROR);
608
609                let source_tag_result = tx.get_source_tag();
610                assert!(source_tag_result.is_err());
611                assert_eq!(source_tag_result.err().unwrap().code(), SOME_ERROR);
612
613                let ticket_seq_result = tx.get_ticket_sequence();
614                assert!(ticket_seq_result.is_err());
615                assert_eq!(ticket_seq_result.err().unwrap().code(), SOME_ERROR);
616            }
617
618            #[test]
619            fn test_optional_fields_return_error_on_invalid_field() {
620                let mut mock = MockHostBindings::new();
621
622                // get_account_txn_id
623                mock.expect_tx_field()
624                    .with(eq(sfield::AccountTxnID), always(), eq(HASH256_SIZE))
625                    .times(1)
626                    .returning(|_, _, _| INVALID_FIELD);
627                // get_delegate
628                mock.expect_tx_field()
629                    .with(eq(sfield::Delegate), always(), eq(ACCOUNT_ID_SIZE))
630                    .times(1)
631                    .returning(|_, _, _| INVALID_FIELD);
632                // get_flags
633                mock.expect_tx_field()
634                    .with(eq(sfield::Flags), always(), eq(4))
635                    .times(1)
636                    .returning(|_, _, _| INVALID_FIELD);
637                // get_last_ledger_sequence
638                mock.expect_tx_field()
639                    .with(eq(sfield::LastLedgerSequence), always(), eq(4))
640                    .times(1)
641                    .returning(|_, _, _| INVALID_FIELD);
642                // get_network_id
643                mock.expect_tx_field()
644                    .with(eq(sfield::NetworkID), always(), eq(4))
645                    .times(1)
646                    .returning(|_, _, _| INVALID_FIELD);
647                // get_source_tag
648                mock.expect_tx_field()
649                    .with(eq(sfield::SourceTag), always(), eq(4))
650                    .times(1)
651                    .returning(|_, _, _| INVALID_FIELD);
652                // get_ticket_sequence
653                mock.expect_tx_field()
654                    .with(eq(sfield::TicketSequence), always(), eq(4))
655                    .times(1)
656                    .returning(|_, _, _| INVALID_FIELD);
657
658                let _guard = setup_mock(mock);
659
660                let tx = TestTransaction;
661
662                // Optional fields should return Err on INVALID_FIELD
663                let account_txn_id_result = tx.get_account_txn_id();
664                assert!(account_txn_id_result.is_err());
665                assert_eq!(account_txn_id_result.err().unwrap().code(), INVALID_FIELD);
666
667                let delegate_result = tx.get_delegate();
668                assert!(delegate_result.is_err());
669                assert_eq!(delegate_result.err().unwrap().code(), INVALID_FIELD);
670
671                let flags_result = tx.get_flags();
672                assert!(flags_result.is_err());
673                assert_eq!(flags_result.err().unwrap().code(), INVALID_FIELD);
674
675                let last_ledger_seq_result = tx.get_last_ledger_sequence();
676                assert!(last_ledger_seq_result.is_err());
677                assert_eq!(last_ledger_seq_result.err().unwrap().code(), INVALID_FIELD);
678
679                let network_id_result = tx.get_network_id();
680                assert!(network_id_result.is_err());
681                assert_eq!(network_id_result.err().unwrap().code(), INVALID_FIELD);
682
683                let source_tag_result = tx.get_source_tag();
684                assert!(source_tag_result.is_err());
685                assert_eq!(source_tag_result.err().unwrap().code(), INVALID_FIELD);
686
687                let ticket_seq_result = tx.get_ticket_sequence();
688                assert!(ticket_seq_result.is_err());
689                assert_eq!(ticket_seq_result.err().unwrap().code(), INVALID_FIELD);
690            }
691        }
692
693        mod required_fields {
694            use crate::current_tx::traits::TransactionCommonFields;
695            use crate::current_tx::traits::tests::TestTransaction;
696            use crate::current_tx::traits::tests::expect_tx_field;
697            use crate::host::error_codes::{FIELD_NOT_FOUND, INVALID_FIELD, SOME_ERROR};
698            use crate::host::host_bindings_trait::MockHostBindings;
699            use crate::host::setup_mock;
700            use crate::sfield;
701            use crate::types::account_id::ACCOUNT_ID_SIZE;
702            use crate::types::amount::AMOUNT_SIZE;
703            use crate::types::blob::SIGNATURE_BLOB_SIZE;
704            use crate::types::public_key::PUBLIC_KEY_BUFFER_SIZE;
705            use mockall::predicate::{always, eq};
706
707            #[test]
708            fn test_mandatory_fields_return_ok() {
709                let mut mock = MockHostBindings::new();
710
711                // get_account
712                expect_tx_field(&mut mock, sfield::Account, ACCOUNT_ID_SIZE, 1);
713                // get_transaction_type
714                expect_tx_field(&mut mock, sfield::TransactionType, 2, 1);
715                // get_gas
716                expect_tx_field(&mut mock, sfield::Gas, 4, 1);
717                // get_fee: a real XRP Fee is 8 bytes written into the 48-byte Amount buffer, not
718                // the full buffer length (`expect_tx_field` would report `AMOUNT_SIZE` written,
719                // which `Amount::decode` correctly rejects as inconsistent with the XRP variant).
720                mock.expect_tx_field()
721                    .with(eq(sfield::Fee), always(), eq(AMOUNT_SIZE))
722                    .times(1)
723                    .returning(|_, _, _| 8);
724                // get_sequence
725                expect_tx_field(&mut mock, sfield::Sequence, 4, 1);
726                // get_signing_pub_key
727                expect_tx_field(&mut mock, sfield::SigningPubKey, PUBLIC_KEY_BUFFER_SIZE, 1);
728                // get_txn_signature
729                expect_tx_field(&mut mock, sfield::TxnSignature, SIGNATURE_BLOB_SIZE, 1);
730
731                let _guard = setup_mock(mock);
732
733                let tx = TestTransaction;
734
735                // All mandatory fields should return Ok
736                assert!(tx.get_account().is_ok());
737                assert!(tx.get_transaction_type().is_ok());
738                assert!(tx.get_gas().is_ok());
739                assert!(tx.get_fee().is_ok());
740                assert!(tx.get_sequence().is_ok());
741                assert!(tx.get_signing_pub_key().is_ok());
742                assert!(tx.get_txn_signature().is_ok());
743            }
744
745            // A zero-length read of a mandatory fixed-size field fails `FieldDecoder::decode`'s
746            // length check and surfaces as `Err(InvalidDecoding)`.
747
748            #[test]
749            fn test_get_account_errors_when_zero_length() {
750                let mut mock = MockHostBindings::new();
751                mock.expect_tx_field()
752                    .with(eq(sfield::Account), always(), eq(ACCOUNT_ID_SIZE))
753                    .returning(|_, _, _| 0);
754
755                let _guard = setup_mock(mock);
756                let result = TestTransaction.get_account();
757                assert!(result.is_err());
758                assert_eq!(
759                    result.err().unwrap().code(),
760                    crate::host::Error::InvalidDecoding.code()
761                );
762            }
763
764            #[test]
765            fn test_get_transaction_type_errors_when_zero_length() {
766                let mut mock = MockHostBindings::new();
767                mock.expect_tx_field()
768                    .with(eq(sfield::TransactionType), always(), eq(2))
769                    .returning(|_, _, _| 0);
770
771                let _guard = setup_mock(mock);
772                let result = TestTransaction.get_transaction_type();
773                assert!(result.is_err());
774                assert_eq!(
775                    result.err().unwrap().code(),
776                    crate::host::Error::InvalidDecoding.code()
777                );
778            }
779
780            #[test]
781            fn test_get_gas_errors_when_zero_length() {
782                let mut mock = MockHostBindings::new();
783                mock.expect_tx_field()
784                    .with(eq(sfield::Gas), always(), eq(4))
785                    .returning(|_, _, _| 0);
786
787                let _guard = setup_mock(mock);
788                let result = TestTransaction.get_gas();
789                assert!(result.is_err());
790                assert_eq!(
791                    result.err().unwrap().code(),
792                    crate::host::Error::InvalidDecoding.code()
793                );
794            }
795
796            #[test]
797            fn test_get_sequence_errors_when_zero_length() {
798                let mut mock = MockHostBindings::new();
799                mock.expect_tx_field()
800                    .with(eq(sfield::Sequence), always(), eq(4))
801                    .returning(|_, _, _| 0);
802
803                let _guard = setup_mock(mock);
804                let result = TestTransaction.get_sequence();
805                assert!(result.is_err());
806                assert_eq!(
807                    result.err().unwrap().code(),
808                    crate::host::Error::InvalidDecoding.code()
809                );
810            }
811
812            #[test]
813            fn test_variable_size_fields_ok_when_zero_length() {
814                let mut mock = MockHostBindings::new();
815
816                // get_signing_pub_key - returns 0 (zero length)
817                mock.expect_tx_field()
818                    .with(
819                        eq(sfield::SigningPubKey),
820                        always(),
821                        eq(PUBLIC_KEY_BUFFER_SIZE),
822                    )
823                    .times(1)
824                    .returning(|_, _, _| 0);
825
826                let _guard = setup_mock(mock);
827
828                let tx = TestTransaction;
829
830                // SigningPubKey is special: zero length indicates multi-signature transaction
831                // and should return Ok(None), not an error
832                let signing_key_result = tx.get_signing_pub_key();
833                assert!(signing_key_result.is_ok());
834                assert!(signing_key_result.unwrap().is_none());
835            }
836
837            #[test]
838            fn test_get_fee_ok_when_host_writes_only_xrp_amount_bytes() {
839                // A real XRP amount from the host is only 8 bytes (not the full 48-byte
840                // AMOUNT_SIZE buffer used for IOU/MPT amounts); decode must zero-pad rather
841                // than requiring an exact 48-byte slice.
842                let mut mock = MockHostBindings::new();
843                mock.expect_tx_field()
844                    .with(eq(sfield::Fee), always(), eq(AMOUNT_SIZE))
845                    .returning(|_, buf, _| {
846                        // XRP positive flag (0x40) + 1,000,000 drops in the low 7 bytes.
847                        let mut bytes = [0u8; 8];
848                        bytes[0] = 0x40;
849                        bytes[1..8].copy_from_slice(&1_000_000u64.to_be_bytes()[1..8]);
850                        unsafe {
851                            core::ptr::copy_nonoverlapping(bytes.as_ptr(), buf, bytes.len());
852                        }
853                        bytes.len() as i32
854                    });
855
856                let _guard = setup_mock(mock);
857                let result = TestTransaction.get_fee();
858                assert!(result.is_ok());
859                match result.unwrap() {
860                    crate::types::amount::Amount::XRP { num_drops } => {
861                        assert_eq!(num_drops, 1_000_000);
862                    }
863                    _ => panic!("Expected Amount::XRP"),
864                }
865            }
866
867            #[test]
868            fn test_get_fee_errors_when_zero_length() {
869                let mut mock = MockHostBindings::new();
870                mock.expect_tx_field()
871                    .with(eq(sfield::Fee), always(), eq(AMOUNT_SIZE))
872                    .returning(|_, _, _| 0);
873
874                let _guard = setup_mock(mock);
875                let result = TestTransaction.get_fee();
876                assert!(result.is_err());
877                assert_eq!(
878                    result.err().unwrap().code(),
879                    crate::host::Error::InvalidDecoding.code()
880                );
881            }
882
883            #[test]
884            #[should_panic]
885            fn test_get_signing_pub_key_panics_on_unexpected_length() {
886                let mut mock = MockHostBindings::new();
887
888                // A SigningPubKey that is neither empty (0, multisign) nor a valid key (33)
889                // can never reach a running escrow: rippled's preflight rejects it. Observing
890                // such a length is an internal invariant violation and must panic.
891                mock.expect_tx_field()
892                    .with(
893                        eq(sfield::SigningPubKey),
894                        always(),
895                        eq(PUBLIC_KEY_BUFFER_SIZE),
896                    )
897                    .returning(|_, _, _| 16);
898
899                let _guard = setup_mock(mock);
900
901                let _ = TestTransaction.get_signing_pub_key();
902            }
903
904            #[test]
905            fn test_mandatory_fields_return_error_on_field_not_found() {
906                let mut mock = MockHostBindings::new();
907
908                // get_account
909                mock.expect_tx_field()
910                    .with(eq(sfield::Account), always(), eq(ACCOUNT_ID_SIZE))
911                    .times(1)
912                    .returning(|_, _, _| FIELD_NOT_FOUND);
913                // get_transaction_type
914                mock.expect_tx_field()
915                    .with(eq(sfield::TransactionType), always(), eq(2))
916                    .times(1)
917                    .returning(|_, _, _| FIELD_NOT_FOUND);
918                // get_gas
919                mock.expect_tx_field()
920                    .with(eq(sfield::Gas), always(), eq(4))
921                    .times(1)
922                    .returning(|_, _, _| FIELD_NOT_FOUND);
923                // get_fee
924                mock.expect_tx_field()
925                    .with(eq(sfield::Fee), always(), eq(AMOUNT_SIZE))
926                    .times(1)
927                    .returning(|_, _, _| FIELD_NOT_FOUND);
928                // get_sequence
929                mock.expect_tx_field()
930                    .with(eq(sfield::Sequence), always(), eq(4))
931                    .times(1)
932                    .returning(|_, _, _| FIELD_NOT_FOUND);
933                // get_signing_pub_key
934                mock.expect_tx_field()
935                    .with(
936                        eq(sfield::SigningPubKey),
937                        always(),
938                        eq(PUBLIC_KEY_BUFFER_SIZE),
939                    )
940                    .times(1)
941                    .returning(|_, _, _| FIELD_NOT_FOUND);
942
943                let _guard = setup_mock(mock);
944
945                let tx = TestTransaction;
946
947                // All mandatory fields should return Err on FIELD_NOT_FOUND
948                let account_result = tx.get_account();
949                assert!(account_result.is_err());
950                assert_eq!(account_result.err().unwrap().code(), FIELD_NOT_FOUND);
951
952                let tx_type_result = tx.get_transaction_type();
953                assert!(tx_type_result.is_err());
954                assert_eq!(tx_type_result.err().unwrap().code(), FIELD_NOT_FOUND);
955
956                let comp_allow_result = tx.get_gas();
957                assert!(comp_allow_result.is_err());
958                assert_eq!(comp_allow_result.err().unwrap().code(), FIELD_NOT_FOUND);
959
960                let fee_result = tx.get_fee();
961                assert!(fee_result.is_err());
962                assert_eq!(fee_result.err().unwrap().code(), FIELD_NOT_FOUND);
963
964                let seq_result = tx.get_sequence();
965                assert!(seq_result.is_err());
966                assert_eq!(seq_result.err().unwrap().code(), FIELD_NOT_FOUND);
967
968                let signing_key_result = tx.get_signing_pub_key();
969                assert!(signing_key_result.is_err());
970                assert_eq!(signing_key_result.err().unwrap().code(), FIELD_NOT_FOUND);
971            }
972
973            #[test]
974            fn test_mandatory_fields_return_error_on_internal_error() {
975                let mut mock = MockHostBindings::new();
976
977                // get_account
978                mock.expect_tx_field()
979                    .with(eq(sfield::Account), always(), eq(ACCOUNT_ID_SIZE))
980                    .times(1)
981                    .returning(|_, _, _| SOME_ERROR);
982                // get_transaction_type
983                mock.expect_tx_field()
984                    .with(eq(sfield::TransactionType), always(), eq(2))
985                    .times(1)
986                    .returning(|_, _, _| SOME_ERROR);
987                // get_gas
988                mock.expect_tx_field()
989                    .with(eq(sfield::Gas), always(), eq(4))
990                    .times(1)
991                    .returning(|_, _, _| SOME_ERROR);
992                // get_fee
993                mock.expect_tx_field()
994                    .with(eq(sfield::Fee), always(), eq(AMOUNT_SIZE))
995                    .times(1)
996                    .returning(|_, _, _| SOME_ERROR);
997                // get_sequence
998                mock.expect_tx_field()
999                    .with(eq(sfield::Sequence), always(), eq(4))
1000                    .times(1)
1001                    .returning(|_, _, _| SOME_ERROR);
1002                // get_signing_pub_key
1003                mock.expect_tx_field()
1004                    .with(
1005                        eq(sfield::SigningPubKey),
1006                        always(),
1007                        eq(PUBLIC_KEY_BUFFER_SIZE),
1008                    )
1009                    .times(1)
1010                    .returning(|_, _, _| SOME_ERROR);
1011
1012                let _guard = setup_mock(mock);
1013
1014                let tx = TestTransaction;
1015
1016                // All mandatory fields should return Err on SOME_ERROR
1017                let account_result = tx.get_account();
1018                assert!(account_result.is_err());
1019                assert_eq!(account_result.err().unwrap().code(), SOME_ERROR);
1020
1021                let tx_type_result = tx.get_transaction_type();
1022                assert!(tx_type_result.is_err());
1023                assert_eq!(tx_type_result.err().unwrap().code(), SOME_ERROR);
1024
1025                let comp_allow_result = tx.get_gas();
1026                assert!(comp_allow_result.is_err());
1027                assert_eq!(comp_allow_result.err().unwrap().code(), SOME_ERROR);
1028
1029                let fee_result = tx.get_fee();
1030                assert!(fee_result.is_err());
1031                assert_eq!(fee_result.err().unwrap().code(), SOME_ERROR);
1032
1033                let seq_result = tx.get_sequence();
1034                assert!(seq_result.is_err());
1035                assert_eq!(seq_result.err().unwrap().code(), SOME_ERROR);
1036
1037                let signing_key_result = tx.get_signing_pub_key();
1038                assert!(signing_key_result.is_err());
1039                assert_eq!(signing_key_result.err().unwrap().code(), SOME_ERROR);
1040            }
1041
1042            #[test]
1043            fn test_mandatory_fields_return_error_on_invalid_field() {
1044                let mut mock = MockHostBindings::new();
1045
1046                // get_account
1047                mock.expect_tx_field()
1048                    .with(eq(sfield::Account), always(), eq(ACCOUNT_ID_SIZE))
1049                    .times(1)
1050                    .returning(|_, _, _| INVALID_FIELD);
1051                // get_transaction_type
1052                mock.expect_tx_field()
1053                    .with(eq(sfield::TransactionType), always(), eq(2))
1054                    .times(1)
1055                    .returning(|_, _, _| INVALID_FIELD);
1056                // get_gas
1057                mock.expect_tx_field()
1058                    .with(eq(sfield::Gas), always(), eq(4))
1059                    .times(1)
1060                    .returning(|_, _, _| INVALID_FIELD);
1061                // get_fee
1062                mock.expect_tx_field()
1063                    .with(eq(sfield::Fee), always(), eq(AMOUNT_SIZE))
1064                    .times(1)
1065                    .returning(|_, _, _| INVALID_FIELD);
1066                // get_sequence
1067                mock.expect_tx_field()
1068                    .with(eq(sfield::Sequence), always(), eq(4))
1069                    .times(1)
1070                    .returning(|_, _, _| INVALID_FIELD);
1071                // get_signing_pub_key
1072                mock.expect_tx_field()
1073                    .with(
1074                        eq(sfield::SigningPubKey),
1075                        always(),
1076                        eq(PUBLIC_KEY_BUFFER_SIZE),
1077                    )
1078                    .times(1)
1079                    .returning(|_, _, _| INVALID_FIELD);
1080
1081                let _guard = setup_mock(mock);
1082
1083                let tx = TestTransaction;
1084
1085                // All mandatory fields should return Err on INVALID_FIELD
1086                let account_result = tx.get_account();
1087                assert!(account_result.is_err());
1088                assert_eq!(account_result.err().unwrap().code(), INVALID_FIELD);
1089
1090                let tx_type_result = tx.get_transaction_type();
1091                assert!(tx_type_result.is_err());
1092                assert_eq!(tx_type_result.err().unwrap().code(), INVALID_FIELD);
1093
1094                let comp_allow_result = tx.get_gas();
1095                assert!(comp_allow_result.is_err());
1096                assert_eq!(comp_allow_result.err().unwrap().code(), INVALID_FIELD);
1097
1098                let fee_result = tx.get_fee();
1099                assert!(fee_result.is_err());
1100                assert_eq!(fee_result.err().unwrap().code(), INVALID_FIELD);
1101
1102                let seq_result = tx.get_sequence();
1103                assert!(seq_result.is_err());
1104                assert_eq!(seq_result.err().unwrap().code(), INVALID_FIELD);
1105
1106                let signing_key_result = tx.get_signing_pub_key();
1107                assert!(signing_key_result.is_err());
1108                assert_eq!(signing_key_result.err().unwrap().code(), INVALID_FIELD);
1109            }
1110        }
1111    }
1112}