From e1398ffcad22bffe1ef85eeb2ac1c594e1a07d80 Mon Sep 17 00:00:00 2001 From: r4bbit <445106+0x-r4bbit@users.noreply.github.com> Date: Fri, 7 Aug 2026 14:19:21 +0200 Subject: [PATCH] feat(apps/amm): create pools via the new createPool op; drop the pool-watch poll MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../components/liquidity/NewPositionForm.qml | 30 ++-- apps/amm/qml/pages/LiquidityPage.qml | 4 - apps/amm/qml/state/NewPositionFlow.qml | 156 +++++++----------- apps/amm/src/AmmUiBackend.cpp | 27 +++ apps/amm/src/AmmUiBackend.h | 6 + apps/amm/src/AmmUiBackend.rep | 22 +++ apps/amm/tests/qml/tst_LiquidityPage.qml | 104 ------------ apps/amm/tests/qml/tst_NewPositionForm.qml | 30 ---- 8 files changed, 125 insertions(+), 254 deletions(-) diff --git a/apps/amm/qml/components/liquidity/NewPositionForm.qml b/apps/amm/qml/components/liquidity/NewPositionForm.qml index 0c2f6a5..624f309 100644 --- a/apps/amm/qml/components/liquidity/NewPositionForm.qml +++ b/apps/amm/qml/components/liquidity/NewPositionForm.qml @@ -48,7 +48,6 @@ AmmActionCard { readonly property bool quoteLoading: root.flowState.quoteLoading === true readonly property bool submitting: root.flowState.submitting === true readonly property bool quoteStale: root.flowState.quoteStale === true - readonly property bool poolCreationPending: root.flowState.poolCreationPending === true readonly property string submitError: root.flowState.errorCode ? root.issueText(root.flowState.errorCode) : "" readonly property string transactionId: String(root.flowState.transactionId || "") @@ -95,7 +94,11 @@ AmmActionCard { && !root.quoteLoading && !root.quoteStale && !root.submitting - && !root.poolCreationPending + // A create-pool tx for this pair is already in flight + // (transactionId set) but the pool still reads missing until + // the chain processes it — block confirm so a stale + // missing_pool quote can't submit a duplicate NewDefinition. + && !(root.missingPool && root.transactionId.length > 0) signal quoteRequested(bool immediate, var quoteRequest) signal confirmationRequested(var snapshot) @@ -600,7 +603,6 @@ AmmActionCard { text: root.submitting ? qsTr("Submitting…") : root.contextLoading ? qsTr("Loading…") - : root.poolCreationPending ? qsTr("Waiting for pool") : root.missingPool ? qsTr("Create pool") : qsTr("Add liquidity") enabled: root.canConfirm onClicked: root.confirmationRequested(root.submissionSnapshot()) @@ -856,22 +858,6 @@ AmmActionCard { root.requestQuote(true) } - function acceptPoolActivation(quote) { - if (!quote || quote.status !== "ok" - || quote.poolStatus !== "active_pool" - || !root.quoteMatchesSelectedPair(quote)) { - return false - } - root.confirmedPoolStatus = "active_pool" - root.activePoolQuote = quote - root.amountA = "" - root.amountB = "" - root.minimumAmountARaw = "" - root.minimumAmountBRaw = "" - root.localErrors = [] - return true - } - function noteDraftChanged() { root.draftChanged() } @@ -1449,8 +1435,12 @@ AmmActionCard { var built = root.buildQuoteRequest() return { "request": built.request, - "poolProbeRequest": root.poolProbeRequest(built.request), "quoteHash": String(root.quotePayload.quoteHash || ""), + // Canonical-order holdings for the create path's createPool call: the + // request's tokenAId/amountARaw are canonical, so holdingAId must be the + // canonical token A's holding too (createPool re-canonicalizes as a no-op). + "holdingAId": String((root.displayIsCanonical ? root.tokenA : root.tokenB).holdingId || ""), + "holdingBId": String((root.displayIsCanonical ? root.tokenB : root.tokenA).holdingId || ""), "pairText": qsTr("%1 / %2").arg(root.shortTokenName(root.tokenA)).arg(root.shortTokenName(root.tokenB)), "feeText": root.feeLabel(root.selectedFeeBps), "depositAText": root.quoteAmount("actualAmountARaw", "actualAmountBRaw", "A"), diff --git a/apps/amm/qml/pages/LiquidityPage.qml b/apps/amm/qml/pages/LiquidityPage.qml index 297b6d9..9a3cc1b 100644 --- a/apps/amm/qml/pages/LiquidityPage.qml +++ b/apps/amm/qml/pages/LiquidityPage.qml @@ -248,10 +248,6 @@ Item { form.failTokenResolution(code) } - function onPoolActivated(quote) { - form.acceptPoolActivation(quote) - } - function onQuoteRefreshRequested(immediate) { form.requestQuote(immediate) } diff --git a/apps/amm/qml/state/NewPositionFlow.qml b/apps/amm/qml/state/NewPositionFlow.qml index 087e5f7..ce0babe 100644 --- a/apps/amm/qml/state/NewPositionFlow.qml +++ b/apps/amm/qml/state/NewPositionFlow.qml @@ -20,7 +20,6 @@ QtObject { "quoteLoading": root.quoteLoading, "quoteStale": root.quoteStale, "submitting": root.submitting, - "poolCreationPending": root.selectedPoolCreationPending(), "transactionId": root.transactionId, "errorCode": root.flowErrorCode || root.contextErrorCode || root.quoteErrorCode @@ -39,12 +38,9 @@ QtObject { property string contextErrorCode: "" property string quoteErrorCode: "" property var pendingQuoteRequest: ({ "ok": false, "request": ({}) }) - property var pendingPoolProbes: [] - property bool poolProbeInFlight: false signal tokenResolutionFinished(bool finalResponse) signal tokenResolutionFailed(string code) - signal poolActivated(var quote) signal quoteRefreshRequested(bool immediate) signal submitSucceeded signal submitFailed @@ -57,13 +53,6 @@ QtObject { onTriggered: root.requestQuoteNow(root.quoteSerial) } - property Timer poolPoller: Timer { - interval: 5000 - repeat: true - running: root.pendingPoolProbes.length > 0 - onTriggered: root.pollPendingPool() - } - onNewPositionContextChanged: root.invalidateQuote() onWalletStateReadyChanged: { @@ -207,13 +196,19 @@ QtObject { return } + // Pool creation (initialPriceRealRaw is set only on the missing-pool path) goes + // through the new createPool op — hex ids, caller-provided accounts. Add-liquidity + // keeps the legacy submitNewPosition. + if (snapshot.request.initialPriceRealRaw !== undefined) { + root.createPool(snapshot) + return + } + root.runtime.watch(root.backend.submitNewPosition(snapshot.request, snapshot.quoteHash), function(result) { if (result && result.status === "submitted" && /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test( String(result.transactionId || ""))) { - if (snapshot.request.initialPriceRealRaw !== undefined) - root.watchPoolCreation(snapshot.poolProbeRequest, result.deadlineMs) root.submitting = false root.transactionId = result.transactionId root.flowErrorCode = "" @@ -230,6 +225,58 @@ QtObject { }) } + // Create a pool via the new createPool op. A new pool has no pre-existing LP + // holding, so the caller provides a fresh account: create one, then submit. No + // confirmation poll yet (transactionStatus is pending an upstream dependency). + function createPool(snapshot) { + root.runtime.watch(root.backend.createAccountPublic(), + function(lpId) { + if (!lpId || String(lpId).length === 0) { + root.finishSubmitFailure(root.quoteError("wallet_submission_failed")) + return + } + root.submitCreatePool(snapshot, String(lpId)) + }, + function(error) { + root.finishSubmitFailure(root.quoteError("wallet_submission_failed")) + }) + } + + function submitCreatePool(snapshot, lpHoldingId) { + var request = { + "tokenAId": snapshot.request.tokenAId, + "tokenBId": snapshot.request.tokenBId, + "holdingAId": snapshot.holdingAId, + "holdingBId": snapshot.holdingBId, + "lpHoldingId": lpHoldingId, + "amountARaw": snapshot.request.amountARaw, + "amountBRaw": snapshot.request.amountBRaw, + "feeBps": snapshot.request.feeBps, + // u64-max sentinel = no deadline, same as the swap submits. + "deadlineMs": "18446744073709551615" + } + root.runtime.watch(root.backend.createPool(request), + function(result) { + if (result && result.status === "ok" + && String(result.transactionId || "").length > 0) { + root.submitting = false + root.transactionId = String(result.transactionId) + root.flowErrorCode = "" + root.contextErrorCode = "" + root.quoteErrorCode = "" + root.invalidateQuote() + root.submitSucceeded() + return + } + var code = result && result.error ? String(result.error) + : "wallet_submission_failed" + root.finishSubmitFailure(root.quoteError(code)) + }, + function(error) { + root.finishSubmitFailure(root.quoteError("wallet_submission_failed")) + }) + } + function finishSubmitFailure(result) { root.submitting = false const hasFreshQuote = result && result.quote @@ -247,89 +294,6 @@ QtObject { root.scheduleQuote(true, root.pendingQuoteRequest) } - function watchPoolCreation(request, deadlineMs) { - const key = root.pairKey(request) - let deadline = Number(deadlineMs) - if (!isFinite(deadline) || deadline <= 0) - deadline = 0 - const pending = root.pendingPoolProbes.filter(function(item) { - return item.key !== key - }) - pending.push({ - "key": key, - "request": request, - "deadlineMs": deadline - }) - root.pendingPoolProbes = pending - Qt.callLater(root.pollPendingPool) - } - - function pollPendingPool() { - if (root.poolProbeInFlight || root.pendingPoolProbes.length === 0 - || !root.walletStateReady || root.runtime === null) { - return - } - const pending = root.pendingPoolProbes[0] - root.poolProbeInFlight = true - root.runtime.watch(root.backend.quoteNewPosition(pending.request), - function(quote) { - root.finishPoolProbe(pending, quote) - }, - function(error) { - root.finishPoolProbe(pending, null) - }) - } - - function finishPoolProbe(pending, quote) { - root.poolProbeInFlight = false - if (quote && quote.poolStatus === "active_pool") { - root.removePendingPool(pending.key) - if (root.matchesSelectedPair(pending.request)) { - root.poolActivated(quote) - root.invalidateQuote() - root.refreshContext(true) - } - return - } - if (pending.deadlineMs > 0 && Date.now() >= pending.deadlineMs) { - root.removePendingPool(pending.key) - return - } - root.rotatePendingPool() - } - - function pairKey(request) { - return String(request.tokenAId || "") + ":" + String(request.tokenBId || "") - } - - function matchesSelectedPair(request) { - return root.pairKey(root.pendingQuoteRequest.request || {}) === root.pairKey(request) - } - - function selectedPoolCreationPending() { - const request = root.pendingQuoteRequest.request || {} - if (!request.tokenAId || !request.tokenBId) - return false - const selected = root.pairKey(request) - return root.pendingPoolProbes.some(function(item) { - return item.key === selected - }) - } - - function removePendingPool(key) { - root.pendingPoolProbes = root.pendingPoolProbes.filter(function(item) { - return item.key !== key - }) - } - - function rotatePendingPool() { - if (root.pendingPoolProbes.length < 2) - return - const pending = root.pendingPoolProbes.slice(1) - pending.push(root.pendingPoolProbes[0]) - root.pendingPoolProbes = pending - } - function draftChanged() { root.invalidateQuote() root.transactionId = "" diff --git a/apps/amm/src/AmmUiBackend.cpp b/apps/amm/src/AmmUiBackend.cpp index 0cabae4..328553a 100644 --- a/apps/amm/src/AmmUiBackend.cpp +++ b/apps/amm/src/AmmUiBackend.cpp @@ -271,3 +271,30 @@ QVariantList AmmUiBackend::tokenList() { return m_logos->amm_module.tokenList(); } + +QVariantMap AmmUiBackend::liquidityQuote(QVariantMap request) +{ + // Read-only create-pool preview — no wallet guard. The module prices the opening + // LP and price server-side from the two deposit amounts. + return m_logos->amm_module.liquidityQuote(request); +} + +QVariantMap AmmUiBackend::createPool(QVariantMap request) +{ + // Same connected-state submit guard as the swaps — this app's lock is + // authoritative even though the shared wallet may remain open elsewhere. + if (!isWalletOpen()) + return QVariantMap { + { QStringLiteral("status"), QStringLiteral("error") }, + { QStringLiteral("error"), QStringLiteral("wallet_unavailable") }, + }; + + // The caller supplies the fresh LP holding in the request; the backend forwards to + // the module (it creates no wallet accounts) and refreshes balances on a successful + // submit. + const QVariantMap result = m_logos->amm_module.createPool(request); + if (result.value(QStringLiteral("status")).toString() == QStringLiteral("ok") + && !result.value(QStringLiteral("transactionId")).toString().isEmpty()) + refreshBalances(); + return result; +} diff --git a/apps/amm/src/AmmUiBackend.h b/apps/amm/src/AmmUiBackend.h index 3ddef29..196a2a4 100644 --- a/apps/amm/src/AmmUiBackend.h +++ b/apps/amm/src/AmmUiBackend.h @@ -71,6 +71,12 @@ public slots: // Reads the token list from TOKENS_CONFIG (via the module) so the Swap UI's // token picker is config-driven instead of hardcoded. QVariantList tokenList() override; + // Create-pool preview (liquidityQuote, read-only) and submit (createPool). The caller + // supplies lpHoldingId in the request — a fresh account it created via + // createAccountPublic() — so createPool forwards to the module and creates no wallet + // accounts here. + QVariantMap liquidityQuote(QVariantMap request) override; + QVariantMap createPool(QVariantMap request) override; private: void syncWalletState(); diff --git a/apps/amm/src/AmmUiBackend.rep b/apps/amm/src/AmmUiBackend.rep index 7cdbf1c..b270256 100644 --- a/apps/amm/src/AmmUiBackend.rep +++ b/apps/amm/src/AmmUiBackend.rep @@ -92,4 +92,26 @@ class AmmUiBackend // a QVariantList of QVariantMap entries. Returns an empty list if // TOKENS_CONFIG is unset/unreadable/invalid. SLOT(QVariantList tokenList()) + + // Server-side create-pool preview from the two deposit amounts. `request` + // carries { tokenAId, tokenBId, amountARaw, amountBRaw } (ids hex or base58; + // amounts decimal-string base units). Returns { status:"ok", error:"", + // amountARaw, amountBRaw, expectedLpRaw, lockedLpRaw, initialPriceRaw } — the + // LP the creator receives and the opening price, via the shared on-chain math. + // On failure { status:"error", error: } — invalid_token_id, + // same_token_pair, amount_too_low, amount_required, bad_amount, backend_error. + // 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)) + // 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 + // decimal strings). A new pool has no existing LP holding, so the caller supplies + // lpHoldingId — a fresh account it created via createAccountPublic(); the backend + // just forwards to the module and creates no wallet accounts here. Returns + // { status:"ok", error:"", transactionId: } on success, else + // { status:"error", error: } (wallet_unavailable, config_missing, + // invalid_account_id, bad_amount, bad_fee_bps_amount, invalid_fee_tier, + // wallet_submission_failed, backend_error). + SLOT(QVariantMap createPool(QVariantMap request)) } diff --git a/apps/amm/tests/qml/tst_LiquidityPage.qml b/apps/amm/tests/qml/tst_LiquidityPage.qml index 8374c60..adc723a 100644 --- a/apps/amm/tests/qml/tst_LiquidityPage.qml +++ b/apps/amm/tests/qml/tst_LiquidityPage.qml @@ -191,44 +191,6 @@ TestCase { compare(page.flow.submitting, false) } - function test_base58MissingPoolSubmissionStartsPoolWatch() { - var backend = createTemporaryObject(backendComponent, testCase, { - "walletStateReady": true, - "submitResult": { - "status": "submitted", - "transactionId": submittedTransactionId, - "deadlineMs": String(Date.now() + 60000) - } - }) - var runtime = createTemporaryObject(runtimeComponent, testCase) - var page = createTemporaryObject(pageComponent, testCase, { - "backend": backend, - "runtime": runtime - }) - verify(backend) - verify(runtime) - verify(page) - - var probe = { - "tokenAId": "22222222222222222222222222222222", - "tokenBId": "33333333333333333333333333333333" - } - page.flow.pendingQuoteRequest = { "ok": true, "request": probe } - page.flow.confirm({ - "request": { - "initialPriceRealRaw": "18446744073709551616" - }, - "poolProbeRequest": probe, - "quoteHash": "sha256:expected" - }) - wait(0) - - compare(page.flow.transactionId, submittedTransactionId) - compare(page.flow.pendingPoolProbes.length, 1) - compare(page.flow.selectedPoolCreationPending(), true) - compare(page.flow.poolProbeInFlight, false) - } - function test_nativeHexSubmittedResultDoesNotEnterSuccessState() { var backend = createTemporaryObject(backendComponent, testCase, { "walletStateReady": true, @@ -257,70 +219,4 @@ TestCase { compare(page.flow.submitting, false) } - function test_poolProbeDoesNotPublishProbeAmountsAsCurrentQuote() { - var backend = createTemporaryObject(backendComponent, testCase, { - "walletStateReady": true - }) - var page = createTemporaryObject(pageComponent, testCase, { - "backend": backend - }) - verify(backend) - verify(page) - - var request = { - "tokenAId": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", - "tokenBId": "22222222222222222222222222222222" - } - var pending = { "key": page.flow.pairKey(request), "request": request } - page.flow.pendingQuoteRequest = { "ok": true, "request": request } - page.flow.pendingPoolProbes = [pending] - page.flow.newPositionQuote = { - "status": "ok", - "poolStatus": "missing_pool", - "tokenAId": request.tokenAId, - "tokenBId": request.tokenBId - } - page.flow.quoteStale = false - - page.flow.finishPoolProbe(pending, { - "status": "ok", - "poolStatus": "active_pool", - "tokenAId": request.tokenAId, - "tokenBId": request.tokenBId - }) - - compare(page.flow.pendingPoolProbes.length, 0) - compare(page.flow.newPositionQuote.poolStatus, "missing_pool") - verify(page.flow.quoteStale) - } - - function test_poolProbeStopsBlockingAfterTransactionDeadline() { - var backend = createTemporaryObject(backendComponent, testCase, { - "walletStateReady": true - }) - var page = createTemporaryObject(pageComponent, testCase, { - "backend": backend - }) - verify(backend) - verify(page) - - var request = { - "tokenAId": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", - "tokenBId": "22222222222222222222222222222222" - } - var pending = { - "key": page.flow.pairKey(request), - "request": request, - "deadlineMs": Date.now() - 1 - } - page.flow.pendingQuoteRequest = { "ok": true, "request": request } - page.flow.pendingPoolProbes = [pending] - page.flow.poolProbeInFlight = true - - page.flow.finishPoolProbe(pending, null) - - compare(page.flow.pendingPoolProbes.length, 0) - compare(page.flow.poolProbeInFlight, false) - verify(!page.flow.selectedPoolCreationPending()) - } } diff --git a/apps/amm/tests/qml/tst_NewPositionForm.qml b/apps/amm/tests/qml/tst_NewPositionForm.qml index 28c9a31..cf6c06a 100644 --- a/apps/amm/tests/qml/tst_NewPositionForm.qml +++ b/apps/amm/tests/qml/tst_NewPositionForm.qml @@ -345,36 +345,6 @@ TestCase { compare(form.amountA, "2") } - function test_poolActivationClearsCreationDraftWithoutPublishingProbeAmounts() { - var form = createForm() - form.confirmedPoolStatus = "missing_pool" - form.amountA = "3" - form.amountB = "2" - form.minimumAmountARaw = "3" - form.minimumAmountBRaw = "2000000" - - verify(form.acceptPoolActivation({ - "status": "ok", - "tokenAId": tokenHigh, - "tokenBId": tokenLow, - "poolStatus": "active_pool", - "reserveARaw": "3000000", - "reserveBRaw": "2" - })) - - verify(form.activePool) - compare(form.activePoolQuote.poolStatus, "active_pool") - compare(form.amountA, "") - compare(form.amountB, "") - compare(form.minimumAmountARaw, "") - compare(form.minimumAmountBRaw, "") - quoteRequestedSpy.target = form - quoteRequestedSpy.clear() - form.requestQuote(true) - compare(quoteRequestedSpy.count, 0) - compare(form.localErrors.length, 0) - } - function test_staleQuoteErrorsDoNotMarkCurrentDraft() { var quote = { "status": "ok",