From 1b28e6bf733eb8357fdfba66d735ce080609d0f2 Mon Sep 17 00:00:00 2001 From: f321x Date: Wed, 14 Jan 2026 16:03:41 +0100 Subject: [PATCH] TxEditor: move swap request to TxEditor Moves the logic requesting the forward swap into the TxEditor so it can use the open transport and doesn't have to reconnect to the relays again. Also disables the "Preview" button in the TxEditor when the transaction will send change to lightning. This should prevent the user from saving the transaction to history and broadcasting it later or exporting it and broadcasting it through some external way. Broadcasting needs to happen directly after the TxEditor so we can send the second rpc call to the swapserver and await the incoming htlcs before broadcasting the (funding-) transaction. --- electrum/gui/qt/confirm_tx_dialog.py | 68 +++++++++++++++++++++------- electrum/gui/qt/main_window.py | 8 ++-- electrum/gui/qt/send_tab.py | 18 -------- 3 files changed, 56 insertions(+), 38 deletions(-) diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py index f59e2ed3f..a4c9b4f44 100644 --- a/electrum/gui/qt/confirm_tx_dialog.py +++ b/electrum/gui/qt/confirm_tx_dialog.py @@ -28,6 +28,7 @@ from decimal import Decimal from functools import partial from typing import TYPE_CHECKING, Optional, Union from concurrent.futures import Future +from enum import Enum, auto from PyQt6.QtCore import Qt, QTimer, pyqtSlot, pyqtSignal from PyQt6.QtGui import QIcon @@ -35,20 +36,20 @@ from PyQt6.QtWidgets import (QHBoxLayout, QVBoxLayout, QLabel, QGridLayout, QPus QComboBox, QTabWidget, QWidget, QStackedWidget) from electrum.i18n import _ -from electrum.util import (quantize_feerate, profiler, NotEnoughFunds, NoDynamicFeeEstimates, - get_asyncio_loop, wait_for2) +from electrum.util import (UserCancelled, quantize_feerate, profiler, NotEnoughFunds, NoDynamicFeeEstimates, + get_asyncio_loop, wait_for2, UserFacingException) from electrum.plugin import run_hook -from electrum.transaction import PartialTransaction, TxOutput +from electrum.transaction import PartialTransaction, PartialTxOutput from electrum.wallet import InternalAddressCorruption from electrum.bitcoin import DummyAddress from electrum.fee_policy import FeePolicy, FixedFeePolicy, FeeMethod from electrum.logging import Logger -from electrum.submarine_swaps import NostrTransport, HttpTransport, SwapServerTransport +from electrum.submarine_swaps import NostrTransport, HttpTransport, SwapServerTransport, SwapServerError from electrum.gui.messages import MSG_SUBMARINE_PAYMENT_HELP_TEXT from .util import (WindowModalDialog, ColorScheme, HelpLabel, Buttons, CancelButton, WWLabel, read_QIcon, qt_event_listener, QtEventListener, IconLabel, - HelpButton) + HelpButton, RunCoroutineDialog) from .transaction_dialog import TxSizeLabel, TxFiatLabel, TxInOutWidget from .fee_slider import FeeSlider, FeeComboBox from .amountedit import FeerateEdit, BTCAmountEdit @@ -60,6 +61,15 @@ if TYPE_CHECKING: from .main_window import ElectrumWindow +class TxEditorContext(Enum): + """ + Context for which the TxEditor gets launched. + Allows to enable/disable certain features. + """ + PAYMENT = auto() + CHANNEL_FUNDING = auto() + + class TxEditor(WindowModalDialog, QtEventListener, Logger): swap_availability_changed = pyqtSignal() @@ -69,8 +79,8 @@ class TxEditor(WindowModalDialog, QtEventListener, Logger): window: 'ElectrumWindow', make_tx, output_value: Union[int, str], - payee_outputs: Optional[list[TxOutput]] = None, - allow_preview=True, + payee_outputs: Optional[list[PartialTxOutput]] = None, + context: TxEditorContext = TxEditorContext.PAYMENT, batching_candidates=None, ): @@ -93,8 +103,7 @@ class TxEditor(WindowModalDialog, QtEventListener, Logger): self.not_enough_funds = False self.no_dynfee_estimates = False self.needs_update = False - # preview is disabled for lightning channel funding - self.allow_preview = allow_preview + self.context = context self.is_preview = False self._base_tx = None # for batching self.batching_candidates = batching_candidates @@ -311,8 +320,7 @@ class TxEditor(WindowModalDialog, QtEventListener, Logger): # always show onchain payment tab self.tab_widget.addTab(self.onchain_tab, _('Onchain Transaction')) - # allow_preview is false for ln channel opening txs - allow_swaps = self.allow_preview and self.payee_outputs and self.swap_manager + allow_swaps = self.context == TxEditorContext.PAYMENT and self.payee_outputs and self.swap_manager if self.config.WALLET_ENABLE_SUBMARINE_PAYMENTS and allow_swaps: i = self.tab_widget.addTab(self.submarine_payment_tab, _('Submarine Payment')) tooltip = self.config.cv.WALLET_ENABLE_SUBMARINE_PAYMENTS.get_long_desc() @@ -477,7 +485,7 @@ class TxEditor(WindowModalDialog, QtEventListener, Logger): self.change_to_ln_swap_providers_button = SwapProvidersButton(lambda: self.swap_transport, self.config, self.main_window) self.preview_button = QPushButton(_('Preview')) self.preview_button.clicked.connect(self.on_preview) - self.preview_button.setVisible(self.allow_preview) + self.preview_button.setVisible(self.context != TxEditorContext.CHANNEL_FUNDING) self.ok_button = QPushButton(_('OK')) self.ok_button.clicked.connect(self.on_send) self.ok_button.setDefault(True) @@ -608,9 +616,13 @@ class TxEditor(WindowModalDialog, QtEventListener, Logger): return self.tx if not cancelled else None def on_send(self): + if self.tx and self.tx.get_dummy_output(DummyAddress.SWAP): + if not self.request_forward_swap(): + return self.accept() def on_preview(self): + assert not self.tx.get_dummy_output(DummyAddress.SWAP), "no preview when sending change to ln" self.is_preview = True self.accept() @@ -746,8 +758,12 @@ class TxEditor(WindowModalDialog, QtEventListener, Logger): self.message_label.setText(self.error or message_str) def _update_send_button(self): + # disable preview button when sending change to lightning to prevent the user from saving or + # exporting the transaction and broadcasting it later somehow. + send_change_to_ln = self.tx and self.tx.get_dummy_output(DummyAddress.SWAP) enabled = bool(self.tx) and not self.error - self.preview_button.setEnabled(enabled) + self.preview_button.setEnabled(enabled and not send_change_to_ln) + self.preview_button.setToolTip(_("Can't show preview when sending change to lightning") if send_change_to_ln else "") self.ok_button.setEnabled(enabled) def can_pay_assuming_zero_fees(self, confirmed_only: bool) -> bool: @@ -1073,6 +1089,26 @@ class TxEditor(WindowModalDialog, QtEventListener, Logger): self.submarine_stacked_widget.setCurrentIndex(1) self.submarine_ok_button.setEnabled(False) + # --- send change to lightning swap functionality --- + def request_forward_swap(self): + swap_dummy_output = self.tx.get_dummy_output(DummyAddress.SWAP) + sm, transport = self.swap_manager, self.swap_transport + assert sm and transport and swap_dummy_output and isinstance(swap_dummy_output.value, int) + coro = sm.request_swap_for_amount(transport=transport, onchain_amount=int(swap_dummy_output.value)) + coro_dialog = RunCoroutineDialog(self, _('Requesting swap invoice...'), coro) + try: + swap, swap_invoice = coro_dialog.run() + except (SwapServerError, UserFacingException) as e: + self.show_error(str(e)) + return False + except UserCancelled: + return False + self.tx.replace_output_address(DummyAddress.SWAP, swap.lockup_address) + assert self.tx.get_dummy_output(DummyAddress.SWAP) is None + self.tx.swap_invoice = swap_invoice + self.tx.swap_payment_hash = swap.payment_hash + return True + class ConfirmTxDialog(TxEditor): help_text = '' #_('Set the mining fee of your transaction') @@ -1082,8 +1118,8 @@ class ConfirmTxDialog(TxEditor): window: 'ElectrumWindow', make_tx, output_value: Union[int, str], - payee_outputs: Optional[list[TxOutput]] = None, - allow_preview=True, + payee_outputs: Optional[list[PartialTxOutput]] = None, + context: TxEditorContext = TxEditorContext.PAYMENT, batching_candidates=None, ): @@ -1094,7 +1130,7 @@ class ConfirmTxDialog(TxEditor): output_value=output_value, payee_outputs=payee_outputs, title=_("New Transaction"), # todo: adapt title for channel funding tx, swaps - allow_preview=allow_preview, # false for channel funding + context=context, batching_candidates=batching_candidates, ) self.trigger_update() diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 482a4bba0..fe4e7fb5f 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -96,7 +96,7 @@ from .wizard.wallet import WIF_HELP_TEXT from .history_list import HistoryList, HistoryModel from .update_checker import UpdateCheck, UpdateCheckThread from .channels_list import ChannelsList -from .confirm_tx_dialog import ConfirmTxDialog +from .confirm_tx_dialog import ConfirmTxDialog, TxEditorContext from .rbf_dialog import BumpFeeDialog, DSCancelDialog from .qrreader import scan_qrcode_from_camera from .swap_dialog import SwapDialog, InvalidSwapParameters @@ -1504,7 +1504,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): return # we need to know the fee before we broadcast, because the txid is required make_tx = self.mktx_for_open_channel(funding_sat=funding_sat, node_id=node_id) - funding_tx, _, _ = self.confirm_tx_dialog(make_tx, funding_sat, allow_preview=False) + funding_tx, _, _ = self.confirm_tx_dialog(make_tx, funding_sat, context=TxEditorContext.CHANNEL_FUNDING) if not funding_tx: return self._open_channel(connect_str, funding_sat, push_amt, funding_tx) @@ -1514,7 +1514,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): make_tx, output_value, *, payee_outputs: Optional[list[TxOutput]] = None, - allow_preview=True, + context: TxEditorContext = TxEditorContext.PAYMENT, batching_candidates=None, ) -> tuple[Optional[PartialTransaction], bool, bool]: d = ConfirmTxDialog( @@ -1522,7 +1522,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): make_tx=make_tx, output_value=output_value, payee_outputs=payee_outputs, - allow_preview=allow_preview, + context=context, batching_candidates=batching_candidates, ) return d.run(), d.is_preview, d.did_swap diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index bff1b458f..fc02d072c 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -351,24 +351,6 @@ class SendTab(QWidget, MessageBoxMixin, Logger): # user cancelled or paid with swap return - if swap_dummy_output := tx.get_dummy_output(DummyAddress.SWAP): - sm = self.wallet.lnworker.swap_manager - with self.window.create_sm_transport() as transport: - if not self.window.initialize_swap_manager(transport): - return - coro = sm.request_swap_for_amount(transport=transport, onchain_amount=swap_dummy_output.value) - try: - swap, swap_invoice = self.window.run_coroutine_dialog(coro, _('Requesting swap invoice...')) - except (SwapServerError, UserFacingException) as e: - self.show_error(str(e)) - return - except UserCancelled: - return - tx.replace_output_address(DummyAddress.SWAP, swap.lockup_address) - assert tx.get_dummy_output(DummyAddress.SWAP) is None - tx.swap_invoice = swap_invoice - tx.swap_payment_hash = swap.payment_hash - if is_preview: self.window.show_transaction( tx,