Browse Source

Display mined tx outputs as ShortIDs instead of full transaction outpoints.

ShortIDs were originally designed for lightning channels, and are now
understood by some block explorers.

This allows to remove one column in the UTXO tab (height is redundant).

In the transaction dialog, the space saving ensures that all inputs fit
into one line (it was not the case previously with p2wsh addresses).
For clarity and consistency, the ShortID is displayed for both inputs
and outputs in the transaction dialog.
ThomasV 3 years ago
parent
commit
d6febb5c12
  1. 25
      electrum/address_synchronizer.py
  2. 50
      electrum/gui/qt/transaction_dialog.py
  3. 8
      electrum/gui/qt/utxo_list.py
  4. 59
      electrum/lnutil.py
  5. 19
      electrum/transaction.py
  6. 59
      electrum/util.py
  7. 2
      electrum/wallet.py

25
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,

50
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 '<address unknown>'
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)

8
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)

59
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"<ShortChannelID: {format_short_channel_id(self)}>"
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)

19
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

59
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"<ShortID: {format_short_channel_id(self)}>"
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'}

2
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)

Loading…
Cancel
Save