diff --git a/amm-ui/qml/Main.qml b/amm-ui/qml/Main.qml index 52fa1ba..c1ff8dd 100644 --- a/amm-ui/qml/Main.qml +++ b/amm-ui/qml/Main.qml @@ -1,6 +1,9 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 +import "components" +import "state" +import "pages" Item { id: root @@ -14,139 +17,160 @@ Item { { symbol: "TOK6", name: "Token 6", color: "#9b59b6", letter: "L", address: "0x1337000000000000000000000000000000000cafe", usdPrice: 0.42 } ] - // ── Theme ───────────────────────────────────────────────────────────────── - QtObject { - id: theme - property bool isDark: false - property var colors: isDark ? dark : light - - readonly property var light: ({ - background: "#f7f7f5", - cardBg: "#ffffff", - inputBg: "#f0f0ee", - panelBg: "#e8e8e4", - panelHoverBg: "#ddddd8", - textPrimary: "#111111", - textSecondary: "#777770", - textPlaceholder: "#bbbbb5", - border: Qt.rgba(0,0,0,0.08), - borderStrong: Qt.rgba(0,0,0,0.10), - divider: Qt.rgba(0,0,0,0.06), - ctaBg: "#111111", - ctaHoverBg: "#2a2a28", - selection: "#b5c4a5", - noTokenCircle: "#c8c8c4", - orb1: "#7a8c6a", - orb2: "#b5c4a5", - orb3: "#7a8c6a", - orb4: "#c8d4b8" - }) - - readonly property var dark: ({ - background: "#0d0d12", - cardBg: "#1a1a22", - inputBg: "#222230", - panelBg: "#2a2a38", - panelHoverBg: "#363650", - textPrimary: "#ffffff", - textSecondary: "#888899", - textPlaceholder: "#444455", - border: Qt.rgba(1,1,1,0.08), - borderStrong: Qt.rgba(1,1,1,0.10), - divider: Qt.rgba(1,1,1,0.06), - ctaBg: "#2d1530", - ctaHoverBg: "#3d1f40", - selection: "#4c1d4b", - noTokenCircle: "#444455", - orb1: "#627eea", - orb2: "#9b59b6", - orb3: "#fc72ff", - orb4: "#26a17b" - }) + // ── Navigation bar ──────────────────────────────────────────────────────── + // Pinned to the top; self-contained styling, unaffected by view themes. + NavBar { + id: navbar + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + z: 100 } - // ── Root background ─────────────────────────────────────────────────────── - Rectangle { - anchors.fill: parent - color: theme.colors.background - Behavior on color { ColorAnimation { duration: 300 } } - - // Theme toggle - Rectangle { - anchors.top: parent.top - anchors.right: parent.right - anchors.margins: 16 - width: 44; height: 24; radius: 12 - color: theme.colors.panelBg - border.color: theme.colors.border - border.width: 1 - Text { - anchors.centerIn: parent - text: theme.isDark ? "☀" : "☾" - font.pixelSize: 13 - color: theme.colors.textSecondary - } - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: theme.isDark = !theme.isDark - } - } + // ── Content area (below the nav bar) ────────────────────────────────────── + Item { + anchors.top: navbar.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom - // Decorative orbs - Rectangle { x: -180; y: -120; width: 560; height: 560; radius: 280; color: theme.colors.orb1; opacity: 0.07 } - Rectangle { x: parent.width - 280; y: parent.height - 320; width: 480; height: 480; radius: 240; color: theme.colors.orb2; opacity: 0.09 } - Rectangle { x: parent.width - 200; y: -80; width: 380; height: 380; radius: 190; color: theme.colors.orb3; opacity: 0.05 } - Rectangle { x: 40; y: parent.height - 260; width: 320; height: 320; radius: 160; color: theme.colors.orb4; opacity: 0.08 } - - ColumnLayout { - anchors.centerIn: parent - spacing: 28 - - Text { - Layout.alignment: Qt.AlignHCenter - text: "Logos AMM" - color: theme.colors.textPrimary - font.pixelSize: 48 - font.weight: Font.Bold + // ── Trade view ──────────────────────────────────────────────────────── + Item { + anchors.fill: parent + visible: navbar.currentIndex === 0 + + // Trade view theme — scoped here, invisible to NavBar and LP view. + QtObject { + id: theme + property bool isDark: false + property var colors: isDark ? dark : light + + readonly property var light: ({ + background: "#f7f7f5", + cardBg: "#ffffff", + inputBg: "#f0f0ee", + panelBg: "#e8e8e4", + panelHoverBg: "#ddddd8", + textPrimary: "#111111", + textSecondary: "#777770", + textPlaceholder: "#bbbbb5", + border: Qt.rgba(0,0,0,0.08), + borderStrong: Qt.rgba(0,0,0,0.10), + divider: Qt.rgba(0,0,0,0.06), + ctaBg: "#111111", + ctaHoverBg: "#2a2a28", + selection: "#b5c4a5", + noTokenCircle: "#c8c8c4", + orb1: "#7a8c6a", + orb2: "#b5c4a5", + orb3: "#7a8c6a", + orb4: "#c8d4b8" + }) + + readonly property var dark: ({ + background: "#0d0d12", + cardBg: "#1a1a22", + inputBg: "#222230", + panelBg: "#2a2a38", + panelHoverBg: "#363650", + textPrimary: "#ffffff", + textSecondary: "#888899", + textPlaceholder: "#444455", + border: Qt.rgba(1,1,1,0.08), + borderStrong: Qt.rgba(1,1,1,0.10), + divider: Qt.rgba(1,1,1,0.06), + ctaBg: "#2d1530", + ctaHoverBg: "#3d1f40", + selection: "#4c1d4b", + noTokenCircle: "#444455", + orb1: "#627eea", + orb2: "#9b59b6", + orb3: "#fc72ff", + orb4: "#26a17b" + }) } - SwapCard { - id: swapCard - Layout.alignment: Qt.AlignHCenter - theme: theme - tokens: root.tokenData - width: Math.min(480, root.width - 32) + Rectangle { + anchors.fill: parent + color: theme.colors.background + Behavior on color { ColorAnimation { duration: 300 } } + + // Theme toggle + Rectangle { + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: 16 + width: 44; height: 24; radius: 12 + color: theme.colors.panelBg + border.color: theme.colors.border + border.width: 1 + Text { + anchors.centerIn: parent + text: theme.isDark ? "☀" : "☾" + font.pixelSize: 13 + color: theme.colors.textSecondary + } + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: theme.isDark = !theme.isDark + } + } - onRequestTokenSelect: function(side) { - tokenModal.targetSide = side - tokenModal.open() + // Decorative orbs + Rectangle { x: -180; y: -120; width: 560; height: 560; radius: 280; color: theme.colors.orb1; opacity: 0.07 } + Rectangle { x: parent.width - 280; y: parent.height - 320; width: 480; height: 480; radius: 240; color: theme.colors.orb2; opacity: 0.09 } + Rectangle { x: parent.width - 200; y: -80; width: 380; height: 380; radius: 190; color: theme.colors.orb3; opacity: 0.05 } + Rectangle { x: 40; y: parent.height - 260; width: 320; height: 320; radius: 160; color: theme.colors.orb4; opacity: 0.08 } + + ColumnLayout { + anchors.centerIn: parent + spacing: 28 + + SwapCard { + id: swapCard + Layout.alignment: Qt.AlignHCenter + theme: theme + tokens: root.tokenData + width: Math.min(480, root.width - 32) + + onRequestTokenSelect: function(side) { + tokenModal.targetSide = side + tokenModal.open() + } + } + + Text { + Layout.alignment: Qt.AlignHCenter + text: "Buy and sell crypto on LEZ." + textFormat: Text.RichText + color: theme.colors.textSecondary + font.pixelSize: 15 + horizontalAlignment: Text.AlignHCenter + } } - } - Text { - Layout.alignment: Qt.AlignHCenter - text: "Buy and sell crypto on LEZ." - textFormat: Text.RichText - color: theme.colors.textSecondary - font.pixelSize: 15 - horizontalAlignment: Text.AlignHCenter + TokenSelectorModal { + id: tokenModal + anchors.fill: parent + z: 10 + theme: theme + tokens: root.tokenData + + property string targetSide: "sell" + + onTokenSelected: function(tok) { + swapCard.setToken(targetSide, tok) + tokenModal.close() + } + } } } - TokenSelectorModal { - id: tokenModal + // ── Liquidity view ──────────────────────────────────────────────────── + LiquidityPage { anchors.fill: parent - z: 10 - theme: theme - tokens: root.tokenData - - property string targetSide: "sell" - - onTokenSelected: function(tok) { - swapCard.setToken(targetSide, tok) - tokenModal.close() - } + visible: navbar.currentIndex === 1 } } } diff --git a/amm-ui/qml/NavBar.qml b/amm-ui/qml/NavBar.qml new file mode 100644 index 0000000..4d8a105 --- /dev/null +++ b/amm-ui/qml/NavBar.qml @@ -0,0 +1,86 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.15 + +// Self-contained navigation bar — styling is independent of any view's theme. +// Use currentIndex to read the active tab; tabChanged(index) fires on selection. +Item { + id: root + + property int currentIndex: 0 + readonly property var tabs: ["Trade", "Liquidity"] + + signal tabChanged(int index) + + implicitHeight: 56 + + Rectangle { + anchors.fill: parent + color: "#ffffff" + + // Bottom separator + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: 1 + color: Qt.rgba(0, 0, 0, 0.08) + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 20 + anchors.rightMargin: 20 + spacing: 4 + + // App identity + Text { + text: "Logos AMM" + color: "#111111" + font.pixelSize: 17 + font.weight: Font.Bold + } + + Item { Layout.fillWidth: true } + + // Tab pills + Row { + spacing: 4 + + Repeater { + model: root.tabs + + delegate: Rectangle { + readonly property bool active: root.currentIndex === index + + height: 36 + width: tabLabel.implicitWidth + 28 + radius: 18 + color: active ? "#111111" : "transparent" + + Behavior on color { ColorAnimation { duration: 150 } } + + Text { + id: tabLabel + anchors.centerIn: parent + text: modelData + color: active ? "#ffffff" : "#666666" + font.pixelSize: 14 + font.weight: active ? Font.Medium : Font.Normal + + Behavior on color { ColorAnimation { duration: 150 } } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + root.currentIndex = index + root.tabChanged(index) + } + } + } + } + } + } + } +} diff --git a/amm-ui/qml/components/AddLiquidityForm.qml b/amm-ui/qml/components/AddLiquidityForm.qml new file mode 100644 index 0000000..2a1de26 --- /dev/null +++ b/amm-ui/qml/components/AddLiquidityForm.qml @@ -0,0 +1,221 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import "../state" + +Rectangle { + id: root + + required property DummyPoolState poolState + + property real slippageTolerancePercent: 0.5 + 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 int minLpReceived: root.poolState.minReceivedAmount(root.preview.deltaLp, root.slippageTolerancePercent) + 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 minReceivedIsZero: root.hasAnyAmount && root.minLpReceived === 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 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") : "" + + signal slippageToleranceChangeRequested(real tolerancePercent) + signal addLiquidityRequested(var snapshot) + + color: "#00000000" + implicitHeight: content.implicitHeight + radius: 0 + border.width: 0 + + ColumnLayout { + id: content + + anchors.fill: parent + spacing: 10 + + 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("Current price") + value: qsTr("1 %1 = %2 %3").arg(root.poolState.tokenB).arg(root.poolState.formatInteger(root.poolState.tokenAPerTokenB)).arg(root.poolState.tokenA) + + 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) + visible: root.hasAnyAmount + + Layout.fillWidth: true + } + + SlippageToleranceControl { + tolerancePercent: root.slippageTolerancePercent + + Layout.fillWidth: true + + onToleranceChangeRequested: function (tolerancePercent) { + root.slippageToleranceChangeRequested(tolerancePercent); + } + } + + SummaryRow { + label: qsTr("Min LP received") + value: root.poolState.formatLpAmount(root.minLpReceived) + visible: root.hasAnyAmount + + Layout.fillWidth: true + } + + Text { + color: "#F08A76" + font.pixelSize: 12 + lineHeight: 1.25 + text: qsTr("Minimum received is 0. Increase amount or lower slippage.") + visible: root.minReceivedIsZero + wrapMode: Text.WordWrap + + 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 + } + + 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 + }; + } +} diff --git a/amm-ui/qml/components/EstimateInfoButton.qml b/amm-ui/qml/components/EstimateInfoButton.qml new file mode 100644 index 0000000..1d3a109 --- /dev/null +++ b/amm-ui/qml/components/EstimateInfoButton.qml @@ -0,0 +1,64 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +Button { + id: root + + property string helpText: qsTr("This value is derived from your LP token balance, total LP supply, and current pool reserves.") + + activeFocusOnTab: true + focusPolicy: Qt.StrongFocus + hoverEnabled: true + text: qsTr("?") + + Accessible.name: qsTr("Why this value is an estimate") + + onClicked: estimatePopup.opened ? estimatePopup.close() : estimatePopup.open() + + Keys.onEscapePressed: estimatePopup.close() + + contentItem: Text { + color: root.activeFocus || root.hovered || estimatePopup.opened ? "#F26A21" : "#E7E1D8" + font.bold: true + font.pixelSize: 11 + horizontalAlignment: Text.AlignHCenter + text: qsTr("i") + verticalAlignment: Text.AlignVCenter + } + + background: Rectangle { + border.color: root.activeFocus || estimatePopup.opened ? "#F26A21" : "#343434" + border.width: 1 + color: root.pressed ? "#2A221D" : "#1D1D1D" + radius: 8 + } + + Popup { + id: estimatePopup + + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + focus: true + modal: false + padding: 10 + width: 224 + x: Math.max(-width + root.width, -196) + y: root.height + 4 + + onClosed: root.forceActiveFocus() + + background: Rectangle { + border.color: "#343434" + border.width: 1 + color: "#1D1D1D" + radius: 8 + } + + contentItem: Text { + color: "#E7E1D8" + font.pixelSize: 12 + lineHeight: 1.25 + text: root.helpText + wrapMode: Text.WordWrap + } + } +} diff --git a/amm-ui/qml/components/LiquidityActionTabs.qml b/amm-ui/qml/components/LiquidityActionTabs.qml new file mode 100644 index 0000000..5a7c1d1 --- /dev/null +++ b/amm-ui/qml/components/LiquidityActionTabs.qml @@ -0,0 +1,91 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +Rectangle { + id: root + + property int currentIndex: 0 + + signal tabRequested(int index) + + color: "#181818" + implicitHeight: 42 + radius: 8 + border.color: "#303030" + border.width: 1 + + RowLayout { + anchors.fill: parent + anchors.margins: 4 + spacing: 4 + + Button { + id: addTab + + activeFocusOnTab: true + focusPolicy: Qt.StrongFocus + hoverEnabled: true + text: qsTr("Add liquidity") + + Accessible.name: addTab.text + Accessible.role: Accessible.PageTab + + Layout.fillHeight: true + Layout.fillWidth: true + + onClicked: root.tabRequested(0) + + contentItem: Text { + color: root.currentIndex === 0 ? "#F2D8C7" : addTab.hovered || addTab.activeFocus ? "#E7E1D8" : "#8E8780" + elide: Text.ElideRight + font.bold: true + font.pixelSize: 12 + horizontalAlignment: Text.AlignHCenter + text: addTab.text + verticalAlignment: Text.AlignVCenter + } + + background: Rectangle { + border.color: addTab.activeFocus || root.currentIndex === 0 ? "#F26A21" : "#181818" + border.width: 1 + color: addTab.pressed ? "#2A1D16" : root.currentIndex === 0 ? "#211914" : addTab.hovered || addTab.activeFocus ? "#202020" : "#121212" + radius: 6 + } + } + + Button { + id: removeTab + + activeFocusOnTab: true + focusPolicy: Qt.StrongFocus + hoverEnabled: true + text: qsTr("Remove liquidity") + + Accessible.name: removeTab.text + Accessible.role: Accessible.PageTab + + Layout.fillHeight: true + Layout.fillWidth: true + + onClicked: root.tabRequested(1) + + contentItem: Text { + color: root.currentIndex === 1 ? "#F2D8C7" : removeTab.hovered || removeTab.activeFocus ? "#E7E1D8" : "#8E8780" + elide: Text.ElideRight + font.bold: true + font.pixelSize: 12 + horizontalAlignment: Text.AlignHCenter + text: removeTab.text + verticalAlignment: Text.AlignVCenter + } + + background: Rectangle { + border.color: removeTab.activeFocus || root.currentIndex === 1 ? "#F26A21" : "#181818" + border.width: 1 + color: removeTab.pressed ? "#2A1D16" : root.currentIndex === 1 ? "#211914" : removeTab.hovered || removeTab.activeFocus ? "#202020" : "#121212" + radius: 6 + } + } + } +} diff --git a/amm-ui/qml/components/LiquidityConfirmationDialog.qml b/amm-ui/qml/components/LiquidityConfirmationDialog.qml new file mode 100644 index 0000000..16b0dc6 --- /dev/null +++ b/amm-ui/qml/components/LiquidityConfirmationDialog.qml @@ -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 + } + } + } + } + } +} diff --git a/amm-ui/qml/components/PoolPositionSummary.qml b/amm-ui/qml/components/PoolPositionSummary.qml new file mode 100644 index 0000000..599aeba --- /dev/null +++ b/amm-ui/qml/components/PoolPositionSummary.qml @@ -0,0 +1,98 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import "../state" + +Rectangle { + id: root + + 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.") + + color: "#151515" + implicitHeight: content.implicitHeight + 20 + radius: 8 + border.color: "#303030" + border.width: 1 + + ColumnLayout { + id: content + + anchors.fill: parent + anchors.margins: 10 + spacing: 6 + + RowLayout { + spacing: 10 + + 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 + } + } + } + + SummaryRow { + estimated: true + estimateHelp: root.estimateHelp + label: qsTr("Owned") + 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 + } + + SummaryRow { + label: qsTr("Pool") + 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 + } + + SummaryRow { + label: qsTr("Fee") + value: root.poolState.feeTier + + Layout.fillWidth: true + } + } +} diff --git a/amm-ui/qml/components/RemoveLiquidityForm.qml b/amm-ui/qml/components/RemoveLiquidityForm.qml new file mode 100644 index 0000000..47fbbbf --- /dev/null +++ b/amm-ui/qml/components/RemoveLiquidityForm.qml @@ -0,0 +1,491 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import "../state" + +Rectangle { + id: root + + required property DummyPoolState poolState + + property real slippageTolerancePercent: 0.5 + property int burnAmount: 0 + readonly property int maxBurnAmount: root.poolState.clampBurnAmount(root.poolState.userLpBalance) + readonly property bool hasLpTokens: root.maxBurnAmount > 0 + readonly property int preset25Amount: root.poolState.burnAmountForPercent(25) + readonly property int preset50Amount: root.poolState.burnAmountForPercent(50) + readonly property int preset75Amount: root.poolState.burnAmountForPercent(75) + readonly property real removePercent: root.maxBurnAmount > 0 ? root.burnAmount * 100 / root.maxBurnAmount : 0 + readonly property var preview: root.poolState.removeLiquidityPreview(root.burnAmount) + 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 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 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 removeLiquidityRequested(var snapshot) + + color: "#00000000" + implicitHeight: content.implicitHeight + radius: 0 + border.width: 0 + + onMaxBurnAmountChanged: { + if (root.burnAmount > root.maxBurnAmount) { + root.setBurnAmount(root.maxBurnAmount); + } + } + + ColumnLayout { + id: content + + anchors.fill: parent + spacing: 10 + + Text { + color: "#F26A21" + font.pixelSize: 12 + text: qsTr("No LP tokens") + visible: !root.hasLpTokens + + Layout.fillWidth: true + } + + Text { + color: "#A9A098" + font.pixelSize: 12 + lineHeight: 1.25 + text: qsTr("Add liquidity first to receive LP tokens before removing from this pool.") + visible: !root.hasLpTokens + wrapMode: Text.WordWrap + + Layout.fillWidth: true + } + + Rectangle { + color: root.hasLpTokens ? "#151515" : "#121212" + radius: 8 + border.color: burnField.activeFocus ? "#F26A21" : "#343434" + border.width: 1 + + Layout.fillWidth: true + Layout.preferredHeight: inputContent.implicitHeight + 20 + + ColumnLayout { + id: inputContent + + anchors.fill: parent + anchors.margins: 10 + spacing: 8 + + RowLayout { + spacing: 8 + + Layout.fillWidth: true + + Text { + color: "#A9A098" + elide: Text.ElideRight + font.pixelSize: 12 + text: qsTr("LP tokens to burn") + + Layout.fillWidth: true + } + + Text { + color: "#A9A098" + elide: Text.ElideRight + font.pixelSize: 11 + horizontalAlignment: Text.AlignRight + text: qsTr("Available LP: %1").arg(root.poolState.formatInteger(root.poolState.userLpBalance)) + + Layout.maximumWidth: 170 + } + } + + TextField { + id: burnField + + activeFocusOnTab: root.hasLpTokens + color: "#E7E1D8" + enabled: root.hasLpTokens + font.bold: true + font.pixelSize: 18 + inputMethodHints: Qt.ImhDigitsOnly + placeholderText: qsTr("0") + selectByMouse: true + selectedTextColor: "#151515" + selectionColor: "#F26A21" + text: root.burnAmount > 0 ? String(root.burnAmount) : "" + validator: RegularExpressionValidator { + regularExpression: /[0-9]*/ + } + + Accessible.name: qsTr("LP tokens to burn") + + Layout.fillWidth: true + Layout.minimumHeight: 44 + + onTextEdited: root.setBurnAmount(text) + + background: Rectangle { + border.color: burnField.activeFocus ? "#F26A21" : "#343434" + border.width: 1 + color: burnField.activeFocus ? "#1F1B18" : "#101010" + radius: 6 + } + } + } + } + + RowLayout { + spacing: 6 + + Layout.fillWidth: true + + Button { + id: preset25 + + activeFocusOnTab: root.hasLpTokens + enabled: root.hasLpTokens + focusPolicy: Qt.StrongFocus + hoverEnabled: true + text: qsTr("25%") + + Accessible.name: qsTr("Remove 25 percent") + + Layout.fillWidth: true + Layout.minimumHeight: 44 + + onClicked: root.setBurnPercent(25) + + contentItem: Text { + color: preset25.enabled && (preset25.hovered || preset25.activeFocus || root.preset25Amount === root.burnAmount) ? "#151515" : "#A9A098" + font.bold: true + font.pixelSize: 11 + horizontalAlignment: Text.AlignHCenter + text: preset25.text + verticalAlignment: Text.AlignVCenter + } + + background: Rectangle { + border.color: preset25.activeFocus || root.preset25Amount === root.burnAmount ? "#F26A21" : "#343434" + border.width: 1 + color: preset25.pressed ? "#D95C1E" : root.preset25Amount === root.burnAmount ? "#F26A21" : preset25.hovered || preset25.activeFocus ? "#E7E1D8" : "#151515" + radius: 6 + } + } + + Button { + id: preset50 + + activeFocusOnTab: root.hasLpTokens + enabled: root.hasLpTokens + focusPolicy: Qt.StrongFocus + hoverEnabled: true + text: qsTr("50%") + + Accessible.name: qsTr("Remove 50 percent") + + Layout.fillWidth: true + Layout.minimumHeight: 44 + + onClicked: root.setBurnPercent(50) + + contentItem: Text { + color: preset50.enabled && (preset50.hovered || preset50.activeFocus || root.preset50Amount === root.burnAmount) ? "#151515" : "#A9A098" + font.bold: true + font.pixelSize: 11 + horizontalAlignment: Text.AlignHCenter + text: preset50.text + verticalAlignment: Text.AlignVCenter + } + + background: Rectangle { + border.color: preset50.activeFocus || root.preset50Amount === root.burnAmount ? "#F26A21" : "#343434" + border.width: 1 + color: preset50.pressed ? "#D95C1E" : root.preset50Amount === root.burnAmount ? "#F26A21" : preset50.hovered || preset50.activeFocus ? "#E7E1D8" : "#151515" + radius: 6 + } + } + + Button { + id: preset75 + + activeFocusOnTab: root.hasLpTokens + enabled: root.hasLpTokens + focusPolicy: Qt.StrongFocus + hoverEnabled: true + text: qsTr("75%") + + Accessible.name: qsTr("Remove 75 percent") + + Layout.fillWidth: true + Layout.minimumHeight: 44 + + onClicked: root.setBurnPercent(75) + + contentItem: Text { + color: preset75.enabled && (preset75.hovered || preset75.activeFocus || root.preset75Amount === root.burnAmount) ? "#151515" : "#A9A098" + font.bold: true + font.pixelSize: 11 + horizontalAlignment: Text.AlignHCenter + text: preset75.text + verticalAlignment: Text.AlignVCenter + } + + background: Rectangle { + border.color: preset75.activeFocus || root.preset75Amount === root.burnAmount ? "#F26A21" : "#343434" + border.width: 1 + color: preset75.pressed ? "#D95C1E" : root.preset75Amount === root.burnAmount ? "#F26A21" : preset75.hovered || preset75.activeFocus ? "#E7E1D8" : "#151515" + radius: 6 + } + } + + Button { + id: presetMax + + activeFocusOnTab: root.hasLpTokens + enabled: root.hasLpTokens + focusPolicy: Qt.StrongFocus + hoverEnabled: true + text: qsTr("MAX") + + Accessible.name: qsTr("Remove maximum LP balance") + + Layout.fillWidth: true + Layout.minimumHeight: 44 + + onClicked: root.setBurnAmount(root.maxBurnAmount) + + contentItem: Text { + color: presetMax.enabled && (presetMax.hovered || presetMax.activeFocus || root.burnAmount === root.maxBurnAmount) ? "#151515" : "#A9A098" + font.bold: true + font.pixelSize: 11 + horizontalAlignment: Text.AlignHCenter + text: presetMax.text + verticalAlignment: Text.AlignVCenter + } + + background: Rectangle { + border.color: presetMax.activeFocus || root.burnAmount === root.maxBurnAmount ? "#F26A21" : "#343434" + border.width: 1 + color: presetMax.pressed ? "#D95C1E" : root.burnAmount === root.maxBurnAmount ? "#F26A21" : presetMax.hovered || presetMax.activeFocus ? "#E7E1D8" : "#151515" + radius: 6 + } + } + } + + ColumnLayout { + spacing: 6 + + Layout.fillWidth: true + + RowLayout { + spacing: 8 + + Layout.fillWidth: true + + Text { + color: "#A9A098" + font.pixelSize: 12 + text: qsTr("Pool share to remove") + + Layout.fillWidth: true + } + + Text { + color: "#E7E1D8" + font.bold: true + font.pixelSize: 12 + horizontalAlignment: Text.AlignRight + text: root.poolState.formatPercent(root.removePercent) + + Layout.maximumWidth: 72 + } + } + + Slider { + id: burnSlider + + activeFocusOnTab: root.hasLpTokens + enabled: root.hasLpTokens + from: 0 + stepSize: 1 + to: 100 + value: root.removePercent + + Accessible.name: qsTr("Pool share to remove") + Accessible.role: Accessible.Slider + + Layout.fillWidth: true + Layout.minimumHeight: 44 + + onMoved: root.setBurnPercent(Math.round(value)) + + background: Rectangle { + color: "#343434" + implicitHeight: 4 + radius: 2 + x: burnSlider.leftPadding + y: burnSlider.topPadding + burnSlider.availableHeight / 2 - height / 2 + + width: burnSlider.availableWidth + + Rectangle { + color: burnSlider.enabled ? "#F26A21" : "#56504A" + height: parent.height + radius: 2 + width: burnSlider.visualPosition * parent.width + } + } + + handle: Rectangle { + border.color: burnSlider.activeFocus ? "#E7E1D8" : "#F26A21" + border.width: 1 + color: burnSlider.enabled ? "#F26A21" : "#56504A" + height: 18 + radius: 9 + width: 18 + x: burnSlider.leftPadding + burnSlider.visualPosition * (burnSlider.availableWidth - width) + y: burnSlider.topPadding + burnSlider.availableHeight / 2 - height / 2 + } + } + } + + SummaryRow { + estimated: true + estimateHelp: root.estimateHelp + label: qsTr("Withdraw %1").arg(root.poolState.tokenA) + value: root.poolState.formatTokenAmount(root.preview.withdrawA, root.poolState.tokenA) + visible: root.burnAmount > 0 + + Layout.fillWidth: true + } + + SummaryRow { + estimated: true + estimateHelp: root.estimateHelp + label: qsTr("Withdraw %1").arg(root.poolState.tokenB) + value: root.poolState.formatTokenAmount(root.preview.withdrawB, root.poolState.tokenB) + visible: root.burnAmount > 0 + + Layout.fillWidth: true + } + + SlippageToleranceControl { + tolerancePercent: root.slippageTolerancePercent + + Layout.fillWidth: true + + onToleranceChangeRequested: function (tolerancePercent) { + root.slippageToleranceChangeRequested(tolerancePercent); + } + } + + SummaryRow { + label: qsTr("Min %1 received").arg(root.poolState.tokenA) + value: root.poolState.formatTokenAmount(root.minTokenAReceived, root.poolState.tokenA) + visible: root.burnAmount > 0 + + Layout.fillWidth: true + } + + SummaryRow { + label: qsTr("Min %1 received").arg(root.poolState.tokenB) + value: root.poolState.formatTokenAmount(root.minTokenBReceived, root.poolState.tokenB) + visible: root.burnAmount > 0 + + Layout.fillWidth: true + } + + Text { + color: "#F08A76" + font.pixelSize: 12 + lineHeight: 1.25 + text: qsTr("Minimum received is 0. Increase amount or lower slippage.") + visible: root.minReceivedIsZero + wrapMode: Text.WordWrap + + Layout.fillWidth: true + } + + SummaryRow { + estimated: true + estimateHelp: root.estimateHelp + label: qsTr("Position after") + value: root.poolState.formatPoolShare(root.preview.newUserShare) + visible: root.burnAmount > 0 + + 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) + }; + } +} diff --git a/amm-ui/qml/components/SlippageToleranceControl.qml b/amm-ui/qml/components/SlippageToleranceControl.qml new file mode 100644 index 0000000..08b6dd7 --- /dev/null +++ b/amm-ui/qml/components/SlippageToleranceControl.qml @@ -0,0 +1,259 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +Item { + id: root + + property real tolerancePercent: 0.5 + property string customText: "" + readonly property string thresholdText: root.tolerancePercent <= 1 ? qsTr("Standard slippage") : root.tolerancePercent <= 5 ? qsTr("Higher slippage") : qsTr("High slippage risk") + readonly property string thresholdIcon: root.tolerancePercent <= 1 ? "i" : root.tolerancePercent <= 5 ? "!" : "!!" + + signal toleranceChangeRequested(real tolerancePercent) + + implicitHeight: content.implicitHeight + + Component.onCompleted: root.restoreCustomText() + + onTolerancePercentChanged: { + if (!customField.activeFocus) { + root.restoreCustomText(); + } + } + + function formatTolerance(value) { + const amount = Number(value) || 0; + return amount.toFixed(2).replace(/0+$/, "").replace(/[.]$/, ""); + } + + function restoreCustomText() { + root.customText = root.formatTolerance(root.tolerancePercent); + } + + function clampTolerance(value) { + return Math.max(0.01, Math.min(50, Number(value) || 0)); + } + + function commitPreset(value) { + const nextValue = root.clampTolerance(value); + root.customText = root.formatTolerance(nextValue); + root.toleranceChangeRequested(nextValue); + } + + function commitCustom() { + const parsed = Number(root.customText); + + if (root.customText.length === 0 || !isFinite(parsed) || parsed < 0) { + root.restoreCustomText(); + return; + } + + root.commitPreset(parsed); + } + + ColumnLayout { + id: content + + anchors.fill: parent + spacing: 6 + + RowLayout { + spacing: 8 + + 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 { + spacing: 6 + + Layout.fillWidth: true + + Button { + id: preset01 + + readonly property real presetValue: 0.1 + readonly property bool selected: Math.abs(root.tolerancePercent - presetValue) < 0.000001 + + activeFocusOnTab: true + focusPolicy: Qt.StrongFocus + hoverEnabled: true + text: qsTr("0.1%") + + Accessible.name: qsTr("Set slippage tolerance to 0.1 percent") + + Layout.fillWidth: true + Layout.minimumHeight: 44 + + onClicked: root.commitPreset(presetValue) + + contentItem: Text { + color: preset01.selected ? "#F2D8C7" : preset01.hovered || preset01.activeFocus ? "#E7E1D8" : "#A9A098" + font.bold: true + font.pixelSize: 11 + horizontalAlignment: Text.AlignHCenter + text: preset01.text + verticalAlignment: Text.AlignVCenter + } + + background: Rectangle { + border.color: preset01.activeFocus || preset01.selected ? "#F26A21" : "#343434" + border.width: 1 + color: preset01.pressed ? "#2A1D16" : preset01.selected ? "#211914" : preset01.hovered || preset01.activeFocus ? "#202020" : "#101010" + radius: 6 + } + } + + Button { + id: preset05 + + readonly property real presetValue: 0.5 + readonly property bool selected: Math.abs(root.tolerancePercent - presetValue) < 0.000001 + + activeFocusOnTab: true + focusPolicy: Qt.StrongFocus + hoverEnabled: true + text: qsTr("0.5%") + + Accessible.name: qsTr("Set slippage tolerance to 0.5 percent") + + Layout.fillWidth: true + Layout.minimumHeight: 44 + + onClicked: root.commitPreset(presetValue) + + contentItem: Text { + color: preset05.selected ? "#F2D8C7" : preset05.hovered || preset05.activeFocus ? "#E7E1D8" : "#A9A098" + font.bold: true + font.pixelSize: 11 + horizontalAlignment: Text.AlignHCenter + text: preset05.text + verticalAlignment: Text.AlignVCenter + } + + background: Rectangle { + border.color: preset05.activeFocus || preset05.selected ? "#F26A21" : "#343434" + border.width: 1 + color: preset05.pressed ? "#2A1D16" : preset05.selected ? "#211914" : preset05.hovered || preset05.activeFocus ? "#202020" : "#101010" + radius: 6 + } + } + + Button { + id: preset10 + + readonly property real presetValue: 1.0 + readonly property bool selected: Math.abs(root.tolerancePercent - presetValue) < 0.000001 + + activeFocusOnTab: true + focusPolicy: Qt.StrongFocus + hoverEnabled: true + text: qsTr("1.0%") + + Accessible.name: qsTr("Set slippage tolerance to 1.0 percent") + + Layout.fillWidth: true + Layout.minimumHeight: 44 + + onClicked: root.commitPreset(presetValue) + + contentItem: Text { + color: preset10.selected ? "#F2D8C7" : preset10.hovered || preset10.activeFocus ? "#E7E1D8" : "#A9A098" + font.bold: true + font.pixelSize: 11 + horizontalAlignment: Text.AlignHCenter + text: preset10.text + verticalAlignment: Text.AlignVCenter + } + + background: Rectangle { + border.color: preset10.activeFocus || preset10.selected ? "#F26A21" : "#343434" + border.width: 1 + color: preset10.pressed ? "#2A1D16" : preset10.selected ? "#211914" : preset10.hovered || preset10.activeFocus ? "#202020" : "#101010" + radius: 6 + } + } + + Rectangle { + color: customField.activeFocus ? "#1F1B18" : "#101010" + radius: 6 + border.color: customField.activeFocus ? "#F26A21" : "#343434" + border.width: 1 + + Layout.minimumHeight: 44 + Layout.preferredWidth: 88 + + RowLayout { + spacing: 4 + + anchors { + fill: parent + leftMargin: 8 + rightMargin: 8 + } + + TextField { + id: customField + + activeFocusOnTab: true + color: "#E7E1D8" + font.bold: true + font.pixelSize: 12 + horizontalAlignment: Text.AlignRight + inputMethodHints: Qt.ImhFormattedNumbersOnly + placeholderText: qsTr("0.5") + selectByMouse: true + selectedTextColor: "#151515" + selectionColor: "#F26A21" + text: root.customText + validator: RegularExpressionValidator { + regularExpression: /[0-9]*([.][0-9]*)?/ + } + + Accessible.name: qsTr("Custom slippage tolerance percent") + + Layout.fillWidth: true + Layout.minimumHeight: 42 + + onEditingFinished: root.commitCustom() + onTextEdited: root.customText = text + Keys.onEscapePressed: { + root.restoreCustomText(); + customField.focus = false; + } + + background: Item {} + } + + Text { + color: "#A9A098" + font.bold: true + font.pixelSize: 12 + text: qsTr("%") + verticalAlignment: Text.AlignVCenter + + Layout.preferredWidth: 10 + } + } + } + } + } +} diff --git a/amm-ui/qml/components/SuccessToast.qml b/amm-ui/qml/components/SuccessToast.qml new file mode 100644 index 0000000..0c24253 --- /dev/null +++ b/amm-ui/qml/components/SuccessToast.qml @@ -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 + } + } + } + } +} diff --git a/amm-ui/qml/components/SummaryRow.qml b/amm-ui/qml/components/SummaryRow.qml new file mode 100644 index 0000000..0c34046 --- /dev/null +++ b/amm-ui/qml/components/SummaryRow.qml @@ -0,0 +1,60 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.15 + +Item { + id: root + + property string label: "" + property string value: "" + property bool estimated: false + property string estimateHelp: qsTr("This value is derived from your LP token balance, total LP supply, and current pool reserves.") + + implicitHeight: Math.max(18, Math.max(labelText.implicitHeight, valueGroup.implicitHeight)) + + RowLayout { + anchors.fill: parent + spacing: 8 + + Text { + id: labelText + + color: "#A9A098" + elide: Text.ElideRight + font.pixelSize: 12 + text: root.label + verticalAlignment: Text.AlignVCenter + + Layout.fillWidth: true + } + + RowLayout { + id: valueGroup + + spacing: 4 + + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + + Text { + color: "#E7E1D8" + elide: Text.ElideRight + font.bold: true + font.pixelSize: 12 + horizontalAlignment: Text.AlignRight + text: root.value + verticalAlignment: Text.AlignVCenter + + Layout.maximumWidth: Math.max(178, root.width * 0.55) + } + + EstimateInfoButton { + enabled: root.estimated + helpText: root.estimateHelp + opacity: root.estimated ? 1 : 0 + visible: root.estimated + + Layout.preferredHeight: 18 + Layout.preferredWidth: root.estimated ? 18 : 0 + } + } + } +} diff --git a/amm-ui/qml/components/TokenAmountInput.qml b/amm-ui/qml/components/TokenAmountInput.qml new file mode 100644 index 0000000..120be57 --- /dev/null +++ b/amm-ui/qml/components/TokenAmountInput.qml @@ -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 + } + } + } +} diff --git a/amm-ui/qml/pages/LiquidityPage.qml b/amm-ui/qml/pages/LiquidityPage.qml new file mode 100644 index 0000000..c2aa033 --- /dev/null +++ b/amm-ui/qml/pages/LiquidityPage.qml @@ -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")); + } + } +} diff --git a/amm-ui/qml/state/DummyPoolState.qml b/amm-ui/qml/state/DummyPoolState.qml new file mode 100644 index 0000000..7e4d573 --- /dev/null +++ b/amm-ui/qml/state/DummyPoolState.qml @@ -0,0 +1,205 @@ +import QtQuick 2.15 + +QtObject { + id: root + + property string tokenA: "USDC" + property string tokenB: "ETH" + property string feeTier: "0.30%" + property real userLpBalance: 1118033 + property real reserveA: 1000000 + property real reserveB: 500 + property real totalLpSupply: 22360679 + property real walletBalanceA: 60000 + property real walletBalanceB: 20 + readonly property real minimumLiquidity: 1000 + + readonly property real poolShare: totalLpSupply > 0 ? userLpBalance / totalLpSupply : 0 + readonly property real userOwnedA: reserveA * poolShare + readonly property real userOwnedB: reserveB * poolShare + readonly property real tokenAPerTokenB: reserveB > 0 ? Math.floor(reserveA / reserveB) : 0 + + function applyAddLiquidity(actualA, actualB, mintedLp) { + const safeA = Math.max(0, Number(actualA) || 0); + const safeB = Math.max(0, Number(actualB) || 0); + const safeLp = Math.max(0, Number(mintedLp) || 0); + + reserveA += safeA; + reserveB += safeB; + totalLpSupply += safeLp; + userLpBalance += safeLp; + } + + function applyRemoveLiquidity(withdrawA, withdrawB, burnedLp) { + const safeA = Math.max(0, Number(withdrawA) || 0); + const safeB = Math.max(0, Number(withdrawB) || 0); + const safeLp = Math.max(0, Number(burnedLp) || 0); + + reserveA = Math.max(0, reserveA - safeA); + reserveB = Math.max(0, reserveB - safeB); + totalLpSupply = Math.max(0, totalLpSupply - safeLp); + userLpBalance = Math.max(0, userLpBalance - safeLp); + } + + function resetDummyState() { + tokenA = "USDC"; + tokenB = "ETH"; + feeTier = "0.30%"; + userLpBalance = 1118033; + reserveA = 1000000; + reserveB = 500; + 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 clampBurnAmount(value) { + return Math.min(floorAmount(value), Math.max(0, floorAmount(userLpBalance))); + } + + function clampSlippageTolerancePercent(value) { + return Math.max(0.01, Math.min(50, Number(value) || 0)); + } + + function minReceivedAmount(previewAmount, slippageTolerancePercent) { + const safeAmount = floorAmount(previewAmount); + const safeSlippage = clampSlippageTolerancePercent(slippageTolerancePercent); + + return Math.floor(safeAmount * (1 - safeSlippage / 100)); + } + + function burnAmountForPercent(percent) { + const safePercent = Math.max(0, Math.min(100, Number(percent) || 0)); + + if (safePercent === 100) { + return clampBurnAmount(userLpBalance); + } + + return clampBurnAmount(Math.floor(userLpBalance * safePercent / 100)); + } + + function removeLiquidityPreview(burnedLp) { + const safeBurnedLp = totalLpSupply > 0 ? Math.min(clampBurnAmount(burnedLp), floorAmount(totalLpSupply)) : 0; + const withdrawA = totalLpSupply > 0 ? Math.floor(reserveA * safeBurnedLp / totalLpSupply) : 0; + const withdrawB = totalLpSupply > 0 ? Math.floor(reserveB * safeBurnedLp / totalLpSupply) : 0; + const newTotalLpSupply = Math.max(0, floorAmount(totalLpSupply) - safeBurnedLp); + const newUserLpBalance = Math.max(0, floorAmount(userLpBalance) - safeBurnedLp); + + return { + "burnedLp": safeBurnedLp, + "newReserveA": Math.max(0, reserveA - withdrawA), + "newReserveB": Math.max(0, reserveB - withdrawB), + "newTotalLpSupply": newTotalLpSupply, + "newUserLpBalance": newUserLpBalance, + "newUserShare": newTotalLpSupply > 0 ? newUserLpBalance / newTotalLpSupply : 0, + "withdrawA": withdrawA, + "withdrawB": withdrawB + }; + } + + function formatInteger(value) { + const rounded = Math.round(Number(value) || 0); + 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 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) { + return formatDecimal(value); + } + + function formatTokenAmount(value, token) { + return formatDecimal(value) + " " + token; + } + + function formatCompactTokenAmount(value, token) { + return formatCompactDecimal(value) + " " + token; + } + + function formatLpAmount(value) { + return formatInteger(value) + " LP"; + } + + function formatPoolShare(value) { + return "\u2248 " + (Math.max(0, Number(value) || 0) * 100).toFixed(2) + "%"; + } + + function formatPercent(value) { + const amount = Math.max(0, Number(value) || 0); + + if (Math.abs(amount - Math.round(amount)) < 0.000001) { + return Math.round(amount).toString() + "%"; + } + + return amount.toFixed(2).replace(/0+$/, "").replace(/[.]$/, "") + "%"; + } +}