Merge pull request #10188 from f321x/capacity_warning_zero_amount
qt: receive dialog: also propose swaps for 0 amount invoices
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user