1
0
Files
electrum/electrum/gui/qt/new_channel_dialog.py
f321x b4e93e7e38 fix: prevent lnrater from blocking if no good peers
the while loop in `suggest_node_channel_open()` of lnrater would not
break if there are no "good" peers available available. As a result the gui
blocks and electrum has to be killed. This can happen for example on
signet.
This removes the tested pk from the list of candidates so each candidate
gets tested only once.
2025-06-03 10:06:39 +02:00

159 lines
6.7 KiB
Python

from typing import TYPE_CHECKING, Optional
from PyQt6.QtWidgets import QLabel, QVBoxLayout, QGridLayout, QPushButton, QComboBox, QLineEdit, QHBoxLayout
import electrum_ecc as ecc
from electrum.i18n import _
from electrum.lnutil import MIN_FUNDING_SAT
from electrum.lnworker import hardcoded_trampoline_nodes
from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates
from electrum.fee_policy import FeePolicy
from .util import (WindowModalDialog, Buttons, OkButton, CancelButton,
EnterButton, WWLabel, char_width_in_lineedit)
from .amountedit import BTCAmountEdit
from .my_treeview import create_toolbar_with_menu
if TYPE_CHECKING:
from .main_window import ElectrumWindow
class NewChannelDialog(WindowModalDialog):
def __init__(self, window: 'ElectrumWindow', amount_sat: Optional[int] = None, min_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())
self.min_amount_sat = min_amount_sat or MIN_FUNDING_SAT
vbox = QVBoxLayout(self)
toolbar, menu = create_toolbar_with_menu(self.config, '')
menu.addConfig(
self.config.cv.LIGHTNING_USE_RECOVERABLE_CHANNELS,
checked=self.lnworker.has_recoverable_channels(),
).setEnabled(self.lnworker.can_have_recoverable_channels())
vbox.addLayout(toolbar)
msg = _('Choose a remote node and an amount to fund the channel.')
msg += '\n' + _('Minimum required amount: {}').format(self.window.format_amount_and_units(self.min_amount_sat))
vbox.addWidget(WWLabel(msg))
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:
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)
btn_width = 10 * char_width_in_lineedit()
self.min_button = EnterButton(_("Min"), self.spend_min)
self.min_button.setEnabled(bool(self.min_amount_sat))
self.min_button.setFixedWidth(btn_width)
self.max_button = EnterButton(_("Max"), self.spend_max)
self.max_button.setFixedWidth(btn_width)
self.max_button.setCheckable(True)
self.clear_button = QPushButton(self, text=_('Clear'))
self.clear_button.clicked.connect(self.on_clear)
self.clear_button.setFixedWidth(btn_width)
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(_('Remote Node')), 0, 0)
h.addWidget(self.trampoline_combo, 0, 1, 1, 4)
h.addWidget(QLabel('Amount'), 2, 0)
amt_hbox = QHBoxLayout()
amt_hbox.setContentsMargins(0, 0, 0, 0)
amt_hbox.addWidget(self.amount_e)
amt_hbox.addWidget(self.min_button)
amt_hbox.addWidget(self.max_button)
amt_hbox.addWidget(self.clear_button)
amt_hbox.addStretch()
h.addLayout(amt_hbox, 2, 1, 1, 4)
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() or b"").hex()
if not nodeid:
self.remote_nodeid.setText("")
self.remote_nodeid.setPlaceholderText(
_("Couldn't find suitable peer yet, try again later.")
)
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:
self.remote_nodeid.setText('')
self.remote_nodeid.repaint() # macOS hack for #6269
self.max_button.setChecked(False)
self.max_button.repaint() # macOS hack for #6269
def spend_min(self):
self.max_button.setChecked(False)
self.amount_e.setFrozen(False)
self.amount_e.setAmount(self.min_amount_sat)
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(FeePolicy(self.config.FEE_POLICY))
except (NotEnoughFunds, NoDynamicFeeEstimates) as e:
self.max_button.setChecked(False)
self.amount_e.setFrozen(False)
self.window.show_error(str(e))
return
amount = tx.output_value()
amount = min(amount, self.config.LIGHTNING_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() < self.config.LIGHTNING_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 not funding_sat:
return
if funding_sat != '!':
if self.min_amount_sat and funding_sat < self.min_amount_sat:
self.window.show_error(_('Amount too low'))
return
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:
return
self.window.open_channel(connect_str, funding_sat, 0)
return True