From 7bb4ea150f8cbc9da3edc589c94883c195a72d9a Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 30 Jan 2019 11:10:11 +0100 Subject: [PATCH] gui: show incoming lightning requests, add on-chain icon --- electrum/gui/qt/invoice_list.py | 51 ++++++++++++++++++++-------- electrum/gui/qt/main_window.py | 6 ++-- electrum/gui/qt/request_list.py | 58 ++++++++++++++------------------ electrum/gui/qt/util.py | 3 +- electrum/paymentrequest.py | 3 ++ icons/bitcoin.png | Bin 0 -> 8928 bytes 6 files changed, 71 insertions(+), 50 deletions(-) create mode 100644 icons/bitcoin.png diff --git a/electrum/gui/qt/invoice_list.py b/electrum/gui/qt/invoice_list.py index 11d9b1121..c04d0d1fc 100644 --- a/electrum/gui/qt/invoice_list.py +++ b/electrum/gui/qt/invoice_list.py @@ -30,7 +30,9 @@ from PyQt5.QtGui import QStandardItemModel, QStandardItem, QFont from PyQt5.QtWidgets import QHeaderView, QMenu from electrum.i18n import _ -from electrum.util import format_time +from electrum.util import format_time, pr_tooltips, PR_UNPAID +from electrum.lnutil import lndecode +from electrum.bitcoin import COIN from .util import (MyTreeView, read_QIcon, MONOSPACE_FONT, PR_UNPAID, pr_tooltips, import_meta_gui, export_meta_gui, pr_icons) @@ -40,26 +42,23 @@ class InvoiceList(MyTreeView): class Columns(IntEnum): DATE = 0 - REQUESTOR = 1 - DESCRIPTION = 2 - AMOUNT = 3 - STATUS = 4 + DESCRIPTION = 1 + AMOUNT = 2 + STATUS = 3 headers = { Columns.DATE: _('Expires'), - Columns.REQUESTOR: _('Requestor'), Columns.DESCRIPTION: _('Description'), Columns.AMOUNT: _('Amount'), Columns.STATUS: _('Status'), } - filter_columns = [Columns.DATE, Columns.REQUESTOR, Columns.DESCRIPTION, Columns.AMOUNT] + filter_columns = [Columns.DATE, Columns.DESCRIPTION, Columns.AMOUNT] def __init__(self, parent): super().__init__(parent, self.create_menu, stretch_column=self.Columns.DESCRIPTION, editable_columns=[]) self.setSortingEnabled(True) - self.setColumnWidth(self.Columns.REQUESTOR, 200) self.setModel(QStandardItemModel(self)) self.update() @@ -67,26 +66,50 @@ class InvoiceList(MyTreeView): inv_list = self.parent.invoices.unpaid_invoices() self.model().clear() self.update_headers(self.__class__.headers) - self.header().setSectionResizeMode(self.Columns.REQUESTOR, QHeaderView.Interactive) for idx, pr in enumerate(inv_list): key = pr.get_id() status = self.parent.invoices.get_status(key) if status is None: continue requestor = pr.get_requestor() - exp = pr.get_expiration_date() + exp = pr.get_time() date_str = format_time(exp) if exp else _('Never') - labels = [date_str, requestor, pr.memo, self.parent.format_amount(pr.get_amount(), whitespaces=True), pr_tooltips.get(status,'')] + labels = [date_str, '[%s] '%requestor + pr.memo, self.parent.format_amount(pr.get_amount(), whitespaces=True), pr_tooltips.get(status,'')] items = [QStandardItem(e) for e in labels] self.set_editability(items) + items[self.Columns.DATE].setIcon(read_QIcon('bitcoin.png')) items[self.Columns.STATUS].setIcon(read_QIcon(pr_icons.get(status))) items[self.Columns.DATE].setData(key, role=Qt.UserRole) - items[self.Columns.REQUESTOR].setFont(QFont(MONOSPACE_FONT)) - items[self.Columns.AMOUNT].setFont(QFont(MONOSPACE_FONT)) self.model().insertRow(idx, items) + + lnworker = self.parent.wallet.lnworker + for key, (preimage_hex, invoice, is_received, pay_timestamp) in lnworker.invoices.items(): + if is_received: + continue + status = lnworker.get_invoice_status(key) + lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP) + amount_sat = lnaddr.amount*COIN if lnaddr.amount else None + amount_str = self.parent.format_amount(amount_sat) if amount_sat else '' + description = '' + for k,v in lnaddr.tags: + if k == 'd': + description = v + break + date_str = format_time(lnaddr.date) + labels = [date_str, description, amount_str, pr_tooltips.get(status,'')] + items = [QStandardItem(e) for e in labels] + #items[0].setData(REQUEST_TYPE_LN, ROLE_REQUEST_TYPE) + #items[0].setData(key, ROLE_RHASH_OR_ADDR) + items[0].setIcon(self.icon_cache.get(':icons/lightning.png')) + items[3].setIcon(self.icon_cache.get(pr_icons.get(status))) + self.model().insertRow(self.model().rowCount(), items) + self.selectionModel().select(self.model().index(0,0), QItemSelectionModel.SelectCurrent) + # sort requests by date + self.model().sort(0) + # hide list if empty if self.parent.isVisible(): - b = len(inv_list) > 0 + b = self.model().rowCount() > 0 self.setVisible(b) self.parent.invoices_label.setVisible(b) self.filter() diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 9cc070c56..1b3ad08bc 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -953,8 +953,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): grid.addWidget(self.expires_label, 2, 1) self.create_invoice_button = QPushButton(_('On-chain')) + self.create_invoice_button.setIcon(QIcon(":icons/bitcoin.png")) self.create_invoice_button.clicked.connect(lambda: self.create_invoice(False)) self.create_lightning_invoice_button = QPushButton(_('Lightning')) + self.create_lightning_invoice_button.setIcon(QIcon(":icons/lightning.png")) self.create_lightning_invoice_button.clicked.connect(lambda: self.create_invoice(True)) self.receive_buttons = buttons = QHBoxLayout() buttons.addStretch(1) @@ -974,7 +976,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): self.receive_qr.enterEvent = lambda x: self.app.setOverrideCursor(QCursor(Qt.PointingHandCursor)) self.receive_qr.leaveEvent = lambda x: self.app.setOverrideCursor(QCursor(Qt.ArrowCursor)) - self.receive_requests_label = QLabel(_('Requests')) + self.receive_requests_label = QLabel(_('Incoming invoices')) from .request_list import RequestList self.request_list = RequestList(self) @@ -1395,7 +1397,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): self.fee_e.textChanged.connect(entry_changed) self.feerate_e.textChanged.connect(entry_changed) - self.invoices_label = QLabel(_('Invoices')) + self.invoices_label = QLabel(_('Outgoing invoices')) from .invoice_list import InvoiceList self.invoice_list = InvoiceList(self) diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py index 428a7603c..0ba5cee34 100644 --- a/electrum/gui/qt/request_list.py +++ b/electrum/gui/qt/request_list.py @@ -51,14 +51,12 @@ class RequestList(MyTreeView): class Columns(IntEnum): DATE = 0 - TYPE = 1 - DESCRIPTION = 2 - AMOUNT = 3 - STATUS = 4 + DESCRIPTION = 1 + AMOUNT = 2 + STATUS = 3 headers = { Columns.DATE: _('Date'), - Columns.TYPE: _('Type'), Columns.DESCRIPTION: _('Description'), Columns.AMOUNT: _('Amount'), Columns.STATUS: _('Status'), @@ -68,7 +66,7 @@ class RequestList(MyTreeView): def __init__(self, parent): super().__init__(parent, self.create_menu, stretch_column=self.Columns.DESCRIPTION, - editable_columns=[]) + editable_columns=[self.Columns.AMOUNT]) self.setModel(QStandardItemModel(self)) self.setSortingEnabled(True) self.update() @@ -76,7 +74,7 @@ class RequestList(MyTreeView): def select_key(self, key): for i in range(self.model().rowCount()): - item = self.model().index(i, 0) + item = self.model().index(i, self.Columns.DATE) row_key = item.data(ROLE_RHASH_OR_ADDR) if item.data(ROLE_REQUEST_TYPE) == REQUEST_TYPE_LN: row_key = self.wallet.lnworker.invoices[row_key][1] @@ -86,7 +84,7 @@ class RequestList(MyTreeView): def item_changed(self, idx): # TODO use siblingAtColumn when min Qt version is >=5.11 - item = self.model().itemFromIndex(idx.sibling(idx.row(), 0)) + item = self.model().itemFromIndex(idx.sibling(idx.row(), self.Columns.DATE)) request_type = item.data(ROLE_REQUEST_TYPE) key = item.data(ROLE_RHASH_OR_ADDR) if request_type == REQUEST_TYPE_BITCOIN: @@ -104,19 +102,8 @@ class RequestList(MyTreeView): def update(self): self.wallet = self.parent.wallet - # hide receive tab if no receive requests available - if self.parent.isVisible(): - b = len(self.wallet.receive_requests) > 0 or len(self.wallet.lnworker.invoices) > 0 - self.setVisible(b) - self.parent.receive_requests_label.setVisible(b) - if not b: - self.parent.expires_label.hide() - self.parent.expires_combo.show() - domain = self.wallet.get_receiving_addresses() - self.parent.update_receive_address_styling() - self.model().clear() self.update_headers(self.__class__.headers) for req in self.wallet.get_sorted_requests(self.config): @@ -132,17 +119,18 @@ class RequestList(MyTreeView): signature = req.get('sig') requestor = req.get('name', '') amount_str = self.parent.format_amount(amount) if amount else "" - labels = [date, 'on-chain', message, amount_str, pr_tooltips.get(status,'')] + labels = [date, message, amount_str, pr_tooltips.get(status,'')] items = [QStandardItem(e) for e in labels] self.set_editability(items) if signature is not None: - items[self.Columns.TYPE].setIcon(read_QIcon("seal.png")) - items[self.Columns.TYPE].setToolTip(f'signed by {requestor}') - if status is not PR_UNKNOWN: - items[self.Columns.STATUS].setIcon(read_QIcon(pr_icons.get(status))) + items[self.Columns.DATE].setIcon(read_QIcon("seal.png")) + items[self.Columns.DATE].setToolTip(f'signed by {requestor}') + else: + items[self.Columns.DATE].setIcon(read_QIcon("bitcoin.png")) + items[self.Columns.STATUS].setIcon(read_QIcon(pr_icons.get(status))) self.model().insertRow(self.model().rowCount(), items) - items[0].setData(REQUEST_TYPE_BITCOIN, ROLE_REQUEST_TYPE) - items[0].setData(address, ROLE_RHASH_OR_ADDR) + items[self.Columns.DATE].setData(REQUEST_TYPE_BITCOIN, ROLE_REQUEST_TYPE) + items[self.Columns.DATE].setData(address, ROLE_RHASH_OR_ADDR) self.filter() # lightning lnworker = self.wallet.lnworker @@ -159,16 +147,20 @@ class RequestList(MyTreeView): description = v break date = format_time(lnaddr.date) - labels = [date, 'lightning', description, amount_str, pr_tooltips.get(status,'')] + labels = [date, description, amount_str, pr_tooltips.get(status,'')] items = [QStandardItem(e) for e in labels] - items[1].setIcon(self.icon_cache.get(":icons/lightning.png")) - items[0].setData(REQUEST_TYPE_LN, ROLE_REQUEST_TYPE) - items[0].setData(key, ROLE_RHASH_OR_ADDR) - if status is not PR_UNKNOWN: - items[4].setIcon(self.icon_cache.get(pr_icons.get(status))) + items[self.Columns.DATE].setIcon(self.icon_cache.get(":icons/lightning.png")) + items[self.Columns.DATE].setData(REQUEST_TYPE_LN, ROLE_REQUEST_TYPE) + items[self.Columns.DATE].setData(key, ROLE_RHASH_OR_ADDR) + items[self.Columns.STATUS].setIcon(self.icon_cache.get(pr_icons.get(status))) self.model().insertRow(self.model().rowCount(), items) # sort requests by date - self.model().sort(0) + self.model().sort(self.Columns.DATE) + # hide list if empty + if self.parent.isVisible(): + b = self.model().rowCount() > 0 + self.setVisible(b) + self.parent.receive_requests_label.setVisible(b) def create_menu(self, position): idx = self.indexAt(position) diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 60c22203a..ea3681dd2 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -24,7 +24,7 @@ from PyQt5.QtWidgets import (QPushButton, QLabel, QMessageBox, QHBoxLayout, from electrum.i18n import _, languages from electrum.util import FileImportFailed, FileExportFailed, make_aiohttp_session, PrintError, resource_path -from electrum.util import PR_UNPAID, PR_PAID, PR_EXPIRED, PR_INFLIGHT +from electrum.util import PR_UNPAID, PR_PAID, PR_EXPIRED, PR_INFLIGHT, PR_UNKNOWN if TYPE_CHECKING: from .main_window import ElectrumWindow @@ -41,6 +41,7 @@ else: dialogs = [] pr_icons = { + PR_UNKNOWN:"unpaid.png", PR_UNPAID:"unpaid.png", PR_PAID:"confirmed.png", PR_EXPIRED:"expired.png", diff --git a/electrum/paymentrequest.py b/electrum/paymentrequest.py index 29712a945..df1ec423b 100644 --- a/electrum/paymentrequest.py +++ b/electrum/paymentrequest.py @@ -246,6 +246,9 @@ class PaymentRequest: return None return self.details.expires and self.details.expires < int(time.time()) + def get_time(self): + return self.details.time + def get_expiration_date(self): return self.details.expires diff --git a/icons/bitcoin.png b/icons/bitcoin.png new file mode 100644 index 0000000000000000000000000000000000000000..a597e86135c8f391aabe333f96ed89603ff56a82 GIT binary patch literal 8928 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 BmLmWF literal 0 HcmV?d00001