mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-05-19 15:39:28 +00:00
parent
e75a778f7a
commit
58a705c0bc
@ -180,15 +180,32 @@ Item {
|
|||||||
color: "#151515"
|
color: "#151515"
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
Flickable {
|
||||||
anchors.fill: parent
|
id: scroll
|
||||||
anchors.margins: 12
|
|
||||||
spacing: 10
|
|
||||||
|
|
||||||
PoolPositionSummary {
|
anchors.fill: parent
|
||||||
poolState: poolState
|
clip: true
|
||||||
Layout.fillWidth: true
|
contentHeight: content.implicitHeight + 24
|
||||||
Layout.preferredHeight: implicitHeight
|
contentWidth: width
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: content
|
||||||
|
spacing: 10
|
||||||
|
width: scroll.width - 24
|
||||||
|
x: 12
|
||||||
|
y: 12
|
||||||
|
|
||||||
|
PoolPositionSummary {
|
||||||
|
poolState: poolState
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: implicitHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
AddLiquidityForm {
|
||||||
|
poolState: poolState
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: implicitHeight
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
142
amm-ui/qml/components/AddLiquidityForm.qml
Normal file
142
amm-ui/qml/components/AddLiquidityForm.qml
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import QtQuick 2.15
|
||||||
|
import QtQuick.Layouts 1.15
|
||||||
|
import "../state"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property DummyPoolState poolState
|
||||||
|
|
||||||
|
property string amountA: ""
|
||||||
|
property string amountB: ""
|
||||||
|
property string lastEditedToken: "A"
|
||||||
|
readonly property real parsedA: root.poolState.parseAmount(root.amountA)
|
||||||
|
readonly property real parsedB: root.poolState.parseAmount(root.amountB)
|
||||||
|
readonly property var preview: root.poolState.addLiquidityPreview(root.parsedA, root.parsedB)
|
||||||
|
readonly property bool hasAnyAmount: root.parsedA > 0 || root.parsedB > 0
|
||||||
|
readonly property bool amountAOverBalance: root.parsedA > root.poolState.walletBalanceA
|
||||||
|
readonly property bool amountBOverBalance: root.parsedB > root.poolState.walletBalanceB
|
||||||
|
readonly property bool zeroTokenDeposit: root.hasAnyAmount && (root.preview.actualA === 0 || root.preview.actualB === 0)
|
||||||
|
readonly property bool zeroLpDeposit: root.preview.actualA > 0 && root.preview.actualB > 0 && root.preview.deltaLp === 0
|
||||||
|
readonly property string warningText: root.zeroTokenDeposit ? qsTr("Deposit would be rejected because one token amount rounds to zero") : root.zeroLpDeposit ? qsTr("Deposit would mint 0 LP tokens") : ""
|
||||||
|
|
||||||
|
color: "#1D1D1D"
|
||||||
|
implicitHeight: content.implicitHeight + 20
|
||||||
|
radius: 8
|
||||||
|
border.color: "#343434"
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
function setAmounts(nextA, nextB, intentToken, showZero) {
|
||||||
|
root.lastEditedToken = intentToken;
|
||||||
|
root.amountA = nextA > 0 || showZero ? root.poolState.formatInputAmount(nextA) : "";
|
||||||
|
root.amountB = nextB > 0 || showZero ? root.poolState.formatInputAmount(nextB) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFromTokenA(value) {
|
||||||
|
if (value.length === 0) {
|
||||||
|
setAmounts(0, 0, "A", false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextA = root.poolState.parseAmount(value);
|
||||||
|
setAmounts(nextA, root.poolState.amountBForA(nextA), "A", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFromTokenB(value) {
|
||||||
|
if (value.length === 0) {
|
||||||
|
setAmounts(0, 0, "B", false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextB = root.poolState.parseAmount(value);
|
||||||
|
setAmounts(root.poolState.amountAForB(nextB), nextB, "B", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useMax(intentToken) {
|
||||||
|
const capped = root.poolState.maxAddLiquidityForBalances();
|
||||||
|
setAmounts(capped.actualA, capped.actualB, intentToken, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: content
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 10
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
Text {
|
||||||
|
color: "#E7E1D8"
|
||||||
|
font.bold: true
|
||||||
|
font.pixelSize: 16
|
||||||
|
text: qsTr("Add liquidity")
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
SummaryRow {
|
||||||
|
label: qsTr("Current ratio")
|
||||||
|
value: qsTr("1 %1 = %2 %3").arg(root.poolState.tokenB).arg(root.poolState.formatInteger(root.poolState.tokenAPerTokenB)).arg(root.poolState.tokenA)
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
TokenAmountInput {
|
||||||
|
balance: root.poolState.formatTokenAmount(root.poolState.walletBalanceA, root.poolState.tokenA)
|
||||||
|
errorText: root.amountAOverBalance ? qsTr("Insufficient %1 balance").arg(root.poolState.tokenA) : ""
|
||||||
|
helperText: root.lastEditedToken === "B" && root.amountA.length > 0 ? qsTr("Calculated from current pool ratio") : ""
|
||||||
|
label: qsTr("Token A amount")
|
||||||
|
token: root.poolState.tokenA
|
||||||
|
text: root.amountA
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
onEditingChanged: function (value) {
|
||||||
|
root.updateFromTokenA(value);
|
||||||
|
}
|
||||||
|
onMaxClicked: root.useMax("A")
|
||||||
|
}
|
||||||
|
|
||||||
|
TokenAmountInput {
|
||||||
|
balance: root.poolState.formatTokenAmount(root.poolState.walletBalanceB, root.poolState.tokenB)
|
||||||
|
errorText: root.amountBOverBalance ? qsTr("Insufficient %1 balance").arg(root.poolState.tokenB) : ""
|
||||||
|
helperText: root.lastEditedToken === "A" && root.amountB.length > 0 ? qsTr("Calculated from current pool ratio") : ""
|
||||||
|
label: qsTr("Token B amount")
|
||||||
|
token: root.poolState.tokenB
|
||||||
|
text: root.amountB
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
onEditingChanged: function (value) {
|
||||||
|
root.updateFromTokenB(value);
|
||||||
|
}
|
||||||
|
onMaxClicked: root.useMax("B")
|
||||||
|
}
|
||||||
|
|
||||||
|
SummaryRow {
|
||||||
|
label: qsTr("Required ratio")
|
||||||
|
value: qsTr("%1 %2 / 1 %3").arg(root.poolState.formatInteger(root.poolState.tokenAPerTokenB)).arg(root.poolState.tokenA).arg(root.poolState.tokenB)
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
SummaryRow {
|
||||||
|
estimated: true
|
||||||
|
estimateHelp: qsTr("Estimated with the same integer floor math used by the add-liquidity contract path.")
|
||||||
|
label: qsTr("Estimated LP tokens")
|
||||||
|
value: root.poolState.formatLpAmount(root.preview.deltaLp)
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
color: "#F08A76"
|
||||||
|
font.pixelSize: 12
|
||||||
|
lineHeight: 1.25
|
||||||
|
text: root.warningText
|
||||||
|
visible: root.warningText.length > 0
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
156
amm-ui/qml/components/TokenAmountInput.qml
Normal file
156
amm-ui/qml/components/TokenAmountInput.qml
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
import QtQuick 2.15
|
||||||
|
import QtQuick.Controls 2.15
|
||||||
|
import QtQuick.Layouts 1.15
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property alias text: amountField.text
|
||||||
|
property string balance: ""
|
||||||
|
property string errorText: ""
|
||||||
|
property string helperText: ""
|
||||||
|
property string label: ""
|
||||||
|
property string token: ""
|
||||||
|
|
||||||
|
signal editingChanged(string value)
|
||||||
|
signal maxClicked
|
||||||
|
|
||||||
|
color: "#151515"
|
||||||
|
implicitHeight: content.implicitHeight + 20
|
||||||
|
radius: 8
|
||||||
|
border.color: root.errorText.length > 0 ? "#D85F4B" : amountField.activeFocus ? "#F26A21" : "#343434"
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Accessible.name: root.label
|
||||||
|
Accessible.role: Accessible.EditableText
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: content
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 10
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Text {
|
||||||
|
color: "#A9A098"
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.pixelSize: 12
|
||||||
|
text: root.label
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
color: "#E7E1D8"
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.bold: true
|
||||||
|
font.pixelSize: 12
|
||||||
|
horizontalAlignment: Text.AlignRight
|
||||||
|
text: root.token
|
||||||
|
|
||||||
|
Layout.maximumWidth: 76
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
TextField {
|
||||||
|
id: amountField
|
||||||
|
|
||||||
|
activeFocusOnTab: true
|
||||||
|
color: "#E7E1D8"
|
||||||
|
font.bold: true
|
||||||
|
font.pixelSize: 18
|
||||||
|
inputMethodHints: Qt.ImhFormattedNumbersOnly
|
||||||
|
placeholderText: qsTr("0")
|
||||||
|
selectByMouse: true
|
||||||
|
selectedTextColor: "#151515"
|
||||||
|
selectionColor: "#F26A21"
|
||||||
|
validator: RegularExpressionValidator {
|
||||||
|
regularExpression: /[0-9]*([.][0-9]*)?/
|
||||||
|
}
|
||||||
|
|
||||||
|
Accessible.name: root.label
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.minimumHeight: 44
|
||||||
|
|
||||||
|
onTextEdited: root.editingChanged(text)
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
border.color: amountField.activeFocus ? "#F26A21" : "#343434"
|
||||||
|
border.width: 1
|
||||||
|
color: amountField.activeFocus ? "#1F1B18" : "#101010"
|
||||||
|
radius: 6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
id: maxButton
|
||||||
|
|
||||||
|
activeFocusOnTab: true
|
||||||
|
focusPolicy: Qt.StrongFocus
|
||||||
|
hoverEnabled: true
|
||||||
|
text: qsTr("MAX")
|
||||||
|
|
||||||
|
Accessible.name: qsTr("Use maximum %1 balance").arg(root.token)
|
||||||
|
|
||||||
|
Layout.minimumHeight: 44
|
||||||
|
Layout.preferredWidth: 58
|
||||||
|
|
||||||
|
onClicked: root.maxClicked()
|
||||||
|
|
||||||
|
contentItem: Text {
|
||||||
|
color: maxButton.activeFocus || maxButton.hovered ? "#151515" : "#F26A21"
|
||||||
|
font.bold: true
|
||||||
|
font.pixelSize: 11
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
text: maxButton.text
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
border.color: "#F26A21"
|
||||||
|
border.width: 1
|
||||||
|
color: maxButton.pressed ? "#D95C1E" : maxButton.hovered || maxButton.activeFocus ? "#F26A21" : "#201712"
|
||||||
|
radius: 6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Text {
|
||||||
|
color: root.errorText.length > 0 ? "#F08A76" : root.helperText.length > 0 ? "#F26A21" : "#A9A098"
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.pixelSize: 11
|
||||||
|
text: root.errorText.length > 0 ? root.errorText : root.helperText
|
||||||
|
visible: text.length > 0
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
color: "#A9A098"
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.pixelSize: 11
|
||||||
|
horizontalAlignment: Text.AlignRight
|
||||||
|
text: qsTr("Balance %1").arg(root.balance)
|
||||||
|
|
||||||
|
Layout.alignment: Qt.AlignRight
|
||||||
|
Layout.maximumWidth: 150
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,10 +10,13 @@ QtObject {
|
|||||||
property real reserveA: 1000000
|
property real reserveA: 1000000
|
||||||
property real reserveB: 500
|
property real reserveB: 500
|
||||||
property real totalLpSupply: 22360679
|
property real totalLpSupply: 22360679
|
||||||
|
property real walletBalanceA: 60000
|
||||||
|
property real walletBalanceB: 20
|
||||||
|
|
||||||
readonly property real poolShare: totalLpSupply > 0 ? userLpBalance / totalLpSupply : 0
|
readonly property real poolShare: totalLpSupply > 0 ? userLpBalance / totalLpSupply : 0
|
||||||
readonly property real userOwnedA: reserveA * poolShare
|
readonly property real userOwnedA: reserveA * poolShare
|
||||||
readonly property real userOwnedB: reserveB * poolShare
|
readonly property real userOwnedB: reserveB * poolShare
|
||||||
|
readonly property real tokenAPerTokenB: reserveB > 0 ? Math.floor(reserveA / reserveB) : 0
|
||||||
|
|
||||||
function applyAddLiquidity(actualA, actualB, mintedLp) {
|
function applyAddLiquidity(actualA, actualB, mintedLp) {
|
||||||
const safeA = Math.max(0, Number(actualA) || 0);
|
const safeA = Math.max(0, Number(actualA) || 0);
|
||||||
@ -45,6 +48,55 @@ QtObject {
|
|||||||
reserveA = 1000000;
|
reserveA = 1000000;
|
||||||
reserveB = 500;
|
reserveB = 500;
|
||||||
totalLpSupply = 22360679;
|
totalLpSupply = 22360679;
|
||||||
|
walletBalanceA = 60000;
|
||||||
|
walletBalanceB = 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAmount(value) {
|
||||||
|
return Math.max(0, Number(value) || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function floorAmount(value) {
|
||||||
|
return Math.floor(parseAmount(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function amountBForA(amountA) {
|
||||||
|
if (reserveA <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return reserveB * parseAmount(amountA) / reserveA;
|
||||||
|
}
|
||||||
|
|
||||||
|
function amountAForB(amountB) {
|
||||||
|
if (reserveB <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return reserveA * parseAmount(amountB) / reserveB;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addLiquidityPreview(maxA, maxB) {
|
||||||
|
const safeMaxA = parseAmount(maxA);
|
||||||
|
const safeMaxB = parseAmount(maxB);
|
||||||
|
const idealA = reserveB > 0 ? reserveA * safeMaxB / reserveB : 0;
|
||||||
|
const idealB = reserveA > 0 ? reserveB * safeMaxA / reserveA : 0;
|
||||||
|
const actualA = Math.min(idealA, safeMaxA);
|
||||||
|
const actualB = Math.min(idealB, safeMaxB);
|
||||||
|
const lpFromA = reserveA > 0 ? Math.floor(totalLpSupply * actualA / reserveA) : 0;
|
||||||
|
const lpFromB = reserveB > 0 ? Math.floor(totalLpSupply * actualB / reserveB) : 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
"actualA": actualA,
|
||||||
|
"actualB": actualB,
|
||||||
|
"deltaLp": Math.min(lpFromA, lpFromB),
|
||||||
|
"idealA": idealA,
|
||||||
|
"idealB": idealB
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function maxAddLiquidityForBalances() {
|
||||||
|
return addLiquidityPreview(walletBalanceA, walletBalanceB);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatInteger(value) {
|
function formatInteger(value) {
|
||||||
@ -52,8 +104,22 @@ QtObject {
|
|||||||
return rounded.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
return rounded.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDecimal(value) {
|
||||||
|
const amount = Number(value) || 0;
|
||||||
|
|
||||||
|
if (Math.abs(amount - Math.round(amount)) < 0.000001) {
|
||||||
|
return formatInteger(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
return amount.toFixed(6).replace(/0+$/, "").replace(/[.]$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatInputAmount(value) {
|
||||||
|
return formatDecimal(value);
|
||||||
|
}
|
||||||
|
|
||||||
function formatTokenAmount(value, token) {
|
function formatTokenAmount(value, token) {
|
||||||
return formatInteger(value) + " " + token;
|
return formatDecimal(value) + " " + token;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatLpAmount(value) {
|
function formatLpAmount(value) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user