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:
@@ -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:
|
||||||
|
|||||||
@@ -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. '
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user