From 913f5ec6d80347321ad6ebbbcfa9f8028ebaa55d Mon Sep 17 00:00:00 2001 From: f321x Date: Tue, 8 Apr 2025 16:27:54 +0200 Subject: [PATCH] add option to show warnings on wallet close, add warning for ongoing submarine swap --- electrum/gui/qt/main_window.py | 61 +++++++++++++++++++++++++++++++++- electrum/submarine_swaps.py | 12 ++++++- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 31518139882..52892bc9b69 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -34,7 +34,7 @@ from functools import partial import queue import asyncio -from typing import Optional, TYPE_CHECKING, Sequence, Union, Dict, Mapping +from typing import Optional, TYPE_CHECKING, Sequence, Union, Dict, Mapping, Callable, List, Set import concurrent.futures from PyQt6.QtGui import QPixmap, QKeySequence, QIcon, QCursor, QFont, QFontMetrics, QAction, QShortcut @@ -271,6 +271,9 @@ def add_optional_tab(tabs, tab, icon, description): # network callbacks self.register_callbacks() + # wallet closing warning callbacks + self.closing_warning_callbacks = [] # type: List[Callable[[], Optional[str]]] + self.register_closing_warning_callback(self._check_ongoing_submarine_swaps_callback) # banner may already be there if self.network and self.network.banner: self.console.showMessage(self.network.banner) @@ -2684,8 +2687,64 @@ def settings_dialog(self): if d.need_restart: self.show_warning(_('Please restart Electrum to activate the new GUI settings'), title=_('Success')) + def _show_closing_warnings(self) -> bool: + """Show any closing warnings and return True if the user chose to quit anyway.""" + + warnings: Set[str] = set() + for cb in self.closing_warning_callbacks: + if warning := cb(): + warnings.add(warning) + + for warning in list(warnings)[:3]: + warning = _("An ongoing operation prevents Electrum from closing:") + "\n\n" + warning + result = self.question( + msg=warning, + icon=QMessageBox.Icon.Warning, + title=_("Are you sure you want to close?"), + ) + if not result: + break + else: + # user chose to cancel all warnings or there were no warnings + return True + return False + + def register_closing_warning_callback(self, callback: Callable[[], Optional[str]]) -> None: + """ + Registers a callback that will be called when the wallet is closed. If the callback + returns a string it will be shown to the user as a warning to prevent them closing the wallet. + """ + assert not asyncio.iscoroutinefunction(callback) + def warning_callback() -> Optional[str]: + try: + return callback() + except Exception: + self.logger.exception("Error in closing warning callback") + return None + self.logger.debug(f"registering wallet closing warning callback") + self.closing_warning_callbacks.append(warning_callback) + + def _check_ongoing_submarine_swaps_callback(self) -> Optional[str]: + """Callback that will return a warning string if there are unconfirmed swap funding txs.""" + if not (self.wallet.has_lightning() and self.wallet.lnworker.swap_manager): + return None + if ongoing_swaps := self.wallet.lnworker.swap_manager.get_pending_swaps(): + return "".join(( + f"{str(len(ongoing_swaps))} ", + _("pending submarine swap") if len(ongoing_swaps) == 1 else _("pending submarine swaps"), + ":\n", + _("Wait until the funding transaction of your swap confirms, otherwise you risk losing your"), + " ", + _("funds") if any(not swap.is_reverse for swap in ongoing_swaps) else _("mining fee prepayment"), + ".", + )) + return None + def closeEvent(self, event): # note that closeEvent is NOT called if the user quits with Ctrl-C + if not self._show_closing_warnings(): + event.ignore() + return self.clean_up() event.accept() diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index 967abe0b834..0800ffa8d1e 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -2,7 +2,7 @@ import json import os import ssl -from typing import TYPE_CHECKING, Optional, Dict, Union, Sequence, Tuple, Iterable +from typing import TYPE_CHECKING, Optional, Dict, Sequence, Tuple, Iterable, List from decimal import Decimal import math import time @@ -1279,6 +1279,16 @@ def get_group_id_for_payment_hash(self, payment_hash): if swap: return swap.spending_txid if swap.is_reverse else swap.funding_txid + def get_pending_swaps(self) -> List[SwapData]: + """Returns a list of swaps with unconfirmed funding tx (which require us to stay online).""" + pending_swaps: List[SwapData] = [] + for swap in self.swaps.values(): + if swap.funding_txid and not swap.is_redeemed: + # swap is funded but not marked redeemed by _claim_swap + if self.lnworker.wallet.adb.get_tx_height(swap.funding_txid).height < 1: + # funding TX is not yet confirmed, wallet should not be closed + pending_swaps.append(swap) + return pending_swaps class SwapServerTransport(Logger):