URI: 
       tlnchannel: implement "freezing" channels (for sending) - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit deb50e7ec310e60133a141a7ca4eb71456de61b5
   DIR parent 9c8d2be6389e8265d08e4c58fe5cc20b3d630911
  HTML 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:
         M electrum/gui/qt/channels_list.py    |      40 +++++++++++++++++++++++++++----
         M electrum/lnchannel.py               |      31 ++++++++++++++++++++++++++-----
       
       2 files changed, 61 insertions(+), 10 deletions(-)
       ---
   DIR diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py
       t@@ -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 _
       t@@ -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
        
        
       t@@ -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=[])
       t@@ -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))
       t@@ -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)
       t@@ -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() + '; '\
   DIR diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py
       t@@ -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')
       t@@ -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:
       t@@ -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
       t@@ -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: