Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Keylets

A keylet is a 34-byte locator for a ledger object: a type prefix plus the 32-byte hash-derived index the protocol uses to find that object in the ledger’s state map. Almost every ledger read that isn’t the originating transaction itself starts by computing a keylet, then loading the object it points at into a slot. This page covers rshooks’s 26 typed keylet_xxx helpers, a worked example that computes and stores them, and account_id!, the companion macro for building compile-time r-address constants.

Why typed helpers, not one untyped function

The host exposes a single util_keylet function that takes a keylet_type plus up to six same-typed u32 components (a..f). Which of those six are used, how many, and what each one means — a raw value like a sequence number, or a pointer into the hook’s own linear memory for an account ID or hash — all depend silently on keylet_type. Nothing at the type level stops passing an account pointer where a sequence number was expected, and getting it wrong is either a runtime NO_SUCH_KEYLET/ INVALID_ARGUMENT, or worse, a keylet that silently resolves to the wrong object.

rshooks::api::keylet has one function per KEYLET_* constant instead, each taking exactly the arguments its own type needs as the real rshooks::types newtypes — keylet_account takes only an &AccountId, keylet_line takes two &AccountIds and a &CurrencyCode, keylet_offer takes an &AccountId and a u32 sequence. Every one is a thin #[inline(always)] pass-through to the same underlying host call, so this costs nothing beyond the raw call itself.

The 26 typed helpers

functionKEYLET_*ledger object addressed
keylet_hook(account)KEYLET_HOOK (1)account’s installed hook chain
keylet_hook_state(account, key, namespace)KEYLET_HOOK_STATE (2)one hook-state entry
keylet_account(account)KEYLET_ACCOUNT (3)account’s AccountRoot
keylet_amendments()KEYLET_AMENDMENTS (4)the ledger’s singleton Amendments object
keylet_child(parent)KEYLET_CHILD (5)a derived pseudo-account keyed one level below parent
keylet_skip(ledger_index)KEYLET_SKIP (6)a SkipList object (current, or as of a historical ledger)
keylet_fees()KEYLET_FEES (7)the ledger’s singleton FeeSettings
keylet_negative_unl()KEYLET_NEGATIVE_UNL (8)the ledger’s singleton NegativeUNL
keylet_line(a, b, currency)KEYLET_LINE (9)the trust line (RippleState) between two accounts
keylet_offer(account, seq)KEYLET_OFFER (10)account’s Offer created at sequence seq
keylet_quality(dir, high, low)KEYLET_QUALITY (11)the order-book directory page at a given exchange rate
keylet_emitted_dir()KEYLET_EMITTED_DIR (12)the singleton directory of outstanding emitted transactions
keylet_ticket(account, seq)KEYLET_TICKET (13)account’s Ticket at seq — see the note below
keylet_signers(account)KEYLET_SIGNERS (14)account’s SignerList
keylet_check(account, seq)KEYLET_CHECK (15)account’s Check created at sequence seq
keylet_deposit_preauth(owner, authorized)KEYLET_DEPOSIT_PREAUTH (16)a recorded deposit preauthorization
keylet_unchecked(hash)KEYLET_UNCHECKED (17)hash reinterpreted directly as a keylet index, unvalidated
keylet_owner_dir(account)KEYLET_OWNER_DIR (18)account’s owner directory root
keylet_page(root, high, low)KEYLET_PAGE (19)directory page high/low under directory root
keylet_escrow(account, seq)KEYLET_ESCROW (20)account’s Escrow created at sequence seq
keylet_paychan(src, dst, seq)KEYLET_PAYCHAN (21)the PayChannel from src to dst created at seq
keylet_emitted(hash)KEYLET_EMITTED (22)the EmittedTxn bookkeeping entry for hash
keylet_nft_offer(account, seq)KEYLET_NFT_OFFER (23)account’s NFTokenOffer created at sequence seq
keylet_hook_definition(hash)KEYLET_HOOK_DEFINITION (24)the account-independent HookDefinition for wasm hash hash
keylet_hook_state_dir(account, namespace)KEYLET_HOOK_STATE_DIR (25)the directory of account’s hook-state entries under namespace
keylet_cron(account, start_time)KEYLET_CRON (26)account’s Cron entry firing at start_time

Every function returns Result<Keylet>. keylet_hook addresses the account’s installed hook chain; keylet_hook_definition addresses a single hook’s own account-independent definition — the two are easy to conflate but key different objects. Likewise keylet_owner_dir (an account’s own directory root) is distinct from keylet_page, which addresses one page of any directory once you already have that directory’s root index.

keylet_ticket has a known host limitation on the tested xahaud build: the host’s util_keylet rejects KEYLET_TICKET regardless of ticket_seq, even though the identical account/sequence shape works through the ledger_entry RPC and every structurally similar type (keylet_offer, keylet_escrow, keylet_check, keylet_signers) succeeds. The helper stays in rshooks — it matches the documented argument shape and a future host build may support it — but treat it as untested until your target node confirms otherwise.

A worked example

examples/13_keylets computes 25 of the 26 keylet types (everything but keylet_ticket, for the reason above) from the invoking transaction’s sfAccount/sfDestination plus a handful of fixed test inputs, and writes every 34-byte result into hook state:

let Ok(owner) = otxn_field_typed(sfAccount) else {
    rollback!(b"keylets: sfAccount missing from the originating transaction", ...)
};
let Ok(dest) = otxn_field_typed(sfDestination) else {
    rollback!(b"keylets: sfDestination missing from the originating transaction", ...)
};

let Ok(keylet) = keylet_account(&owner) else {
    rollback!(b"keylets: a keylet_xxx call failed", ...)
};
if state_set(keylet.as_ref(), &KeyletKey::Account.encode()).is_err() {
    rollback!(b"keylets: state_set failed", ...);
}

(condensed from examples/13_keylets/src/lib.rs, which repeats this shape once per keylet type using a small compute/store helper pair). Every keylet here is computed entirely from inputs already available at compile time or read directly off the invoking transaction — no other ledger object has to exist first, so the hook works against a bare node with no setup.

To go from a keylet to the object it addresses, load it into a slot (see Slots and Ledger Objects):

let account = SlotObject::from_keylet(&keylet_account(accid)?)?;
let seq: u32 = account.get(sfSequence)?.value()?;

account_id! for compile-time r-addresses

Several keylet arguments are &AccountId, but a classic Xahau/XRPL r-address (rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh) is base58-encoded text, not the raw 20-byte form the Hook API and keylet_xxx want. account_id! decodes that text entirely at compile time — base58 decode, version-byte check, and double-SHA256 checksum verification all run inside the proc macro at cargo build time, never inside the compiled wasm:

use rshooks::prelude::*;

const OWNER: AccountId = account_id!("rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh");

Because the expansion is a bare AccountId([..]) literal, OWNER works in const/static position and the compiled wasm is byte-identical to hand-writing the 20-byte array yourself — examples/14_account-id-macro’s e2e suite asserts exactly that against a hand-written control. A malformed address (bad checksum, wrong length, wrong version byte) is a compile_error!, not a runtime failure:

// Bad checksum — last character altered.
rshooks::account_id!("rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTH");

Reach for account_id! whenever a keylet argument, a hard-coded genesis account, or any other fixed r-address needs to become an AccountId — it replaces hand-computing or hex-pasting the 20 bytes yourself.