Ricardo Guilherme Schmidt 7c98ca7b42
feat(amm-ui): reshape liquidity position layout
Add responsive step navigation and a wider deposit workspace while preserving the shared create-pool and add-liquidity flow.
2026-07-16 15:46:50 +02:00

1548 lines
62 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: AmountMath.implyDecimals(root.tokenA.totalSupplyRaw || "0")
readonly property int decimalsB: AmountMath.implyDecimals(root.tokenB.totalSupplyRaw || "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.schema === "new-position.v1"
&& 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.ensurePair)
onNewPositionContextChanged: Qt.callLater(root.applyContextChange)
function applyContextChange() {
if (root.resolvingToken)
root.finishTokenResolution()
else
root.ensurePair()
}
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
}
var decimals = AmountMath.implyDecimals(token.totalSupplyRaw || "0")
root.tokenResolutionMessage = qsTr("%1 - supply %2 (%3 implied decimals)")
.arg(token.name || root.shortId(token.definitionId))
.arg(AmountMath.formatRaw(token.totalSupplyRaw || "0", decimals))
.arg(decimals)
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 ensurePair() {
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 (selectable.length === 0) {
root.selectedTokenAId = ""
root.selectedTokenBId = ""
if (previousA.length > 0 || previousB.length > 0)
root.resetPairDraft()
return
}
if (selectable.indexOf(root.selectedTokenAId) < 0)
root.selectedTokenAId = selectable[0]
if (selectable.indexOf(root.selectedTokenBId) < 0
|| root.selectedTokenBId === root.selectedTokenAId) {
root.selectedTokenBId = selectable.length > 1 ? selectable[1] : ""
}
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.schema !== "new-position.v1"
|| 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 canonicalAId = root.displayIsCanonical ? root.selectedTokenAId : root.selectedTokenBId
var canonicalBId = root.displayIsCanonical ? root.selectedTokenBId : root.selectedTokenAId
var request = {
"schema": "new-position.v1",
"tokenAId": canonicalAId,
"tokenBId": canonicalBId,
"feeBps": root.selectedFeeBps
}
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 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("Too many decimal places for this token."),
"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
value = AmountMath.trimHumanPrecision(value, decimals)
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
value = AmountMath.trimHumanPrecision(value, decimals)
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
if (root.amountA.length === 0 && root.amountB.length === 0) {
root.amountA = AmountMath.formatRaw(
root.probeRaw(root.tokenA, root.decimalsA), root.decimalsA)
root.amountB = AmountMath.formatRaw(
root.probeRaw(root.tokenB, root.decimalsB), root.decimalsB)
}
root.requestQuote(true)
return
}
if (root.quotePayload.status !== "ok")
return
if (root.activePool && root.amountA.length === 0 && root.amountB.length === 0) {
root.amountA = AmountMath.formatRaw(root.displayRaw("maxAmountARaw", "maxAmountBRaw", "A"), root.decimalsA)
root.amountB = AmountMath.formatRaw(root.displayRaw("maxAmountARaw", "maxAmountBRaw", "B"), root.decimalsB)
}
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) {
var decimals = AmountMath.implyDecimals(token.totalSupplyRaw || "0")
return qsTr("Available %1").arg(root.balanceText(token, decimals))
}
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)
}
}