adb: change API of util.TxMinedInfo: height() is now always SPV-ed
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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. '
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user