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
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
from datetime import datetime, timedelta
|
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 pyqtProperty, pyqtSignal, pyqtSlot, QObject
|
||||||
from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex
|
from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex
|
||||||
@@ -97,9 +97,9 @@ class QETransactionListModel(QAbstractListModel, QtEventListener):
|
|||||||
self.tx_history = []
|
self.tx_history = []
|
||||||
self.endResetModel()
|
self.endResetModel()
|
||||||
|
|
||||||
def tx_to_model(self, tx):
|
def tx_to_model(self, tx_item):
|
||||||
#self._logger.debug(str(tx))
|
#self._logger.debug(str(tx_item))
|
||||||
item = tx
|
item = tx_item
|
||||||
|
|
||||||
item['key'] = item['txid'] if 'txid' in item else item['payment_hash']
|
item['key'] = item['txid'] if 'txid' in item else item['payment_hash']
|
||||||
|
|
||||||
@@ -118,15 +118,19 @@ class QETransactionListModel(QAbstractListModel, QtEventListener):
|
|||||||
|
|
||||||
if 'txid' in item:
|
if 'txid' in item:
|
||||||
tx = self.wallet.db.get_transaction(item['txid'])
|
tx = self.wallet.db.get_transaction(item['txid'])
|
||||||
assert tx is not None
|
if tx:
|
||||||
item['complete'] = tx.is_complete()
|
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
|
# 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
|
# FIXME just use wallet.get_tx_status, and change that as needed
|
||||||
if not item['timestamp']: # onchain: local or mempool or unverified txs
|
if not item['timestamp']: # onchain: local or mempool or unverified txs
|
||||||
txinfo = self.wallet.get_tx_info(tx)
|
txid = item['txid']
|
||||||
item['section'] = 'mempool' if item['complete'] and not txinfo.can_broadcast else 'local'
|
assert txid
|
||||||
status, status_str = self.wallet.get_tx_status(item['txid'], txinfo.tx_mined_status)
|
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
|
item['date'] = status_str
|
||||||
else: # lightning or already mined (and SPV-ed) onchain txs
|
else: # lightning or already mined (and SPV-ed) onchain txs
|
||||||
item['section'] = self.get_section_by_timestamp(item['timestamp'])
|
item['section'] = self.get_section_by_timestamp(item['timestamp'])
|
||||||
@@ -162,6 +166,17 @@ class QETransactionListModel(QAbstractListModel, QtEventListener):
|
|||||||
section = 'older'
|
section = 'older'
|
||||||
return date.strftime(dfmt[section])
|
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
|
# initial model data
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
@pyqtSlot(bool)
|
@pyqtSlot(bool)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import sys
|
|||||||
import time
|
import time
|
||||||
import datetime
|
import datetime
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from typing import TYPE_CHECKING, Tuple, Dict
|
from typing import TYPE_CHECKING, Tuple, Dict, Any
|
||||||
import threading
|
import threading
|
||||||
import enum
|
import enum
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
@@ -128,7 +128,7 @@ class HistoryNode(CustomNode):
|
|||||||
try:
|
try:
|
||||||
status, status_str = self.model.tx_status_cache[tx_hash]
|
status, status_str = self.model.tx_status_cache[tx_hash]
|
||||||
except KeyError:
|
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)
|
status, status_str = window.wallet.get_tx_status(tx_hash, tx_mined_info)
|
||||||
|
|
||||||
if role == ROLE_SORT_ORDER:
|
if role == ROLE_SORT_ORDER:
|
||||||
@@ -353,7 +353,7 @@ class HistoryModel(CustomModel, Logger):
|
|||||||
self.tx_status_cache.clear()
|
self.tx_status_cache.clear()
|
||||||
for txid, tx_item in self.transactions.items():
|
for txid, tx_item in self.transactions.items():
|
||||||
if not tx_item.get('lightning', False):
|
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)
|
self.tx_status_cache[txid] = self.window.wallet.get_tx_status(txid, tx_mined_info)
|
||||||
# update counter
|
# update counter
|
||||||
num_tx = len(self.transactions)
|
num_tx = len(self.transactions)
|
||||||
@@ -404,7 +404,7 @@ class HistoryModel(CustomModel, Logger):
|
|||||||
for tx_hash, tx_item in list(self.transactions.items()):
|
for tx_hash, tx_item in list(self.transactions.items()):
|
||||||
if tx_item.get('lightning'):
|
if tx_item.get('lightning'):
|
||||||
continue
|
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:
|
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
|
# note: we could actually break here if we wanted to rely on the order of txns in self.transactions
|
||||||
continue
|
continue
|
||||||
@@ -441,8 +441,8 @@ class HistoryModel(CustomModel, Logger):
|
|||||||
return super().flags(idx) | int(extra_flags)
|
return super().flags(idx) | int(extra_flags)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def tx_mined_info_from_tx_item(tx_item):
|
def _tx_mined_info_from_tx_item(tx_item: Dict[str, Any]) -> TxMinedInfo:
|
||||||
# FIXME a bit hackish to have to reconstruct the TxMinedInfo...
|
# FIXME a bit hackish to have to reconstruct the TxMinedInfo... same thing in qml-gui
|
||||||
tx_mined_info = TxMinedInfo(
|
tx_mined_info = TxMinedInfo(
|
||||||
height=tx_item['height'],
|
height=tx_item['height'],
|
||||||
conf=tx_item['confirmations'],
|
conf=tx_item['confirmations'],
|
||||||
|
|||||||
@@ -1340,6 +1340,15 @@ class TxMinedInfo(NamedTuple):
|
|||||||
return f"{self.height}x{self.txpos}"
|
return f"{self.height}x{self.txpos}"
|
||||||
return None
|
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):
|
class ShortID(bytes):
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user