From 2c67c5e1e5859d72c3fdbee9c235443674e0933e Mon Sep 17 00:00:00 2001 From: f321x Date: Mon, 1 Sep 2025 10:09:33 +0200 Subject: [PATCH 1/4] swaps: make min swap amount a const var instead of hardcoding 20_000 sat directly in the code, make MIN_SWAP_AMOUNT_SAT a const variable outside of SwapManager so it can be used for other code too. --- electrum/submarine_swaps.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index f0e84c56c..5174ef738 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -59,6 +59,7 @@ if TYPE_CHECKING: SWAP_TX_SIZE = 150 # default tx size, used for mining fee estimation +MIN_SWAP_AMOUNT_SAT = 20_000 MIN_LOCKTIME_DELTA = 60 LOCKTIME_DELTA_REFUND = 70 MAX_LOCKTIME_DELTA = 100 @@ -1128,7 +1129,7 @@ class SwapManager(Logger): def server_update_pairs(self) -> None: """ for server """ self.percentage = float(self.config.SWAPSERVER_FEE_MILLIONTHS) / 10000 # type: ignore - self._min_amount = 20000 + self._min_amount = MIN_SWAP_AMOUNT_SAT oc_balance_sat: int = self.wallet.get_spendable_balance_sat() max_forward: int = min(int(self.lnworker.num_sats_can_receive()), oc_balance_sat, 10000000) max_reverse: int = min(int(self.lnworker.num_sats_can_send()), 10000000) From a62dbb5650644fd2fb53fbca4606ec437718309b Mon Sep 17 00:00:00 2001 From: f321x Date: Mon, 1 Sep 2025 10:28:27 +0200 Subject: [PATCH 2/4] qt: receive tab: also suggest swap for 0 amnt req Also suggest a submarine swap if the user creates a 0 amount invoice and has 0 sat incoming liquidity as it won't be possible to receive anything. Users potentially just open a channel, then want to create a lightning invoice without amount like they are used to from onchain addresses, and then wonder why receiving doesn't work. So we should at least propose a swap if there is no inbound liquidity at all. --- electrum/lnworker.py | 2 +- electrum/wallet.py | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 0261e3fe0..bdf96e5e4 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -3003,7 +3003,7 @@ class LNWallet(LNWorker): return None def suggest_swap_to_receive(self, amount_sat): - assert amount_sat > self.num_sats_can_receive() + assert amount_sat > self.num_sats_can_receive(), f"{amount_sat=} | {self.num_sats_can_receive()=}" try: suggestions = self._suggest_channels_for_rebalance(RECEIVED, amount_sat) except NotEnoughFunds: diff --git a/electrum/wallet.py b/electrum/wallet.py index e28778abd..0b471b765 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -80,6 +80,7 @@ from .lnutil import MIN_FUNDING_SAT from .lntransport import extract_nodeid from .descriptor import Descriptor from .txbatcher import TxBatcher +from .submarine_swaps import MIN_SWAP_AMOUNT_SAT if TYPE_CHECKING: from .network import Network @@ -3429,7 +3430,8 @@ class Abstract_Wallet(ABC, Logger, EventListener): self.lnworker and len([chan for chan in self.lnworker.channels.values() if chan.is_open()]) > 0 ) lightning_online = self.lnworker and self.lnworker.num_peers() > 0 - can_receive_lightning = self.lnworker and amount_sat <= self.lnworker.num_sats_can_receive() + can_receive = self.lnworker.num_sats_can_receive() + can_receive_lightning = self.lnworker and can_receive > 0 and amount_sat <= can_receive try: zeroconf_nodeid = extract_nodeid(self.config.ZEROCONF_TRUSTED_NODE)[0] except Exception: @@ -3469,7 +3471,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): ln_help = _('You must be online to receive Lightning payments.') elif not can_receive_lightning or (amount_sat <= 0 and not lightning_has_channels): ln_rebalance_suggestion = self.lnworker.suggest_rebalance_to_receive(amount_sat) - ln_swap_suggestion = self.lnworker.suggest_swap_to_receive(amount_sat) + ln_swap_suggestion = self.lnworker.suggest_swap_to_receive(max(amount_sat, MIN_SWAP_AMOUNT_SAT)) # prefer to use swaps over JIT channels if possible if can_get_zeroconf_channel and not bool(ln_rebalance_suggestion) and not bool(ln_swap_suggestion): if amount_sat < MIN_FUNDING_SAT: @@ -3483,11 +3485,11 @@ class Abstract_Wallet(ABC, Logger, EventListener): f'service provider. Service fees are deducted from the incoming payment.') else: ln_is_error = True - ln_help = _('You do not have the capacity to receive this amount with Lightning.') + ln_help = _('You do not have enough capacity to receive with Lightning.') if bool(ln_rebalance_suggestion): - ln_help += '\n\n' + _('You may have that capacity if you rebalance your channels.') + ln_help += '\n\n' + _('You may have enough capacity if you rebalance your channels.') elif bool(ln_swap_suggestion): - ln_help += '\n\n' + _('You may have that capacity if you swap some of your funds.') + ln_help += '\n\n' + _('You may have enough capacity if you swap some of your funds.') # for URI that has LN part but no onchain part, copy error: if not addr and ln_is_error: URI_is_error = ln_is_error From 3df1af8ee35674bf34cecc672f5f25b09c34c549 Mon Sep 17 00:00:00 2001 From: f321x Date: Mon, 1 Sep 2025 10:37:30 +0200 Subject: [PATCH 3/4] qt: SwapDialog: don't force amount on user When a SwapDialog gets initiated with a recv_amount_sat through the receive tab the Max Button and edits are disabled and the user is forced to do a swap with the preset amount. Maybe the user wants to do a larger swap? --- electrum/gui/qt/swap_dialog.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/electrum/gui/qt/swap_dialog.py b/electrum/gui/qt/swap_dialog.py index 19de6be8d..49680a4af 100644 --- a/electrum/gui/qt/swap_dialog.py +++ b/electrum/gui/qt/swap_dialog.py @@ -81,9 +81,6 @@ class SwapDialog(WindowModalDialog, QtEventListener): # textEdited is triggered only for user editing of the fields self.send_amount_e.textEdited.connect(self.uncheck_max) self.recv_amount_e.textEdited.connect(self.uncheck_max) - self.send_amount_e.setEnabled(recv_amount_sat is None) - self.recv_amount_e.setEnabled(recv_amount_sat is None) - self.max_button.setEnabled(recv_amount_sat is None) self.fee_policy = FeePolicy(self.config.FEE_POLICY) self.fee_slider = FeeSlider(parent=self, network=self.network, fee_policy=self.fee_policy, callback=self.fee_slider_callback) @@ -282,7 +279,7 @@ class SwapDialog(WindowModalDialog, QtEventListener): min_swap_limit, max_swap_limit = self.get_client_swap_limits_sat() if max_swap_limit == 0: swap_name = _("reverse") if self.is_reverse else _("forward") - swap_limit_str = _("No {} swap possible").format(swap_name) + swap_limit_str = _("No {} swap possible with this provider").format(swap_name) else: swap_limit_str = (f"{self.window.format_amount(min_swap_limit)} - " f"{self.window.format_amount(max_swap_limit)} {w_base_unit}") From 45bdd6a827593949dbf4ad0e53c9abcf1e897328 Mon Sep 17 00:00:00 2001 From: f321x Date: Thu, 4 Sep 2025 12:24:12 +0200 Subject: [PATCH 4/4] swaps: add type hints to gui swap suggestion flow Adds type more type hints and clearer variable names to the swap suggestion flow. --- electrum/gui/qt/main_window.py | 30 +++++++++++++++++++++--------- electrum/gui/qt/receive_tab.py | 2 +- electrum/gui/qt/send_tab.py | 2 +- electrum/gui/qt/swap_dialog.py | 27 ++++++++++++++++++--------- electrum/gui/qt/utxo_list.py | 2 +- electrum/lnworker.py | 6 +++--- 6 files changed, 45 insertions(+), 24 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index a2aaf6a98..927e6820a 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -107,6 +107,7 @@ from electrum.gui.common_qt.util import TaskThread if TYPE_CHECKING: from . import ElectrumGui from electrum.submarine_swaps import SwapOffer + from electrum.lnchannel import Channel class StatusBarButton(QToolButton): @@ -1258,32 +1259,43 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): def protect(self, func, args, password): return func(*args, password) - def run_swap_dialog(self, is_reverse=None, recv_amount_sat=None, channels=None): + def run_swap_dialog( + self, + is_reverse: Optional[bool] = None, + recv_amount_sat_or_max: Optional[Union[int, str]] = None, + channels: Optional[Sequence['Channel']] = None, + ) -> bool: if not self.network: self.show_error(_("You are offline.")) - return + return False if not self.wallet.lnworker: self.show_error(_('Lightning is disabled')) - return + return False 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 + return False transport = self.create_sm_transport() if not transport: - return + return False with transport: if not self.initialize_swap_manager(transport): - return - d = SwapDialog(self, transport, is_reverse=is_reverse, recv_amount_sat=recv_amount_sat, channels=channels) + return False + d = SwapDialog( + self, + transport, + is_reverse=is_reverse, + recv_amount_sat_or_max=recv_amount_sat_or_max, + channels=channels + ) try: return d.run(transport) except InvalidSwapParameters as e: self.show_error(str(e)) - return + return False except UserCancelled: - return + return False def create_sm_transport(self) -> Optional['SwapServerTransport']: sm = self.wallet.lnworker.swap_manager diff --git a/electrum/gui/qt/receive_tab.py b/electrum/gui/qt/receive_tab.py index 39ec97d80..74377b319 100644 --- a/electrum/gui/qt/receive_tab.py +++ b/electrum/gui/qt/receive_tab.py @@ -112,7 +112,7 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger): def on_receive_swap(): if self.receive_swap_button.suggestion: chan, swap_recv_amount_sat = self.receive_swap_button.suggestion - self.window.run_swap_dialog(is_reverse=True, recv_amount_sat=swap_recv_amount_sat, channels=[chan]) + self.window.run_swap_dialog(is_reverse=True, recv_amount_sat_or_max=swap_recv_amount_sat, channels=[chan]) self.receive_swap_button.clicked.connect(on_receive_swap) buttons = QHBoxLayout() buttons.addWidget(self.receive_rebalance_button) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 1550e85fd..474cbe483 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -712,7 +712,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger): self.window.new_channel_dialog(amount_sat=amount_sat, min_amount_sat=min_amount_sat) elif r == 'swap': chan, swap_recv_amount_sat = can_pay_with_swap - self.window.run_swap_dialog(is_reverse=False, recv_amount_sat=swap_recv_amount_sat, channels=[chan]) + self.window.run_swap_dialog(is_reverse=False, recv_amount_sat_or_max=swap_recv_amount_sat, channels=[chan]) elif r == 'onchain': self.pay_onchain_dialog(invoice.get_outputs(), nonlocal_only=True, invoice=invoice) return diff --git a/electrum/gui/qt/swap_dialog.py b/electrum/gui/qt/swap_dialog.py index 49680a4af..73a003c04 100644 --- a/electrum/gui/qt/swap_dialog.py +++ b/electrum/gui/qt/swap_dialog.py @@ -27,6 +27,7 @@ from .my_treeview import create_toolbar_with_menu, MyTreeView if TYPE_CHECKING: from .main_window import ElectrumWindow from electrum.submarine_swaps import SwapServerTransport, SwapOffer + from electrum.lnchannel import Channel CANNOT_RECEIVE_WARNING = _( """The requested amount is higher than what you can receive in your currently open channels. @@ -43,7 +44,14 @@ class InvalidSwapParameters(Exception): pass class SwapDialog(WindowModalDialog, QtEventListener): - def __init__(self, window: 'ElectrumWindow', transport: 'SwapServerTransport', is_reverse=None, recv_amount_sat=None, channels=None): + def __init__( + self, + window: 'ElectrumWindow', + transport: 'SwapServerTransport', + is_reverse: Optional[bool] = None, + recv_amount_sat_or_max: Optional[Union[int, str]] = None, # sat or '!' + channels: Optional[Sequence['Channel']] = None, + ): WindowModalDialog.__init__(self, window, _('Submarine Swap')) self.window = window self.config = window.config @@ -120,8 +128,9 @@ class SwapDialog(WindowModalDialog, QtEventListener): buttons = Buttons(CancelButton(self), self.ok_button) vbox.addLayout(buttons) buttons.insertWidget(0, self.server_button) - if recv_amount_sat: - self.init_recv_amount(recv_amount_sat) + if recv_amount_sat_or_max: + assert isinstance(recv_amount_sat_or_max, (int, str)), f"invalid {type(recv_amount_sat_or_max)=}" + self.init_recv_amount(recv_amount_sat_or_max) self.update() self.needs_tx_update = True @@ -321,15 +330,15 @@ class SwapDialog(WindowModalDialog, QtEventListener): self.fee_label.setText(fee_text) self.fee_label.repaint() # macOS hack for #6269 - def run(self, transport): + def run(self, transport: 'SwapServerTransport') -> bool: """Can raise InvalidSwapParameters.""" if not self.exec(): - return + return False if self.is_reverse: lightning_amount = self.send_amount_e.get_amount() onchain_amount = self.recv_amount_e.get_amount() if lightning_amount is None or onchain_amount is None: - return + return False sm = self.swap_manager coro = sm.reverse_swap( transport=transport, @@ -342,17 +351,17 @@ class SwapDialog(WindowModalDialog, QtEventListener): funding_txid = self.window.run_coroutine_dialog(coro, _('Initiating swap...')) except Exception as e: self.window.show_error(f"Reverse swap failed: {str(e)}") - return + return False self.window.on_swap_result(funding_txid, is_reverse=True) return True else: lightning_amount = self.recv_amount_e.get_amount() onchain_amount = self.send_amount_e.get_amount() if lightning_amount is None or onchain_amount is None: - return + return False if lightning_amount > self.lnworker.num_sats_can_receive(): if not self.window.question(CANNOT_RECEIVE_WARNING): - return + return False self.window.protect(self.do_normal_swap, (transport, lightning_amount, onchain_amount)) return True diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py index 6be01fb35..4e6de3511 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -255,7 +255,7 @@ class UTXOList(MyTreeView): def swap_coins(self, coins): #self.clear_coincontrol() self.add_to_coincontrol(coins) - self.main_window.run_swap_dialog(is_reverse=False, recv_amount_sat='!') + self.main_window.run_swap_dialog(is_reverse=False, recv_amount_sat_or_max='!') self.clear_coincontrol() def can_open_channel(self, coins): diff --git a/electrum/lnworker.py b/electrum/lnworker.py index bdf96e5e4..18abdd998 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -2925,10 +2925,10 @@ class LNWallet(LNWorker): # add safety margin delta += delta // 100 + 1 if func(deltas={chan:delta}) >= amount_sat: - suggestions.append((chan, delta)) + suggestions.append((chan, int(delta))) elif direction == RECEIVED and func(deltas={chan:2*delta}) >= amount_sat: # MPP heuristics has a 0.5 slope - suggestions.append((chan, 2*delta)) + suggestions.append((chan, int(2*delta))) if not suggestions: raise NotEnoughFunds return suggestions @@ -3002,7 +3002,7 @@ class LNWallet(LNWorker): return chan, swap_recv_amount return None - def suggest_swap_to_receive(self, amount_sat): + def suggest_swap_to_receive(self, amount_sat: int): assert amount_sat > self.num_sats_can_receive(), f"{amount_sat=} | {self.num_sats_can_receive()=}" try: suggestions = self._suggest_channels_for_rebalance(RECEIVED, amount_sat)