diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py index cc52d1bdd..dfd7a889c 100644 --- a/electrum/gui/qt/confirm_tx_dialog.py +++ b/electrum/gui/qt/confirm_tx_dialog.py @@ -42,7 +42,7 @@ from electrum.simple_config import SimpleConfig from electrum.bitcoin import DummyAddress from .util import (WindowModalDialog, ColorScheme, HelpLabel, Buttons, CancelButton, - BlockingWaitingDialog, PasswordLineEdit, WWLabel, read_QIcon) + PasswordLineEdit, WWLabel, read_QIcon) from .fee_slider import FeeSlider, FeeComboBox @@ -532,7 +532,7 @@ class TxEditor(WindowModalDialog): if self.not_enough_funds: self.io_widget.update(None) self.set_feerounding_visibility(False) - self.messages = [] + self.messages = [_('Preparing transaction...')] else: self.messages = self.get_messages() self.update_fee_fields() @@ -619,7 +619,7 @@ class ConfirmTxDialog(TxEditor): title=_("New Transaction"), # todo: adapt title for channel funding tx, swaps allow_preview=allow_preview) - BlockingWaitingDialog(window, _("Preparing transaction..."), self.update) + self.trigger_update() def _update_amount_label(self): tx = self.tx diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index ba5ab98ef..5d2bb5e0f 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -90,7 +90,7 @@ from .util import (read_QIcon, ColorScheme, text_dialog, icon_path, WaitingDialo import_meta_gui, export_meta_gui, filename_field, address_field, char_width_in_lineedit, webopen, TRANSACTION_FILE_EXTENSION_FILTER_ANY, MONOSPACE_FONT, - getOpenFileName, getSaveFileName, BlockingWaitingDialog, font_height) + getOpenFileName, getSaveFileName, font_height) from .util import ButtonsLineEdit, ShowQRLineEdit from .util import QtEventListener, qt_event_listener, event_listener from .wizard.wallet import WIF_HELP_TEXT @@ -301,26 +301,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): self._update_check_thread.checked.connect(on_version_received) self._update_check_thread.start() - def run_coroutine_dialog(self, coro, text, on_result, on_cancelled): - """ run coroutine in a waiting dialog, with a Cancel button that cancels the coroutine """ - from electrum import util - loop = util.get_asyncio_loop() - assert util.get_running_loop() != loop, 'must not be called from asyncio thread' - future = asyncio.run_coroutine_threadsafe(coro, loop) - def task(): - try: - return future.result() - except concurrent.futures.CancelledError: - on_cancelled() - try: - WaitingDialog( - self, text, task, - on_success=on_result, - on_error=self.on_error, - on_cancel=future.cancel) - except Exception as e: - self.show_error(str(e)) - raise + def run_coroutine_dialog(self, coro, text): + """ run coroutine in a waiting dialog, with a Cancel button that cancels the coroutine""" + from .util import RunCoroutineDialog + d = RunCoroutineDialog(self, text, coro) + return d.run() def run_coroutine_from_thread(self, coro, name, on_result=None): if self._cleaned_up: @@ -1180,10 +1165,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): if not self.wallet.lnworker.num_sats_can_send() and not self.wallet.lnworker.num_sats_can_receive(): self.show_error(_("You do not have liquidity in your active channels.")) return - def get_pairs_thread(): - self.network.run_from_another_thread(self.wallet.lnworker.swap_manager.get_pairs()) try: - BlockingWaitingDialog(self, _('Please wait...'), get_pairs_thread) + self.run_coroutine_dialog( + self.wallet.lnworker.swap_manager.get_pairs(), _('Please wait...')) except SwapServerError as e: self.show_error(str(e)) return diff --git a/electrum/gui/qt/swap_dialog.py b/electrum/gui/qt/swap_dialog.py index 3b5022511..0cd91932f 100644 --- a/electrum/gui/qt/swap_dialog.py +++ b/electrum/gui/qt/swap_dialog.py @@ -4,7 +4,7 @@ from PyQt6.QtCore import pyqtSignal from PyQt6.QtWidgets import QLabel, QVBoxLayout, QGridLayout, QPushButton from electrum.i18n import _ -from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates +from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates, UserCancelled from electrum.bitcoin import DummyAddress from electrum.transaction import PartialTxOutput, PartialTransaction @@ -319,25 +319,33 @@ class SwapDialog(WindowModalDialog, QtEventListener): recv_amount = self.recv_amount_e.get_amount() self.ok_button.setEnabled(bool(send_amount) and bool(recv_amount)) - def do_normal_swap(self, lightning_amount, onchain_amount, password): + async def _do_normal_swap(self, lightning_amount, onchain_amount, password): dummy_tx = self._create_tx(onchain_amount) assert dummy_tx sm = self.swap_manager + swap, invoice = await sm.request_normal_swap( + lightning_amount_sat=lightning_amount, + expected_onchain_amount_sat=onchain_amount, + channels=self.channels, + ) + self._current_swap = swap + tx = sm.create_funding_tx(swap, dummy_tx, password=password) + txid = await sm.wait_for_htlcs_and_broadcast(swap=swap, invoice=invoice, tx=tx) + return txid + + def do_normal_swap(self, lightning_amount, onchain_amount, password): self._current_swap = None - async def coro(): - swap, invoice = await sm.request_normal_swap( - lightning_amount_sat=lightning_amount, - expected_onchain_amount_sat=onchain_amount, - channels=self.channels, - ) - self._current_swap = swap - tx = sm.create_funding_tx(swap, dummy_tx, password=password) - txid = await sm.wait_for_htlcs_and_broadcast(swap=swap, invoice=invoice, tx=tx) - return txid - self.window.run_coroutine_dialog( - coro(), _('Awaiting swap payment...'), - on_result=lambda funding_txid: self.window.on_swap_result(funding_txid, is_reverse=False), - on_cancelled=lambda: sm.cancel_normal_swap(self._current_swap)) + coro = self._do_normal_swap(lightning_amount, onchain_amount, password) + try: + funding_txid = self.window.run_coroutine_dialog(coro, _('Awaiting swap payment...')) + except UserCancelled: + self.swap_manager.cancel_normal_swap(self._current_swap) + self.window.show_message(_('Swap cancelled')) + return + except Exception as e: + self.window.show_error(str(e)) + return + self.window.on_swap_result(funding_txid, is_reverse=False) def get_description(self): onchain_funds = "onchain funds" diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index bf0ef39d8..ff7a15a18 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -62,7 +62,7 @@ from .util import (MessageBoxMixin, read_QIcon, Buttons, icon_path, char_width_in_lineedit, TRANSACTION_FILE_EXTENSION_FILTER_SEPARATE, TRANSACTION_FILE_EXTENSION_FILTER_ONLY_COMPLETE_TX, TRANSACTION_FILE_EXTENSION_FILTER_ONLY_PARTIAL_TX, - BlockingWaitingDialog, getSaveFileName, ColorSchemeItem, + getSaveFileName, ColorSchemeItem, get_iconname_qrcode, VLine, WaitingDialog) from .rate_limiter import rate_limited from .my_treeview import create_toolbar_with_menu, QMenuWithConfig @@ -582,11 +582,9 @@ class TxDialog(QDialog, MessageBoxMixin): # FIXME for PSBTs, we do a blocking fetch, as the missing data might be needed for e.g. signing # - otherwise, the missing data is for display-completeness only, e.g. fee, input addresses (we do it async) if not tx.is_complete() and tx.is_missing_info_from_network(): - BlockingWaitingDialog( - self, + self.main_window.run_coroutine_dialog( + tx.add_info_from_network(self.wallet.network, timeout=10), _("Adding info to tx, from network..."), - lambda: Network.run_from_another_thread( - tx.add_info_from_network(self.wallet.network, timeout=10)), ) else: self.maybe_fetch_txin_data() diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 4a06f6a29..79f5c1bce 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -21,7 +21,7 @@ from PyQt6.QtWidgets import (QPushButton, QLabel, QMessageBox, QHBoxLayout, QVBo from electrum.i18n import _ from electrum.util import FileImportFailed, FileExportFailed, resource_path -from electrum.util import EventListener, event_listener, get_logger +from electrum.util import EventListener, event_listener, get_logger, UserCancelled from electrum.invoices import PR_UNPAID, PR_PAID, PR_EXPIRED, PR_INFLIGHT, PR_UNKNOWN, PR_FAILED, PR_ROUTING, PR_UNCONFIRMED, PR_BROADCASTING, PR_BROADCAST from electrum.logging import Logger from electrum.qrreader import MissingQrDetectionLib @@ -371,32 +371,32 @@ class WaitingDialog(WindowModalDialog): self.message_label.setText(msg) -class BlockingWaitingDialog(WindowModalDialog): - """Shows a waiting dialog whilst running a task. - Should be called from the GUI thread. The GUI thread will be blocked while - the task is running; the point of the dialog is to provide feedback - to the user regarding what is going on. - """ - def __init__(self, parent: QWidget, message: str, task: Callable[[], Any]): - assert parent - if isinstance(parent, MessageBoxMixin): - parent = parent.top_level_window() - WindowModalDialog.__init__(self, parent, _("Please wait")) - self.message_label = QLabel(message) - vbox = QVBoxLayout(self) - vbox.addWidget(self.message_label) - self.finished.connect(self.deleteLater) # see #3956 - # show popup - self.show() - # refresh GUI; needed for popup to appear and for message_label to get drawn - QCoreApplication.processEvents() - QCoreApplication.processEvents() - try: - # block and run given task - task() - finally: - # close popup - self.accept() +class RunCoroutineDialog(WaitingDialog): + + def __init__(self, parent: QWidget, message: str, coroutine): + from electrum import util + import asyncio + import concurrent.futures + loop = util.get_asyncio_loop() + assert util.get_running_loop() != loop, 'must not be called from asyncio thread' + self._exception = None + self._result = None + self._future = asyncio.run_coroutine_threadsafe(coroutine, loop) + def task(): + try: + self._result = self._future.result() + except concurrent.futures.CancelledError: + self._exception = UserCancelled + except Exception as e: + self._exception = e + WaitingDialog.__init__(self, parent, message, task, on_cancel=self._future.cancel) + + def run(self): + self.exec() + if self._exception: + raise self._exception + else: + return self._result def line_dialog(parent, title, label, ok_label, default=None):