fix(apps/amm): disable the already-selected token in the swap token picker

The token selector let you pick the same token on both sides of a swap.
That drove resolvePool into amm_client_pool_pda with def_a == def_b, which
hits `panic!("Definitions match")` in amm_core (a pool needs two distinct
tokens). Because that panic crosses the `#[no_mangle]` FFI boundary — which
can't unwind — it aborts, taking the whole UI process down.

Guard it at the source: the picker now disables (dims, no hover/click, tags
"Selected") whichever token is already chosen on the opposite side, so the
two sides can never match.

- TokenListItem: add a `disabled` state (opacity, inert MouseArea, tag)
- TokenSelectorModal: add `disabledDefinitionId`; gate both the popular-token
  pills and the list rows on it
- SwapPage: on open, set it to the opposite side's selected token
This commit is contained in:
r4bbit
2026-08-13 13:03:58 +02:00
parent 62dc45177d
commit bf63070a9e
3 changed files with 35 additions and 6 deletions
+16 -3
View File
@@ -9,15 +9,20 @@ Item {
property string tokenName: ""
property string tokenSymbol: ""
property string tokenDefinitionId: ""
// When true, the token is already selected on the other side of the swap,
// so it's shown dimmed and can't be picked (a pool needs two distinct
// tokens — picking the same one both sides panics amm_core's pool PDA).
property bool disabled: false
signal clicked()
implicitHeight: 56
opacity: root.disabled ? 0.35 : 1.0
Rectangle {
anchors.fill: parent
radius: 12
color: hoverArea.containsMouse ? theme.colors.panelBg : "transparent"
color: (!root.disabled && hoverArea.containsMouse) ? theme.colors.panelBg : "transparent"
Behavior on color { ColorAnimation { duration: 120 } }
RowLayout {
@@ -62,13 +67,21 @@ Item {
}
}
}
Text {
visible: root.disabled
text: qsTr("Selected")
color: theme.colors.textSecondary
font.pixelSize: 12
}
}
MouseArea {
id: hoverArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: !root.disabled
hoverEnabled: !root.disabled
cursorShape: root.disabled ? Qt.ArrowCursor : Qt.PointingHandCursor
onClicked: root.clicked()
}
}
@@ -12,6 +12,10 @@ Item {
property var theme
property var tokens: []
property string searchText: ""
// definitionId of the token already chosen on the other side of the swap.
// That token is shown disabled here so the two sides can never match (a
// same-token pool has no PDA — it panics amm_core).
property string disabledDefinitionId: ""
signal tokenSelected(var token)
@@ -118,12 +122,17 @@ Item {
Repeater {
model: root.tokens.slice(0, 5)
delegate: Rectangle {
id: pill
readonly property bool isDisabled:
root.disabledDefinitionId !== "" &&
modelData.definitionId === root.disabledDefinitionId
height: 40
radius: 20
color: pillHover.containsMouse ? theme.colors.panelHoverBg : theme.colors.panelBg
color: (!pill.isDisabled && pillHover.containsMouse) ? theme.colors.panelHoverBg : theme.colors.panelBg
border.color: theme.colors.border
border.width: 1
width: pillRow.implicitWidth + 24
opacity: pill.isDisabled ? 0.35 : 1.0
Behavior on color { ColorAnimation { duration: 120 } }
RowLayout {
id: pillRow
@@ -139,8 +148,9 @@ Item {
MouseArea {
id: pillHover
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: !pill.isDisabled
hoverEnabled: !pill.isDisabled
cursorShape: pill.isDisabled ? Qt.ArrowCursor : Qt.PointingHandCursor
onClicked: root.tokenSelected(modelData)
}
}
@@ -170,6 +180,8 @@ Item {
tokenName: modelData.name
tokenSymbol: modelData.symbol
tokenDefinitionId: modelData.definitionId
disabled: root.disabledDefinitionId !== "" &&
modelData.definitionId === root.disabledDefinitionId
onClicked: root.tokenSelected(modelData)
}
}
+4
View File
@@ -144,6 +144,10 @@ Item {
onRequestTokenSelect: function(side) {
tokenModal.targetSide = side
// Disable the token already picked on the opposite side so the
// two sides can't match (a same-token pool panics amm_core).
var other = side === "sell" ? swapCard.buyToken : swapCard.sellToken
tokenModal.disabledDefinitionId = other ? other.definitionId : ""
tokenModal.open()
}