diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py index be9561711..5f84cc76c 100644 --- a/electrum/gui/qt/channels_list.py +++ b/electrum/gui/qt/channels_list.py @@ -71,6 +71,7 @@ class ChannelsList(MyTreeView): self.network = self.parent.network self.wallet = self.parent.wallet self.setSortingEnabled(True) + self.selectionModel().selectionChanged.connect(self.on_selection_changed) @property # property because lnworker might be initialized at runtime @@ -194,14 +195,8 @@ class ChannelsList(MyTreeView): msg = messages.MSG_NON_TRAMPOLINE_CHANNEL_FROZEN_WITHOUT_GOSSIP self.main_window.show_warning(msg, title=_('Channel is frozen for sending')) - def create_menu(self, position): - menu = QMenu() - menu.setSeparatorsCollapsible(True) # consecutive separators are merged together + def get_rebalance_pair(self): selected = self.selected_in_column(self.Columns.NODE_ALIAS) - if not selected: - menu.addAction(_("Import channel backup"), lambda: self.parent.do_process_from_text_channel_backup()) - menu.exec_(self.viewport().mapToGlobal(position)) - return if len(selected) == 2: idx1 = selected[0] idx2 = selected[1] @@ -210,6 +205,28 @@ class ChannelsList(MyTreeView): chan1 = self.lnworker.channels.get(channel_id1) chan2 = self.lnworker.channels.get(channel_id2) if chan1 and chan2 and (self.lnworker.channel_db or chan1.node_id != chan2.node_id): + return chan1, chan2 + return None, None + + def on_rebalance(self): + chan1, chan2 = self.get_rebalance_pair() + self.parent.rebalance_dialog(chan1, chan2) + + def on_selection_changed(self): + chan1, chan2 = self.get_rebalance_pair() + self.rebalance_button.setEnabled(chan1 is not None) + + def create_menu(self, position): + menu = QMenu() + menu.setSeparatorsCollapsible(True) # consecutive separators are merged together + selected = self.selected_in_column(self.Columns.NODE_ALIAS) + if not selected: + menu.addAction(_("Import channel backup"), lambda: self.parent.do_process_from_text_channel_backup()) + menu.exec_(self.viewport().mapToGlobal(position)) + return + if len(selected) == 2: + chan1, chan2 = self.get_rebalance_pair() + if chan1 and chan2: menu.addAction(_("Rebalance"), lambda: self.parent.rebalance_dialog(chan1, chan2)) menu.exec_(self.viewport().mapToGlobal(position)) return @@ -356,12 +373,16 @@ class ChannelsList(MyTreeView): self.can_send_label = QLabel('') h.addWidget(self.can_send_label) h.addStretch() + self.rebalance_button = EnterButton(_('Rebalance'), lambda x: self.on_rebalance()) + self.rebalance_button.setToolTip("Select two active channels to rebalance.") + self.rebalance_button.setDisabled(True) self.swap_button = EnterButton(_('Swap'), lambda x: self.parent.run_swap_dialog()) self.swap_button.setToolTip("Have at least one channel to do swaps.") self.swap_button.setDisabled(True) self.new_channel_button = EnterButton(_('Open Channel'), self.new_channel_with_warning) self.new_channel_button.setEnabled(self.parent.wallet.has_lightning()) h.addWidget(self.new_channel_button) + h.addWidget(self.rebalance_button) h.addWidget(self.swap_button) return h diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 5ba1037ff..cc862711c 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -3725,42 +3725,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): 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_(): + from .rebalance_dialog import RebalanceDialog + if chan1 is None or chan2 is None: 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, _('Rebalancing channels')) - self.update_current_request() # this will gray out the button + d = RebalanceDialog(self, chan1, chan2, amount_sat) + d.run() diff --git a/electrum/gui/qt/rebalance_dialog.py b/electrum/gui/qt/rebalance_dialog.py new file mode 100644 index 000000000..527eab48e --- /dev/null +++ b/electrum/gui/qt/rebalance_dialog.py @@ -0,0 +1,69 @@ +from PyQt5.QtCore import pyqtSignal +from PyQt5.QtWidgets import QLabel, QVBoxLayout, QGridLayout, QPushButton + +from electrum.i18n import _ +from .util import WindowModalDialog, Buttons, OkButton, CancelButton, WWLabel +from .amountedit import BTCAmountEdit + + +class RebalanceDialog(WindowModalDialog): + + def __init__(self, window, chan1, chan2, amount_sat): + WindowModalDialog.__init__(self, window, _("Rebalance channels")) + self.window = window + self.wallet = window.wallet + self.chan1 = chan1 + self.chan2 = chan2 + vbox = QVBoxLayout(self) + vbox.addWidget(WWLabel(_('Rebalance your channels in order to increase your sending or receiving capacity') + ':')) + grid = QGridLayout() + self.amount_e = BTCAmountEdit(self.window.get_decimal_point) + self.amount_e.setAmount(amount_sat) + self.amount_e.textChanged.connect(self.on_amount) + self.rev_button = QPushButton(u'\U000021c4') + self.rev_button.clicked.connect(self.on_reverse) + self.max_button = QPushButton('Max') + self.max_button.clicked.connect(self.on_max) + self.label1 = QLabel('') + self.label2 = QLabel('') + self.ok_button = OkButton(self) + self.ok_button.setEnabled(False) + grid.addWidget(QLabel(_("From channel")), 0, 0) + grid.addWidget(self.label1, 0, 1) + grid.addWidget(QLabel(_("To channel")), 1, 0) + grid.addWidget(self.label2, 1, 1) + grid.addWidget(QLabel(_("Amount")), 2, 0) + grid.addWidget(self.amount_e, 2, 1) + grid.addWidget(self.max_button, 2, 2) + grid.addWidget(self.rev_button, 0, 2) + vbox.addLayout(grid) + vbox.addLayout(Buttons(CancelButton(self), self.ok_button)) + self.update() + + def on_reverse(self, x): + a, b = self.chan1, self.chan2 + self.chan1, self.chan2 = b, a + self.amount_e.setAmount(None) + self.update() + + def on_amount(self, x): + self.update() + + def on_max(self, x): + n_sat = self.wallet.lnworker.num_sats_can_rebalance(self.chan1, self.chan2) + self.amount_e.setAmount(n_sat) + + def update(self): + self.label1.setText(self.chan1.short_id_for_GUI()) + self.label2.setText(self.chan2.short_id_for_GUI()) + amount_sat = self.amount_e.get_amount() + b = bool(amount_sat) and self.wallet.lnworker.num_sats_can_rebalance(self.chan1, self.chan2) <= amount_sat + self.ok_button.setEnabled(b) + + def run(self): + if not self.exec_(): + return + amount_msat = self.amount_e.get_amount() * 1000 + coro = self.wallet.lnworker.rebalance_channels(self.chan1, self.chan2, amount_msat=amount_msat) + self.window.run_coroutine_from_thread(coro, _('Rebalancing channels')) + self.window.update_current_request() # this will gray out the button diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index bcbbad72c..fa29789dd 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -558,6 +558,9 @@ class Channel(AbstractChannel): forwarding_fee_base_msat = 1000 forwarding_fee_proportional_millionths = 1 + def __repr__(self): + return "Channel(%s)"%self.get_id_for_log() + def __init__(self, state: 'StoredDict', *, sweep_address=None, name=None, lnworker=None, initial_feerate=None): self.name = name self.channel_id = bfh(state["channel_id"]) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index b550886c5..f61a3a460 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -2232,6 +2232,14 @@ class LNWallet(LNWorker): else: return False + def num_sats_can_rebalance(self, chan1, chan2): + # TODO: we should be able to spend 'max', with variable fee + n1 = chan1.available_to_spend(LOCAL) + n1 -= self.fee_estimate(n1) + n2 = chan2.available_to_spend(REMOTE) + amount_sat = min(n1, n2) // 1000 + return amount_sat + def suggest_rebalance_to_send(self, amount_sat): return self._suggest_rebalance(SENT, amount_sat)