Files
lez-programs/apps/amm/qml/components/swap/TokenInput.qml
T
r4bbit 4651f28a05 fix(apps/amm): one token list and one token picker for both views
The two views listed different tokens: swap rendered tokenList()
(TOKENS_CONFIG verbatim, no chain check) while liquidity rendered
resolveTokens(), which reads each id on-chain and silently drops any whose
definition isn't a readable fungible token owned by the configured
TokenProgram.
2026-08-20 15:21:47 +02:00

207 lines
8.5 KiB
QML

import QtQuick 2.15
import QtQuick.Layouts 1.15
import Logos.Wallet
import "../shared/TokenVisuals.js" as TokenVisuals
Rectangle {
id: root
property var theme
property string label: ""
property string amount: ""
property string usdValue: ""
property var token: null
// The wallet's token holdings (backend.tokenHoldings()); the selector narrows
// them to this slot's token. The chosen holding id is exposed as selectedHoldingId.
property var holdings: []
// The token's definitionId as configured (TOKENS_CONFIG passes it through
// as-is — base58 or hex). tokenHoldings emits both encodings per holding, so
// match on whichever this id is: a 64-char hex string filters the holding's
// definitionIdHex, otherwise the base58 definitionId. (Filtering on a single
// fixed encoding shows "No funds" whenever the config uses the other one.)
readonly property string tokenDefinitionId: root.token ? String(root.token.definitionId || "") : ""
readonly property bool tokenIdIsHex: /^[0-9a-fA-F]{64}$/.test(root.tokenDefinitionId)
readonly property string selectedHoldingId: accountSelector.selectedAccountId
property bool active: true
// When true, restrict input to digits only — used for the sell-amount
// field, whose value is sent to the backend as a raw base-units integer
// string (see decimalToU128Le in AmmUiBackend.cpp); fractional/decimal
// input there fails opaquely rather than being scaled.
property bool digitsOnly: false
// objectName forwarded to the inner TextInput so UI tests can target the
// sell/buy amount fields deterministically (see apps/amm/tests/).
property alias inputObjectName: tiInput.objectName
// objectName forwarded to the token-select button, so tests can open the
// right picker by objectId rather than fuzzy text.
property alias buttonObjectName: tokenButton.objectName
// objectName forwarded to the account selector, so tests can pick the funding
// holding for this side deterministically.
property alias selectorObjectName: accountSelector.objectName
signal tokenClicked()
signal inputEdited(string newValue)
Binding {
target: tiInput
property: "text"
value: root.amount
}
radius: 16
color: root.active ? theme.colors.inputBg : theme.colors.panelBg
// Height grows with the content: the amount/token row plus the full-width
// account selector below it (34/20/0px depending on how many holdings match).
implicitHeight: tiContent.implicitHeight + 28
Behavior on color { ColorAnimation { duration: 300 } }
ColumnLayout {
id: tiContent
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.leftMargin: 16
anchors.rightMargin: 16
anchors.topMargin: 14
spacing: 10
RowLayout {
Layout.fillWidth: true
spacing: 8
ColumnLayout {
Layout.fillWidth: true
spacing: 4
Text {
text: root.label
color: theme.colors.textSecondary
font.pixelSize: 14
}
Item {
Layout.fillWidth: true
height: 44
TextInput {
id: tiInput
anchors.fill: parent
color: root.active ? theme.colors.textPrimary : theme.colors.textSecondary
font.pixelSize: 36
font.weight: Font.Bold
selectionColor: theme.colors.selection
clip: true
onTextEdited: {
if (root.digitsOnly) {
// Amounts are base units (integers) — strip any
// character that slips past the validator (e.g.
// via paste) before it reaches the backend.
var filtered = text.replace(/[^0-9]/g, "")
if (filtered !== text)
text = filtered // does not re-trigger onTextEdited
root.inputEdited(filtered)
} else {
root.inputEdited(text)
}
}
validator: RegularExpressionValidator {
regularExpression: root.digitsOnly ? /^[0-9]*$/ : /^[0-9]*\.?[0-9]*$/
}
}
Text {
anchors.fill: parent
text: "0"
color: theme.colors.textPlaceholder
font: tiInput.font
visible: tiInput.text === "" && !tiInput.activeFocus
verticalAlignment: Text.AlignVCenter
}
}
Text {
text: root.usdValue
color: theme.colors.textSecondary
font.pixelSize: 13
visible: root.usdValue !== ""
}
}
Rectangle {
id: tokenButton
Layout.alignment: Qt.AlignVCenter
height: 40
radius: 20
color: tokenBtnHover.containsMouse ? theme.colors.panelHoverBg : theme.colors.panelBg
implicitWidth: tokenBtnRow.implicitWidth + 24
Behavior on color { ColorAnimation { duration: 120 } }
RowLayout {
id: tokenBtnRow
anchors.centerIn: parent
spacing: 6
Rectangle {
width: 24; height: 24; radius: 12
color: root.token ? TokenVisuals.colorFor(root.token.symbol) : theme.colors.noTokenCircle
visible: root.token !== null
Text {
anchors.centerIn: parent
text: root.token ? TokenVisuals.letterFor(root.token.symbol) : ""
color: "#ffffff"
font.pixelSize: 10
font.weight: Font.Bold
}
}
Text {
text: root.token ? root.token.symbol : "Select token"
color: theme.colors.textPrimary
font.pixelSize: 15
font.weight: root.token ? Font.Medium : Font.Normal
}
Text {
text: "▼"
color: theme.colors.textSecondary
font.pixelSize: 10
}
}
MouseArea {
id: tokenBtnHover
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.tokenClicked()
}
}
}
// Which of the user's holdings for this token to use, full width below the
// amount/token row. Always shown when there is at least one holding
// (auto-selecting the single one); "No funds" when there are none.
ProgramAccountSelector {
id: accountSelector
// Half width, pinned to the right edge under the token button.
Layout.alignment: Qt.AlignRight
Layout.preferredWidth: Math.round(tiContent.width / 2)
sourceModel: root.holdings
accountType: "TokenHolding"
// Match the holding encoding to the configured id's encoding (see
// tokenDefinitionId above): hex → definitionIdHex, base58 → definitionId.
stateField: root.tokenIdIsHex ? "definitionIdHex" : "definitionId"
stateValue: root.tokenIdIsHex ? root.tokenDefinitionId.toLowerCase() : root.tokenDefinitionId
selectionMode: ProgramAccountSelector.Input
showWhenSingle: true
textAlignment: Text.AlignRight
backgroundColor: root.theme.colors.panelBg
hoverColor: root.theme.colors.panelHoverBg
textColor: root.theme.colors.textPrimary
secondaryTextColor: root.theme.colors.textSecondary
borderColor: root.theme.colors.borderStrong
focusColor: root.theme.colors.ctaBg
}
}
}