gui: show incoming lightning requests, add on-chain icon
This commit is contained in:
@@ -30,7 +30,9 @@ from PyQt5.QtGui import QStandardItemModel, QStandardItem, QFont
|
|||||||
from PyQt5.QtWidgets import QHeaderView, QMenu
|
from PyQt5.QtWidgets import QHeaderView, QMenu
|
||||||
|
|
||||||
from electrum.i18n import _
|
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,
|
from .util import (MyTreeView, read_QIcon, MONOSPACE_FONT, PR_UNPAID,
|
||||||
pr_tooltips, import_meta_gui, export_meta_gui, pr_icons)
|
pr_tooltips, import_meta_gui, export_meta_gui, pr_icons)
|
||||||
@@ -40,26 +42,23 @@ class InvoiceList(MyTreeView):
|
|||||||
|
|
||||||
class Columns(IntEnum):
|
class Columns(IntEnum):
|
||||||
DATE = 0
|
DATE = 0
|
||||||
REQUESTOR = 1
|
DESCRIPTION = 1
|
||||||
DESCRIPTION = 2
|
AMOUNT = 2
|
||||||
AMOUNT = 3
|
STATUS = 3
|
||||||
STATUS = 4
|
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
Columns.DATE: _('Expires'),
|
Columns.DATE: _('Expires'),
|
||||||
Columns.REQUESTOR: _('Requestor'),
|
|
||||||
Columns.DESCRIPTION: _('Description'),
|
Columns.DESCRIPTION: _('Description'),
|
||||||
Columns.AMOUNT: _('Amount'),
|
Columns.AMOUNT: _('Amount'),
|
||||||
Columns.STATUS: _('Status'),
|
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):
|
def __init__(self, parent):
|
||||||
super().__init__(parent, self.create_menu,
|
super().__init__(parent, self.create_menu,
|
||||||
stretch_column=self.Columns.DESCRIPTION,
|
stretch_column=self.Columns.DESCRIPTION,
|
||||||
editable_columns=[])
|
editable_columns=[])
|
||||||
self.setSortingEnabled(True)
|
self.setSortingEnabled(True)
|
||||||
self.setColumnWidth(self.Columns.REQUESTOR, 200)
|
|
||||||
self.setModel(QStandardItemModel(self))
|
self.setModel(QStandardItemModel(self))
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
@@ -67,26 +66,50 @@ class InvoiceList(MyTreeView):
|
|||||||
inv_list = self.parent.invoices.unpaid_invoices()
|
inv_list = self.parent.invoices.unpaid_invoices()
|
||||||
self.model().clear()
|
self.model().clear()
|
||||||
self.update_headers(self.__class__.headers)
|
self.update_headers(self.__class__.headers)
|
||||||
self.header().setSectionResizeMode(self.Columns.REQUESTOR, QHeaderView.Interactive)
|
|
||||||
for idx, pr in enumerate(inv_list):
|
for idx, pr in enumerate(inv_list):
|
||||||
key = pr.get_id()
|
key = pr.get_id()
|
||||||
status = self.parent.invoices.get_status(key)
|
status = self.parent.invoices.get_status(key)
|
||||||
if status is None:
|
if status is None:
|
||||||
continue
|
continue
|
||||||
requestor = pr.get_requestor()
|
requestor = pr.get_requestor()
|
||||||
exp = pr.get_expiration_date()
|
exp = pr.get_time()
|
||||||
date_str = format_time(exp) if exp else _('Never')
|
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]
|
items = [QStandardItem(e) for e in labels]
|
||||||
self.set_editability(items)
|
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.STATUS].setIcon(read_QIcon(pr_icons.get(status)))
|
||||||
items[self.Columns.DATE].setData(key, role=Qt.UserRole)
|
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)
|
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)
|
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():
|
if self.parent.isVisible():
|
||||||
b = len(inv_list) > 0
|
b = self.model().rowCount() > 0
|
||||||
self.setVisible(b)
|
self.setVisible(b)
|
||||||
self.parent.invoices_label.setVisible(b)
|
self.parent.invoices_label.setVisible(b)
|
||||||
self.filter()
|
self.filter()
|
||||||
|
|||||||
@@ -953,8 +953,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||||||
grid.addWidget(self.expires_label, 2, 1)
|
grid.addWidget(self.expires_label, 2, 1)
|
||||||
|
|
||||||
self.create_invoice_button = QPushButton(_('On-chain'))
|
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_invoice_button.clicked.connect(lambda: self.create_invoice(False))
|
||||||
self.create_lightning_invoice_button = QPushButton(_('Lightning'))
|
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.create_lightning_invoice_button.clicked.connect(lambda: self.create_invoice(True))
|
||||||
self.receive_buttons = buttons = QHBoxLayout()
|
self.receive_buttons = buttons = QHBoxLayout()
|
||||||
buttons.addStretch(1)
|
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.enterEvent = lambda x: self.app.setOverrideCursor(QCursor(Qt.PointingHandCursor))
|
||||||
self.receive_qr.leaveEvent = lambda x: self.app.setOverrideCursor(QCursor(Qt.ArrowCursor))
|
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
|
from .request_list import RequestList
|
||||||
self.request_list = RequestList(self)
|
self.request_list = RequestList(self)
|
||||||
@@ -1395,7 +1397,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||||||
self.fee_e.textChanged.connect(entry_changed)
|
self.fee_e.textChanged.connect(entry_changed)
|
||||||
self.feerate_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
|
from .invoice_list import InvoiceList
|
||||||
self.invoice_list = InvoiceList(self)
|
self.invoice_list = InvoiceList(self)
|
||||||
|
|
||||||
|
|||||||
@@ -51,14 +51,12 @@ class RequestList(MyTreeView):
|
|||||||
|
|
||||||
class Columns(IntEnum):
|
class Columns(IntEnum):
|
||||||
DATE = 0
|
DATE = 0
|
||||||
TYPE = 1
|
DESCRIPTION = 1
|
||||||
DESCRIPTION = 2
|
AMOUNT = 2
|
||||||
AMOUNT = 3
|
STATUS = 3
|
||||||
STATUS = 4
|
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
Columns.DATE: _('Date'),
|
Columns.DATE: _('Date'),
|
||||||
Columns.TYPE: _('Type'),
|
|
||||||
Columns.DESCRIPTION: _('Description'),
|
Columns.DESCRIPTION: _('Description'),
|
||||||
Columns.AMOUNT: _('Amount'),
|
Columns.AMOUNT: _('Amount'),
|
||||||
Columns.STATUS: _('Status'),
|
Columns.STATUS: _('Status'),
|
||||||
@@ -68,7 +66,7 @@ class RequestList(MyTreeView):
|
|||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
super().__init__(parent, self.create_menu,
|
super().__init__(parent, self.create_menu,
|
||||||
stretch_column=self.Columns.DESCRIPTION,
|
stretch_column=self.Columns.DESCRIPTION,
|
||||||
editable_columns=[])
|
editable_columns=[self.Columns.AMOUNT])
|
||||||
self.setModel(QStandardItemModel(self))
|
self.setModel(QStandardItemModel(self))
|
||||||
self.setSortingEnabled(True)
|
self.setSortingEnabled(True)
|
||||||
self.update()
|
self.update()
|
||||||
@@ -76,7 +74,7 @@ class RequestList(MyTreeView):
|
|||||||
|
|
||||||
def select_key(self, key):
|
def select_key(self, key):
|
||||||
for i in range(self.model().rowCount()):
|
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)
|
row_key = item.data(ROLE_RHASH_OR_ADDR)
|
||||||
if item.data(ROLE_REQUEST_TYPE) == REQUEST_TYPE_LN:
|
if item.data(ROLE_REQUEST_TYPE) == REQUEST_TYPE_LN:
|
||||||
row_key = self.wallet.lnworker.invoices[row_key][1]
|
row_key = self.wallet.lnworker.invoices[row_key][1]
|
||||||
@@ -86,7 +84,7 @@ class RequestList(MyTreeView):
|
|||||||
|
|
||||||
def item_changed(self, idx):
|
def item_changed(self, idx):
|
||||||
# TODO use siblingAtColumn when min Qt version is >=5.11
|
# 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)
|
request_type = item.data(ROLE_REQUEST_TYPE)
|
||||||
key = item.data(ROLE_RHASH_OR_ADDR)
|
key = item.data(ROLE_RHASH_OR_ADDR)
|
||||||
if request_type == REQUEST_TYPE_BITCOIN:
|
if request_type == REQUEST_TYPE_BITCOIN:
|
||||||
@@ -104,19 +102,8 @@ class RequestList(MyTreeView):
|
|||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
self.wallet = self.parent.wallet
|
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()
|
domain = self.wallet.get_receiving_addresses()
|
||||||
|
|
||||||
self.parent.update_receive_address_styling()
|
self.parent.update_receive_address_styling()
|
||||||
|
|
||||||
self.model().clear()
|
self.model().clear()
|
||||||
self.update_headers(self.__class__.headers)
|
self.update_headers(self.__class__.headers)
|
||||||
for req in self.wallet.get_sorted_requests(self.config):
|
for req in self.wallet.get_sorted_requests(self.config):
|
||||||
@@ -132,17 +119,18 @@ class RequestList(MyTreeView):
|
|||||||
signature = req.get('sig')
|
signature = req.get('sig')
|
||||||
requestor = req.get('name', '')
|
requestor = req.get('name', '')
|
||||||
amount_str = self.parent.format_amount(amount) if amount else ""
|
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]
|
items = [QStandardItem(e) for e in labels]
|
||||||
self.set_editability(items)
|
self.set_editability(items)
|
||||||
if signature is not None:
|
if signature is not None:
|
||||||
items[self.Columns.TYPE].setIcon(read_QIcon("seal.png"))
|
items[self.Columns.DATE].setIcon(read_QIcon("seal.png"))
|
||||||
items[self.Columns.TYPE].setToolTip(f'signed by {requestor}')
|
items[self.Columns.DATE].setToolTip(f'signed by {requestor}')
|
||||||
if status is not PR_UNKNOWN:
|
else:
|
||||||
items[self.Columns.STATUS].setIcon(read_QIcon(pr_icons.get(status)))
|
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)
|
self.model().insertRow(self.model().rowCount(), items)
|
||||||
items[0].setData(REQUEST_TYPE_BITCOIN, ROLE_REQUEST_TYPE)
|
items[self.Columns.DATE].setData(REQUEST_TYPE_BITCOIN, ROLE_REQUEST_TYPE)
|
||||||
items[0].setData(address, ROLE_RHASH_OR_ADDR)
|
items[self.Columns.DATE].setData(address, ROLE_RHASH_OR_ADDR)
|
||||||
self.filter()
|
self.filter()
|
||||||
# lightning
|
# lightning
|
||||||
lnworker = self.wallet.lnworker
|
lnworker = self.wallet.lnworker
|
||||||
@@ -159,16 +147,20 @@ class RequestList(MyTreeView):
|
|||||||
description = v
|
description = v
|
||||||
break
|
break
|
||||||
date = format_time(lnaddr.date)
|
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 = [QStandardItem(e) for e in labels]
|
||||||
items[1].setIcon(self.icon_cache.get(":icons/lightning.png"))
|
items[self.Columns.DATE].setIcon(self.icon_cache.get(":icons/lightning.png"))
|
||||||
items[0].setData(REQUEST_TYPE_LN, ROLE_REQUEST_TYPE)
|
items[self.Columns.DATE].setData(REQUEST_TYPE_LN, ROLE_REQUEST_TYPE)
|
||||||
items[0].setData(key, ROLE_RHASH_OR_ADDR)
|
items[self.Columns.DATE].setData(key, ROLE_RHASH_OR_ADDR)
|
||||||
if status is not PR_UNKNOWN:
|
items[self.Columns.STATUS].setIcon(self.icon_cache.get(pr_icons.get(status)))
|
||||||
items[4].setIcon(self.icon_cache.get(pr_icons.get(status)))
|
|
||||||
self.model().insertRow(self.model().rowCount(), items)
|
self.model().insertRow(self.model().rowCount(), items)
|
||||||
# sort requests by date
|
# 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):
|
def create_menu(self, position):
|
||||||
idx = self.indexAt(position)
|
idx = self.indexAt(position)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from PyQt5.QtWidgets import (QPushButton, QLabel, QMessageBox, QHBoxLayout,
|
|||||||
|
|
||||||
from electrum.i18n import _, languages
|
from electrum.i18n import _, languages
|
||||||
from electrum.util import FileImportFailed, FileExportFailed, make_aiohttp_session, PrintError, resource_path
|
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:
|
if TYPE_CHECKING:
|
||||||
from .main_window import ElectrumWindow
|
from .main_window import ElectrumWindow
|
||||||
@@ -41,6 +41,7 @@ else:
|
|||||||
dialogs = []
|
dialogs = []
|
||||||
|
|
||||||
pr_icons = {
|
pr_icons = {
|
||||||
|
PR_UNKNOWN:"unpaid.png",
|
||||||
PR_UNPAID:"unpaid.png",
|
PR_UNPAID:"unpaid.png",
|
||||||
PR_PAID:"confirmed.png",
|
PR_PAID:"confirmed.png",
|
||||||
PR_EXPIRED:"expired.png",
|
PR_EXPIRED:"expired.png",
|
||||||
|
|||||||
@@ -246,6 +246,9 @@ class PaymentRequest:
|
|||||||
return None
|
return None
|
||||||
return self.details.expires and self.details.expires < int(time.time())
|
return self.details.expires and self.details.expires < int(time.time())
|
||||||
|
|
||||||
|
def get_time(self):
|
||||||
|
return self.details.time
|
||||||
|
|
||||||
def get_expiration_date(self):
|
def get_expiration_date(self):
|
||||||
return self.details.expires
|
return self.details.expires
|
||||||
|
|
||||||
|
|||||||
BIN
icons/bitcoin.png
Normal file
BIN
icons/bitcoin.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.7 KiB |
Reference in New Issue
Block a user