Commit Graph
196 Commits
Author SHA1 Message Date
r4bbit d48900e931 refactor(amm): move tokenList off the module; app reads TOKENS_CONFIG
Token discovery is an app concern, not module business — same rationale as
poolList reading AMM_POOLS_CONFIG. Drop tokenList() from amm_module and have the
app read the config itself.
2026-08-14 11:45:07 +02:00
r4bbit 7e45e44eac feat(amm): add oracle setup ops (createPriceObservations / createOraclePriceAccount)
Expose the two TWAP oracle-setup instructions as module ops so a pool's price feeds
can be seeded from the app. Both chain into the configured oracle, seeded from
validated pool state (initial tick read on-chain) — nothing is caller-priced, and
each window is a distinct feed account.
2026-08-13 22:43:48 +02:00
r4bbit ca8adfc4af feat(amm): add transferOwnership (UpdateConfig admin transfer)
Expose the authority-only UpdateConfig as a module op so the admin can transfer
AMM ownership. The guest change (UpdateConfig restricted to the current admin) is
already shipped; this is the module wrapper.
2026-08-13 21:32:42 +02:00
r4bbit 56c80ce29d feat(amm): add configAccount read (decode the singleton config)
Expose the AMM config account as a read op, so a future config/admin view can show
the authority and the token/twap program ids the AMM chains into.
2026-08-13 21:30:14 +02:00
r4bbit 92f55652a8 refactor(amm): enrich resolvePool into resolvePoolAccount
Return the pool's full derived state from one read instead of just existence +
reserves, so callers get the derived accounts (for future account views / oracle
setup) without re-deriving.

FFI resolve_pool: drop the `exists` boolean — the presence of data is the signal.
An existing pool returns { status:"ok", ..., poolId, defAHex, defBHex, vaultAId,
vaultBId, lpDefinitionId, reserveA, reserveB, liquiditySupply, feeBps }; a missing /
uninitialized pool is the { status:"error", error:"no_pool", poolId } error (still
carrying the derived poolId for address derivation).

Module: resolvePool -> resolvePoolAccount — { status:"error", error } envelope for
hard failures, and orient reserves + defs + vaults to the caller's requested order.

Backend + QML: rename the slot; SwapCard and NewPositionFlow switch the existence
check from pool.exists to pool.status === "ok" (reserve field names unchanged, so
no other consumer edits). no_pool still routes to create-pool; hard errors still
surface.
2026-08-13 16:55:45 +02:00
r4bbit cbb75c38fd refactor(amm): remove the dead Network/context machinery
The liquidity token surface moved app-side (resolveTokens + custom tokens),
leaving the whole newPositionContext path dormant. Delete it end to end and
point the swap methods at the same lean program-id helper everything else uses.
2026-08-13 16:17:58 +02:00
r4bbit 10b52ea2f6 fix(apps/amm): repair addCustomToken — restore token resolution and closing brace
The Copilot Autofix squash reworked addCustomToken to persist the canonical
definitionId (and to fail when the store write fails), but it deleted the line
that resolves the token — `const QVariantList rows = resolveTokens(probe, …)` —
while still calling `rows.first()`, and it dropped the function's closing brace.
The result didn't compile: `rows` was undefined and customTokenStorePath parsed
as a nested definition ("function definition is not allowed here").

Restore the resolveTokens call and the `rows.isEmpty()` guard before `rows` is
used, and re-add the closing brace. The autofix's intent is preserved: resolve
the pasted id, keep the canonical definitionId, persist it, and surface a
backend_error if saveCustomTokenIds fails
2026-08-13 15:45:37 +02:00
r4bbit 7a7ebfdbaf feat(amm): source liquidity tokens app-side + add custom tokens by id
Move the liquidity token selector off the module's stateful newPositionContext
onto a lean, app-owned surface, and let users add unlisted tokens by id.

FFI: new stateless `resolve_tokens` op — the app passes an explicit id set and
gets uniform selector rows `{ definitionId (base58), name, totalSupply, holdingId,
balance }`, held tokens first, unresolvable/non-fungible ids omitted. Reuses the
per-token definition/holding logic from `context`, without the network/status
envelope. Unit-tested.

Module: `resolveTokens(request, wallet_open)` reads the definitions + wallet and
calls the op (ids wrapped in a map — the universal-module glue only marshals
map/scalar inputs, not bare lists).

Backend: the app owns the id set — configured tokens (TOKENS_CONFIG) plus the
user's persisted custom ids. Held-but-unlisted tokens are NOT auto-listed (the
list mirrors the swap side); a token you hold still shows its balance once listed.
`addCustomToken` validates a pasted id by resolving its on-chain definition, then
persists it to CUSTOM_TOKEN_CONFIG (defaulting to the per-user app-data store, with
a HOME fallback so persistence never silently no-ops on an empty path).

QML: NewPositionForm/LiquidityPage take tokens/walletReady/loadingTokens as inputs
and drive selection + custom-token resolution through the backend; dropped all
newPositionContext reads and the selectable/status/code row fields.

Tests: custom-token.mjs creates token D on-chain (left out of the token config)
and verifies pasting its id resolves, selects, and persists it across a reload.
The setup script mints token D and initializes/prints the isolated
CUSTOM_TOKEN_CONFIG store
2026-08-13 15:33:31 +02:00
r4bbit cb1b457ad3 feat(amm): expose supported fee tiers via feeTiers() op
The liquidity form's fee-tier selector was fed from the module's
newPositionContext, which hardcoded an empty list — leaving the selector
blank. Source the tiers from the program instead so the UI can never
drift from what the guest accepts.

Add amm_core::SUPPORTED_FEE_TIERS: the canonical ascending list of raw
bps ([1, 5, 30, 100]), built from the existing FEE_TIER_BPS_* constants.
is_supported_fee_tier's match is left unchanged and the new const is
unused on-chain, so the guest ImageID is unaffected; a drift-guard test
locks the list to the check (every entry accepted, neighbours rejected,
ascending/deduped).

Wire it through the stack:
- FFI: amm_fee_tiers op reading SUPPORTED_FEE_TIERS -> { feeTiers: [...] }
  (empty FeeTiersRequest, cbindgen header regenerated).
- Module: LogosList feeTiers() unwrapping the list, like tokenHoldings.
- Backend: QVariantList feeTiers() QtRO slot forwarding to the module.
- QML: LiquidityPage fetches backend.feeTiers() once (wallet-independent)
  and injects it into NewPositionForm, which wraps each int into a
  { feeBps } row for the existing delegate. Drop the now-dead feeTiers
  key from the flow's loadingContext().
2026-08-13 14:22:11 +02:00
Ricardo Guilherme Schmidt 4cfc03a815 feat(apps/amm): drive the Pools list from AMM_POOLS_CONFIG
The Pools page shipped with a hardcoded four-pair sample. Replace it with a
config-driven "known pools" list, mirroring how the Swap token picker reads
TOKENS_CONFIG: the app loads a flat JSON array from the AMM_POOLS_CONFIG
environment variable and renders one row per entry. Adding pairs is a config
edit — no app change.

Pool discovery is an app concern, so the config is read in the backend
(AmmUiBackend::poolList, Qt JSON) rather than the amm_module — the module is
shedding app-specific view surface (tokenList/newPositionContext), so pools go
where tokens are heading, not where they are today. poolList() fails soft to an
empty list when AMM_POOLS_CONFIG is unset/unreadable/not an array, and skips
individual entries missing tokenA/tokenB/a numeric feeBps.

Each entry carries the display symbols (tokenA/tokenB), feeBps, and the on-chain
identifiers (poolId, tokenADefinitionId, tokenBDefinitionId) so a row can later
be resolved against chain state. PoolsPage takes injected backend/runtime and
loads via runtime.watch(backend.poolList()); the Repeater renders entries
generically.

The AMM testnet setup script now emits amm-pools.json from a POOL_SPECS array
(one line per seeded pool, currently the seeded TKA/TKB pool) and prints
AMM_POOLS_CONFIG in the launch instructions. Adds amm-pools.json.example, a
README section, and gitignores the runtime config files.
2026-08-13 14:13:31 +02:00
r4bbit c47f387ad4 refactor(amm): rename the create-pool quote surface for symmetry
Two naming cleanups on the create-pool quote, aligning it with the add / remove
counterparts (per modules/amm/INTERFACE.md). Pure renames — no behavior change.

- `liquidityQuote` → `createPoolQuote` across the stack: the FFI op
  (`liquidity_quote` → `create_pool_quote`, `LiquidityQuoteRequest` →
  `CreatePoolQuoteRequest`, `amm_liquidity_quote` → `amm_create_pool_quote`,
  cbindgen header regenerated), the module method, the AmmUiBackend slot, and the
  QML call site. It really is the create-pool quote — `addLiquidityQuote` /
  `removeLiquidityQuote` are the other branches — so the old name misled.
- `initialPriceRealRaw` → `priceRaw` (request field `initial_price_real_raw` →
  `price_raw`): drops the legacy "Real" and unifies the price key with the add /
  remove quotes, which already return `priceRaw`. Create, add, and remove quotes
  now all speak `priceRaw`; the create-vs-add routing in NewPositionFlow keys on
  `request.priceRaw`.
2026-08-13 13:25:20 +02:00
r4bbit 037e0a192c refactor(apps/amm): drop dead liquidity-form leftovers from the quote migration
Cleanup surfaced while auditing the lean-quote migration:
- Remove the `quote_changed` / `quote_not_submittable` error messages — relics of
  the dropped quoteHash/canSubmit commitment model, emitted by nothing now.
- Remove the `network_unknown` / `network_mismatch` messages — the `Network`
  model is gone, so they're never emitted.
- warningText: drop the dead `quotePayload.warnings` branch (lean quotes carry no
  warnings); keep the live token-sourcing-context warnings.
2026-08-13 13:25:20 +02:00
Ricardo Guilherme Schmidt 574d814f48 fix(amm): restore liquidity controls after refresh 2026-08-13 13:24:16 +02:00
r4bbit bf63070a9e fix(apps/amm): disable the already-selected token in the swap token picker
The token selector let you pick the same token on both sides of a swap.
That drove resolvePool into amm_client_pool_pda with def_a == def_b, which
hits `panic!("Definitions match")` in amm_core (a pool needs two distinct
tokens). Because that panic crosses the `#[no_mangle]` FFI boundary — which
can't unwind — it aborts, taking the whole UI process down.

Guard it at the source: the picker now disables (dims, no hover/click, tags
"Selected") whichever token is already chosen on the opposite side, so the
two sides can never match.

- TokenListItem: add a `disabled` state (opacity, inert MouseArea, tag)
- TokenSelectorModal: add `disabledDefinitionId`; gate both the popular-token
  pills and the list rows on it
- SwapPage: on open, set it to the opposite side's selected token
2026-08-13 13:03:58 +02:00
r4bbit 62dc45177d feat(modules/amm): add sync-reserves module op
A permissionless keeper op that refreshes a pool's stored reserves to the live
vault balances (and its TWAP tick). Same lean pattern as the other plans, but
minimal: SyncReserves is a unit instruction — no quote, no user inputs (no
amounts/slippage/deadline/holdings), and nothing signs.

FFI (modules/amm/ffi):
- sync_reserves_plan: encodes SyncReserves over the fixed 6-account IDL order
  (config, pool, vault_a, vault_b, current_tick, clock), all non-signing.
  config/pool/current_tick/clock are order-independent PDAs from derive_pair;
  the vaults come from the pool's stored ids in pool_data (read-only, but the
  guest still asserts them — a non-canonically-stored pool would otherwise
  mismatch). Fails closed: same_token_pair, config_unavailable, no_pool.
- Wired through mod.rs / ffi.rs (cbindgen header regenerated). Tests cover the
  stored-vault + zero-signer + unit-instruction layout and the fail-closed
  paths. amm_ffi: 43 tests pass, clippy clean. (lib.rs is a cargo fmt re-wrap.)

C++ module (modules/amm/src):
- syncReserves reads config + pool server-side, calls the plan, and submits.
  request is just { tokenAId, tokenBId } — no holdings/amounts/deadline. Public
  method → auto-exposed via the universal-module dispatch.
2026-08-11 17:51:51 +02:00
r4bbit 44b70e4333 feat(modules/amm): add remove-liquidity module ops
The remove-liquidity counterpart of the add ops, following the same lean
pattern: pure Rust FFI pricing/plan + thin C++ orchestration, hex ids end to
end, the token pair oriented to the pool's stored order server-side. No UI yet.

FFI (modules/amm/ffi):
- remove_liquidity_quote: burning lpAmountRaw returns the proportional share of
  each reserve — withdraw = floor(reserve·lp/supply), the guest's own math —
  plus the slippage-floored minimumAmount{A,B}Raw the submit enforces and the
  pool's spot price, all in the caller's display order. Guards: same_token_pair,
  invalid_slippage, no_pool, insufficient_pool_liquidity (burn exceeds the
  supply unlocked above MINIMUM_LIQUIDITY), pair_mismatch, amount_too_low,
  minimum_amount_zero.
- remove_liquidity_plan: encodes RemoveLiquidity over the fixed 10-account IDL
  order, orienting (min_amount, holding) to the pool's stored order like the add
  plan — but only user_holding_lp signs (it is burned) and there is no fresh
  holding: the existing token a/b holdings receive the withdrawal.
- Wired through mod.rs / ffi.rs (cbindgen header regenerated). Unit tests cover
  the guest-formula pricing + display orientation, the guard set, the plan's
  account/signer layout, and fail-closed. amm_ffi: 41 tests pass, clippy clean.

C++ module (modules/amm/src):
- removeLiquidityQuote / removeLiquidity mirror addLiquidityQuote / addLiquidity:
  read the pool server-side, call the ops, submit. removeLiquidity takes no fresh
  account (the LP holding already exists) and threads the caller-provided
  deadlineMs like the other submits. Public methods → auto-exposed via the
  universal-module dispatch.
2026-08-11 17:51:51 +02:00
r4bbit 64e7614e74 refactor(amm): remove the dead newPosition quote path
Both liquidity branches now quote through the lean ops (liquidityQuote /
addLiquidityQuote), so quoteNewPosition and the heavy amm_quote machinery it
drove are unreachable. Remove them end to end.

FFI (modules/amm/ffi):
- Drop the amm_quote entry point and the whole quote-evaluation graph:
  api/{accounts,commitment,funding,position}.rs, the QuoteRequest /
  PositionRequest / PairSnapshot request types, quote_error::fatal_quote, and
  api/clock.rs (its decode_clock was quote-only). quote.rs keeps only the shared
  opening-deposit math (minimum_opening_pair + helpers) that liquidity_quote
  reuses.
- Trim the fields the quote path was the sole reader of: SelectedHolding.account
  and PairIds.{token_program,twap_program}.
- Drop the quote-path unit tests; keep the math / pair / context / holding /
  swap ones (37 pass, clippy clean).

Module (modules/amm/src):
- Remove AmmModuleImpl::quoteNewPosition and its buildQuoteInput snapshot helper.

App (apps/amm):
- Remove the AmmUiBackend quoteNewPosition slot (.rep/.h/.cpp) and the dead QML
  backend mock + obsolete fresh-quote test.
- finishSubmitFailure no longer keeps a submit-returned re-quote (the lean submit
  ops never return one); it always re-quotes on failure.
- submissionSnapshot drops the always-empty quoteHash and derives the confirm
  dialog's action from the resolved pool state instead of the dead
  quotePayload.instruction (restores the "Create pool" / "Add liquidity" label).
2026-08-11 17:51:51 +02:00
r4bbit eb98aac31f feat(amm): drive the create-pool liquidity preview from liquidityQuote
Both liquidity branches now quote through the lean composable ops. The create
path joins the add path (already on addLiquidityQuote) by reworking
liquidity_quote into a dual-mode create quote and wiring the form to it via
the resolvePool pool read. quoteNewPosition/amm_quote are no longer reached
from the UI.

FFI (modules/amm/ffi):
- liquidity_quote is dual-mode: price-only (initialPriceRealRaw, no amounts)
  returns the minimum opening deposit via minimum_opening_pair; supplied
  amounts return the actual deposit with the price derived from them. Emits
  actual/minimum amounts, expectedLp, lockedLp and the Q64.64 price.
- LiquidityQuoteRequest gains initial_price_real_raw (Option<String>, needed
  only in price-only mode).

Module (modules/amm/src):
- liquidityQuote forwards initialPriceRealRaw to the op.

UI (apps/amm/qml):
- Route create-vs-add on the pool read (resolvePool -> poolExists); create
  quotes via liquidityQuote, assembled into the missing-pool shape the form
  already consumes.
- poolStatus moves off the quote onto the flow's poolExists; the form derives
  activePool/missingPool from it. Trim the vestigial quote fields (canSubmit,
  requiresFreshLp, warnings, errors[], accountPreview, the "Pool" row) and drop
  the account-plan panel for parity with the swap view.
- Fix a real bug: a pair change now resets poolExists (resetPoolExistence) so
  resetPairDraft re-resolves the pool like a fresh selection. Otherwise an
  active pool kept stale (cleared) reserves with no re-quote, and the deposit
  ratio-fill silently no-op'd.

Tests (apps/amm/tests):
- Read activePool instead of the removed poolStatus. The add test waits for the
  reset's active-pool quote to settle (reserves reloaded) before the ratio-fill;
  the create test resets the draft to clear leftover cross-run amounts and the
  stale submitted transactionId.
2026-08-11 17:51:51 +02:00
r4bbit 60e38f4e5f feat(apps/amm): drive add-liquidity quoting from addLiquidityQuote in the UI
Wire the liquidity view's active-pool preview onto the lean addLiquidityQuote +
resolvePool, off the legacy quoteNewPosition. Create-pool quoting stays legacy
for now.

- Expose addLiquidityQuote as a QtRO slot + backend forwarding.
- NewPositionFlow.requestQuoteNow routes on resolvePool.exists (existence, like
  the swap card — no quote-derived poolStatus): active -> addLiquidityQuote,
  assembled into the shape the form consumes (reserves/fee from resolvePool,
  minimumLpRaw from the quote); missing -> legacy quoteNewPosition.
- Drop the obsolete quoteHash gate from canConfirm (the lean quotes are
  stateless).
2026-08-11 17:51:51 +02:00
r4bbit 37f28fe663 feat(modules/amm): add_liquidity_quote takes slippage, returns minimumLpRaw
The add-liquidity quote now takes slippageBps and returns minimumLpRaw =
floor(delta_lp * (10000 - slippage) / 10000) — the LP floor the submit passes as
min_amount_liquidity, mirroring the swap quotes' minReceivedRaw. Computing it in
Rust keeps the u128 slippage math out of the UI. Adds invalid_slippage (>= 100%)
and minimum_lp_zero (slippage leaves no floor) errors; the module's
addLiquidityQuote forwards slippageBps.
2026-08-11 17:51:51 +02:00
r4bbit c95322c033 refactor(modules/amm): resolvePool accepts base58 ids and orients reserves
resolvePool now normalizes base58-or-hex ids (the liquidity view passes base58,
the swap card hex) and orients the returned reserves to the caller's requested
token order — reserveA is the requested tokenA's reserve — instead of the pool's
stored order, which needn't be canonical (e.g. a pool created outside the FFI,
like the testnet setup's). Callers read reserveA/reserveB directly; the swap card
is unaffected (it reads the reserves for display only and never oriented them
itself).
2026-08-11 17:51:51 +02:00
r4bbit b1b6ec8517 refactor(amm): remove the dead submitNewPosition path
Now that add-liquidity and pool creation submit via addLiquidity / createPool,
the legacy submitNewPosition path is dead. Remove it end to end.

App backend (AmmUiBackend):
- Drop the submitNewPosition slot/method and its newPositionError helper.

Module (AmmModuleImpl):
- Drop submitNewPosition, the m_requestPending guard, and the now-orphaned
  nowMs/parseU64 helpers (+ the <chrono> include).

FFI (amm_ffi) — the plan op only submitNewPosition called:
- Delete plan.rs; remove amm_plan (extern + regenerated header), api::plan,
  and PlanRequest.
- Remove the plan-only machinery it fed: QuoteBranch, NewPositionPlan,
  EvaluatedQuote.plan/quote_hash, and AccountPlan's wallet_args /
  requires_fresh_lp / contains / validate_ready, plus the plan construction
  in quote.rs.
- Tests: drop the plan-only tests/helpers; keep quote coverage by trimming
  the mixed tests to their quote assertions.

QML tests (tst_LiquidityPage):
- Remove the two legacy-submit tests (base58-only success) + the
  submitNewPosition mock and its now-unused fixtures. Keep the
  finishSubmitFailure test (unchanged behaviour).

quoteNewPosition and its machinery (amm_quote, buildQuoteInput, PairSnapshot,
AccountPlan preview/sources, commitment) stay — they retire with the legacy
quoting in the quote-migration vertical.
2026-08-11 17:51:51 +02:00
r4bbit 71fb18c50f feat(apps/amm): wire the add-liquidity submit end-to-end
Migrate the active-pool branch of the liquidity form onto the new addLiquidity
op (quoting stays on legacy quoteNewPosition for now, as agreed).

- AmmUiBackend: add the addLiquidity QtRO slot; forwards to the module and
  refreshes balances (mirrors createPool).
- NewPositionFlow: route the active-pool confirm to addLiquidity, minting a
  fresh LP holding then submitting {tokenA/B, holdingA/B, lpHolding, maxAmountA/B,
  minLpRaw, deadline}. minLpRaw comes from the legacy quote's minimumLpRaw.
- NewPositionForm: show the per-side account selectors in add mode; gate the CTA
  on both holdings and on the deposit amounts being present (hasDepositAmounts) —
  the pair's probe quote otherwise reports canSubmit on simulated amounts and
  wrongly enables the button before any amount is entered.
- amm_ffi: fix add_liquidity_plan to orient the (max amount, holding) pair to the
  pool's STORED definition order, not is_canonical_pair — a pool created outside
  the FFI (the testnet setup's spel new-definition) can store a non-canonical
  order, which otherwise sent a holding into the wrong vault (Transfer
  sender/recipient definition mismatch). Test uses a non-canonical pool.

Adds add-liquidity.mjs: asserts the CTA stays disabled with no amounts, submits
an add to the seeded A/B pool, and verifies reserveA grew on-chain.
2026-08-11 17:51:51 +02:00
r4bbit 0eaf51476b feat(modules/amm): add the add-liquidity API and quoting
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.
2026-08-11 12:56:37 +02:00
r4bbit ad020b1c7f docs(token): add token program usage runbook"
A spel-oriented guide for the token program, in the same style as the testnet
runbook but scoped to token operations only: create fungible definitions, hand
out holdings, transfer, mint, burn, rotate/renounce the mint authority, and
print NFTs.

Verified against main (4363f13) on the LEZ v0.2.0 pin: the regenerated IDL
matches artifacts/token-idl.json, all 63 token_program tests pass, and every
quoted abort message exists in programs/token/src.

Documents three constraints that are easy to hit and hard to infer:
- transfer's recipient and mint's holding are not signers, so a fresh account
  cannot be claimed by them — initialize-account first.
- spel has no optional args or optional accounts: Option<account_id> flags are
  always required (pass 'none'), and authority ops are split into self vs
  *-with-authority variants.
- new-definition-with-metadata is unusable from the CLI in spel v0.5.0 and
  v0.6.0 — its serializer has no encoding for IDL 'defined' types.
2026-08-11 12:52:34 +02:00
r4bbit b1b4631234 feat(apps/amm): pick the token account per side when creating a pool
Add per-side account selectors to the create-pool form, sourced from the
wallet's token holdings, so a user holding a token in several accounts chooses
which funds each deposit. A single holding auto-selects.

Mirrors the swap card: the selector lives inside the token input card, below
the token button. AmmTokenAmountSurface gains an optional footer slot (unchanged
when unset, so add-liquidity renders as before); TokenAmountInput fills it with a
ProgramAccountSelector filtered on the token's base58 definitionId.

- LiquidityPage fetches backend.tokenHoldings(), refetching when the wallet opens
- NewPositionForm routes the chosen holdings into submissionSnapshot's createPool
  call; canConfirm now requires both holdings when creating a pool (add-liquidity
  is untouched — it enumerates holdings server-side)
- create-pool.mjs selects the funding account for each side before submitting
2026-08-11 12:04:48 +02:00
r4bbit 86575a65aa test(apps/amm): select the funding account before submitting a swap
canSubmit now requires a chosen holding on both swap sides, so the swap e2e
test must select them first. The selector auto-selects a single holding, but
pick it explicitly (robust to multi-account wallets) via a new selectAccount
step that waits for the holdings to load and selects the first match.

Expose each side's selector via selectorObjectName (swapSell/BuyAccountSelector)
and surface sellHolding/buyHolding in the failure diagnostics.
2026-08-11 12:04:48 +02:00
r4bbit 968001066c feat(apps/amm): pick the token account per swap side
Add a per-slot account selector to the swap card's sell/buy inputs, sourced
from the wallet's token holdings, so a user holding a token in several accounts
chooses which funds each side. A single holding auto-selects.

- SwapPage fetches backend.tokenHoldings(), refetching when the wallet opens
- TokenInput embeds ProgramAccountSelector (half width, right-aligned) in a
  full-width row below the amount/token row; exposes it as selectedHoldingId
- SwapCard drives swapExactInput/Output with the selected holdings and blocks
  submit ("Select token accounts") until both are chosen
- ProgramAccountSelector gains showWhenSingle + textAlignment opt-ins
2026-08-11 12:04:48 +02:00
r4bbit 2a3be1278a feat(modules/amm): add tokenHoldings — list the wallet's token holdings
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.
2026-08-11 12:04:48 +02:00
Ricardo Guilherme Schmidt e0ae3208a1 feat(wallet): add reusable ProgramAccountSelector component
A shared QML select box for choosing a program account (a token holding) that
matches given criteria (accountType + a state field such as the token
definitionId), listing matches with balances and emitting selectionChanged.
Registered in the wallet module's public QML.

Cherry-picked from feat/shared-wallet-token-holding-selector, reduced to the
reusable component only; the AMM swap/liquidity wiring and FFI account source
are reimplemented on the current (post swap + createPool) code.
2026-08-11 12:04:48 +02:00
r4bbit 02e77032b7 test(apps/amm): add create-pool UI e2e test + Token C setup
- setup-amm-testnet.sh: mint a third token (TKC), appended to ACCOUNT_LABELS so
  the deterministic a/b/lp ids don't shift and left unseeded (only A/B is created)
  for the test to create the A/C pair. Split wallet restore from account
  registration (ensure_accounts always runs) so adding a token needs no re-restore.
- create-pool.mjs: drives the Liquidity view to create the A/C pool, then verifies
  it on-chain via the swap card's resolvePool. evaluate() fallbacks for the
  submit/confirm buttons (synthetic clicks aren't reliable on QtQuick Buttons).
- Adds test-hook objectNames (newPositionSubmitButton, liquidityConfirmDialog).
2026-08-11 12:04:48 +02:00
r4bbit e1398ffcad feat(apps/amm): create pools via the new createPool op; drop the pool-watch poll
Route pool creation through the redesigned createPool op and retire the old
create-flow machinery. Add-liquidity stays on the legacy submitNewPosition.

Module (amm_module):
- createPool becomes a single-param envelope: it reads the caller-provided
  lpHoldingId from the request (a new pool has no pre-existing LP holding),
  dropping the requires-fresh-lp handshake — the module never creates wallet
  accounts. Returns { status, error, transactionId }; unlike the swaps, a
  submit failure carries a code so the UI can explain why.

Backend (AmmUiBackend):
- Expose liquidityQuote (read-only preview) and createPool slots. createPool
  guards on the app's wallet-open state, forwards the request, and refreshes
  balances on success. No account creation here.

UI (liquidity flow/form):
- NewPositionFlow.confirm() branches on the missing-pool signal: create ->
  createPool (mint a fresh public LP account via createAccountPublic, then
  submit); add -> unchanged submitNewPosition. Hex transactionId accepted.
- submissionSnapshot supplies canonical-order holdingAId/holdingBId.
- (liquidityQuote is wired to the backend but the create preview still rides
  the legacy quote for now.)

Cleanup: the pool-creation confirmation poll is orphaned now that create no
longer submits via submitNewPosition. It polled the pool account by re-quoting
until poolStatus flipped to active_pool — a stopgap for the missing
transactionStatus/poll_tx on the lez module. Removed pendingPoolProbes,
poolPoller, watchPoolCreation, pollPendingPool, finishPoolProbe,
rotate/removePendingPool, pairKey, matchesSelectedPair,
selectedPoolCreationPending, poolActivated, the poolCreationPending state,
acceptPoolActivation, and their now-obsolete tests.
2026-08-11 12:04:48 +02:00
r4bbit 526d50bff1 feat(modules/amm): add createPool quote + plan ops and module methods
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.
2026-08-11 12:04:47 +02:00
r4bbit 4363f13912 feat(apps/amm): submit exact-output swaps and drop client-side swap math
Wire the Buy direction to submit: AmmUiBackend gains a swapExactOutput slot
(guarded like swapExactInput), SwapCard.executeSwap branches on direction —
sell -> swapExactInput(minReceivedRaw), buy -> swapExactOutput(maxInRaw) — and
canSubmit/submitButtonText/buildSnapshot handle both. The confirmation dialog
switches wording by mode ("You pay at most" / "You receive exactly" for exact
output). The Buy field is now digitsOnly since its value is submitted as a raw
base-units integer.

With both directions priced and oriented by the module, the client no longer
needs any pool math. Remove the reserve-orientation chain (sellIsPoolA,
buy/sellReserveNum, poolReserveA/B, poolDefAHex) — resolvePool now reports only
existence and fee — and the now-dead helpers (formatBaseUnits/formatAmountValue,
DummySwapState.amountInFor/minReceived/priceImpactPercent/maxSent). The
impossible-swap guard moves from a client reserve compare to the module's
output_exceeds_liquidity error, surfaced as "Insufficient liquidity".
2026-08-06 23:15:02 +02:00
r4bbit d9876d08ca feat(apps/amm): drive the exact-output swap preview from swapExactOutQuote
Wire the Buy direction of the swap card to the module's server-side
swapExactOutQuote, mirroring the exact-input path. AmmUiBackend gains a
swapExactOutQuote(tokenIn, tokenOut, amountOutDecimal, slippageBps) slot
returning { requiredInRaw, maxInRaw, priceImpactBps } (read-only).

Editing the Buy amount now debounces a swapExactOutQuote call and sources the
required input (shown in the Sell field), the price impact, and the slippage
ceiling from it — the exact figures come straight from the quote's raw integer
strings, so the preview matches execution and no reserve orientation happens
client-side. A retyped amount invalidates the quote up front.

SwapSummary's last row is generalised from a hardcoded "Min received" to a
direction-aware bound (boundLabel/boundText): "Min received" (min out) for
exact input, "Maximum sent" (max in) for exact output. SwapConfirmationSummary
is updated for the renamed property.

The Buy field remains preview-only — canSubmit is still sell-only, pending the
exact-output submit wiring. The now-orphaned DummySwapState pricing helpers are
left for a follow-up cleanup.
2026-08-06 23:15:02 +02:00
r4bbit 37c2f294ac feat(apps/amm): drive the exact-input swap preview from swapExactInQuote
Wire the Sell direction of the swap card to the module's server-side quote
instead of the client-side DummySwapState estimate. AmmUiBackend gains a
swapExactInQuote(tokenIn, tokenOut, amountInDecimal, slippageBps) slot that
forwards to amm_module and returns { expectedOutRaw, minReceivedRaw,
priceImpactBps } (read-only, no wallet guard).

SwapCard debounces a swapExactInQuote call as the user types a Sell amount and
sources the expected output, min received, and price impact from it. The exact
figures shown (buy field, confirmation snapshot) and the submitted min_out come
straight from the quote's raw integer strings, so the preview can't drift from
execution and the client no longer orients reserves (fixes the #236 defAHex
bug) or recomputes min_out in double/BigInt. A retyped amount invalidates the
quote immediately and blocks submit until the re-quote lands.

The Buy direction is unchanged — still a local DummySwapState preview, pending
the exact-output wiring. resolvePool stays as the source of pool existence, the
fee row, and the Buy-side reserves.
2026-08-06 23:15:01 +02:00
r4bbit 56b2d3a282 feat(modules/amm): add swap_exact_out_plan op and module swapExactOutput
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.
2026-08-06 23:15:01 +02:00
r4bbit b2f0e4b851 refactor(modules/amm): drop status/code from swap_exact_in_plan
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.
2026-08-06 14:31:49 +02:00
r4bbit bf1f76b051 fix(modules/amm): swap plan must use the pool's stored vault ids
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.
2026-08-06 14:31:49 +02:00
r4bbit a389dc6056 refactor(modules/amm): rename swap_plan to swap_exact_in_plan
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.
2026-08-06 14:31:49 +02:00
r4bbit d038b3d060 feat(modules/amm): add swap_exact_out_quote op and module swapExactOutQuote
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.
2026-08-06 14:31:49 +02:00
r4bbit cd8a84374b refactor(amm): extract swap-input formula into amm_core
Lift the inverse constant-product SwapExactOutput math out of the guest's
exact_output_swap_logic into amm_core::swap_exact_out_amounts(amount_out,
reserve_in, reserve_out, fee_bps) -> Option<(effective_in, required_in)>:
ceil(reserve_in * amount_out / (reserve_out - amount_out)) lifted through the
fee via mul_div_ceil. exact_output_swap_logic now calls it and keeps its
nonzero/exceeds-reserve asserts. Behavior-preserving (the panic-message tests
still pass); None (out >= reserve or zero fee multiplier) surfaces as the
existing expect.

Makes the on-chain exact-output pricing one reusable function so the
off-chain quote can produce byte-identical required-input figures instead of
re-deriving the inverse formula. Mirrors swap_exact_in_amounts.
2026-08-06 14:31:49 +02:00
r4bbit abc6d27a9f feat(modules/amm): add swap_exact_in_quote op and module swapExactInQuote
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.
2026-08-06 14:31:49 +02:00
r4bbit c3dc9dd94f refactor(amm): extract swap-output formula into amm_core
Lift the constant-product SwapExactInput math out of the guest's swap_logic
into amm_core::swap_exact_in_amounts(amount_in, reserve_in, reserve_out,
fee_bps) -> (effective_in, amount_out). swap_logic now calls it and keeps its
nonzero input/output asserts. Behavior-preserving (the two panic-message
tests still pass); the only change is that the impossible-overflow expects
become saturating.

Makes the on-chain pricing one reusable function so the off-chain swap quote
can produce byte-identical expected-output figures instead of re-deriving the
formula.
2026-08-05 22:54:37 +02:00
r4bbit 6a951f3cad feat(modules/amm): add pool_id operation
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).
2026-08-05 20:24:24 +02:00
r4bbit de9a3d5320 fix(amm)!: restrict UpdateConfig to authority transfer only
UpdateConfig let the admin rewrite token_program_id and
twap_oracle_program_id in place. Both are immutable deployment
parameters — twap_oracle_program_id derives the current-tick PDA and
every price-observation/price account PDA, and token_program_id is the
program the AMM issues its vault transfers to — so changing either
after any pool exists would orphan every derived account and vault. A
genuine change requires redeploying the AMM, never an in-place edit.

- Instruction::UpdateConfig now carries a single required field,
  new_authority; the two program-id fields are removed.
- update_config assigns the new authority directly (no Option/if-let);
  PDA + admin + signature preconditions unchanged.
- Guest handler and IDL updated to match.
- Drop the integration test that mutated token_program_id (it
  exercised the vulnerability); keep reject-non-admin and
  authority-handoff, and assert program ids survive a transfer.

BREAKING CHANGE: the UpdateConfig instruction ABI changed — the
token_program_id and twap_oracle_program_id fields are removed and
new_authority is now required (was Option). Any client constructing
UpdateConfig must be updated. The instruction enum change also alters
the program ImageID: redeploy and update every ImageID-derived value
(deployed program ids, client/config files, PDA-derived addresses,
AMM/ATA program-id inputs) before submitting
2026-08-05 18:08:43 +02:00
Andrea Franz 200f429ec6 test(amm-ui): feed setup password via stdin so bootstrap never blocks 2026-08-05 17:19:38 +02:00
Andrea Franz c5b9508e4d docs(amm-ui): clarify test wallet password (throwaway setup vs restore-keys) 2026-08-05 17:19:38 +02:00
Andrea Franz 0576b10dcb test(amm-ui): feed wallet setup password non-interactively 2026-08-05 17:19:38 +02:00
Andrea Franz 191942f2a8 docs(amm-ui): document isolated UI test flow in tests/README.md 2026-08-05 17:19:38 +02:00