diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py index f02f15161..c200f900d 100644 --- a/electrum/gui/qt/channels_list.py +++ b/electrum/gui/qt/channels_list.py @@ -15,9 +15,8 @@ from electrum.util import bh2u, NotEnoughFunds, NoDynamicFeeEstimates from electrum.i18n import _ from electrum.lnchannel import AbstractChannel, PeerState, ChannelBackup, Channel, ChannelState from electrum.wallet import Abstract_Wallet -from electrum.lnutil import LOCAL, REMOTE, format_short_channel_id, LN_MAX_FUNDING_SAT +from electrum.lnutil import LOCAL, REMOTE, format_short_channel_id from electrum.lnworker import LNWallet -from electrum import ecc from electrum.gui import messages from .util import (MyTreeView, WindowModalDialog, Buttons, OkButton, CancelButton, @@ -384,105 +383,9 @@ class ChannelsList(MyTreeView): d.exec_() def new_channel_dialog(self, *, amount_sat=None): - lnworker = self.parent.wallet.lnworker - d = WindowModalDialog(self.parent, _('Open Channel')) - vbox = QVBoxLayout(d) - if self.parent.network.channel_db: - vbox.addWidget(QLabel(_('Enter Remote Node ID or connection string or invoice'))) - remote_nodeid = QLineEdit() - remote_nodeid.setMinimumWidth(700) - suggest_button = QPushButton(d, text=_('Suggest Peer')) - def on_suggest(): - self.parent.wallet.network.start_gossip() - nodeid = bh2u(lnworker.suggest_peer() or b'') - if not nodeid: - remote_nodeid.setText("") - remote_nodeid.setPlaceholderText( - "Please wait until the graph is synchronized to 30%, and then try again.") - else: - remote_nodeid.setText(nodeid) - remote_nodeid.repaint() # macOS hack for #6269 - suggest_button.clicked.connect(on_suggest) - else: - from electrum.lnworker import hardcoded_trampoline_nodes - vbox.addWidget(QLabel(_('Choose a trampoline node to open a channel with'))) - trampolines = hardcoded_trampoline_nodes() - trampoline_names = list(trampolines.keys()) - trampoline_combo = QComboBox() - trampoline_combo.addItems(trampoline_names) - trampoline_combo.setCurrentIndex(1) - - amount_e = BTCAmountEdit(self.parent.get_decimal_point) - amount_e.setAmount(amount_sat) - # max button - def spend_max(): - amount_e.setFrozen(max_button.isChecked()) - if not max_button.isChecked(): - return - dummy_nodeid = ecc.GENERATOR.get_public_key_bytes(compressed=True) - make_tx = self.parent.mktx_for_open_channel(funding_sat='!', node_id=dummy_nodeid) - try: - tx = make_tx(None) - except (NotEnoughFunds, NoDynamicFeeEstimates) as e: - max_button.setChecked(False) - amount_e.setFrozen(False) - self.main_window.show_error(str(e)) - return - amount = tx.output_value() - amount = min(amount, LN_MAX_FUNDING_SAT) - amount_e.setAmount(amount) - max_button = EnterButton(_("Max"), spend_max) - max_button.setFixedWidth(100) - max_button.setCheckable(True) - - clear_button = QPushButton(d, text=_('Clear')) - def on_clear(): - amount_e.setText('') - amount_e.setFrozen(False) - amount_e.repaint() # macOS hack for #6269 - if self.parent.network.channel_db: - remote_nodeid.setText('') - remote_nodeid.repaint() # macOS hack for #6269 - max_button.setChecked(False) - max_button.repaint() # macOS hack for #6269 - clear_button.clicked.connect(on_clear) - clear_button.setFixedWidth(100) - h = QGridLayout() - if self.parent.network.channel_db: - h.addWidget(QLabel(_('Remote Node ID')), 0, 0) - h.addWidget(remote_nodeid, 0, 1, 1, 4) - h.addWidget(suggest_button, 0, 5) - else: - h.addWidget(QLabel(_('Trampoline')), 0, 0) - h.addWidget(trampoline_combo, 0, 1, 1, 4) - - h.addWidget(QLabel('Amount'), 2, 0) - h.addWidget(amount_e, 2, 1) - h.addWidget(max_button, 2, 2) - h.addWidget(clear_button, 2, 3) - vbox.addLayout(h) - vbox.addStretch() - ok_button = OkButton(d) - ok_button.setDefault(True) - vbox.addLayout(Buttons(CancelButton(d), ok_button)) - if not d.exec_(): - return - if max_button.isChecked() and amount_e.get_amount() < LN_MAX_FUNDING_SAT: - # if 'max' enabled and amount is strictly less than max allowed, - # that means we have fewer coins than max allowed, and hence we can - # spend all coins - funding_sat = '!' - else: - funding_sat = amount_e.get_amount() - if self.parent.network.channel_db: - connect_str = str(remote_nodeid.text()).strip() - else: - name = trampoline_names[trampoline_combo.currentIndex()] - connect_str = str(trampolines[name]) - if not connect_str or not funding_sat: - return - self.parent.open_channel(connect_str, funding_sat, 0) - return True + from .new_channel_dialog import NewChannelDialog + d = NewChannelDialog(self.parent, amount_sat) + return d.run() def swap_dialog(self): from .swap_dialog import SwapDialog diff --git a/electrum/gui/qt/new_channel_dialog.py b/electrum/gui/qt/new_channel_dialog.py new file mode 100644 index 000000000..304f8fb52 --- /dev/null +++ b/electrum/gui/qt/new_channel_dialog.py @@ -0,0 +1,126 @@ +from typing import TYPE_CHECKING, Optional + +from PyQt5.QtWidgets import QLabel, QVBoxLayout, QGridLayout, QPushButton, QComboBox, QLineEdit + +from electrum.i18n import _ +from electrum.transaction import PartialTxOutput, PartialTransaction +from electrum.lnutil import LN_MAX_FUNDING_SAT +from electrum.lnworker import hardcoded_trampoline_nodes +from electrum import ecc + + +from .util import (WindowModalDialog, Buttons, OkButton, CancelButton, + EnterButton, ColorScheme, WWLabel, read_QIcon, IconLabel) +from .amountedit import BTCAmountEdit + + +if TYPE_CHECKING: + from .main_window import ElectrumWindow + + + +class NewChannelDialog(WindowModalDialog): + + def __init__(self, window: 'ElectrumWindow', amount_sat: Optional[int] = None): + WindowModalDialog.__init__(self, window, _('Open Channel')) + self.window = window + self.network = window.network + self.config = window.config + self.lnworker = self.window.wallet.lnworker + self.trampolines = hardcoded_trampoline_nodes() + self.trampoline_names = list(self.trampolines.keys()) + vbox = QVBoxLayout(self) + if self.network.channel_db: + vbox.addWidget(QLabel(_('Enter Remote Node ID or connection string or invoice'))) + self.remote_nodeid = QLineEdit() + self.remote_nodeid.setMinimumWidth(700) + self.suggest_button = QPushButton(self, text=_('Suggest Peer')) + self.suggest_button.clicked.connect(self.on_suggest) + else: + vbox.addWidget(QLabel(_('Choose a trampoline node to open a channel with'))) + self.trampoline_combo = QComboBox() + self.trampoline_combo.addItems(self.trampoline_names) + self.trampoline_combo.setCurrentIndex(1) + self.amount_e = BTCAmountEdit(self.window.get_decimal_point) + self.amount_e.setAmount(amount_sat) + self.max_button = EnterButton(_("Max"), self.spend_max) + self.max_button.setFixedWidth(100) + self.max_button.setCheckable(True) + self.clear_button = QPushButton(self, text=_('Clear')) + self.clear_button.clicked.connect(self.on_clear) + self.clear_button.setFixedWidth(100) + h = QGridLayout() + if self.network.channel_db: + h.addWidget(QLabel(_('Remote Node ID')), 0, 0) + h.addWidget(self.remote_nodeid, 0, 1, 1, 4) + h.addWidget(self.suggest_button, 0, 5) + else: + h.addWidget(QLabel(_('Trampoline')), 0, 0) + h.addWidget(self.trampoline_combo, 0, 1, 1, 4) + h.addWidget(QLabel('Amount'), 2, 0) + h.addWidget(self.amount_e, 2, 1) + h.addWidget(self.max_button, 2, 2) + h.addWidget(self.clear_button, 2, 3) + vbox.addLayout(h) + vbox.addStretch() + ok_button = OkButton(self) + ok_button.setDefault(True) + vbox.addLayout(Buttons(CancelButton(self), ok_button)) + + def on_suggest(self): + self.network.start_gossip() + nodeid = self.lnworker.suggest_peer().hex() or '' + if not nodeid: + self.remote_nodeid.setText("") + self.remote_nodeid.setPlaceholderText( + "Please wait until the graph is synchronized to 30%, and then try again.") + else: + self.remote_nodeid.setText(nodeid) + self.remote_nodeid.repaint() # macOS hack for #6269 + + def on_clear(self): + self.amount_e.setText('') + self.amount_e.setFrozen(False) + self.amount_e.repaint() # macOS hack for #6269 + if self.network.channel_db: + remote_nodeid.setText('') + remote_nodeid.repaint() # macOS hack for #6269 + self.max_button.setChecked(False) + self.max_button.repaint() # macOS hack for #6269 + + def spend_max(self): + self.amount_e.setFrozen(self.max_button.isChecked()) + if not self.max_button.isChecked(): + return + dummy_nodeid = ecc.GENERATOR.get_public_key_bytes(compressed=True) + make_tx = self.window.mktx_for_open_channel(funding_sat='!', node_id=dummy_nodeid) + try: + tx = make_tx(None) + except (NotEnoughFunds, NoDynamicFeeEstimates) as e: + max_button.setChecked(False) + self.amount_e.setFrozen(False) + self.main_window.show_error(str(e)) + return + amount = tx.output_value() + amount = min(amount, LN_MAX_FUNDING_SAT) + self.amount_e.setAmount(amount) + + def run(self): + if not self.exec_(): + return + if self.max_button.isChecked() and self.amount_e.get_amount() < LN_MAX_FUNDING_SAT: + # if 'max' enabled and amount is strictly less than max allowed, + # that means we have fewer coins than max allowed, and hence we can + # spend all coins + funding_sat = '!' + else: + funding_sat = self.amount_e.get_amount() + if self.network.channel_db: + connect_str = str(self.remote_nodeid.text()).strip() + else: + name = self.trampoline_names[self.trampoline_combo.currentIndex()] + connect_str = str(self.trampolines[name]) + if not connect_str or not funding_sat: + return + self.window.open_channel(connect_str, funding_sat, 0) + return True