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}