Files
lez-programs/apps/amm/qml/components/swap/TokenInput.qml
r4bbit 86575a65aa test(apps/amm): select the funding account before submitting a swap
canSubmit now requires a chosen holding on both swap sides, so the swap e2e
test must select them first. The selector auto-selects a single holding, but
pick it explicitly (robust to multi-account wallets) via a new selectAccount
step that waits for the holdings to load and selects the first match.

Expose each side's selector via selectorObjectName (swapSell/BuyAccountSelector)
and surface sellHolding/buyHolding in the failure diagnostics.
2026-08-11 12:04:48 +02:00

203 lines
8.1 KiB
QML

import QtQuick 2.15
import QtQuick.Layouts 1.15
import Logos.Wallet
import "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: []
readonly property string tokenDefinitionIdHex: root.token ? String(root.token.definitionId || "") : ""
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"
// The swap view is hex end-to-end: the token's definitionId is hex, so match the
// holding's hex definitionIdHex (tokenHoldings emits both — `definitionId` is base58
// for the liquidity view). Filtering on `definitionId` here never matches a hex
// stateValue, so every token would show "No funds".
stateField: "definitionIdHex"
stateValue: root.tokenDefinitionIdHex
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
}
}
}