diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index 76a0255e2..a435c127d 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -778,13 +778,13 @@ class AddressSynchronizer(Logger, EventListener): sent = {} for tx_hash, height in h: hh, pos = self.get_txpos(tx_hash) + assert hh == height d = self.db.get_txo_addr(tx_hash, address) for n, (v, is_cb) in d.items(): received[tx_hash + ':%d'%n] = (height, pos, v, is_cb) - for tx_hash, height in h: l = self.db.get_txi_addr(tx_hash, address) for txi, v in l: - sent[txi] = tx_hash, height + sent[txi] = tx_hash, height, pos return received, sent def get_addr_outputs(self, address: str) -> Dict[TxOutpoint, PartialTxInput]: @@ -799,7 +799,7 @@ class AddressSynchronizer(Logger, EventListener): utxo.block_height = tx_height utxo.block_txpos = tx_pos if prevout_str in sent: - txid, height = sent[prevout_str] + txid, height, pos = sent[prevout_str] utxo.spent_txid = txid utxo.spent_height = height else: diff --git a/electrum/gui/qt/utxo_dialog.py b/electrum/gui/qt/utxo_dialog.py index 30dc80b1c..af8a7a618 100644 --- a/electrum/gui/qt/utxo_dialog.py +++ b/electrum/gui/qt/utxo_dialog.py @@ -28,13 +28,14 @@ import copy from PyQt5.QtCore import Qt, QUrl from PyQt5.QtGui import QTextCharFormat, QFont -from PyQt5.QtWidgets import QVBoxLayout, QLabel, QTextBrowser +from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QLabel, QTextBrowser from electrum.i18n import _ from .util import WindowModalDialog, ButtonsLineEdit, ShowQRLineEdit, ColorScheme, Buttons, CloseButton, MONOSPACE_FONT, WWLabel from .history_list import HistoryList, HistoryModel from .qrtextedit import ShowQRTextEdit +from .transaction_dialog import TxOutputColoring if TYPE_CHECKING: from .main_window import ElectrumWindow @@ -66,6 +67,10 @@ class UTXODialog(WindowModalDialog): self.parents_list.setMinimumWidth(900) self.parents_list.setMinimumHeight(400) self.parents_list.setLineWrapMode(QTextBrowser.NoWrap) + self.txo_color_parent = TxOutputColoring( + legend=_("Direct parent"), color=ColorScheme.BLUE, tooltip=_("Direct parent")) + self.txo_color_uncle = TxOutputColoring( + legend=_("Address reuse"), color=ColorScheme.RED, tooltip=_("Address reuse")) cursor = self.parents_list.textCursor() ext = QTextCharFormat() @@ -81,7 +86,7 @@ class UTXODialog(WindowModalDialog): ASCII_PIPE = '│' ASCII_SPACE = ' ' - def print_ascii_tree(_txid, prefix, is_last): + def print_ascii_tree(_txid, prefix, is_last, is_uncle): if _txid not in parents: return tx_height, tx_pos = self.wallet.adb.get_txpos(_txid) @@ -91,7 +96,10 @@ class UTXODialog(WindowModalDialog): label = '[duplicate]' c = '' if _txid == txid else (ASCII_EDGE if is_last else ASCII_BRANCH) cursor.insertText(prefix + c, ext) - lnk = QTextCharFormat() + if is_uncle: + lnk = QTextCharFormat(self.txo_color_uncle.text_char_format) + else: + lnk = QTextCharFormat(self.txo_color_parent.text_char_format) lnk.setToolTip(_('Click to open, right-click for menu')) lnk.setAnchorHref(_txid) #lnk.setAnchorNames([a_name]) @@ -102,22 +110,27 @@ class UTXODialog(WindowModalDialog): cursor.insertText(label, ext) cursor.insertBlock() next_prefix = '' if txid == _txid else prefix + (ASCII_SPACE if is_last else ASCII_PIPE) - parents_list = parents_copy.pop(_txid, []) - for i, p in enumerate(parents_list): - is_last = i == len(parents_list) - 1 - print_ascii_tree(p, next_prefix, is_last) + parents_list, uncle_list = parents_copy.pop(_txid, ([],[])) + for i, p in enumerate(parents_list + uncle_list): + is_last = (i == len(parents_list) + len(uncle_list)- 1) + is_uncle = (i > len(parents_list) - 1) + print_ascii_tree(p, next_prefix, is_last, is_uncle) + # recursively build the tree - print_ascii_tree(txid, '', False) + print_ascii_tree(txid, '', False, False) vbox = QVBoxLayout() vbox.addWidget(QLabel(_("Output point") + ": " + str(self.utxo.short_id))) vbox.addWidget(QLabel(_("Amount") + ": " + self.main_window.format_amount_and_units(self.utxo.value_sats()))) vbox.addWidget(QLabel(_("This UTXO has {} parent transactions in your wallet").format(num_parents))) vbox.addWidget(self.parents_list) - msg = ' '.join([ - _("Note: This analysis only shows parent transactions, and does not take address reuse into consideration."), - _("If you reuse addresses, more links can be established between your transactions, that are not displayed here.") - ]) - vbox.addWidget(WWLabel(msg)) + legend_hbox = QHBoxLayout() + legend_hbox.setContentsMargins(0, 0, 0, 0) + legend_hbox.addStretch(2) + legend_hbox.addWidget(self.txo_color_parent.legend_label) + legend_hbox.addWidget(self.txo_color_uncle.legend_label) + vbox.addLayout(legend_hbox) + self.txo_color_parent.legend_label.setVisible(True) + self.txo_color_uncle.legend_label.setVisible(True) vbox.addLayout(Buttons(CloseButton(self))) self.setLayout(vbox) # set cursor to top diff --git a/electrum/wallet.py b/electrum/wallet.py index 0985fa94a..9b0681589 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -871,23 +871,37 @@ class Abstract_Wallet(ABC, Logger, EventListener): """ if not self.is_up_to_date(): return {} - if self._last_full_history is None: - self._last_full_history = self.get_full_history(None) - with self.lock, self.transaction_lock: + if self._last_full_history is None: + self._last_full_history = self.get_full_history(None) result = self._tx_parents_cache.get(txid, None) if result is not None: return result result = {} parents = [] + uncles = [] tx = self.adb.get_transaction(txid) assert tx, f"cannot find {txid} in db" for i, txin in enumerate(tx.inputs()): _txid = txin.prevout.txid.hex() parents.append(_txid) + # detect address reuse + addr = self.adb.get_txin_address(txin) + received, sent = self.adb.get_addr_io(addr) + if len(sent) > 1: + my_txid, my_height, my_pos = sent[txin.prevout.to_str()] + assert my_txid == txid + for k, v in sent.items(): + if k != txin.prevout.to_str(): + reuse_txid, reuse_height, reuse_pos = v + if (reuse_height, reuse_pos) < (my_height, my_pos): + uncle_txid, uncle_index = k.split(':') + uncles.append(uncle_txid) + + for _txid in parents + uncles: if _txid in self._last_full_history.keys(): result.update(self.get_tx_parents(_txid)) - result[txid] = parents + result[txid] = parents, uncles self._tx_parents_cache[txid] = result return result