Lesson 7: The Hooks API - Core Functions | Hooks & Smart Contracts | XRP Academy - XRP Academy
3 free lessons remaining this month

Free preview access resets monthly

Upgrade for Unlimited
Skip to main content
beginner60 min

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 5

What 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

1

Control functions end execution.

accept() and rollback() don't return—Hook stops immediately.

2

otxn_* functions read the triggering transaction.

Use otxn_field() with sfCodes for structured access.

3

Slots are temporary containers for ledger objects.

Generate keylet → load into slot → read fields.

4

State persists across Hook executions.

Use state() and state_set() with 32-byte keys and 256-byte values.

5

Emissions require reservation.

Call etxn_reserve() before emit(), use macros to build transactions. ---