Lesson 10: Transaction Inspection - Reading Originating Transactions
Learning Objectives
Extract all standard transaction fields using otxn_field()
Handle different transaction types appropriately
Parse XRP and IOU amounts correctly
Read and process memos from transactions
Build complex filtering logic based on transaction properties
Your Hook receives one primary input: the originating transaction. Everything your Hook can know about the outside world (besides ledger state) comes from inspecting this transaction:
TRANSACTION DATA AVAILABLE
UNIVERSAL FIELDS (all transactions):
├── Account (sender)
├── TransactionType
├── Fee
├── Sequence
├── SigningPubKey
├── TxnSignature
└── Memos (optional)
TYPE-SPECIFIC FIELDS:
├── Payment: Destination, Amount, DestinationTag, etc.
├── TrustSet: LimitAmount, QualityIn, QualityOut
├── OfferCreate: TakerPays, TakerGets, Expiration
├── AccountSet: SetFlag, ClearFlag, Domain
├── SetHook: Hooks array
└── ... many more types
```
Every transaction has a sender:
// Get sender account ID (20 bytes)
uint8_t sender[20];
int64_t len = otxn_field(SBUF(sender), sfAccount);
if (len != 20) {
rollback(SBUF("Could not read sender"), 1);
}
// sender now contains 20-byte account ID
// NOT the r-address string—the binary ID
// To convert to r-address for logging:
char sender_addr[35];
util_raddr(SBUF(sender_addr), SBUF(sender));
// sender_addr now contains "rXXXX..." string
// Get transaction type code
int64_t type = otxn_type();
// Common type codes:
#define ttPAYMENT 0
#define ttESCROW_CREATE 1
#define ttESCROW_FINISH 2
#define ttACCOUNT_SET 3
#define ttESCROW_CANCEL 4
#define ttREGULAR_KEY_SET 5
#define ttNICKNAME_SET 6 // Deprecated
#define ttOFFER_CREATE 7
#define ttOFFER_CANCEL 8
#define ttTICKET_CREATE 10
#define ttSIGNER_LIST_SET 12
#define ttPAYCHAN_CREATE 13
#define ttPAYCHAN_FUND 14
#define ttPAYCHAN_CLAIM 15
#define ttCHECK_CREATE 16
#define ttCHECK_CASH 17
#define ttCHECK_CANCEL 18
#define ttDEPOSIT_PREAUTH 19
#define ttTRUST_SET 20
#define ttACCOUNT_DELETE 21
#define ttSET_HOOK 22
#define ttURI_TOKEN_MINT 45
#define ttURI_TOKEN_BURN 46
// ... and more
// Example: Only process payments
if (type != ttPAYMENT) {
accept(SBUF("Non-payment accepted"), 0);
}
```
// Get fee (in drops)
uint8_t fee_buf[8];
int64_t len = otxn_field(SBUF(fee_buf), sfFee);
if (len == 8) {
int64_t fee = INT64_FROM_BUF(fee_buf);
trace(SBUF("Transaction fee:"), fee);
}
```
// Get sequence number
uint8_t seq_buf[4];
int64_t len = otxn_field(SBUF(seq_buf), sfSequence);
if (len == 4) {
uint32_t sequence = UINT32_FROM_BUF(seq_buf);
trace(SBUF("Sequence:"), sequence);
}
```
// Get transaction hash
uint8_t txid[32];
int64_t len = otxn_id(SBUF(txid), 0);
if (len != 32) {
rollback(SBUF("Could not get txid"), 1);
}
// Useful for logging/deduplication
trace(SBUF("TxID first byte:"), txid[0]);
```
// Get destination account (payments only)
uint8_t dest[20];
int64_t len = otxn_field(SBUF(dest), sfDestination);
if (len != 20) {
// Not a payment, or malformed
rollback(SBUF("No destination"), 1);
}
// Check if payment is to us (Hook account)
uint8_t hook_acc[20];
hook_account(SBUF(hook_acc));
int to_us = 1;
for (int i = 0; GUARD(20), i < 20; ++i) {
if (dest[i] != hook_acc[i]) {
to_us = 0;
break;
}
}
if (to_us) {
trace(SBUF("Payment TO hook account"), 0);
} else {
trace(SBUF("Payment FROM hook account"), 0);
}
```
For XRP payments:
// Simple XRP amount
int64_t drops = otxn_amount();
if (drops < 0) {
// This is an IOU payment, not XRP
trace(SBUF("IOU payment detected"), 0);
} else {
trace(SBUF("XRP drops:"), drops);
// Convert to XRP for display
int64_t xrp = drops / 1000000;
int64_t remainder = drops % 1000000;
trace(SBUF("XRP whole:"), xrp);
trace(SBUF("XRP fraction (microXRP):"), remainder);
}
For IOU payments, you need to parse the full Amount field:
// IOU amounts are 48 bytes
uint8_t amount_buf[48];
int64_t len = otxn_field(SBUF(amount_buf), sfAmount);
if (len == 8) {
// XRP amount (8 bytes)
int64_t drops = INT64_FROM_BUF(amount_buf);
trace(SBUF("XRP amount:"), drops);
} else if (len == 48) {
// IOU amount (48 bytes)
// Structure:
// Bytes 0-7: Amount value (special float encoding)
// Bytes 8-27: Currency code (20 bytes, first 12 are usually 0)
// Bytes 28-47: Issuer account ID (20 bytes)
// Extract currency (usually bytes 12-14 are the 3-char code)
trace(SBUF("Currency byte 12:"), amount_buf[12]);
trace(SBUF("Currency byte 13:"), amount_buf[13]);
trace(SBUF("Currency byte 14:"), amount_buf[14]);
// Extract issuer (last 20 bytes)
uint8_t issuer[20];
for (int i = 0; GUARD(20), i < 20; ++i) {
issuer[i] = amount_buf[28 + i];
}
trace(SBUF("Issuer first byte:"), issuer[0]);
}
// Get destination tag (optional)
uint8_t dtag_buf[4];
int64_t len = otxn_field(SBUF(dtag_buf), sfDestinationTag);
if (len == 4) {
uint32_t dtag = UINT32_FROM_BUF(dtag_buf);
trace(SBUF("Destination tag:"), dtag);
} else {
trace(SBUF("No destination tag"), 0);
}
```
// Get source tag (optional)
uint8_t stag_buf[4];
int64_t len = otxn_field(SBUF(stag_buf), sfSourceTag);
if (len == 4) {
uint32_t stag = UINT32_FROM_BUF(stag_buf);
trace(SBUF("Source tag:"), stag);
}
```
// Check if transaction has memos
int64_t memo_len = otxn_field(0, 0, sfMemos);
if (memo_len > 0) {
trace(SBUF("Has memos, total bytes:"), memo_len);
} else {
trace(SBUF("No memos"), 0);
}
```
// Load transaction into slot for memo access
int64_t slot = otxn_slot(1); // Load into slot 1
if (slot < 0) {
rollback(SBUF("Could not slot transaction"), 1);
}
// Get memos array from slot
int64_t memo_slot = slot_subfield(1, sfMemos, 2); // Memos to slot 2
if (memo_slot < 0) {
// No memos
trace(SBUF("No memos in transaction"), 0);
} else {
// Get first memo
int64_t memo0_slot = slot_subarray(2, 0, 3); // First memo to slot 3
if (memo0_slot >= 0) {
// Get memo data
int64_t data_slot = slot_subfield(3, sfMemoData, 4);
if (data_slot >= 0) {
// Read memo data
uint8_t memo_data[256];
int64_t data_len = slot_size(4);
if (data_len > 0 && data_len <= 256) {
slot(SBUF(memo_data), 4);
trace(SBUF("Memo data length:"), data_len);
trace(SBUF("Memo first byte:"), memo_data[0]);
}
}
}
}
```
// Check for specific memo type
int has_special_memo = 0;
uint8_t memo_type[] = "command";
// Load transaction
otxn_slot(1);
// Check memos
int64_t memo_slot = slot_subfield(1, sfMemos, 2);
if (memo_slot >= 0) {
// Iterate memos (assume max 3)
for (int i = 0; GUARD(3), i < 3; ++i) {
int64_t m = slot_subarray(2, i, 10 + i);
if (m < 0) break; // No more memos
// Check memo type
int64_t type_slot = slot_subfield(10 + i, sfMemoType, 20 + i);
if (type_slot >= 0) {
uint8_t type_buf[32];
int64_t type_len = slot_size(20 + i);
if (type_len > 0 && type_len <= 32) {
slot(SBUF(type_buf), 20 + i);
// Compare with expected type
if (type_len == sizeof(memo_type) - 1) {
int match = 1;
for (int j = 0; GUARD(7), j < 7; ++j) {
if (type_buf[j] != memo_type[j]) {
match = 0;
break;
}
}
if (match) {
has_special_memo = 1;
break;
}
}
}
}
}
}
if (has_special_memo) {
trace(SBUF("Found command memo"), 0);
}
```
int64_t hook(uint32_t reserved) {
_g(1, 1);
int64_t type = otxn_type();
switch (type) {
case 0: // Payment
return handle_payment();
case 7: // OfferCreate
return handle_offer();
case 20: // TrustSet
return handle_trust();
default:
// Allow other types by default
accept(SBUF("OK"), 0);
return 0;
}
}
int64_t handle_payment() {
_g(2, 1);
// Payment-specific logic
int64_t amount = otxn_amount();
if (amount < 1000000) { // < 1 XRP
rollback(SBUF("Payment too small"), 1);
}
accept(SBUF("Payment accepted"), 0);
return 0;
}
int64_t handle_offer() {
_g(3, 1);
// Offer-specific logic
// Maybe reject all offers?
rollback(SBUF("Offers not allowed"), 2);
return 0;
}
int64_t handle_trust() {
_g(4, 1);
// Trust line logic
accept(SBUF("Trust line accepted"), 0);
return 0;
}
```
// For OfferCreate transactions
// TakerPays (what creator pays)
uint8_t taker_pays[48];
int64_t len = otxn_field(SBUF(taker_pays), sfTakerPays);
// Parse as amount (8 bytes = XRP, 48 bytes = IOU)
// TakerGets (what creator gets)
uint8_t taker_gets[48];
len = otxn_field(SBUF(taker_gets), sfTakerGets);
// Expiration (optional)
uint8_t exp_buf[4];
len = otxn_field(SBUF(exp_buf), sfExpiration);
if (len == 4) {
uint32_t expiration = UINT32_FROM_BUF(exp_buf);
trace(SBUF("Offer expires:"), expiration);
}
```
// For TrustSet transactions
// LimitAmount (trust limit)
uint8_t limit[48];
int64_t len = otxn_field(SBUF(limit), sfLimitAmount);
// Always 48 bytes for IOU
// QualityIn/Out (optional)
uint8_t qin_buf[4];
len = otxn_field(SBUF(qin_buf), sfQualityIn);
if (len == 4) {
uint32_t quality_in = UINT32_FROM_BUF(qin_buf);
trace(SBUF("Quality in:"), quality_in);
}
```
// Reject payments below minimum
#define MIN_PAYMENT 10000000 // 10 XRP
int64_t hook(uint32_t reserved) {
_g(1, 1);
int64_t type = otxn_type();
if (type != 0) {
accept(SBUF("Not a payment"), 0);
}
int64_t amount = otxn_amount();
if (amount < 0) {
// IOU - might want different minimum
rollback(SBUF("XRP payments only"), 1);
}
if (amount < MIN_PAYMENT) {
rollback(SBUF("Below minimum"), 2);
}
accept(SBUF("Payment accepted"), 0);
return 0;
}
```
// Whitelist certain senders
uint8_t whitelist[3][20] = {
{0x12, 0x34, /* ... 18 more bytes ... */},
{0x56, 0x78, /* ... */},
{0x9A, 0xBC, /* ... */}
};
int is_whitelisted(uint8_t* account) {
for (int i = 0; GUARD(3), i < 3; ++i) {
int match = 1;
for (int j = 0; GUARD(60), j < 20; ++j) {
if (whitelist[i][j] != account[j]) {
match = 0;
break;
}
}
if (match) return 1;
}
return 0;
}
int64_t hook(uint32_t reserved) {
_g(1, 1);
uint8_t sender[20];
otxn_field(SBUF(sender), sfAccount);
if (!is_whitelisted(sender)) {
rollback(SBUF("Not whitelisted"), 1);
}
accept(SBUF("Whitelisted sender"), 0);
return 0;
}
```
// Route based on destination tag
int64_t hook(uint32_t reserved) {
_g(1, 1);
uint8_t dtag_buf[4];
int64_t len = otxn_field(SBUF(dtag_buf), sfDestinationTag);
if (len != 4) {
rollback(SBUF("Destination tag required"), 1);
}
uint32_t dtag = UINT32_FROM_BUF(dtag_buf);
if (dtag >= 1000 && dtag < 2000) {
// Department A
trace(SBUF("Routing to Dept A"), dtag);
} else if (dtag >= 2000 && dtag < 3000) {
// Department B
trace(SBUF("Routing to Dept B"), dtag);
} else {
rollback(SBUF("Invalid department tag"), 2);
}
accept(SBUF("Routed"), 0);
return 0;
}
```
/**
* Smart Payment Filter
*
* Accepts payments based on:
* - Minimum amount (10 XRP)
* - Required destination tag
* - Optional memo for special handling
*/
#include "hookapi.h"
#define MIN_XRP_DROPS 10000000 // 10 XRP
#define SPECIAL_TAG 12345
int64_t hook(uint32_t reserved) {
_g(1, 1);
trace(SBUF("=== Smart Payment Filter ==="), 0);
// 1. Check transaction type
int64_t type = otxn_type();
if (type != 0) {
trace(SBUF("Non-payment, accepting"), type);
accept(SBUF("OK"), 0);
}
// 2. Check amount
int64_t amount = otxn_amount();
trace(SBUF("Amount (drops):"), amount);
if (amount < 0) {
rollback(SBUF("XRP only - no IOUs"), 1);
}
if (amount < MIN_XRP_DROPS) {
rollback(SBUF("Minimum 10 XRP required"), 2);
}
// 3. Check destination tag
uint8_t dtag_buf[4];
int64_t dtag_len = otxn_field(SBUF(dtag_buf), sfDestinationTag);
if (dtag_len != 4) {
rollback(SBUF("Destination tag required"), 3);
}
uint32_t dtag = UINT32_FROM_BUF(dtag_buf);
trace(SBUF("Destination tag:"), dtag);
// 4. Special handling for specific tag
if (dtag == SPECIAL_TAG) {
trace(SBUF("Special tag detected!"), 0);
// Could emit notification, store state, etc.
}
// 5. Log sender
uint8_t sender[20];
int64_t sender_len = otxn_field(SBUF(sender), sfAccount);
if (sender_len == 20) {
trace(SBUF("Sender byte 0:"), sender[0]);
}
// 6. Accept
int64_t xrp = amount / 1000000;
trace(SBUF("Accepting XRP:"), xrp);
accept(SBUF("Payment accepted"), 0);
return 0;
}
int64_t cbak(uint32_t reserved) {
return 0;
}
```
✅ All standard fields are accessible. otxn_field() with appropriate sfCode retrieves any field.
✅ Type-specific handling works. You can build sophisticated logic based on transaction type.
✅ Memos are parseable. Though complex, memo data can be extracted and used.
⚠️ New transaction types may add new fields. API may evolve.
⚠️ IOU parsing complexity. Amount encoding is non-trivial.
🔴 Assuming field presence. Always check return values—fields may be absent.
🔴 Wrong buffer sizes. Account IDs are 20 bytes, hashes are 32 bytes, etc.
🔴 Ignoring IOU handling. If you expect XRP, explicitly reject IOUs.
Transaction inspection is the foundation of useful Hooks. Once you can reliably read sender, destination, amount, type, and memos, you can implement sophisticated business logic. The patterns are consistent: check return values, use correct buffer sizes, and handle missing optional fields gracefully.
Assignment: Build a comprehensive transaction analyzer that extracts and logs all available data.
Requirements:
Create a Hook that:
Extract and log: sender, type, fee, sequence, txid
Handle all correctly with proper buffer sizes
If payment: extract amount, destination, tags
Distinguish XRP from IOU
Log amount in both drops and XRP
Check if memos exist
If present, extract first memo data
Log memo type and data length
Implement rules:
Output: Comprehensive trace output showing all extracted data
- Correct field extraction (30%)
- Proper error handling (25%)
- Complete memo processing (20%)
- Correct decision logic (25%)
Time investment: 3-4 hours
Value: Reference implementation for transaction inspection
Knowledge Check
Question 1 of 5What does otxn_field() return on success?
- sfcodes.h in hooks-toolkit
- XRPL serialization documentation
- xrpl.org/transaction-types.html
- XRPL protocol specification
For Next Lesson:
With transaction inspection mastered, we'll explore the powerful state system—persistent data storage that survives across Hook executions.
End of Lesson 10
Total words: ~4,400
Estimated completion time: 60 minutes reading + 3-4 hours for deliverable
Key Takeaways
otxn_field() is your primary tool.
Pass buffer, size, and sfCode to extract any field.
otxn_type() determines transaction category.
Different types have different fields.
otxn_amount() is for XRP only.
Returns negative for IOUs—use otxn_field() for full IOU amount.
Memos require slot-based access.
Load transaction to slot, then navigate to memo data.
Always validate inputs.
Check return values, handle missing fields, reject invalid transactions. ---