Appearance
Example: Memo Routing
Transactions can carry a Memos array, and hooks can carry HookParameters. Both let a caller pass instructions to a hook. This example reads the memos off the originating transaction, drills into a memo entry, and branches on its content. It closes with a section on debugging via trace.
Purpose
- Load the originating transaction's
sfMemosarray and iterate it. - Extract
sfMemoDatafrom a memo entry and route behavior on its value. - Show the simpler
otxn_paramalternative for structured key/value input. - Observe hook execution with
trace/trace_num/trace_float.
APIs used
| API | Purpose | Reference |
|---|---|---|
otxn_slot | Load the whole originating transaction into a slot. | transaction |
slot_subfield | Narrow a slot to a subfield (sfMemos, sfMemoData). | Slot APIs |
slot_count | Number of elements in a slotted array. | Slot APIs |
slot_subarray | Expand one array element into its own slot. | Slot APIs |
slot | Copy a slot's bytes into memory. | Slot APIs |
otxn_param | Read a HookParameter by key (the simpler alternative). | transaction |
trace / trace_num / trace_float | Emit debug output. | utility |
accept / rollback | Terminate the hook. | Control APIs |
Helper macros used: SBUF, GUARD, BUFFER_EQUAL_STR_GUARD, TRACEHEX, TRACEVAR — see macros.
Processing flow
- Guard the entry with
_g(1,1). otxn_slot(1)— load the transaction into slot 1.slot_subfield(1, sfMemos, 2)— narrow to theMemosarray in slot 2. If the transaction has no memos this returnsDOESNT_EXIST (-5).slot_count(2)— how many memo entries there are.- For each index
i:slot_subarray(2, i, 3)expands entryiinto slot 3, thenslot_subfield(3, sfMemoData, 4)narrows to that entry'sMemoData, andslot(SBUF(buf), 4)copies the bytes out. - Compare the memo bytes and branch (route).
accept/rollbackaccordingly.
Note: a memo array element is a Memo object, and slot_subfield reads sfMemoData directly from the element slot — the wrapper is handled for you.
Complete code example
This hook looks for a memo whose MemoData is the ASCII string PING and accepts only if it finds one; otherwise it rejects.
c
#include "hookapi.h"
int64_t
hook(uint32_t reserved)
{
_g(1, 1);
// 1. Load the originating transaction, then narrow to its Memos array.
if (otxn_slot(1) != 1)
rollback(SBUF("memo: cannot slot txn"), 1);
if (slot_subfield(1, sfMemos, 2) != 2)
rollback(SBUF("memo: no memos"), 2); // DOESNT_EXIST if absent
int64_t count = slot_count(2);
if (count < 0)
rollback(SBUF("memo: not an array"), 3); // NOT_AN_ARRAY on non-array
// 2. Scan each memo entry for MemoData == "PING".
int matched = 0;
for (int i = 0; GUARD(256), i < count && i < 256; ++i)
{
// Expand entry i into slot 3.
if (slot_subarray(2, i, 3) != 3)
continue;
// Narrow to this entry's MemoData; skip entries that lack it.
if (slot_subfield(3, sfMemoData, 4) != 4)
continue;
uint8_t buf[16];
int64_t len = slot(SBUF(buf), 4);
if (len < 0)
continue;
// Compare the memo bytes to the literal "PING" (4 bytes, no NUL).
int equal = 0;
BUFFER_EQUAL_STR_GUARD(equal, buf, len, "PING", 256);
if (equal)
{
matched = 1;
break;
}
}
if (!matched)
rollback(SBUF("memo: no PING memo"), 4);
accept(SBUF("memo: PING accepted"), 0);
return 0;
}The loop carries a guard (GUARD(256)), the count is bounded, every slot API return is checked, and both outcomes terminate.
The simpler alternative: otxn_param
If you control the caller, HookParameters are easier than memos: they are a direct key/value store on the transaction, read in one call with no slot plumbing.
c
uint8_t val[32];
int64_t len = otxn_param(SBUF(val), "route", 5); // key = "route"
if (len == DOESNT_EXIST)
accept(SBUF("no route param"), 0); // parameter absent
if (len < 0)
rollback(SBUF("param read error"), 1);
// branch on val[0..len]otxn_param returns the value length, or DOESNT_EXIST (-5) if the key is not present. Prefer this over memos for machine-to-hook signaling; reserve memos for data that must ride along with an ordinary payment.
Variations
Route to different actions. Replace the single "PING" check with a switch on the first memo byte (a one-byte opcode), or with several BUFFER_EQUAL_STR_GUARD comparisons, each selecting a branch.
Read MemoType / MemoFormat too. The same pattern applies with sfMemoType and sfMemoFormat — narrow the entry slot to each subfield in turn.
First memo only. If you only care about the first memo, skip slot_count and the loop: slot_subarray(2, 0, 3) then read sfMemoData from slot 3.
Debugging with trace
trace, trace_num, and trace_float write to the server's log at trace log level, each line tagged HookTrace[<account>]:. They are no-ops unless the server is running with trace logging enabled, so they cost nothing when the level is higher.
| Call | Emits |
|---|---|
trace(SBUF("msg"), SBUF(buf), 1) | A message plus buf rendered as hex (as_hex = 1); pass 0 to render as raw text. |
trace_num(SBUF("count"), count) | A message plus a signed integer. |
trace_float(SBUF("amt"), xfl) | A message plus an XFL value, formatted as a number. |
The macro.h wrappers are the ergonomic form and compile out when NDEBUG is set (they are gated on the DEBUG flag):
c
TRACEVAR(count); // trace_num with "count" as the label
TRACEHEX(buf); // trace buf as hex, labelled "buf"
TRACEXFL(amt); // trace_float labelled "amt"
TRACESTR("reached A"); // trace a literal stringWhere to see it. Run xahaud (for example in standalone mode) with the log level set to trace; hook trace lines appear in the server log interleaved with other trace output. In the unit-test harness the same output surfaces when the test's log threshold includes trace. There is no on-ledger record of trace output — it is purely a server-side diagnostic.
Caveats
- Slots are a finite resource (255 max). Expanding many array elements without reusing a slot number exhausts them and later calls return
NO_FREE_SLOTS (-6); reuse a fixed destination slot inside the loop, as the example does with slot 3. slot_subfield(..., sfMemos, ...)returnsDOESNT_EXIST (-5)when the transaction has no memos andslot_countreturnsNOT_AN_ARRAY (-22)on a non-array slot — handle both.- Memo bytes are raw: what looks like text may be arbitrary binary. Compare with a length-checked helper (
BUFFER_EQUAL_STR_GUARD), not a NUL-terminated string function.
Common mistakes
- Reading
sfMemoDataoff the array slot instead of an element slot. You mustslot_subarrayto an element first, thenslot_subfield(sfMemoData)on that element. Applyingslot_subfieldto the array slot does not work. - Looping without a guard. Iterating memo entries with an unguarded
forfails validation (GUARD_MISSING) or tripsGUARD_VIOLATION (-16)at runtime. PutGUARD(n)in the loop and bound the index. - Assuming memos exist. Not every transaction carries memos; branch on the
DOESNT_EXISTreturn fromslot_subfield(sfMemos)rather than proceeding. - Leaving heavy
tracecalls in a production hook. Trace output is a debugging aid; while it is a no-op when trace logging is off, dead trace code still adds WASM size and instructions. Strip it or rely on theDEBUG-gated macros for release builds. - Confusing memos with parameters.
sfMemosis read via slots; HookParameters are read withotxn_param. They are different mechanisms — pick one per field.