From 57855e6073c3e1c3bf75aceed0038872276193b0 Mon Sep 17 00:00:00 2001 From: Ricardo Guilherme Schmidt <3esmit@gmail.com> Date: Wed, 15 Jul 2026 21:24:56 -0300 Subject: [PATCH] fix(amm-ui): require explicit liquidity inputs Keep wallet assets as token choices without automatically selecting a pair. Keep pool-probe amounts internal so active-pool quotes do not populate the form before user input. --- .../components/liquidity/NewPositionForm.qml | 61 +++++++++---------- apps/amm/tests/qml/tst_NewPositionForm.qml | 46 ++++++++------ 2 files changed, 55 insertions(+), 52 deletions(-) diff --git a/apps/amm/qml/components/liquidity/NewPositionForm.qml b/apps/amm/qml/components/liquidity/NewPositionForm.qml index c70000f..9cd9896 100644 --- a/apps/amm/qml/components/liquidity/NewPositionForm.qml +++ b/apps/amm/qml/components/liquidity/NewPositionForm.qml @@ -109,13 +109,13 @@ AmmActionCard { implicitHeight: content.implicitHeight + root.contentPadding * 2 implicitWidth: 480 - Component.onCompleted: Qt.callLater(root.ensurePair) + Component.onCompleted: Qt.callLater(root.reconcileSelection) onNewPositionContextChanged: Qt.callLater(root.applyContextChange) function applyContextChange() { if (root.resolvingToken) root.finishTokenResolution() else - root.ensurePair() + root.reconcileSelection() } onQuotePayloadChanged: { if (root.quoteStale) @@ -786,26 +786,23 @@ AmmActionCard { root.tokenResolutionErrorSide = side } - function ensurePair() { + function reconcileSelection() { var status = String(root.newPositionContext.status || "") if (status !== "ready" && status !== "no_wallet") return var previousA = root.selectedTokenAId var previousB = root.selectedTokenBId var selectable = root.selectableTokenIds() - if (selectable.length === 0) { + if (root.selectedTokenAId.length > 0 + && selectable.indexOf(root.selectedTokenAId) < 0) { root.selectedTokenAId = "" + } + if (root.selectedTokenBId.length > 0 + && selectable.indexOf(root.selectedTokenBId) < 0) { root.selectedTokenBId = "" - if (previousA.length > 0 || previousB.length > 0) - root.resetPairDraft() - return - } - if (selectable.indexOf(root.selectedTokenAId) < 0) - root.selectedTokenAId = selectable[0] - if (selectable.indexOf(root.selectedTokenBId) < 0 - || root.selectedTokenBId === root.selectedTokenAId) { - root.selectedTokenBId = selectable.length > 1 ? selectable[1] : "" } + if (root.selectedTokenBId === root.selectedTokenAId) + root.selectedTokenBId = "" if (root.selectedTokenAId !== previousA || root.selectedTokenBId !== previousB) root.resetPairDraft() else if (root.hasPair) @@ -987,14 +984,7 @@ AmmActionCard { return { "ok": false, "errors": errors, "request": ({}) } } - var canonicalAId = root.displayIsCanonical ? root.selectedTokenAId : root.selectedTokenBId - var canonicalBId = root.displayIsCanonical ? root.selectedTokenBId : root.selectedTokenAId - var request = { - "schema": "new-position.v1", - "tokenAId": canonicalAId, - "tokenBId": canonicalBId, - "feeBps": root.selectedFeeBps - } + var request = root.pairRequest() if (root.activePool) { var parsedA = AmountMath.parseHuman(root.amountA, root.decimalsA) @@ -1085,6 +1075,17 @@ AmmActionCard { return { "ok": errors.length === 0, "errors": errors, "request": request } } + function pairRequest() { + return { + "schema": "new-position.v1", + "tokenAId": root.displayIsCanonical + ? root.selectedTokenAId : root.selectedTokenBId, + "tokenBId": root.displayIsCanonical + ? root.selectedTokenBId : root.selectedTokenAId, + "feeBps": root.selectedFeeBps + } + } + function requestQuote(immediate) { if (root.activePool && root.amountA.length === 0 && root.amountB.length === 0) { root.localErrors = [] @@ -1327,24 +1328,18 @@ AmmActionCard { return if (root.poolFeeBps > 0 && root.selectedFeeBps !== root.poolFeeBps) { root.selectedFeeBps = root.poolFeeBps - if (root.amountA.length === 0 && root.amountB.length === 0) { - root.amountA = AmountMath.formatRaw( - root.probeRaw(root.tokenA, root.decimalsA), root.decimalsA) - root.amountB = AmountMath.formatRaw( - root.probeRaw(root.tokenB, root.decimalsB), root.decimalsB) - } - root.requestQuote(true) + root.localErrors = [] + root.quoteRequested(true, { + "ok": true, + "errors": [], + "request": root.poolProbeRequest(root.pairRequest()) + }) return } if (root.quotePayload.status !== "ok") return - if (root.activePool && root.amountA.length === 0 && root.amountB.length === 0) { - root.amountA = AmountMath.formatRaw(root.displayRaw("maxAmountARaw", "maxAmountBRaw", "A"), root.decimalsA) - root.amountB = AmountMath.formatRaw(root.displayRaw("maxAmountARaw", "maxAmountBRaw", "B"), root.decimalsB) - } - if (root.missingPool) { var rawA = root.displayRaw("actualAmountARaw", "actualAmountBRaw", "A") var rawB = root.displayRaw("actualAmountARaw", "actualAmountBRaw", "B") diff --git a/apps/amm/tests/qml/tst_NewPositionForm.qml b/apps/amm/tests/qml/tst_NewPositionForm.qml index 083be1f..d2a8192 100644 --- a/apps/amm/tests/qml/tst_NewPositionForm.qml +++ b/apps/amm/tests/qml/tst_NewPositionForm.qml @@ -91,17 +91,33 @@ TestCase { } } - function createForm() { + function createEmptyForm(context) { var form = createTemporaryObject(formComponent, testCase, { - "flowState": flowState(({})) + "flowState": flowState(({})), + "newPositionContext": context || readyContext() }) verify(form) wait(0) + return form + } + + function createForm(context) { + var form = createEmptyForm(context) + form.selectToken("A", tokenLow) + form.selectToken("B", tokenHigh) compare(form.selectedTokenAId, tokenLow) compare(form.selectedTokenBId, tokenHigh) return form } + function test_walletTokensDoNotPrefillPosition() { + var form = createEmptyForm() + compare(form.selectedTokenAId, "") + compare(form.selectedTokenBId, "") + compare(form.amountA, "") + compare(form.amountB, "") + } + function test_displayOrderMapsToCanonicalRequestAfterSwap() { var form = createForm() var built = form.buildQuoteRequest() @@ -159,12 +175,7 @@ TestCase { } function test_missingPoolAcceptsLargeDirectAmountsFromEitherSide() { - var form = createTemporaryObject(formComponent, testCase, { - "flowState": flowState(({})), - "newPositionContext": rawAmountsContext() - }) - verify(form) - wait(0) + var form = createForm(rawAmountsContext()) form.priceAmountA = "15" form.priceAmountB = "10" form.flowState = flowState({ @@ -199,12 +210,7 @@ TestCase { } function test_missingPoolRoundsPairedRawAmounts() { - var form = createTemporaryObject(formComponent, testCase, { - "flowState": flowState(({})), - "newPositionContext": rawAmountsContext() - }) - verify(form) - wait(0) + var form = createForm(rawAmountsContext()) form.priceAmountA = "15" form.priceAmountB = "10" form.flowState = flowState({ @@ -415,14 +421,14 @@ TestCase { var form = createForm() form.flowState = flowState(quote) wait(0) - compare(form.amountA, "20") - compare(form.amountB, "4") + compare(form.amountA, "") + compare(form.amountB, "") quoteRequestedSpy.target = form quoteRequestedSpy.clear() form.editActiveAmount("A", "5") compare(form.amountA, "5") - compare(form.amountB, "4") + compare(form.amountB, "") compare(quoteRequestedSpy.count, 0) form.finishActiveAmount("A", "5") @@ -509,6 +515,8 @@ TestCase { wait(0) compare(form.selectedFeeBps, 5) + compare(form.amountA, "") + compare(form.amountB, "") compare(quoteRequestedSpy.count, 1) verify(quoteRequestedSpy.signalArguments[0][1].ok) compare(quoteRequestedSpy.signalArguments[0][1].request.maxAmountARaw, "5000000000") @@ -582,8 +590,8 @@ TestCase { } wait(0) - compare(form.selectedTokenAId, tokenHigh) - compare(form.selectedTokenBId, tokenThird) + compare(form.selectedTokenAId, "") + compare(form.selectedTokenBId, tokenHigh) compare(form.amountA, "") compare(form.amountB, "") compare(form.minimumAmountARaw, "")