Skip to content

Hooks and Metis compatibility

Implement an external evaluator with contextual accounts and explicit simulated effects.

Prerequisites: callback authentication, swap context, and state permissions. GraplAmm supports hook pools through an injected HookQuoteAdapter. The generic integration owns snapshots, core traversal, reply validation, and native construction. Hook implementations stay beside their external programs. A hooked pool without a matching adapter rejects; a no-hook pool needs no adapter.

Keep policy implementations outside the library

The contract lives in integrations/metis/hooks.rs, exposed as grapl_amm::metis::hooks. The fee, window, and membership examples implement it through ExampleHookQuoteAdapter in examples/hooks/shared/src/metis.rs, reusing the same source evaluator as their SBF programs. The generic library contains no allowlist, time schedule, or reward economics.

ExampleOrdinary Amm::quoteRequired inputs
Dynamic feeSupported with its injected adapterValidated schedule and frozen Clock
Trading windowSupported during the configured windowValidated window and frozen Clock
AllowlistRejects without actor contextContextual actor, proof payload, policy domain, and Clock
Prepared rewardsRequires a compatible effect evaluatorActor, complete swap intent, exact accounts, and validated reward state
Sponsored rewardsSupported with SponsoredRewardQuoteAdapterPublic configuration/issuer/mint snapshot; real wallet bound later; funded sponsor at execution
Lifecycle probeNo supplied example adapterAn explicit implementation covering its enabled swap callbacks

SingleRangeQuoter is a separate, older one-segment snapshot contract that rejects hooks. Its restrictions do not describe GraplAmm.

1. Declare exactly what the adapter supports

Trait methodResponsibility
program_id, program_labelBind one program address and name its local dependency
requirementsReject unsupported profiles/phases; declare policy count, actor need, and Clock need
validate_snapshotValidate the supplied accounts' encoding, relationships, and invariants
account_planReturn ordered HookAccountRequest { key, access } entries
requires_swap_detailsRequire complete execution intent when missing fields could change evaluation
evaluateProduce a canonical reply for a read-only evaluator
evaluate_effectsProduce a reply plus explicit account-byte updates for a stateful evaluator
execution_account_planOpt into late wallet-bound account assembly; default None preserves prepared snapshot behavior

The default account plan uses at most four canonical read-only policy PDAs. Override it for hook state, external state, or external programs. requires_swap_details() defaults to false. The default effect evaluator delegates to evaluate only for read-only phases; requested writes require an explicit effect implementation.

Every enabled swap callback must be reproduced. Both callback views have entered = true and share a simulated next operation sequence and frozen Clock. BeforeSwap receives the initial snapshot and unfilled intent. AfterSwap receives final pool state and the settled traversal result. The generic adapter checks the reply's phase, allow decision, profile, and configured fee cap through the core ABI.

2. Discover and prepare an isolated snapshot

Create GraplAmm through Amm::from_keyed_account, then bind an adapter with with_hook_adapter(Arc::new(adapter)). Binding invalidates quote readiness.

For a contextual account plan:

  1. Build the request's HookSwapContext.
  2. Call get_accounts_to_update_with_context(&context).
  3. Fetch the six native pool/tick/mint/vault dependencies, executable hook, and ordered extra accounts.
  4. Call prepare_context(provider, clock, &context) to obtain a separately refreshed clone.
  5. Quote and construct the swap using that clone and the same context.

prepare_context preserves the original AMM instance. It does not put a buyer or proof into shared mutable request state. HookAccountSnapshot contains key, owner, and bytes; the loader also validates executable status and the core's permissions, class/owner declarations, aliases, and account bounds. The external adapter validates each role's exact address and contents.

Read-only public policies can use get_accounts_to_update and update_with_clock. Standard Amm::update freezes the injected AmmContext.clock_ref. The host must obtain coherent accounts and time: separate atomic ClockRef reads do not establish one bank snapshot. A failed in-place refresh clears readiness; failed prepare_context leaves the original adapter unchanged. Quotes fetch no accounts and read no mutable Clock.

3. Supply the execution fields the policy needs

The current context is:

pub struct HookSwapContext {
    pub actor: Option<Pubkey>,
    pub payload: Vec<u8>,
    pub details: Option<HookSwapDetails>,
    pub accounts: Vec<HookAccountRequest>,
}

For an actor/proof-only policy, a context fragment can leave the other fields at their defaults:

let context = HookSwapContext {
    actor: Some(actor),
    payload: membership_payload,
    ..Default::default()
};

An adapter requiring complete intent must receive details: direction, mints, settlement account addresses, authority/owner, requested input, minimum output, and price limit, with settled: None. When supplying details for a quote, set actor explicitly: input_authority and output_owner must equal that non-default actor, and the settlement accounts must be distinct and non-default. These are claimed bindings until execution verifies them. Supply an ordered account plan when the adapter requires one. A generic quote must reject if it cannot determine execution-dependent eligibility.

HookEvaluation borrows pool, invocation, accounts, and optional full clock; intent_complete identifies whether complete execution details were supplied. The invocation is the canonical ABI 4 type, including its account descriptors. Quote identity is claimed context, not a signature or a reservation of the next pool sequence.

Ordinary Amm::quote uses empty context and rejects an actor-dependent or complete-intent-dependent adapter. The upstream QuoteParams has no wallet, signature, arbitrary proof, or timestamp field. Contextual methods supplement the local adapter API without modifying Jupiter's public request format.

4. Simulate state effects separately from swap output

evaluate_effects returns HookEvaluationResult { reply, updates }. Each HookAccountUpdate { key, bytes } replaces an existing account's bytes under the current phase's declared write permission. Duplicate updates, undeclared keys, read-only accounts, executable accounts, and byte-length changes reject. This interface cannot create accounts, resize them, change their owner/lamports, or bypass protected aliases.

Before-phase changes feed the later phase in the same simulation. quote_effects_with_context returns HookQuoteResult { quote, updates }, containing final changed account bytes separately from the AMM quote. The original snapshot stays unchanged; this is hypothetical execution, not committed state.

For prepared rewards, compare mint supply, destination balance, and issuance counters through these effects. Prepare those accounts before quoting/swapping. Never add reward credits to Quote.out_amount: that field counts the selected AMM output token.

The opt-in sponsored adapter quotes public eligibility and the AMM output without predicting wallet reward bytes. execution_account_plan derives recipient accounts only after SwapParams.user is available. The hook creates missing accounts from its own rent reserve and applies participant caps during execution. This is an explicit public quote mode, not simulation of account creation. It rejects identity-dependent allowlists, and a quote cannot guarantee sponsor funding or recipient-account validity.

5. Preserve context in native construction

Use swap_with_context(&swap_params, &context) with the quoted actor, payload, details, and account plan. The builder binds an explicit actor to SwapParams.user; an omitted actor is filled from that user for construction. It requires user == token_transfer_authority. Explicit settlement addresses must match the native swap accounts. The transaction and core still authenticate signer privilege and token-account ownership.

The builder returns Swap::Placeholder plus native account metas. Hook extras follow the twelve native accounts and visited tick pages, never request signer privilege, and request transaction writability only for enabled write phases. The core reconstructs the narrower callback privileges. Use encode_native_swap to preserve the supported native decision and matching SwapArgs descriptors rather than assembling placeholder bytes yourself.

The current native decision uses the GRAPLM03 envelope and rejects legacy decisions. Encoding must preserve the requested input; when complete context bound a minimum output, a different minimum rejects. The price limit, payload, and account descriptors travel in the decision as well.

A routed CPI signer can be a PDA owning intermediate token accounts. That authenticated local authority does not universally identify the original human wallet or final beneficiary. Public routed identity needs actual route/CPI evidence; the local placeholder is not a registered Jupiter destination.

6. Compare quotes with actual settlement

Compare quote amounts and supported simulated effects with settled reserve and hook-account changes. Cover both directions, each advertised swap mode, policy boundaries, rejection, and rollback. See quote consistency.

Refresh for changed account/time state. A window can close, a proof can expire, or another transaction can consume a reward budget before execution. Program upgrades also require revalidating evaluator compatibility; a matching program address does not prove matching bytecode.

Never make a pool quotable by ignoring a callback, omitting state, or substituting a static fee. Implement the missing behavior or reject explicitly. Public routing additionally requires destination/encoder support, account/CU checks, and the separate Jupiter integration process.

Continue with the general Metis guide, reward example, and hook testing. integrations/metis/HOOK_API.md is the source-level adapter contract.

Sale-funded output fees

The adapter applies the same bounded output-fee calculation as core. Quote.out_amount is net, and HookQuoteResult.output_fee_amount exposes the separate output charge. Input-fee metadata remains denominated in the input mint. Both vault sufficiency and net minOut are checked. The rewards example uses this to replenish its rent sponsor with native SOL.