feat(amm/ui): show swap summary under the swap card

This commit is contained in:
Andrea Franz 2026-05-05 18:27:45 +02:00
parent c8a192e377
commit 3df3c3d7c4
4 changed files with 249 additions and 74 deletions

View File

@ -1,6 +1,8 @@
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls 2.15 import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
import "components"
import "state"
Rectangle { Rectangle {
id: root id: root
@ -11,7 +13,11 @@ Rectangle {
property var buyToken: null property var buyToken: null
property string sellAmount: "" property string sellAmount: ""
property real slippageTolerancePercent: 0.5 property real slippageTolerancePercent: 0.5
readonly property real feePercent: 0.30
DummySwapState {
id: swapState
feeBps: 30
}
signal requestTokenSelect(string side) signal requestTokenSelect(string side)
signal submitRequested(var snapshot) signal submitRequested(var snapshot)
@ -25,24 +31,18 @@ Rectangle {
root.sellAmount = "" root.sellAmount = ""
} }
readonly property real sellReserve: sellToken ? (sellToken.reserve || 0) : 0
readonly property real buyReserve: buyToken ? (buyToken.reserve || 0) : 0
readonly property real parsedSellAmount: { readonly property real parsedSellAmount: {
var amt = parseFloat(sellAmount) var amt = parseFloat(sellAmount)
return isNaN(amt) || amt < 0 ? 0 : amt return isNaN(amt) || amt < 0 ? 0 : amt
} }
readonly property real parsedBuyAmount: { readonly property real parsedBuyAmount: swapState.amountOutFor(parsedSellAmount, sellReserve, buyReserve)
if (!sellToken || !buyToken || parsedSellAmount <= 0) return 0 readonly property real feeAmount: swapState.feeAmount(parsedSellAmount)
return parsedSellAmount * sellToken.usdPrice / buyToken.usdPrice readonly property real minReceivedAmount: swapState.minReceived(parsedBuyAmount, slippageTolerancePercent)
} readonly property real priceImpactPercent: swapState.priceImpactPercent(parsedSellAmount, parsedBuyAmount, sellReserve, buyReserve)
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 bool hasAmount: parsedSellAmount > 0 readonly property bool hasAmount: parsedSellAmount > 0
readonly property bool tokensSelected: sellToken !== null && buyToken !== null readonly property bool tokensSelected: sellToken !== null && buyToken !== null
@ -89,9 +89,10 @@ Rectangle {
"sellAmount": formatAmountValue(parsedSellAmount), "sellAmount": formatAmountValue(parsedSellAmount),
"buyAmount": formatAmountValue(parsedBuyAmount), "buyAmount": formatAmountValue(parsedBuyAmount),
"minReceived": formatAmountValue(minReceivedAmount), "minReceived": formatAmountValue(minReceivedAmount),
"feePercent": feePercent.toFixed(2) + "%", "feeAmount": swapState.formatTokenAmount(feeAmount, sellToken ? sellToken.symbol : ""),
"priceImpactPercent": priceImpactPercent < 0.01 ? "<0.01%" : priceImpactPercent.toFixed(2) + "%", "priceImpactPercent": swapState.formatPercent(priceImpactPercent),
"slippageTolerance": slippageTolerancePercent.toFixed(2).replace(/0+$/, "").replace(/[.]$/, "") + "%" "priceImpactPercentValue": priceImpactPercent,
"slippageTolerance": swapState.formatSlippagePercent(slippageTolerancePercent)
} }
} }
@ -176,6 +177,20 @@ Rectangle {
onTokenClicked: root.requestTokenSelect("buy") 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 { Rectangle {
id: ctaBox id: ctaBox
Layout.fillWidth: true Layout.fillWidth: true

View File

@ -141,64 +141,16 @@ FocusScope {
} }
} }
ColumnLayout { SwapSummary {
spacing: 8
Layout.fillWidth: true Layout.fillWidth: true
theme: root.theme
Item { feeText: root.snapshot.feeAmount || ""
Layout.fillWidth: true priceImpactText: root.snapshot.priceImpactPercent || ""
implicitHeight: 18 priceImpactPercent: Number(root.snapshot.priceImpactPercentValue) || 0
Text { slippageText: root.snapshot.slippageTolerance || ""
anchors.left: parent.left; anchors.verticalCenter: parent.verticalCenter minReceivedText: qsTr("%1 %2")
text: qsTr("Fee"); color: root.theme.colors.textSecondary; font.pixelSize: 12 .arg(root.snapshot.minReceived || "")
} .arg(root.snapshot.buyToken || "")
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
}
}
} }
RowLayout { RowLayout {

View File

@ -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
}
}
}
}

View File

@ -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(/[.]$/, "") + "%";
}
}