Lesson 13: Ledger State Access - Reading the Blockchain | 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 13: Ledger State Access - Reading the Blockchain

Learning Objectives

Use the slot system to load and query ledger objects

Generate keylets for accounts, trust lines, offers, and other objects

Extract fields from loaded ledger objects

Build verification logic based on ledger state

Understand access limitations and error handling

The triggering transaction tells you what someone is trying to do. Ledger state tells you the context:

TRANSACTION DATA:        LEDGER STATE:
├── Sender               ├── Sender's balance
├── Destination          ├── Sender's trust lines
├── Amount               ├── Sender's offers
├── Type                 ├── Destination's status
└── Memos                ├── Current exchange rates
                         └── Any account's state

Slots are temporary containers for ledger objects:

SLOT WORKFLOW

1. GENERATE KEYLET

1. LOAD INTO SLOT

1. READ FIELDS

1. SLOTS CLEARED
// Slots 0-255 are available
// You choose which slot to use

int64_t slot = slot_set(SBUF(keylet), 1); // Use slot 1
int64_t slot2 = slot_set(SBUF(keylet2), 2); // Use slot 2

// Slots can be reused
slot_set(SBUF(keylet3), 1); // Overwrites slot 1
```

// Load object into slot
int64_t slot_set(
    uint32_t read_ptr, uint32_t read_len,  // Keylet (34 bytes)
    int32_t slot                            // Slot number
);
// Returns: slot number on success, negative on error

// Get size of object in slot
int64_t slot_size(int32_t slot);
// Returns: size in bytes, or negative if slot empty

// Extract field from slot
int64_t slot_subfield(
int32_t parent_slot, // Slot containing object
int32_t field_id, // sfCode of field
int32_t new_slot // Destination slot (0 to return value directly)
);
// Returns: new slot number OR field value, depending on field type

// Read slot contents into buffer
int64_t slot(
uint32_t write_ptr, uint32_t write_len, // Output buffer
int32_t slot // Slot to read
);
// Returns: bytes read
```


A keylet is a 34-byte identifier for a ledger object:

// Keylet structure
// Byte 0-1: Object type identifier
// Byte 2-33: Object-specific key (usually hash)

// All keylets are 34 bytes
uint8_t keylet[34];
int64_t util_keylet(
    uint32_t write_ptr, uint32_t write_len,  // Output (34 bytes)
    uint32_t keylet_type,                     // Type of object
    uint32_t a, uint32_t b,                   // Type-specific parameters
    uint32_t c, uint32_t d,
    uint32_t e, uint32_t f
);

// Parameters vary by keylet type
// Unused parameters should be 0
```

// Get keylet for any account
uint8_t keylet[34];
uint8_t account_id[20] = { /* 20-byte account ID */ };

int64_t result = util_keylet(
SBUF(keylet),
KEYLET_ACCOUNT,
(uint32_t)account_id, 20,
0, 0, 0, 0
);

if (result != 34) {
rollback(SBUF("Could not generate account keylet"), 1);
}

// Load account into slot
int64_t slot = slot_set(SBUF(keylet), 1);
if (slot < 0) {
rollback(SBUF("Account not found"), 2);
}
```

// Get keylet for a trust line between two accounts
uint8_t keylet[34];
uint8_t low_account[20];   // Numerically lower account ID
uint8_t high_account[20];  // Numerically higher account ID
uint8_t currency[20];      // Currency code (padded to 20 bytes)

// Trust line keylets require accounts in specific order
// "Low" and "High" refer to numeric comparison of account IDs

int64_t result = util_keylet(
SBUF(keylet),
KEYLET_LINE,
(uint32_t)low_account, 20,
(uint32_t)high_account, 20,
(uint32_t)currency, 20
);

// Load trust line
int64_t slot = slot_set(SBUF(keylet), 1);
```

// Get keylet for a DEX offer
uint8_t keylet[34];
uint8_t account[20];  // Account that created offer
uint32_t offer_seq;   // Sequence number of OfferCreate

int64_t result = util_keylet(
SBUF(keylet),
KEYLET_OFFER,
(uint32_t)account, 20,
offer_seq, 0, 0, 0
);
```

// Available keylet types:
KEYLET_ACCOUNT      // Account root
KEYLET_LINE         // Trust line
KEYLET_OFFER        // DEX offer
KEYLET_ESCROW       // Escrow
KEYLET_PAYCHAN      // Payment channel
KEYLET_CHECK        // Check
KEYLET_SIGNERS      // Signer list
KEYLET_HOOK         // Hook definition
KEYLET_HOOK_STATE   // Hook state entry
// ... and more

// Get any account's XRP balance

uint8_t account_id[20];
// ... set account_id ...

// Generate keylet
uint8_t keylet[34];
util_keylet(SBUF(keylet), KEYLET_ACCOUNT,
(uint32_t)account_id, 20, 0, 0, 0, 0);

// Load into slot
int64_t slot = slot_set(SBUF(keylet), 1);
if (slot < 0) {
trace(SBUF("Account not found"), 0);
// Account doesn't exist
}

// Get balance field
// sfBalance returns XRP balance directly when new_slot is 0
int64_t balance = slot_subfield(1, sfBalance, 0);
trace(SBUF("Balance (drops):"), balance);
```

// Get account's current sequence number
int64_t sequence = slot_subfield(1, sfSequence, 0);
trace(SBUF("Sequence:"), sequence);
// Get account flags
int64_t flags = slot_subfield(1, sfFlags, 0);

// Check specific flags
if (flags & 0x00010000) { // lsfRequireDestTag
trace(SBUF("Account requires destination tag"), 0);
}

if (flags & 0x00040000) { // lsfDisallowXRP
trace(SBUF("Account disallows XRP"), 0);
}
```

// Get number of owned objects (affects reserve)
int64_t owner_count = slot_subfield(1, sfOwnerCount, 0);
trace(SBUF("Owner count:"), owner_count);

// Calculate approximate reserve
int64_t base_reserve = 10000000; // 10 XRP
int64_t owner_reserve = 2000000; // 2 XRP per owned object
int64_t total_reserve = base_reserve + (owner_count * owner_reserve);
```


// Check trust line between two accounts

uint8_t account_a[20]; // First account
uint8_t account_b[20]; // Second account
uint8_t currency[20] = {0}; // Currency code

// Set up currency (e.g., "USD" -> padded 20 bytes)
// Standard currency codes are 3 characters, zero-padded
currency[12] = 'U';
currency[13] = 'S';
currency[14] = 'D';

// Determine low/high account (numeric comparison)
// For simplicity, try both orders or implement comparison

uint8_t keylet[34];
util_keylet(SBUF(keylet), KEYLET_LINE,
(uint32_t)account_a, 20,
(uint32_t)account_b, 20,
(uint32_t)currency, 20);

int64_t slot = slot_set(SBUF(keylet), 1);
if (slot < 0) {
trace(SBUF("No trust line exists"), 0);
}
```

// Trust line balance is stored differently
// It's relative to the "low" account

// Load balance field into slot 2
int64_t bal_slot = slot_subfield(1, sfBalance, 2);
if (bal_slot < 0) {
trace(SBUF("Could not get balance"), 0);
}

// Read the amount structure (48 bytes for IOU)
uint8_t balance_buf[48];
int64_t len = slot(SBUF(balance_buf), 2);

// Parse the IOU amount (complex encoding)
// Positive = low account is owed
// Negative = high account is owed
```

// Get limit from perspective of one account
// LimitAmount is for the low account
// HighLimit is for the high account

int64_t limit_slot = slot_subfield(1, sfLimitAmount, 3);
// Or sfHighLimit for the other side
```


// Before accepting payment, verify sender can afford it

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

// Get sender
uint8_t sender[20];
otxn_field(SBUF(sender), sfAccount);

// Get payment amount
int64_t amount = otxn_amount();
if (amount <= 0) {
accept(SBUF("Not XRP"), 0);
}

// Load sender's account
uint8_t keylet[34];
util_keylet(SBUF(keylet), KEYLET_ACCOUNT,
(uint32_t)sender, 20, 0, 0, 0, 0);

int64_t slot = slot_set(SBUF(keylet), 1);
if (slot < 0) {
rollback(SBUF("Sender account not found"), 1);
}

int64_t balance = slot_subfield(1, sfBalance, 0);
int64_t owner_count = slot_subfield(1, sfOwnerCount, 0);

// Calculate sender's reserve
int64_t reserve = 10000000 + (owner_count * 2000000);
int64_t available = balance - reserve;

trace(SBUF("Sender balance:"), balance);
trace(SBUF("Sender available:"), available);

// Get transaction fee
uint8_t fee_buf[8];
otxn_field(SBUF(fee_buf), sfFee);
int64_t fee = INT64_FROM_BUF(fee_buf);

// Verify sender has enough
if (available < amount + fee) {
trace(SBUF("Sender cannot afford this payment"), 0);
// Note: Ledger would reject anyway, but we can give better message
}

accept(SBUF("Payment verified"), 0);
return 0;
}
```

// Verify destination can receive XRP

uint8_t dest[20];
otxn_field(SBUF(dest), sfDestination);

uint8_t keylet[34];
util_keylet(SBUF(keylet), KEYLET_ACCOUNT,
(uint32_t)dest, 20, 0, 0, 0, 0);

int64_t slot = slot_set(SBUF(keylet), 1);
if (slot < 0) {
// Account doesn't exist - payment would create it
// Check if enough for reserve
int64_t amount = otxn_amount();
if (amount < 10000000) { // Minimum to create account
rollback(SBUF("Amount too small to create account"), 1);
}
} else {
// Account exists - check flags
int64_t flags = slot_subfield(1, sfFlags, 0);

if (flags & 0x00040000) { // lsfDisallowXRP
rollback(SBUF("Destination disallows XRP"), 2);
}

if (flags & 0x00010000) { // lsfRequireDestTag
// Check if we have destination tag
uint8_t dtag_buf[4];
int64_t dtag_len = otxn_field(SBUF(dtag_buf), sfDestinationTag);
if (dtag_len != 4) {
rollback(SBUF("Destination requires tag"), 3);
}
}
}
```

// Only accept if Hook account balance stays above threshold

#define MIN_BALANCE 50000000 // 50 XRP

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

// Get Hook account
uint8_t hook_acc[20];
hook_account(SBUF(hook_acc));

// Load Hook account
uint8_t keylet[34];
util_keylet(SBUF(keylet), KEYLET_ACCOUNT,
(uint32_t)hook_acc, 20, 0, 0, 0, 0);

slot_set(SBUF(keylet), 1);
int64_t balance = slot_subfield(1, sfBalance, 0);

// Check if outgoing payment
uint8_t sender[20];
otxn_field(SBUF(sender), sfAccount);

int is_outgoing = 1;
for (int i = 0; GUARD(20), i < 20; ++i) {
if (sender[i] != hook_acc[i]) {
is_outgoing = 0;
break;
}
}

if (is_outgoing) {
int64_t amount = otxn_amount();
uint8_t fee_buf[8];
otxn_field(SBUF(fee_buf), sfFee);
int64_t fee = INT64_FROM_BUF(fee_buf);

int64_t after = balance - amount - fee;

if (after < MIN_BALANCE) {
rollback(SBUF("Would drop below minimum"), 1);
}
}

accept(SBUF("OK"), 0);
return 0;
}
```


// Handle missing ledger objects gracefully

int64_t slot = slot_set(SBUF(keylet), 1);

if (slot == DOESNT_EXIST) {
// Object doesn't exist in ledger
// This is normal for:
// - Accounts that haven't been created
// - Trust lines that don't exist
// - Offers that expired or were consumed
trace(SBUF("Object not found"), 0);
}

if (slot == INVALID_ARGUMENT) {
// Keylet was malformed
rollback(SBUF("Invalid keylet"), 1);
}

if (slot < 0) {
// Other error
trace(SBUF("Slot error:"), slot);
}
```

// Handle optional fields

int64_t slot = slot_set(SBUF(keylet), 1);
if (slot < 0) return handle_missing();

// Try to get optional field
int64_t email_hash = slot_subfield(1, sfEmailHash, 0);
if (email_hash < 0) {
// Field not present - this is normal
trace(SBUF("No email hash set"), 0);
} else {
trace(SBUF("Has email hash"), 0);
}
```


Ledger access works. Any public ledger object can be read via keylets and slots.

Balance verification is possible. Can check account balances before making decisions.

The slot system is consistent. Same pattern for all object types.

⚠️ Performance with many slot operations. Heavy ledger access may impact execution.

⚠️ All keylet types documented. Some may have sparse documentation.

🔴 Assuming objects exist. Always check slot_set return value.

🔴 Wrong low/high account order for trust lines. Will generate wrong keylet.

🔴 Trusting stale data. Ledger state is from start of transaction, not real-time.

Ledger state access gives Hooks powerful context awareness. The slot system is consistent but requires understanding keylet generation for each object type. Most use cases only need account balance checks, which are straightforward. Trust lines and offers are more complex but follow the same patterns.


Assignment: Build a Hook that makes decisions based on ledger state.

Requirements:

Implement a "Smart Gate" Hook that:

  • Load sender's account from ledger

  • Check sender's XRP balance

  • Reject if sender has < 100 XRP (too poor)

  • Load destination's account (if exists)

  • Check destination flags

  • Warn (trace) if destination requires tag but none provided

  • Load Hook account's balance

  • For outgoing payments, verify balance stays > 20 XRP

  • Reject if would go below threshold

  • If sender has > 10,000 XRP, log "VIP sender"

  • If sender has > 1,000 XRP, log "Premium sender"

  • Otherwise log "Standard sender"

  • Correct keylet generation (25%)

  • Proper error handling for missing objects (25%)

  • Accurate balance calculations (25%)

  • Clean code with good logging (25%)

Time investment: 3-4 hours
Value: Essential pattern for balance-aware applications


Knowledge Check

Question 1 of 4

What is the size of a keylet in bytes?

  • XRPL ledger object documentation
  • hooks-toolkit keylet definitions
  • xrpl.org/accountroot.html
  • Flag bitmask definitions

For Next Lesson:
We'll build a complete Hook project from scratch, applying all concepts learned so far.


End of Lesson 13

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

Key Takeaways

1

Keylets identify ledger objects.

Generate with util_keylet(), load with slot_set().

2

Slots are temporary containers.

Use slots 1-255, cleared after Hook execution.

3

slot_subfield extracts data.

Pass 0 for new_slot to get value directly.

4

Always check for missing objects.

slot_set returns negative if object doesn't exist.

5

Balance = available + reserve.

Calculate available funds by subtracting reserve. ---