diff --git a/amm-ui/qml/SwapCard.qml b/amm-ui/qml/SwapCard.qml index 9419246..d2efde4 100644 --- a/amm-ui/qml/SwapCard.qml +++ b/amm-ui/qml/SwapCard.qml @@ -1,6 +1,8 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 +import "components" +import "state" Rectangle { id: root @@ -11,7 +13,11 @@ Rectangle { property var buyToken: null property string sellAmount: "" property real slippageTolerancePercent: 0.5 - readonly property real feePercent: 0.30 + + DummySwapState { + id: swapState + feeBps: 30 + } signal requestTokenSelect(string side) signal submitRequested(var snapshot) @@ -25,24 +31,18 @@ Rectangle { root.sellAmount = "" } + 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) return isNaN(amt) || amt < 0 ? 0 : amt } - readonly property real parsedBuyAmount: { - if (!sellToken || !buyToken || parsedSellAmount <= 0) return 0 - return parsedSellAmount * sellToken.usdPrice / buyToken.usdPrice - } - - readonly property real minReceivedAmount: parsedBuyAmount * (1 - slippageTolerancePercent / 100) - - readonly property real priceImpactPercent: { - if (!sellToken || parsedSellAmount <= 0) return 0 - var reserve = sellToken.reserve || 0 - if (reserve <= 0) return 0 - return parsedSellAmount / (reserve + parsedSellAmount) * 100 - } + readonly property real parsedBuyAmount: swapState.amountOutFor(parsedSellAmount, 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 bool tokensSelected: sellToken !== null && buyToken !== null @@ -89,9 +89,10 @@ Rectangle { "sellAmount": formatAmountValue(parsedSellAmount), "buyAmount": formatAmountValue(parsedBuyAmount), "minReceived": formatAmountValue(minReceivedAmount), - "feePercent": feePercent.toFixed(2) + "%", - "priceImpactPercent": priceImpactPercent < 0.01 ? "<0.01%" : priceImpactPercent.toFixed(2) + "%", - "slippageTolerance": slippageTolerancePercent.toFixed(2).replace(/0+$/, "").replace(/[.]$/, "") + "%" + "feeAmount": swapState.formatTokenAmount(feeAmount, sellToken ? sellToken.symbol : ""), + "priceImpactPercent": swapState.formatPercent(priceImpactPercent), + "priceImpactPercentValue": priceImpactPercent, + "slippageTolerance": swapState.formatSlippagePercent(slippageTolerancePercent) } } @@ -176,6 +177,20 @@ Rectangle { onTokenClicked: root.requestTokenSelect("buy") } + SwapSummary { + Layout.fillWidth: true + Layout.topMargin: 12 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + theme: root.theme + visible: root.tokensSelected && root.hasAmount + feeText: swapState.formatTokenAmount(root.feeAmount, root.sellToken ? root.sellToken.symbol : "") + priceImpactText: swapState.formatPercent(root.priceImpactPercent) + priceImpactPercent: root.priceImpactPercent + slippageText: swapState.formatSlippagePercent(root.slippageTolerancePercent) + minReceivedText: swapState.formatTokenAmount(root.minReceivedAmount, root.buyToken ? root.buyToken.symbol : "") + } + Rectangle { id: ctaBox Layout.fillWidth: true diff --git a/amm-ui/qml/components/SwapConfirmationDialog.qml b/amm-ui/qml/components/SwapConfirmationDialog.qml index c43204c..eb55751 100644 --- a/amm-ui/qml/components/SwapConfirmationDialog.qml +++ b/amm-ui/qml/components/SwapConfirmationDialog.qml @@ -141,64 +141,16 @@ FocusScope { } } - ColumnLayout { - spacing: 8 + SwapSummary { Layout.fillWidth: true - - Item { - Layout.fillWidth: true - implicitHeight: 18 - Text { - anchors.left: parent.left; anchors.verticalCenter: parent.verticalCenter - text: qsTr("Fee"); color: root.theme.colors.textSecondary; font.pixelSize: 12 - } - Text { - anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter - text: root.snapshot.feePercent || "" - color: root.theme.colors.textPrimary; font.pixelSize: 12; font.bold: true - } - } - Item { - Layout.fillWidth: true - implicitHeight: 18 - Text { - anchors.left: parent.left; anchors.verticalCenter: parent.verticalCenter - text: qsTr("Price impact"); color: root.theme.colors.textSecondary; font.pixelSize: 12 - } - Text { - anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter - text: root.snapshot.priceImpactPercent || "" - color: root.theme.colors.textPrimary; font.pixelSize: 12; font.bold: true - } - } - Item { - Layout.fillWidth: true - implicitHeight: 18 - Text { - anchors.left: parent.left; anchors.verticalCenter: parent.verticalCenter - text: qsTr("Slippage tolerance"); color: root.theme.colors.textSecondary; font.pixelSize: 12 - } - Text { - anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter - text: root.snapshot.slippageTolerance || "" - color: root.theme.colors.textPrimary; font.pixelSize: 12; font.bold: true - } - } - Item { - Layout.fillWidth: true - implicitHeight: 18 - Text { - anchors.left: parent.left; anchors.verticalCenter: parent.verticalCenter - text: qsTr("Min received"); color: root.theme.colors.textSecondary; font.pixelSize: 12 - } - Text { - anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter - text: qsTr("%1 %2") - .arg(root.snapshot.minReceived || "") - .arg(root.snapshot.buyToken || "") - color: root.theme.colors.textPrimary; font.pixelSize: 12; font.bold: true - } - } + theme: root.theme + feeText: root.snapshot.feeAmount || "" + 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 || "") } RowLayout { diff --git a/amm-ui/qml/components/SwapSummary.qml b/amm-ui/qml/components/SwapSummary.qml new file mode 100644 index 0000000..99e467d --- /dev/null +++ b/amm-ui/qml/components/SwapSummary.qml @@ -0,0 +1,120 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.15 + +Item { + id: root + + property var theme + property string feeText: "" + property string priceImpactText: "" + property real priceImpactPercent: 0 + property string slippageText: "" + property string minReceivedText: "" + + readonly property color priceImpactColor: { + if (root.priceImpactPercent > 5) return "#F08A76"; + if (root.priceImpactPercent > 1) return "#F2B366"; + return root.theme.colors.textPrimary; + } + + implicitHeight: column.implicitHeight + + ColumnLayout { + id: column + + anchors.fill: parent + spacing: 8 + + Item { + implicitHeight: 18 + + Layout.fillWidth: true + + Text { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + color: root.theme.colors.textSecondary + font.pixelSize: 12 + text: qsTr("Fee") + } + + Text { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + color: root.theme.colors.textPrimary + font.bold: true + font.pixelSize: 12 + text: root.feeText + } + } + + Item { + implicitHeight: 18 + + Layout.fillWidth: true + + Text { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + color: root.theme.colors.textSecondary + font.pixelSize: 12 + text: qsTr("Price impact") + } + + Text { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + color: root.priceImpactColor + font.bold: true + font.pixelSize: 12 + text: root.priceImpactText + } + } + + Item { + implicitHeight: 18 + + Layout.fillWidth: true + + Text { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + color: root.theme.colors.textSecondary + font.pixelSize: 12 + text: qsTr("Slippage tolerance") + } + + Text { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + color: root.theme.colors.textPrimary + font.bold: true + font.pixelSize: 12 + text: root.slippageText + } + } + + Item { + implicitHeight: 18 + + Layout.fillWidth: true + + Text { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + color: root.theme.colors.textSecondary + font.pixelSize: 12 + text: qsTr("Min received") + } + + Text { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + color: root.theme.colors.textPrimary + font.bold: true + font.pixelSize: 12 + text: root.minReceivedText + } + } + } +} diff --git a/amm-ui/qml/state/DummySwapState.qml b/amm-ui/qml/state/DummySwapState.qml new file mode 100644 index 0000000..b3382a5 --- /dev/null +++ b/amm-ui/qml/state/DummySwapState.qml @@ -0,0 +1,88 @@ +import QtQuick 2.15 + +QtObject { + id: root + + property int feeBps: 30 + + function parseAmount(value) { + return Math.max(0, Number(value) || 0); + } + + function clampSlippagePercent(value) { + return Math.max(0, Math.min(50, Number(value) || 0)); + } + + function feeAmount(amountIn) { + return parseAmount(amountIn) * root.feeBps / 10000; + } + + function amountOutFor(amountIn, reserveIn, reserveOut) { + const safeAmountIn = parseAmount(amountIn); + const safeReserveIn = parseAmount(reserveIn); + const safeReserveOut = parseAmount(reserveOut); + + if (safeAmountIn <= 0 || safeReserveIn <= 0 || safeReserveOut <= 0) { + return 0; + } + + const amountInAfterFee = safeAmountIn * (10000 - root.feeBps) / 10000; + return safeReserveOut * amountInAfterFee / (safeReserveIn + amountInAfterFee); + } + + function priceImpactPercent(amountIn, amountOut, reserveIn, reserveOut) { + const safeAmountIn = parseAmount(amountIn); + const safeAmountOut = parseAmount(amountOut); + const safeReserveIn = parseAmount(reserveIn); + const safeReserveOut = parseAmount(reserveOut); + + if (safeAmountIn <= 0 || safeAmountOut <= 0) { + return 0; + } + if (safeReserveIn <= 0 || safeReserveOut <= 0) { + return 0; + } + if (safeReserveOut - safeAmountOut <= 0) { + return 0; + } + + const priceBefore = safeReserveIn / safeReserveOut; + const priceAfter = (safeReserveIn + safeAmountIn) / (safeReserveOut - safeAmountOut); + return (priceAfter - priceBefore) / priceBefore * 100; + } + + function minReceived(amountOut, slippagePercent) { + const safeAmount = parseAmount(amountOut); + const safeSlippage = clampSlippagePercent(slippagePercent); + return safeAmount * (1 - safeSlippage / 100); + } + + function maxSent(amountIn, slippagePercent) { + const safeAmount = parseAmount(amountIn); + const safeSlippage = clampSlippagePercent(slippagePercent); + return safeAmount * (1 + safeSlippage / 100); + } + + function formatAmountValue(value) { + const amount = Math.max(0, Number(value) || 0); + if (amount >= 1) return amount.toFixed(2); + if (amount >= 0.0001) return amount.toFixed(6); + return amount.toFixed(8); + } + + function formatTokenAmount(value, symbol) { + const formatted = formatAmountValue(value); + return symbol ? formatted + " " + symbol : formatted; + } + + function formatPercent(value) { + const amount = Number(value) || 0; + if (amount > 0 && amount < 0.01) return "<0.01%"; + return amount.toFixed(2) + "%"; + } + + function formatSlippagePercent(value) { + const amount = clampSlippagePercent(value); + return amount.toFixed(2).replace(/0+$/, "").replace(/[.]$/, "") + "%"; + } +}