diff --git a/amm-ui/qml/SwapCard.qml b/amm-ui/qml/SwapCard.qml index d2efde4..3ae4e52 100644 --- a/amm-ui/qml/SwapCard.qml +++ b/amm-ui/qml/SwapCard.qml @@ -11,7 +11,9 @@ Rectangle { property var tokens: [] property var sellToken: null property var buyToken: null - property string sellAmount: "" + property string sellInput: "" + property string buyInput: "" + property string editingSide: "sell" property real slippageTolerancePercent: 0.5 DummySwapState { @@ -28,27 +30,44 @@ Rectangle { } function resetAmounts() { - root.sellAmount = "" + root.sellInput = "" + root.buyInput = "" + root.editingSide = "sell" } readonly property real sellReserve: sellToken ? (sellToken.reserve || 0) : 0 readonly property real buyReserve: buyToken ? (buyToken.reserve || 0) : 0 - readonly property real parsedSellAmount: { - var amt = parseFloat(sellAmount) + readonly property real parsedSellInput: { + var amt = parseFloat(sellInput) return isNaN(amt) || amt < 0 ? 0 : amt } - readonly property real parsedBuyAmount: swapState.amountOutFor(parsedSellAmount, sellReserve, buyReserve) + readonly property real parsedBuyInput: { + var amt = parseFloat(buyInput) + return isNaN(amt) || amt < 0 ? 0 : amt + } + + readonly property real parsedSellAmount: editingSide === "sell" + ? parsedSellInput + : swapState.amountInFor(parsedBuyInput, sellReserve, buyReserve) + + readonly property real parsedBuyAmount: editingSide === "buy" + ? parsedBuyInput + : swapState.amountOutFor(parsedSellInput, sellReserve, buyReserve) + readonly property real feeAmount: swapState.feeAmount(parsedSellAmount) readonly property real minReceivedAmount: swapState.minReceived(parsedBuyAmount, slippageTolerancePercent) readonly property real priceImpactPercent: swapState.priceImpactPercent(parsedSellAmount, parsedBuyAmount, sellReserve, buyReserve) - readonly property bool hasAmount: parsedSellAmount > 0 + 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 hasAmount: editingSide === "sell" ? parsedSellInput > 0 : parsedBuyInput > 0 readonly property bool tokensSelected: sellToken !== null && buyToken !== null readonly property bool insufficientBalance: hasAmount && sellToken !== null && parsedSellAmount > (sellToken.balance || 0) readonly property bool insufficientLiquidity: hasAmount && buyToken !== null && parsedBuyAmount > (buyToken.reserve || 0) - readonly property bool canSubmit: tokensSelected && hasAmount && !insufficientBalance && !insufficientLiquidity + readonly property bool canSubmit: tokensSelected && hasAmount && parsedSellAmount > 0 && parsedBuyAmount > 0 && !insufficientBalance && !insufficientLiquidity readonly property string submitButtonText: { if (!hasAmount || !tokensSelected) return qsTr("Enter an amount") @@ -63,21 +82,22 @@ Rectangle { return val.toFixed(8) } - readonly property string buyAmount: { - if (!sellToken || !buyToken || sellAmount === "") return "" - if (parsedSellAmount <= 0) return "" - return formatAmountValue(parsedBuyAmount) - } + readonly property string sellDisplay: editingSide === "sell" + ? sellInput + : (parsedSellAmount > 0 ? formatAmountValue(parsedSellAmount) : "") + + readonly property string buyDisplay: editingSide === "buy" + ? buyInput + : (parsedBuyAmount > 0 ? formatAmountValue(parsedBuyAmount) : "") readonly property string sellUsd: { - if (!sellToken || sellAmount === "") return "" - if (parsedSellAmount <= 0) return "" + if (!sellToken || parsedSellAmount <= 0) return "" var val = parsedSellAmount * sellToken.usdPrice return "~$" + val.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",") } readonly property string buyUsd: { - if (!buyToken || buyAmount === "") return "" + if (!buyToken || parsedBuyAmount <= 0) return "" var val = parsedBuyAmount * buyToken.usdPrice return "~$" + val.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",") } @@ -92,7 +112,9 @@ Rectangle { "feeAmount": swapState.formatTokenAmount(feeAmount, sellToken ? sellToken.symbol : ""), "priceImpactPercent": swapState.formatPercent(priceImpactPercent), "priceImpactPercentValue": priceImpactPercent, - "slippageTolerance": swapState.formatSlippagePercent(slippageTolerancePercent) + "slippageTolerance": swapState.formatSlippagePercent(slippageTolerancePercent), + "swapMode": swapMode, + "swapModeText": swapModeText } } @@ -117,11 +139,14 @@ Rectangle { Layout.fillWidth: true theme: root.theme label: "Sell" - amount: root.sellAmount + amount: root.sellDisplay usdValue: root.sellUsd token: root.sellToken - readOnly: false - onInputEdited: function(v) { root.sellAmount = v } + active: root.editingSide === "sell" + onInputEdited: function(v) { + root.sellInput = v + if (root.editingSide !== "sell") root.editingSide = "sell" + } onTokenClicked: root.requestTokenSelect("sell") } @@ -170,10 +195,14 @@ Rectangle { Layout.fillWidth: true theme: root.theme label: "Buy" - amount: root.buyAmount + amount: root.buyDisplay usdValue: root.buyUsd token: root.buyToken - readOnly: true + active: root.editingSide === "buy" + onInputEdited: function(v) { + root.buyInput = v + if (root.editingSide !== "buy") root.editingSide = "buy" + } onTokenClicked: root.requestTokenSelect("buy") } @@ -184,6 +213,7 @@ Rectangle { Layout.rightMargin: 16 theme: root.theme visible: root.tokensSelected && root.hasAmount + swapModeText: root.swapModeText feeText: swapState.formatTokenAmount(root.feeAmount, root.sellToken ? root.sellToken.symbol : "") priceImpactText: swapState.formatPercent(root.priceImpactPercent) priceImpactPercent: root.priceImpactPercent diff --git a/amm-ui/qml/TokenInput.qml b/amm-ui/qml/TokenInput.qml index 86193b7..07b2362 100644 --- a/amm-ui/qml/TokenInput.qml +++ b/amm-ui/qml/TokenInput.qml @@ -9,7 +9,7 @@ Rectangle { property string amount: "" property string usdValue: "" property var token: null - property bool readOnly: false + property bool active: true signal tokenClicked() signal inputEdited(string newValue) @@ -18,11 +18,10 @@ Rectangle { target: tiInput property: "text" value: root.amount - when: root.readOnly } radius: 16 - color: theme.colors.inputBg + color: root.active ? theme.colors.inputBg : theme.colors.panelBg implicitHeight: 110 Behavior on color { ColorAnimation { duration: 300 } } @@ -52,13 +51,12 @@ Rectangle { TextInput { id: tiInput anchors.fill: parent - color: theme.colors.textPrimary + color: root.active ? theme.colors.textPrimary : theme.colors.textSecondary font.pixelSize: 36 font.weight: Font.Bold - readOnly: root.readOnly selectionColor: theme.colors.selection clip: true - onTextChanged: { if (!root.readOnly) root.inputEdited(text) } + onTextEdited: root.inputEdited(text) validator: RegularExpressionValidator { regularExpression: /^[0-9]*\.?[0-9]*$/ } diff --git a/amm-ui/qml/components/SwapConfirmationDialog.qml b/amm-ui/qml/components/SwapConfirmationDialog.qml index eb55751..54f61b4 100644 --- a/amm-ui/qml/components/SwapConfirmationDialog.qml +++ b/amm-ui/qml/components/SwapConfirmationDialog.qml @@ -144,6 +144,7 @@ FocusScope { SwapSummary { Layout.fillWidth: true theme: root.theme + swapModeText: root.snapshot.swapModeText || "" feeText: root.snapshot.feeAmount || "" priceImpactText: root.snapshot.priceImpactPercent || "" priceImpactPercent: Number(root.snapshot.priceImpactPercentValue) || 0 diff --git a/amm-ui/qml/components/SwapSummary.qml b/amm-ui/qml/components/SwapSummary.qml index 99e467d..4bc4750 100644 --- a/amm-ui/qml/components/SwapSummary.qml +++ b/amm-ui/qml/components/SwapSummary.qml @@ -5,6 +5,7 @@ Item { id: root property var theme + property string swapModeText: "" property string feeText: "" property string priceImpactText: "" property real priceImpactPercent: 0 @@ -25,6 +26,30 @@ Item { anchors.fill: parent spacing: 8 + Item { + implicitHeight: 18 + visible: root.swapModeText.length > 0 + + Layout.fillWidth: true + + Text { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + color: root.theme.colors.textSecondary + font.pixelSize: 12 + text: qsTr("Type of swap") + } + + Text { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + color: root.theme.colors.textPrimary + font.bold: true + font.pixelSize: 12 + text: root.swapModeText + } + } + Item { implicitHeight: 18 diff --git a/amm-ui/qml/state/DummySwapState.qml b/amm-ui/qml/state/DummySwapState.qml index b3382a5..3194620 100644 --- a/amm-ui/qml/state/DummySwapState.qml +++ b/amm-ui/qml/state/DummySwapState.qml @@ -30,6 +30,22 @@ QtObject { return safeReserveOut * amountInAfterFee / (safeReserveIn + amountInAfterFee); } + function amountInFor(amountOut, reserveIn, reserveOut) { + const safeAmountOut = parseAmount(amountOut); + const safeReserveIn = parseAmount(reserveIn); + const safeReserveOut = parseAmount(reserveOut); + + if (safeAmountOut <= 0 || safeReserveIn <= 0 || safeReserveOut <= 0) { + return 0; + } + if (safeAmountOut >= safeReserveOut) { + return 0; + } + + const amountInAfterFee = safeAmountOut * safeReserveIn / (safeReserveOut - safeAmountOut); + return amountInAfterFee * 10000 / (10000 - root.feeBps); + } + function priceImpactPercent(amountIn, amountOut, reserveIn, reserveOut) { const safeAmountIn = parseAmount(amountIn); const safeAmountOut = parseAmount(amountOut);