electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

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:
Melectrum/gui/qt/channels_list.py | 40+++++++++++++++++++++++++++++++++++-----
Melectrum/lnchannel.py | 31++++++++++++++++++++++++++-----
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: