diff --git a/electrum/gui/qml/components/ChannelDetails.qml b/electrum/gui/qml/components/ChannelDetails.qml index 265cae031..1b0e333f5 100644 --- a/electrum/gui/qml/components/ChannelDetails.qml +++ b/electrum/gui/qml/components/ChannelDetails.qml @@ -31,17 +31,11 @@ Pane { icon.color: 'transparent' action: Action { text: qsTr('Close channel'); - enabled: false - onTriggered: {} - //icon.source: '../../icons/wallet.png' - } - } - MenuItem { - icon.color: 'transparent' - action: Action { - text: qsTr('Force-close'); - enabled: false - onTriggered: {} + enabled: channeldetails.canClose + onTriggered: { + var dialog = closechannel.createObject(root, { 'channelid': channelid }) + dialog.open() + } //icon.source: '../../icons/wallet.png' } } @@ -245,4 +239,9 @@ Pane { id: share GenericShareDialog {} } + + Component { + id: closechannel + CloseChannelDialog {} + } } diff --git a/electrum/gui/qml/components/CloseChannelDialog.qml b/electrum/gui/qml/components/CloseChannelDialog.qml new file mode 100644 index 000000000..2bd2c34aa --- /dev/null +++ b/electrum/gui/qml/components/CloseChannelDialog.qml @@ -0,0 +1,130 @@ +import QtQuick 2.6 +import QtQuick.Layouts 1.0 +import QtQuick.Controls 2.3 +import QtQuick.Controls.Material 2.0 + +import org.electrum 1.0 + +import "controls" + +Dialog { + id: dialog + width: parent.width + height: parent.height + + property string channelid + + title: qsTr('Close Channel') + standardButtons: closing ? 0 : Dialog.Cancel + + modal: true + parent: Overlay.overlay + Overlay.modal: Rectangle { + color: "#aa000000" + } + property bool closing: false + + closePolicy: Popup.NoAutoClose + + GridLayout { + id: layout + width: parent.width + height: parent.height + columns: 2 + + Label { + text: qsTr('Channel name') + color: Material.accentColor + } + + Label { + text: channeldetails.name + } + + Label { + text: qsTr('Short channel ID') + color: Material.accentColor + } + + Label { + text: channeldetails.short_cid + } + + InfoTextArea { + Layout.columnSpan: 2 + text: qsTr(channeldetails.message_force_close) + } + + ColumnLayout { + Layout.columnSpan: 2 + Layout.alignment: Qt.AlignHCenter + + ButtonGroup { + id: closetypegroup + } + + RadioButton { + ButtonGroup.group: closetypegroup + property string closetype: 'cooperative' + checked: true + enabled: !closing && channeldetails.canCoopClose + text: qsTr('Cooperative close') + } + RadioButton { + ButtonGroup.group: closetypegroup + property string closetype: 'force' + enabled: !closing && channeldetails.canForceClose + text: qsTr('Request Force-close') + } + } + + Button { + Layout.columnSpan: 2 + Layout.alignment: Qt.AlignHCenter + text: qsTr('Close') + enabled: !closing + onClicked: { + closing = true + channeldetails.close_channel(closetypegroup.checkedButton.closetype) + } + + } + + ColumnLayout { + Layout.columnSpan: 2 + Layout.alignment: Qt.AlignHCenter + Label { + id: errorText + visible: !closing && errorText + wrapMode: Text.Wrap + Layout.preferredWidth: layout.width + } + Label { + text: qsTr('Closing...') + visible: closing + } + BusyIndicator { + visible: closing + } + } + Item { Layout.fillHeight: true; Layout.preferredWidth: 1 } + + } + + ChannelDetails { + id: channeldetails + wallet: Daemon.currentWallet + channelid: dialog.channelid + + onChannelCloseSuccess: { + closing = false + dialog.close() + } + + onChannelCloseFailed: { + closing = false + errorText.text = message + } + } + +} diff --git a/electrum/gui/qml/qechanneldetails.py b/electrum/gui/qml/qechanneldetails.py index 96ff41b21..f3080ee3a 100644 --- a/electrum/gui/qml/qechanneldetails.py +++ b/electrum/gui/qml/qechanneldetails.py @@ -1,8 +1,13 @@ +import asyncio + from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, Q_ENUMS +from electrum.i18n import _ +from electrum.gui import messages from electrum.logging import get_logger from electrum.util import register_callback, unregister_callback from electrum.lnutil import LOCAL, REMOTE +from electrum.lnchannel import ChanCloseOption from .qewallet import QEWallet from .qetypes import QEAmount @@ -15,6 +20,8 @@ class QEChannelDetails(QObject): _channel = None channelChanged = pyqtSignal() + channelCloseSuccess = pyqtSignal() + channelCloseFailed = pyqtSignal([str], arguments=['message']) def __init__(self, parent=None): super().__init__(parent) @@ -57,7 +64,7 @@ class QEChannelDetails(QObject): def load(self): lnchannels = self._wallet.wallet.lnworker.channels for channel in lnchannels.values(): - self._logger.debug('%s == %s ?' % (self._channelid, channel.channel_id)) + #self._logger.debug('%s == %s ?' % (self._channelid, channel.channel_id)) if self._channelid == channel.channel_id.hex(): self._channel = channel self.channelChanged.emit() @@ -115,22 +122,54 @@ class QEChannelDetails(QObject): def isOpen(self): return self._channel.is_open() + @pyqtProperty(bool, notify=channelChanged) + def canClose(self): + return self.canCoopClose or self.canForceClose + + @pyqtProperty(bool, notify=channelChanged) + def canCoopClose(self): + return ChanCloseOption.COOP_CLOSE in self._channel.get_close_options() + + @pyqtProperty(bool, notify=channelChanged) + def canForceClose(self): + return ChanCloseOption.LOCAL_FCLOSE in self._channel.get_close_options() + + @pyqtProperty(str, notify=channelChanged) + def message_force_close(self, notify=channelChanged): + return _(messages.MSG_REQUEST_FORCE_CLOSE) + @pyqtSlot() def freezeForSending(self): lnworker = self._channel.lnworker if lnworker.channel_db or lnworker.is_trampoline_peer(self._channel.node_id): - #self.is_frozen_for_sending = not self.is_frozen_for_sending self._channel.set_frozen_for_sending(not self.frozenForSending) self.channelChanged.emit() else: - self._logger.debug('TODO: messages.MSG_NON_TRAMPOLINE_CHANNEL_FROZEN_WITHOUT_GOSSIP') + self._logger.debug(messages.MSG_NON_TRAMPOLINE_CHANNEL_FROZEN_WITHOUT_GOSSIP) @pyqtSlot() def freezeForReceiving(self): lnworker = self._channel.lnworker if lnworker.channel_db or lnworker.is_trampoline_peer(self._channel.node_id): - #self.is_frozen_for_sending = not self.is_frozen_for_sending self._channel.set_frozen_for_receiving(not self.frozenForReceiving) self.channelChanged.emit() else: - self._logger.debug('TODO: messages.MSG_NON_TRAMPOLINE_CHANNEL_FROZEN_WITHOUT_GOSSIP') + self._logger.debug(messages.MSG_NON_TRAMPOLINE_CHANNEL_FROZEN_WITHOUT_GOSSIP) + + # this method assumes the qobject is not destroyed before the close either fails or succeeds + @pyqtSlot(str) + def close_channel(self, closetype): + async def do_close(closetype, channel_id): + try: + if closetype == 'force': + await self._wallet.wallet.lnworker.request_force_close(channel_id) + else: + await self._wallet.wallet.lnworker.close_channel(channel_id) + self.channelCloseSuccess.emit() + except Exception as e: + self._logger.exception("Could not close channel: " + repr(e)) + self.channelCloseFailed.emit(_('Could not close channel: ') + repr(e)) + + loop = self._wallet.wallet.network.asyncio_loop + coro = do_close(closetype, self._channel.channel_id) + asyncio.run_coroutine_threadsafe(coro, loop) diff --git a/electrum/gui/qml/qechannellistmodel.py b/electrum/gui/qml/qechannellistmodel.py index 50fd55dd2..ce6c7964f 100644 --- a/electrum/gui/qml/qechannellistmodel.py +++ b/electrum/gui/qml/qechannellistmodel.py @@ -117,7 +117,7 @@ class QEChannelListModel(QAbstractListModel): def on_channel_updated(self, channel): i = 0 for c in self.channels: - if c['cid'] == channel.channel_id: + if c['cid'] == channel.channel_id.hex(): self.do_update(i,channel) break i = i + 1