From bd1280b8d7b979b0efe59731ebe04db7443258be Mon Sep 17 00:00:00 2001 From: r4bbit <445106+0x-r4bbit@users.noreply.github.com> Date: Thu, 23 Jul 2026 13:47:46 +0200 Subject: [PATCH] fix(apps/amm): disable the already-selected token in the swap token picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../amm/qml/components/swap/TokenListItem.qml | 19 ++++++++++++++++--- .../components/swap/TokenSelectorModal.qml | 18 +++++++++++++++--- apps/amm/qml/pages/SwapPage.qml | 4 ++++ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/apps/amm/qml/components/swap/TokenListItem.qml b/apps/amm/qml/components/swap/TokenListItem.qml index fba33ae3..49f52f9f 100644 --- a/apps/amm/qml/components/swap/TokenListItem.qml +++ b/apps/amm/qml/components/swap/TokenListItem.qml @@ -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() } } diff --git a/apps/amm/qml/components/swap/TokenSelectorModal.qml b/apps/amm/qml/components/swap/TokenSelectorModal.qml index bb487d8a..56d4b2fe 100644 --- a/apps/amm/qml/components/swap/TokenSelectorModal.qml +++ b/apps/amm/qml/components/swap/TokenSelectorModal.qml @@ -9,6 +9,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) @@ -115,12 +119,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 @@ -136,8 +145,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) } } @@ -164,6 +174,8 @@ Item { tokenName: modelData.name tokenSymbol: modelData.symbol tokenDefinitionId: modelData.definitionId + disabled: root.disabledDefinitionId !== "" && + modelData.definitionId === root.disabledDefinitionId onClicked: root.tokenSelected(modelData) } } diff --git a/apps/amm/qml/pages/SwapPage.qml b/apps/amm/qml/pages/SwapPage.qml index 344d47a7..90e8ab7b 100644 --- a/apps/amm/qml/pages/SwapPage.qml +++ b/apps/amm/qml/pages/SwapPage.qml @@ -107,6 +107,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() }