Files
lez-programs/apps/amm/qml/state/NewPositionFlow.qml
r4bbit afeba568d8 refactor(amm): drop the new-position schema version tag
The add-liquidity flow stamped a `new-position.v1` schema tag on every
request and response and validated it across all three layers — the QML
plugin, the amm_module core module, and the amm_client Rust crate. It was a
cross-version compatibility guard, but these artifacts always ship together,
so the contract is honored implicitly, and the swap view already works fine
without one. Dropping it makes the liquidity view consistent with swap and
removes a layer of ceremony.

- amm_client: remove PositionRequest.schema and the unsupported_schema check
  in compute_quote; drop QuoteCommitment.schema (changes quoteHash, which is
  internal-only) and every "schema" response stamp; delete the SCHEMA /
  NEW_POSITION_SCHEMA constants and the public export.
- amm_module: remove the SCHEMA constant and its four response stamps.
- AmmUiBackend: remove its local NEW_POSITION_SCHEMA and the stamps in
  loadingContext() / newPositionError().
- QML: drop the "schema" fields from the request/envelope builders and relax
  the validity gates to check status / canSubmit instead (the sole check in
  NewPositionFlow now guards on a missing `status`); strip the now-dead
  schema fields from the liquidity QML test fixtures.
- Also removes the last stale comment references to the deleted *Runtime
  classes.
2026-08-03 23:01:53 +02:00

372 lines
12 KiB
QML

import QtQml
QtObject {
id: root
property var backend: null
property var runtime: null
property bool active: false
readonly property bool walletStateReady: root.backend !== null
&& root.backend.walletStateReady === true
readonly property var newPositionContext: root.walletStateReady
&& root.backend.newPositionContext
? root.backend.newPositionContext
: root.loadingContext()
readonly property var viewState: ({
"quote": root.newPositionQuote,
"contextLoading": root.contextLoading || !root.walletStateReady
|| root.newPositionContext.status === "loading",
"quoteLoading": root.quoteLoading,
"quoteStale": root.quoteStale,
"submitting": root.submitting,
"poolCreationPending": root.selectedPoolCreationPending(),
"transactionId": root.transactionId,
"errorCode": root.flowErrorCode || root.contextErrorCode
|| root.quoteErrorCode
})
property var newPositionQuote: ({})
property var resolvedTokenIds: []
property int contextSerial: 0
property int quoteSerial: 0
property bool contextLoading: false
property bool quoteLoading: false
property bool quoteStale: true
property bool submitting: false
property string transactionId: ""
property string flowErrorCode: ""
property string contextErrorCode: ""
property string quoteErrorCode: ""
property var pendingQuoteRequest: ({ "ok": false, "request": ({}) })
property var pendingPoolProbes: []
property bool poolProbeInFlight: false
signal tokenResolutionFinished(bool finalResponse)
signal tokenResolutionFailed(string code)
signal poolActivated(var quote)
signal quoteRefreshRequested(bool immediate)
signal submitSucceeded
signal submitFailed
objectName: "newPositionFlow"
property Timer quoteDebounce: Timer {
interval: 250
repeat: false
onTriggered: root.requestQuoteNow(root.quoteSerial)
}
property Timer poolPoller: Timer {
interval: 5000
repeat: true
running: root.pendingPoolProbes.length > 0
onTriggered: root.pollPendingPool()
}
onNewPositionContextChanged: root.invalidateQuote()
onWalletStateReadyChanged: {
++root.contextSerial
if (!root.walletStateReady)
root.contextLoading = false
root.invalidateQuote()
}
onActiveChanged: {
if (!root.active)
return
Qt.callLater(function() {
if (root.active && root.walletStateReady)
root.quoteRefreshRequested(true)
})
}
function contextHints(refreshWalletAccounts) {
const request = root.pendingQuoteRequest.request || {}
const recent = []
if (request.tokenAId)
recent.push(request.tokenAId)
if (request.tokenBId && request.tokenBId !== request.tokenAId)
recent.push(request.tokenBId)
return {
"recentTokenIds": recent,
"resolvedTokenIds": root.resolvedTokenIds,
"refreshWalletAccounts": refreshWalletAccounts === true
}
}
function refreshContext(refreshWalletAccounts, completed) {
const serial = ++root.contextSerial
root.contextLoading = true
if (!root.walletStateReady || root.runtime === null) {
root.contextLoading = false
return
}
root.runtime.watch(root.backend.refreshNewPositionContext(
root.contextHints(refreshWalletAccounts)),
function() {
root.finishContextRefresh(serial, completed)
},
function(error) {
root.failContextRefresh(serial)
})
}
function finishContextRefresh(serial, completed) {
if (serial !== root.contextSerial)
return
root.contextLoading = false
root.contextErrorCode = ""
Qt.callLater(function() {
if (serial !== root.contextSerial)
return
root.tokenResolutionFinished(true)
if (completed)
completed()
})
}
function failContextRefresh(serial) {
if (serial !== root.contextSerial)
return
root.contextLoading = false
root.contextErrorCode = "backend_error"
root.tokenResolutionFailed("backend_error")
}
function resolveToken(tokenId) {
const value = String(tokenId || "").trim()
if (value.length === 0)
return
if (root.resolvedTokenIds.indexOf(value) < 0) {
const next = root.resolvedTokenIds.slice(0)
next.push(value)
root.resolvedTokenIds = next
}
root.refreshContext(false)
}
function scheduleQuote(immediate, quoteRequest) {
++root.quoteSerial
root.pendingQuoteRequest = quoteRequest
root.quoteStale = true
root.quoteLoading = root.walletStateReady && root.active
root.quoteDebounce.stop()
if (!root.walletStateReady || !root.active)
return
if (immediate)
root.requestQuoteNow(root.quoteSerial)
else
root.quoteDebounce.restart()
}
function requestQuoteNow(serial) {
if (serial !== root.quoteSerial)
return
const built = root.pendingQuoteRequest
if (!built.ok) {
root.quoteLoading = false
return
}
if (!root.walletStateReady || !root.active || root.runtime === null) {
root.quoteLoading = false
return
}
root.runtime.watch(root.backend.quoteNewPosition(built.request),
function(quote) {
if (serial !== root.quoteSerial)
return
root.quoteLoading = false
root.quoteStale = false
root.quoteErrorCode = ""
if (!quote || !quote.status)
root.newPositionQuote = root.quoteError("backend_error")
else
root.newPositionQuote = quote
},
function(error) {
if (serial !== root.quoteSerial)
return
root.quoteLoading = false
root.quoteStale = true
root.quoteErrorCode = "backend_error"
})
}
function confirm(snapshot) {
if (root.submitting)
return
root.submitting = true
root.flowErrorCode = ""
if (!root.backend || root.runtime === null) {
root.finishSubmitFailure(root.quoteError("wallet_unavailable"))
return
}
root.runtime.watch(root.backend.submitNewPosition(snapshot.request, snapshot.quoteHash),
function(result) {
if (result && result.status === "submitted"
&& /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(
String(result.transactionId || ""))) {
if (snapshot.request.initialPriceRealRaw !== undefined)
root.watchPoolCreation(snapshot.poolProbeRequest, result.deadlineMs)
root.submitting = false
root.transactionId = result.transactionId
root.flowErrorCode = ""
root.contextErrorCode = ""
root.quoteErrorCode = ""
root.invalidateQuote()
root.submitSucceeded()
return
}
root.finishSubmitFailure(result || root.quoteError("wallet_submission_failed"))
},
function(error) {
root.finishSubmitFailure(root.quoteError("wallet_submission_failed"))
})
}
function finishSubmitFailure(result) {
root.submitting = false
const hasFreshQuote = result && result.quote
&& result.quote.status
if (hasFreshQuote) {
root.newPositionQuote = result.quote
root.quoteLoading = false
root.quoteStale = false
}
const code = result && result.code ? result.code : "wallet_submission_failed"
root.flowErrorCode = code
root.submitFailed()
if (hasFreshQuote)
return
root.scheduleQuote(true, root.pendingQuoteRequest)
}
function watchPoolCreation(request, deadlineMs) {
const key = root.pairKey(request)
let deadline = Number(deadlineMs)
if (!isFinite(deadline) || deadline <= 0)
deadline = 0
const pending = root.pendingPoolProbes.filter(function(item) {
return item.key !== key
})
pending.push({
"key": key,
"request": request,
"deadlineMs": deadline
})
root.pendingPoolProbes = pending
Qt.callLater(root.pollPendingPool)
}
function pollPendingPool() {
if (root.poolProbeInFlight || root.pendingPoolProbes.length === 0
|| !root.walletStateReady || root.runtime === null) {
return
}
const pending = root.pendingPoolProbes[0]
root.poolProbeInFlight = true
root.runtime.watch(root.backend.quoteNewPosition(pending.request),
function(quote) {
root.finishPoolProbe(pending, quote)
},
function(error) {
root.finishPoolProbe(pending, null)
})
}
function finishPoolProbe(pending, quote) {
root.poolProbeInFlight = false
if (quote && quote.poolStatus === "active_pool") {
root.removePendingPool(pending.key)
if (root.matchesSelectedPair(pending.request)) {
root.poolActivated(quote)
root.invalidateQuote()
root.refreshContext(true)
}
return
}
if (pending.deadlineMs > 0 && Date.now() >= pending.deadlineMs) {
root.removePendingPool(pending.key)
return
}
root.rotatePendingPool()
}
function pairKey(request) {
return String(request.tokenAId || "") + ":" + String(request.tokenBId || "")
}
function matchesSelectedPair(request) {
return root.pairKey(root.pendingQuoteRequest.request || {}) === root.pairKey(request)
}
function selectedPoolCreationPending() {
const request = root.pendingQuoteRequest.request || {}
if (!request.tokenAId || !request.tokenBId)
return false
const selected = root.pairKey(request)
return root.pendingPoolProbes.some(function(item) {
return item.key === selected
})
}
function removePendingPool(key) {
root.pendingPoolProbes = root.pendingPoolProbes.filter(function(item) {
return item.key !== key
})
}
function rotatePendingPool() {
if (root.pendingPoolProbes.length < 2)
return
const pending = root.pendingPoolProbes.slice(1)
pending.push(root.pendingPoolProbes[0])
root.pendingPoolProbes = pending
}
function draftChanged() {
root.invalidateQuote()
root.transactionId = ""
root.flowErrorCode = ""
root.contextErrorCode = ""
root.quoteErrorCode = ""
}
function invalidateQuote() {
++root.quoteSerial
root.quoteDebounce.stop()
root.quoteLoading = false
root.quoteStale = true
}
function loadingContext() {
return {
"status": "loading",
"tokens": [],
"feeTiers": []
}
}
function quoteError(code) {
return {
"status": "error",
"canSubmit": false,
"code": code,
"poolStatus": "unavailable_pool",
"errors": [{
"code": code,
"blockingFields": [],
"details": ({})
}],
"warnings": [],
"accountPreview": []
}
}
}