diff --git a/electrum/gui/qml/components/ChannelDetails.qml b/electrum/gui/qml/components/ChannelDetails.qml new file mode 100644 index 000000000..265cae031 --- /dev/null +++ b/electrum/gui/qml/components/ChannelDetails.qml @@ -0,0 +1,248 @@ +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" + +Pane { + id: root + width: parent.width + height: parent.height + + property string channelid + + property string title: qsTr("Channel details") + + property QtObject menu: Menu { + id: menu + MenuItem { + icon.color: 'transparent' + action: Action { + text: qsTr('Backup'); + enabled: false + onTriggered: {} + //icon.source: '../../icons/wallet.png' + } + } + MenuItem { + 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: {} + //icon.source: '../../icons/wallet.png' + } + } + MenuItem { + icon.color: 'transparent' + action: Action { + text: channeldetails.frozenForSending ? qsTr('Unfreeze (for sending)') : qsTr('Freeze (for sending)') + onTriggered: channeldetails.freezeForSending() + //icon.source: '../../icons/wallet.png' + } + } + MenuItem { + icon.color: 'transparent' + action: Action { + text: channeldetails.frozenForReceiving ? qsTr('Unfreeze (for receiving)') : qsTr('Freeze (for receiving)') + onTriggered: channeldetails.freezeForReceiving() + //icon.source: '../../icons/wallet.png' + } + } + } + + Flickable { + anchors.fill: parent + contentHeight: rootLayout.height + clip:true + interactive: height < contentHeight + + GridLayout { + id: rootLayout + width: parent.width + 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 + } + + Label { + text: qsTr('State') + color: Material.accentColor + } + + Label { + text: channeldetails.state + } + + Label { + text: qsTr('Initiator') + color: Material.accentColor + } + + Label { + text: channeldetails.initiator + } + + Label { + text: qsTr('Capacity') + color: Material.accentColor + } + + RowLayout { + Label { + font.family: FixedFont + text: Config.formatSats(channeldetails.capacity) + } + Label { + color: Material.accentColor + text: Config.baseUnit + } + Label { + text: Daemon.fx.enabled + ? '(' + Daemon.fx.fiatValue(channeldetails.capacity) + ' ' + Daemon.fx.fiatCurrency + ')' + : '' + } + } + + Label { + text: qsTr('Can send') + color: Material.accentColor + } + + RowLayout { + visible: !channeldetails.frozenForSending && channeldetails.isOpen + Label { + font.family: FixedFont + text: Config.formatSats(channeldetails.canSend) + } + Label { + color: Material.accentColor + text: Config.baseUnit + } + Label { + text: Daemon.fx.enabled + ? '(' + Daemon.fx.fiatValue(channeldetails.canSend) + ' ' + Daemon.fx.fiatCurrency + ')' + : '' + } + } + Label { + visible: channeldetails.frozenForSending && channeldetails.isOpen + text: qsTr('n/a (frozen)') + } + Label { + visible: !channeldetails.isOpen + text: qsTr('n/a (channel not open)') + } + + Label { + text: qsTr('Can Receive') + color: Material.accentColor + } + + RowLayout { + visible: !channeldetails.frozenForReceiving && channeldetails.isOpen + Label { + font.family: FixedFont + text: Config.formatSats(channeldetails.canReceive) + } + Label { + color: Material.accentColor + text: Config.baseUnit + } + Label { + text: Daemon.fx.enabled + ? '(' + Daemon.fx.fiatValue(channeldetails.canReceive) + ' ' + Daemon.fx.fiatCurrency + ')' + : '' + } + } + Label { + visible: channeldetails.frozenForReceiving && channeldetails.isOpen + text: qsTr('n/a (frozen)') + } + Label { + visible: !channeldetails.isOpen + text: qsTr('n/a (channel not open)') + } + + Label { + text: qsTr('Channel type') + color: Material.accentColor + } + + Label { + text: channeldetails.channelType + } + + Label { + text: qsTr('Remote node ID') + Layout.columnSpan: 2 + color: Material.accentColor + } + + TextHighlightPane { + Layout.columnSpan: 2 + Layout.fillWidth: true + padding: 0 + leftPadding: constants.paddingSmall + + RowLayout { + width: parent.width + Label { + text: channeldetails.pubkey + font.pixelSize: constants.fontSizeLarge + font.family: FixedFont + Layout.fillWidth: true + wrapMode: Text.Wrap + } + ToolButton { + icon.source: '../../icons/share.png' + icon.color: 'transparent' + onClicked: { + var dialog = share.createObject(root, { 'title': qsTr('Channel node ID'), 'text': channeldetails.pubkey }) + dialog.open() + } + } + } + } + + } + } + + ChannelDetails { + id: channeldetails + wallet: Daemon.currentWallet + channelid: root.channelid + } + + Component { + id: share + GenericShareDialog {} + } +} diff --git a/electrum/gui/qml/components/Channels.qml b/electrum/gui/qml/components/Channels.qml index 5462edc7f..a401e0195 100644 --- a/electrum/gui/qml/components/Channels.qml +++ b/electrum/gui/qml/components/Channels.qml @@ -108,7 +108,9 @@ Pane { model: Daemon.currentWallet.channelModel delegate: ChannelDelegate { - //highlighted: ListView.isCurrentItem + onClicked: { + app.stack.push(Qt.resolvedUrl('ChannelDetails.qml'), { 'channelid': model.cid }) + } } ScrollIndicator.vertical: ScrollIndicator { } diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index 75893e9cb..a118b705b 100644 --- a/electrum/gui/qml/qeapp.py +++ b/electrum/gui/qml/qeapp.py @@ -25,6 +25,7 @@ from .qeaddressdetails import QEAddressDetails from .qetxdetails import QETxDetails from .qechannelopener import QEChannelOpener from .qelnpaymentdetails import QELnPaymentDetails +from .qechanneldetails import QEChannelDetails notification = None @@ -149,6 +150,7 @@ class ElectrumQmlApplication(QGuiApplication): qmlRegisterType(QETxDetails, 'org.electrum', 1, 0, 'TxDetails') qmlRegisterType(QEChannelOpener, 'org.electrum', 1, 0, 'ChannelOpener') qmlRegisterType(QELnPaymentDetails, 'org.electrum', 1, 0, 'LnPaymentDetails') + qmlRegisterType(QEChannelDetails, 'org.electrum', 1, 0, 'ChannelDetails') qmlRegisterUncreatableType(QEAmount, 'org.electrum', 1, 0, 'Amount', 'Amount can only be used as property') diff --git a/electrum/gui/qml/qechanneldetails.py b/electrum/gui/qml/qechanneldetails.py new file mode 100644 index 000000000..6a21a0e46 --- /dev/null +++ b/electrum/gui/qml/qechanneldetails.py @@ -0,0 +1,132 @@ +from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, Q_ENUMS + +from electrum.logging import get_logger +from electrum.util import register_callback, unregister_callback +from electrum.lnutil import LOCAL, REMOTE + +from .qewallet import QEWallet +from .qetypes import QEAmount + +class QEChannelDetails(QObject): + + _logger = get_logger(__name__) + _wallet = None + _channelid = None + _channel = None + + channelChanged = pyqtSignal() + + def __init__(self, parent=None): + super().__init__(parent) + register_callback(self.on_network, ['channel']) # TODO unregister too + + def on_network(self, event, *args): + if event == 'channel': + wallet, channel = args + if wallet == self._wallet.wallet and self._channelid == channel.channel_id.hex(): + self.channelChanged.emit() + + walletChanged = pyqtSignal() + @pyqtProperty(QEWallet, notify=walletChanged) + def wallet(self): + return self._wallet + + @wallet.setter + def wallet(self, wallet: QEWallet): + if self._wallet != wallet: + self._wallet = wallet + self.walletChanged.emit() + + channelidChanged = pyqtSignal() + @pyqtProperty(str, notify=channelidChanged) + def channelid(self): + return self._channelid + + @channelid.setter + def channelid(self, channelid: str): + if self._channelid != channelid: + self._channelid = channelid + if channelid: + self.load() + self.channelidChanged.emit() + + def load(self): + lnchannels = self._wallet.wallet.lnworker.channels + for channel in lnchannels.values(): + self._logger.debug('%s == %s ?' % (self._channelid, channel.channel_id)) + if self._channelid == channel.channel_id.hex(): + self._channel = channel + self.channelChanged.emit() + + @pyqtProperty(str, notify=channelChanged) + def name(self): + if not self._channel: + return + return self._wallet.wallet.lnworker.get_node_alias(self._channel.node_id) or self._channel.node_id.hex() + + @pyqtProperty(str, notify=channelChanged) + def pubkey(self): + return self._channel.node_id.hex() #if self._channel else '' + + @pyqtProperty(str, notify=channelChanged) + def short_cid(self): + return self._channel.short_id_for_GUI() + + @pyqtProperty(str, notify=channelChanged) + def state(self): + return self._channel.get_state_for_GUI() + + @pyqtProperty(str, notify=channelChanged) + def initiator(self): + return 'Local' if self._channel.constraints.is_initiator else 'Remote' + + @pyqtProperty(QEAmount, notify=channelChanged) + def capacity(self): + self._capacity = QEAmount(amount_sat=self._channel.get_capacity()) + return self._capacity + + @pyqtProperty(QEAmount, notify=channelChanged) + def canSend(self): + self._can_send = QEAmount(amount_sat=self._channel.available_to_spend(LOCAL)/1000) + return self._can_send + + @pyqtProperty(QEAmount, notify=channelChanged) + def canReceive(self): + self._can_receive = QEAmount(amount_sat=self._channel.available_to_spend(REMOTE)/1000) + return self._can_receive + + @pyqtProperty(bool, notify=channelChanged) + def frozenForSending(self): + return self._channel.is_frozen_for_sending() + + @pyqtProperty(bool, notify=channelChanged) + def frozenForReceiving(self): + return self._channel.is_frozen_for_receiving() + + @pyqtProperty(str, notify=channelChanged) + def channelType(self): + return self._channel.storage['channel_type'].name_minimal + + @pyqtProperty(bool, notify=channelChanged) + def isOpen(self): + return self._channel.is_open() + + @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') + + @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') diff --git a/electrum/gui/qml/qechannellistmodel.py b/electrum/gui/qml/qechannellistmodel.py index 77a05cc2b..296b885ee 100644 --- a/electrum/gui/qml/qechannellistmodel.py +++ b/electrum/gui/qml/qechannellistmodel.py @@ -14,7 +14,7 @@ class QEChannelListModel(QAbstractListModel): # define listmodel rolemap _ROLE_NAMES=('cid','state','initiator','capacity','can_send','can_receive', - 'l_csv_delat','r_csv_delay','send_frozen','receive_frozen', + 'l_csv_delay','r_csv_delay','send_frozen','receive_frozen', 'type','node_id','node_alias','short_cid','funding_tx') _ROLE_KEYS = range(Qt.UserRole, Qt.UserRole + len(_ROLE_NAMES)) _ROLE_MAP = dict(zip(_ROLE_KEYS, [bytearray(x.encode()) for x in _ROLE_NAMES])) @@ -81,7 +81,7 @@ class QEChannelListModel(QAbstractListModel): def channel_to_model(self, lnc): lnworker = self.wallet.lnworker item = {} - item['channel_id'] = lnc.channel_id + item['cid'] = lnc.channel_id.hex() item['node_alias'] = lnworker.get_node_alias(lnc.node_id) or lnc.node_id.hex() item['short_cid'] = lnc.short_id_for_GUI() item['state'] = lnc.get_state_for_GUI() @@ -114,7 +114,7 @@ class QEChannelListModel(QAbstractListModel): def on_channel_updated(self, channel): i = 0 for c in self.channels: - if c['channel_id'] == channel.channel_id: + if c['cid'] == channel.channel_id: self.do_update(i,channel) break i = i + 1 diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index 9dbd3daa5..e74264d55 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -57,9 +57,8 @@ class QEInvoice(QObject): invoiceCreateError = pyqtSignal([str,str], arguments=['code', 'message']) - def __init__(self, config, parent=None): + def __init__(self, parent=None): super().__init__(parent) - self.config = config self.clear() @pyqtProperty(int, notify=invoiceChanged)