invoice_list.py (8468B)
1 #!/usr/bin/env python 2 # 3 # Electrum - lightweight Bitcoin client 4 # Copyright (C) 2015 Thomas Voegtlin 5 # 6 # Permission is hereby granted, free of charge, to any person 7 # obtaining a copy of this software and associated documentation files 8 # (the "Software"), to deal in the Software without restriction, 9 # including without limitation the rights to use, copy, modify, merge, 10 # publish, distribute, sublicense, and/or sell copies of the Software, 11 # and to permit persons to whom the Software is furnished to do so, 12 # subject to the following conditions: 13 # 14 # The above copyright notice and this permission notice shall be 15 # included in all copies or substantial portions of the Software. 16 # 17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 21 # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 22 # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 23 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 # SOFTWARE. 25 26 from enum import IntEnum 27 from typing import Sequence 28 29 from PyQt5.QtCore import Qt, QItemSelectionModel 30 from PyQt5.QtGui import QStandardItemModel, QStandardItem 31 from PyQt5.QtWidgets import QAbstractItemView 32 from PyQt5.QtWidgets import QMenu, QVBoxLayout, QTreeWidget, QTreeWidgetItem, QHeaderView 33 34 from electrum.i18n import _ 35 from electrum.util import format_time 36 from electrum.invoices import Invoice, PR_UNPAID, PR_PAID, PR_INFLIGHT, PR_FAILED, PR_TYPE_ONCHAIN, PR_TYPE_LN 37 from electrum.lnutil import HtlcLog 38 39 from .util import MyTreeView, read_QIcon, MySortModel, pr_icons 40 from .util import CloseButton, Buttons 41 from .util import WindowModalDialog 42 43 44 45 ROLE_REQUEST_TYPE = Qt.UserRole 46 ROLE_REQUEST_ID = Qt.UserRole + 1 47 ROLE_SORT_ORDER = Qt.UserRole + 2 48 49 50 class InvoiceList(MyTreeView): 51 52 class Columns(IntEnum): 53 DATE = 0 54 DESCRIPTION = 1 55 AMOUNT = 2 56 STATUS = 3 57 58 headers = { 59 Columns.DATE: _('Date'), 60 Columns.DESCRIPTION: _('Description'), 61 Columns.AMOUNT: _('Amount'), 62 Columns.STATUS: _('Status'), 63 } 64 filter_columns = [Columns.DATE, Columns.DESCRIPTION, Columns.AMOUNT] 65 66 def __init__(self, parent): 67 super().__init__(parent, self.create_menu, 68 stretch_column=self.Columns.DESCRIPTION, 69 editable_columns=[]) 70 self.std_model = QStandardItemModel(self) 71 self.proxy = MySortModel(self, sort_role=ROLE_SORT_ORDER) 72 self.proxy.setSourceModel(self.std_model) 73 self.setModel(self.proxy) 74 self.setSortingEnabled(True) 75 self.setSelectionMode(QAbstractItemView.ExtendedSelection) 76 self.update() 77 78 def update_item(self, key, invoice: Invoice): 79 model = self.std_model 80 for row in range(0, model.rowCount()): 81 item = model.item(row, 0) 82 if item.data(ROLE_REQUEST_ID) == key: 83 break 84 else: 85 return 86 status_item = model.item(row, self.Columns.STATUS) 87 status = self.parent.wallet.get_invoice_status(invoice) 88 status_str = invoice.get_status_str(status) 89 if self.parent.wallet.lnworker: 90 log = self.parent.wallet.lnworker.logs.get(key) 91 if log and status == PR_INFLIGHT: 92 status_str += '... (%d)'%len(log) 93 status_item.setText(status_str) 94 status_item.setIcon(read_QIcon(pr_icons.get(status))) 95 96 def update(self): 97 # not calling maybe_defer_update() as it interferes with conditional-visibility 98 self.proxy.setDynamicSortFilter(False) # temp. disable re-sorting after every change 99 self.std_model.clear() 100 self.update_headers(self.__class__.headers) 101 for idx, item in enumerate(self.parent.wallet.get_unpaid_invoices()): 102 key = self.parent.wallet.get_key_for_outgoing_invoice(item) 103 if item.is_lightning(): 104 icon_name = 'lightning.png' 105 else: 106 icon_name = 'bitcoin.png' 107 if item.bip70: 108 icon_name = 'seal.png' 109 status = self.parent.wallet.get_invoice_status(item) 110 status_str = item.get_status_str(status) 111 message = item.message 112 amount = item.get_amount_sat() 113 timestamp = item.time or 0 114 date_str = format_time(timestamp) if timestamp else _('Unknown') 115 amount_str = self.parent.format_amount(amount, whitespaces=True) 116 labels = [date_str, message, amount_str, status_str] 117 items = [QStandardItem(e) for e in labels] 118 self.set_editability(items) 119 items[self.Columns.DATE].setIcon(read_QIcon(icon_name)) 120 items[self.Columns.STATUS].setIcon(read_QIcon(pr_icons.get(status))) 121 items[self.Columns.DATE].setData(key, role=ROLE_REQUEST_ID) 122 items[self.Columns.DATE].setData(item.type, role=ROLE_REQUEST_TYPE) 123 items[self.Columns.DATE].setData(timestamp, role=ROLE_SORT_ORDER) 124 self.std_model.insertRow(idx, items) 125 self.filter() 126 self.proxy.setDynamicSortFilter(True) 127 # sort requests by date 128 self.sortByColumn(self.Columns.DATE, Qt.DescendingOrder) 129 # hide list if empty 130 if self.parent.isVisible(): 131 b = self.std_model.rowCount() > 0 132 self.setVisible(b) 133 self.parent.invoices_label.setVisible(b) 134 135 def create_menu(self, position): 136 wallet = self.parent.wallet 137 items = self.selected_in_column(0) 138 if len(items)>1: 139 keys = [ item.data(ROLE_REQUEST_ID) for item in items] 140 invoices = [ wallet.invoices.get(key) for key in keys] 141 can_batch_pay = all([i.type == PR_TYPE_ONCHAIN and wallet.get_invoice_status(i) == PR_UNPAID for i in invoices]) 142 menu = QMenu(self) 143 if can_batch_pay: 144 menu.addAction(_("Batch pay invoices") + "...", lambda: self.parent.pay_multiple_invoices(invoices)) 145 menu.addAction(_("Delete invoices"), lambda: self.parent.delete_invoices(keys)) 146 menu.exec_(self.viewport().mapToGlobal(position)) 147 return 148 idx = self.indexAt(position) 149 item = self.item_from_index(idx) 150 item_col0 = self.item_from_index(idx.sibling(idx.row(), self.Columns.DATE)) 151 if not item or not item_col0: 152 return 153 key = item_col0.data(ROLE_REQUEST_ID) 154 invoice = self.parent.wallet.get_invoice(key) 155 menu = QMenu(self) 156 self.add_copy_menu(menu, idx) 157 if invoice.is_lightning(): 158 menu.addAction(_("Details"), lambda: self.parent.show_lightning_invoice(invoice)) 159 else: 160 if len(invoice.outputs) == 1: 161 menu.addAction(_("Copy Address"), lambda: self.parent.do_copy(invoice.get_address(), title='Bitcoin Address')) 162 menu.addAction(_("Details"), lambda: self.parent.show_onchain_invoice(invoice)) 163 status = wallet.get_invoice_status(invoice) 164 if status == PR_UNPAID: 165 menu.addAction(_("Pay") + "...", lambda: self.parent.do_pay_invoice(invoice)) 166 if status == PR_FAILED: 167 menu.addAction(_("Retry"), lambda: self.parent.do_pay_invoice(invoice)) 168 if self.parent.wallet.lnworker: 169 log = self.parent.wallet.lnworker.logs.get(key) 170 if log: 171 menu.addAction(_("View log"), lambda: self.show_log(key, log)) 172 menu.addAction(_("Delete"), lambda: self.parent.delete_invoices([key])) 173 menu.exec_(self.viewport().mapToGlobal(position)) 174 175 def show_log(self, key, log: Sequence[HtlcLog]): 176 d = WindowModalDialog(self, _("Payment log")) 177 d.setMinimumWidth(600) 178 vbox = QVBoxLayout(d) 179 log_w = QTreeWidget() 180 log_w.setHeaderLabels([_('Hops'), _('Channel ID'), _('Message')]) 181 log_w.header().setSectionResizeMode(2, QHeaderView.Stretch) 182 log_w.header().setSectionResizeMode(1, QHeaderView.ResizeToContents) 183 for payment_attempt_log in log: 184 route_str, chan_str, message = payment_attempt_log.formatted_tuple() 185 x = QTreeWidgetItem([route_str, chan_str, message]) 186 log_w.addTopLevelItem(x) 187 vbox.addWidget(log_w) 188 vbox.addLayout(Buttons(CloseButton(d))) 189 d.exec_()