From 464a7136be95d20752cc0a299856846b184ea58a Mon Sep 17 00:00:00 2001 From: r4bbit <445106+0x-r4bbit@users.noreply.github.com> Date: Mon, 10 Aug 2026 11:05:41 +0200 Subject: [PATCH] feat(apps/amm): drive add-liquidity quoting from addLiquidityQuote in the UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .../components/liquidity/NewPositionForm.qml | 1 - apps/amm/qml/state/NewPositionFlow.qml | 101 +++++++++++++++++- apps/amm/src/AmmUiBackend.cpp | 7 ++ apps/amm/src/AmmUiBackend.h | 2 + apps/amm/src/AmmUiBackend.rep | 9 ++ 5 files changed, 115 insertions(+), 5 deletions(-) diff --git a/apps/amm/qml/components/liquidity/NewPositionForm.qml b/apps/amm/qml/components/liquidity/NewPositionForm.qml index b7cb27e..a7e9db7 100644 --- a/apps/amm/qml/components/liquidity/NewPositionForm.qml +++ b/apps/amm/qml/components/liquidity/NewPositionForm.qml @@ -106,7 +106,6 @@ AmmActionCard { readonly property bool canConfirm: root.quotePayload.status === "ok" && root.quotePayload.canSubmit === true && root.quoteMatchesPair() - && String(root.quotePayload.quoteHash || "").length > 0 && root.holdingsReady && root.hasDepositAmounts && !root.contextLoading diff --git a/apps/amm/qml/state/NewPositionFlow.qml b/apps/amm/qml/state/NewPositionFlow.qml index 6d882be..2b0f5ef 100644 --- a/apps/amm/qml/state/NewPositionFlow.qml +++ b/apps/amm/qml/state/NewPositionFlow.qml @@ -164,6 +164,73 @@ QtObject { return } + // Route on pool existence (read the pool account), like the swap card. resolvePool + // returns the reserves oriented to our requested token order (reserveA is tokenAId's). + root.runtime.watch(root.backend.resolvePool(built.request.tokenAId, built.request.tokenBId), + function(pool) { + if (serial !== root.quoteSerial) + return + if (pool && pool.exists) { + root.requestAddQuote(serial, built, pool) + return + } + // resolvePool returns exists:false for BOTH the normal "no pool yet" case and + // hard failures (no_program_bin, amm_not_initialized, bad_config). Only the + // former — an empty error or no_pool — is a create-pool signal; surface any other + // pool.error as a quote error instead of masking it as a create quote (which + // would hide the backend failure and enable the wrong flow). + var poolError = pool ? String(pool.error || "") : "" + if (poolError.length === 0 || poolError === "no_pool") { + root.requestCreateQuote(serial, built) + } else { + root.quoteLoading = false + root.quoteStale = false + root.quoteErrorCode = "" + root.newPositionQuote = root.quoteError(poolError) + } + }, + function(error) { + if (serial !== root.quoteSerial) + return + root.quoteLoading = false + root.quoteStale = true + root.quoteErrorCode = "backend_error" + }) + } + + // Add-liquidity preview via the lean addLiquidityQuote; reserves + fee come from the + // resolvePool read. The result is assembled into the shape the form already consumes. + function requestAddQuote(serial, built, pool) { + root.runtime.watch(root.backend.addLiquidityQuote({ + "tokenAId": built.request.tokenAId, + "tokenBId": built.request.tokenBId, + "maxAmountARaw": built.request.maxAmountARaw, + "maxAmountBRaw": built.request.maxAmountBRaw, + "slippageBps": built.request.slippageBps + }), + function(quote) { + if (serial !== root.quoteSerial) + return + root.quoteLoading = false + root.quoteStale = false + root.quoteErrorCode = "" + if (quote && quote.status === "ok") + root.newPositionQuote = root.assembleAddQuote(built, pool, quote) + else + root.newPositionQuote = root.quoteError((quote && quote.error) || "backend_error") + }, + function(error) { + if (serial !== root.quoteSerial) + return + root.quoteLoading = false + root.quoteStale = true + root.quoteErrorCode = "backend_error" + }) + } + + // Create-pool preview still on the legacy quoteNewPosition — migrated to createPoolQuote + // (the create counterpart of addLiquidityQuote) in a later step. + function requestCreateQuote(serial, built) { root.runtime.watch(root.backend.quoteNewPosition(built.request), function(quote) { if (serial !== root.quoteSerial) @@ -185,6 +252,31 @@ QtObject { }) } + // Maps addLiquidityQuote + the pool read into the quote shape NewPositionForm reads for an + // active pool. Amounts/reserves are in the request's (canonical) order, matching the form's + // displayIsCanonical mapping; minimumLpRaw is the slippage floor the module computed. + function assembleAddQuote(built, pool, quote) { + return { + "status": "ok", + "poolStatus": "active_pool", + "canSubmit": true, + "tokenAId": built.request.tokenAId, + "tokenBId": built.request.tokenBId, + "actualAmountARaw": String(quote.amountARaw || "0"), + "actualAmountBRaw": String(quote.amountBRaw || "0"), + "expectedLpRaw": String(quote.expectedLpRaw || "0"), + "minimumLpRaw": String(quote.minimumLpRaw || "0"), + "reserveARaw": String(pool.reserveA || "0"), + "reserveBRaw": String(pool.reserveB || "0"), + "poolFeeBps": pool.feeBps, + "requiresFreshLp": true, + "initialPriceRealRaw": String(quote.priceRaw || "0"), + "errors": [], + "warnings": [], + "accountPreview": [] + } + } + function confirm(snapshot) { if (root.submitting) return @@ -199,7 +291,8 @@ QtObject { // Route by pool state: creation (initialPriceRealRaw is set only on the missing-pool // path) goes through createPool; the active-pool branch through addLiquidity. Both // mint a fresh LP holding then submit via the lean module ops (hex ids, - // caller-provided accounts). Quoting stays on the legacy quoteNewPosition for now. + // caller-provided accounts). Add quoting is now on addLiquidityQuote; create quoting + // stays on the legacy quoteNewPosition until createPoolQuote is wired. if (snapshot.request.initialPriceRealRaw !== undefined) root.createPool(snapshot) else @@ -259,9 +352,9 @@ QtObject { } // Add liquidity to an existing pool via the new addLiquidity op. Like createPool a fresh - // LP holding receives the minted LP, so create one then submit. The submit is priced off - // the legacy quote's maxAmounts + minimumLpRaw (quoting stays legacy for now; the - // module's addLiquidityQuote is built but unwired). No confirmation poll yet. + // LP holding receives the minted LP, so create one then submit. The submit reuses the + // addLiquidityQuote result (maxAmounts + minimumLpRaw) carried on the snapshot. No + // confirmation poll yet. function addLiquidity(snapshot) { root.runtime.watch(root.backend.createAccountPublic(), function(lpId) { diff --git a/apps/amm/src/AmmUiBackend.cpp b/apps/amm/src/AmmUiBackend.cpp index 5cacdbf..a28fa9e 100644 --- a/apps/amm/src/AmmUiBackend.cpp +++ b/apps/amm/src/AmmUiBackend.cpp @@ -237,6 +237,13 @@ QVariantMap AmmUiBackend::liquidityQuote(QVariantMap request) return m_logos->amm_module.liquidityQuote(request); } +QVariantMap AmmUiBackend::addLiquidityQuote(QVariantMap request) +{ + // Read-only add-liquidity preview — no wallet guard. The module reads the pool and + // ratio-matches the deposit server-side from the two max amounts. + return m_logos->amm_module.addLiquidityQuote(request); +} + QVariantList AmmUiBackend::tokenHoldings() { // Read-only list of the wallet's token holdings for the account selector. Gated diff --git a/apps/amm/src/AmmUiBackend.h b/apps/amm/src/AmmUiBackend.h index 238badf..c501145 100644 --- a/apps/amm/src/AmmUiBackend.h +++ b/apps/amm/src/AmmUiBackend.h @@ -75,6 +75,8 @@ public slots: // createAccountPublic() — so createPool forwards to the module and creates no wallet // accounts here. QVariantMap liquidityQuote(QVariantMap request) override; + // Read-only add-liquidity preview (forwards to the module). + QVariantMap addLiquidityQuote(QVariantMap request) override; QVariantMap createPool(QVariantMap request) override; // Add-liquidity submit. Forwards to the module; the flow supplies a fresh LP // holding in the request (the backend creates no wallet accounts here). diff --git a/apps/amm/src/AmmUiBackend.rep b/apps/amm/src/AmmUiBackend.rep index f6eaf77..04159e5 100644 --- a/apps/amm/src/AmmUiBackend.rep +++ b/apps/amm/src/AmmUiBackend.rep @@ -102,6 +102,15 @@ class AmmUiBackend // Read-only, no submission (the fee is not needed — it isn't part of the pool // PDA nor the pricing). SLOT(QVariantMap liquidityQuote(QVariantMap request)) + // Server-side add-liquidity preview from the two max deposit amounts. `request` + // carries { tokenAId, tokenBId, maxAmountARaw, maxAmountBRaw, slippageBps } (ids hex or + // base58). Reads the pool and returns { status:"ok", error:"", amountARaw, amountBRaw + // (the actual ratio-matched deposits, display order), expectedLpRaw, minimumLpRaw (the + // slippage floor on the LP minted — the submit's min_amount_liquidity), priceRaw }. On + // failure { status:"error", error: } — no_pool, pair_mismatch, invalid_token_id, + // invalid_slippage, amount_too_low, minimum_lp_zero, bad_amount, backend_error. + // Read-only, no submission. + SLOT(QVariantMap addLiquidityQuote(QVariantMap request)) // Submits a NewDefinition transaction creating the pool for the request's pair. // `request` carries { tokenAId, tokenBId, holdingAId, holdingBId, lpHoldingId, // amountARaw, amountBRaw, feeBps, deadlineMs } (ids hex or base58; amounts/deadline