From 0e6a38742db0b6dacfd43e9583fa94ed0294d271 Mon Sep 17 00:00:00 2001 From: Ricardo Guilherme Schmidt <3esmit@gmail.com> Date: Fri, 17 Jul 2026 20:43:36 -0300 Subject: [PATCH] fix(amm): apply slippage to exact-output input --- apps/amm/qml/components/swap/SwapCard.qml | 16 ++++-- .../swap/SwapConfirmationSummary.qml | 29 +++++++--- apps/amm/qml/components/swap/SwapSummary.qml | 9 ++-- apps/amm/tests/qml/tst_SwapPage.qml | 54 +++++++++++++++++++ 4 files changed, 94 insertions(+), 14 deletions(-) diff --git a/apps/amm/qml/components/swap/SwapCard.qml b/apps/amm/qml/components/swap/SwapCard.qml index 4583099..f27434e 100644 --- a/apps/amm/qml/components/swap/SwapCard.qml +++ b/apps/amm/qml/components/swap/SwapCard.qml @@ -52,10 +52,16 @@ Rectangle { readonly property real feeAmount: swapState.feeAmount(parsedSellAmount) readonly property real minReceivedAmount: swapState.minReceived(parsedBuyAmount, slippageTolerancePercent) + readonly property real maxSentAmount: swapState.maxSent(parsedSellAmount, slippageTolerancePercent) readonly property real priceImpactPercent: swapState.priceImpactPercent(parsedSellAmount, parsedBuyAmount, sellReserve, buyReserve) - readonly property string swapMode: editingSide === "buy" ? "swap-exact-output" : "swap-exact-input" - readonly property string swapModeText: editingSide === "buy" ? qsTr("Exact output") : qsTr("Exact input") + readonly property bool exactOutput: editingSide === "buy" + readonly property string swapMode: exactOutput ? "swap-exact-output" : "swap-exact-input" + readonly property string swapModeText: exactOutput ? qsTr("Exact output") : qsTr("Exact input") + readonly property string limitLabel: exactOutput ? qsTr("Max sent") : qsTr("Min received") + readonly property string limitText: exactOutput + ? swapState.formatTokenAmount(maxSentAmount, sellToken ? sellToken.symbol : "") + : swapState.formatTokenAmount(minReceivedAmount, buyToken ? buyToken.symbol : "") readonly property bool hasAmount: editingSide === "sell" ? parsedSellInput > 0 : parsedBuyInput > 0 readonly property bool tokensSelected: sellToken !== null && buyToken !== null @@ -102,7 +108,8 @@ Rectangle { "buyToken": buyToken ? buyToken.symbol : "", "sellAmount": formatAmountValue(parsedSellAmount), "buyAmount": formatAmountValue(parsedBuyAmount), - "minReceived": formatAmountValue(minReceivedAmount), + "minReceived": exactOutput ? "" : formatAmountValue(minReceivedAmount), + "maxSent": exactOutput ? formatAmountValue(maxSentAmount) : "", "feeAmount": swapState.formatTokenAmount(feeAmount, sellToken ? sellToken.symbol : ""), "priceImpactPercent": swapState.formatPercent(priceImpactPercent), "priceImpactPercentValue": priceImpactPercent, @@ -211,7 +218,8 @@ Rectangle { feeText: swapState.formatTokenAmount(root.feeAmount, root.sellToken ? root.sellToken.symbol : "") priceImpactText: swapState.formatPercent(root.priceImpactPercent) priceImpactPercent: root.priceImpactPercent - minReceivedText: swapState.formatTokenAmount(root.minReceivedAmount, root.buyToken ? root.buyToken.symbol : "") + limitLabel: root.limitLabel + limitText: root.limitText } SlippageToleranceControl { diff --git a/apps/amm/qml/components/swap/SwapConfirmationSummary.qml b/apps/amm/qml/components/swap/SwapConfirmationSummary.qml index c3ff22e..1d801b9 100644 --- a/apps/amm/qml/components/swap/SwapConfirmationSummary.qml +++ b/apps/amm/qml/components/swap/SwapConfirmationSummary.qml @@ -22,16 +22,22 @@ ColumnLayout { spacing: 4 Text { + objectName: "swapPayLabel" Layout.fillWidth: true - text: qsTr("You pay") + text: root.snapshot.swapMode === "swap-exact-output" + ? qsTr("You pay at most") + : qsTr("You pay") color: root.theme.colors.textSecondary font.pixelSize: 12 } Text { + objectName: "swapPayAmount" Layout.fillWidth: true text: qsTr("%1 %2") - .arg(root.snapshot.sellAmount || "") + .arg(root.snapshot.swapMode === "swap-exact-output" + ? root.snapshot.maxSent || "" + : root.snapshot.sellAmount || "") .arg(root.snapshot.sellToken || "") color: root.theme.colors.textPrimary font.bold: true @@ -54,16 +60,22 @@ ColumnLayout { spacing: 4 Text { + objectName: "swapReceiveLabel" Layout.fillWidth: true - text: qsTr("You receive at least") + text: root.snapshot.swapMode === "swap-exact-output" + ? qsTr("You receive") + : qsTr("You receive at least") color: root.theme.colors.textSecondary font.pixelSize: 12 } Text { + objectName: "swapReceiveAmount" Layout.fillWidth: true text: qsTr("%1 %2") - .arg(root.snapshot.minReceived || "") + .arg(root.snapshot.swapMode === "swap-exact-output" + ? root.snapshot.buyAmount || "" + : root.snapshot.minReceived || "") .arg(root.snapshot.buyToken || "") color: root.theme.colors.textPrimary font.bold: true @@ -81,8 +93,11 @@ ColumnLayout { priceImpactText: root.snapshot.priceImpactPercent || "" priceImpactPercent: Number(root.snapshot.priceImpactPercentValue) || 0 slippageText: root.snapshot.slippageTolerance || "" - minReceivedText: qsTr("%1 %2") - .arg(root.snapshot.minReceived || "") - .arg(root.snapshot.buyToken || "") + limitLabel: root.snapshot.swapMode === "swap-exact-output" + ? qsTr("Max sent") + : qsTr("Min received") + limitText: root.snapshot.swapMode === "swap-exact-output" + ? qsTr("%1 %2").arg(root.snapshot.maxSent || "").arg(root.snapshot.sellToken || "") + : qsTr("%1 %2").arg(root.snapshot.minReceived || "").arg(root.snapshot.buyToken || "") } } diff --git a/apps/amm/qml/components/swap/SwapSummary.qml b/apps/amm/qml/components/swap/SwapSummary.qml index 6ca2285..584b1a5 100644 --- a/apps/amm/qml/components/swap/SwapSummary.qml +++ b/apps/amm/qml/components/swap/SwapSummary.qml @@ -10,7 +10,8 @@ Item { property string priceImpactText: "" property real priceImpactPercent: 0 property string slippageText: "" - property string minReceivedText: "" + property string limitLabel: qsTr("Min received") + property string limitText: "" readonly property color priceImpactColor: { if (root.priceImpactPercent > 5) return "#F08A76"; @@ -126,20 +127,22 @@ Item { Layout.fillWidth: true Text { + objectName: "swapLimitLabel" anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter color: root.theme.colors.textSecondary font.pixelSize: 12 - text: qsTr("Min received") + text: root.limitLabel } Text { + objectName: "swapLimitValue" anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter color: root.theme.colors.textPrimary font.bold: true font.pixelSize: 12 - text: root.minReceivedText + text: root.limitText } } } diff --git a/apps/amm/tests/qml/tst_SwapPage.qml b/apps/amm/tests/qml/tst_SwapPage.qml index bd6f66b..1675e77 100644 --- a/apps/amm/tests/qml/tst_SwapPage.qml +++ b/apps/amm/tests/qml/tst_SwapPage.qml @@ -53,5 +53,59 @@ Item { tryCompare(dialog, "opened", false) compare(card.sellInput, "1") } + + function test_exactOutputUsesMaximumInputLimit() { + const page = createTemporaryObject(pageComponent, root) + verify(page) + + const card = findChild(page, "swapCard") + const dialog = findChild(page, "swapPreviewDialog") + verify(card) + verify(dialog) + + card.setToken("sell", page.tokens[0]) + card.setToken("buy", page.tokens[1]) + card.slippageTolerancePercent = 10 + card.buyInput = "1000" + card.editingSide = "buy" + tryCompare(card, "canSubmit", true) + + const snapshot = card.buildSnapshot() + compare(snapshot.swapMode, "swap-exact-output") + compare(snapshot.buyAmount, "1000.00") + compare(snapshot.minReceived, "") + verify(snapshot.maxSent.length > 0) + verify(Number(snapshot.maxSent) > Number(snapshot.sellAmount)) + + dialog.openWithSnapshot(snapshot) + tryCompare(dialog, "opened", true) + tryCompare(findChild(dialog, "swapPayLabel"), "text", "You pay at most") + compare(findChild(dialog, "swapPayAmount").text, snapshot.maxSent + " " + snapshot.sellToken) + compare(findChild(dialog, "swapReceiveLabel").text, "You receive") + compare(findChild(dialog, "swapReceiveAmount").text, snapshot.buyAmount + " " + snapshot.buyToken) + compare(findChild(dialog, "swapLimitLabel").text, "Max sent") + compare(findChild(dialog, "swapLimitValue").text, snapshot.maxSent + " " + snapshot.sellToken) + } + + function test_exactInputUsesMinimumOutputLimit() { + const page = createTemporaryObject(pageComponent, root) + verify(page) + + const card = findChild(page, "swapCard") + verify(card) + card.setToken("sell", page.tokens[0]) + card.setToken("buy", page.tokens[1]) + card.slippageTolerancePercent = 10 + card.sellInput = "1" + card.editingSide = "sell" + tryCompare(card, "canSubmit", true) + + const snapshot = card.buildSnapshot() + compare(snapshot.swapMode, "swap-exact-input") + compare(snapshot.sellAmount, "1.00") + compare(snapshot.maxSent, "") + verify(snapshot.minReceived.length > 0) + verify(Number(snapshot.minReceived) < Number(snapshot.buyAmount)) + } } }