feat(amm-ui): wire new position QML flow #223

This commit is contained in:
Ricardo Guilherme Schmidt 2026-07-10 03:26:03 -03:00
parent b288b543ed
commit 742c73ecae
No known key found for this signature in database
GPG Key ID: 1396EA17DE132FFE
4 changed files with 170 additions and 536 deletions

View File

@ -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
}
}

View File

@ -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) {

View File

@ -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,

View File

@ -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;
}
}