feat(amm/ui): add mock confirmation flow (#64)

Fixes #64
This commit is contained in:
Ricardo Guilherme Schmidt 2026-04-22 19:18:59 -03:00 committed by r4bbit
parent d92a61fd9b
commit a3bdc964c7
11 changed files with 826 additions and 324 deletions

View File

@ -3,6 +3,7 @@ import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
import "components" import "components"
import "state" import "state"
import "pages"
Item { Item {
id: root id: root
@ -167,79 +168,9 @@ Item {
} }
// Liquidity view // Liquidity view
Item { LiquidityPage {
id: liquidityView
anchors.fill: parent anchors.fill: parent
visible: navbar.currentIndex === 1 visible: navbar.currentIndex === 1
property int activeLiquidityTab: 0
property real slippageTolerancePercent: 0.5
DummyPoolState {
id: poolState
}
Rectangle {
anchors.fill: parent
color: "#151515"
}
Flickable {
id: scroll
anchors.fill: parent
clip: true
contentHeight: content.implicitHeight + 24
contentWidth: width
ColumnLayout {
id: content
spacing: 10
width: scroll.width - 24
x: 12
y: 12
PoolPositionSummary {
poolState: poolState
Layout.fillWidth: true
Layout.preferredHeight: implicitHeight
}
LiquidityActionTabs {
currentIndex: liquidityView.activeLiquidityTab
Layout.fillWidth: true
Layout.preferredHeight: implicitHeight
onTabRequested: function(index) {
liquidityView.activeLiquidityTab = index
}
}
AddLiquidityForm {
poolState: poolState
slippageTolerancePercent: liquidityView.slippageTolerancePercent
visible: liquidityView.activeLiquidityTab === 0
Layout.fillWidth: true
Layout.preferredHeight: visible ? implicitHeight : 0
onSlippageToleranceChangeRequested: function(tolerancePercent) {
liquidityView.slippageTolerancePercent = poolState.clampSlippageTolerancePercent(tolerancePercent)
}
}
RemoveLiquidityForm {
poolState: poolState
slippageTolerancePercent: liquidityView.slippageTolerancePercent
visible: liquidityView.activeLiquidityTab === 1
Layout.fillWidth: true
Layout.preferredHeight: visible ? implicitHeight : 0
onSlippageToleranceChangeRequested: function(tolerancePercent) {
liquidityView.slippageTolerancePercent = poolState.clampSlippageTolerancePercent(tolerancePercent)
}
}
}
}
} }
} }
} }

View File

@ -1,4 +1,5 @@
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
import "../state" import "../state"
@ -21,70 +22,24 @@ Rectangle {
readonly property bool minReceivedIsZero: root.hasAnyAmount && root.minLpReceived === 0 readonly property bool minReceivedIsZero: root.hasAnyAmount && root.minLpReceived === 0
readonly property bool zeroTokenDeposit: root.hasAnyAmount && (root.preview.actualA === 0 || root.preview.actualB === 0) 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 bool zeroLpDeposit: root.preview.actualA > 0 && root.preview.actualB > 0 && root.preview.deltaLp === 0
readonly property bool canSubmit: root.hasAnyAmount && !root.amountAOverBalance && !root.amountBOverBalance && !root.minReceivedIsZero && !root.zeroTokenDeposit && !root.zeroLpDeposit
readonly property string submitButtonText: !root.hasAnyAmount ? qsTr("Enter an amount") : root.amountAOverBalance ? qsTr("Insufficient %1 balance").arg(root.poolState.tokenA) : root.amountBOverBalance ? qsTr("Insufficient %1 balance").arg(root.poolState.tokenB) : root.zeroTokenDeposit ? qsTr("Amount rounds to zero") : root.zeroLpDeposit ? qsTr("LP output is 0") : root.minReceivedIsZero ? qsTr("Minimum received is 0") : qsTr("Add Liquidity")
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") : "" 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") : ""
signal slippageToleranceChangeRequested(real tolerancePercent) signal slippageToleranceChangeRequested(real tolerancePercent)
signal addLiquidityRequested(var snapshot)
color: "#1D1D1D" color: "#00000000"
implicitHeight: content.implicitHeight + 20 implicitHeight: content.implicitHeight
radius: 8 radius: 0
border.color: "#343434" border.width: 0
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 { ColumnLayout {
id: content id: content
anchors.fill: parent anchors.fill: parent
anchors.margins: 10
spacing: 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 { TokenAmountInput {
balance: root.poolState.formatTokenAmount(root.poolState.walletBalanceA, root.poolState.tokenA) balance: root.poolState.formatTokenAmount(root.poolState.walletBalanceA, root.poolState.tokenA)
errorText: root.amountAOverBalance ? qsTr("Insufficient %1 balance").arg(root.poolState.tokenA) : "" errorText: root.amountAOverBalance ? qsTr("Insufficient %1 balance").arg(root.poolState.tokenA) : ""
@ -118,8 +73,8 @@ Rectangle {
} }
SummaryRow { SummaryRow {
label: qsTr("Required ratio") label: qsTr("Current price")
value: qsTr("%1 %2 / 1 %3").arg(root.poolState.formatInteger(root.poolState.tokenAPerTokenB)).arg(root.poolState.tokenA).arg(root.poolState.tokenB) value: qsTr("1 %1 = %2 %3").arg(root.poolState.tokenB).arg(root.poolState.formatInteger(root.poolState.tokenAPerTokenB)).arg(root.poolState.tokenA)
Layout.fillWidth: true Layout.fillWidth: true
} }
@ -129,6 +84,7 @@ Rectangle {
estimateHelp: qsTr("Estimated with the same integer floor math used by the add-liquidity contract path.") estimateHelp: qsTr("Estimated with the same integer floor math used by the add-liquidity contract path.")
label: qsTr("Estimated LP tokens") label: qsTr("Estimated LP tokens")
value: root.poolState.formatLpAmount(root.preview.deltaLp) value: root.poolState.formatLpAmount(root.preview.deltaLp)
visible: root.hasAnyAmount
Layout.fillWidth: true Layout.fillWidth: true
} }
@ -146,6 +102,7 @@ Rectangle {
SummaryRow { SummaryRow {
label: qsTr("Min LP received") label: qsTr("Min LP received")
value: root.poolState.formatLpAmount(root.minLpReceived) value: root.poolState.formatLpAmount(root.minLpReceived)
visible: root.hasAnyAmount
Layout.fillWidth: true Layout.fillWidth: true
} }
@ -171,5 +128,94 @@ Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
} }
Button {
id: submitButton
activeFocusOnTab: true
enabled: root.canSubmit
focusPolicy: Qt.StrongFocus
hoverEnabled: true
text: root.submitButtonText
Accessible.name: submitButton.text
Layout.fillWidth: true
Layout.minimumHeight: 44
Layout.preferredHeight: 44
onClicked: root.addLiquidityRequested(root.submitSnapshot())
contentItem: Text {
color: submitButton.enabled ? "#151515" : "#7D756E"
elide: Text.ElideRight
font.bold: true
font.pixelSize: 13
horizontalAlignment: Text.AlignHCenter
text: submitButton.text
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
border.color: submitButton.enabled ? "#F26A21" : "#343434"
border.width: 1
color: submitButton.enabled ? submitButton.pressed ? "#D95C1E" : submitButton.hovered || submitButton.activeFocus ? "#FF8A3D" : "#F26A21" : "#181818"
radius: 6
}
}
}
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);
}
function resetForm() {
root.amountA = "";
root.amountB = "";
root.lastEditedToken = "A";
}
function submitSnapshot() {
return {
"action": "add",
"actualA": root.preview.actualA,
"actualB": root.preview.actualB,
"currentRatio": qsTr("1 %1 = %2 %3").arg(root.poolState.tokenB).arg(root.poolState.formatInteger(root.poolState.tokenAPerTokenB)).arg(root.poolState.tokenA),
"deltaLp": root.preview.deltaLp,
"depositA": root.poolState.formatTokenAmount(root.preview.actualA, root.poolState.tokenA),
"depositB": root.poolState.formatTokenAmount(root.preview.actualB, root.poolState.tokenB),
"feeTier": root.poolState.feeTier,
"minLpReceived": root.poolState.formatLpAmount(root.minLpReceived),
"slippageTolerance": root.poolState.formatPercent(root.slippageTolerancePercent),
"tokenA": root.poolState.tokenA,
"tokenB": root.poolState.tokenB
};
} }
} }

View File

@ -9,10 +9,10 @@ Rectangle {
signal tabRequested(int index) signal tabRequested(int index)
color: "#1D1D1D" color: "#181818"
implicitHeight: 44 implicitHeight: 42
radius: 8 radius: 8
border.color: "#343434" border.color: "#303030"
border.width: 1 border.width: 1
RowLayout { RowLayout {
@ -37,7 +37,7 @@ Rectangle {
onClicked: root.tabRequested(0) onClicked: root.tabRequested(0)
contentItem: Text { contentItem: Text {
color: root.currentIndex === 0 || addTab.hovered || addTab.activeFocus ? "#151515" : "#A9A098" color: root.currentIndex === 0 ? "#F2D8C7" : addTab.hovered || addTab.activeFocus ? "#E7E1D8" : "#8E8780"
elide: Text.ElideRight elide: Text.ElideRight
font.bold: true font.bold: true
font.pixelSize: 12 font.pixelSize: 12
@ -47,9 +47,9 @@ Rectangle {
} }
background: Rectangle { background: Rectangle {
border.color: addTab.activeFocus ? "#F26A21" : root.currentIndex === 0 ? "#F26A21" : "#151515" border.color: addTab.activeFocus || root.currentIndex === 0 ? "#F26A21" : "#181818"
border.width: 1 border.width: 1
color: addTab.pressed ? "#D95C1E" : root.currentIndex === 0 ? "#F26A21" : addTab.hovered || addTab.activeFocus ? "#E7E1D8" : "#151515" color: addTab.pressed ? "#2A1D16" : root.currentIndex === 0 ? "#211914" : addTab.hovered || addTab.activeFocus ? "#202020" : "#121212"
radius: 6 radius: 6
} }
} }
@ -71,7 +71,7 @@ Rectangle {
onClicked: root.tabRequested(1) onClicked: root.tabRequested(1)
contentItem: Text { contentItem: Text {
color: root.currentIndex === 1 || removeTab.hovered || removeTab.activeFocus ? "#151515" : "#A9A098" color: root.currentIndex === 1 ? "#F2D8C7" : removeTab.hovered || removeTab.activeFocus ? "#E7E1D8" : "#8E8780"
elide: Text.ElideRight elide: Text.ElideRight
font.bold: true font.bold: true
font.pixelSize: 12 font.pixelSize: 12
@ -81,9 +81,9 @@ Rectangle {
} }
background: Rectangle { background: Rectangle {
border.color: removeTab.activeFocus ? "#F26A21" : root.currentIndex === 1 ? "#F26A21" : "#151515" border.color: removeTab.activeFocus || root.currentIndex === 1 ? "#F26A21" : "#181818"
border.width: 1 border.width: 1
color: removeTab.pressed ? "#D95C1E" : root.currentIndex === 1 ? "#F26A21" : removeTab.hovered || removeTab.activeFocus ? "#E7E1D8" : "#151515" color: removeTab.pressed ? "#2A1D16" : root.currentIndex === 1 ? "#211914" : removeTab.hovered || removeTab.activeFocus ? "#202020" : "#121212"
radius: 6 radius: 6
} }
} }

View File

@ -0,0 +1,238 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
FocusScope {
id: root
property var snapshot: ({})
property bool open: false
readonly property bool isAdd: root.snapshot.action === "add"
signal canceled
signal confirmed(var snapshot)
visible: root.open
z: 20
Keys.onEscapePressed: root.cancel()
function openWithSnapshot(nextSnapshot) {
root.snapshot = nextSnapshot;
root.open = true;
root.forceActiveFocus();
cancelButton.forceActiveFocus();
}
function cancel() {
root.open = false;
root.canceled();
}
function confirm() {
const confirmedSnapshot = root.snapshot;
root.open = false;
root.confirmed(confirmedSnapshot);
}
Rectangle {
anchors.fill: parent
color: "#99000000"
MouseArea {
anchors.fill: parent
}
}
Rectangle {
id: panel
anchors.centerIn: parent
color: "#1D1D1D"
implicitHeight: dialogContent.implicitHeight + 24
radius: 8
width: Math.max(0, Math.min(360, root.width - 32))
border.color: "#343434"
border.width: 1
ColumnLayout {
id: dialogContent
anchors.fill: parent
anchors.margins: 12
spacing: 12
Text {
color: "#E7E1D8"
font.bold: true
font.pixelSize: 16
text: root.isAdd ? qsTr("Confirm add liquidity") : qsTr("Confirm remove liquidity")
Layout.fillWidth: true
}
ColumnLayout {
spacing: 8
visible: root.isAdd
Layout.fillWidth: true
SummaryRow {
label: qsTr("Deposit %1").arg(root.snapshot.tokenA || "")
value: root.snapshot.depositA || ""
Layout.fillWidth: true
}
SummaryRow {
label: qsTr("Deposit %1").arg(root.snapshot.tokenB || "")
value: root.snapshot.depositB || ""
Layout.fillWidth: true
}
SummaryRow {
label: qsTr("Receive at least")
value: root.snapshot.minLpReceived || ""
Layout.fillWidth: true
}
SummaryRow {
label: qsTr("Current ratio")
value: root.snapshot.currentRatio || ""
Layout.fillWidth: true
}
SummaryRow {
label: qsTr("Fee tier")
value: root.snapshot.feeTier || ""
Layout.fillWidth: true
}
SummaryRow {
label: qsTr("Slippage tolerance")
value: root.snapshot.slippageTolerance || ""
Layout.fillWidth: true
}
}
ColumnLayout {
spacing: 8
visible: !root.isAdd
Layout.fillWidth: true
SummaryRow {
label: qsTr("Burn LP")
value: qsTr("%1 (%2)").arg(root.snapshot.burnText || "").arg(root.snapshot.burnPercent || "")
Layout.fillWidth: true
}
SummaryRow {
label: qsTr("Receive at least %1").arg(root.snapshot.tokenA || "")
value: root.snapshot.minTokenAReceived || ""
Layout.fillWidth: true
}
SummaryRow {
label: qsTr("Receive at least %1").arg(root.snapshot.tokenB || "")
value: root.snapshot.minTokenBReceived || ""
Layout.fillWidth: true
}
SummaryRow {
label: qsTr("Slippage tolerance")
value: root.snapshot.slippageTolerance || ""
Layout.fillWidth: true
}
SummaryRow {
label: qsTr("Post-removal share")
value: root.snapshot.postRemovalShare || ""
Layout.fillWidth: true
}
}
RowLayout {
spacing: 8
Layout.fillWidth: true
Button {
id: cancelButton
activeFocusOnTab: true
focusPolicy: Qt.StrongFocus
hoverEnabled: true
text: qsTr("Cancel")
Accessible.name: cancelButton.text
Layout.fillWidth: true
Layout.minimumHeight: 44
onClicked: root.cancel()
contentItem: Text {
color: cancelButton.hovered || cancelButton.activeFocus ? "#151515" : "#E7E1D8"
elide: Text.ElideRight
font.bold: true
font.pixelSize: 13
horizontalAlignment: Text.AlignHCenter
text: cancelButton.text
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
border.color: cancelButton.activeFocus ? "#F26A21" : "#343434"
border.width: 1
color: cancelButton.pressed ? "#343434" : cancelButton.hovered || cancelButton.activeFocus ? "#E7E1D8" : "#151515"
radius: 6
}
}
Button {
id: confirmButton
activeFocusOnTab: true
focusPolicy: Qt.StrongFocus
hoverEnabled: true
text: qsTr("Confirm")
Accessible.name: confirmButton.text
Layout.fillWidth: true
Layout.minimumHeight: 44
onClicked: root.confirm()
contentItem: Text {
color: "#151515"
elide: Text.ElideRight
font.bold: true
font.pixelSize: 13
horizontalAlignment: Text.AlignHCenter
text: confirmButton.text
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
border.color: "#F26A21"
border.width: 1
color: confirmButton.pressed ? "#D95C1E" : confirmButton.hovered || confirmButton.activeFocus ? "#FF8A3D" : "#F26A21"
radius: 6
}
}
}
}
}
}

View File

@ -8,10 +8,10 @@ Rectangle {
required property DummyPoolState poolState required property DummyPoolState poolState
readonly property string estimateHelp: qsTr("This value is an estimate from the current dummy reserves and your share of total LP supply.") readonly property string estimateHelp: qsTr("This value is an estimate from the current dummy reserves and your share of total LP supply.")
color: "#1D1D1D" color: "#151515"
implicitHeight: content.implicitHeight + 20 implicitHeight: content.implicitHeight + 20
radius: 8 radius: 8
border.color: "#343434" border.color: "#303030"
border.width: 1 border.width: 1
ColumnLayout { ColumnLayout {
@ -19,104 +19,80 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
anchors.margins: 10 anchors.margins: 10
spacing: 4 spacing: 6
Text { RowLayout {
color: "#E7E1D8" spacing: 10
font.bold: true
font.pixelSize: 16
text: qsTr("Pool position")
Layout.fillWidth: true Layout.fillWidth: true
ColumnLayout {
spacing: 2
Layout.fillWidth: true
Text {
color: "#E7E1D8"
font.bold: true
font.pixelSize: 13
text: root.poolState.userLpBalance > 0 ? qsTr("Your position") : qsTr("No position")
Layout.fillWidth: true
}
Text {
color: "#8E8780"
font.pixelSize: 11
text: qsTr("%1 LP tokens").arg(root.poolState.formatInteger(root.poolState.userLpBalance))
visible: root.poolState.userLpBalance > 0
Layout.fillWidth: true
}
}
Rectangle {
color: "#211914"
radius: 10
border.color: "#49301F"
border.width: 1
Layout.preferredHeight: 24
Layout.preferredWidth: shareText.implicitWidth + 18
Text {
id: shareText
anchors.centerIn: parent
color: "#F2D8C7"
font.bold: true
font.pixelSize: 11
text: root.poolState.userLpBalance > 0 ? root.poolState.formatPoolShare(root.poolState.poolShare) : root.poolState.feeTier
}
}
} }
Text { SummaryRow {
color: "#F26A21" estimated: true
font.pixelSize: 12 estimateHelp: root.estimateHelp
text: qsTr("You have no position in this pool") label: qsTr("Owned")
visible: root.poolState.userLpBalance === 0 value: qsTr("%1 + %2").arg(root.poolState.formatCompactTokenAmount(root.poolState.userOwnedA, root.poolState.tokenA)).arg(root.poolState.formatCompactTokenAmount(root.poolState.userOwnedB, root.poolState.tokenB))
visible: root.poolState.userLpBalance > 0
Layout.fillWidth: true Layout.fillWidth: true
} }
SummaryRow { SummaryRow {
label: qsTr("Token A") label: qsTr("Pool")
value: root.poolState.tokenA value: qsTr("%1 / %2").arg(root.poolState.formatCompactTokenAmount(root.poolState.reserveA, root.poolState.tokenA)).arg(root.poolState.formatCompactTokenAmount(root.poolState.reserveB, root.poolState.tokenB))
Layout.fillWidth: true Layout.fillWidth: true
} }
SummaryRow { SummaryRow {
label: qsTr("Token B") label: qsTr("Fee")
value: root.poolState.tokenB
Layout.fillWidth: true
}
SummaryRow {
label: qsTr("Fee tier")
value: root.poolState.feeTier value: root.poolState.feeTier
Layout.fillWidth: true Layout.fillWidth: true
} }
SummaryRow {
label: qsTr("Your LP tokens")
value: root.poolState.formatInteger(root.poolState.userLpBalance)
visible: root.poolState.userLpBalance > 0
Layout.fillWidth: true
}
SummaryRow {
estimated: true
estimateHelp: root.estimateHelp
label: qsTr("Pool share")
value: root.poolState.formatPoolShare(root.poolState.poolShare)
visible: root.poolState.userLpBalance > 0
Layout.fillWidth: true
}
SummaryRow {
estimated: true
estimateHelp: root.estimateHelp
label: qsTr("Your Token A")
value: "\u2248 " + root.poolState.formatTokenAmount(root.poolState.userOwnedA, root.poolState.tokenA)
visible: root.poolState.userLpBalance > 0
Layout.fillWidth: true
}
SummaryRow {
estimated: true
estimateHelp: root.estimateHelp
label: qsTr("Your Token B")
value: "\u2248 " + root.poolState.formatTokenAmount(root.poolState.userOwnedB, root.poolState.tokenB)
visible: root.poolState.userLpBalance > 0
Layout.fillWidth: true
}
SummaryRow {
label: qsTr("Total reserve A")
value: root.poolState.formatTokenAmount(root.poolState.reserveA, root.poolState.tokenA)
Layout.fillWidth: true
}
SummaryRow {
label: qsTr("Total reserve B")
value: root.poolState.formatTokenAmount(root.poolState.reserveB, root.poolState.tokenB)
Layout.fillWidth: true
}
SummaryRow {
label: qsTr("Total LP supply")
value: root.poolState.formatInteger(root.poolState.totalLpSupply)
Layout.fillWidth: true
}
} }
} }

View File

@ -20,15 +20,17 @@ Rectangle {
readonly property int minTokenAReceived: root.poolState.minReceivedAmount(root.preview.withdrawA, root.slippageTolerancePercent) readonly property int minTokenAReceived: root.poolState.minReceivedAmount(root.preview.withdrawA, root.slippageTolerancePercent)
readonly property int minTokenBReceived: root.poolState.minReceivedAmount(root.preview.withdrawB, root.slippageTolerancePercent) readonly property int minTokenBReceived: root.poolState.minReceivedAmount(root.preview.withdrawB, root.slippageTolerancePercent)
readonly property bool minReceivedIsZero: root.burnAmount > 0 && (root.minTokenAReceived === 0 || root.minTokenBReceived === 0) readonly property bool minReceivedIsZero: root.burnAmount > 0 && (root.minTokenAReceived === 0 || root.minTokenBReceived === 0)
readonly property bool canSubmit: root.hasLpTokens && root.burnAmount > 0 && !root.minReceivedIsZero
readonly property string estimateHelp: qsTr("Estimated with the same integer floor math used by the remove-liquidity contract path.") readonly property string estimateHelp: qsTr("Estimated with the same integer floor math used by the remove-liquidity contract path.")
readonly property string submitButtonText: !root.hasLpTokens ? qsTr("No LP balance") : root.burnAmount === 0 ? qsTr("Enter an amount") : root.minReceivedIsZero ? qsTr("Minimum received is 0") : qsTr("Remove Liquidity")
signal slippageToleranceChangeRequested(real tolerancePercent) signal slippageToleranceChangeRequested(real tolerancePercent)
signal removeLiquidityRequested(var snapshot)
color: "#1D1D1D" color: "#00000000"
implicitHeight: content.implicitHeight + 20 implicitHeight: content.implicitHeight
radius: 8 radius: 0
border.color: "#343434" border.width: 0
border.width: 1
onMaxBurnAmountChanged: { onMaxBurnAmountChanged: {
if (root.burnAmount > root.maxBurnAmount) { if (root.burnAmount > root.maxBurnAmount) {
@ -36,40 +38,12 @@ Rectangle {
} }
} }
function setBurnAmount(value) {
root.burnAmount = root.poolState.clampBurnAmount(value);
}
function setBurnPercent(percent) {
root.setBurnAmount(root.poolState.burnAmountForPercent(percent));
}
ColumnLayout { ColumnLayout {
id: content id: content
anchors.fill: parent anchors.fill: parent
anchors.margins: 10
spacing: 10 spacing: 10
Text {
color: "#E7E1D8"
font.bold: true
font.pixelSize: 16
text: qsTr("Remove liquidity")
Layout.fillWidth: true
}
Text {
color: "#A9A098"
font.pixelSize: 12
lineHeight: 1.25
text: qsTr("Burn LP tokens to withdraw your proportional share of both pool tokens.")
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
Text { Text {
color: "#F26A21" color: "#F26A21"
font.pixelSize: 12 font.pixelSize: 12
@ -386,6 +360,7 @@ Rectangle {
estimateHelp: root.estimateHelp estimateHelp: root.estimateHelp
label: qsTr("Withdraw %1").arg(root.poolState.tokenA) label: qsTr("Withdraw %1").arg(root.poolState.tokenA)
value: root.poolState.formatTokenAmount(root.preview.withdrawA, root.poolState.tokenA) value: root.poolState.formatTokenAmount(root.preview.withdrawA, root.poolState.tokenA)
visible: root.burnAmount > 0
Layout.fillWidth: true Layout.fillWidth: true
} }
@ -395,6 +370,7 @@ Rectangle {
estimateHelp: root.estimateHelp estimateHelp: root.estimateHelp
label: qsTr("Withdraw %1").arg(root.poolState.tokenB) label: qsTr("Withdraw %1").arg(root.poolState.tokenB)
value: root.poolState.formatTokenAmount(root.preview.withdrawB, root.poolState.tokenB) value: root.poolState.formatTokenAmount(root.preview.withdrawB, root.poolState.tokenB)
visible: root.burnAmount > 0
Layout.fillWidth: true Layout.fillWidth: true
} }
@ -412,6 +388,7 @@ Rectangle {
SummaryRow { SummaryRow {
label: qsTr("Min %1 received").arg(root.poolState.tokenA) label: qsTr("Min %1 received").arg(root.poolState.tokenA)
value: root.poolState.formatTokenAmount(root.minTokenAReceived, root.poolState.tokenA) value: root.poolState.formatTokenAmount(root.minTokenAReceived, root.poolState.tokenA)
visible: root.burnAmount > 0
Layout.fillWidth: true Layout.fillWidth: true
} }
@ -419,6 +396,7 @@ Rectangle {
SummaryRow { SummaryRow {
label: qsTr("Min %1 received").arg(root.poolState.tokenB) label: qsTr("Min %1 received").arg(root.poolState.tokenB)
value: root.poolState.formatTokenAmount(root.minTokenBReceived, root.poolState.tokenB) value: root.poolState.formatTokenAmount(root.minTokenBReceived, root.poolState.tokenB)
visible: root.burnAmount > 0
Layout.fillWidth: true Layout.fillWidth: true
} }
@ -434,34 +412,80 @@ Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
} }
SummaryRow {
label: qsTr("New reserve A")
value: root.poolState.formatTokenAmount(root.preview.newReserveA, root.poolState.tokenA)
Layout.fillWidth: true
}
SummaryRow {
label: qsTr("New reserve B")
value: root.poolState.formatTokenAmount(root.preview.newReserveB, root.poolState.tokenB)
Layout.fillWidth: true
}
SummaryRow {
label: qsTr("New LP supply")
value: root.poolState.formatInteger(root.preview.newTotalLpSupply)
Layout.fillWidth: true
}
SummaryRow { SummaryRow {
estimated: true estimated: true
estimateHelp: root.estimateHelp estimateHelp: root.estimateHelp
label: qsTr("New user share") label: qsTr("Position after")
value: root.poolState.formatPoolShare(root.preview.newUserShare) value: root.poolState.formatPoolShare(root.preview.newUserShare)
visible: root.burnAmount > 0
Layout.fillWidth: true Layout.fillWidth: true
} }
Button {
id: submitButton
activeFocusOnTab: root.hasLpTokens
enabled: root.canSubmit
focusPolicy: Qt.StrongFocus
hoverEnabled: true
text: root.submitButtonText
Accessible.name: submitButton.text
Layout.fillWidth: true
Layout.minimumHeight: 44
Layout.preferredHeight: 44
onClicked: root.removeLiquidityRequested(root.submitSnapshot())
contentItem: Text {
color: submitButton.enabled ? "#151515" : "#7D756E"
elide: Text.ElideRight
font.bold: true
font.pixelSize: 13
horizontalAlignment: Text.AlignHCenter
text: submitButton.text
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
border.color: submitButton.enabled ? "#F26A21" : "#343434"
border.width: 1
color: submitButton.enabled ? submitButton.pressed ? "#D95C1E" : submitButton.hovered || submitButton.activeFocus ? "#FF8A3D" : "#F26A21" : "#181818"
radius: 6
}
}
}
function setBurnAmount(value) {
root.burnAmount = root.poolState.clampBurnAmount(value);
}
function setBurnPercent(percent) {
root.setBurnAmount(root.poolState.burnAmountForPercent(percent));
}
function resetForm() {
root.setBurnAmount(0);
}
function submitSnapshot() {
return {
"action": "remove",
"burnAmount": root.preview.burnedLp,
"burnPercent": root.poolState.formatPercent(root.removePercent),
"burnText": root.poolState.formatLpAmount(root.preview.burnedLp),
"minTokenAReceived": root.poolState.formatTokenAmount(root.minTokenAReceived, root.poolState.tokenA),
"minTokenBReceived": root.poolState.formatTokenAmount(root.minTokenBReceived, root.poolState.tokenB),
"postRemovalShare": root.poolState.formatPoolShare(root.preview.newUserShare),
"slippageTolerance": root.poolState.formatPercent(root.slippageTolerancePercent),
"tokenA": root.poolState.tokenA,
"tokenB": root.poolState.tokenB,
"withdrawA": root.preview.withdrawA,
"withdrawB": root.preview.withdrawB,
"withdrawAText": root.poolState.formatTokenAmount(root.preview.withdrawA, root.poolState.tokenA),
"withdrawBText": root.poolState.formatTokenAmount(root.preview.withdrawB, root.poolState.tokenB)
};
} }
} }

View File

@ -2,7 +2,7 @@ import QtQuick 2.15
import QtQuick.Controls 2.15 import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
Rectangle { Item {
id: root id: root
property real tolerancePercent: 0.5 property real tolerancePercent: 0.5
@ -12,11 +12,7 @@ Rectangle {
signal toleranceChangeRequested(real tolerancePercent) signal toleranceChangeRequested(real tolerancePercent)
color: "#151515" implicitHeight: content.implicitHeight
implicitHeight: content.implicitHeight + 20
radius: 8
border.color: customField.activeFocus ? "#F26A21" : "#343434"
border.width: 1
Component.onCompleted: root.restoreCustomText() Component.onCompleted: root.restoreCustomText()
@ -60,15 +56,30 @@ Rectangle {
id: content id: content
anchors.fill: parent anchors.fill: parent
anchors.margins: 10 spacing: 6
spacing: 8
Text { RowLayout {
color: "#A9A098" spacing: 8
font.pixelSize: 12
text: qsTr("Slippage tolerance")
Layout.fillWidth: true Layout.fillWidth: true
Text {
color: "#A9A098"
font.pixelSize: 12
text: qsTr("Slippage tolerance")
Layout.fillWidth: true
}
Text {
color: root.tolerancePercent <= 1 ? "#8FD6A4" : root.tolerancePercent <= 5 ? "#F2B366" : "#F08A76"
font.bold: true
font.pixelSize: 11
horizontalAlignment: Text.AlignRight
text: root.thresholdText
Layout.maximumWidth: 150
}
} }
RowLayout { RowLayout {
@ -95,7 +106,7 @@ Rectangle {
onClicked: root.commitPreset(presetValue) onClicked: root.commitPreset(presetValue)
contentItem: Text { contentItem: Text {
color: preset01.hovered || preset01.activeFocus || preset01.selected ? "#151515" : "#A9A098" color: preset01.selected ? "#F2D8C7" : preset01.hovered || preset01.activeFocus ? "#E7E1D8" : "#A9A098"
font.bold: true font.bold: true
font.pixelSize: 11 font.pixelSize: 11
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
@ -106,7 +117,7 @@ Rectangle {
background: Rectangle { background: Rectangle {
border.color: preset01.activeFocus || preset01.selected ? "#F26A21" : "#343434" border.color: preset01.activeFocus || preset01.selected ? "#F26A21" : "#343434"
border.width: 1 border.width: 1
color: preset01.pressed ? "#D95C1E" : preset01.selected ? "#F26A21" : preset01.hovered || preset01.activeFocus ? "#E7E1D8" : "#101010" color: preset01.pressed ? "#2A1D16" : preset01.selected ? "#211914" : preset01.hovered || preset01.activeFocus ? "#202020" : "#101010"
radius: 6 radius: 6
} }
} }
@ -130,7 +141,7 @@ Rectangle {
onClicked: root.commitPreset(presetValue) onClicked: root.commitPreset(presetValue)
contentItem: Text { contentItem: Text {
color: preset05.hovered || preset05.activeFocus || preset05.selected ? "#151515" : "#A9A098" color: preset05.selected ? "#F2D8C7" : preset05.hovered || preset05.activeFocus ? "#E7E1D8" : "#A9A098"
font.bold: true font.bold: true
font.pixelSize: 11 font.pixelSize: 11
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
@ -141,7 +152,7 @@ Rectangle {
background: Rectangle { background: Rectangle {
border.color: preset05.activeFocus || preset05.selected ? "#F26A21" : "#343434" border.color: preset05.activeFocus || preset05.selected ? "#F26A21" : "#343434"
border.width: 1 border.width: 1
color: preset05.pressed ? "#D95C1E" : preset05.selected ? "#F26A21" : preset05.hovered || preset05.activeFocus ? "#E7E1D8" : "#101010" color: preset05.pressed ? "#2A1D16" : preset05.selected ? "#211914" : preset05.hovered || preset05.activeFocus ? "#202020" : "#101010"
radius: 6 radius: 6
} }
} }
@ -165,7 +176,7 @@ Rectangle {
onClicked: root.commitPreset(presetValue) onClicked: root.commitPreset(presetValue)
contentItem: Text { contentItem: Text {
color: preset10.hovered || preset10.activeFocus || preset10.selected ? "#151515" : "#A9A098" color: preset10.selected ? "#F2D8C7" : preset10.hovered || preset10.activeFocus ? "#E7E1D8" : "#A9A098"
font.bold: true font.bold: true
font.pixelSize: 11 font.pixelSize: 11
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
@ -176,7 +187,7 @@ Rectangle {
background: Rectangle { background: Rectangle {
border.color: preset10.activeFocus || preset10.selected ? "#F26A21" : "#343434" border.color: preset10.activeFocus || preset10.selected ? "#F26A21" : "#343434"
border.width: 1 border.width: 1
color: preset10.pressed ? "#D95C1E" : preset10.selected ? "#F26A21" : preset10.hovered || preset10.activeFocus ? "#E7E1D8" : "#101010" color: preset10.pressed ? "#2A1D16" : preset10.selected ? "#211914" : preset10.hovered || preset10.activeFocus ? "#202020" : "#101010"
radius: 6 radius: 6
} }
} }
@ -244,38 +255,5 @@ Rectangle {
} }
} }
} }
RowLayout {
spacing: 6
Layout.fillWidth: true
Text {
color: root.tolerancePercent <= 1 ? "#8FD6A4" : root.tolerancePercent <= 5 ? "#F2B366" : "#F08A76"
font.bold: true
font.pixelSize: 11
horizontalAlignment: Text.AlignHCenter
text: root.thresholdIcon
Layout.preferredWidth: 18
}
Text {
color: root.tolerancePercent <= 1 ? "#8FD6A4" : root.tolerancePercent <= 5 ? "#F2B366" : "#F08A76"
font.pixelSize: 11
text: root.thresholdText
Layout.fillWidth: true
}
}
Text {
color: "#A9A098"
font.pixelSize: 11
text: qsTr("Allowed range: 0.01% to 50%.")
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
} }
} }

View File

@ -0,0 +1,103 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
Item {
id: root
property string message: ""
property string detail: ""
property bool open: false
property int duration: 3600
height: implicitHeight
implicitHeight: toast.implicitHeight
opacity: root.open ? 1 : 0
visible: root.open || fadeOut.running
z: 30
function show(nextMessage, nextDetail) {
root.message = nextMessage;
root.detail = nextDetail || "";
root.open = true;
dismissTimer.restart();
}
Timer {
id: dismissTimer
interval: root.duration
repeat: false
onTriggered: root.open = false
}
Behavior on opacity {
NumberAnimation {
id: fadeOut
duration: 160
easing.type: Easing.OutCubic
}
}
Rectangle {
id: toast
anchors.fill: parent
color: "#20201F"
implicitHeight: Math.max(50, toastContent.implicitHeight + 18)
radius: 8
border.color: "#4D3A2E"
border.width: 1
RowLayout {
id: toastContent
spacing: 8
anchors {
fill: parent
leftMargin: 14
rightMargin: 14
}
Rectangle {
color: "#78C88D"
radius: 6
Layout.alignment: Qt.AlignTop
Layout.topMargin: 3
Layout.preferredHeight: 12
Layout.preferredWidth: 12
}
ColumnLayout {
spacing: 2
Layout.fillWidth: true
Text {
id: toastText
color: "#E7E1D8"
elide: Text.ElideRight
font.bold: true
font.pixelSize: 14
text: root.message
Layout.fillWidth: true
}
Text {
color: "#B8ADA3"
elide: Text.ElideRight
font.pixelSize: 12
text: root.detail
visible: root.detail.length > 0
Layout.fillWidth: true
}
}
}
}
}

View File

@ -43,7 +43,7 @@ Item {
text: root.value text: root.value
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
Layout.maximumWidth: 178 Layout.maximumWidth: Math.max(178, root.width * 0.55)
} }
EstimateInfoButton { EstimateInfoButton {

View File

@ -0,0 +1,188 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import "../components"
import "../state"
Item {
id: root
property int activeLiquidityTab: 0
property real slippageTolerancePercent: 0.5
readonly property int pageMargin: 16
readonly property int preferredCardWidth: 492
readonly property int pageCardY: pageCard.implicitHeight + root.pageMargin * 2 <= scroll.height ? Math.round((scroll.height - pageCard.implicitHeight) / 2) : root.pageMargin
width: parent ? parent.width : implicitWidth
height: parent ? parent.height : implicitHeight
implicitWidth: root.preferredCardWidth + root.pageMargin * 2
implicitHeight: pageCard.implicitHeight + root.pageMargin * 2
DummyPoolState {
id: poolState
}
Rectangle {
anchors.fill: parent
color: "#151515"
}
Flickable {
id: scroll
anchors.fill: parent
clip: true
contentHeight: Math.max(height, pageCard.y + pageCard.implicitHeight + root.pageMargin)
contentWidth: width
enabled: !confirmationDialog.visible
flickableDirection: Flickable.VerticalFlick
Rectangle {
id: pageCard
color: "#1B1B1B"
implicitHeight: shellContent.implicitHeight + 24
radius: 16
border.color: "#303030"
border.width: 1
width: Math.max(0, Math.min(scroll.width - root.pageMargin * 2, root.preferredCardWidth))
x: Math.max(root.pageMargin, (scroll.width - width) / 2)
y: root.pageCardY
ColumnLayout {
id: shellContent
anchors.fill: parent
anchors.margins: 12
spacing: 10
RowLayout {
spacing: 10
Layout.fillWidth: true
Text {
color: "#E7E1D8"
font.bold: true
font.pixelSize: 18
text: qsTr("Liquidity")
Layout.fillWidth: true
}
Rectangle {
color: "#211914"
radius: 12
border.color: "#49301F"
border.width: 1
Layout.preferredHeight: 26
Layout.preferredWidth: pairText.implicitWidth + 20
Text {
id: pairText
anchors.centerIn: parent
color: "#F2D8C7"
font.bold: true
font.pixelSize: 12
text: qsTr("%1 / %2").arg(poolState.tokenA).arg(poolState.tokenB)
}
}
}
LiquidityActionTabs {
currentIndex: root.activeLiquidityTab
Layout.fillWidth: true
Layout.preferredHeight: implicitHeight
onTabRequested: function (index) {
root.activeLiquidityTab = index;
}
}
PoolPositionSummary {
poolState: poolState
Layout.fillWidth: true
Layout.preferredHeight: implicitHeight
}
AddLiquidityForm {
id: addLiquidityForm
poolState: poolState
slippageTolerancePercent: root.slippageTolerancePercent
visible: root.activeLiquidityTab === 0
Layout.fillWidth: true
Layout.preferredHeight: visible ? implicitHeight : 0
onSlippageToleranceChangeRequested: function (tolerancePercent) {
root.slippageTolerancePercent = poolState.clampSlippageTolerancePercent(tolerancePercent);
}
onAddLiquidityRequested: function (snapshot) {
confirmationDialog.openWithSnapshot(snapshot);
}
}
RemoveLiquidityForm {
id: removeLiquidityForm
poolState: poolState
slippageTolerancePercent: root.slippageTolerancePercent
visible: root.activeLiquidityTab === 1
Layout.fillWidth: true
Layout.preferredHeight: visible ? implicitHeight : 0
onSlippageToleranceChangeRequested: function (tolerancePercent) {
root.slippageTolerancePercent = poolState.clampSlippageTolerancePercent(tolerancePercent);
}
onRemoveLiquidityRequested: function (snapshot) {
confirmationDialog.openWithSnapshot(snapshot);
}
}
}
SuccessToast {
id: successToast
width: Math.max(0, Math.min(380, parent.width - 24))
anchors {
bottom: parent.bottom
bottomMargin: 14
horizontalCenter: parent.horizontalCenter
}
}
}
}
LiquidityConfirmationDialog {
id: confirmationDialog
anchors.fill: parent
onConfirmed: function (snapshot) {
root.confirmLiquidityAction(snapshot);
}
}
function confirmLiquidityAction(snapshot) {
if (snapshot.action === "add") {
poolState.applyAddLiquidity(snapshot.actualA, snapshot.actualB, snapshot.deltaLp);
addLiquidityForm.resetForm();
successToast.show(qsTr("Liquidity added"), qsTr("Position updated"));
return;
}
if (snapshot.action === "remove") {
poolState.applyRemoveLiquidity(snapshot.withdrawA, snapshot.withdrawB, snapshot.burnAmount);
removeLiquidityForm.resetForm();
successToast.show(qsTr("Liquidity removed"), qsTr("Position updated"));
}
}
}

View File

@ -159,6 +159,20 @@ QtObject {
return amount.toFixed(6).replace(/0+$/, "").replace(/[.]$/, ""); return amount.toFixed(6).replace(/0+$/, "").replace(/[.]$/, "");
} }
function formatCompactDecimal(value) {
const amount = Number(value) || 0;
if (Math.abs(amount) >= 1000 || Math.abs(amount - Math.round(amount)) < 0.000001) {
return formatInteger(amount);
}
if (Math.abs(amount) >= 1) {
return amount.toFixed(2).replace(/0+$/, "").replace(/[.]$/, "");
}
return amount.toFixed(6).replace(/0+$/, "").replace(/[.]$/, "");
}
function formatInputAmount(value) { function formatInputAmount(value) {
return formatDecimal(value); return formatDecimal(value);
} }
@ -167,6 +181,10 @@ QtObject {
return formatDecimal(value) + " " + token; return formatDecimal(value) + " " + token;
} }
function formatCompactTokenAmount(value, token) {
return formatCompactDecimal(value) + " " + token;
}
function formatLpAmount(value) { function formatLpAmount(value) {
return formatInteger(value) + " LP"; return formatInteger(value) + " LP";
} }