Appearance
Example: Payment Filter
A payment filter is the canonical Hook: it inspects the transaction that touched the account and decides whether to accept (let it apply) or rollback (reject it). This example covers gating on transaction type, matching the counterparty against an allow/block list, and rejecting payments above a threshold for both native XAH and issued (IOU) amounts.
Purpose
- Run only for
Paymenttransactions; pass everything else through. - Reject payments to (or from) a specific account.
- Reject payments whose amount exceeds a limit, handling native XAH and IOU amounts uniformly.
APIs used
| API | Purpose | Reference |
|---|---|---|
otxn_type | Read the originating transaction's type. | transaction |
otxn_field | Read sfAccount / sfDestination / sfAmount off the transaction. | transaction |
otxn_slot | Load the whole originating transaction into a slot. | transaction |
slot_subfield | Narrow a slot to a subfield (sfAmount). | Slot APIs |
slot_float | Read a slotted amount as an XFL value. | Slot APIs |
float_compare | Compare two XFL values. | Float & Amount APIs |
float_set | Build an XFL threshold from exponent + mantissa. | Float & Amount APIs |
hook_account | Read the account the hook is installed on. | control |
accept / rollback | Terminate and apply / reject the transaction. | Control APIs |
Helper macros used: SBUF, GUARD, AMOUNT_TO_DROPS, BUFFER_EQUAL_20, ACCOUNT_COMPARE — see Helper Macros.
Processing flow
- Guard the entry with
_g(1,1)(required once at the top ofhook). - Call
otxn_type(). If it is notttPAYMENT(value0),acceptimmediately — the hook has nothing to say about non-Payments. - Read
sfDestination(orsfAccountfor the sender) withotxn_field. A raw AccountID is 20 bytes, so a successful read returns20. - Compare the 20-byte AccountID against your blocklist with
BUFFER_EQUAL_20. On a match,rollback. - Read the amount. Two equivalent options:
- Native only:
otxn_field(SBUF(buf), sfAmount); a native amount is 8 bytes, so a return of8means XAH. Convert withAMOUNT_TO_DROPS. - Native and IOU: load the amount into a slot and read it as XFL with
slot_float, then compare withfloat_compare. This path is uniform across XAH and issued currencies.
- Native only:
acceptif the payment is within limits, otherwiserollback.
Complete code example
This hook rejects Payments to one blocked account and rejects any Payment worth more than 100 (XAH or any IOU) by reading the amount as XFL.
c
#include "hookapi.h"
// The account payments may NOT be sent to (20-byte raw AccountID).
// Fill in with a real AccountID; zeros here are a placeholder.
uint8_t const BLOCKED[20] = {
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};
int64_t
hook(uint32_t reserved)
{
_g(1, 1); // guard: this hook body runs at most once
// 1. Only filter Payment transactions; pass everything else through.
if (otxn_type() != ttPAYMENT)
accept(SBUF("payment-filter: not a payment"), 0);
// 2. Read the destination AccountID (raw 20 bytes).
uint8_t dest[20];
if (otxn_field(SBUF(dest), sfDestination) != 20)
rollback(SBUF("payment-filter: no destination"), 1);
// 3. Reject payments to the blocked account.
if (BUFFER_EQUAL_20(dest, BLOCKED))
rollback(SBUF("payment-filter: destination blocked"), 2);
// 4. Read the payment amount as an XFL via a slot.
// otxn_slot loads the whole txn; slot_subfield narrows to sfAmount;
// slot_float interprets it as XFL (works for XAH and IOU).
if (otxn_slot(1) != 1)
rollback(SBUF("payment-filter: cannot slot txn"), 3);
if (slot_subfield(1, sfAmount, 2) != 2)
rollback(SBUF("payment-filter: no amount"), 4);
int64_t amt = slot_float(2);
if (amt < 0)
rollback(SBUF("payment-filter: bad amount"), 5);
// 5. Build the threshold 100.0 as XFL and compare.
// float_set(exponent, mantissa): 100 = 1 * 10^2.
int64_t limit = float_set(2, 1);
if (limit < 0)
rollback(SBUF("payment-filter: bad limit"), 6);
// float_compare returns 1 (true) when the mode holds.
if (float_compare(amt, limit, COMPARE_GREATER) == 1)
rollback(SBUF("payment-filter: amount over limit"), 7);
accept(SBUF("payment-filter: ok"), 0);
// Unreachable, but every path must terminate.
return 0;
}Every API return value is checked, the single implicit "loop" (the hook body) carries a guard, and every path ends in accept or rollback.
Variations
Native-only threshold with AMOUNT_TO_DROPS. If your account only ever receives XAH, you can skip the slot machinery:
c
uint8_t amt[48]; // large enough for XAH or IOU
int64_t len = otxn_field(SBUF(amt), sfAmount);
if (len == 8) // 8 bytes => native XAH
{
int64_t drops = AMOUNT_TO_DROPS(amt); // -2 if the buffer is not native
if (drops < 0)
rollback(SBUF("bad native amount"), 1);
if (drops > 100000000) // 100 XAH = 100_000_000 drops
rollback(SBUF("over 100 XAH"), 2);
}
else
{
// len == 48 => IOU; handle or reject as policy requires
rollback(SBUF("IOU not accepted"), 3);
}otxn_field(sfAmount) returns 8 for a native amount and 48 for an IOU amount (the serialized value only — no leading field code). This size test is a reliable native/IOU discriminator.
Gate on the sender instead of the destination. Read sfAccount rather than sfDestination; the sender's AccountID is also 20 bytes.
Ordered comparison with ACCOUNT_COMPARE. When you need a sorted allowlist or a range, ACCOUNT_COMPARE(result, a, b) sets result to -1, 0, or 1.
Allowlist (invert the logic). Loop over an array of permitted AccountIDs and rollback if none matched. Guard the loop: for (int i = 0; GUARD(N), i < N; ++i).
Caveats
- A Strong hook running before the transaction applies can
rollbackto veto it; the sender's fee is still burned. Only a Strong hook installed on an account in the transaction's TSH set is invoked for it — install the hook on the account whose payments you want to police. slot_floaton a slot that is not an amount returnsNOT_AN_AMOUNT (-32); always check for a negative return before comparing.- The
accept/rollbackmessage and code are recorded in the transaction metadata (sfHookExecution→sfHookReturnString/sfHookReturnCode), so use distinct codes to make failures diagnosable.
Common mistakes
- Comparing an amount buffer with
==ormemcmp. Serialized amounts encode currency and issuer, so two economically-equal IOU amounts can differ byte for byte. Convert to XFL and usefloat_compare. - Assuming
sfAmountis always 8 bytes. It is 8 for XAH but 48 for IOU. Sizing the buffer at 8 and reading an IOU truncates or returnsTOO_SMALL (-4). - Using
AMOUNT_TO_DROPSon an IOU buffer. The macro returns-2when the amount is not native (its high bit is set); treat a negative result as "not XAH". - Forgetting to gate on
otxn_type. Without thettPAYMENTcheck the hook runs for every transaction type that reaches the account (offers, trustlines,SetHookitself), andotxn_field(sfDestination)will returnDOESNT_EXIST (-5)on transactions that have no destination. - Treating
float_comparelike a C comparison. It returns1/0(or a negative error), not the sign of a subtraction. Check== 1for "true". - Omitting the leading
_g(1,1). A hook with no guard fails validation atSetHooktime (GUARD_MISSING), so it never installs.