Skip to content

Share AMM logic through a reusable discovery and transaction client #233

Description

@3esmit

Problem

AMM economic calculations currently live inside assertion-driven instruction handlers. Host APIs,
wallets, and user interfaces cannot call those calculations without constructing runtime accounts
and handling panics, so consumers tend to reproduce formulas, account derivation, instruction
layout, and numeric conversion independently.

That creates drift risks across:

  • initial LP supply and permanently locked liquidity;
  • proportional add/remove-liquidity rounding;
  • exact-input and exact-output swap rounding and fees;
  • stored token order and swap direction;
  • post-operation reserves, LP supply, and Q64.64 spot price;
  • reserve synchronization and oracle-price initialization;
  • PDA derivation, account order, signer/init/writable flags;
  • instruction serialization;
  • slippage rounding and conversion from quotes to chain guards; and
  • u128/u64 values crossing JSON, C, and JavaScript-capable UI boundaries.

Proposal

Deliver two layers in one change:

  1. Add a public amm_program::quote module containing deterministic, fallible economic transition
    functions. Refactor AMM handlers to consume those same results.
  2. Add a reusable programs/amm/client crate that validates fetched account snapshots, exposes
    deterministic protocol-account discovery, typed quote orchestration, task-level prepared
    transactions, all canonical low-level transaction plans, exact instruction encoding, and
    lossless JSON/C ABI adapters.

The client remains stateless. It must not perform RPC, own keys, sign, submit, or manage wallet
lifecycle. Rust, C++, QML, services, and other adapters can reuse it without copying AMM protocol
logic.

The task-level API must be sufficient to delete the AMM-specific Rust clients and AMM arithmetic,
PDA, Borsh, account-order, signer, and instruction copies in feat/amm-pool-creation and
feat/amm-swap-onchain. Thin host adapters may remain for RPC, wallet, and Qt representation
translation.

Program-owned economic API

Pair and direction resolution

  • pair_order(pool, first_token_id, second_token_id)
    • returns stored or reversed order;
    • maps raw amounts to and from the pool's stored A/B order.
  • swap_direction(pool, input_token_id)
    • returns A-to-B or B-to-A;
    • rejects definitions outside the pool.

Pool creation

  • create_pool(token_a_amount, token_b_amount, fee_bps)
    • validates nonzero deposits and supported fee tier;
    • returns total initial LP, locked LP, user LP, opening reserves, and opening Q64.64 price.

Add liquidity

  • preview_add_liquidity(pool, vault_a_balance, vault_b_balance, max_a, max_b)
    • returns actual deposits, LP to mint, and post-operation pool values using the smallest
      executable LP guard.
  • add_liquidity(pool, vault_a_balance, vault_b_balance, max_a, max_b, minimum_lp)
    • applies the exact on-chain minimum-LP guard.

Remove liquidity

  • preview_remove_liquidity(pool, user_lp_balance, remove_lp)
    • returns withdrawal amounts, LP to burn, and post-operation pool values using the smallest
      executable withdrawal guards.
  • remove_liquidity(pool, user_lp_balance, remove_lp, minimum_a, minimum_b)
    • applies the exact on-chain minimum-withdrawal guards.

Swaps

  • preview_swap_exact_input(pool, vault_a_balance, vault_b_balance, direction, amount_in)
  • swap_exact_input(pool, vault_a_balance, vault_b_balance, direction, amount_in, minimum_out)
  • preview_swap_exact_output(pool, vault_a_balance, vault_b_balance, direction, amount_out)
  • swap_exact_output(pool, vault_a_balance, vault_b_balance, direction, amount_out, maximum_in)

Swap results include gross input, fee-adjusted input, fee amount, output, direction,
post-operation reserves, and post-operation Q64.64 spot price.

Other reusable behavior

  • sync_reserves(pool, vault_a_balance, vault_b_balance) returns incorporated donations and
    post-sync pool values.
  • create_oracle_price_account(pool, window_duration) returns program-derived base/quote assets,
    initial spot price, and validated window.

Initialization, configuration updates, and price-observation creation contain no reusable AMM
economic calculation. They are covered by transaction planners instead of pass-through quote
functions.

Reusable Rust client

Add amm_client as both an rlib and cdylib workspace member.

Validated account boundary

Expose:

  • AccountSnapshot from canonical nssa_core::account::Account plus AccountId;
  • AmmContext, constructed from a validated config account;
  • PoolContext, which validates canonical pool, vault, and LP-definition PDAs;
  • ValidatedFungibleDefinition;
  • ValidatedFungibleHolding; and
  • ValidatedPoolSnapshot.

Snapshot validation must cover:

  • AMM config ID, owner, and AmmConfig decoding;
  • pool owner, PoolDefinition decoding, distinct tokens, and canonical pool PDA;
  • stored vault/LP IDs and their canonical PDAs;
  • configured Token Program ownership of definitions, vaults, and user holdings;
  • fungible definition and holding decoding;
  • vault holding definition IDs matching the stored pool tokens;
  • LP definition supply matching pool LP supply and its self-authority invariant;
  • remove-liquidity holdings using the pool LP definition;
  • swap input/output holdings using opposite pool tokens; and
  • swap input balance covering the quoted input.

After validation, high-level client quote functions delegate economic work directly to
amm_program::quote. They must not reproduce formulas. All raw amounts remain u128.

Deterministic discovery and pair inspection

Expose pure discovery before consumers fetch account state:

  • derive_config_id(amm_program_id) returns the singleton AMM config ID;
  • inspect_config(amm_program_id, config_snapshot) validates and returns the configured Token
    Program, TWAP Oracle Program, and authority without exposing Borsh decoding to the host;
  • canonical_pair(first_token_id, second_token_id) returns the deterministic pre-pool token order
    used by AMM PDA derivation and rejects identical definitions; and
  • derive_pair_read_manifest(context, first_token_id, second_token_id) returns the config, pool,
    vault-by-token, LP definition, LP lock holding, TWAP current-tick, and clock IDs.

The canonical-pair comparison must be shared with amm_core::compute_pool_pda_seed; UI code must
not reproduce it through base58, hex, or JavaScript string comparison.

After an adapter fetches the manifest, expose inspect_pair over raw account snapshots. Its result
distinguishes a missing pool from an active pool and, for an active pool, returns the validated
stored token order, pool/vault/LP IDs, stored reserves, vault balances, LP supply, fee tier, and
Q64.64 spot price. It also returns caller-to-stored order mapping. Missing-pool validation must model
current guest and chained-program preconditions, not copy stricter assumptions from one UI branch.

Discovery and inspection are stateless. They derive a read set and decode supplied snapshots; they
do not perform network I/O.

User-intent preparation

Keep raw program quotes as the economic authority, then add client-owned intent translation needed
by API/UI consumers:

  • derive the minimum executable opening pair for a requested Q64.64 price;
  • derive the paired opening amount when the user edits one side, using checked integer arithmetic;
  • when both opening amounts and a price intent are supplied, require canonical
    spot_price_q64_64(amount_a, amount_b) to equal that intent;
  • return final opening amounts only after amm_program::quote::create_pool accepts them;
  • accept caller token order for create/add/remove/swap operations and return both caller-order
    display values and stored-order instruction values; and
  • expose an exact, named swap pool_spot_change_bps metric derived from pre/post quote state.

pool_spot_change_bps is pool spot-price movement, not execution-price impact. If execution-price
impact is added later, expose it as a separately named metric with separately tested semantics.
For directional reserves reserve_in/reserve_out, compute the nonnegative relative change from
reserve_in / reserve_out to
(reserve_in + amount_in) / (reserve_out - amount_out), multiply by 10,000, and floor once using
checked widened integer arithmetic. QML/JavaScript must not perform Q64.64, proportional-deposit,
fee, price-movement, or amount-guard arithmetic.

Active-pool preparation always uses the validated pool's stored fee. A UI display fallback or
default fee must never enter an active quote. If a caller supplies an expected fee, treat it only as
an equality precondition and return a stable mismatch error.

Integer-only slippage guards

Expose a validated SlippageTolerance using the canonical 10,000 basis-point denominator, plus
checked minimum-guard floor rounding and maximum-guard ceil rounding. Tolerances from zero through
10,000 basis points are accepted; larger values return a typed error. Positive minimum guards remain
at least one raw unit, and an upper guard beyond u128 returns an overflow error instead of
saturating.

Expose prepared high-level results for:

  • pool creation, returning exact NewDefinition amount fields;
  • add liquidity, deriving min_amount_liquidity, preserving caller-entered token caps as
    instruction maxima, and returning the deterministic actual deposits separately;
  • remove liquidity, deriving both minimum-withdrawal fields;
  • exact-input swap, deriving min_amount_out; and
  • exact-output swap, deriving max_amount_in.

Each prepared result contains the canonical program quote and fields that feed the matching planner
directly. UI and API adapters select a basis-point tolerance but never calculate chain guards.

Create- and add-liquidity preparation must accept the selected user token holdings and LP
destination. It validates configured Token Program ownership, held definition IDs, available token
balances against instruction spend caps, and the LP destination state required by the current
guest/callee path. Exact-output swap preparation likewise validates the maximum-input guard rather
than only the current quoted input. Holding selection and creation of a fresh wallet account remain
wallet policy.
Planner signer/init metadata always comes from the current guest/IDL contract; do not preserve
branch-local metadata when it differs.

Snapshot-to-transaction facade

Keep the low-level quote and planner functions, but expose these task-oriented operations for normal
API/UI use:

  • prepare_create_pool_transaction;
  • prepare_add_liquidity_transaction;
  • prepare_remove_liquidity_transaction;
  • prepare_swap_exact_input_transaction; and
  • prepare_swap_exact_output_transaction.

Each operation accepts raw validated-source snapshots, caller-ordered intent, selected holding IDs,
slippage, and an explicit deadline. It performs validation, caller/stored-order mapping, canonical
quoting, guard derivation, and planning in one call. It returns:

  • the canonical quote and caller-order display values;
  • the exact TransactionPlan built from that quote;
  • a stable quote commitment;
  • writable/affected account IDs; and
  • any wallet-owned prerequisite, such as provisioning a fresh LP destination.

The quote commitment binds its own domain-separation discriminator, supplied AMM program ID,
economic intent, selected holding IDs, generated guards, and source fields that affect economics or
account selection: config, pool, vault balances, token/LP definitions, user holdings, and pair
lifecycle state. It is not a deployment-version proof. Before submission, the host re-fetches
snapshots, calls the same preparation again, compares the commitment, and submits only the newly
returned plan. A changed commitment returns a stable quote_changed result and requires renewed
confirmation.

Use canonical nssa_core::Commitment values for bound account snapshots and a versioned,
typed deterministic serialization for the outer quote commitment. Do not hash presentation JSON.

The commitment excludes app-only network labels, request IDs, deadline-window policy, and ephemeral
clock/oracle values that do not affect the quote or account selection. Those snapshots are still
validated during preparation. Network/request generation IDs remain host correlation data rather
than AMM quote identity.

Canonical transaction planning

Expose one typed planner for every guest instruction:

Guest instruction Client planner
Initialize plan_initialize
UpdateConfig plan_update_config
CreatePriceObservations plan_create_price_observations
CreateOraclePriceAccount plan_create_oracle_price_account
NewDefinition plan_create_pool
AddLiquidity plan_add_liquidity
RemoveLiquidity plan_remove_liquidity
SwapExactInput plan_swap_exact_input
SwapExactOutput plan_swap_exact_output
SyncReserves plan_sync_reserves

TransactionPlan must contain:

  • the target AMM program ID;
  • the actual amm_core::Instruction, not a client mirror;
  • ordered PlannedAccount rows with semantic role, ID, writable, signer, and init flags; and
  • helpers for account IDs, signer flags/IDs, writable/affected IDs, instruction name, and encoded
    instruction words.

Planners derive AMM, oracle, and clock accounts through canonical core helpers. Quote-derived
amounts and guards flow into instruction fields without client-side recalculation.

Exact guest codec

Encode only the canonical instruction enum with the codec consumed by the guest:

pub fn encode_instruction(
    instruction: &amm_core::Instruction,
) -> risc0_zkvm::serde::Result<nssa_core::program::InstructionData> {
    risc0_zkvm::serde::to_vec(instruction)
}

Do not introduce a generated/client-only instruction mirror or alternate payload codec.

Lossless JSON and C ABI

Expose tagged JSON operations through:

  • plan_json, covering all ten canonical low-level planners and five snapshot-to-transaction
    preparation operations; and
  • quote_json, covering protocol-account discovery, pair read manifests, pair inspection,
    pre-pool canonical order, opening-liquidity preparation, pair order, create pool, preview/exact add
    and remove liquidity, preview/exact swaps, five economic preparations, reserve sync, oracle-price
    initialization, swap pool-price movement, and canonical protocol constants.

Requests and responses carry a client wire-schema discriminator such as amm-client.v1. This
versions the JSON contract only. It must not be interpreted as, or coupled to, an on-chain program
version check.

All u128 and u64 request fields use non-empty unsigned decimal strings. Quote result amounts,
reserves, supply, Q64.64 prices, and window durations are also decimal strings. Account snapshot
balance and nonce use the same lossless representation. No floating-point or JSON-number conversion
is allowed for these fields.

Expose exactly three C symbols with a checked-in header:

char *amm_client_plan(const char *request_json);
char *amm_client_quote(const char *request_json);
void amm_client_free(char *value);

The two operations return an owned UTF-8 JSON envelope. amm_client_free releases it and accepts
NULL. Boundary failures such as null input, invalid UTF-8, malformed JSON, validation errors, and
caught panics return structured error codes.

The wire plan result includes ordered account rows, affectedAccountIds, and exact instruction
words. Base58 account IDs and ProgramId word arrays remain the canonical shared representation.
Wallet-specific hex/byte layouts belong in a tested host adapter.

Optimistic same-build guarantee

The shared guarantee is source/build parity: the AMM guest and client built from the corresponding
source execute the same economic functions.

The client accepts a supplied amm_program_id as the transaction target and PDA namespace. It uses
that value for ordinary protocol account/PDA validation. It does not decide whether the supplied ID
is the correct deployed release and must not add:

  • a runtime ProgramId or ImageID allowlist;
  • a compiled ImageID comparison;
  • a compatibility manifest;
  • version metadata checks; or
  • version-specific formula branches.

Pairing a client build with its corresponding deployed program remains deployment/configuration
responsibility.

Shared core and error contracts

Expose from amm_core:

  • SUPPORTED_FEE_TIERS as a slice;
  • a fallible canonical_token_pair helper reused by compute_pool_pda_seed and the client;
  • checked_mul_div_floor; and
  • checked_mul_div_ceil.

Keep panic-based arithmetic helpers for compatibility, implemented through checked variants.

amm_program::quote returns QuoteError for expected invalid input and checked amount overflow.
Q64.64 spot-price conversion preserves spot_price_q64_64 saturation at u128::MAX.

Public errors must provide stable machine-readable codes:

  • non-exhaustive QuoteErrorCode and QuoteError::{kind, code, message};
  • non-exhaustive ClientError for snapshot, planning, balance, and wrapped quote failures;
  • typed client errors for out-of-range slippage and upper-guard overflow; and
  • WireError::code for JSON/C transport mapping.

Quote result structs remain non-exhaustive. Consumers read returned values instead of constructing
results, allowing metadata to be added without breaking callers.

Handler integration

Refactor these handlers to consume shared quote outputs directly:

  • new_definition;
  • add_liquidity;
  • remove_liquidity;
  • swap_exact_input;
  • swap_exact_output;
  • sync_reserves; and
  • create_oracle_price_account.

Handlers retain runtime account decoding, signer/init rules, deadlines, post-state ordering, PDA
authorization, chained token/oracle calls, and clock validation. They must not recalculate amounts
or pool updates returned by the quote module.

Feature-branch consumer contract

The shared library must replace every AMM-owned concern consumed by the two audited feature
branches. Migration targets the current guest and checked-in IDL, not either branch's older copied
contract.

Branch concern Shared owner
Config inspection plus pool, vault, LP, current-tick, and clock derivation Discovery/read-manifest API over canonical core helpers
Config, pool, token, holding, vault, LP, tick, and clock decoding/validation Typed snapshot and pair-inspection API
Missing versus active pool Pair lifecycle inspection
Pre-pool token ordering Shared canonical-pair helper used by pool PDA seed derivation
Desired opening price and minimum executable deposits Opening-liquidity preparation, verified through quote::create_pool
Caller-order proportional add amounts Pair-aware transaction preparation
User funding and LP destination readiness Create/add transaction preparation
Fee tiers, minimum liquidity, denominators Protocol constants response
Add/remove/swap amounts, fees, direction, slippage guards, pool updates amm_program::quote plus client preparation
Swap pool-price movement Exact integer pool_spot_change_bps result
Account order, writable/signer/init flags, affected accounts TransactionPlan, checked against current IDL
Instruction value and bytes Actual amm_core::Instruction plus RISC Zero Serde
Quote freshness between confirmation and submit Source-bound quote commitment and re-preparation

The feat/amm-swap-onchain branch predates the current swap ABI. Its legacy
token_definition_id_in, two-signer layout, and
amm_client_program_id_from_elf/AMM_PROGRAM_BIN path are not compatibility requirements. Rebase
the consumer and use the configured ProgramId plus current one-input-signer plan returned by the
shared client.

The feat/amm-pool-creation branch contains account metadata that differs from the current IDL,
including init flags for new-pool vaults and signer/init treatment for a fresh add-liquidity LP
holding. Delete that metadata copy during migration; the shared IDL-tested plan is authoritative.

Application adapters may still own token-catalog merging, wallet holding selection, request
generation/cancellation, raw RPC normalization, network labels/fingerprints, account-ID and
instruction-byte representation translation, key/account creation, signing, submission, polling,
and human formatting. None may retain AMM formulas, Borsh AMM decoding, PDA recipes, instruction
arguments, account order, or signer/init rules.

Compatibility

This change must not alter:

  • amm_core::Instruction variants or field order;
  • guest instruction signatures or account order;
  • RISC Zero Serde instruction encoding;
  • PDA seeds;
  • stored PoolDefinition encoding; or
  • checked-in AMM IDL contents.

The guest source changes, so deployment must rebuild the binary, inspect its resulting ImageID, and
configure that program identity normally. That operational step does not become a runtime client
compatibility gate.

Out of scope

  • network I/O, account-read batching, caching, retry, and endpoint orchestration;
  • token-catalog and wallet-holding selection policy;
  • generic sequencer-response normalization and wallet-specific representation translation;
  • wallet/key lifecycle, signing, and submission;
  • deadline-window policy, transaction polling, and post-submit refresh;
  • UI-specific formatting or presentation state; and
  • runtime release/version enforcement.

Deterministic AMM protocol read-set discovery is in scope even though executing those reads is not.

Tests

Add contract coverage for:

  • every program-owned quote, preview/exact guard, direction, fee, rounding, sync, and oracle path;
  • every typed quote-error code and stable string;
  • checked amount overflow and canonical spot-price saturation;
  • config, pool, vault, definition, LP, and user-holding validation;
  • unrelated vault and unrelated swap-output rejection;
  • LP supply/authority inconsistency and insufficient swap balance;
  • u128 and u64 values above 2^53 remaining exact;
  • all ten instructions round-tripping through RISC Zero Serde;
  • planner account role/order/writable/signer/init parity with the checked-in IDL;
  • account IDs, signer flags, and signer IDs remaining positionally aligned;
  • quote amounts and guards reaching instruction fields without recalculation;
  • minimum/maximum slippage rounding, basis-point bounds, positive one-unit guards, and checked upper
    overflow;
  • all five prepared results feeding their corresponding planner instruction fields exactly;
  • JSON/C success responses preserving decimal strings;
  • prepared wire responses preserving instruction arguments above 2^53 as decimal strings;
  • canonical fee tiers and minimum-liquidity metadata reaching C/JSON consumers without a local
    copy;
  • config and pair read manifests matching canonical core PDA helpers in both caller token orders;
  • missing/active pair inspection, stored/caller order, reserves, fee, and dependency IDs;
  • opening-price preparation at, below, and above Q64.64, including minimum-liquidity and overflow
    boundaries;
  • caller-order create/add preparation returning exact stored-order instruction fields;
  • create/add holding ownership, token-definition, insufficient-balance, and LP-destination cases;
  • all five snapshot-to-transaction operations returning a quote, source-bound commitment, complete
    IDL-matching plan, and affected account IDs;
  • commitment stability for identical inputs and change detection for every bound intent, holding,
    source-account, and guard field;
  • exact pool_spot_change_bps semantics without floating point;
  • versioned wire requests, branch fixture payloads, and values above 2^53; and
  • null, invalid UTF-8, malformed JSON, and NULL free behavior.

Keep existing handler and integration tests passing. Regenerate and diff the AMM IDL to prove no
wire-schema drift.

Acceptance criteria

  • Program handlers and host consumers execute one economic implementation.
  • The reusable client validates protocol snapshots before quoting.
  • Every guest instruction has a canonical typed planner.
  • Every plan stores the real amm_core::Instruction and uses exact RISC Zero Serde.
  • High-level create/add/remove/exact-in/exact-out calls return planner-ready instruction amounts;
    their snapshot-to-transaction variants return the complete plan and quote commitment; UI/API code
    performs no AMM, ordering, slippage, or Q64.64 arithmetic.
  • Deterministic discovery returns every AMM-owned account ID needed to fetch and inspect either a
    missing or active pair without consumer PDA/Borsh copies.
  • Create/add preparation validates selected funding holdings and LP destination requirements.
  • Both audited feature branches can delete their local AMM Rust/FFI implementations; remaining host
    code is limited to RPC, wallet, representation, correlation, and presentation adapters.
  • Planner account ABI mechanically matches the checked-in IDL.
  • JSON/C boundaries preserve every u128 and u64 through decimal strings.
  • The JSON contract has a schema discriminator independent of program release identity.
  • The three C symbols and ownership contract are documented and tested.
  • Errors have typed/stable codes and public result types remain extensible.
  • No runtime ProgramId/ImageID/version compatibility check is introduced.
  • No instruction, IDL, PDA, or stored-state schema changes are introduced.
  • Existing handler tests, client contract tests, workspace checks, and IDL checks pass.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions