Skip to content
Mainnet Live. All executions, addresses and hashes are live.
WORMHOOK
conceptualv0.1 · skeleton docs

HookCall

The request a Solana app sends. Small enough to audit, explicit enough to enforce.

Schema

hookcall.tsconceptual
type HookCall = {
  version: number;        // schema version, currently 1
  sourceChain: string;    // "solana"
  sourceApp: string;      // registered app's program ID (base58)
  hookId: string;         // "imd.research.v1"
  nonce: string;          // per-app, strictly increasing (u64)
  action: string;         // "START_RESEARCH"
  payloadHash: string;    // H(canonical payload)
  maxSpend?: string;      // decimal string in the hook's spend currency
  expiry: number;         // unix seconds; gateway refuses after this
};

The payload itself travels alongside the call. The hash commits to it, so a payload altered in transit fails verification.

Fields

FieldWhy it exists
versionLets the gateway reject encodings it doesn't understand instead of misreading them.
sourceChain, sourceAppIdentify the caller. The gateway also checks the Wormhole emitter matches the registered router.
hookId, actionWhat to run. Unknown hook → WH-303; paused → WH-304; not allowed → WH-305.
nonceMakes every call unique. Together with the source app and payload hash it derives the call ID.
payloadHashCommits to the payload so it can't be swapped.
maxSpendUpper bound on value the hook may consume. Checked against the hook cap and app cap (WH-306).
expiryA call delivered late is refused, never executed late (WH-201).

Call ID

conceptual
callId = H(sourceChain ‖ sourceApp ‖ nonce ‖ payloadHash)

The ID is deterministic, so retries and redeliveries of the same call share it. The gateway records consumed IDs, which is why a duplicate delivery is rejected (WH-302) rather than executed twice.

Building one

You don't assemble HookCalls by hand. The SDK does it:

const call = await client.prepareCall({
  hookId: "imd.research.v1",
  action: "START_RESEARCH",
  payload: { query: "…", depth: "standard", budget: "40" },
  maxSpend: "40",
  ttl: 900,                   // expiry = now + 15 minutes
});

call.callId;                  // 0x…
call.call.payloadHash;        // 0x…
call.encoded;                 // mock encoding (skeleton)

Choosing a TTL

Expiry must cover source finality (~13s on Solana), executor pickup, and destination inclusion. On Ethereum, a TTL under a few minutes leaves no slack for a slow executor. The SDK defaults to 900 seconds. Shorter TTLs are useful for time-sensitive actions, and you should expect WH-201 when delivery is slow.