diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index bfef3e3b1..9fbbcb5d9 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -108,6 +108,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): @@ -1259,32 +1260,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 3c1adcced..734953430 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -719,7 +719,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 19de6be8d..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 @@ -81,9 +89,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) @@ -123,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 @@ -282,7 +288,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}") @@ -324,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, @@ -345,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 778b1bd8f..c2db82f61 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 8139e6588..c4409cbc2 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,8 +3002,8 @@ class LNWallet(LNWorker): return chan, swap_recv_amount return None - def suggest_swap_to_receive(self, amount_sat): - assert amount_sat > self.num_sats_can_receive() + 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) except NotEnoughFunds: diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index 0adf5d887..5112b18b4 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 @@ -1136,7 +1137,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) diff --git a/electrum/wallet.py b/electrum/wallet.py index e0ef00641..711e10437 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 @@ -3438,7 +3439,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: @@ -3478,7 +3480,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: @@ -3492,11 +3494,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