mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-26 14:41:12 +00:00
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.
1535 lines
60 KiB
QML
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)
|
|
}
|
|
}
|