diff --git a/apps/amm/qml/components/liquidity/NewPositionConfirmationDialog.qml b/apps/amm/qml/components/liquidity/NewPositionConfirmationDialog.qml index e07c782..e217b63 100644 --- a/apps/amm/qml/components/liquidity/NewPositionConfirmationDialog.qml +++ b/apps/amm/qml/components/liquidity/NewPositionConfirmationDialog.qml @@ -7,6 +7,8 @@ FocusScope { property var snapshot: ({}) property bool open: false + property bool busy: false + property string errorText: "" signal canceled signal confirmed(var snapshot) @@ -18,20 +20,28 @@ FocusScope { function openWithSnapshot(nextSnapshot) { root.snapshot = nextSnapshot; + root.errorText = ""; root.open = true; root.forceActiveFocus(); cancelButton.forceActiveFocus(); } function cancel() { + if (root.busy) + return; root.open = false; root.canceled(); } function confirm() { - const confirmedSnapshot = root.snapshot; + if (root.busy) + return; + root.errorText = ""; + root.confirmed(root.snapshot); + } + + function closeAfterSuccess() { root.open = false; - root.confirmed(confirmedSnapshot); } Rectangle { @@ -154,6 +164,29 @@ FocusScope { Layout.fillWidth: true } + Rectangle { + color: "#211914" + radius: 8 + border.color: "#49301F" + border.width: 1 + visible: root.errorText.length > 0 + + Layout.fillWidth: true + Layout.preferredHeight: visible ? submitError.implicitHeight + 20 : 0 + + Text { + id: submitError + + anchors.fill: parent + anchors.margins: 10 + color: "#F08A76" + font.pixelSize: 12 + lineHeight: 1.2 + text: root.errorText + wrapMode: Text.WordWrap + } + } + RowLayout { spacing: 8 @@ -163,6 +196,7 @@ FocusScope { id: cancelButton activeFocusOnTab: true + enabled: !root.busy focusPolicy: Qt.StrongFocus hoverEnabled: true text: qsTr("Cancel") @@ -196,9 +230,10 @@ FocusScope { id: confirmButton activeFocusOnTab: true + enabled: !root.busy focusPolicy: Qt.StrongFocus hoverEnabled: true - text: qsTr("Submit") + text: root.busy ? qsTr("Submitting") : qsTr("Submit") Accessible.name: confirmButton.text @@ -208,7 +243,7 @@ FocusScope { onClicked: root.confirm() contentItem: Text { - color: "#151515" + color: confirmButton.enabled ? "#151515" : "#7D756E" elide: Text.ElideRight font.bold: true font.pixelSize: 13 @@ -218,9 +253,9 @@ FocusScope { } background: Rectangle { - border.color: "#F26A21" + border.color: confirmButton.enabled ? "#F26A21" : "#343434" border.width: 1 - color: confirmButton.pressed ? "#D95C1E" : confirmButton.hovered || confirmButton.activeFocus ? "#FF8A3D" : "#F26A21" + color: confirmButton.enabled ? confirmButton.pressed ? "#D95C1E" : confirmButton.hovered || confirmButton.activeFocus ? "#FF8A3D" : "#F26A21" : "#181818" radius: 6 } } diff --git a/apps/amm/qml/components/liquidity/NewPositionForm.qml b/apps/amm/qml/components/liquidity/NewPositionForm.qml index 66566fe..582a12e 100644 --- a/apps/amm/qml/components/liquidity/NewPositionForm.qml +++ b/apps/amm/qml/components/liquidity/NewPositionForm.qml @@ -7,41 +7,10 @@ Rectangle { id: root 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 var quotePayload: ({}) + property bool contextLoading: false + property bool quoteLoading: false + property bool submitting: false property int selectedTokenAIndex: 0 property int selectedTokenBIndex: 1 property int selectedFeeBps: 30 @@ -52,7 +21,6 @@ Rectangle { property string initialPrice: "2500" property int depositScale: 1 property string submitError: "" - property bool applyingQuote: false readonly property var emptyToken: ({ "symbol": "", @@ -65,14 +33,15 @@ Rectangle { 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 quote: root.completeQuote(root.quotePayload) 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 bool canConfirm: root.quote.status === "ok" + readonly property bool canConfirm: !root.contextLoading && !root.quoteLoading && !root.submitting && 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) + readonly property bool showQuoteError: !root.quoteLoading && root.quote.status === "error" && (root.poolStatus === "unavailable_pool" || root.missingPool || root.hasActiveInput) signal quoteRequested(var request) signal confirmationRequested(var snapshot) @@ -266,7 +235,7 @@ Rectangle { readonly property bool duplicate: root.selectedTokenBIndex === index activeFocusOnTab: true - enabled: !duplicate + enabled: !duplicate && !root.contextLoading && !root.submitting focusPolicy: Qt.StrongFocus hoverEnabled: true text: holding.symbol @@ -349,7 +318,7 @@ Rectangle { readonly property bool duplicate: root.selectedTokenAIndex === index activeFocusOnTab: true - enabled: !duplicate + enabled: !duplicate && !root.contextLoading && !root.submitting focusPolicy: Qt.StrongFocus hoverEnabled: true text: holding.symbol @@ -409,6 +378,17 @@ Rectangle { } } + Text { + color: "#A9A098" + font.pixelSize: 12 + lineHeight: 1.2 + text: root.contextLoading ? qsTr("Loading wallet holdings.") : root.newPositionContext.statusDetail || qsTr("Connect a wallet to load token holdings.") + visible: root.holdings.length === 0 || root.contextLoading + wrapMode: Text.WordWrap + + Layout.fillWidth: true + } + Text { color: "#E7E1D8" font.bold: true @@ -436,7 +416,7 @@ Rectangle { readonly property bool chipEnabled: disabledReason.length === 0 readonly property bool selected: root.selectedFeeBps === tier.bps - activeFocusOnTab: chipEnabled + activeFocusOnTab: chipEnabled && !root.submitting color: selected ? "#211914" : feeMouse.containsMouse && chipEnabled ? "#202020" : "#101010" radius: 6 border.color: selected || activeFocus ? "#F26A21" : chipEnabled ? "#343434" : "#2A2A2A" @@ -451,11 +431,11 @@ Rectangle { Layout.minimumHeight: 42 Keys.onSpacePressed: { - if (chipEnabled) + if (chipEnabled && !root.submitting) root.selectFeeTier(tier.bps); } Keys.onReturnPressed: { - if (chipEnabled) + if (chipEnabled && !root.submitting) root.selectFeeTier(tier.bps); } @@ -471,11 +451,11 @@ Rectangle { id: feeMouse anchors.fill: parent - cursorShape: feeChip.chipEnabled ? Qt.PointingHandCursor : Qt.ArrowCursor + cursorShape: feeChip.chipEnabled && !root.submitting ? Qt.PointingHandCursor : Qt.ArrowCursor hoverEnabled: true onClicked: { - if (feeChip.chipEnabled) { + if (feeChip.chipEnabled && !root.submitting) { feeChip.forceActiveFocus(); root.selectFeeTier(feeChip.tier.bps); } @@ -763,7 +743,7 @@ Rectangle { enabled: root.canConfirm focusPolicy: Qt.StrongFocus hoverEnabled: true - text: root.canConfirm ? qsTr("Preview and confirm") : qsTr("Preview unavailable") + text: root.submitting ? qsTr("Submitting") : root.quoteLoading ? qsTr("Updating preview") : root.canConfirm ? qsTr("Preview and confirm") : qsTr("Preview unavailable") Accessible.name: confirmButton.text @@ -824,11 +804,11 @@ Rectangle { } Text { - color: root.quote.status === "ok" ? "#8FD6A4" : "#F08A76" + color: root.quoteLoading ? "#F2B366" : root.quote.status === "ok" ? "#8FD6A4" : "#F08A76" font.bold: true font.pixelSize: 11 horizontalAlignment: Text.AlignRight - text: root.quote.status === "ok" ? qsTr("Ready") : qsTr("Needs input") + text: root.quoteLoading ? qsTr("Updating") : root.quote.status === "ok" ? qsTr("Ready") : qsTr("Needs input") Layout.maximumWidth: 120 } @@ -1045,11 +1025,6 @@ Rectangle { 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); @@ -1076,6 +1051,64 @@ Rectangle { }; } + function completeAmount(payload, symbol) { + const value = payload || root.amountValue(0, symbol); + const amount = Math.max(0, Number(value.value) || 0); + const input = value.input || root.formatAmount(amount); + return { + "value": amount, + "input": input, + "display": value.display || root.formatTokenAmount(amount, symbol), + "symbol": value.symbol || symbol + }; + } + + function completeQuote(payload) { + const quote = payload || {}; + const pool = quote.pool || {}; + const deposit = quote.deposit || {}; + const lp = quote.lp || {}; + const position = quote.position || {}; + const tokenA = root.tokenA.symbol; + const tokenB = root.tokenB.symbol; + + return { + "status": quote.status || "error", + "error": quote.error || "", + "poolStatus": quote.poolStatus || "unavailable_pool", + "statusLabel": quote.statusLabel || qsTr("Unavailable"), + "statusDetail": quote.statusDetail || quote.error || qsTr("Connect a wallet to preview this position."), + "instruction": quote.instruction || "", + "storedFeeBps": quote.storedFeeBps || 0, + "feeBps": quote.feeBps || root.selectedFeeBps, + "feeLabel": quote.feeLabel || root.feeLabel(root.selectedFeeBps), + "quoteHash": quote.quoteHash || "", + "pool": { + "id": pool.id || "", + "priceText": pool.priceText || "", + "reserveText": pool.reserveText || "" + }, + "deposit": { + "maxA": root.completeAmount(deposit.maxA, tokenA), + "maxB": root.completeAmount(deposit.maxB, tokenB), + "actualA": root.completeAmount(deposit.actualA, tokenA), + "actualB": root.completeAmount(deposit.actualB, tokenB) + }, + "lp": { + "expected": root.completeAmount(lp.expected, "LP"), + "minimum": root.completeAmount(lp.minimum, "LP"), + "locked": root.completeAmount(lp.locked, "LP") + }, + "position": { + "userLp": position.userLp || "0 LP", + "share": position.share || "-", + "ownedA": position.ownedA || root.formatTokenAmount(0, tokenA), + "ownedB": position.ownedB || root.formatTokenAmount(0, tokenB) + }, + "accountChanges": quote.accountChanges || [] + }; + } + function feeLabel(bps) { if (bps === 1) return "0.01%"; @@ -1188,12 +1221,10 @@ Rectangle { if (root.quote.status !== "ok" || root.quote.poolStatus !== "active_pool") return; - root.applyingQuote = true; if (root.editedSide === "A") root.amountB = root.quote.deposit.maxB.input; else root.amountA = root.quote.deposit.maxA.input; - root.applyingQuote = false; } function useMaxActive(side) { diff --git a/apps/amm/qml/pages/LiquidityPage.qml b/apps/amm/qml/pages/LiquidityPage.qml index 2c2db92..6be0b7b 100644 --- a/apps/amm/qml/pages/LiquidityPage.qml +++ b/apps/amm/qml/pages/LiquidityPage.qml @@ -8,9 +8,12 @@ Item { property var backend: null property var newPositionContext: ({}) - property var newPositionQuote: ({}) + property var newPositionQuote: root.errorQuote(qsTr("Wallet backend is unavailable."), root.currentRequest()) property int quoteSerial: 0 property bool formReady: false + property bool contextLoading: false + property bool quoteLoading: false + property bool submitting: false readonly property int pageMargin: 16 readonly property int preferredCardWidth: 960 @@ -53,7 +56,10 @@ Item { id: newPositionForm newPositionContext: root.newPositionContext - quote: root.newPositionQuote + quotePayload: root.newPositionQuote + contextLoading: root.contextLoading + quoteLoading: root.quoteLoading + submitting: root.submitting 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 @@ -90,6 +96,7 @@ Item { id: confirmationDialog anchors.fill: parent + busy: root.submitting onConfirmed: function (snapshot) { root.confirmNewPosition(snapshot); @@ -97,28 +104,42 @@ Item { } function confirmNewPosition(snapshot) { + if (root.submitting) + return; + + root.submitting = true; + confirmationDialog.errorText = ""; + newPositionForm.setSubmitError(""); + if (!root.backend || typeof logos === "undefined") { - newPositionForm.setSubmitError(qsTr("Wallet backend is unavailable.")); + root.submitting = false; + root.showSubmitError(qsTr("Wallet backend is unavailable.")); return; } logos.watch(root.backend.submitNewPosition(snapshot.request, snapshot.quoteHash), function(result) { + root.submitting = false; if (result.status !== "ok") { - newPositionForm.setSubmitError(result.error); + root.showSubmitError(result.error); return; } + confirmationDialog.closeAfterSuccess(); newPositionForm.resetAfterSubmit(); successToast.show(result.message, result.detail); }, function(error) { - newPositionForm.setSubmitError(qsTr("Error submitting position: %1").arg(error)); + root.submitting = false; + root.showSubmitError(qsTr("Error submitting position: %1").arg(error)); }); } function refreshNewPositionContext() { + root.contextLoading = true; + if (!root.backend || typeof logos === "undefined") { + root.contextLoading = false; root.newPositionContext = { "activeAccountDisplay": qsTr("Not connected"), "holdings": [], @@ -130,16 +151,21 @@ Item { logos.watch(root.backend.refreshNewPositionContext(), function(context) { + root.contextLoading = false; root.newPositionContext = context; root.requestQuote(root.currentRequest()); }, function(error) { + root.contextLoading = false; root.newPositionQuote = root.errorQuote(qsTr("Error loading position context: %1").arg(error), root.currentRequest()); }); } function requestQuote(request) { + root.quoteLoading = true; + if (!root.backend || typeof logos === "undefined") { + root.quoteLoading = false; root.newPositionQuote = root.errorQuote(qsTr("Wallet backend is unavailable."), request); return; } @@ -147,15 +173,25 @@ Item { const serial = ++root.quoteSerial; logos.watch(root.backend.quoteNewPosition(request), function(quote) { - if (serial === root.quoteSerial) + if (serial === root.quoteSerial) { + root.quoteLoading = false; root.newPositionQuote = quote; + } }, function(error) { - if (serial === root.quoteSerial) + if (serial === root.quoteSerial) { + root.quoteLoading = false; root.newPositionQuote = root.errorQuote(qsTr("Error loading quote: %1").arg(error), request); + } }); } + function showSubmitError(message) { + const text = message && message.length > 0 ? message : qsTr("Position submission failed."); + confirmationDialog.errorText = text; + newPositionForm.setSubmitError(text); + } + function amountValue(symbol) { return { "value": 0, diff --git a/apps/amm/qml/state/NewPositionPrototypeBackend.qml b/apps/amm/qml/state/NewPositionPrototypeBackend.qml deleted file mode 100644 index f81f221..0000000 --- a/apps/amm/qml/state/NewPositionPrototypeBackend.qml +++ /dev/null @@ -1,468 +0,0 @@ -import QtQuick 2.15 - -QtObject { - id: root - - readonly property string activeAccount: "lz1q9s...42fd" - readonly property var holdings: [ - { - "symbol": "USDC", - "name": "USD Coin", - "definitionId": "token:usdc-testnet", - "holdingId": "holding:active:usdc", - "balance": 12450, - "balanceText": "12,450.00 USDC", - "accent": "#2E7CF6" - }, - { - "symbol": "LOGOS", - "name": "Logos", - "definitionId": "token:logos-testnet", - "holdingId": "holding:active:logos", - "balance": 850000, - "balanceText": "850,000 LOGOS", - "accent": "#F26A21" - }, - { - "symbol": "WETH", - "name": "Wrapped Ether", - "definitionId": "token:weth-testnet", - "holdingId": "holding:active:weth", - "balance": 3.25, - "balanceText": "3.25 WETH", - "accent": "#B7C2D8" - } - ] - readonly property var feeTiers: [ - { - "bps": 1, - "label": "0.01%", - "supported": true - }, - { - "bps": 5, - "label": "0.05%", - "supported": true - }, - { - "bps": 25, - "label": "0.25%", - "supported": false - }, - { - "bps": 30, - "label": "0.30%", - "supported": true - }, - { - "bps": 100, - "label": "1.00%", - "supported": true - } - ] - readonly property real minimumLiquidity: 1000 - - function holdingBySymbol(symbol) { - for (let i = 0; i < root.holdings.length; ++i) { - if (root.holdings[i].symbol === symbol) - return root.holdings[i]; - } - - return null; - } - - function feeTierByBps(bps) { - for (let i = 0; i < root.feeTiers.length; ++i) { - if (root.feeTiers[i].bps === bps) - return root.feeTiers[i]; - } - - return null; - } - - function feeLabel(bps) { - const tier = feeTierByBps(bps); - return tier ? tier.label : qsTr("%1 bps").arg(bps); - } - - function parseAmount(value) { - const parsed = Number(value); - return isFinite(parsed) && parsed > 0 ? parsed : 0; - } - - function formatAmount(value) { - const amount = Math.max(0, Number(value) || 0); - - if (amount >= 1000) - return amount.toFixed(2).replace(/\.00$/, "").replace(/\B(?=(\d{3})+(?!\d))/g, ","); - - if (amount >= 1) - return amount.toFixed(4).replace(/0+$/, "").replace(/[.]$/, ""); - - return amount.toFixed(6).replace(/0+$/, "").replace(/[.]$/, ""); - } - - function formatTokenAmount(value, symbol) { - return qsTr("%1 %2").arg(formatAmount(value)).arg(symbol); - } - - function unorderedPairKey(symbolA, symbolB) { - return symbolA < symbolB ? symbolA + "/" + symbolB : symbolB + "/" + symbolA; - } - - function poolContext(symbolA, symbolB) { - if (!symbolA || !symbolB || symbolA === symbolB) { - return { - "poolStatus": "unavailable_pool", - "statusLabel": qsTr("Unavailable"), - "detail": qsTr("Choose two different assets from the active account."), - "instruction": "", - "storedFeeBps": 0, - "poolId": "", - "priceText": "", - "reserveText": "" - }; - } - - const key = unorderedPairKey(symbolA, symbolB); - - if (key === "LOGOS/USDC") { - return { - "poolStatus": "active_pool", - "statusLabel": qsTr("Active pool"), - "detail": qsTr("Deposits are quoted against the existing pool ratio. Nonmatching fee tiers are locked."), - "instruction": "add_liquidity", - "storedFeeBps": 30, - "poolId": "pool:usdc-logos", - "priceText": qsTr("1 USDC = 8 LOGOS"), - "reserveText": qsTr("1,250,000 USDC / 10,000,000 LOGOS") - }; - } - - if (key === "USDC/WETH") { - return { - "poolStatus": "missing_pool", - "statusLabel": qsTr("Missing pool"), - "detail": qsTr("Set the initial price first, then scale both deposits together."), - "instruction": "new_definition", - "storedFeeBps": 0, - "poolId": "pool:usdc-weth", - "priceText": qsTr("No reserves yet"), - "reserveText": qsTr("Pool account is empty") - }; - } - - return { - "poolStatus": "unavailable_pool", - "statusLabel": qsTr("Unavailable"), - "detail": qsTr("A pool account exists, but it cannot be quoted safely for this prototype state."), - "instruction": "", - "storedFeeBps": 0, - "poolId": "pool:logos-weth", - "priceText": qsTr("Quote disabled"), - "reserveText": qsTr("Unsupported stored pool state") - }; - } - - function activeQuote(request, context) { - const tokenA = holdingBySymbol(request.tokenA); - const tokenB = holdingBySymbol(request.tokenB); - const inputA = parseAmount(request.amountA); - const inputB = parseAmount(request.amountB); - const editA = request.editedSide !== "B"; - const amountA = editA ? inputA : inputB / activeRatio(request.tokenA, request.tokenB); - const amountB = editA ? inputA * activeRatio(request.tokenA, request.tokenB) : inputB; - const expectedLp = Math.floor(Math.min(amountA * 5.5, amountB * 0.69)); - const minLp = Math.floor(expectedLp * (10000 - request.slippageBps) / 10000); - - if (inputA <= 0 && inputB <= 0) - return quoteError(context, request, qsTr("Enter a deposit amount to preview LP output.")); - - if (amountA > tokenA.balance) - return quoteError(context, request, qsTr("Insufficient %1 balance.").arg(tokenA.symbol)); - - if (amountB > tokenB.balance) - return quoteError(context, request, qsTr("Insufficient %1 balance.").arg(tokenB.symbol)); - - if (minLp <= 0) - return quoteError(context, request, qsTr("LP minimum rounds to zero. Increase deposit amount.")); - - return quoteOk(context, request, { - "maxA": amountA, - "maxB": amountB, - "actualA": amountA, - "actualB": amountB, - "expectedLp": expectedLp, - "minimumLp": minLp, - "lockedLp": 0, - "position": { - "userLp": "148,320 LP", - "share": "1.18%", - "ownedA": request.tokenA === "USDC" ? "14,750 USDC" : "118,000 LOGOS", - "ownedB": request.tokenB === "LOGOS" ? "118,000 LOGOS" : "14,750 USDC" - }, - "accountChanges": [ - { - "role": qsTr("Config"), - "id": "amm:config", - "action": qsTr("Read") - }, - { - "role": qsTr("Pool"), - "id": context.poolId, - "action": qsTr("Update") - }, - { - "role": qsTr("Vault A"), - "id": "vault:" + request.tokenA.toLowerCase(), - "action": qsTr("Update") - }, - { - "role": qsTr("Vault B"), - "id": "vault:" + request.tokenB.toLowerCase(), - "action": qsTr("Update") - }, - { - "role": qsTr("User LP holding"), - "id": "holding:active:lp", - "action": qsTr("Update or create") - }, - { - "role": qsTr("Clock"), - "id": "clock:canonical", - "action": qsTr("Read") - } - ] - }); - } - - function missingQuote(request, context) { - const tokenA = holdingBySymbol(request.tokenA); - const tokenB = holdingBySymbol(request.tokenB); - const price = parseAmount(request.initialPrice) || defaultInitialPrice(request.tokenA, request.tokenB); - const scale = Math.max(1, Number(request.depositScale) || 1); - const amountA = price >= 1 ? price * scale : scale; - const amountB = price >= 1 ? scale : scale / price; - const expectedLp = Math.floor(Math.sqrt(amountA * amountB) * 48); - const userLp = Math.max(0, expectedLp - root.minimumLiquidity); - const minLp = Math.floor(userLp * (10000 - request.slippageBps) / 10000); - - if (amountA > tokenA.balance) - return quoteError(context, request, qsTr("Initial deposit exceeds %1 balance.").arg(tokenA.symbol)); - - if (amountB > tokenB.balance) - return quoteError(context, request, qsTr("Initial deposit exceeds %1 balance.").arg(tokenB.symbol)); - - if (userLp <= 0) - return quoteError(context, request, qsTr("Deposit must mint more than the locked minimum liquidity.")); - - return quoteOk(context, request, { - "maxA": amountA, - "maxB": amountB, - "actualA": amountA, - "actualB": amountB, - "expectedLp": userLp, - "minimumLp": minLp, - "lockedLp": root.minimumLiquidity, - "position": { - "userLp": "0 LP", - "share": "New pool", - "ownedA": "0 " + request.tokenA, - "ownedB": "0 " + request.tokenB - }, - "accountChanges": [ - { - "role": qsTr("Config"), - "id": "amm:config", - "action": qsTr("Read") - }, - { - "role": qsTr("Pool"), - "id": context.poolId, - "action": qsTr("Create") - }, - { - "role": qsTr("Vault A"), - "id": "vault:" + request.tokenA.toLowerCase(), - "action": qsTr("Update or create") - }, - { - "role": qsTr("Vault B"), - "id": "vault:" + request.tokenB.toLowerCase(), - "action": qsTr("Update or create") - }, - { - "role": qsTr("LP definition"), - "id": "lp:" + context.poolId, - "action": qsTr("Create") - }, - { - "role": qsTr("LP lock holding"), - "id": "holding:lp-lock", - "action": qsTr("Create") - }, - { - "role": qsTr("User LP holding"), - "id": "holding:active:lp", - "action": qsTr("Update or create") - }, - { - "role": qsTr("Current tick"), - "id": "twap:" + context.poolId, - "action": qsTr("Create") - }, - { - "role": qsTr("Clock"), - "id": "clock:canonical", - "action": qsTr("Read") - } - ] - }); - } - - function quoteNewPosition(request) { - const context = poolContext(request.tokenA, request.tokenB); - const tier = feeTierByBps(request.feeBps); - - if (!tier || !tier.supported) - return quoteError(context, request, qsTr("Fee tier is not supported by the AMM program.")); - - if (context.poolStatus === "active_pool" && request.feeBps !== context.storedFeeBps) - return quoteError(context, request, qsTr("Existing pool uses %1.").arg(feeLabel(context.storedFeeBps))); - - if (context.poolStatus === "active_pool") - return activeQuote(request, context); - - if (context.poolStatus === "missing_pool") - return missingQuote(request, context); - - return quoteError(context, request, context.detail); - } - - function submitNewPosition(request, quoteHash) { - const quote = quoteNewPosition(request); - - if (quote.status !== "ok") { - return { - "status": "error", - "error": quote.error - }; - } - - if (quote.quoteHash !== quoteHash) { - return { - "status": "error", - "error": qsTr("Quote changed. Refresh preview before submitting.") - }; - } - - return { - "status": "ok", - "message": quote.instruction === "new_definition" ? qsTr("Pool creation submitted") : qsTr("Liquidity deposit submitted"), - "detail": qsTr("%1 / %2").arg(request.tokenA).arg(request.tokenB) - }; - } - - function quoteOk(context, request, amounts) { - return { - "status": "ok", - "error": "", - "poolStatus": context.poolStatus, - "statusLabel": context.statusLabel, - "statusDetail": context.detail, - "instruction": context.instruction, - "feeBps": request.feeBps, - "feeLabel": feeLabel(request.feeBps), - "quoteHash": quoteHash(request), - "pool": { - "id": context.poolId, - "priceText": context.poolStatus === "missing_pool" ? qsTr("1 %1 = %2 %3").arg(request.tokenB).arg(formatAmount(parseAmount(request.initialPrice) || defaultInitialPrice(request.tokenA, request.tokenB))).arg(request.tokenA) : context.priceText, - "reserveText": context.reserveText - }, - "deposit": { - "maxA": amountValue(amounts.maxA, request.tokenA), - "maxB": amountValue(amounts.maxB, request.tokenB), - "actualA": amountValue(amounts.actualA, request.tokenA), - "actualB": amountValue(amounts.actualB, request.tokenB) - }, - "lp": { - "expected": amountValue(amounts.expectedLp, "LP"), - "minimum": amountValue(amounts.minimumLp, "LP"), - "locked": amountValue(amounts.lockedLp, "LP") - }, - "position": amounts.position, - "accountChanges": amounts.accountChanges - }; - } - - function quoteError(context, request, errorText) { - return { - "status": "error", - "error": errorText, - "poolStatus": context.poolStatus, - "statusLabel": context.statusLabel, - "statusDetail": context.detail, - "instruction": context.instruction, - "feeBps": request.feeBps, - "feeLabel": feeLabel(request.feeBps), - "quoteHash": quoteHash(request), - "pool": { - "id": context.poolId, - "priceText": context.priceText, - "reserveText": context.reserveText - }, - "deposit": { - "maxA": amountValue(0, request.tokenA), - "maxB": amountValue(0, request.tokenB), - "actualA": amountValue(0, request.tokenA), - "actualB": amountValue(0, request.tokenB) - }, - "lp": { - "expected": amountValue(0, "LP"), - "minimum": amountValue(0, "LP"), - "locked": amountValue(context.poolStatus === "missing_pool" ? root.minimumLiquidity : 0, "LP") - }, - "position": { - "userLp": "0 LP", - "share": "-", - "ownedA": "0 " + request.tokenA, - "ownedB": "0 " + request.tokenB - }, - "accountChanges": [] - }; - } - - function amountValue(value, symbol) { - const amount = Math.max(0, Number(value) || 0); - return { - "value": amount, - "input": formatAmount(amount), - "display": formatTokenAmount(amount, symbol), - "symbol": symbol - }; - } - - function activeRatio(symbolA, symbolB) { - if (symbolA === "USDC" && symbolB === "LOGOS") - return 8; - - if (symbolA === "LOGOS" && symbolB === "USDC") - return 0.125; - - return 1; - } - - function defaultInitialPrice(symbolA, symbolB) { - if (symbolA === "USDC" && symbolB === "WETH") - return 2500; - - if (symbolA === "WETH" && symbolB === "USDC") - return 0.0004; - - return 1; - } - - function quoteHash(request) { - return "demo-" + request.tokenA + "-" + request.tokenB + "-" + request.feeBps + "-" + request.editedSide + "-" + request.amountA + "-" + request.amountB + "-" + request.initialPrice + "-" + request.depositScale + "-" + request.slippageBps; - } -}