URI: 
       thistory_list.py - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
       thistory_list.py (36531B)
       ---
            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 import os
           27 import sys
           28 import datetime
           29 from datetime import date
           30 from typing import TYPE_CHECKING, Tuple, Dict
           31 import threading
           32 from enum import IntEnum
           33 from decimal import Decimal
           34 
           35 from PyQt5.QtGui import QMouseEvent, QFont, QBrush, QColor
           36 from PyQt5.QtCore import (Qt, QPersistentModelIndex, QModelIndex, QAbstractItemModel,
           37                           QSortFilterProxyModel, QVariant, QItemSelectionModel, QDate, QPoint)
           38 from PyQt5.QtWidgets import (QMenu, QHeaderView, QLabel, QMessageBox,
           39                              QPushButton, QComboBox, QVBoxLayout, QCalendarWidget,
           40                              QGridLayout)
           41 
           42 from electrum.address_synchronizer import TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE
           43 from electrum.i18n import _
           44 from electrum.util import (block_explorer_URL, profiler, TxMinedInfo,
           45                            OrderedDictWithIndex, timestamp_to_datetime,
           46                            Satoshis, Fiat, format_time)
           47 from electrum.logging import get_logger, Logger
           48 
           49 from .custom_model import CustomNode, CustomModel
           50 from .util import (read_QIcon, MONOSPACE_FONT, Buttons, CancelButton, OkButton,
           51                    filename_field, MyTreeView, AcceptFileDragDrop, WindowModalDialog,
           52                    CloseButton, webopen)
           53 
           54 if TYPE_CHECKING:
           55     from electrum.wallet import Abstract_Wallet
           56     from .main_window import ElectrumWindow
           57 
           58 
           59 _logger = get_logger(__name__)
           60 
           61 
           62 try:
           63     from electrum.plot import plot_history, NothingToPlotException
           64 except:
           65     _logger.info("could not import electrum.plot. This feature needs matplotlib to be installed.")
           66     plot_history = None
           67 
           68 # note: this list needs to be kept in sync with another in kivy
           69 TX_ICONS = [
           70     "unconfirmed.png",
           71     "warning.png",
           72     "unconfirmed.png",
           73     "offline_tx.png",
           74     "clock1.png",
           75     "clock2.png",
           76     "clock3.png",
           77     "clock4.png",
           78     "clock5.png",
           79     "confirmed.png",
           80 ]
           81 
           82 class HistoryColumns(IntEnum):
           83     STATUS = 0
           84     DESCRIPTION = 1
           85     AMOUNT = 2
           86     BALANCE = 3
           87     FIAT_VALUE = 4
           88     FIAT_ACQ_PRICE = 5
           89     FIAT_CAP_GAINS = 6
           90     TXID = 7
           91 
           92 class HistorySortModel(QSortFilterProxyModel):
           93     def lessThan(self, source_left: QModelIndex, source_right: QModelIndex):
           94         item1 = self.sourceModel().data(source_left, Qt.UserRole)
           95         item2 = self.sourceModel().data(source_right, Qt.UserRole)
           96         if item1 is None or item2 is None:
           97             raise Exception(f'UserRole not set for column {source_left.column()}')
           98         v1 = item1.value()
           99         v2 = item2.value()
          100         if v1 is None or isinstance(v1, Decimal) and v1.is_nan(): v1 = -float("inf")
          101         if v2 is None or isinstance(v2, Decimal) and v2.is_nan(): v2 = -float("inf")
          102         try:
          103             return v1 < v2
          104         except:
          105             return False
          106 
          107 def get_item_key(tx_item):
          108     return tx_item.get('txid') or tx_item['payment_hash']
          109 
          110 
          111 class HistoryNode(CustomNode):
          112 
          113     def get_data_for_role(self, index: QModelIndex, role: Qt.ItemDataRole) -> QVariant:
          114         # note: this method is performance-critical.
          115         # it is called a lot, and so must run extremely fast.
          116         assert index.isValid()
          117         col = index.column()
          118         window = self.model.parent
          119         tx_item = self.get_data()
          120         is_lightning = tx_item.get('lightning', False)
          121         timestamp = tx_item['timestamp']
          122         if is_lightning:
          123             status = 0
          124             if timestamp is None:
          125                 status_str = 'unconfirmed'
          126             else:
          127                 status_str = format_time(int(timestamp))
          128         else:
          129             tx_hash = tx_item['txid']
          130             conf = tx_item['confirmations']
          131             try:
          132                 status, status_str = self.model.tx_status_cache[tx_hash]
          133             except KeyError:
          134                 tx_mined_info = self.model.tx_mined_info_from_tx_item(tx_item)
          135                 status, status_str = window.wallet.get_tx_status(tx_hash, tx_mined_info)
          136 
          137         if role == Qt.UserRole:
          138             # for sorting
          139             d = {
          140                 HistoryColumns.STATUS:
          141                     # respect sort order of self.transactions (wallet.get_full_history)
          142                     -index.row(),
          143                 HistoryColumns.DESCRIPTION:
          144                     tx_item['label'] if 'label' in tx_item else None,
          145                 HistoryColumns.AMOUNT:
          146                     (tx_item['bc_value'].value if 'bc_value' in tx_item else 0)\
          147                     + (tx_item['ln_value'].value if 'ln_value' in tx_item else 0),
          148                 HistoryColumns.BALANCE:
          149                     (tx_item['balance'].value if 'balance' in tx_item else 0),
          150                 HistoryColumns.FIAT_VALUE:
          151                     tx_item['fiat_value'].value if 'fiat_value' in tx_item else None,
          152                 HistoryColumns.FIAT_ACQ_PRICE:
          153                     tx_item['acquisition_price'].value if 'acquisition_price' in tx_item else None,
          154                 HistoryColumns.FIAT_CAP_GAINS:
          155                     tx_item['capital_gain'].value if 'capital_gain' in tx_item else None,
          156                 HistoryColumns.TXID: tx_hash if not is_lightning else None,
          157             }
          158             return QVariant(d[col])
          159         if role not in (Qt.DisplayRole, Qt.EditRole):
          160             if col == HistoryColumns.STATUS and role == Qt.DecorationRole:
          161                 icon = "lightning" if is_lightning else TX_ICONS[status]
          162                 return QVariant(read_QIcon(icon))
          163             elif col == HistoryColumns.STATUS and role == Qt.ToolTipRole:
          164                 if is_lightning:
          165                     msg = 'lightning transaction'
          166                 else:  # on-chain
          167                     if tx_item['height'] == TX_HEIGHT_LOCAL:
          168                         # note: should we also explain double-spends?
          169                         msg = _("This transaction is only available on your local machine.\n"
          170                                 "The currently connected server does not know about it.\n"
          171                                 "You can either broadcast it now, or simply remove it.")
          172                     else:
          173                         msg = str(conf) + _(" confirmation" + ("s" if conf != 1 else ""))
          174                 return QVariant(msg)
          175             elif col > HistoryColumns.DESCRIPTION and role == Qt.TextAlignmentRole:
          176                 return QVariant(int(Qt.AlignRight | Qt.AlignVCenter))
          177             elif col > HistoryColumns.DESCRIPTION and role == Qt.FontRole:
          178                 monospace_font = QFont(MONOSPACE_FONT)
          179                 return QVariant(monospace_font)
          180             #elif col == HistoryColumns.DESCRIPTION and role == Qt.DecorationRole and not is_lightning\
          181             #        and self.parent.wallet.invoices.paid.get(tx_hash):
          182             #    return QVariant(read_QIcon("seal"))
          183             elif col in (HistoryColumns.DESCRIPTION, HistoryColumns.AMOUNT) \
          184                     and role == Qt.ForegroundRole and tx_item['value'].value < 0:
          185                 red_brush = QBrush(QColor("#BC1E1E"))
          186                 return QVariant(red_brush)
          187             elif col == HistoryColumns.FIAT_VALUE and role == Qt.ForegroundRole \
          188                     and not tx_item.get('fiat_default') and tx_item.get('fiat_value') is not None:
          189                 blue_brush = QBrush(QColor("#1E1EFF"))
          190                 return QVariant(blue_brush)
          191             return QVariant()
          192         if col == HistoryColumns.STATUS:
          193             return QVariant(status_str)
          194         elif col == HistoryColumns.DESCRIPTION and 'label' in tx_item:
          195             return QVariant(tx_item['label'])
          196         elif col == HistoryColumns.AMOUNT:
          197             bc_value = tx_item['bc_value'].value if 'bc_value' in tx_item else 0
          198             ln_value = tx_item['ln_value'].value if 'ln_value' in tx_item else 0
          199             value = bc_value + ln_value
          200             v_str = window.format_amount(value, is_diff=True, whitespaces=True)
          201             return QVariant(v_str)
          202         elif col == HistoryColumns.BALANCE:
          203             balance = tx_item['balance'].value
          204             balance_str = window.format_amount(balance, whitespaces=True)
          205             return QVariant(balance_str)
          206         elif col == HistoryColumns.FIAT_VALUE and 'fiat_value' in tx_item:
          207             value_str = window.fx.format_fiat(tx_item['fiat_value'].value)
          208             return QVariant(value_str)
          209         elif col == HistoryColumns.FIAT_ACQ_PRICE and \
          210                 tx_item['value'].value < 0 and 'acquisition_price' in tx_item:
          211             # fixme: should use is_mine
          212             acq = tx_item['acquisition_price'].value
          213             return QVariant(window.fx.format_fiat(acq))
          214         elif col == HistoryColumns.FIAT_CAP_GAINS and 'capital_gain' in tx_item:
          215             cg = tx_item['capital_gain'].value
          216             return QVariant(window.fx.format_fiat(cg))
          217         elif col == HistoryColumns.TXID:
          218             return QVariant(tx_hash) if not is_lightning else QVariant('')
          219         return QVariant()
          220 
          221 
          222 class HistoryModel(CustomModel, Logger):
          223 
          224     def __init__(self, parent: 'ElectrumWindow'):
          225         CustomModel.__init__(self, parent, len(HistoryColumns))
          226         Logger.__init__(self)
          227         self.parent = parent
          228         self.view = None  # type: HistoryList
          229         self.transactions = OrderedDictWithIndex()
          230         self.tx_status_cache = {}  # type: Dict[str, Tuple[int, str]]
          231 
          232     def set_view(self, history_list: 'HistoryList'):
          233         # FIXME HistoryModel and HistoryList mutually depend on each other.
          234         # After constructing both, this method needs to be called.
          235         self.view = history_list  # type: HistoryList
          236         self.set_visibility_of_columns()
          237 
          238     def update_label(self, index):
          239         tx_item = index.internalPointer().get_data()
          240         tx_item['label'] = self.parent.wallet.get_label_for_txid(get_item_key(tx_item))
          241         topLeft = bottomRight = self.createIndex(index.row(), HistoryColumns.DESCRIPTION)
          242         self.dataChanged.emit(topLeft, bottomRight, [Qt.DisplayRole])
          243         self.parent.utxo_list.update()
          244 
          245     def get_domain(self):
          246         """Overridden in address_dialog.py"""
          247         return self.parent.wallet.get_addresses()
          248 
          249     def should_include_lightning_payments(self) -> bool:
          250         """Overridden in address_dialog.py"""
          251         return True
          252 
          253     @profiler
          254     def refresh(self, reason: str):
          255         self.logger.info(f"refreshing... reason: {reason}")
          256         assert self.parent.gui_thread == threading.current_thread(), 'must be called from GUI thread'
          257         assert self.view, 'view not set'
          258         if self.view.maybe_defer_update():
          259             return
          260         selected = self.view.selectionModel().currentIndex()
          261         selected_row = None
          262         if selected:
          263             selected_row = selected.row()
          264         fx = self.parent.fx
          265         if fx: fx.history_used_spot = False
          266         wallet = self.parent.wallet
          267         self.set_visibility_of_columns()
          268         transactions = wallet.get_full_history(
          269             self.parent.fx,
          270             onchain_domain=self.get_domain(),
          271             include_lightning=self.should_include_lightning_payments())
          272         if transactions == self.transactions:
          273             return
          274         old_length = self._root.childCount()
          275         if old_length != 0:
          276             self.beginRemoveRows(QModelIndex(), 0, old_length)
          277             self.transactions.clear()
          278             self._root = HistoryNode(self, None)
          279             self.endRemoveRows()
          280         parents = {}
          281         for tx_item in transactions.values():
          282             node = HistoryNode(self, tx_item)
          283             group_id = tx_item.get('group_id')
          284             if group_id is None:
          285                 self._root.addChild(node)
          286             else:
          287                 parent = parents.get(group_id)
          288                 if parent is None:
          289                     # create parent if it does not exist
          290                     self._root.addChild(node)
          291                     parents[group_id] = node
          292                 else:
          293                     # if parent has no children, create two children
          294                     if parent.childCount() == 0:
          295                         child_data = dict(parent.get_data())
          296                         node1 = HistoryNode(self, child_data)
          297                         parent.addChild(node1)
          298                         parent._data['label'] = child_data.get('group_label')
          299                         parent._data['bc_value'] = child_data.get('bc_value', Satoshis(0))
          300                         parent._data['ln_value'] = child_data.get('ln_value', Satoshis(0))
          301                     # add child to parent
          302                     parent.addChild(node)
          303                     # update parent data
          304                     parent._data['balance'] = tx_item['balance']
          305                     parent._data['value'] += tx_item['value']
          306                     if 'group_label' in tx_item:
          307                         parent._data['label'] = tx_item['group_label']
          308                     if 'bc_value' in tx_item:
          309                         parent._data['bc_value'] += tx_item['bc_value']
          310                     if 'ln_value' in tx_item:
          311                         parent._data['ln_value'] += tx_item['ln_value']
          312                     if 'fiat_value' in tx_item:
          313                         parent._data['fiat_value'] += tx_item['fiat_value']
          314                     if tx_item.get('txid') == group_id:
          315                         parent._data['lightning'] = False
          316                         parent._data['txid'] = tx_item['txid']
          317                         parent._data['timestamp'] = tx_item['timestamp']
          318                         parent._data['height'] = tx_item['height']
          319                         parent._data['confirmations'] = tx_item['confirmations']
          320 
          321         new_length = self._root.childCount()
          322         self.beginInsertRows(QModelIndex(), 0, new_length-1)
          323         self.transactions = transactions
          324         self.endInsertRows()
          325 
          326         if selected_row:
          327             self.view.selectionModel().select(self.createIndex(selected_row, 0), QItemSelectionModel.Rows | QItemSelectionModel.SelectCurrent)
          328         self.view.filter()
          329         # update time filter
          330         if not self.view.years and self.transactions:
          331             start_date = date.today()
          332             end_date = date.today()
          333             if len(self.transactions) > 0:
          334                 start_date = self.transactions.value_from_pos(0).get('date') or start_date
          335                 end_date = self.transactions.value_from_pos(len(self.transactions) - 1).get('date') or end_date
          336             self.view.years = [str(i) for i in range(start_date.year, end_date.year + 1)]
          337             self.view.period_combo.insertItems(1, self.view.years)
          338         # update tx_status_cache
          339         self.tx_status_cache.clear()
          340         for txid, tx_item in self.transactions.items():
          341             if not tx_item.get('lightning', False):
          342                 tx_mined_info = self.tx_mined_info_from_tx_item(tx_item)
          343                 self.tx_status_cache[txid] = self.parent.wallet.get_tx_status(txid, tx_mined_info)
          344 
          345     def set_visibility_of_columns(self):
          346         def set_visible(col: int, b: bool):
          347             self.view.showColumn(col) if b else self.view.hideColumn(col)
          348         # txid
          349         set_visible(HistoryColumns.TXID, False)
          350         # fiat
          351         history = self.parent.fx.show_history()
          352         cap_gains = self.parent.fx.get_history_capital_gains_config()
          353         set_visible(HistoryColumns.FIAT_VALUE, history)
          354         set_visible(HistoryColumns.FIAT_ACQ_PRICE, history and cap_gains)
          355         set_visible(HistoryColumns.FIAT_CAP_GAINS, history and cap_gains)
          356 
          357     def update_fiat(self, idx):
          358         tx_item = idx.internalPointer().get_data()
          359         txid = tx_item['txid']
          360         fee = tx_item.get('fee')
          361         value = tx_item['value'].value
          362         fiat_fields = self.parent.wallet.get_tx_item_fiat(
          363             tx_hash=txid, amount_sat=value, fx=self.parent.fx, tx_fee=fee.value if fee else None)
          364         tx_item.update(fiat_fields)
          365         self.dataChanged.emit(idx, idx, [Qt.DisplayRole, Qt.ForegroundRole])
          366 
          367     def update_tx_mined_status(self, tx_hash: str, tx_mined_info: TxMinedInfo):
          368         try:
          369             row = self.transactions.pos_from_key(tx_hash)
          370             tx_item = self.transactions[tx_hash]
          371         except KeyError:
          372             return
          373         self.tx_status_cache[tx_hash] = self.parent.wallet.get_tx_status(tx_hash, tx_mined_info)
          374         tx_item.update({
          375             'confirmations':  tx_mined_info.conf,
          376             'timestamp':      tx_mined_info.timestamp,
          377             'txpos_in_block': tx_mined_info.txpos,
          378             'date':           timestamp_to_datetime(tx_mined_info.timestamp),
          379         })
          380         topLeft = self.createIndex(row, 0)
          381         bottomRight = self.createIndex(row, len(HistoryColumns) - 1)
          382         self.dataChanged.emit(topLeft, bottomRight)
          383 
          384     def on_fee_histogram(self):
          385         for tx_hash, tx_item in list(self.transactions.items()):
          386             if tx_item.get('lightning'):
          387                 continue
          388             tx_mined_info = self.tx_mined_info_from_tx_item(tx_item)
          389             if tx_mined_info.conf > 0:
          390                 # note: we could actually break here if we wanted to rely on the order of txns in self.transactions
          391                 continue
          392             self.update_tx_mined_status(tx_hash, tx_mined_info)
          393 
          394     def headerData(self, section: int, orientation: Qt.Orientation, role: Qt.ItemDataRole):
          395         assert orientation == Qt.Horizontal
          396         if role != Qt.DisplayRole:
          397             return None
          398         fx = self.parent.fx
          399         fiat_title = 'n/a fiat value'
          400         fiat_acq_title = 'n/a fiat acquisition price'
          401         fiat_cg_title = 'n/a fiat capital gains'
          402         if fx and fx.show_history():
          403             fiat_title = '%s '%fx.ccy + _('Value')
          404             fiat_acq_title = '%s '%fx.ccy + _('Acquisition price')
          405             fiat_cg_title =  '%s '%fx.ccy + _('Capital Gains')
          406         return {
          407             HistoryColumns.STATUS: _('Date'),
          408             HistoryColumns.DESCRIPTION: _('Description'),
          409             HistoryColumns.AMOUNT: _('Amount'),
          410             HistoryColumns.BALANCE: _('Balance'),
          411             HistoryColumns.FIAT_VALUE: fiat_title,
          412             HistoryColumns.FIAT_ACQ_PRICE: fiat_acq_title,
          413             HistoryColumns.FIAT_CAP_GAINS: fiat_cg_title,
          414             HistoryColumns.TXID: 'TXID',
          415         }[section]
          416 
          417     def flags(self, idx):
          418         extra_flags = Qt.NoItemFlags # type: Qt.ItemFlag
          419         if idx.column() in self.view.editable_columns:
          420             extra_flags |= Qt.ItemIsEditable
          421         return super().flags(idx) | int(extra_flags)
          422 
          423     @staticmethod
          424     def tx_mined_info_from_tx_item(tx_item):
          425         tx_mined_info = TxMinedInfo(height=tx_item['height'],
          426                                     conf=tx_item['confirmations'],
          427                                     timestamp=tx_item['timestamp'])
          428         return tx_mined_info
          429 
          430 class HistoryList(MyTreeView, AcceptFileDragDrop):
          431     filter_columns = [HistoryColumns.STATUS,
          432                       HistoryColumns.DESCRIPTION,
          433                       HistoryColumns.AMOUNT,
          434                       HistoryColumns.TXID]
          435 
          436     def tx_item_from_proxy_row(self, proxy_row):
          437         hm_idx = self.model().mapToSource(self.model().index(proxy_row, 0))
          438         return hm_idx.internalPointer().get_data()
          439 
          440     def should_hide(self, proxy_row):
          441         if self.start_timestamp and self.end_timestamp:
          442             tx_item = self.tx_item_from_proxy_row(proxy_row)
          443             date = tx_item['date']
          444             if date:
          445                 in_interval = self.start_timestamp <= date <= self.end_timestamp
          446                 if not in_interval:
          447                     return True
          448             return False
          449 
          450     def __init__(self, parent, model: HistoryModel):
          451         super().__init__(parent, self.create_menu, stretch_column=HistoryColumns.DESCRIPTION)
          452         self.config = parent.config
          453         self.hm = model
          454         self.proxy = HistorySortModel(self)
          455         self.proxy.setSourceModel(model)
          456         self.setModel(self.proxy)
          457         AcceptFileDragDrop.__init__(self, ".txn")
          458         self.setSortingEnabled(True)
          459         self.start_timestamp = None
          460         self.end_timestamp = None
          461         self.years = []
          462         self.create_toolbar_buttons()
          463         self.wallet = self.parent.wallet  # type: Abstract_Wallet
          464         self.sortByColumn(HistoryColumns.STATUS, Qt.AscendingOrder)
          465         self.editable_columns |= {HistoryColumns.FIAT_VALUE}
          466         self.setRootIsDecorated(True)
          467         self.header().setStretchLastSection(False)
          468         for col in HistoryColumns:
          469             sm = QHeaderView.Stretch if col == self.stretch_column else QHeaderView.ResizeToContents
          470             self.header().setSectionResizeMode(col, sm)
          471 
          472     def update(self):
          473         self.hm.refresh('HistoryList.update()')
          474 
          475     def format_date(self, d):
          476         return str(datetime.date(d.year, d.month, d.day)) if d else _('None')
          477 
          478     def on_combo(self, x):
          479         s = self.period_combo.itemText(x)
          480         x = s == _('Custom')
          481         self.start_button.setEnabled(x)
          482         self.end_button.setEnabled(x)
          483         if s == _('All'):
          484             self.start_timestamp = None
          485             self.end_timestamp = None
          486             self.start_button.setText("-")
          487             self.end_button.setText("-")
          488         else:
          489             try:
          490                 year = int(s)
          491             except:
          492                 return
          493             self.start_timestamp = start_date = datetime.datetime(year, 1, 1)
          494             self.end_timestamp = end_date = datetime.datetime(year+1, 1, 1)
          495             self.start_button.setText(_('From') + ' ' + self.format_date(start_date))
          496             self.end_button.setText(_('To') + ' ' + self.format_date(end_date))
          497         self.hide_rows()
          498 
          499     def create_toolbar_buttons(self):
          500         self.period_combo = QComboBox()
          501         self.start_button = QPushButton('-')
          502         self.start_button.pressed.connect(self.select_start_date)
          503         self.start_button.setEnabled(False)
          504         self.end_button = QPushButton('-')
          505         self.end_button.pressed.connect(self.select_end_date)
          506         self.end_button.setEnabled(False)
          507         self.period_combo.addItems([_('All'), _('Custom')])
          508         self.period_combo.activated.connect(self.on_combo)
          509 
          510     def get_toolbar_buttons(self):
          511         return self.period_combo, self.start_button, self.end_button
          512 
          513     def on_hide_toolbar(self):
          514         self.start_timestamp = None
          515         self.end_timestamp = None
          516         self.hide_rows()
          517 
          518     def save_toolbar_state(self, state, config):
          519         config.set_key('show_toolbar_history', state)
          520 
          521     def select_start_date(self):
          522         self.start_timestamp = self.select_date(self.start_button)
          523         self.hide_rows()
          524 
          525     def select_end_date(self):
          526         self.end_timestamp = self.select_date(self.end_button)
          527         self.hide_rows()
          528 
          529     def select_date(self, button):
          530         d = WindowModalDialog(self, _("Select date"))
          531         d.setMinimumSize(600, 150)
          532         d.date = None
          533         vbox = QVBoxLayout()
          534         def on_date(date):
          535             d.date = date
          536         cal = QCalendarWidget()
          537         cal.setGridVisible(True)
          538         cal.clicked[QDate].connect(on_date)
          539         vbox.addWidget(cal)
          540         vbox.addLayout(Buttons(OkButton(d), CancelButton(d)))
          541         d.setLayout(vbox)
          542         if d.exec_():
          543             if d.date is None:
          544                 return None
          545             date = d.date.toPyDate()
          546             button.setText(self.format_date(date))
          547             return datetime.datetime(date.year, date.month, date.day)
          548 
          549     def show_summary(self):
          550         h = self.parent.wallet.get_detailed_history()['summary']
          551         if not h:
          552             self.parent.show_message(_("Nothing to summarize."))
          553             return
          554         start_date = h.get('start_date')
          555         end_date = h.get('end_date')
          556         format_amount = lambda x: self.parent.format_amount(x.value) + ' ' + self.parent.base_unit()
          557         d = WindowModalDialog(self, _("Summary"))
          558         d.setMinimumSize(600, 150)
          559         vbox = QVBoxLayout()
          560         grid = QGridLayout()
          561         grid.addWidget(QLabel(_("Start")), 0, 0)
          562         grid.addWidget(QLabel(self.format_date(start_date)), 0, 1)
          563         grid.addWidget(QLabel(str(h.get('fiat_start_value')) + '/BTC'), 0, 2)
          564         grid.addWidget(QLabel(_("Initial balance")), 1, 0)
          565         grid.addWidget(QLabel(format_amount(h['start_balance'])), 1, 1)
          566         grid.addWidget(QLabel(str(h.get('fiat_start_balance'))), 1, 2)
          567         grid.addWidget(QLabel(_("End")), 2, 0)
          568         grid.addWidget(QLabel(self.format_date(end_date)), 2, 1)
          569         grid.addWidget(QLabel(str(h.get('fiat_end_value')) + '/BTC'), 2, 2)
          570         grid.addWidget(QLabel(_("Final balance")), 4, 0)
          571         grid.addWidget(QLabel(format_amount(h['end_balance'])), 4, 1)
          572         grid.addWidget(QLabel(str(h.get('fiat_end_balance'))), 4, 2)
          573         grid.addWidget(QLabel(_("Income")), 5, 0)
          574         grid.addWidget(QLabel(format_amount(h.get('incoming'))), 5, 1)
          575         grid.addWidget(QLabel(str(h.get('fiat_incoming'))), 5, 2)
          576         grid.addWidget(QLabel(_("Expenditures")), 6, 0)
          577         grid.addWidget(QLabel(format_amount(h.get('outgoing'))), 6, 1)
          578         grid.addWidget(QLabel(str(h.get('fiat_outgoing'))), 6, 2)
          579         grid.addWidget(QLabel(_("Capital gains")), 7, 0)
          580         grid.addWidget(QLabel(str(h.get('fiat_capital_gains'))), 7, 2)
          581         grid.addWidget(QLabel(_("Unrealized gains")), 8, 0)
          582         grid.addWidget(QLabel(str(h.get('fiat_unrealized_gains', ''))), 8, 2)
          583         vbox.addLayout(grid)
          584         vbox.addLayout(Buttons(CloseButton(d)))
          585         d.setLayout(vbox)
          586         d.exec_()
          587 
          588     def plot_history_dialog(self):
          589         if plot_history is None:
          590             self.parent.show_message(
          591                 _("Can't plot history.") + '\n' +
          592                 _("Perhaps some dependencies are missing...") + " (matplotlib?)")
          593             return
          594         try:
          595             plt = plot_history(list(self.hm.transactions.values()))
          596             plt.show()
          597         except NothingToPlotException as e:
          598             self.parent.show_message(str(e))
          599 
          600     def on_edited(self, index, user_role, text):
          601         index = self.model().mapToSource(index)
          602         tx_item = index.internalPointer().get_data()
          603         column = index.column()
          604         key = get_item_key(tx_item)
          605         if column == HistoryColumns.DESCRIPTION:
          606             if self.wallet.set_label(key, text): #changed
          607                 self.hm.update_label(index)
          608                 self.parent.update_completions()
          609         elif column == HistoryColumns.FIAT_VALUE:
          610             self.wallet.set_fiat_value(key, self.parent.fx.ccy, text, self.parent.fx, tx_item['value'].value)
          611             value = tx_item['value'].value
          612             if value is not None:
          613                 self.hm.update_fiat(index)
          614         else:
          615             assert False
          616 
          617     def mouseDoubleClickEvent(self, event: QMouseEvent):
          618         idx = self.indexAt(event.pos())
          619         if not idx.isValid():
          620             return
          621         tx_item = self.tx_item_from_proxy_row(idx.row())
          622         if self.hm.flags(self.model().mapToSource(idx)) & Qt.ItemIsEditable:
          623             super().mouseDoubleClickEvent(event)
          624         else:
          625             if tx_item.get('lightning'):
          626                 if tx_item['type'] == 'payment':
          627                     self.parent.show_lightning_transaction(tx_item)
          628                 return
          629             tx_hash = tx_item['txid']
          630             tx = self.wallet.db.get_transaction(tx_hash)
          631             if not tx:
          632                 return
          633             self.show_transaction(tx_item, tx)
          634 
          635     def show_transaction(self, tx_item, tx):
          636         tx_hash = tx_item['txid']
          637         label = self.wallet.get_label_for_txid(tx_hash) or None # prefer 'None' if not defined (force tx dialog to hide Description field if missing)
          638         self.parent.show_transaction(tx, tx_desc=label)
          639 
          640     def add_copy_menu(self, menu, idx):
          641         cc = menu.addMenu(_("Copy"))
          642         for column in HistoryColumns:
          643             if self.isColumnHidden(column):
          644                 continue
          645             column_title = self.hm.headerData(column, Qt.Horizontal, Qt.DisplayRole)
          646             idx2 = idx.sibling(idx.row(), column)
          647             column_data = (self.hm.data(idx2, Qt.DisplayRole).value() or '').strip()
          648             cc.addAction(
          649                 column_title,
          650                 lambda text=column_data, title=column_title:
          651                 self.place_text_on_clipboard(text, title=title))
          652         return cc
          653 
          654     def create_menu(self, position: QPoint):
          655         org_idx: QModelIndex = self.indexAt(position)
          656         idx = self.proxy.mapToSource(org_idx)
          657         if not idx.isValid():
          658             # can happen e.g. before list is populated for the first time
          659             return
          660         tx_item = idx.internalPointer().get_data()
          661         if tx_item.get('lightning') and tx_item['type'] == 'payment':
          662             menu = QMenu()
          663             menu.addAction(_("View Payment"), lambda: self.parent.show_lightning_transaction(tx_item))
          664             cc = self.add_copy_menu(menu, idx)
          665             cc.addAction(_("Payment Hash"), lambda: self.place_text_on_clipboard(tx_item['payment_hash'], title="Payment Hash"))
          666             cc.addAction(_("Preimage"), lambda: self.place_text_on_clipboard(tx_item['preimage'], title="Preimage"))
          667             key = tx_item['payment_hash']
          668             log = self.wallet.lnworker.logs.get(key)
          669             if log:
          670                 menu.addAction(_("View log"), lambda: self.parent.invoice_list.show_log(key, log))
          671             menu.exec_(self.viewport().mapToGlobal(position))
          672             return
          673         tx_hash = tx_item['txid']
          674         if tx_item.get('lightning'):
          675             tx = self.wallet.lnworker.lnwatcher.db.get_transaction(tx_hash)
          676         else:
          677             tx = self.wallet.db.get_transaction(tx_hash)
          678         if not tx:
          679             return
          680         tx_URL = block_explorer_URL(self.config, 'tx', tx_hash)
          681         tx_details = self.wallet.get_tx_info(tx)
          682         is_unconfirmed = tx_details.tx_mined_status.height <= 0
          683         menu = QMenu()
          684         if tx_details.can_remove:
          685             menu.addAction(_("Remove"), lambda: self.remove_local_tx(tx_hash))
          686         cc = self.add_copy_menu(menu, idx)
          687         cc.addAction(_("Transaction ID"), lambda: self.place_text_on_clipboard(tx_hash, title="TXID"))
          688         for c in self.editable_columns:
          689             if self.isColumnHidden(c): continue
          690             label = self.hm.headerData(c, Qt.Horizontal, Qt.DisplayRole)
          691             # TODO use siblingAtColumn when min Qt version is >=5.11
          692             persistent = QPersistentModelIndex(org_idx.sibling(org_idx.row(), c))
          693             menu.addAction(_("Edit {}").format(label), lambda p=persistent: self.edit(QModelIndex(p)))
          694         menu.addAction(_("View Transaction"), lambda: self.show_transaction(tx_item, tx))
          695         channel_id = tx_item.get('channel_id')
          696         if channel_id:
          697             menu.addAction(_("View Channel"), lambda: self.parent.show_channel(bytes.fromhex(channel_id)))
          698         if is_unconfirmed and tx:
          699             if tx_details.can_bump:
          700                 menu.addAction(_("Increase fee"), lambda: self.parent.bump_fee_dialog(tx))
          701             else:
          702                 if tx_details.can_cpfp:
          703                     menu.addAction(_("Child pays for parent"), lambda: self.parent.cpfp_dialog(tx))
          704             if tx_details.can_dscancel:
          705                 menu.addAction(_("Cancel (double-spend)"), lambda: self.parent.dscancel_dialog(tx))
          706         invoices = self.wallet.get_relevant_invoices_for_tx(tx)
          707         if len(invoices) == 1:
          708             menu.addAction(_("View invoice"), lambda inv=invoices[0]: self.parent.show_onchain_invoice(inv))
          709         elif len(invoices) > 1:
          710             menu_invs = menu.addMenu(_("Related invoices"))
          711             for inv in invoices:
          712                 menu_invs.addAction(_("View invoice"), lambda inv=inv: self.parent.show_onchain_invoice(inv))
          713         if tx_URL:
          714             menu.addAction(_("View on block explorer"), lambda: webopen(tx_URL))
          715         menu.exec_(self.viewport().mapToGlobal(position))
          716 
          717     def remove_local_tx(self, tx_hash: str):
          718         num_child_txs = len(self.wallet.get_depending_transactions(tx_hash))
          719         question = _("Are you sure you want to remove this transaction?")
          720         if num_child_txs > 0:
          721             question = (_("Are you sure you want to remove this transaction and {} child transactions?")
          722                         .format(num_child_txs))
          723         if not self.parent.question(msg=question,
          724                                     title=_("Please confirm")):
          725             return
          726         self.wallet.remove_transaction(tx_hash)
          727         self.wallet.save_db()
          728         # need to update at least: history_list, utxo_list, address_list
          729         self.parent.need_update.set()
          730 
          731     def onFileAdded(self, fn):
          732         try:
          733             with open(fn) as f:
          734                 tx = self.parent.tx_from_text(f.read())
          735         except IOError as e:
          736             self.parent.show_error(e)
          737             return
          738         if not tx:
          739             return
          740         self.parent.save_transaction_into_wallet(tx)
          741 
          742     def export_history_dialog(self):
          743         d = WindowModalDialog(self, _('Export History'))
          744         d.setMinimumSize(400, 200)
          745         vbox = QVBoxLayout(d)
          746         defaultname = os.path.expanduser('~/electrum-history.csv')
          747         select_msg = _('Select file to export your wallet transactions to')
          748         hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
          749         vbox.addLayout(hbox)
          750         vbox.addStretch(1)
          751         hbox = Buttons(CancelButton(d), OkButton(d, _('Export')))
          752         vbox.addLayout(hbox)
          753         #run_hook('export_history_dialog', self, hbox)
          754         self.update()
          755         if not d.exec_():
          756             return
          757         filename = filename_e.text()
          758         if not filename:
          759             return
          760         try:
          761             self.do_export_history(filename, csv_button.isChecked())
          762         except (IOError, os.error) as reason:
          763             export_error_label = _("Electrum was unable to produce a transaction export.")
          764             self.parent.show_critical(export_error_label + "\n" + str(reason), title=_("Unable to export history"))
          765             return
          766         self.parent.show_message(_("Your wallet history has been successfully exported."))
          767 
          768     def do_export_history(self, file_name, is_csv):
          769         hist = self.wallet.get_detailed_history(fx=self.parent.fx)
          770         txns = hist['transactions']
          771         lines = []
          772         if is_csv:
          773             for item in txns:
          774                 lines.append([item['txid'],
          775                               item.get('label', ''),
          776                               item['confirmations'],
          777                               item['bc_value'],
          778                               item.get('fiat_value', ''),
          779                               item.get('fee', ''),
          780                               item.get('fiat_fee', ''),
          781                               item['date']])
          782         with open(file_name, "w+", encoding='utf-8') as f:
          783             if is_csv:
          784                 import csv
          785                 transaction = csv.writer(f, lineterminator='\n')
          786                 transaction.writerow(["transaction_hash",
          787                                       "label",
          788                                       "confirmations",
          789                                       "value",
          790                                       "fiat_value",
          791                                       "fee",
          792                                       "fiat_fee",
          793                                       "timestamp"])
          794                 for line in lines:
          795                     transaction.writerow(line)
          796             else:
          797                 from electrum.util import json_encode
          798                 f.write(json_encode(txns))
          799 
          800     def get_text_and_userrole_from_coordinate(self, row, col):
          801         idx = self.model().mapToSource(self.model().index(row, col))
          802         tx_item = idx.internalPointer().get_data()
          803         return self.hm.data(idx, Qt.DisplayRole).value(), get_item_key(tx_item)