Skip to content

State APIs

This page documents the four state Hook APIs: the functions that read and write a hook's persistent key/value storage, both on the hook's own account and — with an explicit grant — on other accounts.

All function signatures on this page match the Hook API.

Return codes reference the shared error table in ../../glossary, and the values quoted below match the Hook API's error definitions.

The implementation uses WASM-facing wrappers and a state cache.


What Hook State is

Hook State is a persistent key/value store attached to an account. Each stored entry is addressed by a triple:

  • Account — whose ledger the entry lives on. For state/state_set this is always the account the hook is installed on (hook_account). For the _foreign variants you name the account explicitly.
  • Namespace — a 32-byte (uint256) tag that partitions an account's state. Each hook runs with a current namespace (sfHookNamespace). state/state_set and the _foreign variants with a zero-length namespace use that current namespace; otherwise you pass a 32-byte namespace.
  • Key — a 32-byte key. Keys shorter than 32 bytes are accepted and zero-padded on the left to 32 bytes: the key "key" is stored as 29 zero bytes followed by the three ASCII bytes. A key must be 1..32 bytes; 0 bytes returns TOO_SMALL and more than 32 returns TOO_BIG.

Each entry is a HookState ledger object (ltHOOK_STATE) identified by its account, key, and namespace.

Value size

A state value may be up to the scaled state-data limit.

  • The base limit is 256 bytes.
  • An account may raise it with sfHookStateScale (1..16); the limit is 256 * scale, i.e. up to 4096 bytes at scale 16. Accounts without sfHookStateScale use scale 1 (256 bytes).

Writing more than the limit returns TOO_BIG. The scale is read from the account object at write time.

State entries cost reserve

Creating a new state entry consumes owner reserve. The first time an account is touched in a hook execution, the implementation computes how many reserve increments the account can still afford:

availableForReserves = (balance - accountReserve(ownerCount)) / feeIncrement

Each new entry consumes scale reserve positions. If the account cannot afford the next entry the write fails with RESERVE_INSUFFICIENT (-38). Overwriting an existing entry does not consume additional reserve. Deleting an entry (see state_set below) frees it.

Namespaces

An account may hold at most 256 namespaces.

Creating a state entry in a brand-new namespace counts against this limit and returns TOO_MANY_NAMESPACES (-45) if exceeded. The new-namespace accounting is gated behind the fixXahauV1 amendment.

Two distinct "too many modifications" limits

There are two separate limits on how much state a single transaction may change, and they are enforced at different points:

  1. Per-execution cache limit — 256. A write is rejected with TOO_MANY_STATE_MODIFICATIONS (-44) once 256 entries have been modified within the current hook's state cache.
  2. Combined-chain write limit. When state is finally flushed to the ledger, modified entries are counted across the combined hook chains, and the transaction returns tecHOOK_REJECTED if the count exceeds the 256-entry limit. The TOO_MANY_STATE_MODIFICATIONS (-44) error is raised by this same 256-entry limit. Treat 256 as the operative figure for both mechanisms above.

Index

FunctionPurpose
stateRead state under the hook account's current namespace.
state_setWrite (or delete) state under the hook account's current namespace.
state_foreignRead state from a specified account and namespace.
state_foreign_setWrite state on another account's namespace (requires a grant).