Browse Source

lnchannel: implement "freezing" channels (for sending)

and expose it in Qt GUI
hard-fail-on-bad-server-string
SomberNight 5 years ago
parent
commit
deb50e7ec3
No known key found for this signature in database GPG Key ID: B33B5F232C6271E9
  1. 36
      electrum/gui/qt/channels_list.py
  2. 31
      electrum/lnchannel.py

36
electrum/gui/qt/channels_list.py

@ -1,12 +1,13 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import traceback import traceback
from enum import IntEnum from enum import IntEnum
from typing import Sequence, Optional
from PyQt5 import QtCore, QtGui from PyQt5 import QtCore, QtGui
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (QMenu, QHBoxLayout, QLabel, QVBoxLayout, QGridLayout, QLineEdit, from PyQt5.QtWidgets import (QMenu, QHBoxLayout, QLabel, QVBoxLayout, QGridLayout, QLineEdit,
QPushButton, QAbstractItemView) QPushButton, QAbstractItemView)
from PyQt5.QtGui import QFont from PyQt5.QtGui import QFont, QStandardItem, QBrush
from electrum.util import bh2u, NotEnoughFunds, NoDynamicFeeEstimates from electrum.util import bh2u, NotEnoughFunds, NoDynamicFeeEstimates
from electrum.i18n import _ from electrum.i18n import _
@ -15,7 +16,7 @@ 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, LN_MAX_FUNDING_SAT
from .util import (MyTreeView, WindowModalDialog, Buttons, OkButton, CancelButton, from .util import (MyTreeView, WindowModalDialog, Buttons, OkButton, CancelButton,
EnterButton, WaitingDialog, MONOSPACE_FONT) EnterButton, WaitingDialog, MONOSPACE_FONT, ColorScheme)
from .amountedit import BTCAmountEdit, FreezableLineEdit from .amountedit import BTCAmountEdit, FreezableLineEdit
@ -43,6 +44,8 @@ class ChannelsList(MyTreeView):
Columns.CHANNEL_STATUS: _('Status'), Columns.CHANNEL_STATUS: _('Status'),
} }
_default_item_bg_brush = None # type: Optional[QBrush]
def __init__(self, parent): def __init__(self, parent):
super().__init__(parent, self.create_menu, stretch_column=self.Columns.NODE_ID, super().__init__(parent, self.create_menu, stretch_column=self.Columns.NODE_ID,
editable_columns=[]) editable_columns=[])
@ -141,6 +144,12 @@ class ChannelsList(MyTreeView):
cc = self.add_copy_menu(menu, idx) cc = self.add_copy_menu(menu, idx)
cc.addAction(_("Long Channel ID"), lambda: self.place_text_on_clipboard(channel_id.hex(), cc.addAction(_("Long Channel ID"), lambda: self.place_text_on_clipboard(channel_id.hex(),
title=_("Long Channel ID"))) title=_("Long Channel ID")))
if not chan.is_frozen():
menu.addAction(_("Freeze"), lambda: chan.set_frozen(True))
else:
menu.addAction(_("Unfreeze"), lambda: chan.set_frozen(False))
funding_tx = self.parent.wallet.db.get_transaction(chan.funding_outpoint.txid) funding_tx = self.parent.wallet.db.get_transaction(chan.funding_outpoint.txid)
if funding_tx: if funding_tx:
menu.addAction(_("View funding transaction"), lambda: self.parent.show_transaction(funding_tx)) menu.addAction(_("View funding transaction"), lambda: self.parent.show_transaction(funding_tx))
@ -169,9 +178,12 @@ class ChannelsList(MyTreeView):
return return
for row in range(self.model().rowCount()): for row in range(self.model().rowCount()):
item = self.model().item(row, self.Columns.NODE_ID) item = self.model().item(row, self.Columns.NODE_ID)
if item.data(ROLE_CHANNEL_ID) == chan.channel_id: if item.data(ROLE_CHANNEL_ID) != chan.channel_id:
continue
for column, v in enumerate(self.format_fields(chan)): for column, v in enumerate(self.format_fields(chan)):
self.model().item(row, column).setData(v, QtCore.Qt.DisplayRole) self.model().item(row, column).setData(v, QtCore.Qt.DisplayRole)
items = [self.model().item(row, column) for column in self.Columns]
self._update_chan_frozen_bg(chan=chan, items=items)
self.update_can_send(lnworker) self.update_can_send(lnworker)
@QtCore.pyqtSlot(Abstract_Wallet) @QtCore.pyqtSlot(Abstract_Wallet)
@ -187,13 +199,31 @@ class ChannelsList(MyTreeView):
for chan in lnworker.channels.values(): for chan in lnworker.channels.values():
items = [QtGui.QStandardItem(x) for x in self.format_fields(chan)] items = [QtGui.QStandardItem(x) for x in self.format_fields(chan)]
self.set_editability(items) self.set_editability(items)
if self._default_item_bg_brush is None:
self._default_item_bg_brush = items[self.Columns.NODE_ID].background()
items[self.Columns.NODE_ID].setData(chan.channel_id, ROLE_CHANNEL_ID) items[self.Columns.NODE_ID].setData(chan.channel_id, ROLE_CHANNEL_ID)
items[self.Columns.NODE_ID].setFont(QFont(MONOSPACE_FONT)) items[self.Columns.NODE_ID].setFont(QFont(MONOSPACE_FONT))
items[self.Columns.LOCAL_BALANCE].setFont(QFont(MONOSPACE_FONT)) items[self.Columns.LOCAL_BALANCE].setFont(QFont(MONOSPACE_FONT))
items[self.Columns.REMOTE_BALANCE].setFont(QFont(MONOSPACE_FONT)) items[self.Columns.REMOTE_BALANCE].setFont(QFont(MONOSPACE_FONT))
self._update_chan_frozen_bg(chan=chan, items=items)
self.model().insertRow(0, items) self.model().insertRow(0, items)
self.sortByColumn(self.Columns.SHORT_CHANID, Qt.DescendingOrder) self.sortByColumn(self.Columns.SHORT_CHANID, Qt.DescendingOrder)
def _update_chan_frozen_bg(self, *, chan: Channel, items: Sequence[QStandardItem]):
assert self._default_item_bg_brush is not None
for col in [
self.Columns.LOCAL_BALANCE,
self.Columns.REMOTE_BALANCE,
self.Columns.CHANNEL_STATUS,
]:
item = items[col]
if chan.is_frozen():
item.setBackground(ColorScheme.BLUE.as_color(True))
item.setToolTip(_("This channel is frozen. Frozen channels will not be used for outgoing payments."))
else:
item.setBackground(self._default_item_bg_brush)
item.setToolTip("")
def update_can_send(self, lnworker): def update_can_send(self, lnworker):
msg = _('Can send') + ' ' + self.parent.format_amount(lnworker.can_send())\ msg = _('Can send') + ' ' + self.parent.format_amount(lnworker.can_send())\
+ ' ' + self.parent.base_unit() + '; '\ + ' ' + self.parent.base_unit() + '; '\

31
electrum/lnchannel.py

@ -390,7 +390,22 @@ class Channel(Logger):
def is_redeemed(self): def is_redeemed(self):
return self.get_state() == channel_states.REDEEMED return self.get_state() == channel_states.REDEEMED
def _check_can_pay(self, amount_msat: int) -> None: def is_frozen(self) -> bool:
"""Whether the user has marked this channel as frozen.
Frozen channels are not supposed to be used for new outgoing payments.
(note that payment-forwarding ignores this option)
"""
return self.storage.get('frozen_for_sending', False)
def set_frozen(self, b: bool) -> None:
self.storage['frozen_for_sending'] = bool(b)
if self.lnworker:
self.lnworker.network.trigger_callback('channel', self)
def _assert_we_can_add_htlc(self, amount_msat: int) -> None:
"""Raises PaymentFailure if the local party cannot add this new HTLC.
(this is relevant both for payments initiated by us and when forwarding)
"""
# TODO check if this method uses correct ctns (should use "latest" + 1) # TODO check if this method uses correct ctns (should use "latest" + 1)
if self.is_closed(): if self.is_closed():
raise PaymentFailure('Channel closed') raise PaymentFailure('Channel closed')
@ -398,6 +413,8 @@ class Channel(Logger):
raise PaymentFailure('Channel not open', self.get_state()) raise PaymentFailure('Channel not open', self.get_state())
if not self.can_send_ctx_updates(): if not self.can_send_ctx_updates():
raise PaymentFailure('Channel cannot send ctx updates') raise PaymentFailure('Channel cannot send ctx updates')
if not self.can_send_update_add_htlc():
raise PaymentFailure('Channel cannot add htlc')
if self.available_to_spend(LOCAL) < amount_msat: if self.available_to_spend(LOCAL) < amount_msat:
raise PaymentFailure(f'Not enough local balance. Have: {self.available_to_spend(LOCAL)}, Need: {amount_msat}') raise PaymentFailure(f'Not enough local balance. Have: {self.available_to_spend(LOCAL)}, Need: {amount_msat}')
if len(self.hm.htlcs(LOCAL)) + 1 > self.config[REMOTE].max_accepted_htlcs: if len(self.hm.htlcs(LOCAL)) + 1 > self.config[REMOTE].max_accepted_htlcs:
@ -409,9 +426,14 @@ class Channel(Logger):
if amount_msat < self.config[REMOTE].htlc_minimum_msat: if amount_msat < self.config[REMOTE].htlc_minimum_msat:
raise PaymentFailure(f'HTLC value too small: {amount_msat} msat') raise PaymentFailure(f'HTLC value too small: {amount_msat} msat')
def can_pay(self, amount_msat): def can_pay(self, amount_msat: int) -> bool:
"""Returns whether we can initiate a new payment of given value.
(we are the payer, not just a forwarding node)
"""
if self.is_frozen():
return False
try: try:
self._check_can_pay(amount_msat) self._assert_we_can_add_htlc(amount_msat)
except PaymentFailure: except PaymentFailure:
return False return False
return True return True
@ -430,11 +452,10 @@ class Channel(Logger):
This docstring is from LND. This docstring is from LND.
""" """
assert self.can_send_ctx_updates(), f"cannot update channel. {self.get_state()!r} {self.peer_state!r}"
if isinstance(htlc, dict): # legacy conversion # FIXME remove if isinstance(htlc, dict): # legacy conversion # FIXME remove
htlc = UpdateAddHtlc(**htlc) htlc = UpdateAddHtlc(**htlc)
assert isinstance(htlc, UpdateAddHtlc) assert isinstance(htlc, UpdateAddHtlc)
self._check_can_pay(htlc.amount_msat) self._assert_we_can_add_htlc(htlc.amount_msat)
if htlc.htlc_id is None: if htlc.htlc_id is None:
htlc = attr.evolve(htlc, htlc_id=self.hm.get_next_htlc_id(LOCAL)) htlc = attr.evolve(htlc, htlc_id=self.hm.get_next_htlc_id(LOCAL))
with self.db_lock: with self.db_lock:

Loading…
Cancel
Save