Bring pool creation onto the redesigned lean module surface
mirroring the shipped swap vertical.
FFI (amm_ffi):
- amm_liquidity_quote: a pure create-pool preview from the two deposit
amounts — expectedLpRaw / initialPriceRaw / lockedLpRaw via the shared
amm_core opening-LP math (isqrt_product, MINIMUM_LIQUIDITY,
spot_price_q64_64), so the preview equals what new_definition mints. No
chain reads, no quoteHash, and no fee input (the fee is neither part of the
pool PDA nor the pricing — one pool per pair).
- amm_create_pool_plan: canonicalizes the pair, moving amounts and user
holdings as one unit so each (vault, holding, amount) triple names the same
token, then emits the fixed 11-account NewDefinition plan (only the user
a/b and fresh LP holdings sign).
Module (AmmModuleImpl):
- liquidityQuote(request): thin preview wrapper, normalizes ids to hex.
- createPool(request, fresh_lp_id): a new pool always needs a fresh LP
holding, so an empty fresh_lp_id returns requiresFreshLp without
submitting; otherwise builds the plan and submits, returning a hex
transactionId.
Server-side SwapExactOutput submission, mirroring the exact-in path. The
swap_exact_out_plan FFI op resolves the pool via the shared derive_pair,
encodes amm_core::Instruction::SwapExactOutput { exact_amount_out,
max_amount_in, deadline }, and returns the same four tx-submission fields as
swap_exact_in_plan (programId, accountIds, signingRequirements, instruction)
in the identical fixed 8-account IDL order — only the user's input holding
signs. Recoverable domain failures (same_token_pair, config_unavailable) go
through the FFI envelope as Err. C export amm_swap_exact_out_plan (cbindgen
header regenerated).
The module swapExactOutput(defA, defB, userIn, userOut, amountOut, maxIn,
deadline) method reads the AMM config, calls the op, and submits the plan via
send_generic_public_transaction, returning the tx hash (empty string on
failure). Same argument conventions and I/O-free split as swapExactInput.
Not yet wired into AmmUiBackend/QML — the SwapCard rewire is a follow-up. The
existing swap UI path is untouched, so nothing breaks.
The plan op is internal plumbing — its only consumer is the module's
swapExactInput, which just forwards the four tx-submission fields to the
wallet. Collapse its response onto the FFI envelope: return { programId,
accountIds, signingRequirements, instruction } directly and route the
recoverable domain failures (same_token_pair, config_unavailable) through
Err -> { ok: false, error } instead of a nested { status, code } object.
Also drop the deadlineMs echo — it's unused (the deadline is already encoded
in the instruction bytes). swapExactInput now treats any non-ok plan as a
failure. No behavior change for the UI: the plan JSON never crosses the
module boundary.
The swap-submission plan derived the pool's vaults from the canonical token
order (compute_vault_pda(pool, canonical_token_a/b) via derive_pair), but the
guest asserts the provided vaults against the pool's stored vault_a_id /
vault_b_id, which are in the pool's *creation* order. compute_pool_pda_seed
canonicalizes, so the pool address is order-independent, but NewDefinition
stores def_a/def_b (and their vaults) as created — so for a pool created in
non-canonical order, the plan put vault_b in the vault_a slot and the guest
panicked with "Vault A was not provided", reverting the swap.
Read the pool account in swapExactInput and pass its data to
swap_exact_in_plan, which now uses pool.vault_a_id / pool.vault_b_id verbatim
for the vault slots (pool / current_tick / clock stay from derive_pair, since
those are order-independent). This restores the order-agnostic behavior the
original swap client had before it was rewired onto the canonical derive_pair
in 737b2f6.
Adds a regression test that builds a non-canonically-created pool (stored
def_a = the smaller-valued token) and asserts the plan emits the pool's stored
vaults, guarding that they differ from the canonical derivation.
Rename the swap-submission planning op for symmetry with the exact-in/exact-out
split already applied to the quote ops (swap_exact_in_quote /
swap_exact_out_quote): swap_plan -> swap_exact_in_plan, SwapPlanRequest ->
SwapExactInPlanRequest, and the C export amm_swap_plan -> amm_swap_exact_in_plan
(cbindgen header regenerated). The module's swapExactInput call site and trace
are updated to match.
Server-side SwapExactOutput preview. The swap_exact_out_quote FFI op orients
the pool reserves to the requested in/out direction, prices via the shared
amm_core::swap_exact_out_amounts (so requiredIn matches the chain), and
derives the slippage ceiling: { requiredInRaw, maxInRaw, priceImpactBps }.
no_pool and output_exceeds_liquidity (amount_out >= reserve) are returned as
errors. Read-only — no quoteHash; the on-chain max_amount_in is the real
guard.
The module swapExactOutQuote(tokenIn, tokenOut, amountOut, slippageBps)
method derives the pool via the config-free pool_id op, reads it, and wraps
the op in the { status, error, ... } envelope. Mirrors swapExactInQuote.
Not yet consumed by the QML swap view, so nothing breaks. Lets the buy field
stay editable when the SwapCard is rewired in a follow-up.
Server-side SwapExactInput preview. The swap_exact_in_quote FFI op orients the pool reserves to the requested in/out direction, prices via the shared amm_core::swap_exact_in_amounts (so expectedOut matches the chain), and derives the slippage floor: { expectedOutRaw, minReceivedRaw, priceImpactBps }. no_pool is returned as an error; pool metadata (reserves/fee) comes from resolve_pool, so it isn't echoed. Read-only — no quoteHash; the on-chain min_amount_out is the real guard.
The module swapExactInQuote(tokenIn, tokenOut, amountIn, slippageBps) method derives the pool via the config-free pool_id op, reads it, and wraps the op in the { status, error, ... } envelope; call() now surfaces the op error code so no_pool propagates.
The QML swap view still uses the old path, so nothing breaks. Will make the QML consume it in a follow-up.
After the amm_module refactor, this crate is linked only by the module (the
UI delegates to modules().amm_module and links nothing), so its home under
apps/amm/ and the name "client" were both misnomers: it's the AMM business
logic the module wraps, reached across an FFI boundary — the same relationship
logos_execution_zone has with wallet_ffi. Co-locate it with the module that
owns it and name it for that role.
The FFI surface is unchanged — the exported functions are already amm_* (not
amm_client_*) — so only the crate, directory, generated header, dylib, and
package names move. The module's call sites are untouched; it just includes the
renamed header.
- apps/amm/client → modules/amm/ffi (git-tracked rename; history preserved)
- crate amm_client → amm_ffi: package name, include/amm_ffi.h, libamm_ffi.dylib,
AMM_FFI_H guard, build.rs output path, tests/public_api.rs import
- workspace member path + Cargo.lock
- flake.nix: pname, -p, header-copy path, dylib install_name, packages.amm_ffi,
ammModuleOutputs externalLibInputs, DYLD wrapper
- modules/amm: metadata external_libraries, CMakeLists EXTERNAL_LIBS, impl
#include + comments, README, flake note
- apps/amm/flake.nix: drop the now-dead amm_client external-lib input (the UI
links no external lib of its own)
Resulting layout:
modules/amm/
src/ # C++ module (amm_module_impl.{h,cpp})
ffi/ # Rust crate amm_ffi (Cargo.toml, src/, include/amm_ffi.h)
The add-liquidity flow stamped a `new-position.v1` schema tag on every
request and response and validated it across all three layers — the QML
plugin, the amm_module core module, and the amm_client Rust crate. It was a
cross-version compatibility guard, but these artifacts always ship together,
so the contract is honored implicitly, and the swap view already works fine
without one. Dropping it makes the liquidity view consistent with swap and
removes a layer of ceremony.
- amm_client: remove PositionRequest.schema and the unsupported_schema check
in compute_quote; drop QuoteCommitment.schema (changes quoteHash, which is
internal-only) and every "schema" response stamp; delete the SCHEMA /
NEW_POSITION_SCHEMA constants and the public export.
- amm_module: remove the SCHEMA constant and its four response stamps.
- AmmUiBackend: remove its local NEW_POSITION_SCHEMA and the stamps in
loadingContext() / newPositionError().
- QML: drop the "schema" fields from the request/envelope builders and relax
the validity gates to check status / canSubmit instead (the sole check in
NewPositionFlow now guards on a missing `status`); strip the now-dead
schema fields from the liquidity QML test fixtures.
- Also removes the last stale comment references to the deleted *Runtime
classes.
Introduce modules/amm — the AMM business logic as a universal core Logos module,
consumed identically by the QML UI (via modules().amm_module) and headlessly
(logoscore call amm_module ...). The module is a thin transport adapter: the
domain math lives in the Rust amm_client crate (the transport-independent JSON
FFI), and the module sequences those pure ops with chain I/O delegated to the
logos_execution_zone wallet module. It reaches the same shared wallet instance
the UI opened (Basecamp loads core modules as singletons; standalone the
LogosAPI client cache dedups the connection), so it never opens a second wallet.
The module owns the full AMM surface — not just swaps:
- resolvePool / swapExactInput / tokenList (the swap path)
- newPositionContext / quoteNewPosition / submitNewPosition (add-liquidity)
apps/amm: delete the app-side orchestration (SwapRuntime, NewPositionRuntime,
AmmClient/BundledAmmClient) and the amm_client link. AmmUiBackend now owns only
wallet-session lifecycle and forwards every AMM slot to modules().amm_module.
The one wallet-keyset mutation add-liquidity needs — creating a fresh LP holding
— stays in the backend (via its wallet provider, keeping the account model and
on-disk storage coherent): the module returns "requires_fresh_lp" without
submitting, the backend creates the account and resubmits with its id.
flake.nix / CMakeLists / metadata: the module links the amm_client crate; the UI
links no external lib and depends on amm_module (injected into the UI builder's
flakeInputs so the dependency resolves).
- amounts/deadline declared nlohmann::json so the generated dispatch accepts a
JSON number (bare small ints on the CLI) or a string (exact u128 from the UI,
or a quote-wrapped big value on the CLI); JSON floats are rejected rather than
submit a silently-rounded amount.
- AMM_DEBUG-gated tracing for the swap path.
- Drop tests/cpp/NewPositionRuntimeTest.cpp with the class it covered
(module-level tests to follow).
- modules/amm/README.md: architecture, headless prerequisites, logoscore recipe.