adb: change API of util.TxMinedInfo: height() is now always SPV-ed

This commit is contained in:
SomberNight
2025-09-10 15:24:28 +00:00
parent 47f1a2d7a2
commit b944371ffd
21 changed files with 105 additions and 87 deletions
+21 -20
View File
@@ -23,6 +23,7 @@
import asyncio
import copy
import dataclasses
import threading
import itertools
from collections import defaultdict
@@ -146,7 +147,7 @@ class AddressSynchronizer(Logger, EventListener):
h = {}
related_txns = self._history_local.get(addr, set())
for tx_hash in related_txns:
tx_height = self.get_tx_height(tx_hash).height
tx_height = self.get_tx_height(tx_hash).height()
h[tx_hash] = tx_height
return h
@@ -272,7 +273,7 @@ class AddressSynchronizer(Logger, EventListener):
tx.deserialize()
for txin in tx._inputs:
tx_mined_info = self.get_tx_height(txin.prevout.txid.hex())
txin.block_height = tx_mined_info.height # not SPV-ed
txin.block_height = tx_mined_info.height()
txin.block_txpos = tx_mined_info.txpos
return tx
@@ -297,7 +298,7 @@ class AddressSynchronizer(Logger, EventListener):
# of add_transaction tx, we might learn of more-and-more inputs of
# being is_mine, as we roll the gap_limit forward
is_coinbase = tx.inputs()[0].is_coinbase_input()
tx_height = self.get_tx_height(tx_hash, force_local_if_missing_tx=False).height
tx_height = self.get_tx_height(tx_hash, force_local_if_missing_tx=False).height()
if not allow_unrelated:
# note that during sync, if the transactions are not properly sorted,
# it could happen that we think tx is unrelated but actually one of the inputs is is_mine.
@@ -316,10 +317,10 @@ class AddressSynchronizer(Logger, EventListener):
conflicting_txns = self.get_conflicting_transactions(tx)
if conflicting_txns:
existing_mempool_txn = any(
self.get_tx_height(tx_hash2).height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT)
self.get_tx_height(tx_hash2).height() in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT)
for tx_hash2 in conflicting_txns)
existing_confirmed_txn = any(
self.get_tx_height(tx_hash2).height > 0
self.get_tx_height(tx_hash2).height() > 0
for tx_hash2 in conflicting_txns)
if existing_confirmed_txn and tx_height <= 0:
# this is a non-confirmed tx that conflicts with confirmed txns; drop.
@@ -502,7 +503,7 @@ class AddressSynchronizer(Logger, EventListener):
@with_lock
def remove_local_transactions_we_dont_have(self):
for txid in itertools.chain(self.db.list_txi(), self.db.list_txo()):
tx_height = self.get_tx_height(txid).height
tx_height = self.get_tx_height(txid).height()
if tx_height == TX_HEIGHT_LOCAL and not self.db.get_transaction(txid):
self.remove_transaction(txid)
@@ -516,7 +517,7 @@ class AddressSynchronizer(Logger, EventListener):
def _get_tx_sort_key(self, tx_hash: str) -> Tuple[int, int]:
"""Returns a key to be used for sorting txs."""
tx_mined_info = self.get_tx_height(tx_hash)
height = self.tx_height_to_sort_height(tx_mined_info.height)
height = self.tx_height_to_sort_height(tx_mined_info.height())
txpos = tx_mined_info.txpos or -1
return height, txpos
@@ -664,7 +665,7 @@ class AddressSynchronizer(Logger, EventListener):
with self.lock:
for tx_hash in self.db.list_verified_tx():
info = self.db.get_verified_tx(tx_hash)
tx_height = info.height
tx_height = info._height
if tx_height > above_height:
header = blockchain.read_header(tx_height)
if not header or hash_header(header) != info.header_hash:
@@ -711,25 +712,25 @@ class AddressSynchronizer(Logger, EventListener):
force_local_if_missing_tx: bool = True,
) -> TxMinedInfo:
if tx_hash is None: # ugly backwards compat...
return TxMinedInfo(height=TX_HEIGHT_LOCAL, conf=0)
return TxMinedInfo(_height=TX_HEIGHT_LOCAL, conf=0)
with self.lock:
if verified_tx_mined_info := self.db.get_verified_tx(tx_hash): # mined and spv-ed
conf = max(self.get_local_height() - verified_tx_mined_info.height + 1, 0)
tx_mined_info = verified_tx_mined_info._replace(conf=conf)
conf = max(self.get_local_height() - verified_tx_mined_info._height + 1, 0)
tx_mined_info = dataclasses.replace(verified_tx_mined_info, conf=conf)
elif tx_hash in self.unverified_tx: # mined, no spv
height = self.unverified_tx[tx_hash]
tx_mined_info = TxMinedInfo(height=height, conf=0)
tx_mined_info = TxMinedInfo(_height=height, conf=0)
elif tx_hash in self.unconfirmed_tx: # mempool
height = self.unconfirmed_tx[tx_hash]
tx_mined_info = TxMinedInfo(height=height, conf=0)
tx_mined_info = TxMinedInfo(_height=height, conf=0)
elif wanted_height := self.future_tx.get(tx_hash): # future
if wanted_height > self.get_local_height():
tx_mined_info = TxMinedInfo(height=TX_HEIGHT_FUTURE, conf=0, wanted_height=wanted_height)
tx_mined_info = TxMinedInfo(_height=TX_HEIGHT_FUTURE, conf=0, wanted_height=wanted_height)
else:
tx_mined_info = TxMinedInfo(height=TX_HEIGHT_LOCAL, conf=0)
tx_mined_info = TxMinedInfo(_height=TX_HEIGHT_LOCAL, conf=0)
else: # local
tx_mined_info = TxMinedInfo(height=TX_HEIGHT_LOCAL, conf=0)
if tx_mined_info.height in (TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE):
tx_mined_info = TxMinedInfo(_height=TX_HEIGHT_LOCAL, conf=0)
if tx_mined_info.height() in (TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE):
return tx_mined_info
if force_local_if_missing_tx:
# It can happen for a txid in any state (unconf/unverified/verified) that we
@@ -739,7 +740,7 @@ class AddressSynchronizer(Logger, EventListener):
# a different tx if only the witness differs. We should compare wtxids.
tx = self.db.get_transaction(tx_hash)
if tx is None or isinstance(tx, PartialTransaction):
return TxMinedInfo(height=TX_HEIGHT_LOCAL, conf=0)
return TxMinedInfo(_height=TX_HEIGHT_LOCAL, conf=0)
return tx_mined_info
def up_to_date_changed(self) -> None:
@@ -1047,7 +1048,7 @@ class AddressSynchronizer(Logger, EventListener):
spender_txid = self.db.get_spent_outpoint(prev_txid, int(index))
# discard local spenders
tx_mined_status = self.get_tx_height(spender_txid)
if tx_mined_status.height in [TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE]:
if tx_mined_status.height() in [TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE]:
spender_txid = None
if not spender_txid:
return None
@@ -1063,7 +1064,7 @@ class AddressSynchronizer(Logger, EventListener):
if not txid:
return TxMinedDepth.FREE
tx_mined_depth = self.get_tx_height(txid)
height, conf = tx_mined_depth.height, tx_mined_depth.conf
height, conf = tx_mined_depth.height(), tx_mined_depth.conf
if conf > 20: # FIXME unify with lnutil.REDEEM_AFTER_DOUBLE_SPENT_DELAY ?
return TxMinedDepth.DEEP
elif conf > 0:
+1 -1
View File
@@ -1634,7 +1634,7 @@ class Commands(Logger):
arg:txid:txid:Transaction ID
"""
height = wallet.adb.get_tx_height(txid).height
height = wallet.adb.get_tx_height(txid).height()
if height != TX_HEIGHT_LOCAL:
raise UserFacingException(
f'Only local transactions can be removed. '
+4 -4
View File
@@ -196,7 +196,7 @@ class QETransactionListModel(QAbstractListModel, QtEventListener):
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'],
_height=tx_item['height'],
conf=tx_item['confirmations'],
timestamp=tx_item['timestamp'],
wanted_height=tx_item.get('wanted_height', None),
@@ -229,10 +229,10 @@ class QETransactionListModel(QAbstractListModel, QtEventListener):
self._dirty = False
def on_tx_verified(self, txid, info):
def on_tx_verified(self, txid: str, info: TxMinedInfo):
for i, tx in enumerate(self.tx_history):
if 'txid' in tx and tx['txid'] == txid:
tx['height'] = info.height
tx['height'] = info.height()
tx['confirmations'] = info.conf
tx['timestamp'] = info.timestamp
tx['section'] = self.get_section_by_timestamp(info.timestamp)
@@ -255,7 +255,7 @@ class QETransactionListModel(QAbstractListModel, QtEventListener):
status, status_str = self.wallet.get_tx_status(txid, txinfo.tx_mined_status)
tx_item['date'] = status_str
# note: if the height changes, that might affect the history order, but we won't re-sort now.
tx_item['height'] = self.wallet.adb.get_tx_height(txid).height
tx_item['height'] = self.wallet.adb.get_tx_height(txid).height()
index = self.index(tx_item_idx, 0)
roles = [self._ROLE_RMAP[x] for x in ['height', 'date']]
self.dataChanged.emit(index, index, roles)
+3 -3
View File
@@ -352,14 +352,14 @@ class QETxDetails(QObject, QtEventListener):
self._lock_delay = 0
self._in_mempool = False
self._is_mined = False if not txinfo.tx_mined_status else txinfo.tx_mined_status.height > 0
self._is_mined = False if not txinfo.tx_mined_status else txinfo.tx_mined_status.height() > 0
if self._is_mined:
self.update_mined_status(txinfo.tx_mined_status)
else:
if txinfo.tx_mined_status.height in [TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT]:
if txinfo.tx_mined_status.height() in [TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT]:
self._mempool_depth = FeePolicy.depth_tooltip(txinfo.mempool_depth_bytes)
self._in_mempool = True
elif txinfo.tx_mined_status.height == TX_HEIGHT_FUTURE:
elif txinfo.tx_mined_status.height() == TX_HEIGHT_FUTURE:
self._lock_delay = txinfo.tx_mined_status.wanted_height - self._wallet.wallet.adb.get_local_height()
if isinstance(self._tx, PartialTransaction):
self._sighash_danger = self._wallet.wallet.check_sighash(self._tx)
+2 -2
View File
@@ -439,7 +439,7 @@ class HistoryModel(CustomModel, Logger):
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 qml-gui
tx_mined_info = TxMinedInfo(
height=tx_item['height'],
_height=tx_item['height'],
conf=tx_item['confirmations'],
timestamp=tx_item['timestamp'],
wanted_height=tx_item.get('wanted_height', None),
@@ -764,7 +764,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop):
return
tx_URL = block_explorer_URL(self.config, 'tx', tx_hash)
tx_details = self.wallet.get_tx_info(tx)
is_unconfirmed = tx_details.tx_mined_status.height <= 0
is_unconfirmed = tx_details.tx_mined_status.height() <= 0
menu = QMenu()
menu.addAction(_("Details"), lambda: self.main_window.show_transaction(tx))
if tx_details.can_remove:
+2 -2
View File
@@ -282,7 +282,7 @@ class TxInOutWidget(QWidget):
tx_hash = self.tx.txid()
if tx_hash:
tx_mined_info = self.wallet.adb.get_tx_height(tx_hash)
tx_height = tx_mined_info.height
tx_height = tx_mined_info.height()
tx_pos = tx_mined_info.txpos
cursor = o_text.textCursor()
for txout_idx, o in enumerate(self.tx.outputs()):
@@ -915,7 +915,7 @@ class TxDialog(QDialog, MessageBoxMixin):
self.rbf_label.setText(_('Replace by fee: {}').format(_('True') if self.tx.is_rbf_enabled() else _('False')))
if tx_mined_status.header_hash:
self.block_height_label.setText(_("At block height: {}").format(tx_mined_status.height))
self.block_height_label.setText(_("At block height: {}").format(tx_mined_status.height()))
else:
self.block_height_label.hide()
if amount is None and ln_amount is None:
+1 -1
View File
@@ -108,7 +108,7 @@ class UTXODialog(WindowModalDialog):
if _txid not in parents:
return
tx_mined_info = self.wallet.adb.get_tx_height(_txid)
tx_height = tx_mined_info.height
tx_height = tx_mined_info.height()
tx_pos = tx_mined_info.txpos
key = "%dx%d"%(tx_height, tx_pos) if tx_pos is not None else _txid[0:8]
label = self.wallet.get_label_for_txid(_txid) or ""
+7 -7
View File
@@ -336,9 +336,9 @@ class AbstractChannel(Logger, ABC):
closing_txid: str, closing_height: TxMinedInfo, keep_watching: bool) -> None:
# note: state transitions are irreversible, but
# save_funding_height, save_closing_height are reversible
if funding_height.height == TX_HEIGHT_LOCAL:
if funding_height.height() == TX_HEIGHT_LOCAL:
self.update_unfunded_state()
elif closing_height.height == TX_HEIGHT_LOCAL:
elif closing_height.height() == TX_HEIGHT_LOCAL:
self.update_funded_state(
funding_txid=funding_txid,
funding_height=funding_height)
@@ -401,11 +401,11 @@ class AbstractChannel(Logger, ABC):
self.lnworker.remove_channel(self.channel_id)
def update_funded_state(self, *, funding_txid: str, funding_height: TxMinedInfo) -> None:
self.save_funding_height(txid=funding_txid, height=funding_height.height, timestamp=funding_height.timestamp)
self.save_funding_height(txid=funding_txid, height=funding_height.height(), timestamp=funding_height.timestamp)
self.delete_closing_height()
if funding_height.conf>0:
self.set_short_channel_id(ShortChannelID.from_components(
funding_height.height, funding_height.txpos, self.funding_outpoint.output_index))
funding_height.height(), funding_height.txpos, self.funding_outpoint.output_index))
if self.get_state() == ChannelState.OPENING:
if self.is_funding_tx_mined(funding_height):
self.set_state(ChannelState.FUNDED)
@@ -423,11 +423,11 @@ class AbstractChannel(Logger, ABC):
def update_closed_state(self, *, funding_txid: str, funding_height: TxMinedInfo,
closing_txid: str, closing_height: TxMinedInfo, keep_watching: bool) -> None:
self.save_funding_height(txid=funding_txid, height=funding_height.height, timestamp=funding_height.timestamp)
self.save_closing_height(txid=closing_txid, height=closing_height.height, timestamp=closing_height.timestamp)
self.save_funding_height(txid=funding_txid, height=funding_height.height(), timestamp=funding_height.timestamp)
self.save_closing_height(txid=closing_txid, height=closing_height.height(), timestamp=closing_height.timestamp)
if funding_height.conf>0:
self.set_short_channel_id(ShortChannelID.from_components(
funding_height.height, funding_height.txpos, self.funding_outpoint.output_index))
funding_height.height(), funding_height.txpos, self.funding_outpoint.output_index))
if self.get_state() < ChannelState.CLOSED:
conf = closing_height.conf
if conf > 0:
+1 -1
View File
@@ -279,5 +279,5 @@ class LNWatcher(Logger, EventListener):
# We should not keep warning the user forever.
return
tx_mined_status = self.adb.get_tx_height(spender_txid)
if tx_mined_status.height == TX_HEIGHT_LOCAL:
if tx_mined_status.height() == TX_HEIGHT_LOCAL:
self._pending_force_closes.add(chan)
+1 -1
View File
@@ -1264,7 +1264,7 @@ class LNWallet(LNWorker):
elif chan.get_state() == ChannelState.FORCE_CLOSING:
force_close_tx = chan.force_close_tx()
txid = force_close_tx.txid()
height = self.lnwatcher.adb.get_tx_height(txid).height
height = self.lnwatcher.adb.get_tx_height(txid).height()
if height == TX_HEIGHT_LOCAL:
self.logger.info('REBROADCASTING CLOSING TX')
await self.network.try_broadcasting(force_close_tx, 'force-close')
+1 -1
View File
@@ -218,7 +218,7 @@ class WatchTower(Logger, EventListener):
return keep_watching
async def broadcast_or_log(self, funding_outpoint: str, tx: Transaction):
height = self.adb.get_tx_height(tx.txid()).height
height = self.adb.get_tx_height(tx.txid()).height()
if height != TX_HEIGHT_LOCAL:
return
try:
+3 -3
View File
@@ -1440,7 +1440,7 @@ class SwapManager(Logger):
delta = current_height - swap.locktime
if self.wallet.adb.is_mine(swap.lockup_address):
tx_height = self.wallet.adb.get_tx_height(swap.funding_txid)
if swap.is_reverse and tx_height.height <= 0:
if swap.is_reverse and tx_height.height() <= 0:
label += ' (%s)' % _('waiting for funding tx confirmation')
if not swap.is_reverse and not swap.is_redeemed and swap.spending_txid is None and delta < 0:
label += f' (refundable in {-delta} blocks)' # fixme: only if unspent
@@ -1481,8 +1481,8 @@ class SwapManager(Logger):
# and adb will return TX_HEIGHT_LOCAL
continue
# note: adb.get_tx_height returns TX_HEIGHT_LOCAL if the txid is unknown
funding_height = self.lnworker.wallet.adb.get_tx_height(swap.funding_txid).height
spending_height = self.lnworker.wallet.adb.get_tx_height(swap.spending_txid).height
funding_height = self.lnworker.wallet.adb.get_tx_height(swap.funding_txid).height()
spending_height = self.lnworker.wallet.adb.get_tx_height(swap.spending_txid).height()
if funding_height > TX_HEIGHT_LOCAL and spending_height <= TX_HEIGHT_LOCAL:
pending_swaps.append(swap)
return pending_swaps
+6 -6
View File
@@ -174,9 +174,9 @@ class TxBatcher(Logger):
prev_txid, index = outpoint.split(':')
if spender_txid := self.wallet.adb.db.get_spent_outpoint(prev_txid, int(index)):
tx_mined_status = self.wallet.adb.get_tx_height(spender_txid)
if tx_mined_status.height > 0:
if tx_mined_status.height() > 0:
return
if tx_mined_status.height not in [TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE]:
if tx_mined_status.height() not in [TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE]:
return
self.logger.info(f'will broadcast standalone tx {sweep_info.name}')
tx = PartialTransaction.from_io([sweep_info.txin], [sweep_info.txout], locktime=sweep_info.cltv_abs, version=2)
@@ -293,7 +293,7 @@ class TxBatch(Logger):
prev_txid, index = prevout.split(':')
if spender_txid := self.wallet.adb.db.get_spent_outpoint(prev_txid, int(index)):
tx_mined_status = self.wallet.adb.get_tx_height(spender_txid)
if tx_mined_status.height > 0:
if tx_mined_status.height() > 0:
return
self._unconfirmed_sweeps.add(txin.prevout)
self.logger.info(f'add_sweep_info: {sweep_info.name} {sweep_info.txin.prevout.to_str()}')
@@ -331,7 +331,7 @@ class TxBatch(Logger):
continue
if spender_txid := self.wallet.adb.db.get_spent_outpoint(prev_txid, int(index)):
tx_mined_status = self.wallet.adb.get_tx_height(spender_txid)
if tx_mined_status.height not in [TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE]:
if tx_mined_status.height() not in [TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE]:
continue
if prevout in tx_prevouts:
continue
@@ -378,7 +378,7 @@ class TxBatch(Logger):
self.logger.info(f'base tx confirmed {txid}')
self._clear_unconfirmed_sweeps(tx)
self._start_new_batch(tx)
if tx_mined_status.height in [TX_HEIGHT_LOCAL]:
if tx_mined_status.height() in [TX_HEIGHT_LOCAL]:
# this may happen if our Electrum server is unresponsive
# server could also be lying to us. Rebroadcasting might
# help, if we have switched to another server.
@@ -590,7 +590,7 @@ class TxBatch(Logger):
wanted_height_cltv = sweep_info.cltv_abs
if wanted_height_cltv - local_height > 0:
can_broadcast = False
prev_height = self.wallet.adb.get_tx_height(prev_txid).height
prev_height = self.wallet.adb.get_tx_height(prev_txid).height()
if sweep_info.csv_delay:
if prev_height > 0:
wanted_height_csv = prev_height + sweep_info.csv_delay - 1
+18 -7
View File
@@ -1264,26 +1264,37 @@ def with_lock(func):
return func_wrapper
class TxMinedInfo(NamedTuple):
height: int # height of block that mined tx
@dataclass(frozen=True, kw_only=True)
class TxMinedInfo:
_height: int # height of block that mined tx
conf: Optional[int] = None # number of confirmations, SPV verified. >=0, or None (None means unknown)
timestamp: Optional[int] = None # timestamp of block that mined tx
txpos: Optional[int] = None # position of tx in serialized block
header_hash: Optional[str] = None # hash of block that mined tx
wanted_height: Optional[int] = None # in case of timelock, min abs block height
def height(self) -> int:
"""Treat unverified heights as unconfirmed."""
h = self._height
if h > 0:
if self.conf is not None and self.conf >= 1:
return h
return 0 # treat it as unconfirmed until SPV-ed
else: # h <= 0
return h
def short_id(self) -> Optional[str]:
if self.txpos is not None and self.txpos >= 0:
assert self.height > 0
return f"{self.height}x{self.txpos}"
assert self.height() > 0
return f"{self.height()}x{self.txpos}"
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:
if self.height() > 0:
return False
if self.height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT):
if self.height() in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT):
return False
return True
@@ -2360,7 +2371,7 @@ class OnchainHistoryItem(NamedTuple):
'txid': self.txid,
'amount_sat': self.amount_sat,
'fee_sat': self.fee_sat,
'height': self.tx_mined_status.height,
'height': self.tx_mined_status.height(),
'confirmations': self.tx_mined_status.conf,
'timestamp': self.tx_mined_status.timestamp,
'monotonic_timestamp': self.monotonic_timestamp,
+1 -1
View File
@@ -131,7 +131,7 @@ class SPV(NetworkJobOnDefaultServer):
self.requested_merkle.discard(tx_hash)
self.logger.info(f"verified {tx_hash}")
header_hash = hash_header(header)
tx_info = TxMinedInfo(height=tx_height,
tx_info = TxMinedInfo(_height=tx_height,
timestamp=header.get('timestamp'),
txpos=pos,
header_hash=header_hash)
+12 -12
View File
@@ -949,7 +949,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
and (tx_we_already_have_in_db is None or not tx_we_already_have_in_db.is_complete()))
label = ''
tx_mined_status = self.adb.get_tx_height(tx_hash)
can_remove = ((tx_mined_status.height in [TX_HEIGHT_FUTURE, TX_HEIGHT_LOCAL])
can_remove = ((tx_mined_status.height() in [TX_HEIGHT_FUTURE, TX_HEIGHT_LOCAL])
# otherwise 'height' is unreliable (typically LOCAL):
and is_relevant
# don't offer during common signing flow, e.g. when watch-only wallet starts creating a tx:
@@ -958,12 +958,12 @@ class Abstract_Wallet(ABC, Logger, EventListener):
if tx.is_complete():
if tx_we_already_have_in_db:
label = self.get_label_for_txid(tx_hash)
if tx_mined_status.height > 0:
if tx_mined_status.height() > 0:
if tx_mined_status.conf:
status = _("{} confirmations").format(tx_mined_status.conf)
else:
status = _('Not verified')
elif tx_mined_status.height in (TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED):
elif tx_mined_status.height() in (TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED):
status = _('Unconfirmed')
if fee is None:
fee = self.adb.get_tx_fee(tx_hash)
@@ -981,7 +981,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
can_cpfp = False
else:
status = _('Local')
if tx_mined_status.height == TX_HEIGHT_FUTURE:
if tx_mined_status.height() == TX_HEIGHT_FUTURE:
num_blocks_remainining = tx_mined_status.wanted_height - self.adb.get_local_height()
num_blocks_remainining = max(0, num_blocks_remainining)
status = _('Local (future: {})').format(_('in {} blocks').format(num_blocks_remainining))
@@ -1043,7 +1043,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
# populate cache in chronological order (confirmed tx only)
# todo: get_full_history should return unconfirmed tx topologically sorted
for _txid, tx_item in self._last_full_history.items():
if tx_item.tx_mined_status.height > 0:
if tx_item.tx_mined_status.height() > 0:
self.get_tx_parents(_txid)
result = self._tx_parents_cache.get(txid, None)
@@ -1224,7 +1224,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
monotonic_timestamp = 0
for hist_item in self.adb.get_history(domain=domain):
timestamp = (hist_item.tx_mined_status.timestamp or TX_TIMESTAMP_INF)
height = hist_item.tx_mined_status.height
height = hist_item.tx_mined_status.height()
if from_timestamp and (timestamp or now) < from_timestamp:
continue
if to_timestamp and (timestamp or now) >= to_timestamp:
@@ -1405,7 +1405,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
for prevout, v in prevouts_and_values:
relevant_txs.add(prevout.txid.hex())
tx_height = self.adb.get_tx_height(prevout.txid.hex())
if 0 < tx_height.height <= invoice.height: # exclude txs older than invoice
if 0 < tx_height.height() <= invoice.height: # exclude txs older than invoice
continue
confs_and_values.append((tx_height.conf or 0, v))
# check that there is at least one TXO, and that they pay enough.
@@ -1748,7 +1748,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
def get_tx_status(self, tx_hash: str, tx_mined_info: TxMinedInfo):
extra = []
height = tx_mined_info.height
height = tx_mined_info.height()
conf = tx_mined_info.conf
timestamp = tx_mined_info.timestamp
if height == TX_HEIGHT_FUTURE:
@@ -1812,7 +1812,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
# tx should not be mined yet
if hist_item.tx_mined_status.conf > 0: continue
# conservative future proofing of code: only allow known unconfirmed types
if hist_item.tx_mined_status.height not in (
if hist_item.tx_mined_status.height() not in (
TX_HEIGHT_UNCONFIRMED,
TX_HEIGHT_UNCONF_PARENT,
TX_HEIGHT_LOCAL):
@@ -2016,7 +2016,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
if base_tx:
# make sure we don't try to spend change from the tx-to-be-replaced:
coins = [c for c in coins if c.prevout.txid.hex() != base_tx.txid()]
is_local = self.adb.get_tx_height(base_tx.txid()).height == TX_HEIGHT_LOCAL
is_local = self.adb.get_tx_height(base_tx.txid()).height() == TX_HEIGHT_LOCAL
if not isinstance(base_tx, PartialTransaction):
base_tx = PartialTransaction.from_tx(base_tx)
base_tx.add_info_from_wallet(self)
@@ -2149,7 +2149,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
return bool(frozen)
# State not set. We implicitly mark certain coins as frozen:
tx_mined_status = self.adb.get_tx_height(utxo.prevout.txid.hex())
if tx_mined_status.height == TX_HEIGHT_FUTURE:
if tx_mined_status.height() == TX_HEIGHT_FUTURE:
return True
if self._is_coin_small_and_unconfirmed(utxo):
return True
@@ -2675,7 +2675,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
txin.script_descriptor = self.get_script_descriptor_for_address(address)
txin.is_mine = True
self._add_txinout_derivation_info(txin, address, only_der_suffix=only_der_suffix)
txin.block_height = self.adb.get_tx_height(txin.prevout.txid.hex()).height
txin.block_height = self.adb.get_tx_height(txin.prevout.txid.hex()).height()
def has_support_for_slip_19_ownership_proofs(self) -> bool:
return False
+4 -2
View File
@@ -1508,7 +1508,7 @@ class WalletDB(JsonDB):
if txid not in self.verified_tx:
return None
height, timestamp, txpos, header_hash = self.verified_tx[txid]
return TxMinedInfo(height=height,
return TxMinedInfo(_height=height,
conf=None,
timestamp=timestamp,
txpos=txpos,
@@ -1518,7 +1518,9 @@ class WalletDB(JsonDB):
def add_verified_tx(self, txid: str, info: TxMinedInfo):
assert isinstance(txid, str)
assert isinstance(info, TxMinedInfo)
self.verified_tx[txid] = (info.height, info.timestamp, info.txpos, info.header_hash)
height = info._height # number of conf is dynamic and might not be set here
assert height > 0, height
self.verified_tx[txid] = (height, info.timestamp, info.txpos, info.header_hash)
@modifier
def remove_verified_tx(self, txid: str):
+3 -3
View File
@@ -80,7 +80,7 @@ class TestWalletPaymentRequests(ElectrumTestCase):
self.assertEqual(PR_UNCONFIRMED, wallet1.get_invoice_status(pr))
# tx gets mined
wallet1.db.put('stored_height', 1010)
tx_info = TxMinedInfo(height=1001,
tx_info = TxMinedInfo(_height=1001,
timestamp=pr.get_time() + 100,
txpos=1,
header_hash="01"*32)
@@ -111,7 +111,7 @@ class TestWalletPaymentRequests(ElectrumTestCase):
self.assertEqual(PR_UNCONFIRMED, wallet1.get_invoice_status(pr))
# tx gets mined
wallet1.db.put('stored_height', 1010)
tx_info = TxMinedInfo(height=1001,
tx_info = TxMinedInfo(_height=1001,
timestamp=pr.get_time() + 100,
txpos=1,
header_hash="01"*32)
@@ -141,7 +141,7 @@ class TestWalletPaymentRequests(ElectrumTestCase):
wallet1.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)
self.assertEqual(PR_UNCONFIRMED, wallet1.get_invoice_status(pr))
# tx mined in the past (before invoice creation)
tx_info = TxMinedInfo(height=990,
tx_info = TxMinedInfo(_height=990,
timestamp=pr.get_time() + 100,
txpos=1,
header_hash="01" * 32)
+6 -2
View File
@@ -2,6 +2,8 @@ import unittest
import logging
from unittest import mock
import asyncio
import dataclasses
from aiorpcx import timeout_after
from electrum import storage, bitcoin, keystore, wallet
@@ -152,7 +154,7 @@ class TestTxBatcher(ElectrumTestCase):
# tx1 gets confirmed, tx2 gets removed
wallet.adb.receive_tx_callback(tx1, tx_height=1)
tx_mined_status = wallet.adb.get_tx_height(tx1.txid())
wallet.adb.add_verified_tx(tx1.txid(), tx_mined_status._replace(conf=1))
wallet.adb.add_verified_tx(tx1.txid(), dataclasses.replace(tx_mined_status, conf=1))
assert wallet.adb.get_transaction(tx1.txid()) is not None
assert wallet.adb.get_transaction(tx1_prime.txid()) is None
# txbatcher creates tx2
@@ -195,7 +197,7 @@ class TestTxBatcher(ElectrumTestCase):
# tx1 gets confirmed
wallet.adb.receive_tx_callback(tx1, tx_height=1)
tx_mined_status = wallet.adb.get_tx_height(tx1.txid())
wallet.adb.add_verified_tx(tx1.txid(), tx_mined_status._replace(conf=1))
wallet.adb.add_verified_tx(tx1.txid(), dataclasses.replace(tx_mined_status, conf=1))
tx2 = await self.network.next_tx()
assert len(tx2.outputs()) == 2
@@ -209,6 +211,8 @@ class TestTxBatcher(ElectrumTestCase):
wallet = self._create_wallet()
wallet.adb.db.transactions[SWAPDATA.funding_txid] = tx = Transaction(SWAP_FUNDING_TX)
wallet.adb.receive_tx_callback(tx, tx_height=1)
tx_mined_status = wallet.adb.get_tx_height(tx.txid())
wallet.adb.add_verified_tx(tx.txid(), dataclasses.replace(tx_mined_status, conf=1))
wallet.txbatcher.add_sweep_input('default', SWAP_SWEEP_INFO)
tx = await self.network.next_tx()
txid = tx.txid()
+1 -1
View File
@@ -164,7 +164,7 @@ class FakeADB:
def get_tx_height(self, txid):
# because we use a current timestamp, and history is empty,
# FxThread.history_rate will use spot prices
return TxMinedInfo(height=10, conf=10, timestamp=int(time.time()), header_hash='def')
return TxMinedInfo(_height=10, conf=10, timestamp=int(time.time()), header_hash='def')
class FakeWallet:
def __init__(self, fiat_value):
+7 -7
View File
@@ -2924,7 +2924,7 @@ class TestWalletSending(ElectrumTestCase):
assert payment_txid
# save payment_tx as LOCAL and UNSIGNED
wallet.adb.add_transaction(payment_tx)
self.assertEqual(TX_HEIGHT_LOCAL, wallet.adb.get_tx_height(payment_txid).height)
self.assertEqual(TX_HEIGHT_LOCAL, wallet.adb.get_tx_height(payment_txid).height())
self.assertEqual(1, len(wallet.get_spendable_coins(nonlocal_only=True)))
self.assertEqual(2, len(wallet.get_spendable_coins(nonlocal_only=False)))
# transition payment_tx to mempool (but it is still unsigned!)
@@ -2937,13 +2937,13 @@ class TestWalletSending(ElectrumTestCase):
# but the wallet db does not yet have the corresponding full tx.
# In such cases, we instead want the txid to be considered LOCAL.
wallet.adb.receive_tx_callback(payment_tx, tx_height=TX_HEIGHT_UNCONFIRMED)
self.assertEqual(TX_HEIGHT_LOCAL, wallet.adb.get_tx_height(payment_txid).height)
self.assertEqual(TX_HEIGHT_LOCAL, wallet.adb.get_tx_height(payment_txid).height())
self.assertEqual(1, len(wallet.get_spendable_coins(nonlocal_only=True)))
self.assertEqual(2, len(wallet.get_spendable_coins(nonlocal_only=False)))
# wallet gets signed tx (e.g. from network). payment_tx is now considered to be in mempool
wallet.sign_transaction(payment_tx, password=None)
wallet.adb.receive_tx_callback(payment_tx, tx_height=TX_HEIGHT_UNCONFIRMED)
self.assertEqual(TX_HEIGHT_UNCONFIRMED, wallet.adb.get_tx_height(payment_txid).height)
self.assertEqual(TX_HEIGHT_UNCONFIRMED, wallet.adb.get_tx_height(payment_txid).height())
self.assertEqual(2, len(wallet.get_spendable_coins(nonlocal_only=True)))
self.assertEqual(2, len(wallet.get_spendable_coins(nonlocal_only=False)))
@@ -2962,10 +2962,10 @@ class TestWalletSending(ElectrumTestCase):
assert payment_txid
# save payment_tx as LOCAL and UNSIGNED
wallet.adb.add_transaction(payment_tx)
self.assertEqual(TX_HEIGHT_LOCAL, wallet.adb.get_tx_height(payment_txid).height)
self.assertEqual(TX_HEIGHT_LOCAL, wallet.adb.get_tx_height(payment_txid).height())
# mark payment_tx as future
wallet.adb.set_future_tx(payment_txid, wanted_height=300)
self.assertEqual(TX_HEIGHT_FUTURE, wallet.adb.get_tx_height(payment_txid).height)
self.assertEqual(TX_HEIGHT_FUTURE, wallet.adb.get_tx_height(payment_txid).height())
@mock.patch.object(wallet.Abstract_Wallet, 'save_db')
async def test_imported_wallet_usechange_off(self, mock_save_db):
@@ -4458,7 +4458,7 @@ class TestWalletHistory_HelperFns(ElectrumTestCase):
wallet1.adb.add_transaction(tx)
# let's see if the calculated feerate correct:
self.assertEqual((3, 'Local [26.3 sat/vB]'),
wallet1.get_tx_status(tx.txid(), TxMinedInfo(height=TX_HEIGHT_LOCAL, conf=0)))
wallet1.get_tx_status(tx.txid(), TxMinedInfo(_height=TX_HEIGHT_LOCAL, conf=0)))
@mock.patch.object(wallet.Abstract_Wallet, 'save_db')
async def test_get_tx_status_feerate_for_local_2of3_multisig_signed_tx(self, mock_save_db):
@@ -4481,7 +4481,7 @@ class TestWalletHistory_HelperFns(ElectrumTestCase):
wallet1.adb.add_transaction(tx)
# let's see if the calculated feerate correct:
self.assertEqual((3, 'Local [26.3 sat/vB]'),
wallet1.get_tx_status(tx.txid(), TxMinedInfo(height=TX_HEIGHT_LOCAL, conf=0)))
wallet1.get_tx_status(tx.txid(), TxMinedInfo(_height=TX_HEIGHT_LOCAL, conf=0)))
class TestImportedWallet(ElectrumTestCase):