mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-05-18 15:09:51 +00:00
feat(amm/ui): show swap summary under the swap card
This commit is contained in:
parent
c8a192e377
commit
3df3c3d7c4
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
120
amm-ui/qml/components/SwapSummary.qml
Normal file
120
amm-ui/qml/components/SwapSummary.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
88
amm-ui/qml/state/DummySwapState.qml
Normal file
88
amm-ui/qml/state/DummySwapState.qml
Normal 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(/[.]$/, "") + "%";
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user