commit deb50e7ec310e60133a141a7ca4eb71456de61b5
parent 9c8d2be6389e8265d08e4c58fe5cc20b3d630911
Author: SomberNight <somber.night@protonmail.com>
Date: Thu, 26 Mar 2020 03:32:44 +0100
lnchannel: implement "freezing" channels (for sending)
and expose it in Qt GUI
Diffstat:
2 files changed, 61 insertions(+), 10 deletions(-)
diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py
@@ -1,12 +1,13 @@
# -*- coding: utf-8 -*-
import traceback
from enum import IntEnum
+from typing import Sequence, Optional
from PyQt5 import QtCore, QtGui
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (QMenu, QHBoxLayout, QLabel, QVBoxLayout, QGridLayout, QLineEdit,
QPushButton, QAbstractItemView)
-from PyQt5.QtGui import QFont
+from PyQt5.QtGui import QFont, QStandardItem, QBrush
from electrum.util import bh2u, NotEnoughFunds, NoDynamicFeeEstimates
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 .util import (MyTreeView, WindowModalDialog, Buttons, OkButton, CancelButton,
- EnterButton, WaitingDialog, MONOSPACE_FONT)
+ EnterButton, WaitingDialog, MONOSPACE_FONT, ColorScheme)
from .amountedit import BTCAmountEdit, FreezableLineEdit
@@ -43,6 +44,8 @@ class ChannelsList(MyTreeView):
Columns.CHANNEL_STATUS: _('Status'),
}
+ _default_item_bg_brush = None # type: Optional[QBrush]
+
def __init__(self, parent):
super().__init__(parent, self.create_menu, stretch_column=self.Columns.NODE_ID,
editable_columns=[])
@@ -141,6 +144,12 @@ class ChannelsList(MyTreeView):
cc = self.add_copy_menu(menu, idx)
cc.addAction(_("Long Channel ID"), lambda: self.place_text_on_clipboard(channel_id.hex(),
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)
if funding_tx:
menu.addAction(_("View funding transaction"), lambda: self.parent.show_transaction(funding_tx))
@@ -169,9 +178,12 @@ class ChannelsList(MyTreeView):
return
for row in range(self.model().rowCount()):
item = self.model().item(row, self.Columns.NODE_ID)
- if item.data(ROLE_CHANNEL_ID) == chan.channel_id:
- for column, v in enumerate(self.format_fields(chan)):
- self.model().item(row, column).setData(v, QtCore.Qt.DisplayRole)
+ if item.data(ROLE_CHANNEL_ID) != chan.channel_id:
+ continue
+ for column, v in enumerate(self.format_fields(chan)):
+ 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)
@QtCore.pyqtSlot(Abstract_Wallet)
@@ -187,13 +199,31 @@ class ChannelsList(MyTreeView):
for chan in lnworker.channels.values():
items = [QtGui.QStandardItem(x) for x in self.format_fields(chan)]
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].setFont(QFont(MONOSPACE_FONT))
items[self.Columns.LOCAL_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.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):
msg = _('Can send') + ' ' + self.parent.format_amount(lnworker.can_send())\
+ ' ' + self.parent.base_unit() + '; '\
diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py
@@ -390,7 +390,22 @@ class Channel(Logger):
def is_redeemed(self):
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)
if self.is_closed():
raise PaymentFailure('Channel closed')
@@ -398,6 +413,8 @@ class Channel(Logger):
raise PaymentFailure('Channel not open', self.get_state())
if not self.can_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:
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:
@@ -409,9 +426,14 @@ class Channel(Logger):
if amount_msat < self.config[REMOTE].htlc_minimum_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:
- self._check_can_pay(amount_msat)
+ self._assert_we_can_add_htlc(amount_msat)
except PaymentFailure:
return False
return True
@@ -430,11 +452,10 @@ class Channel(Logger):
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
htlc = UpdateAddHtlc(**htlc)
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:
htlc = attr.evolve(htlc, htlc_id=self.hm.get_next_htlc_id(LOCAL))
with self.db_lock: