Files
lez-programs/apps/amm/qml/components/liquidity/NewPositionForm.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

1535 lines
60 KiB
QML

pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Logos.Controls
import Logos.Icons
import Logos.Wallet
import "AmountMath.js" as AmountMath
AmmActionCard {
id: root
theme: fallbackTheme
AmmTheme {
id: fallbackTheme
}
property var newPositionContext: ({})
property var flowState: ({})
property string selectedTokenAId: ""
property string selectedTokenBId: ""
property int selectedFeeBps: 30
property int slippageBps: 50
property string amountA: ""
property string amountB: ""
property string priceAmountA: "1"
property string priceAmountB: "1"
property string minimumAmountARaw: ""
property string minimumAmountBRaw: ""
property var localErrors: []
property string resolvingTokenId: ""
property string resolvingTokenSide: ""
property string tokenResolutionError: ""
property string tokenResolutionErrorSide: ""
property string tokenResolutionMessage: ""
property string confirmedPoolStatus: ""
property var activePoolQuote: ({})
property string headingText: qsTr("New position")
property string headingDetail: ""
property bool showRefreshAction: true
readonly property var quotePayload: root.flowState.quote || ({})
readonly property bool contextLoading: root.flowState.contextLoading === true
readonly property bool quoteLoading: root.flowState.quoteLoading === true
readonly property bool submitting: root.flowState.submitting === true
readonly property bool quoteStale: root.flowState.quoteStale === true
readonly property bool poolCreationPending: root.flowState.poolCreationPending === true
readonly property string submitError: root.flowState.errorCode
? root.issueText(root.flowState.errorCode) : ""
readonly property string transactionId: String(root.flowState.transactionId || "")
readonly property var emptyToken: ({
"definitionId": "",
"name": "",
"totalSupplyRaw": "0",
"balanceRaw": "0",
"selectable": false
})
readonly property var tokens: root.newPositionContext && root.newPositionContext.tokens
? root.newPositionContext.tokens : []
readonly property var feeTiers: root.newPositionContext && root.newPositionContext.feeTiers
? root.newPositionContext.feeTiers : []
readonly property var tokenA: root.tokenById(root.selectedTokenAId)
readonly property var tokenB: root.tokenById(root.selectedTokenBId)
readonly property int decimalsA: 0
readonly property int decimalsB: 0
readonly property bool displayIsCanonical: root.selectedTokenAId.length > 0
&& AmountMath.compareBase58Ids(root.selectedTokenAId,
root.selectedTokenBId) > 0
readonly property int canonicalDecimalsA: root.displayIsCanonical ? root.decimalsA : root.decimalsB
readonly property int canonicalDecimalsB: root.displayIsCanonical ? root.decimalsB : root.decimalsA
readonly property string initialPrice: AmountMath.ratioValue(root.priceAmountA,
root.priceAmountB,
12)
readonly property string inverseInitialPrice: AmountMath.ratioValue(root.priceAmountB,
root.priceAmountA,
12)
readonly property string poolStatus: root.effectivePoolStatus()
readonly property bool activePool: root.poolStatus === "active_pool"
readonly property bool missingPool: root.poolStatus === "missing_pool"
readonly property int poolFeeBps: root.knownPoolFeeBps()
readonly property bool compact: root.width < 420
readonly property bool hasPair: root.selectedTokenAId.length > 0
&& root.selectedTokenBId.length > 0
&& root.selectedTokenAId !== root.selectedTokenBId
readonly property bool resolvingToken: root.resolvingTokenId.length > 0
readonly property bool canConfirm: root.quotePayload.status === "ok"
&& root.quotePayload.canSubmit === true
&& root.quoteMatchesPair()
&& String(root.quotePayload.quoteHash || "").length > 0
&& !root.contextLoading
&& !root.quoteLoading
&& !root.quoteStale
&& !root.submitting
&& !root.poolCreationPending
signal quoteRequested(bool immediate, var quoteRequest)
signal confirmationRequested(var snapshot)
signal tokenResolveRequested(string tokenId)
signal draftChanged
signal refreshRequested
readonly property int contentPadding: width >= 600 ? 24 : 16
implicitHeight: content.implicitHeight + root.contentPadding * 2
implicitWidth: 480
Component.onCompleted: Qt.callLater(root.reconcileSelection)
onNewPositionContextChanged: Qt.callLater(root.applyContextChange)
function applyContextChange() {
if (root.resolvingToken)
root.finishTokenResolution()
else
root.reconcileSelection()
}
onQuotePayloadChanged: {
if (root.quoteStale)
return
root.rememberPoolStatus()
root.rememberActivePoolQuote()
Qt.callLater(root.applyQuoteSideEffects)
}
Component {
id: priceAmountAAdjustment
PriceRatioAdjustment {
amount: root.priceAmountA
enabled: !root.submitting
fieldName: "priceAmountAField"
invalid: root.fieldHasError("initialPrice")
onEdited: function(value) { root.editPrice("A", value) }
}
}
Component {
id: priceAmountBAdjustment
PriceRatioAdjustment {
amount: root.priceAmountB
enabled: !root.submitting
fieldName: "priceAmountBField"
invalid: root.fieldHasError("initialPrice")
onEdited: function(value) { root.editPrice("B", value) }
}
}
ColumnLayout {
id: content
anchors.fill: parent
anchors.margins: root.contentPadding
spacing: 16
RowLayout {
Layout.fillWidth: true
spacing: 12
ColumnLayout {
Layout.fillWidth: true
spacing: 3
Text {
text: root.headingText
color: root.theme.colors.textPrimary
font.pixelSize: 18
font.weight: Font.DemiBold
font.letterSpacing: 0
}
Text {
text: root.headingDetail.length > 0
? root.headingDetail : root.contextStatusText()
color: root.theme.colors.textSecondary
font.pixelSize: 12
elide: Text.ElideRight
Layout.fillWidth: true
}
}
BusyIndicator {
running: root.contextLoading || root.quoteLoading
visible: running
implicitWidth: 24
implicitHeight: 24
}
LogosIconButton {
iconSource: LogosIcons.refresh
iconColor: root.theme.colors.textSecondary
iconSize: 18
Layout.preferredWidth: 36
Layout.preferredHeight: 36
enabled: !root.contextLoading && !root.submitting
visible: root.showRefreshAction
Accessible.name: qsTr("Refresh position data")
ToolTip.visible: hovered
ToolTip.text: Accessible.name
onClicked: root.refreshRequested()
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: networkMessage.implicitHeight + 20
radius: 6
color: root.theme.colors.panelBg
border.color: root.theme.colors.error
visible: root.contextBlocksForm()
Text {
id: networkMessage
anchors.fill: parent
anchors.margins: 10
text: root.contextErrorText()
color: root.theme.colors.textPrimary
font.pixelSize: 12
wrapMode: Text.Wrap
verticalAlignment: Text.AlignVCenter
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
TokenAmountInput {
id: tokenAInput
objectName: "tokenAAmountInput"
Layout.fillWidth: true
theme: root.theme
text: root.amountA
label: qsTr("Token A amount")
balance: root.contextLoading ? "" : root.balanceText(root.tokenA, root.decimalsA)
helperText: root.missingPool && !root.compact
? root.minimumAmountText("A") : ""
errorText: root.formErrorText()
invalid: root.fieldHasError("amountA")
readOnly: root.submitting || (!root.activePool && !root.missingPool)
showMaxButton: root.activePool
tokenData: root.tokenA.definitionId ? root.tokenA : null
tokens: root.tokens
selectedTokenId: root.selectedTokenAId
tokenInvalid: root.tokenHasError("A")
tokenSelectionEnabled: !root.contextLoading && !root.submitting
adjustment: root.missingPool ? priceAmountAAdjustment : null
adjustmentWidth: root.missingPool ? (root.compact ? 100 : 142) : 0
adjustmentHeight: root.missingPool ? 24 : 0
disabledReasonForCode: function(code) { return root.issueText(code) }
detailForToken: function(token) { return root.tokenBalanceDetail(token) }
onEditingChanged: function(value) {
if (root.activePool)
root.editActiveAmount("A", value)
else
root.editMissingAmount("A", value)
}
onEditingCommitted: function(value) {
if (root.activePool)
root.finishActiveAmount("A", value)
else
root.finishMissingAmount("A", value)
}
onMaxClicked: root.useMaximum()
onTokenSelected: function(tokenId) { root.resolveToken("A", tokenId) }
onTokenEntered: function(value) { root.resolveToken("A", value) }
}
AmmPairSeparator {
Layout.fillWidth: true
theme: root.theme
enabled: root.hasPair && !root.submitting
onClicked: root.swapTokens()
}
TokenAmountInput {
id: tokenBInput
objectName: "tokenBAmountInput"
Layout.fillWidth: true
theme: root.theme
text: root.amountB
label: qsTr("Token B amount")
balance: root.contextLoading ? "" : root.balanceText(root.tokenB, root.decimalsB)
helperText: root.missingPool && !root.compact
? root.minimumAmountText("B") : ""
invalid: root.fieldHasError("amountB")
readOnly: root.submitting || (!root.activePool && !root.missingPool)
showMaxButton: root.activePool
tokenData: root.tokenB.definitionId ? root.tokenB : null
tokens: root.tokens
selectedTokenId: root.selectedTokenBId
tokenInvalid: root.tokenHasError("B")
tokenSelectionEnabled: !root.contextLoading && !root.submitting
adjustment: root.missingPool ? priceAmountBAdjustment : null
adjustmentWidth: root.missingPool ? (root.compact ? 100 : 142) : 0
adjustmentHeight: root.missingPool ? 24 : 0
disabledReasonForCode: function(code) { return root.issueText(code) }
detailForToken: function(token) { return root.tokenBalanceDetail(token) }
onEditingChanged: function(value) {
if (root.activePool)
root.editActiveAmount("B", value)
else
root.editMissingAmount("B", value)
}
onEditingCommitted: function(value) {
if (root.activePool)
root.finishActiveAmount("B", value)
else
root.finishMissingAmount("B", value)
}
onMaxClicked: root.useMaximum()
onTokenSelected: function(tokenId) { root.resolveToken("B", tokenId) }
onTokenEntered: function(value) { root.resolveToken("B", value) }
}
}
Text {
Layout.fillWidth: true
visible: root.resolvingToken || root.tokenResolutionMessage.length > 0
text: root.resolvingToken
? qsTr("Resolving TokenDefinition...")
: root.tokenResolutionMessage
color: root.theme.colors.textSecondary
font.pixelSize: 11
wrapMode: Text.Wrap
}
Text {
Layout.fillWidth: true
visible: text.length > 0
text: root.activePool ? root.activePriceText()
: root.missingPool ? root.depositMultiplierText() : ""
color: root.theme.colors.textSecondary
font.pixelSize: 12
horizontalAlignment: Text.AlignRight
wrapMode: Text.Wrap
}
Rectangle {
Layout.fillWidth: true
implicitHeight: 1
color: root.theme.colors.divider
}
ColumnLayout {
Layout.fillWidth: true
spacing: 8
visible: !root.contextLoading
Text {
text: qsTr("Fee tier")
color: root.theme.colors.textPrimary
font.pixelSize: 13
font.weight: Font.Medium
}
GridLayout {
Layout.fillWidth: true
columns: root.compact ? 2 : 4
columnSpacing: 8
rowSpacing: 8
Repeater {
model: root.feeTiers
Item {
id: feeTierOption
required property var modelData
readonly property string disabledReason: root.feeDisabledReason(modelData)
readonly property bool invalid: root.fieldHasError("feeBps")
&& feeTierButton.checked
Layout.fillWidth: true
implicitHeight: 40
Button {
id: feeTierButton
anchors.fill: parent
text: parent.modelData.label || root.feeLabel(parent.modelData.feeBps)
checkable: true
checked: root.selectedFeeBps === parent.modelData.feeBps
enabled: parent.disabledReason.length === 0 && !root.submitting
onClicked: root.selectFee(parent.modelData.feeBps)
contentItem: Text {
text: feeTierButton.text
color: feeTierButton.enabled
? root.theme.colors.textPrimary
: root.theme.colors.textPlaceholder
font.pixelSize: 12
font.weight: Font.Medium
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
radius: 6
color: feeTierButton.checked
? root.theme.colors.selection
: root.theme.colors.inputBg
border.color: feeTierOption.invalid
? root.theme.colors.error
: feeTierButton.checked
? root.theme.colors.ctaBg
: root.theme.colors.borderStrong
border.width: 1
}
}
MouseArea {
id: disabledFeeHover
anchors.fill: parent
enabled: parent.disabledReason.length > 0
hoverEnabled: true
acceptedButtons: Qt.NoButton
}
ToolTip.visible: disabledFeeHover.containsMouse
ToolTip.text: disabledReason
}
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: 10
visible: root.activePool
Text {
text: qsTr("Slippage")
color: root.theme.colors.textSecondary
font.pixelSize: 12
Layout.fillWidth: true
}
Item {
Layout.preferredWidth: slippageControl.implicitWidth
Layout.preferredHeight: slippageControl.implicitHeight
LogosSpinBox {
id: slippageControl
anchors.fill: parent
from: 0
to: 5000
stepSize: 10
editable: true
value: root.slippageBps
enabled: !root.submitting
textFromValue: function(value) { return root.formatBps(value) }
valueFromText: function(text) {
var parsed = AmountMath.parseHuman(String(text).replace("%", "").trim(), 2)
return parsed.ok ? AmountMath.toU32(parsed.raw) : root.slippageBps
}
onValueModified: {
root.slippageBps = value
root.noteDraftChanged()
root.requestQuote(true)
}
}
Rectangle {
anchors.fill: parent
visible: root.fieldHasError("slippageBps")
color: "transparent"
radius: 6
border.color: root.theme.colors.error
border.width: 1
}
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 9
visible: root.quotePayload.status === "ok"
&& root.quoteMatchesPair()
&& !root.quoteStale
Rectangle {
Layout.fillWidth: true
implicitHeight: 1
color: root.theme.colors.divider
}
LabelValueRow {
label: root.activePool ? qsTr("Expected spend") : qsTr("Opening deposit")
value: root.depositSummary()
}
LabelValueRow {
visible: root.missingPool
label: qsTr("Initial price")
value: root.initialPriceText(false)
}
LabelValueRow {
visible: root.missingPool
label: qsTr("Inverse price")
value: root.initialPriceText(true)
}
LabelValueRow {
visible: root.missingPool
label: qsTr("Deposit multiplier")
value: root.depositMultiplierValue()
}
LabelValueRow {
visible: root.missingPool
label: qsTr("Deposit scale")
value: root.depositBasisPointsText()
}
LabelValueRow {
label: qsTr("Expected LP")
value: root.rawLpText(root.quotePayload.expectedLpRaw)
}
LabelValueRow {
label: root.activePool ? qsTr("Minimum LP") : qsTr("Locked LP")
value: root.rawLpText(root.activePool
? root.quotePayload.minimumLpRaw
: root.quotePayload.lockedLpRaw)
}
LabelValueRow {
label: qsTr("Pool")
value: String(root.quotePayload.poolId || "")
valueWrapAnywhere: true
}
LogosButton {
id: accountPlanButton
text: qsTr("Account plan (%1)").arg(root.accountPreview().length)
enabled: root.accountPreview().length > 0
property bool checked: false
implicitWidth: 150
implicitHeight: 36
radius: 6
Layout.alignment: Qt.AlignLeft
onClicked: checked = !checked
}
ColumnLayout {
Layout.fillWidth: true
spacing: 5
visible: accountPlanButton.checked
Repeater {
model: root.accountPreview()
LabelValueRow {
required property var modelData
label: qsTr("%1. %2 · %3").arg(modelData.order + 1).arg(modelData.role).arg(modelData.action)
value: modelData.accountId ? modelData.accountId : qsTr("Assigned by wallet")
valueWrapAnywhere: true
}
}
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: warningTextItem.implicitHeight + 20
radius: 6
color: root.theme.colors.panelBg
border.color: root.theme.colors.ctaBg
visible: root.warningText().length > 0
Text {
id: warningTextItem
anchors.fill: parent
anchors.margins: 10
text: root.warningText()
color: root.theme.colors.textPrimary
font.pixelSize: 12
wrapMode: Text.Wrap
verticalAlignment: Text.AlignVCenter
}
}
SubmittedTransaction {
Layout.fillWidth: true
title: qsTr("Position submitted")
transactionId: root.transactionId
visible: root.transactionId.length > 0
}
AmmPrimaryButton {
Layout.fillWidth: true
Layout.minimumHeight: 56
theme: root.theme
text: root.submitting
? qsTr("Submitting…")
: root.contextLoading ? qsTr("Loading…")
: root.poolCreationPending ? qsTr("Waiting for pool")
: root.missingPool ? qsTr("Create pool") : qsTr("Add liquidity")
enabled: root.canConfirm
onClicked: root.confirmationRequested(root.submissionSnapshot())
}
}
component LabelValueRow: RowLayout {
required property string label
required property string value
property bool valueWrapAnywhere: false
Layout.fillWidth: true
spacing: 12
Text {
text: parent.label
color: root.theme.colors.textSecondary
font.pixelSize: 12
Layout.fillWidth: true
wrapMode: Text.Wrap
}
Text {
text: parent.value
color: root.theme.colors.textPrimary
font.pixelSize: 12
font.weight: Font.Medium
horizontalAlignment: Text.AlignRight
wrapMode: parent.valueWrapAnywhere ? Text.WrapAnywhere : Text.Wrap
Layout.maximumWidth: root.compact ? 190 : 280
}
}
component PriceRatioAdjustment: RowLayout {
id: ratioAdjustment
required property string amount
required property string fieldName
required property bool invalid
signal edited(string value)
implicitWidth: 142
implicitHeight: 24
spacing: 6
Text {
text: qsTr("Price")
color: root.theme.colors.textSecondary
font.pixelSize: 11
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 24
radius: 6
color: root.theme.colors.panelBg
border.color: ratioAdjustment.invalid
? root.theme.colors.error
: ratioInput.activeFocus
? root.theme.colors.ctaBg
: root.theme.colors.borderStrong
border.width: 1
TextInput {
id: ratioInput
objectName: ratioAdjustment.fieldName
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 8
text: ratioAdjustment.amount
enabled: ratioAdjustment.enabled
color: enabled ? root.theme.colors.textPrimary
: root.theme.colors.textSecondary
font.pixelSize: 12
selectionColor: root.theme.colors.selection
selectedTextColor: root.theme.colors.textPrimary
horizontalAlignment: Text.AlignRight
verticalAlignment: Text.AlignVCenter
inputMethodHints: Qt.ImhFormattedNumbersOnly
maximumLength: 80
validator: RegularExpressionValidator {
regularExpression: /[0-9]*([.][0-9]*)?/
}
Accessible.name: qsTr("Initial price ratio amount")
onTextEdited: ratioAdjustment.edited(text)
}
}
}
function tokenById(tokenId) {
for (var i = 0; i < root.tokens.length; ++i) {
if (root.tokens[i].definitionId === tokenId)
return root.tokens[i]
}
return root.emptyToken
}
function selectableTokenIds() {
var result = []
for (var i = 0; i < root.tokens.length; ++i) {
if (root.tokens[i].selectable === true)
result.push(root.tokens[i].definitionId)
}
return result
}
function isBase58TokenId(tokenId) {
return /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(String(tokenId || ""))
}
function resolveToken(side, value) {
var tokenId = String(value || "").trim()
root.tokenResolutionError = ""
root.tokenResolutionErrorSide = ""
root.tokenResolutionMessage = ""
if (!root.isBase58TokenId(tokenId)) {
root.tokenResolutionError = root.issueText("invalid_token_id")
root.tokenResolutionErrorSide = side
return
}
var current = root.tokenById(tokenId)
if (current.definitionId === tokenId) {
if (current.selectable === true)
root.selectToken(side, tokenId)
else {
root.tokenResolutionError = root.issueText(current.code || current.status)
root.tokenResolutionErrorSide = side
}
return
}
root.resolvingTokenId = tokenId
root.resolvingTokenSide = side
root.tokenResolveRequested(tokenId)
}
function finishTokenResolution(finalResponse) {
if (!root.resolvingToken)
return
var token = root.tokenById(root.resolvingTokenId)
if (!token || !token.definitionId) {
if (finalResponse === true) {
var currentStatus = String(root.newPositionContext.status || "")
var code = currentStatus !== "ready" && currentStatus !== "no_wallet"
&& currentStatus !== "loading"
? root.newPositionContext.code || currentStatus
: "token_definition_unreadable"
root.failTokenResolution(code)
}
return
}
var status = String(root.newPositionContext.status || "")
if (status === "loading")
return
if (status !== "ready" && status !== "no_wallet") {
root.failTokenResolution(root.newPositionContext.code || status)
return
}
var side = root.resolvingTokenSide
root.resolvingTokenId = ""
root.resolvingTokenSide = ""
if (token.selectable !== true) {
root.tokenResolutionError = root.issueText(token.code || token.status)
root.tokenResolutionErrorSide = side
return
}
root.tokenResolutionMessage = qsTr("%1 - raw supply %2")
.arg(token.name || root.shortId(token.definitionId))
.arg(AmountMath.formatRaw(token.totalSupplyRaw || "0", 0))
root.selectToken(side, token.definitionId)
}
function failTokenResolution(code) {
if (!root.resolvingToken)
return
var side = root.resolvingTokenSide
root.resolvingTokenId = ""
root.resolvingTokenSide = ""
root.tokenResolutionError = root.issueText(code)
root.tokenResolutionErrorSide = side
}
function reconcileSelection() {
var status = String(root.newPositionContext.status || "")
if (status !== "ready" && status !== "no_wallet")
return
var previousA = root.selectedTokenAId
var previousB = root.selectedTokenBId
var selectable = root.selectableTokenIds()
if (root.selectedTokenAId.length > 0
&& selectable.indexOf(root.selectedTokenAId) < 0) {
root.selectedTokenAId = ""
}
if (root.selectedTokenBId.length > 0
&& selectable.indexOf(root.selectedTokenBId) < 0) {
root.selectedTokenBId = ""
}
if (root.selectedTokenBId === root.selectedTokenAId)
root.selectedTokenBId = ""
if (root.selectedTokenAId !== previousA || root.selectedTokenBId !== previousB)
root.resetPairDraft()
else if (root.hasPair)
root.requestQuote(true)
}
function selectToken(side, tokenId) {
if (tokenId.length === 0)
return
root.tokenResolutionError = ""
root.tokenResolutionErrorSide = ""
if (side === "A") {
if (tokenId === root.selectedTokenBId)
root.selectedTokenBId = root.selectedTokenAId
root.selectedTokenAId = tokenId
} else {
if (tokenId === root.selectedTokenAId)
root.selectedTokenAId = root.selectedTokenBId
root.selectedTokenBId = tokenId
}
root.resetPairDraft()
}
function swapTokens() {
var tokenId = root.selectedTokenAId
root.selectedTokenAId = root.selectedTokenBId
root.selectedTokenBId = tokenId
var amount = root.amountA
root.amountA = root.amountB
root.amountB = amount
var minimum = root.minimumAmountARaw
root.minimumAmountARaw = root.minimumAmountBRaw
root.minimumAmountBRaw = minimum
var priceAmount = root.priceAmountA
root.priceAmountA = root.priceAmountB
root.priceAmountB = priceAmount
root.noteDraftChanged()
root.requestQuote(true)
}
function resetPairDraft() {
root.confirmedPoolStatus = ""
root.activePoolQuote = ({})
root.amountA = ""
root.amountB = ""
root.priceAmountA = "1"
root.priceAmountB = "1"
root.minimumAmountARaw = ""
root.minimumAmountBRaw = ""
root.localErrors = []
root.noteDraftChanged()
root.requestQuote(true)
}
function acceptPoolActivation(quote) {
if (!quote || quote.status !== "ok"
|| quote.poolStatus !== "active_pool"
|| !root.quoteMatchesSelectedPair(quote)) {
return false
}
root.confirmedPoolStatus = "active_pool"
root.activePoolQuote = quote
root.amountA = ""
root.amountB = ""
root.minimumAmountARaw = ""
root.minimumAmountBRaw = ""
root.localErrors = []
return true
}
function noteDraftChanged() {
root.draftChanged()
}
function effectivePoolStatus() {
if (root.quoteStale || !root.quoteMatchesPair())
return root.confirmedPoolStatus
var status = String(root.quotePayload.poolStatus || "")
if (status === "active_pool" || status === "missing_pool")
return status
if (root.quotePayload.code === "fee_tier_mismatch")
return "active_pool"
return root.confirmedPoolStatus
}
function rememberPoolStatus() {
if (!root.quoteMatchesPair())
return
var status = String(root.quotePayload.poolStatus || "")
if (status === "active_pool" || status === "missing_pool")
root.confirmedPoolStatus = status
else if (root.quotePayload.code === "fee_tier_mismatch")
root.confirmedPoolStatus = "active_pool"
}
function rememberActivePoolQuote() {
if (root.quotePayload.status !== "ok"
|| root.quotePayload.poolStatus !== "active_pool"
|| !root.quoteMatchesPair()) {
return
}
var reserveA = String(root.quotePayload.reserveARaw || "")
var reserveB = String(root.quotePayload.reserveBRaw || "")
if (AmountMath.isUnsigned(reserveA) && reserveA !== "0"
&& AmountMath.isUnsigned(reserveB) && reserveB !== "0") {
root.activePoolQuote = root.quotePayload
}
}
function knownPoolFeeBps() {
var direct = root.feeBpsFromQuote(root.quotePayload)
if (direct > 0)
return direct
if (root.quoteMatchesSelectedPair(root.activePoolQuote))
return Number(root.activePoolQuote.poolFeeBps || 0)
return 0
}
function feeBpsFromQuote(quote) {
if (!root.quoteMatchesSelectedPair(quote))
return 0
var direct = Number(quote.poolFeeBps || 0)
if (direct > 0)
return direct
var errors = quote.errors || []
for (var i = 0; i < errors.length; ++i) {
var value = Number(errors[i].details ? errors[i].details.poolFeeBps : 0)
if (value > 0)
return value
}
return 0
}
function quoteMatchesPair() {
return root.quoteMatchesSelectedPair(root.quotePayload)
}
function quoteMatchesSelectedPair(quote) {
var tokenAId = String(quote.tokenAId || "")
var tokenBId = String(quote.tokenBId || "")
return root.hasPair
&& ((tokenAId === root.selectedTokenAId && tokenBId === root.selectedTokenBId)
|| (tokenAId === root.selectedTokenBId && tokenBId === root.selectedTokenAId))
}
function selectFee(feeBps) {
root.selectedFeeBps = feeBps
root.noteDraftChanged()
root.requestQuote(true)
}
function feeDisabledReason(tier) {
if (tier.enabled === false)
return tier.disabledReason || qsTr("This fee tier is unavailable.")
if (root.poolFeeBps > 0 && Number(tier.feeBps) !== root.poolFeeBps)
return qsTr("Existing pool uses %1. Fee tier is fixed for this pair.")
.arg(root.feeLabel(root.poolFeeBps))
return ""
}
function feeLabel(feeBps) {
if (feeBps === 1)
return "0.01%"
if (feeBps === 5)
return "0.05%"
if (feeBps === 30)
return "0.30%"
if (feeBps === 100)
return "1.00%"
return root.formatBps(feeBps)
}
function buildQuoteRequest() {
var errors = []
if (!root.hasPair) {
errors.push(root.localIssue("token_pair_required", ["tokenAId", "tokenBId"]))
root.localErrors = errors
return { "ok": false, "errors": errors, "request": ({}) }
}
var request = root.pairRequest()
if (root.activePool) {
var parsedA = AmountMath.parseHuman(root.amountA, root.decimalsA)
var parsedB = AmountMath.parseHuman(root.amountB, root.decimalsB)
if (!parsedA.ok)
errors.push(root.localIssue(parsedA.code, ["amountA"]))
if (!parsedB.ok)
errors.push(root.localIssue(parsedB.code, ["amountB"]))
if (errors.length === 0) {
request.maxAmountARaw = root.displayIsCanonical ? parsedA.raw : parsedB.raw
request.maxAmountBRaw = root.displayIsCanonical ? parsedB.raw : parsedA.raw
request.slippageBps = root.slippageBps
}
} else {
var priceFromAmounts = false
var price = AmountMath.ratioToQ64(root.priceAmountA,
root.priceAmountB,
root.canonicalDecimalsA,
root.canonicalDecimalsB,
root.displayIsCanonical)
if (!price.ok)
errors.push(root.localIssue(price.code, ["initialPrice"]))
if (root.missingPool && root.minimumAmountARaw.length > 0) {
var parsedMissingA = AmountMath.parseHuman(root.amountA, root.decimalsA)
var parsedMissingB = AmountMath.parseHuman(root.amountB, root.decimalsB)
if (!parsedMissingA.ok)
errors.push(root.localIssue(parsedMissingA.code, ["amountA"]))
if (!parsedMissingB.ok)
errors.push(root.localIssue(parsedMissingB.code, ["amountB"]))
if (parsedMissingA.ok && parsedMissingB.ok) {
if (AmountMath.compare(parsedMissingA.raw, root.minimumAmountARaw) < 0)
errors.push(root.localIssue("amount_too_low", ["amountA"]))
if (AmountMath.compare(parsedMissingB.raw, root.minimumAmountBRaw) < 0)
errors.push(root.localIssue("amount_too_low", ["amountB"]))
var pairedB = AmountMath.pairAmount(parsedMissingA.raw,
true,
root.decimalsA,
root.decimalsB,
root.priceAmountA,
root.priceAmountB)
var pairedA = AmountMath.pairAmount(parsedMissingB.raw,
false,
root.decimalsA,
root.decimalsB,
root.priceAmountA,
root.priceAmountB)
var matchesA = pairedA.ok
&& AmountMath.normalize(pairedA.raw)
=== AmountMath.normalize(parsedMissingA.raw)
var matchesB = pairedB.ok
&& AmountMath.normalize(pairedB.raw)
=== AmountMath.normalize(parsedMissingB.raw)
if (!matchesA && !matchesB) {
errors.push(root.localIssue("deposit_ratio_mismatch", ["amountA", "amountB"]))
}
if (errors.length === 0) {
request.amountARaw = root.displayIsCanonical
? parsedMissingA.raw : parsedMissingB.raw
request.amountBRaw = root.displayIsCanonical
? parsedMissingB.raw : parsedMissingA.raw
var actualPrice = AmountMath.ratioToQ64(root.amountA,
root.amountB,
root.canonicalDecimalsA,
root.canonicalDecimalsB,
root.displayIsCanonical)
if (actualPrice.ok) {
request.initialPriceRealRaw = actualPrice.raw
priceFromAmounts = true
} else {
errors.push(root.localIssue(actualPrice.code, ["initialPrice"]))
}
}
}
}
if (price.ok && !priceFromAmounts)
request.initialPriceRealRaw = price.raw
if (!root.missingPool) {
var probeA = root.probeRaw(root.tokenA, root.decimalsA)
var probeB = root.probeRaw(root.tokenB, root.decimalsB)
request.maxAmountARaw = root.displayIsCanonical ? probeA : probeB
request.maxAmountBRaw = root.displayIsCanonical ? probeB : probeA
request.slippageBps = root.slippageBps
}
}
root.localErrors = errors
return { "ok": errors.length === 0, "errors": errors, "request": request }
}
function pairRequest() {
return {
"tokenAId": root.displayIsCanonical
? root.selectedTokenAId : root.selectedTokenBId,
"tokenBId": root.displayIsCanonical
? root.selectedTokenBId : root.selectedTokenAId,
"feeBps": root.selectedFeeBps
}
}
function requestQuote(immediate) {
if (root.activePool && root.amountA.length === 0 && root.amountB.length === 0) {
root.localErrors = []
return
}
root.quoteRequested(immediate, root.buildQuoteRequest())
}
function probeRaw(token, decimals) {
var balance = String(token.balanceRaw || "0")
var simulated = AmountMath.multiply(AmountMath.pow10(decimals), "1000")
if (AmountMath.isUnsigned(balance) && AmountMath.compare(balance, simulated) > 0)
return balance
return simulated
}
function poolProbeRequest(request) {
var probe = {}
for (var field in request)
probe[field] = request[field]
var amountA = root.probeRaw(root.tokenA, root.decimalsA)
var amountB = root.probeRaw(root.tokenB, root.decimalsB)
probe.maxAmountARaw = root.displayIsCanonical ? amountA : amountB
probe.maxAmountBRaw = root.displayIsCanonical ? amountB : amountA
probe.slippageBps = root.slippageBps
return probe
}
function formatBps(value) {
return qsTr("%1%").arg(AmountMath.formatRaw(String(value), 2))
}
function localIssue(code, fields) {
return { "code": code, "blockingFields": fields, "details": ({}) }
}
function canonicalFieldToDisplay(field) {
if (field === "tokenAId")
return root.displayIsCanonical ? "tokenAId" : "tokenBId"
if (field === "tokenBId")
return root.displayIsCanonical ? "tokenBId" : "tokenAId"
if (field === "maxAmountARaw")
return root.displayIsCanonical ? "amountA" : "amountB"
if (field === "maxAmountBRaw")
return root.displayIsCanonical ? "amountB" : "amountA"
if (field === "amountARaw")
return root.displayIsCanonical ? "amountA" : "amountB"
if (field === "amountBRaw")
return root.displayIsCanonical ? "amountB" : "amountA"
if (field === "initialPriceRealRaw")
return "initialPrice"
return field
}
function fieldError(field) {
var collections = [root.localErrors, root.currentQuoteErrors()]
for (var c = 0; c < collections.length; ++c) {
for (var i = 0; i < collections[c].length; ++i) {
var fields = collections[c][i].blockingFields || []
for (var f = 0; f < fields.length; ++f) {
if (root.canonicalFieldToDisplay(fields[f]) === field)
return root.issueText(collections[c][i].code)
}
}
}
return ""
}
function fieldHasError(field) {
return root.fieldError(field).length > 0
}
function tokenHasError(side) {
if (root.tokenResolutionError.length > 0
&& root.tokenResolutionErrorSide === side) {
return true
}
return root.fieldHasError(side === "A" ? "tokenAId" : "tokenBId")
}
function formErrorText() {
if (root.tokenResolutionError.length > 0)
return root.tokenResolutionError
if (root.submitError.length > 0)
return root.submitError
var collections = [root.localErrors, root.currentQuoteErrors()]
for (var c = 0; c < collections.length; ++c) {
for (var i = 0; i < collections[c].length; ++i) {
var code = String(collections[c][i].code || "")
if (code.length > 0)
return root.issueText(code)
}
}
return root.quoteError()
}
function currentQuoteErrors() {
return !root.quoteStale && root.quoteMatchesPair()
? root.quotePayload.errors || [] : []
}
function issueText(code) {
var messages = {
"amount_required": qsTr("Enter a value."),
"amount_must_be_positive": qsTr("Value must be greater than zero."),
"invalid_amount_format": qsTr("Use plain dot-decimal format."),
"invalid_amount_precision": qsTr("Token amounts must use whole raw units."),
"invalid_raw_amount": qsTr("Value is outside the supported range."),
"amount_exceeds_balance": qsTr("Amount exceeds the selected holding balance."),
"amount_too_low": qsTr("Value is too low for this pool."),
"invalid_token_id": qsTr("Enter a valid base58 TokenDefinition ID."),
"deposit_ratio_mismatch": qsTr("Deposit amounts must match the initial price."),
"minimum_lp_zero": qsTr("Slippage leaves no minimum LP output."),
"invalid_slippage": qsTr("Slippage must be between 0% and 50%."),
"fee_tier_mismatch": qsTr("Select the existing pool fee tier."),
"no_wallet": qsTr("Connect a wallet to submit this position."),
"wallet_unavailable": qsTr("Wallet is unavailable."),
"wallet_submission_failed": qsTr("Wallet submission failed. Review and retry manually."),
"signature_rejected": qsTr("Wallet approval was rejected."),
"quote_changed": qsTr("Pool or wallet state changed. Review the refreshed quote."),
"quote_not_submittable": qsTr("Current quote cannot be submitted."),
"submit_in_progress": qsTr("A submission is already in progress."),
"transaction_deadline_expired": qsTr("Wallet approval expired. Retry to create a fresh request."),
"high_slippage": qsTr("High slippage tolerance."),
"token_definition_unreadable": qsTr("A token definition could not be read."),
"token_program_mismatch": qsTr("Token belongs to a different TokenProgram."),
"token_not_fungible": qsTr("Token is not a public fungible token."),
"backend_error": qsTr("Position backend failed. Refresh and retry."),
"network_unknown": qsTr("Network identity could not be verified. Refresh and retry."),
"network_mismatch": qsTr("Connected wallet uses a different network."),
"config_missing": qsTr("Network configuration is missing."),
"account_read_failed": qsTr("Required on-chain state could not be read."),
"pool_unavailable": qsTr("Pool state is unavailable."),
"config_unavailable": qsTr("AMM configuration is unavailable."),
"same_token_pair": qsTr("Select two different tokens."),
"token_pair_required": qsTr("Select two tokens.")
}
return messages[code] || qsTr("Position quote is unavailable (%1).").arg(code || "unknown")
}
function editActiveAmount(side, value) {
if (side === "A")
root.amountA = value
else
root.amountB = value
root.localErrors = []
root.noteDraftChanged()
}
function finishActiveAmount(side, value) {
var decimals = side === "A" ? root.decimalsA : root.decimalsB
if (side === "A")
root.amountA = value
else
root.amountB = value
var reserveA = root.poolReserve("A")
var reserveB = root.poolReserve("B")
var parsed = AmountMath.parseHuman(value, decimals)
if (parsed.ok && reserveA !== "" && reserveB !== ""
&& reserveA !== "0" && reserveB !== "0") {
if (side === "A") {
var rawB = AmountMath.mulDivFloor(parsed.raw, reserveB, reserveA)
root.amountB = AmountMath.formatRaw(rawB, root.decimalsB)
} else {
var rawA = AmountMath.mulDivFloor(parsed.raw, reserveA, reserveB)
root.amountA = AmountMath.formatRaw(rawA, root.decimalsA)
}
}
root.requestQuote(true)
}
function useMaximum() {
var reserveA = root.poolReserve("A")
var reserveB = root.poolReserve("B")
if (!reserveA || !reserveB || reserveA === "0" || reserveB === "0")
return
var balanceA = String(root.tokenA.balanceRaw || "0")
var balanceB = String(root.tokenB.balanceRaw || "0")
var fitA = AmountMath.mulDivFloor(balanceB, reserveA, reserveB)
var rawA = AmountMath.compare(balanceA, fitA) < 0 ? balanceA : fitA
var rawB = AmountMath.mulDivFloor(rawA, reserveB, reserveA)
root.amountA = AmountMath.formatRaw(rawA, root.decimalsA)
root.amountB = AmountMath.formatRaw(rawB, root.decimalsB)
root.noteDraftChanged()
root.requestQuote(true)
}
function editPrice(side, value) {
if (side === "A")
root.priceAmountA = value
else
root.priceAmountB = value
root.minimumAmountARaw = ""
root.minimumAmountBRaw = ""
root.amountA = ""
root.amountB = ""
root.noteDraftChanged()
root.requestQuote(false)
}
function editMissingAmount(side, value) {
if (side === "A")
root.amountA = value
else
root.amountB = value
root.localErrors = []
root.noteDraftChanged()
}
function finishMissingAmount(side, value) {
var decimals = side === "A" ? root.decimalsA : root.decimalsB
if (side === "A")
root.amountA = value
else
root.amountB = value
var parsed = AmountMath.parseHuman(value, decimals)
if (parsed.ok) {
var paired = AmountMath.pairAmount(parsed.raw,
side === "A",
root.decimalsA,
root.decimalsB,
root.priceAmountA,
root.priceAmountB)
if (paired.ok) {
if (side === "A")
root.amountB = AmountMath.formatRaw(paired.raw, root.decimalsB)
else
root.amountA = AmountMath.formatRaw(paired.raw, root.decimalsA)
}
}
root.requestQuote(true)
}
function applyQuoteSideEffects() {
if (root.quoteStale)
return
if (root.poolFeeBps > 0 && root.selectedFeeBps !== root.poolFeeBps) {
root.selectedFeeBps = root.poolFeeBps
root.localErrors = []
root.quoteRequested(true, {
"ok": true,
"errors": [],
"request": root.poolProbeRequest(root.pairRequest())
})
return
}
if (root.quotePayload.status !== "ok")
return
if (root.missingPool) {
var rawA = root.displayRaw("actualAmountARaw", "actualAmountBRaw", "A")
var rawB = root.displayRaw("actualAmountARaw", "actualAmountBRaw", "B")
var minimumA = root.displayRaw("minimumAmountARaw", "minimumAmountBRaw", "A")
var minimumB = root.displayRaw("minimumAmountARaw", "minimumAmountBRaw", "B")
root.minimumAmountARaw = minimumA.length > 0 ? minimumA : rawA
root.minimumAmountBRaw = minimumB.length > 0 ? minimumB : rawB
if (rawA.length > 0 && rawB.length > 0) {
root.amountA = AmountMath.formatRaw(rawA, root.decimalsA)
root.amountB = AmountMath.formatRaw(rawB, root.decimalsB)
}
}
}
function displayRaw(canonicalAField, canonicalBField, side) {
return root.displayQuoteRaw(root.quotePayload, canonicalAField, canonicalBField, side)
}
function displayQuoteRaw(quote, canonicalAField, canonicalBField, side) {
if (!root.quoteMatchesSelectedPair(quote))
return ""
if (side === "A")
return String(quote[root.displayIsCanonical ? canonicalAField : canonicalBField] || "")
return String(quote[root.displayIsCanonical ? canonicalBField : canonicalAField] || "")
}
function poolReserve(side) {
var reserve = root.displayRaw("reserveARaw", "reserveBRaw", side)
if (AmountMath.isUnsigned(reserve) && reserve !== "0")
return reserve
if (!root.quoteMatchesSelectedPair(root.activePoolQuote))
return ""
return root.displayQuoteRaw(root.activePoolQuote, "reserveARaw", "reserveBRaw", side)
}
function quoteAmount(canonicalAField, canonicalBField, side) {
var token = side === "A" ? root.tokenA : root.tokenB
var decimals = side === "A" ? root.decimalsA : root.decimalsB
var raw = root.displayRaw(canonicalAField, canonicalBField, side)
return raw.length > 0
? qsTr("%1 %2").arg(AmountMath.formatRaw(raw, decimals)).arg(root.shortTokenName(token))
: "—"
}
function depositSummary() {
var amountA = root.quoteAmount("actualAmountARaw", "actualAmountBRaw", "A")
var amountB = root.quoteAmount("actualAmountARaw", "actualAmountBRaw", "B")
return amountA + " + " + amountB
}
function initialPriceText(inverse) {
var price = inverse ? root.inverseInitialPrice : root.initialPrice
if (price.length === 0)
return "—"
var from = inverse ? root.tokenB : root.tokenA
var to = inverse ? root.tokenA : root.tokenB
return qsTr("1 %1 = %2 %3")
.arg(root.shortTokenName(from))
.arg(price)
.arg(root.shortTokenName(to))
}
function rawLpText(raw) {
return raw !== undefined && raw !== null && String(raw).length > 0
? qsTr("%1 raw LP").arg(String(raw)) : "—"
}
function activePriceValue() {
var priceRaw = String(root.quotePayload.initialPriceRealRaw || "")
if (priceRaw.length === 0 && root.quoteMatchesSelectedPair(root.activePoolQuote))
priceRaw = String(root.activePoolQuote.initialPriceRealRaw || "")
return AmountMath.priceFromQ64(priceRaw,
root.canonicalDecimalsA,
root.canonicalDecimalsB,
root.displayIsCanonical)
}
function activePriceText() {
var price = root.activePriceValue()
if (price.length === 0)
return ""
return qsTr("1 %1 = %2 %3")
.arg(root.shortTokenName(root.tokenA))
.arg(price)
.arg(root.shortTokenName(root.tokenB))
}
function accountPreview() {
return !root.quoteStale && root.quoteMatchesPair()
? root.quotePayload.accountPreview || [] : []
}
function quoteError() {
if (root.quoteLoading || root.quoteStale)
return ""
if (root.quotePayload.status === "error")
return root.issueText(root.quotePayload.code)
return ""
}
function warningText() {
var warnings = !root.quoteStale && root.quoteMatchesPair()
? root.quotePayload.warnings || [] : []
if (warnings.length === 0)
warnings = root.newPositionContext.warnings || []
return warnings.length > 0 ? root.issueText(warnings[0].code) : ""
}
function submissionSnapshot() {
var built = root.buildQuoteRequest()
return {
"request": built.request,
"poolProbeRequest": root.poolProbeRequest(built.request),
"quoteHash": String(root.quotePayload.quoteHash || ""),
"pairText": qsTr("%1 / %2").arg(root.shortTokenName(root.tokenA)).arg(root.shortTokenName(root.tokenB)),
"feeText": root.feeLabel(root.selectedFeeBps),
"depositAText": root.quoteAmount("actualAmountARaw", "actualAmountBRaw", "A"),
"depositBText": root.quoteAmount("actualAmountARaw", "actualAmountBRaw", "B"),
"expectedLpText": root.rawLpText(root.quotePayload.expectedLpRaw),
"instruction": String(root.quotePayload.instruction || "")
}
}
function shortTokenName(token) {
if (token && token.name && token.name.length > 0)
return token.name
return token && token.definitionId ? root.shortId(token.definitionId) : "—"
}
function balanceText(token, decimals) {
return AmountMath.formatRaw(String(token.balanceRaw || "0"), decimals)
}
function tokenBalanceDetail(token) {
return qsTr("Available %1").arg(root.balanceText(token, 0))
}
function shortId(value) {
var text = String(value || "")
return text.length > 14 ? text.slice(0, 7) + "…" + text.slice(-5) : text
}
function depositMultiplierText() {
var multiplier = root.depositMultiplierValue()
var basisPoints = root.depositBasisPointsText()
return multiplier.length > 0 ? qsTr("%1 · %2").arg(multiplier).arg(basisPoints) : ""
}
function depositMultiplierValue() {
var scale = root.depositScaleValue()
return scale.length > 0
? qsTr("%1x minimum").arg(AmountMath.formatRaw(scale, 4)) : ""
}
function depositBasisPointsText() {
var scale = root.depositScaleValue()
return scale.length > 0 ? qsTr("%1 basis points").arg(scale) : ""
}
function depositScaleValue() {
var parsed = AmountMath.parseHuman(root.amountA, root.decimalsA)
if (!parsed.ok || !AmountMath.isUnsigned(root.minimumAmountARaw)
|| root.minimumAmountARaw === "0"
|| AmountMath.compare(parsed.raw, root.minimumAmountARaw) < 0) {
return ""
}
return AmountMath.divide(AmountMath.multiply(parsed.raw, "10000"),
root.minimumAmountARaw).quotient
}
function minimumAmountText(side) {
var raw = side === "A" ? root.minimumAmountARaw : root.minimumAmountBRaw
var decimals = side === "A" ? root.decimalsA : root.decimalsB
return raw.length > 0
? qsTr("Min %1").arg(AmountMath.formatRaw(raw, decimals)) : ""
}
function contextStatusText() {
var network = String(root.newPositionContext.networkId || "")
if (root.newPositionContext.status === "no_wallet")
return qsTr("%1 · simulation only").arg(network || qsTr("Wallet disconnected"))
if (root.newPositionContext.status === "ready")
return qsTr("%1 · wallet ready").arg(network)
return network.length > 0 ? network : qsTr("Loading network")
}
function contextBlocksForm() {
var status = String(root.newPositionContext.status || "")
return status !== "" && status !== "ready" && status !== "no_wallet" && status !== "loading"
}
function contextErrorText() {
return root.issueText(root.newPositionContext.code || root.newPositionContext.status)
}
}