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}