Skip to content

HookOnIncoming / HookOnOutgoing ​

Purpose: split HookOn into two independent bit fields so a hook can react differently depending on whether the triggering transaction is outgoing from the hook's own account or incoming to it (from some other account's transaction).

Amendment gating: both fields require featureHookOnV2_1, which replaces unsupported featureHookOnV2. Before the replacement amendment, only plain HookOn is accepted; supplying HookOnIncoming/HookOnOutgoing without the directional amendment is rejected (temMALFORMED).

Validation rules:

  • If sfHookOn is absent on a create, featureHookOnV2_1 must be enabled and both sfHookOnOutgoing and sfHookOnIncoming must be present, and they must be different from each other. (If they were required to be equal, you should simply use plain HookOn instead; the pair only exists to let the two directions diverge.)
  • If sfHookOn is present, neither sfHookOnOutgoing nor sfHookOnIncoming may also be present (temMALFORMED otherwise) — the two forms are mutually exclusive at the object level.
  • With featureHookOnV2_1, the same pair, mutual-exclusion, and unequal-value checks are applied when HookOn fields are supplied by install and update, not only by create.
  • hsoDELETE and hsoNSDELETE operations must not carry any of HookOn, HookOnIncoming, or HookOnOutgoing.

Direction semantics — what "incoming" and "outgoing" mean. The perspective is always the account the hook is installed on, and it is decided by the caller, not by the field-resolution code: whichever code resolves the hook chain passes an explicit isOutgoing flag.

The call sites resolve isOutgoing:

  • The hook chain on the transaction's own sfAccount is evaluated with isOutgoing = true → uses HookOnOutgoing.
  • The hook chains on other transactional stakeholders (accounts with a stake in the transaction besides the sender — e.g. a Destination) are evaluated with isOutgoing = false → uses HookOnIncoming.
  • This reading is also consistent with ClaimReward: the hook installed on the reward issuer (not the transaction's Account) is checked against sfHookOnIncoming, because a ClaimReward transaction is incoming to the issuer, not sent by it.

So: outgoing = this account is the transaction's sender (sfAccount); incoming = this account is some other stakeholder of the transaction (e.g. a payment destination), and the transaction arrived at it without it being the sender.

Worked example: a hook installed on alice with HookOnOutgoing armed only for ttPAYMENT and HookOnIncoming armed only for ttINVOKE:

TransactionDirection for aliceFires?
invoke(bob) → dest(alice)incoming (alice is destination)yes (Invoke armed on incoming)
pay(bob, alice, ...)incomingno (Payment not armed on incoming)
pay(alice, bob, ...)outgoing (alice is sender)yes (Payment armed on outgoing)
invoke(alice)outgoingno (Invoke not armed on outgoing)

Common mistakes:

  • Supplying HookOnIncoming/HookOnOutgoing with equal values — rejected; use plain HookOn for that case.
  • Mixing HookOn with either directional field on the same sfHook object — rejected.
  • Assuming "incoming"/"outgoing" refers to money flow direction. It refers to whether this account is the transaction's sender, for any transaction type — including non-payment types like Invoke or ClaimReward.