Lesson 7: The Hooks API - Core Functions
Learning Objectives
Use control functions (accept, rollback) to determine transaction outcomes
Read transaction data using otxn_* functions
Access ledger state through the slot system
Manage Hook state for persistent data storage
Build and emit transactions from within a Hook
The Hooks API is intentionally limited—that's a feature, not a bug. Every function exists because it's necessary for legitimate use cases. There's no general-purpose computation, no external calls, no randomness. What remains is a carefully curated set of operations for blockchain-specific logic.
HOOKS API CATEGORIES
CONTROL (4 functions)
├── accept() - Accept transaction
├── rollback() - Reject transaction
├── _g() - Guard declaration
└── trace() - Debug output
TRANSACTION (8+ functions)
├── otxn_burden() - Hook burden
├── otxn_field() - Get field
├── otxn_field_txt() - Get text field
├── otxn_id() - Transaction ID
├── otxn_type() - Transaction type
├── otxn_param() - Hook parameters
├── otxn_slot() - Load tx into slot
└── otxn_amount() - Get XRP amount
LEDGER (6+ functions)
├── ledger_seq() - Current sequence
├── ledger_last_hash() - Last hash
├── ledger_nonce() - Deterministic nonce
├── slot_* - Object access functions
├── keylet_* - Generate keylets
└── trace_slot() - Debug slot contents
STATE (4 functions)
├── state() - Read own state
├── state_set() - Write state
├── state_foreign() - Read other state
├── state_foreign_set() - Write other state (limited)
EMISSION (7 functions)
├── etxn_reserve() - Reserve emissions
├── etxn_burden() - Calculate burden
├── etxn_details() - Get emit details
├── etxn_fee_base() - Fee calculation
├── etxn_nonce() - Generate nonce
├── etxn_generation() - Generation number
└── emit() - Emit transaction
UTILITY (6+ functions)
├── util_raddr() - ID to r-address
├── util_accid() - r-address to ID
├── util_verify() - Signature verify
├── util_sha512h() - Hash
├── util_keylet() - Generate keylet
└── float_* - High-precision math
```
Signals that the Hook approves this transaction:
int64_t accept(
uint32_t read_ptr, uint32_t read_len, // Message
int64_t error_code // Return code
);
// Usage
accept(SBUF("Transaction approved"), 0);
// Behavior:
// - Transaction proceeds to ledger
// - Message logged in Hook execution results
// - Hook stops executing
// - Any state changes are committed
// - Any emissions are queued
// IMPORTANT: accept() stops execution immediately
accept(SBUF("Done"), 0);
trace(SBUF("This never executes"), 0); // Unreachable!
Signals that the Hook rejects this transaction:
int64_t rollback(
uint32_t read_ptr, uint32_t read_len, // Error message
int64_t error_code // Error code
);
// Usage
rollback(SBUF("Payment rejected: amount too small"), 1);
// Behavior:
// - Transaction FAILS
// - Error message returned to sender
// - No ledger changes (except fee still charged)
// - Hook stops executing
// - State changes reverted
// - No emissions occur
// Common pattern
if (amount < minimum) {
rollback(SBUF("Below minimum"), 1);
}
// Only reached if condition is false
accept(SBUF("OK"), 0);
Declares a guard for execution tracking:
int32_t _g(int32_t id, int32_t maxiter);
// Used in GUARD macro:
#define GUARD(maxiter) _g(__LINE__, (maxiter)+1)
// Usage in loops
for (int i = 0; GUARD(10), i < 10; ++i) {
// Loop body
}
// Manual usage (rare)
while (_g(1, 100), condition) {
// Body
}
// Note: GUARD() automatically uses __LINE__ as id
// This makes each guard unique by source line
Logs debug information during execution:
int64_t trace(
uint32_t mread_ptr, uint32_t mread_len, // Message
int64_t data // Associated number
);
// Usage
trace(SBUF("Starting hook"), 0);
trace(SBUF("Amount received:"), amount);
trace(SBUF("Account bytes:"), account[0]);
// Behavior:
// - Message appears in Hook execution logs
// - data value appears alongside message
// - Only visible during testing/debugging
// - Consider removing in production for efficiency
// Pattern: Strategic trace points
int64_t hook(uint32_t r) {
_g(1, 1);
trace(SBUF("Hook entry"), 0);
int64_t amt = otxn_amount();
trace(SBUF("Amount:"), amt);
if (amt < 1000000) {
trace(SBUF("Rejecting small amount"), amt);
rollback(SBUF("Too small"), 1);
}
trace(SBUF("Accepting"), 0);
accept(SBUF("OK"), 0);
return 0;
}
Gets a serialized field from the originating transaction:
int64_t otxn_field(
uint32_t write_ptr, uint32_t write_len, // Output buffer
uint32_t field_id // Field code (sfXxx)
);
// Returns: Number of bytes written, or negative on error
// Common field codes (defined in sfcodes.h):
// sfAccount - Source account (20 bytes)
// sfDestination - Destination account (20 bytes)
// sfAmount - Amount (8 for XRP, 48 for IOU)
// sfMemos - Memo array (variable)
// sfTransactionType - Transaction type (2 bytes)
// sfFee - Transaction fee (8 bytes)
// sfSequence - Sequence number (4 bytes)
// sfDestinationTag - Destination tag (4 bytes)
// sfSourceTag - Source tag (4 bytes)
// Example: Get destination account
uint8_t dest[20];
int64_t len = otxn_field(SBUF(dest), sfDestination);
if (len != 20) {
rollback(SBUF("No destination"), 1);
}
// Example: Check if field exists
int64_t memo_len = otxn_field(0, 0, sfMemos);
if (memo_len > 0) {
// Transaction has memos
}
Convenience function for XRP amount:
int64_t otxn_amount(void);
// Returns: Amount in drops (XRP payments)
// Or negative for IOU (need otxn_field for full amount)
// Usage
int64_t drops = otxn_amount();
if (drops < 0) {
// This is an IOU payment, not XRP
rollback(SBUF("XRP only"), 1);
}
// Check minimum
if (drops < 10000000) { // 10 XRP
rollback(SBUF("Minimum 10 XRP"), 1);
}
Gets the transaction type code:
int64_t otxn_type(void);
// Returns: Transaction type code
// Common types (defined in transaction type codes):
// ttPAYMENT = 0 - Payment
// ttESCROW_CREATE = 1 - Create escrow
// ttESCROW_FINISH = 2 - Finish escrow
// ttACCOUNT_SET = 3 - Account settings
// ttESCROW_CANCEL = 4 - Cancel escrow
// ttTRUST_SET = 20 - Trust line
// ttOFFER_CREATE = 7 - DEX offer
// ttOFFER_CANCEL = 8 - Cancel offer
// ttSET_HOOK = 22 - Set hook
// ... many more
// Usage
int64_t type = otxn_type();
if (type != ttPAYMENT) {
// Not a payment, maybe allow by default
accept(SBUF("Non-payment accepted"), 0);
}
Gets the originating transaction's hash:
int64_t otxn_id(
uint32_t write_ptr, uint32_t write_len, // Output buffer (32 bytes)
uint32_t flags // Flags
);
// Returns: 32 if successful
// Usage
uint8_t txid[32];
int64_t len = otxn_id(SBUF(txid), 0);
if (len != 32) {
rollback(SBUF("Failed to get tx id"), 1);
}
// Useful for:
// - Logging/debugging
// - Deduplication
// - Cross-referencing transactions
Gets install-time Hook parameters:
int64_t otxn_param(
uint32_t write_ptr, uint32_t write_len, // Output buffer
uint32_t read_ptr, uint32_t read_len // Parameter name
);
// Parameters are set during SetHook transaction
// Useful for configurable Hooks
// Example: Get "threshold" parameter
uint8_t value[8];
int64_t len = otxn_param(SBUF(value), SBUF("threshold"));
if (len <= 0) {
// Parameter not set, use default
threshold = 10000000; // 10 XRP
} else {
// Parse parameter value
threshold = UINT64_FROM_BUF(value);
}
Hooks access ledger objects through "slots"—temporary containers:
// Slot workflow:
// 1. Generate keylet (identifier for ledger object)
// 2. Load object into slot
// 3. Read fields from slot
// 4. Slot cleared after Hook execution
// Available slots: 0-255
// You manage which slot to use
int64_t slot_set(
uint32_t read_ptr, uint32_t read_len, // Keylet (34 bytes)
int32_t slot // Slot number to use
);
// Returns: Slot number if successful, negative on error
// Example: Load an account
uint8_t keylet[34];
util_keylet(SBUF(keylet), KEYLET_ACCOUNT,
account_id, 20, 0, 0, 0, 0);
int64_t slot = slot_set(SBUF(keylet), 1); // Use slot 1
if (slot < 0) {
rollback(SBUF("Account not found"), 1);
}
```
int64_t slot_subfield(
int32_t parent_slot, // Slot containing object
int32_t field_id, // Field to extract
int32_t new_slot // Slot for extracted field (or 0)
);
// Returns: Slot number or value, depending on field type
// Example: Get account balance
int64_t balance = slot_subfield(1, sfBalance, 0);
// balance now contains XRP balance in drops
// Example: Get field into another slot
int64_t new = slot_subfield(1, sfOwnerCount, 2);
// Owner count now in slot 2
```
int64_t util_keylet(
uint32_t write_ptr, uint32_t write_len, // Output (34 bytes)
uint32_t keylet_type, // Type of keylet
uint32_t a, uint32_t b, // Type-specific params
uint32_t c, uint32_t d,
uint32_t e, uint32_t f
);
// Keylet types:
// KEYLET_ACCOUNT - Account root
// KEYLET_LINE - Trust line
// KEYLET_OFFER - DEX offer
// KEYLET_ESCROW - Escrow
// KEYLET_HOOK - Hook definition
// ... and more
// Example: Account keylet
uint8_t keylet[34];
uint8_t account[20]; // Account ID to look up
util_keylet(SBUF(keylet), KEYLET_ACCOUNT,
(uint32_t)account, 20,
0, 0, 0, 0);
```
int64_t ledger_seq(void);
// Returns: Current ledger sequence number
// Usage
int64_t seq = ledger_seq();
// Useful for:
// - Time-based logic (sequences ~3-5 sec apart)
// - Logging
// - Deterministic "randomness" (seed)
```
int64_t state(
uint32_t write_ptr, uint32_t write_len, // Output buffer
uint32_t kread_ptr, uint32_t kread_len // Key (max 32 bytes)
);
// Returns: Bytes read, or negative if not found
// Example
uint8_t value[256];
uint8_t key[] = "counter";
int64_t len = state(SBUF(value), SBUF(key));
if (len < 0) {
// State doesn't exist yet
counter = 0;
} else {
counter = UINT64_FROM_BUF(value);
}
```
int64_t state_set(
uint32_t read_ptr, uint32_t read_len, // Value to store
uint32_t kread_ptr, uint32_t kread_len // Key
);
// Returns: Bytes written, or negative on error
// Example
uint8_t value[8];
UINT64_TO_BUF(value, new_counter);
uint8_t key[] = "counter";
int64_t result = state_set(SBUF(value), SBUF(key));
if (result < 0) {
rollback(SBUF("State write failed"), 1);
}
// To delete state, set empty value:
int64_t result = state_set(0, 0, SBUF(key));
```
int64_t state_foreign(
uint32_t write_ptr, uint32_t write_len, // Output buffer
uint32_t kread_ptr, uint32_t kread_len, // Key
uint32_t nread_ptr, uint32_t nread_len, // Namespace (32 bytes)
uint32_t aread_ptr, uint32_t aread_len // Account ID (20 bytes)
);
// Returns: Bytes read, or negative if not found/not allowed
// Note: Target account must have granted access
// via Hook grant mechanism
// Example
uint8_t value[256];
uint8_t key[] = "shared_data";
uint8_t namespace[32]; // Target Hook's namespace
uint8_t account[20]; // Target account
int64_t len = state_foreign(
SBUF(value),
SBUF(key),
SBUF(namespace),
SBUF(account)
);
```
int64_t etxn_reserve(int32_t count);
// Must be called before emitting transactions
// count = number of transactions you'll emit
// Returns: Number reserved, or negative on error
// Example
int64_t reserved = etxn_reserve(1);
if (reserved != 1) {
rollback(SBUF("Could not reserve emission"), 1);
}
// Now you can emit 1 transaction
```
int64_t etxn_fee_base(
uint32_t tx_ptr, uint32_t tx_len // Transaction blob
);
// Returns: Fee in drops for this transaction
// Example
uint8_t tx[256];
// ... build transaction ...
int64_t fee = etxn_fee_base(SBUF(tx));
// fee is the minimum fee for this emission
```
int64_t emit(
uint32_t write_ptr, uint32_t write_len, // Output (hash)
uint32_t read_ptr, uint32_t read_len // Transaction blob
);
// Returns: 32 (hash length) on success, negative on error
// Example workflow:
// 1. Reserve
etxn_reserve(1);
// 2. Build transaction
uint8_t tx[PREPARE_PAYMENT_SIMPLE_SIZE];
PREPARE_PAYMENT_SIMPLE(tx, amount, dest, 0, 0);
// 3. Emit
uint8_t hash[32];
int64_t result = emit(SBUF(hash), SBUF(tx));
if (result != 32) {
rollback(SBUF("Emission failed"), 1);
}
// hash now contains the emitted transaction's hash
```
// Helper macros for building transactions
// Simple XRP payment
PREPARE_PAYMENT_SIMPLE(
buf_out, // uint8_t[] output buffer
drops, // int64_t amount in drops
to_address, // uint8_t[20] destination
dest_tag, // uint32_t destination tag (0 for none)
src_tag // uint32_t source tag (0 for none)
);
// Size of buffer needed
PREPARE_PAYMENT_SIMPLE_SIZE // Use this to size your buffer
// Example
uint8_t tx[PREPARE_PAYMENT_SIMPLE_SIZE];
uint8_t dest[20];
// ... set dest to destination account ...
PREPARE_PAYMENT_SIMPLE(tx, 1000000, dest, 0, 0);
// More complex transactions require manual serialization
// using sto_* functions
```
int64_t util_accid(
uint32_t write_ptr, uint32_t write_len, // Output (20 bytes)
uint32_t read_ptr, uint32_t read_len // r-address string
);
// Converts "rXXX..." to 20-byte account ID
// Example
uint8_t account[20];
char address[] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh";
int64_t len = util_accid(SBUF(account), SBUF(address));
if (len != 20) {
rollback(SBUF("Invalid address"), 1);
}
```
int64_t util_raddr(
uint32_t write_ptr, uint32_t write_len, // Output (35 bytes max)
uint32_t read_ptr, uint32_t read_len // Account ID (20 bytes)
);
// Converts 20-byte account ID to "rXXX..." string
// Example
uint8_t account[20]; // Have account ID
char address[35];
int64_t len = util_raddr(SBUF(address), SBUF(account));
// address now contains "rXXX..." string
```
int64_t util_sha512h(
uint32_t write_ptr, uint32_t write_len, // Output (32 bytes)
uint32_t read_ptr, uint32_t read_len // Data to hash
);
// Computes SHA-512 half (first 32 bytes of SHA-512)
// Example
uint8_t hash[32];
uint8_t data[] = "data to hash";
int64_t len = util_sha512h(SBUF(hash), SBUF(data));
// hash now contains 32-byte hash
```
For high-precision arithmetic:
// Create a float from mantissa and exponent
int64_t float_set(int32_t exponent, int64_t mantissa);
// Multiply two floats
int64_t float_multiply(int64_t a, int64_t b);
// Divide
int64_t float_divide(int64_t a, int64_t b);
// Sum
int64_t float_sum(int64_t a, int64_t b);
// Compare
int64_t float_compare(int64_t a, int64_t b, uint32_t mode);
// Example: Calculate 10% of an amount
int64_t amount = otxn_amount();
int64_t percent = float_set(-1, 1); // 0.1
int64_t result = float_multiply(
float_set(0, amount), // Convert drops to float
percent
);
int64_t ten_percent = float_int(result, 0, 0); // Back to int
╔═══════════════════════════════════════════════════════════════╗
║ HOOKS API QUICK REFERENCE ║
╠═══════════════════════════════════════════════════════════════╣
║ CONTROL ║
║ accept(SBUF(msg), code) Accept transaction ║
║ rollback(SBUF(msg), code) Reject transaction ║
║ trace(SBUF(msg), num) Debug output ║
║ GUARD(n) Loop guard (max iterations) ║
╠═══════════════════════════════════════════════════════════════╣
║ TRANSACTION ║
║ otxn_amount() Get XRP amount (drops) ║
║ otxn_type() Get transaction type ║
║ otxn_field(SBUF(buf), sf) Get field by sfCode ║
║ otxn_id(SBUF(buf), 0) Get transaction hash ║
║ otxn_param(SBUF(buf), SBUF(name)) Get Hook parameter ║
╠═══════════════════════════════════════════════════════════════╣
║ STATE ║
║ state(SBUF(val), SBUF(key)) Read state ║
║ state_set(SBUF(val), SBUF(key)) Write state ║
║ state_set(0, 0, SBUF(key)) Delete state ║
╠═══════════════════════════════════════════════════════════════╣
║ LEDGER ║
║ ledger_seq() Current ledger number ║
║ util_keylet(SBUF(k), type, params...) Generate keylet ║
║ slot_set(SBUF(keylet), n) Load object into slot n ║
║ slot_subfield(slot, sf, 0) Read field from slot ║
╠═══════════════════════════════════════════════════════════════╣
║ EMISSION ║
║ etxn_reserve(n) Reserve n emission slots ║
║ PREPARE_PAYMENT_SIMPLE(buf, amt, dest, dtag, stag) ║
║ emit(SBUF(hash), SBUF(tx)) Emit transaction ║
╠═══════════════════════════════════════════════════════════════╣
║ COMMON sfCodes ║
║ sfAccount, sfDestination, sfAmount, sfFee ║
║ sfMemos, sfTransactionType, sfSequence ║
╚═══════════════════════════════════════════════════════════════╝✅ The API is sufficient for most transaction-level logic. Payment filtering, auto-transfers, state management all work well.
✅ Return values are consistent. Positive = success (often length), negative = error.
✅ Slot system works for ledger access. Can read any account, trust line, or offer.
⚠️ API may evolve. New functions may be added; some behaviors may change.
⚠️ Documentation gaps exist. Some functions less documented than others.
⚠️ Float functions have learning curve. High-precision math requires understanding the encoding.
🔴 Not checking return values. Always check for negative returns indicating errors.
🔴 Wrong buffer sizes. Account IDs are 20 bytes, hashes are 32 bytes, etc.
🔴 Forgetting to reserve emissions. emit() fails without etxn_reserve().
The Hooks API is compact but complete for its purpose. Most common needs are covered. The main challenge is learning the patterns—once you understand slot-based ledger access and the standard function signatures, the API becomes predictable. Keep this lesson as a reference; you'll consult it frequently.
Assignment: Create a Hook that demonstrates use of functions from each API category.
Requirements:
Implement a "Smart Payment Receiver" Hook that:
Uses trace() to log Hook execution
Uses accept() for approved transactions
Uses rollback() with meaningful error messages
Reads transaction amount using otxn_amount()
Reads destination using otxn_field()
Checks transaction type using otxn_type()
Gets transaction ID using otxn_id()
Uses ledger_seq() to get current ledger
Uses slot system to read sender's account balance
Logs sender's XRP balance
Maintains state with key "total_received"
Reads current total on each payment
Updates total with new payment amount
Logs running total
If payment > 100 XRP, emit 1 XRP "thank you" back to sender
Use PREPARE_PAYMENT_SIMPLE
Log emitted transaction hash
Code Structure:
#include "hookapi.h"
int64_t hook(uint32_t reserved) {
_g(1, 1);
// Part 1: Control - trace entry
// Part 2: Transaction - read all fields
// Part 3: Ledger - check sender balance
// Part 4: State - update running total
// Part 5: Emission - thank you payment if large
accept(SBUF("Success"), 0);
return 0;
}
int64_t cbak(uint32_t reserved) {
return 0;
}
- All five API categories demonstrated (50%)
- Correct error handling (20%)
- Proper guard usage (10%)
- Clean code structure (10%)
- Working deployment and test (10%)
Time investment: 4-5 hours
Value: Working reference implementation for all API categories
Knowledge Check
Question 1 of 5What happens after accept() is called?
- xrpl-hooks.readme.io/docs - Complete function documentation
- Hooks Builder examples - Working code samples
- sfcodes.h in hooks-toolkit - All field codes
- XRPL serialization documentation
For Next Lesson:
We'll write our first real Hook from scratch—a simple "accept all" Hook that logs transactions. Time to apply everything we've learned!
End of Lesson 7
Total words: ~4,700
Estimated completion time: 60 minutes reading + 4-5 hours for deliverable
Key Takeaways
Control functions end execution.
accept() and rollback() don't return—Hook stops immediately.
otxn_* functions read the triggering transaction.
Use otxn_field() with sfCodes for structured access.
Slots are temporary containers for ledger objects.
Generate keylet → load into slot → read fields.
State persists across Hook executions.
Use state() and state_set() with 32-byte keys and 256-byte values.
Emissions require reservation.
Call etxn_reserve() before emit(), use macros to build transactions. ---