diff --git a/electrum/gui/icons/save.png b/electrum/gui/icons/save.png new file mode 100644 index 000000000..43859c85f Binary files /dev/null and b/electrum/gui/icons/save.png differ diff --git a/electrum/gui/qml/components/History.qml b/electrum/gui/qml/components/History.qml index 84d584982..5c1c5a8b9 100644 --- a/electrum/gui/qml/components/History.qml +++ b/electrum/gui/qml/components/History.qml @@ -67,11 +67,19 @@ Pane { Layout.preferredHeight: txinfo.height onClicked: { - var page = app.stack.push(Qt.resolvedUrl('TxDetails.qml'), {'txid': model.txid}) - page.txDetailsChanged.connect(function() { - // update listmodel when details change - visualModel.model.update_tx_label(model.txid, page.label) - }) + if (model.lightning) { + var page = app.stack.push(Qt.resolvedUrl('LightningPaymentDetails.qml'), {'key': model.key}) + page.detailsChanged.connect(function() { + // update listmodel when details change + visualModel.model.update_tx_label(model.key, page.label) + }) + } else { + var page = app.stack.push(Qt.resolvedUrl('TxDetails.qml'), {'txid': model.key}) + page.detailsChanged.connect(function() { + // update listmodel when details change + visualModel.model.update_tx_label(model.key, page.label) + }) + } } GridLayout { @@ -82,6 +90,7 @@ Pane { width: delegate.width - 2*constants.paddingSmall Item { Layout.columnSpan: 3; Layout.preferredWidth: 1; Layout.preferredHeight: 1} + Image { readonly property variant tx_icons : [ "../../../gui/icons/unconfirmed.png", @@ -97,7 +106,7 @@ Pane { Layout.preferredHeight: constants.iconSizeLarge Layout.alignment: Qt.AlignVCenter Layout.rowSpan: 2 - source: tx_icons[Math.min(6,model.confirmations)] + source: model.lightning ? "../../../gui/icons/lightning.png" : tx_icons[Math.min(6,model.confirmations)] } Label { @@ -118,7 +127,7 @@ Pane { color: model.incoming ? constants.colorCredit : constants.colorDebit function updateText() { - text = Config.formatSats(model.bc_value) + text = Config.formatSats(model.value) } Component.onCompleted: updateText() } @@ -137,9 +146,9 @@ Pane { if (!Daemon.fx.enabled) { text = '' } else if (Daemon.fx.historicRates) { - text = Daemon.fx.fiatValueHistoric(model.bc_value, model.timestamp) + ' ' + Daemon.fx.fiatCurrency + text = Daemon.fx.fiatValueHistoric(model.value, model.timestamp) + ' ' + Daemon.fx.fiatCurrency } else { - text = Daemon.fx.fiatValue(model.bc_value, false) + ' ' + Daemon.fx.fiatCurrency + text = Daemon.fx.fiatValue(model.value, false) + ' ' + Daemon.fx.fiatCurrency } } Component.onCompleted: updateText() diff --git a/electrum/gui/qml/components/InvoiceDialog.qml b/electrum/gui/qml/components/InvoiceDialog.qml index cb9f5a848..18697fe2f 100644 --- a/electrum/gui/qml/components/InvoiceDialog.qml +++ b/electrum/gui/qml/components/InvoiceDialog.qml @@ -126,7 +126,7 @@ Dialog { text: qsTr('Save') icon.source: '../../icons/save.png' visible: invoice_key == '' - enabled: invoice.invoiceType == Invoice.OnchainInvoice + enabled: invoice.canSave onClicked: { invoice.save_invoice() dialog.close() @@ -138,10 +138,13 @@ Dialog { icon.source: '../../icons/confirmed.png' enabled: invoice.invoiceType != Invoice.Invalid // TODO && has funds onClicked: { - invoice.save_invoice() + if (invoice_key == '') + invoice.save_invoice() dialog.close() if (invoice.invoiceType == Invoice.OnchainInvoice) { doPay() // only signal here + } else if (invoice.invoiceType == Invoice.LightningInvoice) { + doPay() // only signal here } } } diff --git a/electrum/gui/qml/components/LightningPaymentDetails.qml b/electrum/gui/qml/components/LightningPaymentDetails.qml new file mode 100644 index 000000000..63e423707 --- /dev/null +++ b/electrum/gui/qml/components/LightningPaymentDetails.qml @@ -0,0 +1,261 @@ +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 title: qsTr("Lightning payment details") + + property string key + + property alias label: lnpaymentdetails.label + + signal detailsChanged + + Flickable { + anchors.fill: parent + contentHeight: rootLayout.height + clip: true + interactive: height < contentHeight + + GridLayout { + id: rootLayout + width: parent.width + columns: 2 + + Label { + text: qsTr('Status') + color: Material.accentColor + } + + Label { + text: lnpaymentdetails.status + } + + Label { + text: qsTr('Date') + color: Material.accentColor + } + + Label { + text: lnpaymentdetails.date + } + + Label { + text: lnpaymentdetails.amount.msatsInt > 0 + ? qsTr('Amount received') + : qsTr('Amount sent') + color: Material.accentColor + } + + RowLayout { + Label { + text: Config.formatMilliSats(lnpaymentdetails.amount) + } + Label { + text: Config.baseUnit + color: Material.accentColor + } + } + + Label { + visible: lnpaymentdetails.amount.msatsInt < 0 + text: qsTr('Transaction fee') + color: Material.accentColor + } + + RowLayout { + visible: lnpaymentdetails.amount.msatsInt < 0 + Label { + text: Config.formatMilliSats(lnpaymentdetails.fee) + } + Label { + text: Config.baseUnit + color: Material.accentColor + } + } + + Label { + text: qsTr('Label') + Layout.columnSpan: 2 + color: Material.accentColor + } + + TextHighlightPane { + id: labelContent + + property bool editmode: false + + Layout.columnSpan: 2 + Layout.fillWidth: true + padding: 0 + leftPadding: constants.paddingSmall + + RowLayout { + width: parent.width + Label { + visible: !labelContent.editmode + text: lnpaymentdetails.label + wrapMode: Text.Wrap + Layout.fillWidth: true + font.pixelSize: constants.fontSizeLarge + } + ToolButton { + visible: !labelContent.editmode + icon.source: '../../icons/pen.png' + icon.color: 'transparent' + onClicked: { + labelEdit.text = lnpaymentdetails.label + labelContent.editmode = true + labelEdit.focus = true + } + } + TextField { + id: labelEdit + visible: labelContent.editmode + text: lnpaymentdetails.label + font.pixelSize: constants.fontSizeLarge + Layout.fillWidth: true + } + ToolButton { + visible: labelContent.editmode + icon.source: '../../icons/confirmed.png' + icon.color: 'transparent' + onClicked: { + labelContent.editmode = false + lnpaymentdetails.set_label(labelEdit.text) + } + } + ToolButton { + visible: labelContent.editmode + icon.source: '../../icons/delete.png' + icon.color: 'transparent' + onClicked: labelContent.editmode = false + } + } + } + + Label { + text: qsTr('Payment hash') + Layout.columnSpan: 2 + color: Material.accentColor + } + + TextHighlightPane { + Layout.columnSpan: 2 + Layout.fillWidth: true + padding: 0 + leftPadding: constants.paddingSmall + + RowLayout { + width: parent.width + Label { + text: lnpaymentdetails.payment_hash + 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('Payment hash'), 'text': lnpaymentdetails.payment_hash }) + dialog.open() + } + } + } + } + + Label { + text: qsTr('Preimage') + Layout.columnSpan: 2 + color: Material.accentColor + } + + TextHighlightPane { + Layout.columnSpan: 2 + Layout.fillWidth: true + padding: 0 + leftPadding: constants.paddingSmall + + RowLayout { + width: parent.width + Label { + text: lnpaymentdetails.preimage + 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('Preimage'), 'text': lnpaymentdetails.preimage }) + dialog.open() + } + } + } + } + + Label { + text: qsTr('Lightning invoice') + Layout.columnSpan: 2 + color: Material.accentColor + } + + TextHighlightPane { + Layout.columnSpan: 2 + Layout.fillWidth: true + padding: 0 + leftPadding: constants.paddingSmall + + RowLayout { + width: parent.width + Label { + Layout.fillWidth: true + text: lnpaymentdetails.invoice + font.pixelSize: constants.fontSizeLarge + font.family: FixedFont + wrapMode: Text.Wrap + maximumLineCount: 3 + elide: Text.ElideRight + } + ToolButton { + icon.source: '../../icons/share.png' + icon.color: enabled ? 'transparent' : constants.mutedForeground + enabled: lnpaymentdetails.invoice != '' + onClicked: { + var dialog = share.createObject(root, { 'title': qsTr('Lightning Invoice'), 'text': lnpaymentdetails.invoice }) + dialog.open() + } + } + } + } + + + } + } + + LnPaymentDetails { + id: lnpaymentdetails + wallet: Daemon.currentWallet + key: root.key + onLabelChanged: root.detailsChanged() + } + + Component { + id: share + GenericShareDialog {} + } + +} diff --git a/electrum/gui/qml/components/LightningPaymentProgressDialog.qml b/electrum/gui/qml/components/LightningPaymentProgressDialog.qml new file mode 100644 index 000000000..b81b92201 --- /dev/null +++ b/electrum/gui/qml/components/LightningPaymentProgressDialog.qml @@ -0,0 +1,127 @@ +import QtQuick 2.6 +import QtQuick.Layouts 1.0 +import QtQuick.Controls 2.14 +import QtQuick.Controls.Material 2.0 + +import org.electrum 1.0 + +import "controls" + +Dialog { + id: dialog + + required property string invoice_key + + width: parent.width + height: parent.height + + title: qsTr('Paying Lightning Invoice...') + standardButtons: Dialog.Cancel + + modal: true + parent: Overlay.overlay + Overlay.modal: Rectangle { + color: "#aa000000" + } + + Item { + id: s + state: '' + states: [ + State { + name: '' + }, + State { + name: 'success' + PropertyChanges { target: spinner; running: false } + PropertyChanges { target: helpText; text: qsTr('Paid!') } + PropertyChanges { target: dialog; standardButtons: Dialog.Ok } + PropertyChanges { target: icon; source: '../../icons/confirmed.png' } + }, + State { + name: 'failed' + PropertyChanges { target: spinner; running: false } + PropertyChanges { target: helpText; text: qsTr('Payment failed') } + PropertyChanges { target: dialog; standardButtons: Dialog.Ok } + PropertyChanges { target: errorText; visible: true } + PropertyChanges { target: icon; source: '../../icons/warning.png' } + } + ] + transitions: [ + Transition { + from: '' + to: 'success' + PropertyAnimation { target: helpText; properties: 'text'; duration: 0} + NumberAnimation { target: icon; properties: 'opacity'; from: 0; to: 1; duration: 200 } + NumberAnimation { target: icon; properties: 'scale'; from: 0; to: 1; duration: 500 + easing.type: Easing.OutBack + easing.overshoot: 10 + } + }, + Transition { + from: '' + to: 'failed' + PropertyAnimation { target: helpText; properties: 'text'; duration: 0} + NumberAnimation { target: icon; properties: 'opacity'; from: 0; to: 1; duration: 500 } + } + ] + } + + ColumnLayout { + id: content + anchors.centerIn: parent + + Item { + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: constants.iconSizeXXLarge + Layout.preferredHeight: constants.iconSizeXXLarge + + BusyIndicator { + id: spinner + visible: s.state == '' + width: constants.iconSizeXXLarge + height: constants.iconSizeXXLarge + } + + Image { + id: icon + width: constants.iconSizeXXLarge + height: constants.iconSizeXXLarge + } + } + + Label { + id: helpText + text: qsTr('Paying...') + font.pixelSize: constants.fontSizeXXLarge + Layout.alignment: Qt.AlignHCenter + } + + Label { + id: errorText + font.pixelSize: constants.fontSizeLarge + Layout.alignment: Qt.AlignHCenter + } + } + + Connections { + target: Daemon.currentWallet + function onPaymentSucceeded(key) { + if (key != invoice_key) { + console.log('wrong invoice ' + key + ' != ' + invoice_key) + return + } + console.log('payment succeeded!') + s.state = 'success' + } + function onPaymentFailed(key, reason) { + if (key != invoice_key) { + console.log('wrong invoice ' + key + ' != ' + invoice_key) + return + } + console.log('payment failed: ' + reason) + s.state = 'failed' + errorText.text = reason + } + } +} diff --git a/electrum/gui/qml/components/Send.qml b/electrum/gui/qml/components/Send.qml index 9e4ad1c4f..2a32c7325 100644 --- a/electrum/gui/qml/components/Send.qml +++ b/electrum/gui/qml/components/Send.qml @@ -161,6 +161,7 @@ Pane { rootItem.clear() } } + } } @@ -244,6 +245,11 @@ Pane { } } + Component { + id: lightningPaymentProgressDialog + LightningPaymentProgressDialog {} + } + Component { id: invoiceDialog InvoiceDialog { @@ -255,6 +261,17 @@ Pane { 'message': invoice.message }) dialog.open() + } else if (invoice.invoiceType == Invoice.LightningInvoice) { + console.log('About to pay lightning invoice') + if (invoice.key == '') { + console.log('No invoice key, aborting') + return + } + var dialog = lightningPaymentProgressDialog.createObject(rootItem, { + invoice_key: invoice.key + }) + dialog.open() + Daemon.currentWallet.pay_lightning_invoice(invoice.key) } } } @@ -263,8 +280,7 @@ Pane { Connections { target: Daemon.currentWallet function onInvoiceStatusChanged(key, status) { - // TODO: status from? - //Daemon.currentWallet.invoiceModel.updateInvoice(key, status) + Daemon.currentWallet.invoiceModel.updateInvoice(key, status) } } diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index e1820a3a6..b0e8f8cab 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -18,7 +18,7 @@ Pane { property alias label: txdetails.label - signal txDetailsChanged + signal detailsChanged property QtObject menu: Menu { id: menu @@ -97,11 +97,13 @@ Pane { } Label { + visible: txdetails.amount.satsInt < 0 text: qsTr('Transaction fee') color: Material.accentColor } RowLayout { + visible: txdetails.amount.satsInt < 0 Label { text: Config.formatSats(txdetails.fee) } @@ -111,38 +113,6 @@ Pane { } } - Label { - text: qsTr('Transaction 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: root.txid - 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('Transaction ID'), 'text': root.txid }) - dialog.open() - } - } - } - } - Label { text: qsTr('Label') Layout.columnSpan: 2 @@ -203,6 +173,37 @@ Pane { } } + Label { + text: qsTr('Transaction 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: root.txid + 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('Transaction ID'), 'text': root.txid }) + dialog.open() + } + } + } + } Label { text: qsTr('Outputs') @@ -247,7 +248,7 @@ Pane { id: txdetails wallet: Daemon.currentWallet txid: root.txid - onLabelChanged: txDetailsChanged() + onLabelChanged: root.detailsChanged() } Component { diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index 9218de865..cd1728d59 100644 --- a/electrum/gui/qml/qeapp.py +++ b/electrum/gui/qml/qeapp.py @@ -24,6 +24,7 @@ from .qetypes import QEAmount from .qeaddressdetails import QEAddressDetails from .qetxdetails import QETxDetails from .qechannelopener import QEChannelOpener +from .qelnpaymentdetails import QELnPaymentDetails notification = None @@ -145,6 +146,7 @@ class ElectrumQmlApplication(QGuiApplication): qmlRegisterType(QEAddressDetails, 'org.electrum', 1, 0, 'AddressDetails') qmlRegisterType(QETxDetails, 'org.electrum', 1, 0, 'TxDetails') qmlRegisterType(QEChannelOpener, 'org.electrum', 1, 0, 'ChannelOpener') + qmlRegisterType(QELnPaymentDetails, 'org.electrum', 1, 0, 'LnPaymentDetails') qmlRegisterUncreatableType(QEAmount, 'org.electrum', 1, 0, 'Amount', 'Amount can only be used as property') diff --git a/electrum/gui/qml/qeconfig.py b/electrum/gui/qml/qeconfig.py index 76bca9e64..f8af476d6 100644 --- a/electrum/gui/qml/qeconfig.py +++ b/electrum/gui/qml/qeconfig.py @@ -3,7 +3,7 @@ from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject from decimal import Decimal from electrum.logging import get_logger -from electrum.util import DECIMAL_POINT_DEFAULT +from electrum.util import DECIMAL_POINT_DEFAULT, format_satoshis from .qetypes import QEAmount @@ -92,6 +92,23 @@ class QEConfig(QObject): else: return self.config.format_amount(satoshis) + @pyqtSlot(QEAmount, result=str) + @pyqtSlot(QEAmount, bool, result=str) + def formatMilliSats(self, amount, with_unit=False): + if isinstance(amount, QEAmount): + msats = amount.msatsInt + else: + return '---' + + s = format_satoshis(msats/1000, + decimal_point=self.decimal_point(), + precision=3) + return s + #if with_unit: + #return self.config.format_amount_and_units(msats) + #else: + #return self.config.format_amount(satoshis) + # TODO delegate all this to config.py/util.py def decimal_point(self): return self.config.get('decimal_point', DECIMAL_POINT_DEFAULT) diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index 93f3ac8f5..9dbd3daa5 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -46,6 +46,7 @@ class QEInvoice(QObject): _effectiveInvoice = None _canSave = False _canPay = False + _key = '' invoiceChanged = pyqtSignal() invoiceSaved = pyqtSignal() @@ -128,6 +129,17 @@ class QEInvoice(QObject): status = self._wallet.wallet.get_invoice_status(self._effectiveInvoice) return self._effectiveInvoice.get_status_str(status) + keyChanged = pyqtSignal() + @pyqtProperty(str, notify=keyChanged) + def key(self): + return self._key + + @key.setter + def key(self, key): + if self._key != key: + self._key = key + self.keyChanged.emit() + # single address only, TODO: n outputs @pyqtProperty(str, notify=invoiceChanged) def address(self): @@ -170,6 +182,7 @@ class QEInvoice(QObject): self._logger.debug(repr(invoice)) if invoice: self.set_effective_invoice(invoice) + self.key = key def set_effective_invoice(self, invoice: Invoice): self._effectiveInvoice = invoice @@ -264,9 +277,12 @@ class QEInvoice(QObject): else: self._logger.debug('flow with LN but not LN enabled AND having bip21 uri') self.setValidOnchainInvoice(self._bip21['address']) - elif not self._wallet.wallet.lnworker.channels: - self.validationWarning.emit('no_channels',_('Detected valid Lightning invoice, but there are no open channels')) + else: self.setValidLightningInvoice(lninvoice) + if not self._wallet.wallet.lnworker.channels: + self.validationWarning.emit('no_channels',_('Detected valid Lightning invoice, but there are no open channels')) + else: + self.validationSuccess.emit() else: self._logger.debug('flow without LN but having bip21 uri') if 'amount' not in self._bip21: #TODO can we have amount-less invoices? @@ -286,6 +302,7 @@ class QEInvoice(QObject): if not self._effectiveInvoice: return # TODO detect duplicate? + self.key = self._wallet.wallet.get_key_for_outgoing_invoice(self._effectiveInvoice) self._wallet.wallet.save_invoice(self._effectiveInvoice) self.invoiceSaved.emit() diff --git a/electrum/gui/qml/qeinvoicelistmodel.py b/electrum/gui/qml/qeinvoicelistmodel.py index ea97ae9bf..c9815c775 100644 --- a/electrum/gui/qml/qeinvoicelistmodel.py +++ b/electrum/gui/qml/qeinvoicelistmodel.py @@ -51,7 +51,7 @@ class QEAbstractInvoiceListModel(QAbstractListModel): invoices = [] for invoice in self.get_invoice_list(): item = self.invoice_to_model(invoice) - self._logger.debug(str(item)) + #self._logger.debug(str(item)) invoices.append(item) self.clear() diff --git a/electrum/gui/qml/qelnpaymentdetails.py b/electrum/gui/qml/qelnpaymentdetails.py new file mode 100644 index 000000000..6644ad295 --- /dev/null +++ b/electrum/gui/qml/qelnpaymentdetails.py @@ -0,0 +1,114 @@ +from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject + +from electrum.logging import get_logger +from electrum.util import format_time, bfh, format_time + +from .qewallet import QEWallet +from .qetypes import QEAmount + +class QELnPaymentDetails(QObject): + def __init__(self, parent=None): + super().__init__(parent) + + _logger = get_logger(__name__) + + _wallet = None + _key = None + _date = None + + detailsChanged = pyqtSignal() + + 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() + + keyChanged = pyqtSignal() + @pyqtProperty(str, notify=keyChanged) + def key(self): + return self._key + + @key.setter + def key(self, key: str): + if self._key != key: + self._logger.debug('key set -> %s' % key) + self._key = key + self.keyChanged.emit() + self.update() + + labelChanged = pyqtSignal() + @pyqtProperty(str, notify=labelChanged) + def label(self): + return self._label + + @pyqtSlot(str) + def set_label(self, label: str): + if label != self._label: + self._wallet.wallet.set_label(self._key, label) + self._label = label + self.labelChanged.emit() + + @pyqtProperty(str, notify=detailsChanged) + def status(self): + return self._status + + @pyqtProperty(str, notify=detailsChanged) + def date(self): + return self._date + + @pyqtProperty(str, notify=detailsChanged) + def payment_hash(self): + return self._phash + + @pyqtProperty(str, notify=detailsChanged) + def preimage(self): + return self._preimage + + @pyqtProperty(str, notify=detailsChanged) + def invoice(self): + return self._invoice + + @pyqtProperty(QEAmount, notify=detailsChanged) + def amount(self): + return self._amount + + @pyqtProperty(QEAmount, notify=detailsChanged) + def fee(self): + return self._fee + + def update(self): + if self._wallet is None: + self._logger.error('wallet undefined') + return + + if self._key not in self._wallet.wallet.lnworker.payments: + self._logger.error('payment_hash not found') + return + + # TODO this is horribly inefficient. need a payment getter/query method + tx = self._wallet.wallet.lnworker.get_lightning_history()[bfh(self._key)] + self._logger.debug(str(tx)) + + self._fee = QEAmount() if not tx['fee_msat'] else QEAmount(amount_msat=tx['fee_msat']) + self._amount = QEAmount(amount_msat=tx['amount_msat']) + self._label = tx['label'] + self._date = format_time(tx['timestamp']) + self._status = 'settled' # TODO: other states? get_lightning_history is deciding the filter for us :( + self._phash = tx['payment_hash'] + self._preimage = tx['preimage'] + + invoice = (self._wallet.wallet.get_invoice(self._key) + or self._wallet.wallet.get_request(self._key)) + self._logger.debug(str(invoice)) + if invoice: + self._invoice = invoice.lightning_invoice or '' + else: + self._invoice = '' + + self.detailsChanged.emit() diff --git a/electrum/gui/qml/qetransactionlistmodel.py b/electrum/gui/qml/qetransactionlistmodel.py index 8d8fb0e92..db1488c65 100644 --- a/electrum/gui/qml/qetransactionlistmodel.py +++ b/electrum/gui/qml/qetransactionlistmodel.py @@ -18,8 +18,8 @@ class QETransactionListModel(QAbstractListModel): # define listmodel rolemap _ROLE_NAMES=('txid','fee_sat','height','confirmations','timestamp','monotonic_timestamp', - 'incoming','bc_value','bc_balance','date','label','txpos_in_block','fee', - 'inputs','outputs','section') + 'incoming','value','balance','date','label','txpos_in_block','fee', + 'inputs','outputs','section','type','lightning','payment_hash','key') _ROLE_KEYS = range(Qt.UserRole, Qt.UserRole + len(_ROLE_NAMES)) _ROLE_MAP = dict(zip(_ROLE_KEYS, [bytearray(x.encode()) for x in _ROLE_NAMES])) _ROLE_RMAP = dict(zip(_ROLE_NAMES, _ROLE_KEYS)) @@ -46,12 +46,23 @@ class QETransactionListModel(QAbstractListModel): self.endResetModel() def tx_to_model(self, tx): + #self._logger.debug(str(tx)) item = tx - for output in item['outputs']: - output['value'] = output['value'].value - item['bc_value'] = QEAmount(amount_sat=item['bc_value'].value) - item['bc_balance'] = QEAmount(amount_sat=item['bc_balance'].value) + item['key'] = item['txid'] if 'txid' in item else item['payment_hash'] + + if not 'lightning' in item: + item['lightning'] = False + + if item['lightning']: + item['value'] = QEAmount(amount_sat=item['value'].value, amount_msat=item['amount_msat']) + item['balance'] = QEAmount(amount_sat=item['balance'].value, amount_msat=item['amount_msat']) + if item['type'] == 'payment': + item['incoming'] = True if item['direction'] == 'received' else False + item['confirmations'] = 0 + else: + item['value'] = QEAmount(amount_sat=item['value'].value) + item['balance'] = QEAmount(amount_sat=item['balance'].value) # newly arriving txs have no (block) timestamp # TODO? @@ -90,9 +101,9 @@ class QETransactionListModel(QAbstractListModel): # initial model data def init_model(self): - history = self.wallet.get_detailed_history(show_addresses = True) + history = self.wallet.get_full_history() txs = [] - for tx in history['transactions']: + for key, tx in history.items(): txs.append(self.tx_to_model(tx)) self.clear() @@ -104,7 +115,7 @@ class QETransactionListModel(QAbstractListModel): def update_tx(self, txid, info): i = 0 for tx in self.tx_history: - if tx['txid'] == txid: + if 'txid' in tx and tx['txid'] == txid: tx['height'] = info.height tx['confirmations'] = info.conf tx['timestamp'] = info.timestamp @@ -116,10 +127,10 @@ class QETransactionListModel(QAbstractListModel): i = i + 1 @pyqtSlot(str, str) - def update_tx_label(self, txid, label): + def update_tx_label(self, key, label): i = 0 for tx in self.tx_history: - if tx['txid'] == txid: + if tx['key'] == key: tx['label'] = label index = self.index(i,0) self.dataChanged.emit(index, index, [self._ROLE_RMAP['label']]) @@ -131,7 +142,7 @@ class QETransactionListModel(QAbstractListModel): self._logger.debug('updating height to %d' % height) i = 0 for tx in self.tx_history: - if tx['height'] > 0: + if 'height' in tx and tx['height'] > 0: tx['confirmations'] = height - tx['height'] + 1 index = self.index(i,0) roles = [self._ROLE_RMAP['confirmations']] diff --git a/electrum/gui/qml/qetxdetails.py b/electrum/gui/qml/qetxdetails.py index e8e427ce6..01df0c216 100644 --- a/electrum/gui/qml/qetxdetails.py +++ b/electrum/gui/qml/qetxdetails.py @@ -1,7 +1,5 @@ from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject -#from decimal import Decimal - from electrum.logging import get_logger from electrum.util import format_time diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 06df648a9..9b070bf7e 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -1,6 +1,8 @@ from typing import Optional, TYPE_CHECKING, Sequence, List, Union import queue import time +import asyncio +import threading from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl, QTimer @@ -47,9 +49,11 @@ class QEWallet(QObject): requestStatusChanged = pyqtSignal([str,int], arguments=['key','status']) requestCreateSuccess = pyqtSignal() requestCreateError = pyqtSignal([str,str], arguments=['code','error']) - invoiceStatusChanged = pyqtSignal([str], arguments=['key']) + invoiceStatusChanged = pyqtSignal([str,int], arguments=['key','status']) invoiceCreateSuccess = pyqtSignal() invoiceCreateError = pyqtSignal([str,str], arguments=['code','error']) + paymentSucceeded = pyqtSignal([str], arguments=['key']) + paymentFailed = pyqtSignal([str,str], arguments=['key','reason']) _network_signal = pyqtSignal(str, object) @@ -95,6 +99,10 @@ class QEWallet(QObject): def on_network_qt(self, event, args=None): # note: we get events from all wallets! args are heterogenous so we can't # shortcut here + if event != 'status': + wallet = args[0] + if wallet == self.wallet: + self._logger.debug('event %s' % event) if event == 'status': self.isUptodateChanged.emit() elif event == 'request_status': @@ -105,8 +113,11 @@ class QEWallet(QObject): elif event == 'invoice_status': wallet, key = args if wallet == self.wallet: - self._logger.debug('invoice status %d for key %s' % (c, key)) - self.invoiceStatusChanged.emit(key) + self._logger.debug('invoice status update for key %s' % key) + # FIXME event doesn't pass the new status, so we need to retrieve + invoice = self.wallet.get_invoice(key) + status = self.wallet.get_invoice_status(invoice) + self.invoiceStatusChanged.emit(key, status) elif event == 'new_transaction': wallet, tx = args if wallet == self.wallet: @@ -129,6 +140,15 @@ class QEWallet(QObject): wallet, = args if wallet == self.wallet: self.balanceChanged.emit() + elif event == 'payment_succeeded': + wallet, key = args + if wallet == self.wallet: + self.paymentSucceeded.emit(key) + self._historyModel.init_model() # TODO: be less dramatic + elif event == 'payment_failed': + wallet, key, reason = args + if wallet == self.wallet: + self.paymentFailed.emit(key, reason) else: self._logger.debug('unhandled event: %s %s' % (event, str(args))) @@ -346,6 +366,24 @@ class QEWallet(QObject): return + @pyqtSlot(str) + def pay_lightning_invoice(self, invoice_key): + self._logger.debug('about to pay LN') + invoice = self.wallet.get_invoice(invoice_key) + assert(invoice) + assert(invoice.lightning_invoice) + amount_msat = invoice.get_amount_msat() + def pay_thread(): + try: + coro = self.wallet.lnworker.pay_invoice(invoice.lightning_invoice, amount_msat=amount_msat) + fut = asyncio.run_coroutine_threadsafe(coro, self.wallet.network.asyncio_loop) + fut.result() + except Exception as e: + self.userNotify(repr(e)) + #self.app.show_error(repr(e)) + #self.save_invoice(invoice) + threading.Thread(target=pay_thread).start() + def create_bitcoin_request(self, amount: int, message: str, expiration: int, ignore_gap: bool) -> Optional[str]: addr = self.wallet.get_unused_address() if addr is None: