From ea864cd5c974c46e2fd34dd1b4c9b9863cc5e50e Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 23 Apr 2023 16:00:42 +0000 Subject: [PATCH] qml: TxListModel: don't rely on wallet.db.get_transaction() finding tx tx might get removed from wallet after wallet.get_full_history() but before the model is populated closes https://github.com/spesmilo/electrum/issues/8339 --- electrum/gui/qml/qetransactionlistmodel.py | 33 ++++++++++++++++------ electrum/gui/qt/history_list.py | 12 ++++---- electrum/util.py | 9 ++++++ 3 files changed, 39 insertions(+), 15 deletions(-) diff --git a/electrum/gui/qml/qetransactionlistmodel.py b/electrum/gui/qml/qetransactionlistmodel.py index ea9ae213f..24af49ede 100644 --- a/electrum/gui/qml/qetransactionlistmodel.py +++ b/electrum/gui/qml/qetransactionlistmodel.py @@ -1,5 +1,5 @@ from datetime import datetime, timedelta -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Dict, Any from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex @@ -97,9 +97,9 @@ class QETransactionListModel(QAbstractListModel, QtEventListener): self.tx_history = [] self.endResetModel() - def tx_to_model(self, tx): - #self._logger.debug(str(tx)) - item = tx + def tx_to_model(self, tx_item): + #self._logger.debug(str(tx_item)) + item = tx_item item['key'] = item['txid'] if 'txid' in item else item['payment_hash'] @@ -118,15 +118,19 @@ class QETransactionListModel(QAbstractListModel, QtEventListener): if 'txid' in item: tx = self.wallet.db.get_transaction(item['txid']) - assert tx is not None - item['complete'] = tx.is_complete() + if tx: + item['complete'] = tx.is_complete() + else: # due to races, tx might have already been removed from history + item['complete'] = False # newly arriving txs, or (partially/fully signed) local txs have no (block) timestamp # FIXME just use wallet.get_tx_status, and change that as needed if not item['timestamp']: # onchain: local or mempool or unverified txs - txinfo = self.wallet.get_tx_info(tx) - item['section'] = 'mempool' if item['complete'] and not txinfo.can_broadcast else 'local' - status, status_str = self.wallet.get_tx_status(item['txid'], txinfo.tx_mined_status) + txid = item['txid'] + assert txid + tx_mined_info = self._tx_mined_info_from_tx_item(tx_item) + item['section'] = 'local' if tx_mined_info.is_local_like() else 'mempool' + status, status_str = self.wallet.get_tx_status(txid, tx_mined_info=tx_mined_info) item['date'] = status_str else: # lightning or already mined (and SPV-ed) onchain txs item['section'] = self.get_section_by_timestamp(item['timestamp']) @@ -162,6 +166,17 @@ class QETransactionListModel(QAbstractListModel, QtEventListener): section = 'older' return date.strftime(dfmt[section]) + @staticmethod + def _tx_mined_info_from_tx_item(tx_item: Dict[str, Any]) -> TxMinedInfo: + # FIXME a bit hackish to have to reconstruct the TxMinedInfo... same thing in qt-gui + tx_mined_info = TxMinedInfo( + height=tx_item['height'], + conf=tx_item['confirmations'], + timestamp=tx_item['timestamp'], + wanted_height=tx_item.get('wanted_height', None), + ) + return tx_mined_info + # initial model data @pyqtSlot() @pyqtSlot(bool) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 2770091c8..927659aa5 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -28,7 +28,7 @@ import sys import time import datetime from datetime import date -from typing import TYPE_CHECKING, Tuple, Dict +from typing import TYPE_CHECKING, Tuple, Dict, Any import threading import enum from decimal import Decimal @@ -128,7 +128,7 @@ class HistoryNode(CustomNode): try: status, status_str = self.model.tx_status_cache[tx_hash] except KeyError: - tx_mined_info = self.model.tx_mined_info_from_tx_item(tx_item) + tx_mined_info = self.model._tx_mined_info_from_tx_item(tx_item) status, status_str = window.wallet.get_tx_status(tx_hash, tx_mined_info) if role == ROLE_SORT_ORDER: @@ -353,7 +353,7 @@ class HistoryModel(CustomModel, Logger): self.tx_status_cache.clear() for txid, tx_item in self.transactions.items(): if not tx_item.get('lightning', False): - tx_mined_info = self.tx_mined_info_from_tx_item(tx_item) + tx_mined_info = self._tx_mined_info_from_tx_item(tx_item) self.tx_status_cache[txid] = self.window.wallet.get_tx_status(txid, tx_mined_info) # update counter num_tx = len(self.transactions) @@ -404,7 +404,7 @@ class HistoryModel(CustomModel, Logger): for tx_hash, tx_item in list(self.transactions.items()): if tx_item.get('lightning'): continue - tx_mined_info = self.tx_mined_info_from_tx_item(tx_item) + tx_mined_info = self._tx_mined_info_from_tx_item(tx_item) if tx_mined_info.conf > 0: # note: we could actually break here if we wanted to rely on the order of txns in self.transactions continue @@ -441,8 +441,8 @@ class HistoryModel(CustomModel, Logger): return super().flags(idx) | int(extra_flags) @staticmethod - def tx_mined_info_from_tx_item(tx_item): - # FIXME a bit hackish to have to reconstruct the TxMinedInfo... + def _tx_mined_info_from_tx_item(tx_item: Dict[str, Any]) -> TxMinedInfo: + # FIXME a bit hackish to have to reconstruct the TxMinedInfo... same thing in qml-gui tx_mined_info = TxMinedInfo( height=tx_item['height'], conf=tx_item['confirmations'], diff --git a/electrum/util.py b/electrum/util.py index adf502293..a8e8dd648 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -1340,6 +1340,15 @@ class TxMinedInfo(NamedTuple): return f"{self.height}x{self.txpos}" return None + def is_local_like(self) -> bool: + """Returns whether the tx is local-like (LOCAL/FUTURE).""" + from .address_synchronizer import TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT + if self.height > 0: + return False + if self.height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT): + return False + return True + class ShortID(bytes):