mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-07-14 10:59:29 +00:00
feat(amm-ui): add new position prototype #221
This commit is contained in:
parent
e8c70fb2a5
commit
07533bc799
@ -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 helperText: ""
|
||||
property string label: ""
|
||||
property bool readOnly: false
|
||||
property bool showMaxButton: true
|
||||
property string token: ""
|
||||
|
||||
signal editingChanged(string value)
|
||||
@ -71,6 +73,7 @@ Rectangle {
|
||||
font.pixelSize: 18
|
||||
inputMethodHints: Qt.ImhFormattedNumbersOnly
|
||||
placeholderText: qsTr("0")
|
||||
readOnly: root.readOnly
|
||||
selectByMouse: true
|
||||
selectedTextColor: "#151515"
|
||||
selectionColor: "#F26A21"
|
||||
@ -83,7 +86,10 @@ Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: 44
|
||||
|
||||
onTextEdited: root.editingChanged(text)
|
||||
onTextEdited: {
|
||||
if (!root.readOnly)
|
||||
root.editingChanged(text);
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
border.color: amountField.activeFocus ? "#F26A21" : "#343434"
|
||||
@ -97,14 +103,16 @@ Rectangle {
|
||||
id: maxButton
|
||||
|
||||
activeFocusOnTab: true
|
||||
enabled: root.showMaxButton && !root.readOnly
|
||||
focusPolicy: Qt.StrongFocus
|
||||
hoverEnabled: true
|
||||
text: qsTr("MAX")
|
||||
visible: root.showMaxButton
|
||||
|
||||
Accessible.name: qsTr("Use maximum %1 balance").arg(root.token)
|
||||
|
||||
Layout.minimumHeight: 44
|
||||
Layout.preferredWidth: 58
|
||||
Layout.preferredWidth: visible ? 58 : 0
|
||||
|
||||
onClicked: root.maxClicked()
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import "../components/shared"
|
||||
import "../components/liquidity"
|
||||
import "../state"
|
||||
@ -7,19 +6,17 @@ 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
|
||||
readonly property int preferredCardWidth: 960
|
||||
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
|
||||
height: parent ? parent.height : implicitHeight
|
||||
implicitWidth: root.preferredCardWidth + root.pageMargin * 2
|
||||
implicitHeight: pageCard.implicitHeight + root.pageMargin * 2
|
||||
implicitHeight: newPositionForm.implicitHeight + root.pageMargin * 2
|
||||
|
||||
DummyPoolState {
|
||||
id: poolState
|
||||
NewPositionPrototypeBackend {
|
||||
id: newPositionBackend
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
@ -32,158 +29,57 @@ Item {
|
||||
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
contentHeight: Math.max(height, pageCard.y + pageCard.implicitHeight + root.pageMargin)
|
||||
contentHeight: Math.max(height, newPositionForm.y + newPositionForm.implicitHeight + root.pageMargin)
|
||||
contentWidth: width
|
||||
enabled: !confirmationDialog.visible
|
||||
enabled: !confirmationDialog.open
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
|
||||
Rectangle {
|
||||
id: pageCard
|
||||
NewPositionForm {
|
||||
id: newPositionForm
|
||||
|
||||
color: "#1B1B1B"
|
||||
implicitHeight: shellContent.implicitHeight + 24
|
||||
radius: 16
|
||||
border.color: "#303030"
|
||||
border.width: 1
|
||||
prototypeBackend: newPositionBackend
|
||||
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
|
||||
}
|
||||
onConfirmationRequested: function (snapshot) {
|
||||
confirmationDialog.openWithSnapshot(snapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LiquidityConfirmationDialog {
|
||||
SuccessToast {
|
||||
id: successToast
|
||||
|
||||
width: Math.max(0, Math.min(380, root.width - 32))
|
||||
z: 30
|
||||
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
bottomMargin: 18
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
|
||||
NewPositionConfirmationDialog {
|
||||
id: confirmationDialog
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
onConfirmed: function (snapshot) {
|
||||
root.confirmLiquidityAction(snapshot);
|
||||
root.confirmNewPosition(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"));
|
||||
function confirmNewPosition(snapshot) {
|
||||
const result = newPositionBackend.submitNewPosition(snapshot.request, snapshot.quoteHash);
|
||||
|
||||
if (result.status !== "ok") {
|
||||
newPositionForm.setSubmitError(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (snapshot.action === "remove") {
|
||||
poolState.applyRemoveLiquidity(snapshot.withdrawA, snapshot.withdrawB, snapshot.burnAmount);
|
||||
removeLiquidityForm.resetForm();
|
||||
successToast.show(qsTr("Liquidity removed"), qsTr("Position updated"));
|
||||
}
|
||||
newPositionForm.resetAfterSubmit();
|
||||
successToast.show(result.message, result.detail);
|
||||
}
|
||||
}
|
||||
|
||||
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