From bc9cc518002514b0ec1cba075dd2a6d6c6b7beb9 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 21 May 2022 11:25:53 +0200 Subject: [PATCH] Add 'channels' parameter to create invoice and pay. Add rebalance dialog to GUI --- electrum/gui/qt/channels_list.py | 14 ++++++++-- electrum/gui/qt/main_window.py | 40 +++++++++++++++++++++++++++ electrum/lnworker.py | 47 ++++++++++++++++++++++++-------- 3 files changed, 88 insertions(+), 13 deletions(-) diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py index c8605c7bc..73d88133a 100644 --- a/electrum/gui/qt/channels_list.py +++ b/electrum/gui/qt/channels_list.py @@ -202,8 +202,18 @@ class ChannelsList(MyTreeView): menu.addAction(_("Import channel backup"), lambda: self.parent.do_process_from_text_channel_backup()) menu.exec_(self.viewport().mapToGlobal(position)) return - multi_select = len(selected) > 1 - if multi_select: + if len(selected) == 2: + idx1 = selected[0] + idx2 = selected[1] + channel_id1 = idx1.sibling(idx1.row(), self.Columns.NODE_ALIAS).data(ROLE_CHANNEL_ID) + channel_id2 = idx2.sibling(idx2.row(), self.Columns.NODE_ALIAS).data(ROLE_CHANNEL_ID) + chan1 = self.lnworker.channels.get(channel_id1) + chan2 = self.lnworker.channels.get(channel_id2) + if chan1 and chan2: + menu.addAction(_("Rebalance"), lambda: self.parent.rebalance_dialog(chan1, chan2)) + menu.exec_(self.viewport().mapToGlobal(position)) + return + elif len(selected) > 2: return idx = self.indexAt(position) if not idx.isValid(): diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 0ae602a6e..4bd261c74 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -3652,3 +3652,43 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): _("Electrum will now exit.")) self.showing_cert_mismatch_error = False self.close() + + def rebalance_dialog(self, chan1, chan2, amount_sat=None): + d = WindowModalDialog(self, _("Rebalance channels")) + d.reverse = False + vbox = QVBoxLayout(d) + vbox.addWidget(WWLabel(_('Rebalance your channels in order to increase your sending or receiving capacity') + ':')) + grid = QGridLayout() + amount_e = BTCAmountEdit(self.get_decimal_point) + amount_e.setAmount(amount_sat) + rev_button = QPushButton(u'\U000021c4') + label1 = QLabel('') + label2 = QLabel('') + def update(): + if d.reverse: + d.chan_from = chan2 + d.chan_to = chan1 + else: + d.chan_from = chan1 + d.chan_to = chan2 + label1.setText(d.chan_from.short_id_for_GUI()) + label2.setText(d.chan_to.short_id_for_GUI()) + update() + def on_reverse(): + d.reverse = not d.reverse + update() + rev_button.clicked.connect(on_reverse) + grid.addWidget(QLabel(_("From")), 0, 0) + grid.addWidget(label1, 0, 1) + grid.addWidget(QLabel(_("To")), 1, 0) + grid.addWidget(label2, 1, 1) + grid.addWidget(QLabel(_("Amount")), 2, 0) + grid.addWidget(amount_e, 2, 1) + grid.addWidget(rev_button, 0, 2) + vbox.addLayout(grid) + vbox.addLayout(Buttons(CancelButton(d), OkButton(d))) + if not d.exec_(): + return + amount_msat = amount_e.get_amount() * 1000 + coro = self.wallet.lnworker.rebalance_channels(d.chan_from, d.chan_to, amount_msat=amount_msat) + self.run_coroutine_from_thread(coro) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 0e549f1ad..6a69b7732 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -1103,7 +1103,9 @@ class LNWallet(LNWorker): self, invoice: str, *, amount_msat: int = None, attempts: int = None, # used only in unit tests - full_path: LNPaymentPath = None) -> Tuple[bool, List[HtlcLog]]: + full_path: LNPaymentPath = None, + channels: Optional[Sequence[Channel]] = None, + ) -> Tuple[bool, List[HtlcLog]]: lnaddr = self._check_invoice(invoice, amount_msat=amount_msat) min_cltv_expiry = lnaddr.get_min_final_cltv_expiry() @@ -1138,7 +1140,8 @@ class LNWallet(LNWorker): r_tags=r_tags, invoice_features=invoice_features, attempts=attempts, - full_path=full_path) + full_path=full_path, + channels=channels) success = True except PaymentFailure as e: self.logger.info(f'payment failure: {e!r}') @@ -1167,7 +1170,9 @@ class LNWallet(LNWorker): full_path: LNPaymentPath = None, fwd_trampoline_onion=None, fwd_trampoline_fee=None, - fwd_trampoline_cltv_delta=None) -> None: + fwd_trampoline_cltv_delta=None, + channels: Optional[Sequence[Channel]] = None, + ) -> None: if fwd_trampoline_onion: # todo: compare to the fee of the actual route we found @@ -1204,7 +1209,8 @@ class LNWallet(LNWorker): payment_secret=payment_secret, trampoline_fee_level=trampoline_fee_level, use_two_trampolines=use_two_trampolines, - fwd_trampoline_onion=fwd_trampoline_onion + fwd_trampoline_onion=fwd_trampoline_onion, + channels=channels, ) # 2. send htlcs async for route, amount_msat, total_msat, amount_receiver_msat, cltv_delta, bucket_payment_secret, trampoline_onion in routes: @@ -1493,7 +1499,9 @@ class LNWallet(LNWorker): trampoline_fee_level: int, use_two_trampolines: bool, fwd_trampoline_onion=None, - full_path: LNPaymentPath = None) -> AsyncGenerator[Tuple[LNPaymentRoute, int], None]: + full_path: LNPaymentPath = None, + channels: Optional[Sequence[Channel]] = None, + ) -> AsyncGenerator[Tuple[LNPaymentRoute, int], None]: """Creates multiple routes for splitting a payment over the available private channels. @@ -1503,8 +1511,12 @@ class LNWallet(LNWorker): invoice_features = LnFeatures(invoice_features) trampoline_features = LnFeatures.VAR_ONION_OPT local_height = self.network.get_local_height() - my_active_channels = [chan for chan in self.channels.values() if - chan.is_active() and not chan.is_frozen_for_sending()] + if channels: + my_active_channels = channels + else: + my_active_channels = [ + chan for chan in self.channels.values() if + chan.is_active() and not chan.is_frozen_for_sending()] try: self.logger.info("trying single-part payment") # try to send over a single channel @@ -1760,11 +1772,12 @@ class LNWallet(LNWorker): expiry: int, fallback_address: str, write_to_disk: bool = True, + channels: Optional[Sequence[Channel]] = None, ) -> Tuple[LnAddr, str]: assert amount_msat is None or amount_msat > 0 timestamp = int(time.time()) - routing_hints, trampoline_hints = self.calc_routing_hints_for_invoice(amount_msat) + routing_hints, trampoline_hints = self.calc_routing_hints_for_invoice(amount_msat, channels=channels) if not routing_hints: self.logger.info( "Warning. No routing hints added to invoice. " @@ -1993,11 +2006,12 @@ class LNWallet(LNWorker): self.set_invoice_status(key, PR_UNPAID) util.trigger_callback('payment_failed', self.wallet, key, '') - def calc_routing_hints_for_invoice(self, amount_msat: Optional[int]): + def calc_routing_hints_for_invoice(self, amount_msat: Optional[int], channels=None): """calculate routing hints (BOLT-11 'r' field)""" routing_hints = [] - channels = list(self.get_channels_to_include_in_invoice(amount_msat)) - random.shuffle(channels) # let's not leak channel order + if channels is None: + channels = list(self.get_channels_to_include_in_invoice(amount_msat)) + random.shuffle(channels) # let's not leak channel order scid_to_my_channels = {chan.short_channel_id: chan for chan in channels if chan.short_channel_id is not None} for chan in channels: @@ -2119,6 +2133,17 @@ class LNWallet(LNWorker): ) return Decimal(can_receive_msat) / 1000 + async def rebalance_channels(self, chan1, chan2, amount_msat): + lnaddr, invoice = self.create_invoice( + amount_msat=amount_msat, + message='rebalance', + expiry=3600, + fallback_address=None, + channels = [chan2] + ) + await self.pay_invoice( + invoice, channels=[chan1]) + def num_sats_can_receive_no_mpp(self) -> Decimal: with self.lock: channels = [