1
0

Merge pull request #10216 from SomberNight/202509_adb_spv

adb: change API of util.TxMinedInfo: height() is now always SPV-ed
This commit is contained in:
ghost43
2025-09-24 13:50:14 +00:00
committed by GitHub
22 changed files with 110 additions and 91 deletions

View File

@@ -23,6 +23,7 @@
import asyncio import asyncio
import copy import copy
import dataclasses
import threading import threading
import itertools import itertools
from collections import defaultdict from collections import defaultdict
@@ -139,6 +140,7 @@ class AddressSynchronizer(Logger, EventListener):
def get_address_history(self, addr: str) -> Dict[str, int]: def get_address_history(self, addr: str) -> Dict[str, int]:
"""Returns the history for the address, as a txid->height dict. """Returns the history for the address, as a txid->height dict.
In addition to what we have from the server, this includes local and future txns. In addition to what we have from the server, this includes local and future txns.
Note: heights are SPV-verified.
Also see related method db.get_addr_history, which stores the response from the server, Also see related method db.get_addr_history, which stores the response from the server,
so that only includes txns the server sees. so that only includes txns the server sees.
@@ -146,7 +148,7 @@ class AddressSynchronizer(Logger, EventListener):
h = {} h = {}
related_txns = self._history_local.get(addr, set()) related_txns = self._history_local.get(addr, set())
for tx_hash in related_txns: 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 h[tx_hash] = tx_height
return h return h
@@ -272,7 +274,7 @@ class AddressSynchronizer(Logger, EventListener):
tx.deserialize() tx.deserialize()
for txin in tx._inputs: for txin in tx._inputs:
tx_mined_info = self.get_tx_height(txin.prevout.txid.hex()) 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 txin.block_txpos = tx_mined_info.txpos
return tx return tx
@@ -297,7 +299,7 @@ class AddressSynchronizer(Logger, EventListener):
# of add_transaction tx, we might learn of more-and-more inputs of # of add_transaction tx, we might learn of more-and-more inputs of
# being is_mine, as we roll the gap_limit forward # being is_mine, as we roll the gap_limit forward
is_coinbase = tx.inputs()[0].is_coinbase_input() 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: if not allow_unrelated:
# note that during sync, if the transactions are not properly sorted, # 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. # it could happen that we think tx is unrelated but actually one of the inputs is is_mine.
@@ -316,10 +318,10 @@ class AddressSynchronizer(Logger, EventListener):
conflicting_txns = self.get_conflicting_transactions(tx) conflicting_txns = self.get_conflicting_transactions(tx)
if conflicting_txns: if conflicting_txns:
existing_mempool_txn = any( 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) for tx_hash2 in conflicting_txns)
existing_confirmed_txn = any( 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) for tx_hash2 in conflicting_txns)
if existing_confirmed_txn and tx_height <= 0: if existing_confirmed_txn and tx_height <= 0:
# this is a non-confirmed tx that conflicts with confirmed txns; drop. # this is a non-confirmed tx that conflicts with confirmed txns; drop.
@@ -502,7 +504,7 @@ class AddressSynchronizer(Logger, EventListener):
@with_lock @with_lock
def remove_local_transactions_we_dont_have(self): def remove_local_transactions_we_dont_have(self):
for txid in itertools.chain(self.db.list_txi(), self.db.list_txo()): 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): if tx_height == TX_HEIGHT_LOCAL and not self.db.get_transaction(txid):
self.remove_transaction(txid) self.remove_transaction(txid)
@@ -516,7 +518,7 @@ class AddressSynchronizer(Logger, EventListener):
def _get_tx_sort_key(self, tx_hash: str) -> Tuple[int, int]: def _get_tx_sort_key(self, tx_hash: str) -> Tuple[int, int]:
"""Returns a key to be used for sorting txs.""" """Returns a key to be used for sorting txs."""
tx_mined_info = self.get_tx_height(tx_hash) 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 txpos = tx_mined_info.txpos or -1
return height, txpos return height, txpos
@@ -664,7 +666,7 @@ class AddressSynchronizer(Logger, EventListener):
with self.lock: with self.lock:
for tx_hash in self.db.list_verified_tx(): for tx_hash in self.db.list_verified_tx():
info = self.db.get_verified_tx(tx_hash) info = self.db.get_verified_tx(tx_hash)
tx_height = info.height tx_height = info._height
if tx_height > above_height: if tx_height > above_height:
header = blockchain.read_header(tx_height) header = blockchain.read_header(tx_height)
if not header or hash_header(header) != info.header_hash: if not header or hash_header(header) != info.header_hash:
@@ -711,25 +713,25 @@ class AddressSynchronizer(Logger, EventListener):
force_local_if_missing_tx: bool = True, force_local_if_missing_tx: bool = True,
) -> TxMinedInfo: ) -> TxMinedInfo:
if tx_hash is None: # ugly backwards compat... 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: with self.lock:
if verified_tx_mined_info := self.db.get_verified_tx(tx_hash): # mined and spv-ed 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) conf = max(self.get_local_height() - verified_tx_mined_info._height + 1, 0)
tx_mined_info = verified_tx_mined_info._replace(conf=conf) tx_mined_info = dataclasses.replace(verified_tx_mined_info, conf=conf)
elif tx_hash in self.unverified_tx: # mined, no spv elif tx_hash in self.unverified_tx: # mined, no spv
height = self.unverified_tx[tx_hash] 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 elif tx_hash in self.unconfirmed_tx: # mempool
height = self.unconfirmed_tx[tx_hash] 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 elif wanted_height := self.future_tx.get(tx_hash): # future
if wanted_height > self.get_local_height(): 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: else:
tx_mined_info = TxMinedInfo(height=TX_HEIGHT_LOCAL, conf=0) tx_mined_info = TxMinedInfo(_height=TX_HEIGHT_LOCAL, conf=0)
else: # local else: # local
tx_mined_info = TxMinedInfo(height=TX_HEIGHT_LOCAL, conf=0) tx_mined_info = TxMinedInfo(_height=TX_HEIGHT_LOCAL, conf=0)
if tx_mined_info.height in (TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE): if tx_mined_info.height() in (TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE):
return tx_mined_info return tx_mined_info
if force_local_if_missing_tx: if force_local_if_missing_tx:
# It can happen for a txid in any state (unconf/unverified/verified) that we # It can happen for a txid in any state (unconf/unverified/verified) that we
@@ -739,7 +741,7 @@ class AddressSynchronizer(Logger, EventListener):
# a different tx if only the witness differs. We should compare wtxids. # a different tx if only the witness differs. We should compare wtxids.
tx = self.db.get_transaction(tx_hash) tx = self.db.get_transaction(tx_hash)
if tx is None or isinstance(tx, PartialTransaction): 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 return tx_mined_info
def up_to_date_changed(self) -> None: def up_to_date_changed(self) -> None:
@@ -840,8 +842,8 @@ class AddressSynchronizer(Logger, EventListener):
@with_lock @with_lock
def get_addr_io(self, address: str): def get_addr_io(self, address: str):
h = self.get_address_history(address).items() h = self.get_address_history(address).items()
received = {} received = {} # type: Dict[str, tuple[int, int, int, bool]]
sent = {} sent = {} # type: Dict[str, tuple[str, int, int]]
for tx_hash, height in h: for tx_hash, height in h:
tx_mined_info = self.get_tx_height(tx_hash) tx_mined_info = self.get_tx_height(tx_hash)
txpos = tx_mined_info.txpos if tx_mined_info.txpos is not None else -1 txpos = tx_mined_info.txpos if tx_mined_info.txpos is not None else -1
@@ -1047,7 +1049,7 @@ class AddressSynchronizer(Logger, EventListener):
spender_txid = self.db.get_spent_outpoint(prev_txid, int(index)) spender_txid = self.db.get_spent_outpoint(prev_txid, int(index))
# discard local spenders # discard local spenders
tx_mined_status = self.get_tx_height(spender_txid) 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 spender_txid = None
if not spender_txid: if not spender_txid:
return None return None
@@ -1063,7 +1065,7 @@ class AddressSynchronizer(Logger, EventListener):
if not txid: if not txid:
return TxMinedDepth.FREE return TxMinedDepth.FREE
tx_mined_depth = self.get_tx_height(txid) 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 ? if conf > 20: # FIXME unify with lnutil.REDEEM_AFTER_DOUBLE_SPENT_DELAY ?
return TxMinedDepth.DEEP return TxMinedDepth.DEEP
elif conf > 0: elif conf > 0:

View File

@@ -1634,7 +1634,7 @@ class Commands(Logger):
arg:txid:txid:Transaction ID 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: if height != TX_HEIGHT_LOCAL:
raise UserFacingException( raise UserFacingException(
f'Only local transactions can be removed. ' f'Only local transactions can be removed. '

View File

@@ -196,7 +196,7 @@ class QETransactionListModel(QAbstractListModel, QtEventListener):
def _tx_mined_info_from_tx_item(tx_item: Dict[str, Any]) -> TxMinedInfo: 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 # FIXME a bit hackish to have to reconstruct the TxMinedInfo... same thing in qt-gui
tx_mined_info = TxMinedInfo( tx_mined_info = TxMinedInfo(
height=tx_item['height'], _height=tx_item['height'],
conf=tx_item['confirmations'], conf=tx_item['confirmations'],
timestamp=tx_item['timestamp'], timestamp=tx_item['timestamp'],
wanted_height=tx_item.get('wanted_height', None), wanted_height=tx_item.get('wanted_height', None),
@@ -229,10 +229,10 @@ class QETransactionListModel(QAbstractListModel, QtEventListener):
self._dirty = False 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): for i, tx in enumerate(self.tx_history):
if 'txid' in tx and tx['txid'] == txid: if 'txid' in tx and tx['txid'] == txid:
tx['height'] = info.height tx['height'] = info.height()
tx['confirmations'] = info.conf tx['confirmations'] = info.conf
tx['timestamp'] = info.timestamp tx['timestamp'] = info.timestamp
tx['section'] = self.get_section_by_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) status, status_str = self.wallet.get_tx_status(txid, txinfo.tx_mined_status)
tx_item['date'] = status_str tx_item['date'] = status_str
# note: if the height changes, that might affect the history order, but we won't re-sort now. # 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) index = self.index(tx_item_idx, 0)
roles = [self._ROLE_RMAP[x] for x in ['height', 'date']] roles = [self._ROLE_RMAP[x] for x in ['height', 'date']]
self.dataChanged.emit(index, index, roles) self.dataChanged.emit(index, index, roles)

View File

@@ -352,14 +352,14 @@ class QETxDetails(QObject, QtEventListener):
self._lock_delay = 0 self._lock_delay = 0
self._in_mempool = False 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: if self._is_mined:
self.update_mined_status(txinfo.tx_mined_status) self.update_mined_status(txinfo.tx_mined_status)
else: 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._mempool_depth = FeePolicy.depth_tooltip(txinfo.mempool_depth_bytes)
self._in_mempool = True 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() self._lock_delay = txinfo.tx_mined_status.wanted_height - self._wallet.wallet.adb.get_local_height()
if isinstance(self._tx, PartialTransaction): if isinstance(self._tx, PartialTransaction):
self._sighash_danger = self._wallet.wallet.check_sighash(self._tx) self._sighash_danger = self._wallet.wallet.check_sighash(self._tx)

View File

@@ -439,7 +439,7 @@ class HistoryModel(CustomModel, Logger):
def _tx_mined_info_from_tx_item(tx_item: Dict[str, Any]) -> TxMinedInfo: 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 # FIXME a bit hackish to have to reconstruct the TxMinedInfo... same thing in qml-gui
tx_mined_info = TxMinedInfo( tx_mined_info = TxMinedInfo(
height=tx_item['height'], _height=tx_item['height'],
conf=tx_item['confirmations'], conf=tx_item['confirmations'],
timestamp=tx_item['timestamp'], timestamp=tx_item['timestamp'],
wanted_height=tx_item.get('wanted_height', None), wanted_height=tx_item.get('wanted_height', None),
@@ -764,7 +764,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop):
return return
tx_URL = block_explorer_URL(self.config, 'tx', tx_hash) tx_URL = block_explorer_URL(self.config, 'tx', tx_hash)
tx_details = self.wallet.get_tx_info(tx) 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 = QMenu()
menu.addAction(_("Details"), lambda: self.main_window.show_transaction(tx)) menu.addAction(_("Details"), lambda: self.main_window.show_transaction(tx))
if tx_details.can_remove: if tx_details.can_remove:

View File

@@ -282,7 +282,7 @@ class TxInOutWidget(QWidget):
tx_hash = self.tx.txid() tx_hash = self.tx.txid()
if tx_hash: if tx_hash:
tx_mined_info = self.wallet.adb.get_tx_height(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 tx_pos = tx_mined_info.txpos
cursor = o_text.textCursor() cursor = o_text.textCursor()
for txout_idx, o in enumerate(self.tx.outputs()): 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'))) self.rbf_label.setText(_('Replace by fee: {}').format(_('True') if self.tx.is_rbf_enabled() else _('False')))
if tx_mined_status.header_hash: 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: else:
self.block_height_label.hide() self.block_height_label.hide()
if amount is None and ln_amount is None: if amount is None and ln_amount is None:

View File

@@ -108,7 +108,7 @@ class UTXODialog(WindowModalDialog):
if _txid not in parents: if _txid not in parents:
return return
tx_mined_info = self.wallet.adb.get_tx_height(_txid) 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 tx_pos = tx_mined_info.txpos
key = "%dx%d"%(tx_height, tx_pos) if tx_pos is not None else _txid[0:8] 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 "" label = self.wallet.get_label_for_txid(_txid) or ""

View File

@@ -336,9 +336,9 @@ class AbstractChannel(Logger, ABC):
closing_txid: str, closing_height: TxMinedInfo, keep_watching: bool) -> None: closing_txid: str, closing_height: TxMinedInfo, keep_watching: bool) -> None:
# note: state transitions are irreversible, but # note: state transitions are irreversible, but
# save_funding_height, save_closing_height are reversible # 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() self.update_unfunded_state()
elif closing_height.height == TX_HEIGHT_LOCAL: elif closing_height.height() == TX_HEIGHT_LOCAL:
self.update_funded_state( self.update_funded_state(
funding_txid=funding_txid, funding_txid=funding_txid,
funding_height=funding_height) funding_height=funding_height)
@@ -401,11 +401,11 @@ class AbstractChannel(Logger, ABC):
self.lnworker.remove_channel(self.channel_id) self.lnworker.remove_channel(self.channel_id)
def update_funded_state(self, *, funding_txid: str, funding_height: TxMinedInfo) -> None: 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() self.delete_closing_height()
if funding_height.conf>0: if funding_height.conf>0:
self.set_short_channel_id(ShortChannelID.from_components( 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.get_state() == ChannelState.OPENING:
if self.is_funding_tx_mined(funding_height): if self.is_funding_tx_mined(funding_height):
self.set_state(ChannelState.FUNDED) self.set_state(ChannelState.FUNDED)
@@ -423,11 +423,11 @@ class AbstractChannel(Logger, ABC):
def update_closed_state(self, *, funding_txid: str, funding_height: TxMinedInfo, def update_closed_state(self, *, funding_txid: str, funding_height: TxMinedInfo,
closing_txid: str, closing_height: TxMinedInfo, keep_watching: bool) -> None: 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_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_closing_height(txid=closing_txid, height=closing_height.height(), timestamp=closing_height.timestamp)
if funding_height.conf>0: if funding_height.conf>0:
self.set_short_channel_id(ShortChannelID.from_components( 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: if self.get_state() < ChannelState.CLOSED:
conf = closing_height.conf conf = closing_height.conf
if conf > 0: if conf > 0:

View File

@@ -279,5 +279,5 @@ class LNWatcher(Logger, EventListener):
# We should not keep warning the user forever. # We should not keep warning the user forever.
return return
tx_mined_status = self.adb.get_tx_height(spender_txid) 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) self._pending_force_closes.add(chan)

View File

@@ -1264,7 +1264,7 @@ class LNWallet(LNWorker):
elif chan.get_state() == ChannelState.FORCE_CLOSING: elif chan.get_state() == ChannelState.FORCE_CLOSING:
force_close_tx = chan.force_close_tx() force_close_tx = chan.force_close_tx()
txid = force_close_tx.txid() 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: if height == TX_HEIGHT_LOCAL:
self.logger.info('REBROADCASTING CLOSING TX') self.logger.info('REBROADCASTING CLOSING TX')
await self.network.try_broadcasting(force_close_tx, 'force-close') await self.network.try_broadcasting(force_close_tx, 'force-close')

View File

@@ -218,7 +218,7 @@ class WatchTower(Logger, EventListener):
return keep_watching return keep_watching
async def broadcast_or_log(self, funding_outpoint: str, tx: Transaction): 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: if height != TX_HEIGHT_LOCAL:
return return
try: try:

View File

@@ -1440,7 +1440,7 @@ class SwapManager(Logger):
delta = current_height - swap.locktime delta = current_height - swap.locktime
if self.wallet.adb.is_mine(swap.lockup_address): if self.wallet.adb.is_mine(swap.lockup_address):
tx_height = self.wallet.adb.get_tx_height(swap.funding_txid) 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') 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: 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 label += f' (refundable in {-delta} blocks)' # fixme: only if unspent
@@ -1481,8 +1481,8 @@ class SwapManager(Logger):
# and adb will return TX_HEIGHT_LOCAL # and adb will return TX_HEIGHT_LOCAL
continue continue
# note: adb.get_tx_height returns TX_HEIGHT_LOCAL if the txid is unknown # 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 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 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: if funding_height > TX_HEIGHT_LOCAL and spending_height <= TX_HEIGHT_LOCAL:
pending_swaps.append(swap) pending_swaps.append(swap)
return pending_swaps return pending_swaps

View File

@@ -336,9 +336,9 @@ class TxInput:
self.witness = witness self.witness = witness
self._is_coinbase_output = is_coinbase_output self._is_coinbase_output = is_coinbase_output
# blockchain fields # blockchain fields
self.block_height = None # type: Optional[int] # height at which the TXO is mined; None means unknown. not SPV-ed. self.block_height = None # type: Optional[int] # height at which the TXO is mined; None means unknown. SPV-ed.
self.block_txpos = None # type: Optional[int] # position of tx in block, if TXO is mined; otherwise None or -1 self.block_txpos = None # type: Optional[int] # position of tx in block, if TXO is mined; otherwise None or -1
self.spent_height = None # type: Optional[int] # height at which the TXO got spent self.spent_height = None # type: Optional[int] # height at which the TXO got spent. SPV-ed.
self.spent_txid = None # type: Optional[str] # txid of the spender self.spent_txid = None # type: Optional[str] # txid of the spender
self._utxo = None # type: Optional[Transaction] self._utxo = None # type: Optional[Transaction]
self.__scriptpubkey = None # type: Optional[bytes] self.__scriptpubkey = None # type: Optional[bytes]

View File

@@ -174,9 +174,9 @@ class TxBatcher(Logger):
prev_txid, index = outpoint.split(':') prev_txid, index = outpoint.split(':')
if spender_txid := self.wallet.adb.db.get_spent_outpoint(prev_txid, int(index)): 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) tx_mined_status = self.wallet.adb.get_tx_height(spender_txid)
if tx_mined_status.height > 0: if tx_mined_status.height() > 0:
return 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 return
self.logger.info(f'will broadcast standalone tx {sweep_info.name}') 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) 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(':') prev_txid, index = prevout.split(':')
if spender_txid := self.wallet.adb.db.get_spent_outpoint(prev_txid, int(index)): 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) tx_mined_status = self.wallet.adb.get_tx_height(spender_txid)
if tx_mined_status.height > 0: if tx_mined_status.height() > 0:
return return
self._unconfirmed_sweeps.add(txin.prevout) self._unconfirmed_sweeps.add(txin.prevout)
self.logger.info(f'add_sweep_info: {sweep_info.name} {sweep_info.txin.prevout.to_str()}') self.logger.info(f'add_sweep_info: {sweep_info.name} {sweep_info.txin.prevout.to_str()}')
@@ -331,7 +331,7 @@ class TxBatch(Logger):
continue continue
if spender_txid := self.wallet.adb.db.get_spent_outpoint(prev_txid, int(index)): 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) 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 continue
if prevout in tx_prevouts: if prevout in tx_prevouts:
continue continue
@@ -378,7 +378,7 @@ class TxBatch(Logger):
self.logger.info(f'base tx confirmed {txid}') self.logger.info(f'base tx confirmed {txid}')
self._clear_unconfirmed_sweeps(tx) self._clear_unconfirmed_sweeps(tx)
self._start_new_batch(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 # this may happen if our Electrum server is unresponsive
# server could also be lying to us. Rebroadcasting might # server could also be lying to us. Rebroadcasting might
# help, if we have switched to another server. # help, if we have switched to another server.
@@ -590,7 +590,7 @@ class TxBatch(Logger):
wanted_height_cltv = sweep_info.cltv_abs wanted_height_cltv = sweep_info.cltv_abs
if wanted_height_cltv - local_height > 0: if wanted_height_cltv - local_height > 0:
can_broadcast = False 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 sweep_info.csv_delay:
if prev_height > 0: if prev_height > 0:
wanted_height_csv = prev_height + sweep_info.csv_delay - 1 wanted_height_csv = prev_height + sweep_info.csv_delay - 1

View File

@@ -1264,26 +1264,37 @@ def with_lock(func):
return func_wrapper return func_wrapper
class TxMinedInfo(NamedTuple): @dataclass(frozen=True, kw_only=True)
height: int # height of block that mined tx 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) 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 timestamp: Optional[int] = None # timestamp of block that mined tx
txpos: Optional[int] = None # position of tx in serialized block txpos: Optional[int] = None # position of tx in serialized block
header_hash: Optional[str] = None # hash of block that mined tx header_hash: Optional[str] = None # hash of block that mined tx
wanted_height: Optional[int] = None # in case of timelock, min abs block height 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]: def short_id(self) -> Optional[str]:
if self.txpos is not None and self.txpos >= 0: if self.txpos is not None and self.txpos >= 0:
assert self.height > 0 assert self.height() > 0
return f"{self.height}x{self.txpos}" return f"{self.height()}x{self.txpos}"
return None return None
def is_local_like(self) -> bool: def is_local_like(self) -> bool:
"""Returns whether the tx is local-like (LOCAL/FUTURE).""" """Returns whether the tx is local-like (LOCAL/FUTURE)."""
from .address_synchronizer import TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT from .address_synchronizer import TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT
if self.height > 0: if self.height() > 0:
return False 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 False
return True return True
@@ -2360,7 +2371,7 @@ class OnchainHistoryItem(NamedTuple):
'txid': self.txid, 'txid': self.txid,
'amount_sat': self.amount_sat, 'amount_sat': self.amount_sat,
'fee_sat': self.fee_sat, 'fee_sat': self.fee_sat,
'height': self.tx_mined_status.height, 'height': self.tx_mined_status.height(),
'confirmations': self.tx_mined_status.conf, 'confirmations': self.tx_mined_status.conf,
'timestamp': self.tx_mined_status.timestamp, 'timestamp': self.tx_mined_status.timestamp,
'monotonic_timestamp': self.monotonic_timestamp, 'monotonic_timestamp': self.monotonic_timestamp,

View File

@@ -131,7 +131,7 @@ class SPV(NetworkJobOnDefaultServer):
self.requested_merkle.discard(tx_hash) self.requested_merkle.discard(tx_hash)
self.logger.info(f"verified {tx_hash}") self.logger.info(f"verified {tx_hash}")
header_hash = hash_header(header) header_hash = hash_header(header)
tx_info = TxMinedInfo(height=tx_height, tx_info = TxMinedInfo(_height=tx_height,
timestamp=header.get('timestamp'), timestamp=header.get('timestamp'),
txpos=pos, txpos=pos,
header_hash=header_hash) header_hash=header_hash)

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())) and (tx_we_already_have_in_db is None or not tx_we_already_have_in_db.is_complete()))
label = '' label = ''
tx_mined_status = self.adb.get_tx_height(tx_hash) 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): # otherwise 'height' is unreliable (typically LOCAL):
and is_relevant and is_relevant
# don't offer during common signing flow, e.g. when watch-only wallet starts creating a tx: # 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.is_complete():
if tx_we_already_have_in_db: if tx_we_already_have_in_db:
label = self.get_label_for_txid(tx_hash) 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: if tx_mined_status.conf:
status = _("{} confirmations").format(tx_mined_status.conf) status = _("{} confirmations").format(tx_mined_status.conf)
else: else:
status = _('Not verified') 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') status = _('Unconfirmed')
if fee is None: if fee is None:
fee = self.adb.get_tx_fee(tx_hash) fee = self.adb.get_tx_fee(tx_hash)
@@ -981,7 +981,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
can_cpfp = False can_cpfp = False
else: else:
status = _('Local') 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 = tx_mined_status.wanted_height - self.adb.get_local_height()
num_blocks_remainining = max(0, num_blocks_remainining) num_blocks_remainining = max(0, num_blocks_remainining)
status = _('Local (future: {})').format(_('in {} blocks').format(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) # populate cache in chronological order (confirmed tx only)
# todo: get_full_history should return unconfirmed tx topologically sorted # todo: get_full_history should return unconfirmed tx topologically sorted
for _txid, tx_item in self._last_full_history.items(): 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) self.get_tx_parents(_txid)
result = self._tx_parents_cache.get(txid, None) result = self._tx_parents_cache.get(txid, None)
@@ -1224,7 +1224,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
monotonic_timestamp = 0 monotonic_timestamp = 0
for hist_item in self.adb.get_history(domain=domain): for hist_item in self.adb.get_history(domain=domain):
timestamp = (hist_item.tx_mined_status.timestamp or TX_TIMESTAMP_INF) 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: if from_timestamp and (timestamp or now) < from_timestamp:
continue continue
if to_timestamp and (timestamp or now) >= to_timestamp: 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: for prevout, v in prevouts_and_values:
relevant_txs.add(prevout.txid.hex()) relevant_txs.add(prevout.txid.hex())
tx_height = self.adb.get_tx_height(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 continue
confs_and_values.append((tx_height.conf or 0, v)) confs_and_values.append((tx_height.conf or 0, v))
# check that there is at least one TXO, and that they pay enough. # 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): def get_tx_status(self, tx_hash: str, tx_mined_info: TxMinedInfo):
extra = [] extra = []
height = tx_mined_info.height height = tx_mined_info.height()
conf = tx_mined_info.conf conf = tx_mined_info.conf
timestamp = tx_mined_info.timestamp timestamp = tx_mined_info.timestamp
if height == TX_HEIGHT_FUTURE: if height == TX_HEIGHT_FUTURE:
@@ -1812,7 +1812,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
# tx should not be mined yet # tx should not be mined yet
if hist_item.tx_mined_status.conf > 0: continue if hist_item.tx_mined_status.conf > 0: continue
# conservative future proofing of code: only allow known unconfirmed types # 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_UNCONFIRMED,
TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONF_PARENT,
TX_HEIGHT_LOCAL): TX_HEIGHT_LOCAL):
@@ -2017,7 +2017,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
if base_tx: if base_tx:
# make sure we don't try to spend change from the tx-to-be-replaced: # 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()] 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): if not isinstance(base_tx, PartialTransaction):
base_tx = PartialTransaction.from_tx(base_tx) base_tx = PartialTransaction.from_tx(base_tx)
base_tx.add_info_from_wallet(self) base_tx.add_info_from_wallet(self)
@@ -2151,7 +2151,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
return bool(frozen) return bool(frozen)
# State not set. We implicitly mark certain coins as frozen: # State not set. We implicitly mark certain coins as frozen:
tx_mined_status = self.adb.get_tx_height(utxo.prevout.txid.hex()) 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 return True
if self._is_coin_small_and_unconfirmed(utxo): if self._is_coin_small_and_unconfirmed(utxo):
return True return True
@@ -2677,7 +2677,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
txin.script_descriptor = self.get_script_descriptor_for_address(address) txin.script_descriptor = self.get_script_descriptor_for_address(address)
txin.is_mine = True txin.is_mine = True
self._add_txinout_derivation_info(txin, address, only_der_suffix=only_der_suffix) 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: def has_support_for_slip_19_ownership_proofs(self) -> bool:
return False return False

View File

@@ -1508,7 +1508,7 @@ class WalletDB(JsonDB):
if txid not in self.verified_tx: if txid not in self.verified_tx:
return None return None
height, timestamp, txpos, header_hash = self.verified_tx[txid] height, timestamp, txpos, header_hash = self.verified_tx[txid]
return TxMinedInfo(height=height, return TxMinedInfo(_height=height,
conf=None, conf=None,
timestamp=timestamp, timestamp=timestamp,
txpos=txpos, txpos=txpos,
@@ -1518,7 +1518,9 @@ class WalletDB(JsonDB):
def add_verified_tx(self, txid: str, info: TxMinedInfo): def add_verified_tx(self, txid: str, info: TxMinedInfo):
assert isinstance(txid, str) assert isinstance(txid, str)
assert isinstance(info, TxMinedInfo) 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 @modifier
def remove_verified_tx(self, txid: str): def remove_verified_tx(self, txid: str):

View File

@@ -80,7 +80,7 @@ class TestWalletPaymentRequests(ElectrumTestCase):
self.assertEqual(PR_UNCONFIRMED, wallet1.get_invoice_status(pr)) self.assertEqual(PR_UNCONFIRMED, wallet1.get_invoice_status(pr))
# tx gets mined # tx gets mined
wallet1.db.put('stored_height', 1010) wallet1.db.put('stored_height', 1010)
tx_info = TxMinedInfo(height=1001, tx_info = TxMinedInfo(_height=1001,
timestamp=pr.get_time() + 100, timestamp=pr.get_time() + 100,
txpos=1, txpos=1,
header_hash="01"*32) header_hash="01"*32)
@@ -111,7 +111,7 @@ class TestWalletPaymentRequests(ElectrumTestCase):
self.assertEqual(PR_UNCONFIRMED, wallet1.get_invoice_status(pr)) self.assertEqual(PR_UNCONFIRMED, wallet1.get_invoice_status(pr))
# tx gets mined # tx gets mined
wallet1.db.put('stored_height', 1010) wallet1.db.put('stored_height', 1010)
tx_info = TxMinedInfo(height=1001, tx_info = TxMinedInfo(_height=1001,
timestamp=pr.get_time() + 100, timestamp=pr.get_time() + 100,
txpos=1, txpos=1,
header_hash="01"*32) header_hash="01"*32)
@@ -141,7 +141,7 @@ class TestWalletPaymentRequests(ElectrumTestCase):
wallet1.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED) wallet1.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)
self.assertEqual(PR_UNCONFIRMED, wallet1.get_invoice_status(pr)) self.assertEqual(PR_UNCONFIRMED, wallet1.get_invoice_status(pr))
# tx mined in the past (before invoice creation) # tx mined in the past (before invoice creation)
tx_info = TxMinedInfo(height=990, tx_info = TxMinedInfo(_height=990,
timestamp=pr.get_time() + 100, timestamp=pr.get_time() + 100,
txpos=1, txpos=1,
header_hash="01" * 32) header_hash="01" * 32)

View File

@@ -2,6 +2,8 @@ import unittest
import logging import logging
from unittest import mock from unittest import mock
import asyncio import asyncio
import dataclasses
from aiorpcx import timeout_after from aiorpcx import timeout_after
from electrum import storage, bitcoin, keystore, wallet from electrum import storage, bitcoin, keystore, wallet
@@ -152,7 +154,7 @@ class TestTxBatcher(ElectrumTestCase):
# tx1 gets confirmed, tx2 gets removed # tx1 gets confirmed, tx2 gets removed
wallet.adb.receive_tx_callback(tx1, tx_height=1) wallet.adb.receive_tx_callback(tx1, tx_height=1)
tx_mined_status = wallet.adb.get_tx_height(tx1.txid()) 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.txid()) is not None
assert wallet.adb.get_transaction(tx1_prime.txid()) is None assert wallet.adb.get_transaction(tx1_prime.txid()) is None
# txbatcher creates tx2 # txbatcher creates tx2
@@ -195,7 +197,7 @@ class TestTxBatcher(ElectrumTestCase):
# tx1 gets confirmed # tx1 gets confirmed
wallet.adb.receive_tx_callback(tx1, tx_height=1) wallet.adb.receive_tx_callback(tx1, tx_height=1)
tx_mined_status = wallet.adb.get_tx_height(tx1.txid()) 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() tx2 = await self.network.next_tx()
assert len(tx2.outputs()) == 2 assert len(tx2.outputs()) == 2
@@ -209,6 +211,8 @@ class TestTxBatcher(ElectrumTestCase):
wallet = self._create_wallet() wallet = self._create_wallet()
wallet.adb.db.transactions[SWAPDATA.funding_txid] = tx = Transaction(SWAP_FUNDING_TX) wallet.adb.db.transactions[SWAPDATA.funding_txid] = tx = Transaction(SWAP_FUNDING_TX)
wallet.adb.receive_tx_callback(tx, tx_height=1) 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) wallet.txbatcher.add_sweep_input('default', SWAP_SWEEP_INFO)
tx = await self.network.next_tx() tx = await self.network.next_tx()
txid = tx.txid() txid = tx.txid()

View File

@@ -164,7 +164,7 @@ class FakeADB:
def get_tx_height(self, txid): def get_tx_height(self, txid):
# because we use a current timestamp, and history is empty, # because we use a current timestamp, and history is empty,
# FxThread.history_rate will use spot prices # 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: class FakeWallet:
def __init__(self, fiat_value): def __init__(self, fiat_value):

View File

@@ -2945,7 +2945,7 @@ class TestWalletSending(ElectrumTestCase):
assert payment_txid assert payment_txid
# save payment_tx as LOCAL and UNSIGNED # save payment_tx as LOCAL and UNSIGNED
wallet.adb.add_transaction(payment_tx) 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(1, len(wallet.get_spendable_coins(nonlocal_only=True)))
self.assertEqual(2, len(wallet.get_spendable_coins(nonlocal_only=False))) self.assertEqual(2, len(wallet.get_spendable_coins(nonlocal_only=False)))
# transition payment_tx to mempool (but it is still unsigned!) # transition payment_tx to mempool (but it is still unsigned!)
@@ -2958,13 +2958,13 @@ class TestWalletSending(ElectrumTestCase):
# but the wallet db does not yet have the corresponding full tx. # but the wallet db does not yet have the corresponding full tx.
# In such cases, we instead want the txid to be considered LOCAL. # In such cases, we instead want the txid to be considered LOCAL.
wallet.adb.receive_tx_callback(payment_tx, tx_height=TX_HEIGHT_UNCONFIRMED) 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(1, len(wallet.get_spendable_coins(nonlocal_only=True)))
self.assertEqual(2, len(wallet.get_spendable_coins(nonlocal_only=False))) 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 gets signed tx (e.g. from network). payment_tx is now considered to be in mempool
wallet.sign_transaction(payment_tx, password=None) wallet.sign_transaction(payment_tx, password=None)
wallet.adb.receive_tx_callback(payment_tx, tx_height=TX_HEIGHT_UNCONFIRMED) 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=True)))
self.assertEqual(2, len(wallet.get_spendable_coins(nonlocal_only=False))) self.assertEqual(2, len(wallet.get_spendable_coins(nonlocal_only=False)))
@@ -2983,10 +2983,10 @@ class TestWalletSending(ElectrumTestCase):
assert payment_txid assert payment_txid
# save payment_tx as LOCAL and UNSIGNED # save payment_tx as LOCAL and UNSIGNED
wallet.adb.add_transaction(payment_tx) 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 # mark payment_tx as future
wallet.adb.set_future_tx(payment_txid, wanted_height=300) 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') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
async def test_imported_wallet_usechange_off(self, mock_save_db): async def test_imported_wallet_usechange_off(self, mock_save_db):
@@ -4479,7 +4479,7 @@ class TestWalletHistory_HelperFns(ElectrumTestCase):
wallet1.adb.add_transaction(tx) wallet1.adb.add_transaction(tx)
# let's see if the calculated feerate correct: # let's see if the calculated feerate correct:
self.assertEqual((3, 'Local [26.3 sat/vB]'), 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') @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): async def test_get_tx_status_feerate_for_local_2of3_multisig_signed_tx(self, mock_save_db):
@@ -4502,7 +4502,7 @@ class TestWalletHistory_HelperFns(ElectrumTestCase):
wallet1.adb.add_transaction(tx) wallet1.adb.add_transaction(tx)
# let's see if the calculated feerate correct: # let's see if the calculated feerate correct:
self.assertEqual((3, 'Local [26.3 sat/vB]'), 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): class TestImportedWallet(ElectrumTestCase):