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,