diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index cc45c5826..9a8464c98 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -243,7 +243,14 @@ class AddressSynchronizer(Logger, EventListener): return conflicting_txns def get_transaction(self, txid: str) -> Transaction: - return self.db.get_transaction(txid) + tx = self.db.get_transaction(txid) + # add verified tx info + tx.deserialize() + for txin in tx._inputs: + tx_height, tx_pos = self.get_txpos(txin.prevout.txid.hex()) + txin.block_height = tx_height + txin.block_txpos = tx_pos + return tx def add_transaction(self, tx: Transaction, *, allow_unrelated=False, is_new=True) -> bool: """ @@ -768,9 +775,10 @@ class AddressSynchronizer(Logger, EventListener): received = {} sent = {} for tx_hash, height in h: + hh, pos = self.get_txpos(tx_hash) d = self.db.get_txo_addr(tx_hash, address) for n, (v, is_cb) in d.items(): - received[tx_hash + ':%d'%n] = (height, v, is_cb) + 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: @@ -778,17 +786,18 @@ class AddressSynchronizer(Logger, EventListener): return received, sent def get_addr_outputs(self, address: str) -> Dict[TxOutpoint, PartialTxInput]: - coins, spent = self.get_addr_io(address) + received, sent = self.get_addr_io(address) out = {} - for prevout_str, v in coins.items(): - tx_height, value, is_cb = v + for prevout_str, v in received.items(): + tx_height, tx_pos, value, is_cb = v prevout = TxOutpoint.from_str(prevout_str) utxo = PartialTxInput(prevout=prevout, is_coinbase_output=is_cb) utxo._trusted_address = address utxo._trusted_value_sats = value utxo.block_height = tx_height - if prevout_str in spent: - txid, height = spent[prevout_str] + utxo.block_txpos = tx_pos + if prevout_str in sent: + txid, height = sent[prevout_str] utxo.spent_txid = txid utxo.spent_height = height else: @@ -807,7 +816,7 @@ class AddressSynchronizer(Logger, EventListener): # return the total amount ever received by an address def get_addr_received(self, address): received, sent = self.get_addr_io(address) - return sum([v for height, v, is_cb in received.values()]) + return sum([value for height, pos, value, is_cb in received.values()]) @with_local_height_cached def get_balance(self, domain, *, excluded_addresses: Set[str] = None, diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 0007b2212..0916af943 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -45,8 +45,9 @@ from electrum.bitcoin import base_encode, NLOCKTIME_BLOCKHEIGHT_MAX from electrum.i18n import _ from electrum.plugin import run_hook from electrum import simple_config -from electrum.transaction import SerializationError, Transaction, PartialTransaction, PartialTxInput +from electrum.transaction import SerializationError, Transaction, PartialTransaction, PartialTxInput, TxOutpoint from electrum.logging import get_logger +from electrum.util import ShortID from .util import (MessageBoxMixin, read_QIcon, Buttons, icon_path, MONOSPACE_FONT, ColorScheme, ButtonsLineEdit, ShowQRLineEdit, text_dialog, @@ -593,8 +594,17 @@ class BaseTxDialog(QDialog, MessageBoxMixin): return self.txo_color_2fa.text_char_format return ext - def format_amount(amt): - return self.main_window.format_amount(amt, whitespaces=True) + def insert_tx_io(cursor, is_coinbase, short_id, address, value): + if is_coinbase: + cursor.insertText('coinbase') + else: + address_str = address or '
' + value_str = self.main_window.format_amount(value, whitespaces=True) + cursor.insertText("%-15s\t"%str(short_id), ext) + cursor.insertText("%-62s"%address_str, text_format(address)) + cursor.insertText('\t', ext) + cursor.insertText(value_str, ext) + cursor.insertBlock() i_text = self.inputs_textedit i_text.clear() @@ -602,34 +612,26 @@ class BaseTxDialog(QDialog, MessageBoxMixin): i_text.setReadOnly(True) cursor = i_text.textCursor() for txin in self.tx.inputs(): - if txin.is_coinbase_input(): - cursor.insertText('coinbase') - else: - prevout_hash = txin.prevout.txid.hex() - prevout_n = txin.prevout.out_idx - cursor.insertText(prevout_hash + ":%-4d " % prevout_n, ext) - addr = self.wallet.adb.get_txin_address(txin) - if addr is None: - addr = '' - cursor.insertText(addr, text_format(addr)) - txin_value = self.wallet.adb.get_txin_value(txin) - if txin_value is not None: - cursor.insertText(format_amount(txin_value), ext) - cursor.insertBlock() + addr = self.wallet.adb.get_txin_address(txin) + txin_value = self.wallet.adb.get_txin_value(txin) + insert_tx_io(cursor, txin.is_coinbase_output(), txin.short_id, addr, txin_value) self.outputs_header.setText(_("Outputs") + ' (%d)'%len(self.tx.outputs())) o_text = self.outputs_textedit o_text.clear() o_text.setFont(QFont(MONOSPACE_FONT)) o_text.setReadOnly(True) + tx_height, tx_pos = self.wallet.adb.get_txpos(self.tx.txid()) + tx_hash = bytes.fromhex(self.tx.txid()) cursor = o_text.textCursor() - for o in self.tx.outputs(): - addr, v = o.get_ui_address_str(), o.value - cursor.insertText(addr, text_format(addr)) - if v is not None: - cursor.insertText('\t', ext) - cursor.insertText(format_amount(v), ext) - cursor.insertBlock() + for index, o in enumerate(self.tx.outputs()): + if tx_pos is not None and tx_pos >= 0: + short_id = ShortID.from_components(tx_height, tx_pos, index) + else: + short_id = TxOutpoint(tx_hash, index).short_name() + + addr, value = o.get_ui_address_str(), o.value + insert_tx_io(cursor, False, short_id, addr, value) self.txo_color_recv.legend_label.setVisible(tf_used_recv) self.txo_color_change.legend_label.setVisible(tf_used_change) diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py index 727e38d46..3c1d067e7 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -46,14 +46,12 @@ class UTXOList(MyTreeView): ADDRESS = 1 LABEL = 2 AMOUNT = 3 - HEIGHT = 4 headers = { + Columns.OUTPOINT: _('Output point'), Columns.ADDRESS: _('Address'), Columns.LABEL: _('Label'), Columns.AMOUNT: _('Amount'), - Columns.HEIGHT: _('Height'), - Columns.OUTPOINT: _('Output point'), } filter_columns = [Columns.ADDRESS, Columns.LABEL, Columns.OUTPOINT] stretch_column = Columns.LABEL @@ -86,10 +84,8 @@ class UTXOList(MyTreeView): name = utxo.prevout.to_str() self._utxo_dict[name] = utxo address = utxo.address - height = utxo.block_height - name_short = utxo.prevout.txid.hex()[:16] + '...' + ":%d" % utxo.prevout.out_idx amount = self.parent.format_amount(utxo.value_sats(), whitespaces=True) - labels = [name_short, address, '', amount, '%d'%height] + labels = [str(utxo.short_id), address, '', amount] utxo_item = [QStandardItem(x) for x in labels] self.set_editability(utxo_item) utxo_item[self.Columns.OUTPOINT].setData(name, self.ROLE_CLIPBOARD_DATA) diff --git a/electrum/lnutil.py b/electrum/lnutil.py index 2b1f3a7ce..90c079aa9 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -13,6 +13,9 @@ from aiorpcx import NetAddress from .util import bfh, bh2u, inv_dict, UserFacingException from .util import list_enabled_bits +from .util import ShortID as ShortChannelID +from .util import format_short_id as format_short_channel_id + from .crypto import sha256 from .transaction import (Transaction, PartialTransaction, PartialTxInput, TxOutpoint, PartialTxOutput, opcodes, TxOutput) @@ -1487,63 +1490,7 @@ NUM_MAX_HOPS_IN_PAYMENT_PATH = 20 NUM_MAX_EDGES_IN_PAYMENT_PATH = NUM_MAX_HOPS_IN_PAYMENT_PATH -class ShortChannelID(bytes): - - def __repr__(self): - return f"" - - def __str__(self): - return format_short_channel_id(self) - - @classmethod - def from_components(cls, block_height: int, tx_pos_in_block: int, output_index: int) -> 'ShortChannelID': - bh = block_height.to_bytes(3, byteorder='big') - tpos = tx_pos_in_block.to_bytes(3, byteorder='big') - oi = output_index.to_bytes(2, byteorder='big') - return ShortChannelID(bh + tpos + oi) - - @classmethod - def from_str(cls, scid: str) -> 'ShortChannelID': - """Parses a formatted scid str, e.g. '643920x356x0'.""" - components = scid.split("x") - if len(components) != 3: - raise ValueError(f"failed to parse ShortChannelID: {scid!r}") - try: - components = [int(x) for x in components] - except ValueError: - raise ValueError(f"failed to parse ShortChannelID: {scid!r}") from None - return ShortChannelID.from_components(*components) - - @classmethod - def normalize(cls, data: Union[None, str, bytes, 'ShortChannelID']) -> Optional['ShortChannelID']: - if isinstance(data, ShortChannelID) or data is None: - return data - if isinstance(data, str): - assert len(data) == 16 - return ShortChannelID.fromhex(data) - if isinstance(data, (bytes, bytearray)): - assert len(data) == 8 - return ShortChannelID(data) - - @property - def block_height(self) -> int: - return int.from_bytes(self[:3], byteorder='big') - - @property - def txpos(self) -> int: - return int.from_bytes(self[3:6], byteorder='big') - - @property - def output_index(self) -> int: - return int.from_bytes(self[6:8], byteorder='big') - -def format_short_channel_id(short_channel_id: Optional[bytes]): - if not short_channel_id: - return _('Not yet available') - return str(int.from_bytes(short_channel_id[:3], 'big')) \ - + 'x' + str(int.from_bytes(short_channel_id[3:6], 'big')) \ - + 'x' + str(int.from_bytes(short_channel_id[6:], 'big')) @attr.s(frozen=True) diff --git a/electrum/transaction.py b/electrum/transaction.py index 0fc7d713a..763d003d0 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -51,6 +51,7 @@ from .bitcoin import (TYPE_ADDRESS, TYPE_SCRIPT, hash_160, base_encode, construct_witness, construct_script) from .crypto import sha256d from .logging import get_logger +from .util import ShortID if TYPE_CHECKING: from .wallet import Abstract_Wallet @@ -212,6 +213,9 @@ class TxOutpoint(NamedTuple): def is_coinbase(self) -> bool: return self.txid == bytes(32) + def short_name(self): + return f"{self.txid.hex()[0:10]}:{self.out_idx}" + class TxInput: prevout: TxOutpoint @@ -231,6 +235,18 @@ class TxInput: self.nsequence = nsequence self.witness = witness self._is_coinbase_output = is_coinbase_output + # blockchain fields + self.block_height = None # type: Optional[int] # height at which the TXO is mined; None means unknown + self.block_txpos = None + self.spent_height = None # type: Optional[int] # height at which the TXO got spent + self.spent_txid = None # type: Optional[str] # txid of the spender + + @property + def short_id(self): + if self.block_txpos is not None and self.block_txpos >= 0: + return ShortID.from_components(self.block_height, self.block_txpos, self.prevout.out_idx) + else: + return self.prevout.short_name() def is_coinbase_input(self) -> bool: """Whether this is the input of a coinbase tx.""" @@ -1227,9 +1243,6 @@ class PartialTxInput(TxInput, PSBTSection): self.pubkeys = [] # type: List[bytes] # note: order matters self._trusted_value_sats = None # type: Optional[int] self._trusted_address = None # type: Optional[str] - self.block_height = None # type: Optional[int] # height at which the TXO is mined; None means unknown - self.spent_height = None # type: Optional[int] # height at which the TXO got spent - self.spent_txid = None # type: Optional[str] # txid of the spender self._is_p2sh_segwit = None # type: Optional[bool] # None means unknown self._is_native_segwit = None # type: Optional[bool] # None means unknown self.witness_sizehint = None # type: Optional[int] # byte size of serialized complete witness, for tx size est diff --git a/electrum/util.py b/electrum/util.py index a4388fd28..ba3738e35 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -1286,6 +1286,65 @@ class TxMinedInfo(NamedTuple): header_hash: Optional[str] = None # hash of block that mined tx +class ShortID(bytes): + + def __repr__(self): + return f"" + + def __str__(self): + return format_short_id(self) + + @classmethod + def from_components(cls, block_height: int, tx_pos_in_block: int, output_index: int) -> 'ShortID': + bh = block_height.to_bytes(3, byteorder='big') + tpos = tx_pos_in_block.to_bytes(3, byteorder='big') + oi = output_index.to_bytes(2, byteorder='big') + return ShortID(bh + tpos + oi) + + @classmethod + def from_str(cls, scid: str) -> 'ShortID': + """Parses a formatted scid str, e.g. '643920x356x0'.""" + components = scid.split("x") + if len(components) != 3: + raise ValueError(f"failed to parse ShortID: {scid!r}") + try: + components = [int(x) for x in components] + except ValueError: + raise ValueError(f"failed to parse ShortID: {scid!r}") from None + return ShortID.from_components(*components) + + @classmethod + def normalize(cls, data: Union[None, str, bytes, 'ShortChannelID']) -> Optional['ShortChannelID']: + if isinstance(data, ShortID) or data is None: + return data + if isinstance(data, str): + assert len(data) == 16 + return ShortID.fromhex(data) + if isinstance(data, (bytes, bytearray)): + assert len(data) == 8 + return ShortID(data) + + @property + def block_height(self) -> int: + return int.from_bytes(self[:3], byteorder='big') + + @property + def txpos(self) -> int: + return int.from_bytes(self[3:6], byteorder='big') + + @property + def output_index(self) -> int: + return int.from_bytes(self[6:8], byteorder='big') + + +def format_short_id(short_channel_id: Optional[bytes]): + if not short_channel_id: + return _('Not yet available') + return str(int.from_bytes(short_channel_id[:3], 'big')) \ + + 'x' + str(int.from_bytes(short_channel_id[3:6], 'big')) \ + + 'x' + str(int.from_bytes(short_channel_id[6:], 'big')) + + def make_aiohttp_session(proxy: Optional[dict], headers=None, timeout=None): if headers is None: headers = {'User-Agent': 'Electrum'} diff --git a/electrum/wallet.py b/electrum/wallet.py index 34258778f..f6ef08580 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2116,7 +2116,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): received, spent = self.adb.get_addr_io(address) item = received.get(txin.prevout.to_str()) if item: - txin_value = item[1] + txin_value = item[2] txin.witness_utxo = TxOutput.from_address_and_value(address, txin_value) if txin.utxo is None: txin.utxo = self.get_input_tx(txin.prevout.txid.hex(), ignore_network_issues=ignore_network_issues)