mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-07-15 11:29:59 +00:00
feat(amm-ui): wire new position QML flow #223
This commit is contained in:
parent
b288b543ed
commit
742c73ecae
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user