mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-07-17 12:29:27 +00:00
feat(amm-ui): add new position prototype #221
This commit is contained in:
parent
bfeff4f0b4
commit
8db90fda35
@ -0,0 +1,230 @@
|
|||||||
|
import QtQuick 2.15
|
||||||
|
import QtQuick.Controls 2.15
|
||||||
|
import QtQuick.Layouts 1.15
|
||||||
|
|
||||||
|
FocusScope {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var snapshot: ({})
|
||||||
|
property bool open: false
|
||||||
|
|
||||||
|
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(420, 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: qsTr("Confirm new position")
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
color: "#151515"
|
||||||
|
radius: 8
|
||||||
|
border.color: "#343434"
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: summaryLayout.implicitHeight + 20
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: summaryLayout
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 10
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
SummaryRow {
|
||||||
|
label: qsTr("Pair")
|
||||||
|
value: root.snapshot.pairText || ""
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
SummaryRow {
|
||||||
|
label: qsTr("Instruction")
|
||||||
|
value: root.snapshot.instructionText || ""
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
SummaryRow {
|
||||||
|
label: qsTr("Fee tier")
|
||||||
|
value: root.snapshot.feeLabel || ""
|
||||||
|
|
||||||
|
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("Expected LP")
|
||||||
|
value: root.snapshot.expectedLp || ""
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
SummaryRow {
|
||||||
|
label: qsTr("Minimum LP")
|
||||||
|
value: root.snapshot.minimumLp || ""
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
SummaryRow {
|
||||||
|
label: qsTr("Quote hash")
|
||||||
|
value: root.snapshot.shortQuoteHash || ""
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
color: "#A9A098"
|
||||||
|
font.pixelSize: 12
|
||||||
|
lineHeight: 1.25
|
||||||
|
text: qsTr("Submit will re-quote against current wallet and chain state before dispatch.")
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
|
||||||
|
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("Submit")
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1168
apps/amm/qml/components/liquidity/NewPositionForm.qml
Normal file
1168
apps/amm/qml/components/liquidity/NewPositionForm.qml
Normal file
File diff suppressed because it is too large
Load Diff
@ -10,6 +10,8 @@ Rectangle {
|
|||||||
property string errorText: ""
|
property string errorText: ""
|
||||||
property string helperText: ""
|
property string helperText: ""
|
||||||
property string label: ""
|
property string label: ""
|
||||||
|
property bool readOnly: false
|
||||||
|
property bool showMaxButton: true
|
||||||
property string token: ""
|
property string token: ""
|
||||||
|
|
||||||
signal editingChanged(string value)
|
signal editingChanged(string value)
|
||||||
@ -71,6 +73,7 @@ Rectangle {
|
|||||||
font.pixelSize: 18
|
font.pixelSize: 18
|
||||||
inputMethodHints: Qt.ImhFormattedNumbersOnly
|
inputMethodHints: Qt.ImhFormattedNumbersOnly
|
||||||
placeholderText: qsTr("0")
|
placeholderText: qsTr("0")
|
||||||
|
readOnly: root.readOnly
|
||||||
selectByMouse: true
|
selectByMouse: true
|
||||||
selectedTextColor: "#151515"
|
selectedTextColor: "#151515"
|
||||||
selectionColor: "#F26A21"
|
selectionColor: "#F26A21"
|
||||||
@ -83,7 +86,10 @@ Rectangle {
|
|||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.minimumHeight: 44
|
Layout.minimumHeight: 44
|
||||||
|
|
||||||
onTextEdited: root.editingChanged(text)
|
onTextEdited: {
|
||||||
|
if (!root.readOnly)
|
||||||
|
root.editingChanged(text);
|
||||||
|
}
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
border.color: amountField.activeFocus ? "#F26A21" : "#343434"
|
border.color: amountField.activeFocus ? "#F26A21" : "#343434"
|
||||||
@ -97,14 +103,16 @@ Rectangle {
|
|||||||
id: maxButton
|
id: maxButton
|
||||||
|
|
||||||
activeFocusOnTab: true
|
activeFocusOnTab: true
|
||||||
|
enabled: root.showMaxButton && !root.readOnly
|
||||||
focusPolicy: Qt.StrongFocus
|
focusPolicy: Qt.StrongFocus
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
text: qsTr("MAX")
|
text: qsTr("MAX")
|
||||||
|
visible: root.showMaxButton
|
||||||
|
|
||||||
Accessible.name: qsTr("Use maximum %1 balance").arg(root.token)
|
Accessible.name: qsTr("Use maximum %1 balance").arg(root.token)
|
||||||
|
|
||||||
Layout.minimumHeight: 44
|
Layout.minimumHeight: 44
|
||||||
Layout.preferredWidth: 58
|
Layout.preferredWidth: visible ? 58 : 0
|
||||||
|
|
||||||
onClicked: root.maxClicked()
|
onClicked: root.maxClicked()
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
import QtQuick 2.15
|
import QtQuick 2.15
|
||||||
import QtQuick.Layouts 1.15
|
|
||||||
import Logos.Wallet
|
|
||||||
import "../components/shared"
|
import "../components/shared"
|
||||||
import "../components/liquidity"
|
import "../components/liquidity"
|
||||||
import "../state"
|
import "../state"
|
||||||
@ -8,19 +6,17 @@ import "../state"
|
|||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property int activeLiquidityTab: 0
|
|
||||||
property real slippageTolerancePercent: 0.5
|
|
||||||
readonly property int pageMargin: 16
|
readonly property int pageMargin: 16
|
||||||
readonly property int preferredCardWidth: 492
|
readonly property int preferredCardWidth: 960
|
||||||
readonly property int pageCardY: pageCard.implicitHeight + root.pageMargin * 2 <= scroll.height ? Math.round((scroll.height - pageCard.implicitHeight) / 2) : root.pageMargin
|
readonly property int pageCardY: newPositionForm.implicitHeight + root.pageMargin * 2 <= scroll.height ? Math.max(root.pageMargin, Math.round((scroll.height - newPositionForm.implicitHeight) / 4)) : root.pageMargin
|
||||||
|
|
||||||
width: parent ? parent.width : implicitWidth
|
width: parent ? parent.width : implicitWidth
|
||||||
height: parent ? parent.height : implicitHeight
|
height: parent ? parent.height : implicitHeight
|
||||||
implicitWidth: root.preferredCardWidth + root.pageMargin * 2
|
implicitWidth: root.preferredCardWidth + root.pageMargin * 2
|
||||||
implicitHeight: pageCard.implicitHeight + root.pageMargin * 2
|
implicitHeight: newPositionForm.implicitHeight + root.pageMargin * 2
|
||||||
|
|
||||||
DummyPoolState {
|
NewPositionPrototypeBackend {
|
||||||
id: poolState
|
id: newPositionBackend
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@ -33,166 +29,57 @@ Item {
|
|||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
clip: true
|
clip: true
|
||||||
contentHeight: Math.max(height, pageCard.y + pageCard.implicitHeight + root.pageMargin)
|
contentHeight: Math.max(height, newPositionForm.y + newPositionForm.implicitHeight + root.pageMargin)
|
||||||
contentWidth: width
|
contentWidth: width
|
||||||
enabled: !confirmationDialog.visible
|
enabled: !confirmationDialog.open
|
||||||
flickableDirection: Flickable.VerticalFlick
|
flickableDirection: Flickable.VerticalFlick
|
||||||
|
|
||||||
Rectangle {
|
NewPositionForm {
|
||||||
id: pageCard
|
id: newPositionForm
|
||||||
|
|
||||||
color: "#1B1B1B"
|
prototypeBackend: newPositionBackend
|
||||||
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))
|
width: Math.max(0, Math.min(scroll.width - root.pageMargin * 2, root.preferredCardWidth))
|
||||||
x: Math.max(root.pageMargin, (scroll.width - width) / 2)
|
x: Math.max(root.pageMargin, (scroll.width - width) / 2)
|
||||||
y: root.pageCardY
|
y: root.pageCardY
|
||||||
|
|
||||||
ColumnLayout {
|
onConfirmationRequested: function (snapshot) {
|
||||||
id: shellContent
|
confirmationDialog.openWithSnapshot(snapshot);
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
SuccessToast {
|
||||||
id: liquidityConfirmationSummary
|
id: successToast
|
||||||
|
|
||||||
LiquidityConfirmationSummary { }
|
width: Math.max(0, Math.min(380, root.width - 32))
|
||||||
|
z: 30
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
bottom: parent.bottom
|
||||||
|
bottomMargin: 18
|
||||||
|
horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TransactionConfirmationDialog {
|
NewPositionConfirmationDialog {
|
||||||
id: confirmationDialog
|
id: confirmationDialog
|
||||||
title: snapshot.action === "add"
|
|
||||||
? qsTr("Confirm add liquidity")
|
anchors.fill: parent
|
||||||
: qsTr("Confirm remove liquidity")
|
|
||||||
summary: liquidityConfirmationSummary
|
|
||||||
|
|
||||||
onConfirmed: function (snapshot) {
|
onConfirmed: function (snapshot) {
|
||||||
root.confirmLiquidityAction(snapshot);
|
root.confirmNewPosition(snapshot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmLiquidityAction(snapshot) {
|
function confirmNewPosition(snapshot) {
|
||||||
if (snapshot.action === "add") {
|
const result = newPositionBackend.submitNewPosition(snapshot.request, snapshot.quoteHash);
|
||||||
poolState.applyAddLiquidity(snapshot.actualA, snapshot.actualB, snapshot.deltaLp);
|
|
||||||
addLiquidityForm.resetForm();
|
if (result.status !== "ok") {
|
||||||
successToast.show(qsTr("Liquidity added"), qsTr("Position updated"));
|
newPositionForm.setSubmitError(result.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (snapshot.action === "remove") {
|
newPositionForm.resetAfterSubmit();
|
||||||
poolState.applyRemoveLiquidity(snapshot.withdrawA, snapshot.withdrawB, snapshot.burnAmount);
|
successToast.show(result.message, result.detail);
|
||||||
removeLiquidityForm.resetForm();
|
|
||||||
successToast.show(qsTr("Liquidity removed"), qsTr("Position updated"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
468
apps/amm/qml/state/NewPositionPrototypeBackend.qml
Normal file
468
apps/amm/qml/state/NewPositionPrototypeBackend.qml
Normal file
@ -0,0 +1,468 @@
|
|||||||
|
import QtQuick 2.15
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property string activeAccount: "lz1q9s...42fd"
|
||||||
|
readonly property var holdings: [
|
||||||
|
{
|
||||||
|
"symbol": "USDC",
|
||||||
|
"name": "USD Coin",
|
||||||
|
"definitionId": "token:usdc-testnet",
|
||||||
|
"holdingId": "holding:active:usdc",
|
||||||
|
"balance": 12450,
|
||||||
|
"balanceText": "12,450.00 USDC",
|
||||||
|
"accent": "#2E7CF6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "LOGOS",
|
||||||
|
"name": "Logos",
|
||||||
|
"definitionId": "token:logos-testnet",
|
||||||
|
"holdingId": "holding:active:logos",
|
||||||
|
"balance": 850000,
|
||||||
|
"balanceText": "850,000 LOGOS",
|
||||||
|
"accent": "#F26A21"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "WETH",
|
||||||
|
"name": "Wrapped Ether",
|
||||||
|
"definitionId": "token:weth-testnet",
|
||||||
|
"holdingId": "holding:active:weth",
|
||||||
|
"balance": 3.25,
|
||||||
|
"balanceText": "3.25 WETH",
|
||||||
|
"accent": "#B7C2D8"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
readonly property var feeTiers: [
|
||||||
|
{
|
||||||
|
"bps": 1,
|
||||||
|
"label": "0.01%",
|
||||||
|
"supported": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bps": 5,
|
||||||
|
"label": "0.05%",
|
||||||
|
"supported": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bps": 25,
|
||||||
|
"label": "0.25%",
|
||||||
|
"supported": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bps": 30,
|
||||||
|
"label": "0.30%",
|
||||||
|
"supported": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bps": 100,
|
||||||
|
"label": "1.00%",
|
||||||
|
"supported": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
readonly property real minimumLiquidity: 1000
|
||||||
|
|
||||||
|
function holdingBySymbol(symbol) {
|
||||||
|
for (let i = 0; i < root.holdings.length; ++i) {
|
||||||
|
if (root.holdings[i].symbol === symbol)
|
||||||
|
return root.holdings[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function feeTierByBps(bps) {
|
||||||
|
for (let i = 0; i < root.feeTiers.length; ++i) {
|
||||||
|
if (root.feeTiers[i].bps === bps)
|
||||||
|
return root.feeTiers[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function feeLabel(bps) {
|
||||||
|
const tier = feeTierByBps(bps);
|
||||||
|
return tier ? tier.label : qsTr("%1 bps").arg(bps);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAmount(value) {
|
||||||
|
const parsed = Number(value);
|
||||||
|
return isFinite(parsed) && parsed > 0 ? parsed : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAmount(value) {
|
||||||
|
const amount = Math.max(0, Number(value) || 0);
|
||||||
|
|
||||||
|
if (amount >= 1000)
|
||||||
|
return amount.toFixed(2).replace(/\.00$/, "").replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||||
|
|
||||||
|
if (amount >= 1)
|
||||||
|
return amount.toFixed(4).replace(/0+$/, "").replace(/[.]$/, "");
|
||||||
|
|
||||||
|
return amount.toFixed(6).replace(/0+$/, "").replace(/[.]$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTokenAmount(value, symbol) {
|
||||||
|
return qsTr("%1 %2").arg(formatAmount(value)).arg(symbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unorderedPairKey(symbolA, symbolB) {
|
||||||
|
return symbolA < symbolB ? symbolA + "/" + symbolB : symbolB + "/" + symbolA;
|
||||||
|
}
|
||||||
|
|
||||||
|
function poolContext(symbolA, symbolB) {
|
||||||
|
if (!symbolA || !symbolB || symbolA === symbolB) {
|
||||||
|
return {
|
||||||
|
"poolStatus": "unavailable_pool",
|
||||||
|
"statusLabel": qsTr("Unavailable"),
|
||||||
|
"detail": qsTr("Choose two different assets from the active account."),
|
||||||
|
"instruction": "",
|
||||||
|
"storedFeeBps": 0,
|
||||||
|
"poolId": "",
|
||||||
|
"priceText": "",
|
||||||
|
"reserveText": ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = unorderedPairKey(symbolA, symbolB);
|
||||||
|
|
||||||
|
if (key === "LOGOS/USDC") {
|
||||||
|
return {
|
||||||
|
"poolStatus": "active_pool",
|
||||||
|
"statusLabel": qsTr("Active pool"),
|
||||||
|
"detail": qsTr("Deposits are quoted against the existing pool ratio. Nonmatching fee tiers are locked."),
|
||||||
|
"instruction": "add_liquidity",
|
||||||
|
"storedFeeBps": 30,
|
||||||
|
"poolId": "pool:usdc-logos",
|
||||||
|
"priceText": qsTr("1 USDC = 8 LOGOS"),
|
||||||
|
"reserveText": qsTr("1,250,000 USDC / 10,000,000 LOGOS")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "USDC/WETH") {
|
||||||
|
return {
|
||||||
|
"poolStatus": "missing_pool",
|
||||||
|
"statusLabel": qsTr("Missing pool"),
|
||||||
|
"detail": qsTr("Set the initial price first, then scale both deposits together."),
|
||||||
|
"instruction": "new_definition",
|
||||||
|
"storedFeeBps": 0,
|
||||||
|
"poolId": "pool:usdc-weth",
|
||||||
|
"priceText": qsTr("No reserves yet"),
|
||||||
|
"reserveText": qsTr("Pool account is empty")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"poolStatus": "unavailable_pool",
|
||||||
|
"statusLabel": qsTr("Unavailable"),
|
||||||
|
"detail": qsTr("A pool account exists, but it cannot be quoted safely for this prototype state."),
|
||||||
|
"instruction": "",
|
||||||
|
"storedFeeBps": 0,
|
||||||
|
"poolId": "pool:logos-weth",
|
||||||
|
"priceText": qsTr("Quote disabled"),
|
||||||
|
"reserveText": qsTr("Unsupported stored pool state")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function activeQuote(request, context) {
|
||||||
|
const tokenA = holdingBySymbol(request.tokenA);
|
||||||
|
const tokenB = holdingBySymbol(request.tokenB);
|
||||||
|
const inputA = parseAmount(request.amountA);
|
||||||
|
const inputB = parseAmount(request.amountB);
|
||||||
|
const editA = request.editedSide !== "B";
|
||||||
|
const amountA = editA ? inputA : inputB / activeRatio(request.tokenA, request.tokenB);
|
||||||
|
const amountB = editA ? inputA * activeRatio(request.tokenA, request.tokenB) : inputB;
|
||||||
|
const expectedLp = Math.floor(Math.min(amountA * 5.5, amountB * 0.69));
|
||||||
|
const minLp = Math.floor(expectedLp * (10000 - request.slippageBps) / 10000);
|
||||||
|
|
||||||
|
if (inputA <= 0 && inputB <= 0)
|
||||||
|
return quoteError(context, request, qsTr("Enter a deposit amount to preview LP output."));
|
||||||
|
|
||||||
|
if (amountA > tokenA.balance)
|
||||||
|
return quoteError(context, request, qsTr("Insufficient %1 balance.").arg(tokenA.symbol));
|
||||||
|
|
||||||
|
if (amountB > tokenB.balance)
|
||||||
|
return quoteError(context, request, qsTr("Insufficient %1 balance.").arg(tokenB.symbol));
|
||||||
|
|
||||||
|
if (minLp <= 0)
|
||||||
|
return quoteError(context, request, qsTr("LP minimum rounds to zero. Increase deposit amount."));
|
||||||
|
|
||||||
|
return quoteOk(context, request, {
|
||||||
|
"maxA": amountA,
|
||||||
|
"maxB": amountB,
|
||||||
|
"actualA": amountA,
|
||||||
|
"actualB": amountB,
|
||||||
|
"expectedLp": expectedLp,
|
||||||
|
"minimumLp": minLp,
|
||||||
|
"lockedLp": 0,
|
||||||
|
"position": {
|
||||||
|
"userLp": "148,320 LP",
|
||||||
|
"share": "1.18%",
|
||||||
|
"ownedA": request.tokenA === "USDC" ? "14,750 USDC" : "118,000 LOGOS",
|
||||||
|
"ownedB": request.tokenB === "LOGOS" ? "118,000 LOGOS" : "14,750 USDC"
|
||||||
|
},
|
||||||
|
"accountChanges": [
|
||||||
|
{
|
||||||
|
"role": qsTr("Config"),
|
||||||
|
"id": "amm:config",
|
||||||
|
"action": qsTr("Read")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": qsTr("Pool"),
|
||||||
|
"id": context.poolId,
|
||||||
|
"action": qsTr("Update")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": qsTr("Vault A"),
|
||||||
|
"id": "vault:" + request.tokenA.toLowerCase(),
|
||||||
|
"action": qsTr("Update")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": qsTr("Vault B"),
|
||||||
|
"id": "vault:" + request.tokenB.toLowerCase(),
|
||||||
|
"action": qsTr("Update")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": qsTr("User LP holding"),
|
||||||
|
"id": "holding:active:lp",
|
||||||
|
"action": qsTr("Update or create")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": qsTr("Clock"),
|
||||||
|
"id": "clock:canonical",
|
||||||
|
"action": qsTr("Read")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function missingQuote(request, context) {
|
||||||
|
const tokenA = holdingBySymbol(request.tokenA);
|
||||||
|
const tokenB = holdingBySymbol(request.tokenB);
|
||||||
|
const price = parseAmount(request.initialPrice) || defaultInitialPrice(request.tokenA, request.tokenB);
|
||||||
|
const scale = Math.max(1, Number(request.depositScale) || 1);
|
||||||
|
const amountA = price >= 1 ? price * scale : scale;
|
||||||
|
const amountB = price >= 1 ? scale : scale / price;
|
||||||
|
const expectedLp = Math.floor(Math.sqrt(amountA * amountB) * 48);
|
||||||
|
const userLp = Math.max(0, expectedLp - root.minimumLiquidity);
|
||||||
|
const minLp = Math.floor(userLp * (10000 - request.slippageBps) / 10000);
|
||||||
|
|
||||||
|
if (amountA > tokenA.balance)
|
||||||
|
return quoteError(context, request, qsTr("Initial deposit exceeds %1 balance.").arg(tokenA.symbol));
|
||||||
|
|
||||||
|
if (amountB > tokenB.balance)
|
||||||
|
return quoteError(context, request, qsTr("Initial deposit exceeds %1 balance.").arg(tokenB.symbol));
|
||||||
|
|
||||||
|
if (userLp <= 0)
|
||||||
|
return quoteError(context, request, qsTr("Deposit must mint more than the locked minimum liquidity."));
|
||||||
|
|
||||||
|
return quoteOk(context, request, {
|
||||||
|
"maxA": amountA,
|
||||||
|
"maxB": amountB,
|
||||||
|
"actualA": amountA,
|
||||||
|
"actualB": amountB,
|
||||||
|
"expectedLp": userLp,
|
||||||
|
"minimumLp": minLp,
|
||||||
|
"lockedLp": root.minimumLiquidity,
|
||||||
|
"position": {
|
||||||
|
"userLp": "0 LP",
|
||||||
|
"share": "New pool",
|
||||||
|
"ownedA": "0 " + request.tokenA,
|
||||||
|
"ownedB": "0 " + request.tokenB
|
||||||
|
},
|
||||||
|
"accountChanges": [
|
||||||
|
{
|
||||||
|
"role": qsTr("Config"),
|
||||||
|
"id": "amm:config",
|
||||||
|
"action": qsTr("Read")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": qsTr("Pool"),
|
||||||
|
"id": context.poolId,
|
||||||
|
"action": qsTr("Create")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": qsTr("Vault A"),
|
||||||
|
"id": "vault:" + request.tokenA.toLowerCase(),
|
||||||
|
"action": qsTr("Update or create")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": qsTr("Vault B"),
|
||||||
|
"id": "vault:" + request.tokenB.toLowerCase(),
|
||||||
|
"action": qsTr("Update or create")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": qsTr("LP definition"),
|
||||||
|
"id": "lp:" + context.poolId,
|
||||||
|
"action": qsTr("Create")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": qsTr("LP lock holding"),
|
||||||
|
"id": "holding:lp-lock",
|
||||||
|
"action": qsTr("Create")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": qsTr("User LP holding"),
|
||||||
|
"id": "holding:active:lp",
|
||||||
|
"action": qsTr("Update or create")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": qsTr("Current tick"),
|
||||||
|
"id": "twap:" + context.poolId,
|
||||||
|
"action": qsTr("Create")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": qsTr("Clock"),
|
||||||
|
"id": "clock:canonical",
|
||||||
|
"action": qsTr("Read")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function quoteNewPosition(request) {
|
||||||
|
const context = poolContext(request.tokenA, request.tokenB);
|
||||||
|
const tier = feeTierByBps(request.feeBps);
|
||||||
|
|
||||||
|
if (!tier || !tier.supported)
|
||||||
|
return quoteError(context, request, qsTr("Fee tier is not supported by the AMM program."));
|
||||||
|
|
||||||
|
if (context.poolStatus === "active_pool" && request.feeBps !== context.storedFeeBps)
|
||||||
|
return quoteError(context, request, qsTr("Existing pool uses %1.").arg(feeLabel(context.storedFeeBps)));
|
||||||
|
|
||||||
|
if (context.poolStatus === "active_pool")
|
||||||
|
return activeQuote(request, context);
|
||||||
|
|
||||||
|
if (context.poolStatus === "missing_pool")
|
||||||
|
return missingQuote(request, context);
|
||||||
|
|
||||||
|
return quoteError(context, request, context.detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitNewPosition(request, quoteHash) {
|
||||||
|
const quote = quoteNewPosition(request);
|
||||||
|
|
||||||
|
if (quote.status !== "ok") {
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"error": quote.error
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quote.quoteHash !== quoteHash) {
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"error": qsTr("Quote changed. Refresh preview before submitting.")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"message": quote.instruction === "new_definition" ? qsTr("Pool creation submitted") : qsTr("Liquidity deposit submitted"),
|
||||||
|
"detail": qsTr("%1 / %2").arg(request.tokenA).arg(request.tokenB)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function quoteOk(context, request, amounts) {
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"error": "",
|
||||||
|
"poolStatus": context.poolStatus,
|
||||||
|
"statusLabel": context.statusLabel,
|
||||||
|
"statusDetail": context.detail,
|
||||||
|
"instruction": context.instruction,
|
||||||
|
"feeBps": request.feeBps,
|
||||||
|
"feeLabel": feeLabel(request.feeBps),
|
||||||
|
"quoteHash": quoteHash(request),
|
||||||
|
"pool": {
|
||||||
|
"id": context.poolId,
|
||||||
|
"priceText": context.poolStatus === "missing_pool" ? qsTr("1 %1 = %2 %3").arg(request.tokenB).arg(formatAmount(parseAmount(request.initialPrice) || defaultInitialPrice(request.tokenA, request.tokenB))).arg(request.tokenA) : context.priceText,
|
||||||
|
"reserveText": context.reserveText
|
||||||
|
},
|
||||||
|
"deposit": {
|
||||||
|
"maxA": amountValue(amounts.maxA, request.tokenA),
|
||||||
|
"maxB": amountValue(amounts.maxB, request.tokenB),
|
||||||
|
"actualA": amountValue(amounts.actualA, request.tokenA),
|
||||||
|
"actualB": amountValue(amounts.actualB, request.tokenB)
|
||||||
|
},
|
||||||
|
"lp": {
|
||||||
|
"expected": amountValue(amounts.expectedLp, "LP"),
|
||||||
|
"minimum": amountValue(amounts.minimumLp, "LP"),
|
||||||
|
"locked": amountValue(amounts.lockedLp, "LP")
|
||||||
|
},
|
||||||
|
"position": amounts.position,
|
||||||
|
"accountChanges": amounts.accountChanges
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function quoteError(context, request, errorText) {
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"error": errorText,
|
||||||
|
"poolStatus": context.poolStatus,
|
||||||
|
"statusLabel": context.statusLabel,
|
||||||
|
"statusDetail": context.detail,
|
||||||
|
"instruction": context.instruction,
|
||||||
|
"feeBps": request.feeBps,
|
||||||
|
"feeLabel": feeLabel(request.feeBps),
|
||||||
|
"quoteHash": quoteHash(request),
|
||||||
|
"pool": {
|
||||||
|
"id": context.poolId,
|
||||||
|
"priceText": context.priceText,
|
||||||
|
"reserveText": context.reserveText
|
||||||
|
},
|
||||||
|
"deposit": {
|
||||||
|
"maxA": amountValue(0, request.tokenA),
|
||||||
|
"maxB": amountValue(0, request.tokenB),
|
||||||
|
"actualA": amountValue(0, request.tokenA),
|
||||||
|
"actualB": amountValue(0, request.tokenB)
|
||||||
|
},
|
||||||
|
"lp": {
|
||||||
|
"expected": amountValue(0, "LP"),
|
||||||
|
"minimum": amountValue(0, "LP"),
|
||||||
|
"locked": amountValue(context.poolStatus === "missing_pool" ? root.minimumLiquidity : 0, "LP")
|
||||||
|
},
|
||||||
|
"position": {
|
||||||
|
"userLp": "0 LP",
|
||||||
|
"share": "-",
|
||||||
|
"ownedA": "0 " + request.tokenA,
|
||||||
|
"ownedB": "0 " + request.tokenB
|
||||||
|
},
|
||||||
|
"accountChanges": []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function amountValue(value, symbol) {
|
||||||
|
const amount = Math.max(0, Number(value) || 0);
|
||||||
|
return {
|
||||||
|
"value": amount,
|
||||||
|
"input": formatAmount(amount),
|
||||||
|
"display": formatTokenAmount(amount, symbol),
|
||||||
|
"symbol": symbol
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function activeRatio(symbolA, symbolB) {
|
||||||
|
if (symbolA === "USDC" && symbolB === "LOGOS")
|
||||||
|
return 8;
|
||||||
|
|
||||||
|
if (symbolA === "LOGOS" && symbolB === "USDC")
|
||||||
|
return 0.125;
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultInitialPrice(symbolA, symbolB) {
|
||||||
|
if (symbolA === "USDC" && symbolB === "WETH")
|
||||||
|
return 2500;
|
||||||
|
|
||||||
|
if (symbolA === "WETH" && symbolB === "USDC")
|
||||||
|
return 0.0004;
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function quoteHash(request) {
|
||||||
|
return "demo-" + request.tokenA + "-" + request.tokenB + "-" + request.feeBps + "-" + request.editedSide + "-" + request.amountA + "-" + request.amountB + "-" + request.initialPrice + "-" + request.depositScale + "-" + request.slippageBps;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user