Skip to content

Commit 46eb6e1

Browse files
committed
qt: improve send_change_to_lightning feedback
Improves the GUI feedback given to the user when sending change to lightning by showing different failure messages and the additional fee charged for the swap. Also considers the min/max swap amount announced by the provider when deciding to add the swap dummy output in make tx for improved reliability.
1 parent f4e7ffd commit 46eb6e1

File tree

5 files changed

+82
-28
lines changed

5 files changed

+82
-28
lines changed

electrum/gui/qt/confirm_tx_dialog.py

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@ def cb():
410410
self.resize_to_fit_content()
411411
self.pref_menu.addConfig(self.config.cv.GUI_QT_TX_EDITOR_SHOW_LOCKTIME, callback=cb)
412412
self.pref_menu.addSeparator()
413-
self.pref_menu.addConfig(self.config.cv.WALLET_SEND_CHANGE_TO_LIGHTNING, callback=self.trigger_update)
413+
self.pref_menu.addConfig(self.config.cv.WALLET_SEND_CHANGE_TO_LIGHTNING, callback=self.toggle_send_change_to_ln)
414414
self.pref_menu.addToggle(
415415
_('Use change addresses'),
416416
self.toggle_use_change,
@@ -459,6 +459,15 @@ def toggle_multiple_change(self):
459459
self.wallet.db.put('multiple_change', self.wallet.multiple_change)
460460
self.trigger_update()
461461

462+
def toggle_send_change_to_ln(self):
463+
"""Toggling send_change_to_ln needs to restart the TxDialog as make_tx depends on the
464+
NostrTransport being available"""
465+
self.close()
466+
state = _("Enabled") if self.config.WALLET_SEND_CHANGE_TO_LIGHTNING else _("Disabled")
467+
self.show_message(
468+
msg=_("{} sending change to lightning.").format(state)
469+
)
470+
462471
def set_io_visible(self):
463472
self.io_widget.setVisible(self.config.GUI_QT_TX_EDITOR_SHOW_IO)
464473

@@ -543,6 +552,21 @@ def get_messages(self):
543552
self.error = _('Fee estimates not available. Please set a fixed fee or feerate.')
544553
if self.tx.get_dummy_output(DummyAddress.SWAP):
545554
messages.append(_('This transaction will send funds to a submarine swap.'))
555+
elif self.config.WALLET_SEND_CHANGE_TO_LIGHTNING and self.tx.has_change():
556+
swap_msg = _('Will not send change to lightning: ')
557+
if not self.wallet.has_lightning():
558+
swap_msg += _('Lightning is not enabled.')
559+
elif sum(o.value for o in self.tx.get_change_outputs()) > self.wallet.lnworker.num_sats_can_receive():
560+
swap_msg += _("Your channels cannot receive this amount.")
561+
elif self.wallet.lnworker.swap_manager.is_initialized.is_set():
562+
# we could display the limits here but then the dialog gets very convoluted
563+
swap_msg += _('Change amount outside of your swap providers limits.')
564+
else:
565+
# If initialization of the swap manager failed they already got an error
566+
# popup before, so they should know. It might also be a channel open.
567+
swap_msg = None
568+
if swap_msg:
569+
messages.append(swap_msg)
546570
# warn if spending unconf
547571
if any((txin.block_height is not None and txin.block_height<=0) for txin in self.tx.inputs()):
548572
messages.append(_('This transaction will spend unconfirmed coins.'))
@@ -707,9 +731,22 @@ def create_grid(self):
707731
return grid
708732

709733
def _update_extra_fees(self):
710-
x_fee = run_hook('get_tx_extra_fee', self.wallet, self.tx)
711-
if x_fee:
712-
x_fee_address, x_fee_amount = x_fee
734+
x_fee_total = 0
735+
736+
# fetch plugin extra fees
737+
x_fee_plugin = run_hook('get_tx_extra_fee', self.wallet, self.tx)
738+
if x_fee_plugin:
739+
_x_fee_address_plugin, x_fee_amount_plugin = x_fee_plugin
740+
x_fee_total += x_fee_amount_plugin
741+
742+
# this is a forward swap (send change to lightning)
743+
if dummy_output := self.tx.get_dummy_output(DummyAddress.SWAP):
744+
sm = self.wallet.lnworker.swap_manager
745+
ln_amount_we_recv = sm.get_recv_amount(send_amount=dummy_output.value, is_reverse=False)
746+
if ln_amount_we_recv is not None:
747+
x_fee_total += dummy_output.value - ln_amount_we_recv
748+
749+
if x_fee_total > 0:
713750
self.extra_fee_label.setVisible(True)
714751
self.extra_fee_value.setVisible(True)
715-
self.extra_fee_value.setText(self.main_window.format_amount_and_units(x_fee_amount))
752+
self.extra_fee_value.setText(self.main_window.format_amount_and_units(x_fee_total))

electrum/gui/qt/main_window.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1326,7 +1326,7 @@ async def wait_until_initialized():
13261326
except asyncio.TimeoutError:
13271327
return
13281328
try:
1329-
self.run_coroutine_dialog(wait_until_initialized(), _('Please wait...'))
1329+
self.run_coroutine_dialog(wait_until_initialized(), _('Fetching swap providers...'))
13301330
except UserCancelled:
13311331
return False
13321332
except Exception as e:
@@ -1335,6 +1335,7 @@ async def wait_until_initialized():
13351335

13361336
if not sm.is_initialized.is_set():
13371337
if not self.config.SWAPSERVER_URL:
1338+
assert isinstance(transport, NostrTransport)
13381339
if not self.choose_swapserver_dialog(transport):
13391340
return False
13401341
else:

electrum/gui/qt/send_tab.py

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from electrum.payment_identifier import (PaymentIdentifierType, PaymentIdentifier,
2525
invoice_from_payment_identifier,
2626
payment_identifier_from_invoice, PaymentIdentifierState)
27-
from electrum.submarine_swaps import SwapServerError
27+
from electrum.submarine_swaps import SwapServerError, SwapServerTransport
2828
from electrum.fee_policy import FeePolicy, FixedFeePolicy
2929
from electrum.lnurl import LNURL3Data, request_lnurl_withdraw_callback, LNURLError
3030

@@ -295,17 +295,31 @@ def spend_max(self):
295295
msg += "\n" + _("Some coins are frozen: {} (can be unfrozen in the Addresses or in the Coins tab)").format(frozen_bal)
296296
QToolTip.showText(self.max_button.mapToGlobal(QPoint(0, 0)), msg)
297297

298+
@staticmethod
299+
def maybe_pass_swap_transport(func):
300+
def wrapper(self, *args, **kwargs):
301+
assert isinstance(self, SendTab)
302+
if self.config.WALLET_SEND_CHANGE_TO_LIGHTNING and not kwargs.get('swap_transport'):
303+
with self.window.create_sm_transport() as transport:
304+
if self.window.initialize_swap_manager(transport):
305+
kwargs['swap_transport'] = transport
306+
return func(self, *args, **kwargs)
307+
return func(self, *args, **kwargs)
308+
return wrapper
309+
298310
# TODO: instead of passing outputs, use an invoice instead (like pay_lightning_invoice)
299311
# so we have more context (we cannot rely on send_tab field contents or payment identifier
300312
# as this method is called from other places as well).
313+
@maybe_pass_swap_transport
301314
def pay_onchain_dialog(
302315
self,
303316
outputs: List[PartialTxOutput],
304317
*,
305318
nonlocal_only=False,
306319
external_keypairs: Mapping[bytes, bytes] = None,
307320
get_coins: Callable[..., Sequence[PartialTxInput]] = None,
308-
invoice: Optional[Invoice] = None
321+
invoice: Optional[Invoice] = None,
322+
swap_transport: Optional['SwapServerTransport'] = None,
309323
) -> None:
310324
# trustedcoin requires this
311325
if run_hook('abort_send', self):
@@ -324,7 +338,7 @@ def make_tx(fee_policy, *, confirmed_only=False, base_tx=None):
324338
outputs=outputs,
325339
base_tx=base_tx,
326340
is_sweep=is_sweep,
327-
send_change_to_lightning=self.config.WALLET_SEND_CHANGE_TO_LIGHTNING,
341+
send_change_to_lightning=bool(swap_transport),
328342
merge_duplicate_outputs=self.config.WALLET_MERGE_DUPLICATE_OUTPUTS,
329343
)
330344
output_values = [x.value for x in outputs]
@@ -345,22 +359,20 @@ def make_tx(fee_policy, *, confirmed_only=False, base_tx=None):
345359
return
346360

347361
if swap_dummy_output := tx.get_dummy_output(DummyAddress.SWAP):
348-
sm = self.wallet.lnworker.swap_manager
349-
with self.window.create_sm_transport() as transport:
350-
if not self.window.initialize_swap_manager(transport):
351-
return
352-
coro = sm.request_swap_for_amount(transport=transport, onchain_amount=swap_dummy_output.value)
353-
try:
354-
swap, swap_invoice = self.window.run_coroutine_dialog(coro, _('Requesting swap invoice...'))
355-
except (SwapServerError, UserFacingException) as e:
356-
self.show_error(str(e))
357-
return
358-
except UserCancelled:
359-
return
360-
tx.replace_output_address(DummyAddress.SWAP, swap.lockup_address)
361-
assert tx.get_dummy_output(DummyAddress.SWAP) is None
362-
tx.swap_invoice = swap_invoice
363-
tx.swap_payment_hash = swap.payment_hash
362+
assert swap_transport and swap_transport.sm.is_initialized.is_set()
363+
sm = swap_transport.sm
364+
coro = sm.request_forward_swap_for_amount(transport=swap_transport, onchain_amount=swap_dummy_output.value)
365+
try:
366+
swap, swap_invoice = self.window.run_coroutine_dialog(coro, _('Requesting swap invoice...'))
367+
except (SwapServerError, UserFacingException) as e:
368+
self.show_error(str(e))
369+
return
370+
except UserCancelled:
371+
return
372+
tx.replace_output_address(DummyAddress.SWAP, swap.lockup_address)
373+
assert tx.get_dummy_output(DummyAddress.SWAP) is None
374+
tx.swap_invoice = swap_invoice
375+
tx.swap_payment_hash = swap.payment_hash
364376

365377
if is_preview:
366378
self.window.show_transaction(tx, external_keypairs=external_keypairs, invoice=invoice)

electrum/submarine_swaps.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -982,7 +982,7 @@ def create_funding_tx(
982982
return tx
983983

984984
@log_exceptions
985-
async def request_swap_for_amount(
985+
async def request_forward_swap_for_amount(
986986
self,
987987
*,
988988
transport: 'SwapServerTransport',

electrum/wallet.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2059,10 +2059,14 @@ def fee_estimator(size: Union[int, float, Decimal]) -> int:
20592059
dust_threshold=self.dust_threshold(),
20602060
BIP69_sort=BIP69_sort)
20612061
if self.lnworker and send_change_to_lightning:
2062+
sm = self.lnworker.swap_manager
2063+
assert sm and sm.is_initialized.is_set(), "swap manager should be initialized here"
2064+
min_swap_amnt, max_swap_amnt = sm.get_min_amount(), sm.get_provider_max_reverse_amount()
20622065
change = tx.get_change_outputs()
2063-
if len(change) == 1:
2066+
assert len(change) <= 1, len(change)
2067+
if len(change) == 1: # tx creates change
20642068
amount = change[0].value
2065-
if amount <= self.lnworker.num_sats_can_receive():
2069+
if min_swap_amnt <= amount <= min(self.lnworker.num_sats_can_receive(), max_swap_amnt):
20662070
tx.replace_output_address(change[0].address, DummyAddress.SWAP)
20672071
if self.should_keep_reserve_utxo(tx.inputs(), tx.outputs(), is_anchor_channel_opening):
20682072
raise NotEnoughFunds()

0 commit comments

Comments
 (0)