Lesson 8: Your First Hook - Accept All Transactions | 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
beginner55 min

Lesson 8: Your First Hook - Accept All Transactions

Learning Objectives

Write a minimal Hook that compiles and deploys successfully

Understand every component of the basic Hook structure

Add trace logging to observe Hook execution

Extract and display transaction information

Deploy and test your Hook on testnet

You've learned the philosophy, architecture, C fundamentals, and API. Now we write code. This lesson is practical—follow along in Hooks Builder. Type the code yourself rather than copying; you'll learn more.

  1. Minimal Hook (just accept)
  2. Logging Hook (add trace)
  3. Informative Hook (extract transaction data)
  4. Complete first Hook (production-ready structure)

The smallest Hook that compiles and works:

#include "hookapi.h"

int64_t hook(uint32_t reserved) {
    accept(SBUF("OK"), 0);
    return 0;
}

int64_t cbak(uint32_t reserved) {
    return 0;
}

Let's understand each line:

  • Includes the Hooks API definitions

  • Provides all functions (accept, rollback, etc.)

  • Provides macros (SBUF, GUARD, etc.)

  • Provides sfCodes (sfAccount, sfAmount, etc.)

  • Function signature for main Hook entry point

  • int64_t return type (64-bit integer)

  • reserved parameter (currently unused, for future)

  • This function runs when transactions affect your account

  • Accept the transaction

  • SBUF("OK") expands to pointer and length of "OK"

  • 0 is the return code

  • Execution stops here—nothing after runs

  • Return value (won't be reached after accept)

  • Convention: return 0 for success

  • Callback function (runs if emitted transactions fail)

  • Required even if empty

  • We just return 0 (nothing to do)

Exercise: Deploy the Minimal Hook

  1. Open Hooks Builder (hooks.xrpl.org)
  2. Create or select a funded testnet account
  3. Clear the editor and type the minimal Hook:
#include "hookapi.h"

int64_t hook(uint32_t reserved) {
    accept(SBUF("OK"), 0);
    return 0;
}

int64_t cbak(uint32_t reserved) {
    return 0;
}
  1. Click "Compile"
  2. Verify "Compilation successful" message
  3. Click "Deploy"
  4. Wait for transaction confirmation

Send a payment to your Hook account:

  1. Create or use a second testnet account

  2. Send 10 XRP to your Hook account

  3. Transaction should succeed

  4. Check transaction in explorer—should show Hook execution

  5. Payment arrives at your account

  6. Hook fires (hook() function runs)

  7. accept() is called

  8. Transaction proceeds to ledger

  9. Your account receives the XRP


Let's see what's happening:

#include "hookapi.h"

int64_t hook(uint32_t reserved) {
    // Log that we're executing
    trace(SBUF("Hook executing!"), 0);

// Accept the transaction
    accept(SBUF("Transaction accepted"), 0);

return 0;
}

int64_t cbak(uint32_t reserved) {
    return 0;
}

New line explained:

  • trace() outputs debug information
  • First two params: message (via SBUF)
  • Third param: a number to display (0 here)
  • Output appears in Hooks Builder console
  1. Replace your code with the logging version
  2. Compile and deploy
  3. Send another payment
  4. Check the Output tab in Hooks Builder
  5. You should see "Hook executing!" in the logs

The minimal Hook technically works, but it's missing the initial guard. Let's add it properly:

#include "hookapi.h"

int64_t hook(uint32_t reserved) {
_g(1, 1); // Guard for main function body

trace(SBUF("Hook executing!"), 0);
accept(SBUF("Transaction accepted"), 0);

return 0;
}

int64_t cbak(uint32_t reserved) {
return 0;
}
```

  • First param: guard ID (1)
  • Second param: max iterations (1)
  • This tells the system: "This code path runs at most once"
  • Good practice even in simple Hooks

Let's display the payment amount:

#include "hookapi.h"

int64_t hook(uint32_t reserved) {
    _g(1, 1);

trace(SBUF("Hook executing!"), 0);

// Get the payment amount (in drops)
    int64_t amount_drops = otxn_amount();

trace(SBUF("Amount in drops:"), amount_drops);

// Convert to XRP for display
    int64_t amount_xrp = amount_drops / 1000000;
    trace(SBUF("Amount in XRP:"), amount_xrp);

accept(SBUF("Transaction accepted"), 0);

return 0;
}

int64_t cbak(uint32_t reserved) {
    return 0;
}

New concepts:

  • Gets the XRP amount from the transaction

  • Returns value in drops (1 XRP = 1,000,000 drops)

  • Returns negative for IOU payments

  • Simple integer division to get XRP

  • Note: loses fractional XRP (integer division)

  • For display only—keep drops for calculations

Let's also log who sent the payment:

#include "hookapi.h"

int64_t hook(uint32_t reserved) {
    _g(1, 1);

trace(SBUF("Hook executing!"), 0);

// Get amount
    int64_t amount_drops = otxn_amount();
    trace(SBUF("Amount in drops:"), amount_drops);

// Get sender account ID
    uint8_t sender[20];
    int64_t sender_len = otxn_field(SBUF(sender), sfAccount);

if (sender_len == 20) {
        trace(SBUF("Sender found, first byte:"), sender[0]);
    } else {
        trace(SBUF("Could not get sender"), sender_len);
    }

accept(SBUF("Transaction accepted"), 0);
    return 0;
}

int64_t cbak(uint32_t reserved) {
    return 0;
}

New concepts:

  • Buffer to hold account ID

  • Account IDs are always 20 bytes

  • otxn_field() extracts a field from the transaction

  • sfAccount is the source account

  • Returns number of bytes written (should be 20)

  • Always check return values!

  • 20 means we got the account ID successfully

Different transactions trigger Hooks. Let's identify them:

#include "hookapi.h"

int64_t hook(uint32_t reserved) {
    _g(1, 1);

trace(SBUF("Hook executing!"), 0);

// Get transaction type
    int64_t type = otxn_type();
    trace(SBUF("Transaction type:"), type);

// Get amount (only meaningful for payments)
    int64_t amount = otxn_amount();
    trace(SBUF("Amount:"), amount);

accept(SBUF("Transaction accepted"), 0);
    return 0;
}

int64_t cbak(uint32_t reserved) {
    return 0;
}
  • 0 = Payment
  • 3 = AccountSet
  • 7 = OfferCreate
  • 20 = TrustSet
  • 22 = SetHook

Here's a well-structured first Hook with all the basics:

/**
 * First Hook - Accept All Transactions
 * 
 * This Hook accepts all transactions while logging useful information.
 * It demonstrates:
 * - Basic Hook structure
 * - Transaction data extraction
 * - Proper error handling
 * - Debug logging
 */

#include "hookapi.h"

// Transaction type constants (for readability)
#define TT_PAYMENT 0
#define TT_ACCOUNT_SET 3
#define TT_TRUST_SET 20

int64_t hook(uint32_t reserved) {
    // Main function guard
    _g(1, 1);

// ============================================
    // SECTION 1: Log entry
    // ============================================
    trace(SBUF("=== Hook Execution Start ==="), 0);

// ============================================
    // SECTION 2: Get transaction type
    // ============================================
    int64_t txn_type = otxn_type();
    trace(SBUF("Transaction type:"), txn_type);

// ============================================
    // SECTION 3: Get sender account
    // ============================================
    uint8_t sender_accid[20];
    int64_t sender_len = otxn_field(SBUF(sender_accid), sfAccount);

if (sender_len != 20) {
        trace(SBUF("Warning: Could not get sender"), sender_len);
    } else {
        trace(SBUF("Sender first byte:"), sender_accid[0]);
    }

// ============================================
    // SECTION 4: Get amount (if payment)
    // ============================================
    if (txn_type == TT_PAYMENT) {
        int64_t amount = otxn_amount();

if (amount < 0) {
            trace(SBUF("IOU payment (not XRP)"), 0);
        } else {
            trace(SBUF("XRP amount (drops):"), amount);

// Calculate XRP (integer part only)
            int64_t xrp = amount / 1000000;
            trace(SBUF("XRP amount (whole):"), xrp);
        }
    }

// ============================================
    // SECTION 5: Get transaction ID
    // ============================================
    uint8_t txn_id[32];
    int64_t id_len = otxn_id(SBUF(txn_id), 0);

if (id_len == 32) {
        trace(SBUF("Transaction ID first byte:"), txn_id[0]);
    }

// ============================================
    // SECTION 6: Accept the transaction
    // ============================================
    trace(SBUF("=== Hook Execution End ==="), 0);

accept(SBUF("Transaction processed successfully"), 0);
    return 0;
}

int64_t cbak(uint32_t reserved) {
    // Callback for emission failures
    // Nothing to do in this simple Hook
    return 0;
}
  • Always document what your Hook does
  • List what it demonstrates
  • Future you will thank present you
  • Define constants instead of magic numbers
  • Makes code self-documenting
  • Easier to maintain
  • Clear visual separation
  • Easy to navigate
  • Good for debugging
  • Don't assume all transactions are payments
  • Handle different types appropriately

Test cases to verify:

  1. XRP Payment:

  2. Different Amount:

  3. From Different Account:

  4. Non-Payment Transaction:


  • Missing `#include "hookapi.h"`
  • Missing semicolon somewhere
  • Check the line before the error
  • Missing GUARD() in a loop
  • Or missing _g() at function start (some versions)
  • Hook binary invalid
  • Re-compile and check for warnings
  • Account has master key disabled
  • Use a different account
  • Check you're sending TO the Hook account
  • Check the Hook was deployed successfully
  • Look at account info in explorer
  • Trace output appears in Hooks Builder console
  • Not visible in regular explorer
  • Make sure you're testing from Hooks Builder
  • Check trace output for what happened
  • Look for rollback messages
  • Verify Hook compiled latest version

Now that you have a working Hook, try these extensions:

Add code to log the destination account:

// Get destination account (for payments)
if (txn_type == TT_PAYMENT) {
    uint8_t dest_accid[20];
    int64_t dest_len = otxn_field(SBUF(dest_accid), sfDestination);

if (dest_len == 20) {
        trace(SBUF("Destination first byte:"), dest_accid[0]);
    }
}

Log the current ledger sequence:

// Get current ledger
int64_t ledger = ledger_seq();
trace(SBUF("Current ledger:"), ledger);

Check if someone is sending to themselves:

// Check for self-payment
if (txn_type == TT_PAYMENT && sender_len == 20 && dest_len == 20) {
    int same = 1;
    for (int i = 0; GUARD(20), i < 20; ++i) {
        if (sender_accid[i] != dest_accid[i]) {
            same = 0;
            break;
        }
    }

if (same) {
        trace(SBUF("Self-payment detected!"), 0);
    }
}

Minimal Hooks work. Even the simplest Hook compiles, deploys, and executes.

Trace logging enables understanding. You can see exactly what your Hook does.

The API works as documented. otxn_amount(), otxn_field(), etc. behave predictably.

⚠️ Performance of trace() in production. Consider removing trace calls for efficiency.

⚠️ Behavior across all transaction types. We've mainly tested payments.

🔴 Deploying without testing. Always test on testnet first.

🔴 Assuming successful deployment. Verify Hook appears in account info.

🔴 Ignoring return values. Check that field extractions succeed.

Your first Hook is working. It doesn't do anything useful yet—just accepts and logs—but it's a foundation. Every complex Hook starts here: accept, log, extract data. Master this pattern and you can build anything the API supports.


Assignment: Create an enhanced version of the first Hook with additional features.

Requirements:

Your Hook must:

  • Accept all transactions

  • Log transaction type

  • Log amount (for payments)

  • Log sender account ID (all 20 bytes as separate trace calls for first 3 bytes)

  • Log destination account ID (if payment)

  • Log current ledger sequence

  • Log transaction ID (first 4 bytes)

  • Detect if transaction is XRP vs IOU

  • Detect self-payments

  • Log different messages for different transaction types (at least 3 types)

  • Clear comments explaining each section

  • Defined constants for transaction types

  • Proper error handling for all field extractions

  • Clean, readable structure

  • Complete Hook source code

  • Screenshot of successful compilation

  • Screenshot of deployment transaction

  • Screenshots showing trace output for at least 3 different test transactions

  • Functionality completeness (40%)

  • Code quality and structure (25%)

  • Error handling (15%)

  • Testing evidence (20%)

Time investment: 2-3 hours
Value: Working foundation for all future Hook development


Knowledge Check

Question 1 of 4

What happens if you forget to call accept() or rollback() in your Hook?

  • Hooks Builder example library
  • GitHub: XRPL-Labs/xrpl-hooks examples
  • XRPL documentation on transaction types
  • sfcodes.h in hooks-toolkit

For Next Lesson:
Now that you can accept everything, it's time to reject some things. Lesson 9 covers guards and loops—essential for conditional logic and iteration.


End of Lesson 8

Total words: ~4,200
Estimated completion time: 55 minutes reading + 2-3 hours for deliverable

Key Takeaways

1

Start minimal, add incrementally.

Get something working, then add features.

2

Use trace() liberally during development.

Remove or reduce for production.

3

Check all return values.

Field extraction can fail; handle gracefully.

4

Structure your code clearly.

Comments, sections, and constants improve maintainability.

5

Test after every change.

Recompile, redeploy, and verify behavior. ---