Skip to main content

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