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).
This commit is contained in:
r4bbit
2026-08-11 13:14:59 +02:00
parent 12b6df4546
commit 464a7136be
5 changed files with 115 additions and 5 deletions
@@ -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
+97 -4
View File
@@ -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) {
+7
View File
@@ -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
+2
View File
@@ -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).
+9
View File
@@ -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:<code> } — 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