Appearance
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
sfHookOnis absent on a create,featureHookOnV2_1must be enabled and bothsfHookOnOutgoingandsfHookOnIncomingmust be present, and they must be different from each other. (If they were required to be equal, you should simply use plainHookOninstead; the pair only exists to let the two directions diverge.) - If
sfHookOnis present, neithersfHookOnOutgoingnorsfHookOnIncomingmay also be present (temMALFORMEDotherwise) — the two forms are mutually exclusive at the object level.
- With
featureHookOnV2_1, the same pair, mutual-exclusion, and unequal-value checks are applied whenHookOnfields are supplied by install and update, not only by create. hsoDELETEandhsoNSDELETEoperations must not carry any ofHookOn,HookOnIncoming, orHookOnOutgoing.
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
sfAccountis evaluated withisOutgoing = true→ usesHookOnOutgoing. - The hook chains on other transactional stakeholders (accounts with a stake in the transaction besides the sender — e.g. a
Destination) are evaluated withisOutgoing = false→ usesHookOnIncoming. - This reading is also consistent with
ClaimReward: the hook installed on the reward issuer (not the transaction'sAccount) is checked againstsfHookOnIncoming, because aClaimRewardtransaction 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:
| Transaction | Direction for alice | Fires? |
|---|---|---|
invoke(bob) → dest(alice) | incoming (alice is destination) | yes (Invoke armed on incoming) |
pay(bob, alice, ...) | incoming | no (Payment not armed on incoming) |
pay(alice, bob, ...) | outgoing (alice is sender) | yes (Payment armed on outgoing) |
invoke(alice) | outgoing | no (Invoke not armed on outgoing) |
Common mistakes:
- Supplying
HookOnIncoming/HookOnOutgoingwith equal values — rejected; use plainHookOnfor that case. - Mixing
HookOnwith either directional field on the samesfHookobject — 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
InvokeorClaimReward.