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.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user