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 backend API #222
This commit is contained in:
parent
07533bc799
commit
b288b543ed
@ -87,6 +87,7 @@ Item {
|
||||
|
||||
LiquidityPage {
|
||||
anchors.fill: parent
|
||||
backend: root.ready ? root.backend : null
|
||||
visible: navbar.currentIndex === 1
|
||||
}
|
||||
|
||||
|
||||
@ -2,13 +2,46 @@ import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import "../shared"
|
||||
import "../../state"
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
required property NewPositionPrototypeBackend prototypeBackend
|
||||
|
||||
property var newPositionContext: ({})
|
||||
property var quote: ({
|
||||
"status": "error",
|
||||
"error": "",
|
||||
"poolStatus": "unavailable_pool",
|
||||
"statusLabel": qsTr("Unavailable"),
|
||||
"statusDetail": qsTr("Connect a wallet to preview this position."),
|
||||
"instruction": "",
|
||||
"storedFeeBps": 0,
|
||||
"feeBps": root.selectedFeeBps,
|
||||
"feeLabel": root.feeLabel(root.selectedFeeBps),
|
||||
"quoteHash": "",
|
||||
"pool": {
|
||||
"id": "",
|
||||
"priceText": "",
|
||||
"reserveText": ""
|
||||
},
|
||||
"deposit": {
|
||||
"maxA": root.amountValue(0, root.tokenA.symbol),
|
||||
"maxB": root.amountValue(0, root.tokenB.symbol),
|
||||
"actualA": root.amountValue(0, root.tokenA.symbol),
|
||||
"actualB": root.amountValue(0, root.tokenB.symbol)
|
||||
},
|
||||
"lp": {
|
||||
"expected": root.amountValue(0, "LP"),
|
||||
"minimum": root.amountValue(0, "LP"),
|
||||
"locked": root.amountValue(0, "LP")
|
||||
},
|
||||
"position": {
|
||||
"userLp": "0 LP",
|
||||
"share": "-",
|
||||
"ownedA": root.formatTokenAmount(0, root.tokenA.symbol),
|
||||
"ownedB": root.formatTokenAmount(0, root.tokenB.symbol)
|
||||
},
|
||||
"accountChanges": []
|
||||
})
|
||||
property int selectedTokenAIndex: 0
|
||||
property int selectedTokenBIndex: 1
|
||||
property int selectedFeeBps: 30
|
||||
@ -19,6 +52,7 @@ Rectangle {
|
||||
property string initialPrice: "2500"
|
||||
property int depositScale: 1
|
||||
property string submitError: ""
|
||||
property bool applyingQuote: false
|
||||
|
||||
readonly property var emptyToken: ({
|
||||
"symbol": "",
|
||||
@ -26,20 +60,21 @@ Rectangle {
|
||||
"balanceText": "",
|
||||
"accent": "#343434"
|
||||
})
|
||||
readonly property var holdings: root.prototypeBackend ? root.prototypeBackend.holdings : []
|
||||
readonly property var holdings: root.newPositionContext && root.newPositionContext.holdings ? root.newPositionContext.holdings : []
|
||||
readonly property var feeTiers: root.newPositionContext && root.newPositionContext.feeTiers ? root.newPositionContext.feeTiers : []
|
||||
readonly property string activeAccount: root.newPositionContext && root.newPositionContext.activeAccountDisplay ? root.newPositionContext.activeAccountDisplay : qsTr("Not connected")
|
||||
readonly property var tokenA: root.holdings[root.selectedTokenAIndex] || root.emptyToken
|
||||
readonly property var tokenB: root.holdings[root.selectedTokenBIndex] || root.emptyToken
|
||||
readonly property var poolContext: root.prototypeBackend.poolContext(root.tokenA.symbol, root.tokenB.symbol)
|
||||
readonly property string poolStatus: root.poolContext.poolStatus
|
||||
readonly property string poolStatus: root.quote.poolStatus || "unavailable_pool"
|
||||
readonly property bool activePool: root.poolStatus === "active_pool"
|
||||
readonly property bool missingPool: root.poolStatus === "missing_pool"
|
||||
readonly property int slippageBps: Math.round(root.slippageTolerancePercent * 100)
|
||||
readonly property var quote: root.prototypeBackend.quoteNewPosition(root.buildRequest())
|
||||
readonly property bool canConfirm: root.quote.status === "ok"
|
||||
readonly property bool compact: root.width < 820
|
||||
readonly property bool hasActiveInput: root.amountA.length > 0 || root.amountB.length > 0
|
||||
readonly property bool showQuoteError: root.quote.status === "error" && (root.poolStatus === "unavailable_pool" || root.missingPool || root.hasActiveInput)
|
||||
|
||||
signal quoteRequested(var request)
|
||||
signal confirmationRequested(var snapshot)
|
||||
|
||||
color: "#1B1B1B"
|
||||
@ -50,9 +85,15 @@ Rectangle {
|
||||
|
||||
Component.onCompleted: root.afterPairChanged()
|
||||
|
||||
onNewPositionContextChanged: root.afterPairChanged()
|
||||
onSelectedTokenAIndexChanged: root.afterPairChanged()
|
||||
onSelectedTokenBIndexChanged: root.afterPairChanged()
|
||||
onPoolStatusChanged: root.normalizeFeeSelection()
|
||||
onSelectedFeeBpsChanged: root.requestQuote()
|
||||
onSlippageTolerancePercentChanged: root.requestQuote()
|
||||
onInitialPriceChanged: root.requestQuote()
|
||||
onDepositScaleChanged: root.requestQuote()
|
||||
onQuoteChanged: root.applyQuoteSideEffects()
|
||||
|
||||
ColumnLayout {
|
||||
id: content
|
||||
@ -84,7 +125,7 @@ Rectangle {
|
||||
color: "#A9A098"
|
||||
elide: Text.ElideRight
|
||||
font.pixelSize: 12
|
||||
text: qsTr("Active account %1").arg(root.prototypeBackend.activeAccount)
|
||||
text: qsTr("Active account %1").arg(root.activeAccount)
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
@ -215,7 +256,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.prototypeBackend.holdings
|
||||
model: root.holdings
|
||||
|
||||
delegate: Button {
|
||||
id: tokenAButton
|
||||
@ -298,7 +339,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.prototypeBackend.holdings
|
||||
model: root.holdings
|
||||
|
||||
delegate: Button {
|
||||
id: tokenBButton
|
||||
@ -385,7 +426,7 @@ Rectangle {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Repeater {
|
||||
model: root.prototypeBackend.feeTiers
|
||||
model: root.feeTiers
|
||||
|
||||
delegate: Rectangle {
|
||||
id: feeChip
|
||||
@ -830,7 +871,7 @@ Rectangle {
|
||||
label: qsTr("Expected LP")
|
||||
value: root.quote.lp.expected.display
|
||||
estimated: true
|
||||
estimateHelp: qsTr("Prototype quote mirrors the future backend response shape.")
|
||||
estimateHelp: qsTr("Backend preview quote. Final submission rechecks the quote hash.")
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
@ -1000,30 +1041,111 @@ Rectangle {
|
||||
return root.holdings.length > 0 ? (index + 1) % root.holdings.length : 0;
|
||||
}
|
||||
|
||||
function requestQuote() {
|
||||
root.quoteRequested(root.buildRequest());
|
||||
}
|
||||
|
||||
function parseAmount(value) {
|
||||
const parsed = Number(String(value).replace(/,/g, ""));
|
||||
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$/, "");
|
||||
|
||||
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(root.formatAmount(value)).arg(symbol);
|
||||
}
|
||||
|
||||
function amountValue(value, symbol) {
|
||||
const amount = Math.max(0, Number(value) || 0);
|
||||
return {
|
||||
"value": amount,
|
||||
"input": root.formatAmount(amount),
|
||||
"display": root.formatTokenAmount(amount, symbol),
|
||||
"symbol": symbol
|
||||
};
|
||||
}
|
||||
|
||||
function feeLabel(bps) {
|
||||
if (bps === 1)
|
||||
return "0.01%";
|
||||
if (bps === 5)
|
||||
return "0.05%";
|
||||
if (bps === 30)
|
||||
return "0.30%";
|
||||
if (bps === 100)
|
||||
return "1.00%";
|
||||
return qsTr("%1 bps").arg(bps);
|
||||
}
|
||||
|
||||
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 afterPairChanged() {
|
||||
if (root.holdings.length < 2) {
|
||||
root.selectedTokenAIndex = 0;
|
||||
root.selectedTokenBIndex = 0;
|
||||
root.submitError = "";
|
||||
root.requestQuote();
|
||||
return;
|
||||
}
|
||||
|
||||
if (root.selectedTokenAIndex >= root.holdings.length)
|
||||
root.selectedTokenAIndex = 0;
|
||||
if (root.selectedTokenBIndex >= root.holdings.length)
|
||||
root.selectedTokenBIndex = root.nextTokenIndex(root.selectedTokenAIndex);
|
||||
|
||||
if (root.selectedTokenAIndex === root.selectedTokenBIndex)
|
||||
root.selectedTokenBIndex = root.nextTokenIndex(root.selectedTokenAIndex);
|
||||
|
||||
if (root.tokenA.symbol.length === 0 || root.tokenB.symbol.length === 0)
|
||||
if (root.tokenA.symbol.length === 0 || root.tokenB.symbol.length === 0) {
|
||||
root.requestQuote();
|
||||
return;
|
||||
}
|
||||
|
||||
root.submitError = "";
|
||||
root.amountA = "";
|
||||
root.amountB = "";
|
||||
root.editedSide = "A";
|
||||
root.initialPrice = root.prototypeBackend.formatAmount(root.prototypeBackend.defaultInitialPrice(root.tokenA.symbol, root.tokenB.symbol));
|
||||
root.initialPrice = root.formatAmount(root.defaultInitialPrice(root.tokenA.symbol, root.tokenB.symbol));
|
||||
root.depositScale = 1;
|
||||
root.normalizeFeeSelection();
|
||||
root.requestQuote();
|
||||
}
|
||||
|
||||
function normalizeFeeSelection() {
|
||||
if (root.tokenA.symbol.length === 0 || root.tokenB.symbol.length === 0)
|
||||
return;
|
||||
|
||||
const context = root.prototypeBackend.poolContext(root.tokenA.symbol, root.tokenB.symbol);
|
||||
|
||||
if (context.poolStatus === "active_pool") {
|
||||
root.selectedFeeBps = context.storedFeeBps;
|
||||
if (root.poolStatus === "active_pool" && root.quote.storedFeeBps > 0) {
|
||||
root.selectedFeeBps = root.quote.storedFeeBps;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1040,8 +1162,8 @@ Rectangle {
|
||||
if (!tier.supported)
|
||||
return qsTr("Unsupported by the AMM program. Valid fee tiers are 0.01%, 0.05%, 0.30%, and 1.00%.");
|
||||
|
||||
if (root.poolStatus === "active_pool" && tier.bps !== root.poolContext.storedFeeBps)
|
||||
return qsTr("Existing pool uses %1. Fee tier is fixed by pool configuration.").arg(root.prototypeBackend.feeLabel(root.poolContext.storedFeeBps));
|
||||
if (root.poolStatus === "active_pool" && tier.bps !== root.quote.storedFeeBps)
|
||||
return qsTr("Existing pool uses %1. Fee tier is fixed by pool configuration.").arg(root.feeLabel(root.quote.storedFeeBps));
|
||||
|
||||
if (root.poolStatus === "unavailable_pool")
|
||||
return qsTr("Fee selection is disabled until this pair can be quoted.");
|
||||
@ -1058,30 +1180,31 @@ Rectangle {
|
||||
else
|
||||
root.amountB = value;
|
||||
|
||||
root.syncActiveCounterpart();
|
||||
root.requestQuote();
|
||||
}
|
||||
|
||||
function syncActiveCounterpart() {
|
||||
const nextQuote = root.prototypeBackend.quoteNewPosition(root.buildRequest());
|
||||
|
||||
if (nextQuote.status !== "ok" || nextQuote.poolStatus !== "active_pool")
|
||||
function applyQuoteSideEffects() {
|
||||
root.normalizeFeeSelection();
|
||||
if (root.quote.status !== "ok" || root.quote.poolStatus !== "active_pool")
|
||||
return;
|
||||
|
||||
root.applyingQuote = true;
|
||||
if (root.editedSide === "A")
|
||||
root.amountB = nextQuote.deposit.maxB.input;
|
||||
root.amountB = root.quote.deposit.maxB.input;
|
||||
else
|
||||
root.amountA = nextQuote.deposit.maxA.input;
|
||||
root.amountA = root.quote.deposit.maxA.input;
|
||||
root.applyingQuote = false;
|
||||
}
|
||||
|
||||
function useMaxActive(side) {
|
||||
const ratio = root.prototypeBackend.activeRatio(root.tokenA.symbol, root.tokenB.symbol);
|
||||
const ratio = root.activeRatio(root.tokenA.symbol, root.tokenB.symbol);
|
||||
const maxA = Math.min(root.tokenA.balance, root.tokenB.balance / ratio);
|
||||
const maxB = Math.min(root.tokenB.balance, root.tokenA.balance * ratio);
|
||||
|
||||
if (side === "A")
|
||||
root.editActiveAmount("A", root.prototypeBackend.formatAmount(maxA));
|
||||
root.editActiveAmount("A", root.formatAmount(maxA));
|
||||
else
|
||||
root.editActiveAmount("B", root.prototypeBackend.formatAmount(maxB));
|
||||
root.editActiveAmount("B", root.formatAmount(maxB));
|
||||
}
|
||||
|
||||
function setSubmitError(message) {
|
||||
|
||||
@ -1,11 +1,17 @@
|
||||
import QtQuick 2.15
|
||||
import QtQml 2.15
|
||||
import "../components/shared"
|
||||
import "../components/liquidity"
|
||||
import "../state"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var backend: null
|
||||
property var newPositionContext: ({})
|
||||
property var newPositionQuote: ({})
|
||||
property int quoteSerial: 0
|
||||
property bool formReady: false
|
||||
|
||||
readonly property int pageMargin: 16
|
||||
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
|
||||
@ -15,8 +21,17 @@ Item {
|
||||
implicitWidth: root.preferredCardWidth + root.pageMargin * 2
|
||||
implicitHeight: newPositionForm.implicitHeight + root.pageMargin * 2
|
||||
|
||||
NewPositionPrototypeBackend {
|
||||
id: newPositionBackend
|
||||
Component.onCompleted: root.refreshNewPositionContext()
|
||||
onBackendChanged: root.refreshNewPositionContext()
|
||||
|
||||
Connections {
|
||||
target: root.backend
|
||||
ignoreUnknownSignals: true
|
||||
|
||||
function onNewPositionContextChanged(value) {
|
||||
root.newPositionContext = value;
|
||||
root.requestQuote(root.currentRequest());
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
@ -37,11 +52,21 @@ Item {
|
||||
NewPositionForm {
|
||||
id: newPositionForm
|
||||
|
||||
prototypeBackend: newPositionBackend
|
||||
newPositionContext: root.newPositionContext
|
||||
quote: root.newPositionQuote
|
||||
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
|
||||
|
||||
Component.onCompleted: {
|
||||
root.formReady = true;
|
||||
root.refreshNewPositionContext();
|
||||
}
|
||||
|
||||
onQuoteRequested: function (request) {
|
||||
root.requestQuote(request);
|
||||
}
|
||||
|
||||
onConfirmationRequested: function (snapshot) {
|
||||
confirmationDialog.openWithSnapshot(snapshot);
|
||||
}
|
||||
@ -72,14 +97,128 @@ Item {
|
||||
}
|
||||
|
||||
function confirmNewPosition(snapshot) {
|
||||
const result = newPositionBackend.submitNewPosition(snapshot.request, snapshot.quoteHash);
|
||||
|
||||
if (result.status !== "ok") {
|
||||
newPositionForm.setSubmitError(result.error);
|
||||
if (!root.backend || typeof logos === "undefined") {
|
||||
newPositionForm.setSubmitError(qsTr("Wallet backend is unavailable."));
|
||||
return;
|
||||
}
|
||||
|
||||
newPositionForm.resetAfterSubmit();
|
||||
successToast.show(result.message, result.detail);
|
||||
logos.watch(root.backend.submitNewPosition(snapshot.request, snapshot.quoteHash),
|
||||
function(result) {
|
||||
if (result.status !== "ok") {
|
||||
newPositionForm.setSubmitError(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
newPositionForm.resetAfterSubmit();
|
||||
successToast.show(result.message, result.detail);
|
||||
},
|
||||
function(error) {
|
||||
newPositionForm.setSubmitError(qsTr("Error submitting position: %1").arg(error));
|
||||
});
|
||||
}
|
||||
|
||||
function refreshNewPositionContext() {
|
||||
if (!root.backend || typeof logos === "undefined") {
|
||||
root.newPositionContext = {
|
||||
"activeAccountDisplay": qsTr("Not connected"),
|
||||
"holdings": [],
|
||||
"feeTiers": []
|
||||
};
|
||||
root.newPositionQuote = root.errorQuote(qsTr("Wallet backend is unavailable."), root.currentRequest());
|
||||
return;
|
||||
}
|
||||
|
||||
logos.watch(root.backend.refreshNewPositionContext(),
|
||||
function(context) {
|
||||
root.newPositionContext = context;
|
||||
root.requestQuote(root.currentRequest());
|
||||
},
|
||||
function(error) {
|
||||
root.newPositionQuote = root.errorQuote(qsTr("Error loading position context: %1").arg(error), root.currentRequest());
|
||||
});
|
||||
}
|
||||
|
||||
function requestQuote(request) {
|
||||
if (!root.backend || typeof logos === "undefined") {
|
||||
root.newPositionQuote = root.errorQuote(qsTr("Wallet backend is unavailable."), request);
|
||||
return;
|
||||
}
|
||||
|
||||
const serial = ++root.quoteSerial;
|
||||
logos.watch(root.backend.quoteNewPosition(request),
|
||||
function(quote) {
|
||||
if (serial === root.quoteSerial)
|
||||
root.newPositionQuote = quote;
|
||||
},
|
||||
function(error) {
|
||||
if (serial === root.quoteSerial)
|
||||
root.newPositionQuote = root.errorQuote(qsTr("Error loading quote: %1").arg(error), request);
|
||||
});
|
||||
}
|
||||
|
||||
function amountValue(symbol) {
|
||||
return {
|
||||
"value": 0,
|
||||
"input": "0",
|
||||
"display": qsTr("0 %1").arg(symbol),
|
||||
"symbol": symbol
|
||||
};
|
||||
}
|
||||
|
||||
function currentRequest() {
|
||||
if (root.formReady)
|
||||
return newPositionForm.buildRequest();
|
||||
|
||||
return {
|
||||
"amountA": "",
|
||||
"amountB": "",
|
||||
"depositScale": 1,
|
||||
"editedSide": "A",
|
||||
"feeBps": 30,
|
||||
"initialPrice": "1",
|
||||
"slippageBps": 50,
|
||||
"tokenA": "",
|
||||
"tokenB": ""
|
||||
};
|
||||
}
|
||||
|
||||
function errorQuote(message, request) {
|
||||
const tokenA = request ? request.tokenA : "";
|
||||
const tokenB = request ? request.tokenB : "";
|
||||
return {
|
||||
"status": "error",
|
||||
"error": message,
|
||||
"poolStatus": "unavailable_pool",
|
||||
"statusLabel": qsTr("Unavailable"),
|
||||
"statusDetail": message,
|
||||
"instruction": "",
|
||||
"storedFeeBps": 0,
|
||||
"feeBps": request ? request.feeBps : 0,
|
||||
"feeLabel": "",
|
||||
"quoteHash": "",
|
||||
"pool": {
|
||||
"id": "",
|
||||
"priceText": "",
|
||||
"reserveText": ""
|
||||
},
|
||||
"deposit": {
|
||||
"maxA": root.amountValue(tokenA),
|
||||
"maxB": root.amountValue(tokenB),
|
||||
"actualA": root.amountValue(tokenA),
|
||||
"actualB": root.amountValue(tokenB)
|
||||
},
|
||||
"lp": {
|
||||
"expected": root.amountValue("LP"),
|
||||
"minimum": root.amountValue("LP"),
|
||||
"locked": root.amountValue("LP")
|
||||
},
|
||||
"position": {
|
||||
"userLp": "0 LP",
|
||||
"share": "-",
|
||||
"ownedA": qsTr("0 %1").arg(tokenA),
|
||||
"ownedB": qsTr("0 %1").arg(tokenB)
|
||||
},
|
||||
"accountChanges": []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QCoreApplication>
|
||||
#include <QCryptographicHash>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
@ -20,6 +21,9 @@
|
||||
#include "logos_api.h"
|
||||
#include "logos_sdk.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace {
|
||||
const char SETTINGS_ORG[] = "Logos";
|
||||
const char SETTINGS_APP[] = "AmmUI";
|
||||
@ -38,6 +42,386 @@ namespace {
|
||||
return QUrl::fromUserInput(path).toLocalFile();
|
||||
return path;
|
||||
}
|
||||
|
||||
QString stableId(const QString& key)
|
||||
{
|
||||
const QByteArray digest =
|
||||
QCryptographicHash::hash(key.toUtf8(), QCryptographicHash::Sha256).toHex();
|
||||
return QString::fromLatin1(digest);
|
||||
}
|
||||
|
||||
QString shortId(const QString& id)
|
||||
{
|
||||
if (id.length() <= 14)
|
||||
return id;
|
||||
return id.left(6) + QStringLiteral("...") + id.right(4);
|
||||
}
|
||||
|
||||
double parsePositiveAmount(QString value)
|
||||
{
|
||||
value.remove(QLatin1Char(','));
|
||||
bool ok = false;
|
||||
const double parsed = value.toDouble(&ok);
|
||||
return ok && std::isfinite(parsed) && parsed > 0.0 ? parsed : 0.0;
|
||||
}
|
||||
|
||||
QString formatAmount(double amount)
|
||||
{
|
||||
amount = std::max(0.0, amount);
|
||||
const int decimals = amount >= 1000.0 ? 2 : amount >= 1.0 ? 4 : 6;
|
||||
QString text = QString::number(amount, 'f', decimals);
|
||||
while (text.contains(QLatin1Char('.')) && text.endsWith(QLatin1Char('0')))
|
||||
text.chop(1);
|
||||
if (text.endsWith(QLatin1Char('.')))
|
||||
text.chop(1);
|
||||
return text;
|
||||
}
|
||||
|
||||
QString formatTokenAmount(double amount, const QString& symbol)
|
||||
{
|
||||
return QStringLiteral("%1 %2").arg(formatAmount(amount), symbol);
|
||||
}
|
||||
|
||||
QVariantMap amountValue(double amount, const QString& symbol)
|
||||
{
|
||||
QVariantMap value;
|
||||
value.insert(QStringLiteral("value"), amount);
|
||||
value.insert(QStringLiteral("input"), formatAmount(amount));
|
||||
value.insert(QStringLiteral("display"), formatTokenAmount(amount, symbol));
|
||||
value.insert(QStringLiteral("symbol"), symbol);
|
||||
return value;
|
||||
}
|
||||
|
||||
QString feeLabel(int bps)
|
||||
{
|
||||
if (bps == 1)
|
||||
return QStringLiteral("0.01%");
|
||||
if (bps == 5)
|
||||
return QStringLiteral("0.05%");
|
||||
if (bps == 30)
|
||||
return QStringLiteral("0.30%");
|
||||
if (bps == 100)
|
||||
return QStringLiteral("1.00%");
|
||||
return QStringLiteral("%1 bps").arg(bps);
|
||||
}
|
||||
|
||||
bool isSupportedFeeTier(int bps)
|
||||
{
|
||||
return bps == 1 || bps == 5 || bps == 30 || bps == 100;
|
||||
}
|
||||
|
||||
QVariantList feeTiers()
|
||||
{
|
||||
QVariantList tiers;
|
||||
for (const int bps : {1, 5, 25, 30, 100}) {
|
||||
QVariantMap tier;
|
||||
tier.insert(QStringLiteral("bps"), bps);
|
||||
tier.insert(QStringLiteral("label"), feeLabel(bps));
|
||||
tier.insert(QStringLiteral("supported"), isSupportedFeeTier(bps));
|
||||
tiers.append(tier);
|
||||
}
|
||||
return tiers;
|
||||
}
|
||||
|
||||
double devnetBalance(const QString& symbol)
|
||||
{
|
||||
if (symbol == QStringLiteral("USDC"))
|
||||
return 12450.0;
|
||||
if (symbol == QStringLiteral("LOGOS"))
|
||||
return 850000.0;
|
||||
if (symbol == QStringLiteral("WETH"))
|
||||
return 3.25;
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
QString accentForSymbol(const QString& symbol)
|
||||
{
|
||||
if (symbol == QStringLiteral("USDC"))
|
||||
return QStringLiteral("#2E7CF6");
|
||||
if (symbol == QStringLiteral("LOGOS"))
|
||||
return QStringLiteral("#F26A21");
|
||||
if (symbol == QStringLiteral("WETH"))
|
||||
return QStringLiteral("#B7C2D8");
|
||||
return QStringLiteral("#343434");
|
||||
}
|
||||
|
||||
QVariantMap devnetHolding(const QString& owner, const QString& symbol, const QString& name)
|
||||
{
|
||||
const double balance = devnetBalance(symbol);
|
||||
const QString definitionId = stableId(QStringLiteral("devnet:token-definition:%1").arg(symbol));
|
||||
QVariantMap holding;
|
||||
holding.insert(QStringLiteral("symbol"), symbol);
|
||||
holding.insert(QStringLiteral("name"), name);
|
||||
holding.insert(QStringLiteral("definitionId"), definitionId);
|
||||
holding.insert(QStringLiteral("holdingId"),
|
||||
stableId(QStringLiteral("devnet:token-holding:%1:%2").arg(owner, symbol)));
|
||||
holding.insert(QStringLiteral("balance"), balance);
|
||||
holding.insert(QStringLiteral("balanceText"), formatTokenAmount(balance, symbol));
|
||||
holding.insert(QStringLiteral("accent"), accentForSymbol(symbol));
|
||||
return holding;
|
||||
}
|
||||
|
||||
QVariantList devnetHoldings(const QString& owner)
|
||||
{
|
||||
if (owner.isEmpty())
|
||||
return {};
|
||||
|
||||
QVariantList holdings;
|
||||
holdings.append(devnetHolding(owner, QStringLiteral("USDC"), QStringLiteral("USD Coin")));
|
||||
holdings.append(devnetHolding(owner, QStringLiteral("LOGOS"), QStringLiteral("Logos")));
|
||||
holdings.append(devnetHolding(owner, QStringLiteral("WETH"), QStringLiteral("Wrapped Ether")));
|
||||
return holdings;
|
||||
}
|
||||
|
||||
QVariantMap holdingBySymbol(const QVariantList& holdings, const QString& symbol)
|
||||
{
|
||||
for (const QVariant& item : holdings) {
|
||||
const QVariantMap holding = item.toMap();
|
||||
if (holding.value(QStringLiteral("symbol")).toString() == symbol)
|
||||
return holding;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
QString unorderedPairKey(const QString& symbolA, const QString& symbolB)
|
||||
{
|
||||
return symbolA < symbolB
|
||||
? QStringLiteral("%1/%2").arg(symbolA, symbolB)
|
||||
: QStringLiteral("%1/%2").arg(symbolB, symbolA);
|
||||
}
|
||||
|
||||
double activeRatio(const QString& symbolA, const QString& symbolB)
|
||||
{
|
||||
if (symbolA == QStringLiteral("USDC") && symbolB == QStringLiteral("LOGOS"))
|
||||
return 8.0;
|
||||
if (symbolA == QStringLiteral("LOGOS") && symbolB == QStringLiteral("USDC"))
|
||||
return 0.125;
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
double defaultInitialPrice(const QString& symbolA, const QString& symbolB)
|
||||
{
|
||||
if (symbolA == QStringLiteral("USDC") && symbolB == QStringLiteral("WETH"))
|
||||
return 2500.0;
|
||||
if (symbolA == QStringLiteral("WETH") && symbolB == QStringLiteral("USDC"))
|
||||
return 0.0004;
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
QVariantMap poolContext(const QString& symbolA, const QString& symbolB)
|
||||
{
|
||||
QVariantMap context;
|
||||
context.insert(QStringLiteral("poolStatus"), QStringLiteral("unavailable_pool"));
|
||||
context.insert(QStringLiteral("statusLabel"), QStringLiteral("Unavailable"));
|
||||
context.insert(QStringLiteral("detail"), QStringLiteral("Choose two different assets from the active account."));
|
||||
context.insert(QStringLiteral("instruction"), QString());
|
||||
context.insert(QStringLiteral("storedFeeBps"), 0);
|
||||
context.insert(QStringLiteral("poolId"), QString());
|
||||
context.insert(QStringLiteral("priceText"), QString());
|
||||
context.insert(QStringLiteral("reserveText"), QString());
|
||||
|
||||
if (symbolA.isEmpty() || symbolB.isEmpty() || symbolA == symbolB)
|
||||
return context;
|
||||
|
||||
const QString pairKey = unorderedPairKey(symbolA, symbolB);
|
||||
const QString poolId = stableId(QStringLiteral("devnet:amm-pool:%1").arg(pairKey));
|
||||
context.insert(QStringLiteral("poolId"), poolId);
|
||||
|
||||
if (pairKey == QStringLiteral("LOGOS/USDC")) {
|
||||
context.insert(QStringLiteral("poolStatus"), QStringLiteral("active_pool"));
|
||||
context.insert(QStringLiteral("statusLabel"), QStringLiteral("Active pool"));
|
||||
context.insert(QStringLiteral("detail"), QStringLiteral("Deposits are quoted against the existing pool ratio. Nonmatching fee tiers are locked."));
|
||||
context.insert(QStringLiteral("instruction"), QStringLiteral("add_liquidity"));
|
||||
context.insert(QStringLiteral("storedFeeBps"), 30);
|
||||
context.insert(QStringLiteral("priceText"),
|
||||
symbolA == QStringLiteral("USDC")
|
||||
? QStringLiteral("1 USDC = 8 LOGOS")
|
||||
: QStringLiteral("1 LOGOS = 0.125 USDC"));
|
||||
context.insert(QStringLiteral("reserveText"),
|
||||
symbolA == QStringLiteral("USDC")
|
||||
? QStringLiteral("1,250,000 USDC / 10,000,000 LOGOS")
|
||||
: QStringLiteral("10,000,000 LOGOS / 1,250,000 USDC"));
|
||||
return context;
|
||||
}
|
||||
|
||||
if (pairKey == QStringLiteral("USDC/WETH")) {
|
||||
context.insert(QStringLiteral("poolStatus"), QStringLiteral("missing_pool"));
|
||||
context.insert(QStringLiteral("statusLabel"), QStringLiteral("Missing pool"));
|
||||
context.insert(QStringLiteral("detail"), QStringLiteral("Set the initial price first, then scale both deposits together."));
|
||||
context.insert(QStringLiteral("instruction"), QStringLiteral("new_definition"));
|
||||
context.insert(QStringLiteral("priceText"), QStringLiteral("No reserves yet"));
|
||||
context.insert(QStringLiteral("reserveText"), QStringLiteral("Pool account is empty"));
|
||||
return context;
|
||||
}
|
||||
|
||||
context.insert(QStringLiteral("detail"), QStringLiteral("A pool account exists, but it cannot be quoted safely for this devnet state."));
|
||||
context.insert(QStringLiteral("priceText"), QStringLiteral("Quote disabled"));
|
||||
context.insert(QStringLiteral("reserveText"), QStringLiteral("Unsupported stored pool state"));
|
||||
return context;
|
||||
}
|
||||
|
||||
QString quoteHash(const QVariantMap& request)
|
||||
{
|
||||
const QStringList parts = {
|
||||
request.value(QStringLiteral("tokenA")).toString(),
|
||||
request.value(QStringLiteral("tokenB")).toString(),
|
||||
QString::number(request.value(QStringLiteral("feeBps")).toInt()),
|
||||
request.value(QStringLiteral("editedSide")).toString(),
|
||||
request.value(QStringLiteral("amountA")).toString(),
|
||||
request.value(QStringLiteral("amountB")).toString(),
|
||||
request.value(QStringLiteral("initialPrice")).toString(),
|
||||
QString::number(request.value(QStringLiteral("depositScale")).toInt()),
|
||||
QString::number(request.value(QStringLiteral("slippageBps")).toInt()),
|
||||
};
|
||||
const QByteArray digest =
|
||||
QCryptographicHash::hash(parts.join(QLatin1Char('|')).toUtf8(),
|
||||
QCryptographicHash::Sha256).toHex();
|
||||
return QStringLiteral("sha256-%1").arg(QString::fromLatin1(digest));
|
||||
}
|
||||
|
||||
QVariantMap accountChange(const QString& role, const QString& id, const QString& action)
|
||||
{
|
||||
QVariantMap change;
|
||||
change.insert(QStringLiteral("role"), role);
|
||||
change.insert(QStringLiteral("id"), id);
|
||||
change.insert(QStringLiteral("action"), action);
|
||||
return change;
|
||||
}
|
||||
|
||||
QVariantList accountChanges(const QVariantMap& request, const QVariantMap& context)
|
||||
{
|
||||
const QString tokenA = request.value(QStringLiteral("tokenA")).toString();
|
||||
const QString tokenB = request.value(QStringLiteral("tokenB")).toString();
|
||||
const QString poolId = context.value(QStringLiteral("poolId")).toString();
|
||||
const bool missingPool =
|
||||
context.value(QStringLiteral("poolStatus")).toString() == QStringLiteral("missing_pool");
|
||||
|
||||
QVariantList changes;
|
||||
changes.append(accountChange(QStringLiteral("Config"),
|
||||
stableId(QStringLiteral("devnet:amm-config")),
|
||||
QStringLiteral("Read")));
|
||||
changes.append(accountChange(QStringLiteral("Pool"),
|
||||
poolId,
|
||||
missingPool ? QStringLiteral("Create") : QStringLiteral("Update")));
|
||||
changes.append(accountChange(QStringLiteral("Vault A"),
|
||||
stableId(QStringLiteral("devnet:vault:%1:%2").arg(poolId, tokenA)),
|
||||
missingPool ? QStringLiteral("Update or create") : QStringLiteral("Update")));
|
||||
changes.append(accountChange(QStringLiteral("Vault B"),
|
||||
stableId(QStringLiteral("devnet:vault:%1:%2").arg(poolId, tokenB)),
|
||||
missingPool ? QStringLiteral("Update or create") : QStringLiteral("Update")));
|
||||
if (missingPool) {
|
||||
changes.append(accountChange(QStringLiteral("LP definition"),
|
||||
stableId(QStringLiteral("devnet:lp-definition:%1").arg(poolId)),
|
||||
QStringLiteral("Create")));
|
||||
changes.append(accountChange(QStringLiteral("LP lock holding"),
|
||||
stableId(QStringLiteral("devnet:lp-lock:%1").arg(poolId)),
|
||||
QStringLiteral("Create")));
|
||||
}
|
||||
changes.append(accountChange(QStringLiteral("User LP holding"),
|
||||
stableId(QStringLiteral("devnet:user-lp:%1").arg(poolId)),
|
||||
QStringLiteral("Update or create")));
|
||||
changes.append(accountChange(QStringLiteral("Current tick"),
|
||||
stableId(QStringLiteral("devnet:current-tick:%1").arg(poolId)),
|
||||
missingPool ? QStringLiteral("Create") : QStringLiteral("Update")));
|
||||
changes.append(accountChange(QStringLiteral("Clock"),
|
||||
stableId(QStringLiteral("devnet:clock:canonical")),
|
||||
QStringLiteral("Read")));
|
||||
return changes;
|
||||
}
|
||||
|
||||
QVariantMap baseQuote(const QVariantMap& context, const QVariantMap& request)
|
||||
{
|
||||
QVariantMap quote;
|
||||
quote.insert(QStringLiteral("poolStatus"), context.value(QStringLiteral("poolStatus")));
|
||||
quote.insert(QStringLiteral("statusLabel"), context.value(QStringLiteral("statusLabel")));
|
||||
quote.insert(QStringLiteral("statusDetail"), context.value(QStringLiteral("detail")));
|
||||
quote.insert(QStringLiteral("instruction"), context.value(QStringLiteral("instruction")));
|
||||
quote.insert(QStringLiteral("storedFeeBps"), context.value(QStringLiteral("storedFeeBps")));
|
||||
quote.insert(QStringLiteral("feeBps"), request.value(QStringLiteral("feeBps")).toInt());
|
||||
quote.insert(QStringLiteral("feeLabel"), feeLabel(request.value(QStringLiteral("feeBps")).toInt()));
|
||||
quote.insert(QStringLiteral("quoteHash"), quoteHash(request));
|
||||
|
||||
QVariantMap pool;
|
||||
pool.insert(QStringLiteral("id"), context.value(QStringLiteral("poolId")));
|
||||
pool.insert(QStringLiteral("priceText"), context.value(QStringLiteral("priceText")));
|
||||
pool.insert(QStringLiteral("reserveText"), context.value(QStringLiteral("reserveText")));
|
||||
quote.insert(QStringLiteral("pool"), pool);
|
||||
return quote;
|
||||
}
|
||||
|
||||
QVariantMap quoteOk(const QVariantMap& context,
|
||||
const QVariantMap& request,
|
||||
double maxA,
|
||||
double maxB,
|
||||
double actualA,
|
||||
double actualB,
|
||||
double expectedLp,
|
||||
double minimumLp,
|
||||
double lockedLp,
|
||||
const QVariantMap& position)
|
||||
{
|
||||
QVariantMap quote = baseQuote(context, request);
|
||||
const QString tokenA = request.value(QStringLiteral("tokenA")).toString();
|
||||
const QString tokenB = request.value(QStringLiteral("tokenB")).toString();
|
||||
quote.insert(QStringLiteral("status"), QStringLiteral("ok"));
|
||||
quote.insert(QStringLiteral("error"), QString());
|
||||
|
||||
QVariantMap deposit;
|
||||
deposit.insert(QStringLiteral("maxA"), amountValue(maxA, tokenA));
|
||||
deposit.insert(QStringLiteral("maxB"), amountValue(maxB, tokenB));
|
||||
deposit.insert(QStringLiteral("actualA"), amountValue(actualA, tokenA));
|
||||
deposit.insert(QStringLiteral("actualB"), amountValue(actualB, tokenB));
|
||||
quote.insert(QStringLiteral("deposit"), deposit);
|
||||
|
||||
QVariantMap lp;
|
||||
lp.insert(QStringLiteral("expected"), amountValue(expectedLp, QStringLiteral("LP")));
|
||||
lp.insert(QStringLiteral("minimum"), amountValue(minimumLp, QStringLiteral("LP")));
|
||||
lp.insert(QStringLiteral("locked"), amountValue(lockedLp, QStringLiteral("LP")));
|
||||
quote.insert(QStringLiteral("lp"), lp);
|
||||
|
||||
quote.insert(QStringLiteral("position"), position);
|
||||
quote.insert(QStringLiteral("accountChanges"), accountChanges(request, context));
|
||||
|
||||
QVariantMap transaction;
|
||||
transaction.insert(QStringLiteral("instruction"), context.value(QStringLiteral("instruction")));
|
||||
transaction.insert(QStringLiteral("ready"), false);
|
||||
transaction.insert(QStringLiteral("reason"), QStringLiteral("Submission requires real token holding account ids from wallet discovery."));
|
||||
quote.insert(QStringLiteral("transaction"), transaction);
|
||||
return quote;
|
||||
}
|
||||
|
||||
QVariantMap quoteError(const QVariantMap& context, const QVariantMap& request, const QString& errorText)
|
||||
{
|
||||
QVariantMap quote = baseQuote(context, request);
|
||||
const QString tokenA = request.value(QStringLiteral("tokenA")).toString();
|
||||
const QString tokenB = request.value(QStringLiteral("tokenB")).toString();
|
||||
quote.insert(QStringLiteral("status"), QStringLiteral("error"));
|
||||
quote.insert(QStringLiteral("error"), errorText);
|
||||
|
||||
QVariantMap deposit;
|
||||
deposit.insert(QStringLiteral("maxA"), amountValue(0, tokenA));
|
||||
deposit.insert(QStringLiteral("maxB"), amountValue(0, tokenB));
|
||||
deposit.insert(QStringLiteral("actualA"), amountValue(0, tokenA));
|
||||
deposit.insert(QStringLiteral("actualB"), amountValue(0, tokenB));
|
||||
quote.insert(QStringLiteral("deposit"), deposit);
|
||||
|
||||
QVariantMap lp;
|
||||
lp.insert(QStringLiteral("expected"), amountValue(0, QStringLiteral("LP")));
|
||||
lp.insert(QStringLiteral("minimum"), amountValue(0, QStringLiteral("LP")));
|
||||
const bool missingPool =
|
||||
context.value(QStringLiteral("poolStatus")).toString() == QStringLiteral("missing_pool");
|
||||
lp.insert(QStringLiteral("locked"), amountValue(missingPool ? 1000.0 : 0.0, QStringLiteral("LP")));
|
||||
quote.insert(QStringLiteral("lp"), lp);
|
||||
|
||||
QVariantMap position;
|
||||
position.insert(QStringLiteral("userLp"), QStringLiteral("0 LP"));
|
||||
position.insert(QStringLiteral("share"), QStringLiteral("-"));
|
||||
position.insert(QStringLiteral("ownedA"), formatTokenAmount(0, tokenA));
|
||||
position.insert(QStringLiteral("ownedB"), formatTokenAmount(0, tokenB));
|
||||
quote.insert(QStringLiteral("position"), position);
|
||||
quote.insert(QStringLiteral("accountChanges"), QVariantList());
|
||||
return quote;
|
||||
}
|
||||
}
|
||||
|
||||
QString AmmUiBackend::defaultWalletHome()
|
||||
@ -75,6 +459,7 @@ AmmUiBackend::AmmUiBackend(LogosAPI* logosAPI, QObject* parent)
|
||||
setWalletHome(defaultWalletHome());
|
||||
// Assume reachable until a probe proves otherwise (avoids a startup flash).
|
||||
setSequencerReachable(true);
|
||||
setNewPositionContext(buildNewPositionContext());
|
||||
|
||||
// Periodically re-probe the sequencer so the banner reacts to a node going
|
||||
// up/down while the app is running. Probes are no-ops until a wallet (and
|
||||
@ -131,6 +516,7 @@ void AmmUiBackend::openOrAdoptWallet()
|
||||
m_accountModel->replaceFromJsonArray(existing);
|
||||
refreshBalances();
|
||||
refreshSequencerAddr();
|
||||
refreshNewPositionContext();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -154,6 +540,7 @@ void AmmUiBackend::openOrAdoptWallet()
|
||||
refreshSequencerAddr();
|
||||
} else {
|
||||
qWarning() << "AmmUiBackend: wallet open failed, code" << err;
|
||||
refreshNewPositionContext();
|
||||
}
|
||||
}
|
||||
|
||||
@ -199,6 +586,7 @@ QString AmmUiBackend::createNew(QString configPath, QString storagePath, QString
|
||||
refreshAccounts();
|
||||
refreshBlockHeights();
|
||||
refreshSequencerAddr();
|
||||
refreshNewPositionContext();
|
||||
return mnemonic;
|
||||
}
|
||||
|
||||
@ -214,6 +602,7 @@ bool AmmUiBackend::openExisting()
|
||||
refreshBalances();
|
||||
refreshSequencerAddr();
|
||||
QSettings(SETTINGS_ORG, SETTINGS_APP).setValue(DISCONNECTED_KEY, false);
|
||||
refreshNewPositionContext();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -234,6 +623,7 @@ bool AmmUiBackend::openExisting()
|
||||
refreshAccounts();
|
||||
refreshBlockHeights();
|
||||
refreshSequencerAddr();
|
||||
refreshNewPositionContext();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -246,6 +636,7 @@ void AmmUiBackend::disconnectWallet()
|
||||
setIsWalletOpen(false);
|
||||
m_accountModel->replaceFromJsonArray(QJsonArray());
|
||||
QSettings(SETTINGS_ORG, SETTINGS_APP).setValue(DISCONNECTED_KEY, true);
|
||||
refreshNewPositionContext();
|
||||
}
|
||||
|
||||
QString AmmUiBackend::createAccountPublic()
|
||||
@ -269,6 +660,7 @@ void AmmUiBackend::refreshAccounts()
|
||||
const QJsonArray arr = QJsonArray::fromVariantList(m_logos->logos_execution_zone.list_accounts());
|
||||
m_accountModel->replaceFromJsonArray(arr);
|
||||
refreshBalances();
|
||||
refreshNewPositionContext();
|
||||
}
|
||||
|
||||
void AmmUiBackend::refreshBalances()
|
||||
@ -284,6 +676,7 @@ void AmmUiBackend::refreshBalances()
|
||||
m_accountModel->setBalanceByAddress(addr, getBalance(addr, isPub));
|
||||
}
|
||||
saveWallet();
|
||||
refreshNewPositionContext();
|
||||
}
|
||||
|
||||
QString AmmUiBackend::getBalance(QString accountIdHex, bool isPublic)
|
||||
@ -291,6 +684,216 @@ QString AmmUiBackend::getBalance(QString accountIdHex, bool isPublic)
|
||||
return m_logos->logos_execution_zone.get_balance(accountIdHex, isPublic);
|
||||
}
|
||||
|
||||
QString AmmUiBackend::activeAccountAddress() const
|
||||
{
|
||||
if (m_accountModel->count() <= 0)
|
||||
return {};
|
||||
const QModelIndex idx = m_accountModel->index(0, 0);
|
||||
return m_accountModel->data(idx, AccountModel::AddressRole).toString();
|
||||
}
|
||||
|
||||
QVariantMap AmmUiBackend::buildNewPositionContext() const
|
||||
{
|
||||
QVariantMap context;
|
||||
context.insert(QStringLiteral("minimumLiquidity"), 1000.0);
|
||||
context.insert(QStringLiteral("feeTiers"), feeTiers());
|
||||
|
||||
QVariantMap network;
|
||||
network.insert(QStringLiteral("id"), QStringLiteral("devnet"));
|
||||
network.insert(QStringLiteral("name"), QStringLiteral("Local devnet"));
|
||||
network.insert(QStringLiteral("selector"), QStringLiteral("temporary-file"));
|
||||
context.insert(QStringLiteral("network"), network);
|
||||
|
||||
QVariantMap programIds;
|
||||
programIds.insert(QStringLiteral("amm"), stableId(QStringLiteral("devnet:program:amm")));
|
||||
programIds.insert(QStringLiteral("token"), stableId(QStringLiteral("devnet:program:token")));
|
||||
programIds.insert(QStringLiteral("twapOracle"), stableId(QStringLiteral("devnet:program:twap-oracle")));
|
||||
context.insert(QStringLiteral("programIds"), programIds);
|
||||
|
||||
QVariantMap activeAccount;
|
||||
const bool hasAccount = isWalletOpen() && m_accountModel->count() > 0;
|
||||
if (hasAccount) {
|
||||
const QModelIndex idx = m_accountModel->index(0, 0);
|
||||
const QString address = m_accountModel->data(idx, AccountModel::AddressRole).toString();
|
||||
activeAccount.insert(QStringLiteral("name"), m_accountModel->data(idx, AccountModel::NameRole).toString());
|
||||
activeAccount.insert(QStringLiteral("address"), address);
|
||||
activeAccount.insert(QStringLiteral("display"), shortId(address));
|
||||
activeAccount.insert(QStringLiteral("isPublic"), m_accountModel->data(idx, AccountModel::IsPublicRole).toBool());
|
||||
activeAccount.insert(QStringLiteral("balance"), m_accountModel->data(idx, AccountModel::BalanceRole).toString());
|
||||
context.insert(QStringLiteral("holdings"), devnetHoldings(address));
|
||||
context.insert(QStringLiteral("status"), QStringLiteral("ready"));
|
||||
context.insert(QStringLiteral("statusDetail"), QStringLiteral("Devnet token holdings resolved for the active wallet account."));
|
||||
} else {
|
||||
activeAccount.insert(QStringLiteral("name"), QString());
|
||||
activeAccount.insert(QStringLiteral("address"), QString());
|
||||
activeAccount.insert(QStringLiteral("display"), QStringLiteral("Not connected"));
|
||||
activeAccount.insert(QStringLiteral("isPublic"), true);
|
||||
activeAccount.insert(QStringLiteral("balance"), QString());
|
||||
context.insert(QStringLiteral("holdings"), QVariantList());
|
||||
context.insert(QStringLiteral("status"), isWalletOpen() ? QStringLiteral("no_account") : QStringLiteral("no_wallet"));
|
||||
context.insert(QStringLiteral("statusDetail"),
|
||||
isWalletOpen()
|
||||
? QStringLiteral("Create an account before opening a liquidity position.")
|
||||
: QStringLiteral("Connect a wallet before opening a liquidity position."));
|
||||
}
|
||||
context.insert(QStringLiteral("activeAccount"), activeAccount);
|
||||
context.insert(QStringLiteral("activeAccountDisplay"), activeAccount.value(QStringLiteral("display")));
|
||||
return context;
|
||||
}
|
||||
|
||||
QVariant AmmUiBackend::refreshNewPositionContext()
|
||||
{
|
||||
const QVariantMap context = buildNewPositionContext();
|
||||
setNewPositionContext(context);
|
||||
return context;
|
||||
}
|
||||
|
||||
QVariantMap AmmUiBackend::quoteNewPositionMap(const QVariantMap& request) const
|
||||
{
|
||||
const QString tokenA = request.value(QStringLiteral("tokenA")).toString();
|
||||
const QString tokenB = request.value(QStringLiteral("tokenB")).toString();
|
||||
const QVariantMap context = poolContext(tokenA, tokenB);
|
||||
|
||||
if (!isWalletOpen())
|
||||
return quoteError(context, request, tr("Connect a wallet to preview this position."));
|
||||
|
||||
const QString owner = activeAccountAddress();
|
||||
if (owner.isEmpty())
|
||||
return quoteError(context, request, tr("Create an account before previewing this position."));
|
||||
|
||||
const QVariantList holdings = devnetHoldings(owner);
|
||||
const QVariantMap holdingA = holdingBySymbol(holdings, tokenA);
|
||||
const QVariantMap holdingB = holdingBySymbol(holdings, tokenB);
|
||||
if (holdingA.isEmpty() || holdingB.isEmpty())
|
||||
return quoteError(context, request, tr("Choose two token holdings from the active account."));
|
||||
|
||||
const int feeBps = request.value(QStringLiteral("feeBps")).toInt();
|
||||
if (!isSupportedFeeTier(feeBps))
|
||||
return quoteError(context, request, tr("Fee tier is not supported by the AMM program."));
|
||||
|
||||
const QString poolStatus = context.value(QStringLiteral("poolStatus")).toString();
|
||||
const int storedFeeBps = context.value(QStringLiteral("storedFeeBps")).toInt();
|
||||
if (poolStatus == QStringLiteral("active_pool") && feeBps != storedFeeBps)
|
||||
return quoteError(context, request,
|
||||
tr("Existing pool uses %1.").arg(feeLabel(storedFeeBps)));
|
||||
|
||||
const int slippageBps =
|
||||
std::max(1, std::min(5000, request.value(QStringLiteral("slippageBps")).toInt()));
|
||||
|
||||
if (poolStatus == QStringLiteral("active_pool")) {
|
||||
const double inputA = parsePositiveAmount(request.value(QStringLiteral("amountA")).toString());
|
||||
const double inputB = parsePositiveAmount(request.value(QStringLiteral("amountB")).toString());
|
||||
const bool editA = request.value(QStringLiteral("editedSide")).toString() != QStringLiteral("B");
|
||||
const double ratio = activeRatio(tokenA, tokenB);
|
||||
const double amountA = editA ? inputA : inputB / ratio;
|
||||
const double amountB = editA ? inputA * ratio : inputB;
|
||||
const double expectedLp = std::floor(std::min(amountA * 5.5, amountB * 0.69));
|
||||
const double minimumLp = std::floor(expectedLp * (10000 - slippageBps) / 10000.0);
|
||||
|
||||
if (inputA <= 0.0 && inputB <= 0.0)
|
||||
return quoteError(context, request, tr("Enter a deposit amount to preview LP output."));
|
||||
if (amountA > holdingA.value(QStringLiteral("balance")).toDouble())
|
||||
return quoteError(context, request, tr("Insufficient %1 balance.").arg(tokenA));
|
||||
if (amountB > holdingB.value(QStringLiteral("balance")).toDouble())
|
||||
return quoteError(context, request, tr("Insufficient %1 balance.").arg(tokenB));
|
||||
if (minimumLp <= 0.0)
|
||||
return quoteError(context, request, tr("LP minimum rounds to zero. Increase deposit amount."));
|
||||
|
||||
QVariantMap position;
|
||||
position.insert(QStringLiteral("userLp"), QStringLiteral("148320 LP"));
|
||||
position.insert(QStringLiteral("share"), QStringLiteral("1.18%"));
|
||||
position.insert(QStringLiteral("ownedA"), formatTokenAmount(tokenA == QStringLiteral("USDC") ? 14750.0 : 118000.0, tokenA));
|
||||
position.insert(QStringLiteral("ownedB"), formatTokenAmount(tokenB == QStringLiteral("LOGOS") ? 118000.0 : 14750.0, tokenB));
|
||||
return quoteOk(context, request, amountA, amountB, amountA, amountB, expectedLp, minimumLp, 0.0, position);
|
||||
}
|
||||
|
||||
if (poolStatus == QStringLiteral("missing_pool")) {
|
||||
const double price =
|
||||
parsePositiveAmount(request.value(QStringLiteral("initialPrice")).toString()) > 0.0
|
||||
? parsePositiveAmount(request.value(QStringLiteral("initialPrice")).toString())
|
||||
: defaultInitialPrice(tokenA, tokenB);
|
||||
const double scale = std::max(1, request.value(QStringLiteral("depositScale")).toInt());
|
||||
const double amountA = price >= 1.0 ? price * scale : scale;
|
||||
const double amountB = price >= 1.0 ? scale : scale / price;
|
||||
const double expectedLp = std::floor(std::sqrt(amountA * amountB) * 48.0);
|
||||
const double userLp = std::max(0.0, expectedLp - 1000.0);
|
||||
const double minimumLp = std::floor(userLp * (10000 - slippageBps) / 10000.0);
|
||||
|
||||
if (amountA > holdingA.value(QStringLiteral("balance")).toDouble())
|
||||
return quoteError(context, request, tr("Initial deposit exceeds %1 balance.").arg(tokenA));
|
||||
if (amountB > holdingB.value(QStringLiteral("balance")).toDouble())
|
||||
return quoteError(context, request, tr("Initial deposit exceeds %1 balance.").arg(tokenB));
|
||||
if (userLp <= 0.0)
|
||||
return quoteError(context, request, tr("Deposit must mint more than the locked minimum liquidity."));
|
||||
|
||||
QVariantMap position;
|
||||
position.insert(QStringLiteral("userLp"), QStringLiteral("0 LP"));
|
||||
position.insert(QStringLiteral("share"), QStringLiteral("New pool"));
|
||||
position.insert(QStringLiteral("ownedA"), formatTokenAmount(0, tokenA));
|
||||
position.insert(QStringLiteral("ownedB"), formatTokenAmount(0, tokenB));
|
||||
|
||||
QVariantMap quote =
|
||||
quoteOk(context, request, amountA, amountB, amountA, amountB, userLp, minimumLp, 1000.0, position);
|
||||
QVariantMap pool = quote.value(QStringLiteral("pool")).toMap();
|
||||
pool.insert(QStringLiteral("priceText"),
|
||||
QStringLiteral("1 %1 = %2 %3")
|
||||
.arg(tokenB, formatAmount(price), tokenA));
|
||||
quote.insert(QStringLiteral("pool"), pool);
|
||||
return quote;
|
||||
}
|
||||
|
||||
return quoteError(context, request, context.value(QStringLiteral("detail")).toString());
|
||||
}
|
||||
|
||||
QVariant AmmUiBackend::quoteNewPosition(QVariant request)
|
||||
{
|
||||
return quoteNewPositionMap(request.toMap());
|
||||
}
|
||||
|
||||
QVariant AmmUiBackend::submitNewPosition(QVariant request, QString quoteHash)
|
||||
{
|
||||
const QVariantMap quote = quoteNewPositionMap(request.toMap());
|
||||
if (quote.value(QStringLiteral("status")).toString() != QStringLiteral("ok")) {
|
||||
QVariantMap result;
|
||||
result.insert(QStringLiteral("status"), QStringLiteral("error"));
|
||||
result.insert(QStringLiteral("code"), QStringLiteral("quote_error"));
|
||||
result.insert(QStringLiteral("error"), quote.value(QStringLiteral("error")));
|
||||
result.insert(QStringLiteral("quote"), quote);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (quote.value(QStringLiteral("quoteHash")).toString() != quoteHash) {
|
||||
QVariantMap result;
|
||||
result.insert(QStringLiteral("status"), QStringLiteral("error"));
|
||||
result.insert(QStringLiteral("code"), QStringLiteral("quote_changed"));
|
||||
result.insert(QStringLiteral("error"), tr("Quote changed. Refresh preview before submitting."));
|
||||
result.insert(QStringLiteral("quote"), quote);
|
||||
return result;
|
||||
}
|
||||
|
||||
const QVariantMap transaction = quote.value(QStringLiteral("transaction")).toMap();
|
||||
if (!transaction.value(QStringLiteral("ready")).toBool()) {
|
||||
QVariantMap result;
|
||||
result.insert(QStringLiteral("status"), QStringLiteral("error"));
|
||||
result.insert(QStringLiteral("code"), QStringLiteral("submit_unavailable"));
|
||||
result.insert(QStringLiteral("error"), transaction.value(QStringLiteral("reason")).toString());
|
||||
result.insert(QStringLiteral("quote"), quote);
|
||||
return result;
|
||||
}
|
||||
|
||||
QVariantMap result;
|
||||
result.insert(QStringLiteral("status"), QStringLiteral("ok"));
|
||||
result.insert(QStringLiteral("message"),
|
||||
quote.value(QStringLiteral("instruction")).toString() == QStringLiteral("new_definition")
|
||||
? tr("Pool creation submitted")
|
||||
: tr("Liquidity deposit submitted"));
|
||||
result.insert(QStringLiteral("detail"),
|
||||
QStringLiteral("%1 / %2")
|
||||
.arg(request.toMap().value(QStringLiteral("tokenA")).toString(),
|
||||
request.toMap().value(QStringLiteral("tokenB")).toString()));
|
||||
return result;
|
||||
}
|
||||
|
||||
void AmmUiBackend::refreshBlockHeights()
|
||||
{
|
||||
const int lastVal = m_logos->logos_execution_zone.get_last_synced_block();
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QVariant>
|
||||
|
||||
#include "rep_AmmUiBackend_source.h"
|
||||
|
||||
@ -34,6 +35,9 @@ public slots:
|
||||
void refreshAccounts() override;
|
||||
void refreshBalances() override;
|
||||
QString getBalance(QString accountIdHex, bool isPublic) override;
|
||||
QVariant refreshNewPositionContext() override;
|
||||
QVariant quoteNewPosition(QVariant request) override;
|
||||
QVariant submitNewPosition(QVariant request, QString quoteHash) override;
|
||||
// Return the new wallet's BIP39 mnemonic (empty string on failure) so the
|
||||
// UI can force a one-time seed-phrase backup step.
|
||||
QString createNewDefault(QString password) override;
|
||||
@ -61,6 +65,9 @@ private:
|
||||
void refreshBlockHeights();
|
||||
void refreshSequencerAddr();
|
||||
void saveWallet();
|
||||
QString activeAccountAddress() const;
|
||||
QVariantMap buildNewPositionContext() const;
|
||||
QVariantMap quoteNewPositionMap(const QVariantMap& request) const;
|
||||
|
||||
// Probe the configured sequencer over HTTP and update sequencerReachable.
|
||||
void checkReachability();
|
||||
|
||||
@ -23,6 +23,14 @@ class AmmUiBackend
|
||||
SLOT(void refreshBalances())
|
||||
SLOT(QString getBalance(QString accountIdHex, bool isPublic))
|
||||
|
||||
// New Position backend surface. QML calls these through logos.watch(...).
|
||||
// The QVariant payloads are stable maps/lists so the UI never assembles AMM
|
||||
// transactions or duplicates quote state.
|
||||
PROP(QVariant newPositionContext READONLY)
|
||||
SLOT(QVariant refreshNewPositionContext())
|
||||
SLOT(QVariant quoteNewPosition(QVariant request))
|
||||
SLOT(QVariant submitNewPosition(QVariant request, QString quoteHash))
|
||||
|
||||
// Wallet lifecycle. createNewDefault() is the happy path: it creates a
|
||||
// fresh per-app wallet at walletHome with no path picking. createNew()
|
||||
// keeps explicit paths for an "advanced" flow. Both return the new wallet's
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user