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.
This commit is contained in:
Ricardo Guilherme Schmidt 2026-07-15 21:24:56 -03:00 committed by r4bbit
parent 01829a280c
commit aafe5e900b
2 changed files with 55 additions and 52 deletions

View File

@ -109,13 +109,13 @@ AmmActionCard {
implicitHeight: content.implicitHeight + root.contentPadding * 2 implicitHeight: content.implicitHeight + root.contentPadding * 2
implicitWidth: 480 implicitWidth: 480
Component.onCompleted: Qt.callLater(root.ensurePair) Component.onCompleted: Qt.callLater(root.reconcileSelection)
onNewPositionContextChanged: Qt.callLater(root.applyContextChange) onNewPositionContextChanged: Qt.callLater(root.applyContextChange)
function applyContextChange() { function applyContextChange() {
if (root.resolvingToken) if (root.resolvingToken)
root.finishTokenResolution() root.finishTokenResolution()
else else
root.ensurePair() root.reconcileSelection()
} }
onQuotePayloadChanged: { onQuotePayloadChanged: {
if (root.quoteStale) if (root.quoteStale)
@ -786,26 +786,23 @@ AmmActionCard {
root.tokenResolutionErrorSide = side root.tokenResolutionErrorSide = side
} }
function ensurePair() { function reconcileSelection() {
var status = String(root.newPositionContext.status || "") var status = String(root.newPositionContext.status || "")
if (status !== "ready" && status !== "no_wallet") if (status !== "ready" && status !== "no_wallet")
return return
var previousA = root.selectedTokenAId var previousA = root.selectedTokenAId
var previousB = root.selectedTokenBId var previousB = root.selectedTokenBId
var selectable = root.selectableTokenIds() var selectable = root.selectableTokenIds()
if (selectable.length === 0) { if (root.selectedTokenAId.length > 0
&& selectable.indexOf(root.selectedTokenAId) < 0) {
root.selectedTokenAId = "" root.selectedTokenAId = ""
}
if (root.selectedTokenBId.length > 0
&& selectable.indexOf(root.selectedTokenBId) < 0) {
root.selectedTokenBId = "" 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) if (root.selectedTokenAId !== previousA || root.selectedTokenBId !== previousB)
root.resetPairDraft() root.resetPairDraft()
else if (root.hasPair) else if (root.hasPair)
@ -987,14 +984,7 @@ AmmActionCard {
return { "ok": false, "errors": errors, "request": ({}) } return { "ok": false, "errors": errors, "request": ({}) }
} }
var canonicalAId = root.displayIsCanonical ? root.selectedTokenAId : root.selectedTokenBId var request = root.pairRequest()
var canonicalBId = root.displayIsCanonical ? root.selectedTokenBId : root.selectedTokenAId
var request = {
"schema": "new-position.v1",
"tokenAId": canonicalAId,
"tokenBId": canonicalBId,
"feeBps": root.selectedFeeBps
}
if (root.activePool) { if (root.activePool) {
var parsedA = AmountMath.parseHuman(root.amountA, root.decimalsA) var parsedA = AmountMath.parseHuman(root.amountA, root.decimalsA)
@ -1085,6 +1075,17 @@ AmmActionCard {
return { "ok": errors.length === 0, "errors": errors, "request": request } 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) { function requestQuote(immediate) {
if (root.activePool && root.amountA.length === 0 && root.amountB.length === 0) { if (root.activePool && root.amountA.length === 0 && root.amountB.length === 0) {
root.localErrors = [] root.localErrors = []
@ -1327,24 +1328,18 @@ AmmActionCard {
return return
if (root.poolFeeBps > 0 && root.selectedFeeBps !== root.poolFeeBps) { if (root.poolFeeBps > 0 && root.selectedFeeBps !== root.poolFeeBps) {
root.selectedFeeBps = root.poolFeeBps root.selectedFeeBps = root.poolFeeBps
if (root.amountA.length === 0 && root.amountB.length === 0) { root.localErrors = []
root.amountA = AmountMath.formatRaw( root.quoteRequested(true, {
root.probeRaw(root.tokenA, root.decimalsA), root.decimalsA) "ok": true,
root.amountB = AmountMath.formatRaw( "errors": [],
root.probeRaw(root.tokenB, root.decimalsB), root.decimalsB) "request": root.poolProbeRequest(root.pairRequest())
} })
root.requestQuote(true)
return return
} }
if (root.quotePayload.status !== "ok") if (root.quotePayload.status !== "ok")
return 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) { if (root.missingPool) {
var rawA = root.displayRaw("actualAmountARaw", "actualAmountBRaw", "A") var rawA = root.displayRaw("actualAmountARaw", "actualAmountBRaw", "A")
var rawB = root.displayRaw("actualAmountARaw", "actualAmountBRaw", "B") var rawB = root.displayRaw("actualAmountARaw", "actualAmountBRaw", "B")

View File

@ -91,17 +91,33 @@ TestCase {
} }
} }
function createForm() { function createEmptyForm(context) {
var form = createTemporaryObject(formComponent, testCase, { var form = createTemporaryObject(formComponent, testCase, {
"flowState": flowState(({})) "flowState": flowState(({})),
"newPositionContext": context || readyContext()
}) })
verify(form) verify(form)
wait(0) 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.selectedTokenAId, tokenLow)
compare(form.selectedTokenBId, tokenHigh) compare(form.selectedTokenBId, tokenHigh)
return form return form
} }
function test_walletTokensDoNotPrefillPosition() {
var form = createEmptyForm()
compare(form.selectedTokenAId, "")
compare(form.selectedTokenBId, "")
compare(form.amountA, "")
compare(form.amountB, "")
}
function test_displayOrderMapsToCanonicalRequestAfterSwap() { function test_displayOrderMapsToCanonicalRequestAfterSwap() {
var form = createForm() var form = createForm()
var built = form.buildQuoteRequest() var built = form.buildQuoteRequest()
@ -159,12 +175,7 @@ TestCase {
} }
function test_missingPoolAcceptsLargeDirectAmountsFromEitherSide() { function test_missingPoolAcceptsLargeDirectAmountsFromEitherSide() {
var form = createTemporaryObject(formComponent, testCase, { var form = createForm(rawAmountsContext())
"flowState": flowState(({})),
"newPositionContext": rawAmountsContext()
})
verify(form)
wait(0)
form.priceAmountA = "15" form.priceAmountA = "15"
form.priceAmountB = "10" form.priceAmountB = "10"
form.flowState = flowState({ form.flowState = flowState({
@ -199,12 +210,7 @@ TestCase {
} }
function test_missingPoolRoundsPairedRawAmounts() { function test_missingPoolRoundsPairedRawAmounts() {
var form = createTemporaryObject(formComponent, testCase, { var form = createForm(rawAmountsContext())
"flowState": flowState(({})),
"newPositionContext": rawAmountsContext()
})
verify(form)
wait(0)
form.priceAmountA = "15" form.priceAmountA = "15"
form.priceAmountB = "10" form.priceAmountB = "10"
form.flowState = flowState({ form.flowState = flowState({
@ -415,14 +421,14 @@ TestCase {
var form = createForm() var form = createForm()
form.flowState = flowState(quote) form.flowState = flowState(quote)
wait(0) wait(0)
compare(form.amountA, "20") compare(form.amountA, "")
compare(form.amountB, "4") compare(form.amountB, "")
quoteRequestedSpy.target = form quoteRequestedSpy.target = form
quoteRequestedSpy.clear() quoteRequestedSpy.clear()
form.editActiveAmount("A", "5") form.editActiveAmount("A", "5")
compare(form.amountA, "5") compare(form.amountA, "5")
compare(form.amountB, "4") compare(form.amountB, "")
compare(quoteRequestedSpy.count, 0) compare(quoteRequestedSpy.count, 0)
form.finishActiveAmount("A", "5") form.finishActiveAmount("A", "5")
@ -509,6 +515,8 @@ TestCase {
wait(0) wait(0)
compare(form.selectedFeeBps, 5) compare(form.selectedFeeBps, 5)
compare(form.amountA, "")
compare(form.amountB, "")
compare(quoteRequestedSpy.count, 1) compare(quoteRequestedSpy.count, 1)
verify(quoteRequestedSpy.signalArguments[0][1].ok) verify(quoteRequestedSpy.signalArguments[0][1].ok)
compare(quoteRequestedSpy.signalArguments[0][1].request.maxAmountARaw, "5000000000") compare(quoteRequestedSpy.signalArguments[0][1].request.maxAmountARaw, "5000000000")
@ -582,8 +590,8 @@ TestCase {
} }
wait(0) wait(0)
compare(form.selectedTokenAId, tokenHigh) compare(form.selectedTokenAId, "")
compare(form.selectedTokenBId, tokenThird) compare(form.selectedTokenBId, tokenHigh)
compare(form.amountA, "") compare(form.amountA, "")
compare(form.amountB, "") compare(form.amountB, "")
compare(form.minimumAmountARaw, "") compare(form.minimumAmountARaw, "")