Introduce the add-liquidity vertical mirroring the swap + createPool patterns,
for depositing into an existing pool via the AddLiquidity instruction.
FFI (amm_ffi):
- add_liquidity_quote — decode poolData, orient the caller's max amounts to the
pool's canonical order, run the guest's exact ideal->actual->delta_lp math, and
return { amountARaw, amountBRaw, expectedLpRaw, priceRaw } in display order.
Slippage-free like create's quote; the min-LP floor is applied at submit.
- add_liquidity_plan — canonicalize (token, max-amount, holding) as one unit,
take vaults + LP definition from poolData, emit the 10-account AddLiquidity
order (only the user holdings a/b/LP sign). Takes minLpRaw directly, like
swap_exact_in_plan takes min_out.
Shared with createPool: extract canonical_triples (the pair/amount/holding
canonical swap) and plan_response (the tx-submission envelope); both the create
and add plans now use them.
A thin source for the account selector: an amm_ffi op that decodes the wallet's
fungible TokenHoldings (owned by the configured token program) into
[{ accountId (hex), accountType:\"TokenHolding\", definitionId (base58),
definitionIdHex (hex), balanceRaw }] — one row per holding account, every token,
including zero-balance holdings; narrowing to a specific token is the selector's
job. Exposed via AmmModuleImpl::tokenHoldings and the AmmUiBackend tokenHoldings()
slot, both gated on wallet-open.
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.
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.
Derives a pool's PDA from the AMM program id and the two token ids —
compute_pool_pda(amm_program, canonical(token_in, token_out)) — returning
{ poolId }. Tokens may be given in either order; the op canonicalizes.
Unlike swap_pair, which derives the whole pair account-set (including the
current-tick PDA, which depends on config.twap_oracle_program_id and so
requires reading the config account), the pool address depends only on the
program id and the token pair. So a caller that just needs to locate and
read the pool can skip the config read entirely.
Exposed as the amm_pool_id C ABI export. Additive — no existing op changes.
It backs the config-free pool lookup the upcoming swap-quote path needs (and,
later, resolvePoolAccount).
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)