Learning Xahau: HookOnV2, NamedHooks, and Transaction Simulation. More Control Over When and How Hooks Fire.
HookOnV2: Directional Hook Execution
The Problem with the Old HookOn
Every hook has a HookOn field: a 64-character hex string where each bit corresponds to a Xahau transaction type. A bit set to 1 means the hook does not fire for that type; a bit set to 0 means it does. The logic is inverted.
The problem: HookOn applied equally to all transactions, regardless of direction (incoming or outgoing). If you installed a hook that fires on Payment transactions, it would fire when the account received a payment AND when it sent one. That meant you often had to add direction-checking logic inside the hook itself, reading otxn_field(sfDestination) and comparing it to hook_account(), just to figure out which way the payment was flowing. This is boilerplate you shouldn't have to write.
What HookOnV2 Adds
The amendment introduces two new optional fields on the Hook object inside SetHook:
| Field | Description |
|---|---|
HookOnIncoming |
same 64-char hex format as HookOn, controlling which transaction types fire the hook when the account is the destination |
HookOnOutgoing |
same 64-char hex format as HookOn, controlling which transaction types fire the hook when the account is the source |
Important: HookOnIncoming / HookOnOutgoing and HookOn are mutually exclusive. If you include HookOnIncoming or HookOnOutgoing in a SetHook, you must not include HookOn in the same Hook object - the transaction will be rejected. Use one approach or the other: either the original single bitmask (HookOn) or the directional pair (HookOnIncoming + HookOnOutgoing).
Bitmask Format
HookOnIncoming and HookOnOutgoing use exactly the same (64 hex character) format as the original HookOn field.
// โ wrong - 16 chars, rejected
'FFFFFFFFFBFFFFE'
// โ correct - 64 chars, fires only on Payment
'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBFFFFE'
// โ correct - 64 chars, never fires (fully disabled)
'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'
// โ correct - 64 chars, fires on every transaction type
'0000000000000000000000000000000000000000000000000000000000000000'
You can compute the correct mask for any combination of transaction types using the HookOn Calculator tool.
Installing a Directional Hook (07-hookonv2-directional.js)
require('dotenv').config()
const { Client, Wallet } = require('xahau')
// HookOnV2 bitmask constants.
// Each bit position corresponds to a transaction type; setting a bit to 1
// means the hook does NOT fire for that transaction type (inverted logic).
// These 8-byte hex values are the compact form used with HookOnIncoming /
// HookOnOutgoing (introduced in the HookOnV2 amendment).
// HookOnIncoming/HookOnOutgoing use the same 32-byte (64 hex char) format as HookOn.
const HOOK_ON_PAYMENT_ONLY = 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBFFFFE' // fires only on Payment
const HOOK_ON_DISABLED = 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' // never fires
// Hash of an already-deployed hook on testnet.
// Replace with your own hook hash after uploading a compiled WASM.
const MY_HOOK_HASH = '66A4FC969ADB5998FD371B7B011F1BC3E506D2171F4729B52E57A6A8BC093227'
async function installDirectionalHook() {
const client = new Client('wss://xahau-test.net')
await client.connect()
// Set HUB_SEED in your .env file. The signing account becomes the hook account.
// Get funds at https://xahau-test.net/
const wallet = Wallet.fromSeed(process.env.HUB_SEED, { algorithm: 'secp256k1' })
console.log(`Account: ${wallet.address}`)
// SetHook installs or updates the hook on the account.
// HookOnIncoming/HookOnOutgoing are separate bitmasks for incoming and
// outgoing transactions - a key feature of the HookOnV2 amendment.
// Here: fire only on incoming Payments, never on outgoing transactions.
const setHookTx = await client.autofill({
TransactionType: 'SetHook',
Account: wallet.address,
Hooks: [
{
Hook: {
HookHash: MY_HOOK_HASH,
HookNamespace: '0'.repeat(64), // 32-byte namespace, all zeros
HookFlags: 1, // hsfOVERRIDE: allow replacing existing hook
HookOnIncoming: HOOK_ON_PAYMENT_ONLY, // fires on incoming Payments
HookOnOutgoing: HOOK_ON_DISABLED, // silent on outgoing transactions
}
}
]
})
const { tx_blob } = wallet.sign(setHookTx)
const result = await client.submitAndWait(tx_blob)
console.log(`SetHook: ${result.result.meta.TransactionResult}`)
console.log(`Explorer: https://xaman.app/explorer/21338/${result.result.hash}`)
await client.disconnect()
}
installDirectionalHook().catch(console.error)
After running this, you will get an outcome like this:
Account: rLPaiK8i1a62NvRXXMJPBZqqAAcXN5CZjQ
SetHook: tesSUCCESS
Explorer: https://xaman.app/explorer/21338/57849CBA3A92A91BF689EB50D3ED8CB847242C9B49B6E415ACD0AAC502E00375
HookHash references a hook that has already been compiled and uploaded to the ledger by its original deployer. You can also supply CreateCode (the compiled WASM bytes as hex) to upload and install in one step.
The key difference from a pre-HookOnV2 install is the split into HookOnIncoming and HookOnOutgoing. The hook's C code does not need to change at all. The ledger enforces the direction filter before the hook even runs.
Updating the Bitmasks In Place (08-hookonv2-update.js)
Once a directional hook is installed, you rarely need to touch HookHash or HookNamespace again - most changes are just adjusting which transaction types fire the hook in each direction. Omitting HookHash / CreateCode from the Hook object turns the SetHook into an update-in-place: only the fields you include are changed, everything else about the hook stays exactly as installed.
require('dotenv').config()
const { Client, Wallet } = require('xahau')
// Index of the hook slot whose directional bitmasks you want to update.
// Slot 0 = first hook (as installed in 07-hookonv2-directional.js).
const SLOT_INDEX = 0
// New bitmask values - 256-bit (64 hex chars), same format as HookOn.
// This example flips the direction compared to 07-hookonv2-directional.js:
// incoming โ fires only on Invoke
// outgoing โ fires only on Payment
// Use encode_hook_on at https://xahau.network/docs/tools/ to compute your own values.
const NEW_HOOK_ON_INCOMING = 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFFFFFFFFFFFFFFBFFFFF' // Invoke only
const NEW_HOOK_ON_OUTGOING = 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBFFFFE' // Payment only
// Updating HookOnIncoming/HookOnOutgoing does NOT require re-uploading the WASM.
// A SetHook with only those fields changes the firing conditions in-place;
// the hook code and namespace remain exactly as installed.
async function updateDirectionalHook() {
const client = new Client('wss://xahau-test.net')
await client.connect()
// Must be the account that owns the hook (HUB_SEED in .env).
const wallet = Wallet.fromSeed(process.env.HUB_SEED, { algorithm: 'secp256k1' })
console.log(`Account: ${wallet.address}`)
console.log(`Slot: ${SLOT_INDEX}`)
console.log(`New HookOnIncoming: ${NEW_HOOK_ON_INCOMING}`)
console.log(`New HookOnOutgoing: ${NEW_HOOK_ON_OUTGOING}`)
// Pad earlier slots with empty Hook objects (no-op) so only SLOT_INDEX is touched.
// You cannot use HookOn together with HookOnIncoming/HookOnOutgoing - they are mutually exclusive.
const hooks = Array(SLOT_INDEX).fill({ Hook: {} })
hooks.push({
Hook: {
HookOnIncoming: NEW_HOOK_ON_INCOMING,
HookOnOutgoing: NEW_HOOK_ON_OUTGOING,
}
})
const updateTx = await client.autofill({
TransactionType: 'SetHook',
Account: wallet.address,
Hooks: hooks
})
const { tx_blob } = wallet.sign(updateTx)
const result = await client.submitAndWait(tx_blob)
console.log(`SetHook: ${result.result.meta.TransactionResult}`)
console.log(`Explorer: https://xaman.app/explorer/21338/${result.result.hash}`)
await client.disconnect()
}
updateDirectionalHook().catch(console.error)
If you run this code, you will get something like this:
Account: rLPaiK8i1a62NvRXXMJPBZqqAAcXN5CZjQ
Slot:0
New HookOnIncoming: FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFFFFFFFFFFFFFFBFFFFF
New HookOnOutgoing: FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBFFFFE
SetHook: tesSUCCESS
Explorer: https://xaman.app/explorer/21338/2A7B0CAA959FAEB78D2CC44A42A3B0CE701A120438BD939C291FEFBDA28D202C
Updating HookOnIncoming / HookOnOutgoing does not require re-uploading the WASM. The hook code and namespace remain exactly as installed; only the firing conditions change.
Reverting to Classic HookOn (09-hookon-set.js)
Because HookOn and the directional pair are mutually exclusive on the ledger (not just at install time), switching a hook slot back to non-directional mode is itself an update: send a SetHook with HookOn and the ledger drops whatever HookOnIncoming / HookOnOutgoing values were there before.
require('dotenv').config()
const { Client, Wallet } = require('xahau')
// Index of the hook slot to update.
const SLOT_INDEX = 0
// Classic non-directional HookOn value - 256-bit (64 hex chars).
// This example fires on Payment AND Invoke - a combination not used in 07 or 08.
// Use encode_hook_on at https://xahau.network/docs/tools/ to compute other values.
const HOOK_ON_PAYMENT_AND_INVOKE = 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFFFFFFFFFFFFFFBFFFFE'
// Setting HookOn replaces the directional mode (HookOnIncoming/HookOnOutgoing)
// with classic non-directional mode. You cannot combine HookOn with
// HookOnIncoming or HookOnOutgoing in the same Hook object.
async function setClassicHookOn() {
const client = new Client('wss://xahau-test.net')
await client.connect()
// Must be the account that owns the hook (HUB_SEED in .env).
const wallet = Wallet.fromSeed(process.env.HUB_SEED, { algorithm: 'secp256k1' })
console.log(`Account: ${wallet.address}`)
console.log(`Slot: ${SLOT_INDEX}`)
console.log(`HookOn: ${HOOK_ON_PAYMENT_AND_INVOKE}`)
// Pad earlier slots with empty Hook objects (no-op) so only SLOT_INDEX is touched.
const hooks = Array(SLOT_INDEX).fill({ Hook: {} })
hooks.push({
Hook: {
HookOn: HOOK_ON_PAYMENT_AND_INVOKE,
// Do NOT include HookOnIncoming or HookOnOutgoing here -
// they are mutually exclusive with HookOn.
}
})
const updateTx = await client.autofill({
TransactionType: 'SetHook',
Account: wallet.address,
Hooks: hooks
})
const { tx_blob } = wallet.sign(updateTx)
const result = await client.submitAndWait(tx_blob)
console.log(`SetHook: ${result.result.meta.TransactionResult}`)
console.log(`Explorer: https://xaman.app/explorer/21338/${result.result.hash}`)
await client.disconnect()
}
setClassicHookOn().catch(console.error)
After running this code, you will get output like this:
Account:rLPaiK8i1a62NvRXXMJPBZqqAAcXN5CZjQ
Slot: 0
HookOn: FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFFFFFFFFFFFFFFBFFFFE
SetHook: tesSUCCESS
Explorer: https://xaman.app/explorer/21338/7656275361C0FD90BA7669C443CF7012A43EE88A182C913C03FEB6DBAF892F61
When to Use HookOnV2
| Scenario | Configuration |
|---|---|
| React only to incoming transfers | HookOnIncoming: PAYMENT_ONLY, HookOnOutgoing: DISABLED |
| React only to outgoing transfers | HookOnIncoming: DISABLED, HookOnOutgoing: PAYMENT_ONLY |
| React to both directions differently | Use HookOnIncoming and HookOnOutgoing with different bitmasks |
| React to both directions identically | Use the HookOn field |
NamedHooks: Targeting Specific Hooks by Name
The Problem with Multiple Hooks
An account can have up to 10 hook slots. If you have two hooks installed (say Accept_A and Accept_B, two hooks with identical logic, each just accepts every transaction, but a different TRACESTR so you can tell them apart in the execution trace) and a transaction comes in, both hooks fire if the transaction type matches both HookOn settings. This is sometimes what
Comments
No comments yet. Start the discussion.