diff --git a/apps/amm/qml/Main.qml b/apps/amm/qml/Main.qml index c8619b1..3350071 100644 --- a/apps/amm/qml/Main.qml +++ b/apps/amm/qml/Main.qml @@ -87,6 +87,7 @@ Item { LiquidityPage { anchors.fill: parent + backend: root.ready ? root.backend : null visible: navbar.currentIndex === 1 } diff --git a/apps/amm/qml/components/liquidity/NewPositionForm.qml b/apps/amm/qml/components/liquidity/NewPositionForm.qml index 4019def..66566fe 100644 --- a/apps/amm/qml/components/liquidity/NewPositionForm.qml +++ b/apps/amm/qml/components/liquidity/NewPositionForm.qml @@ -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) { diff --git a/apps/amm/qml/pages/LiquidityPage.qml b/apps/amm/qml/pages/LiquidityPage.qml index 675a6fd..2c2db92 100644 --- a/apps/amm/qml/pages/LiquidityPage.qml +++ b/apps/amm/qml/pages/LiquidityPage.qml @@ -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": [] + }; } } diff --git a/apps/amm/src/AmmUiBackend.cpp b/apps/amm/src/AmmUiBackend.cpp index c4176e3..2ce57dc 100644 --- a/apps/amm/src/AmmUiBackend.cpp +++ b/apps/amm/src/AmmUiBackend.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -20,6 +21,9 @@ #include "logos_api.h" #include "logos_sdk.h" +#include +#include + 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(); diff --git a/apps/amm/src/AmmUiBackend.h b/apps/amm/src/AmmUiBackend.h index 14d1e80..6764676 100644 --- a/apps/amm/src/AmmUiBackend.h +++ b/apps/amm/src/AmmUiBackend.h @@ -3,6 +3,7 @@ #include #include +#include #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(); diff --git a/apps/amm/src/AmmUiBackend.rep b/apps/amm/src/AmmUiBackend.rep index c9e2d50..5bae9eb 100644 --- a/apps/amm/src/AmmUiBackend.rep +++ b/apps/amm/src/AmmUiBackend.rep @@ -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