diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py index c7111d1a1da0..8c0304b10fc5 100644 --- a/electrum/lnwatcher.py +++ b/electrum/lnwatcher.py @@ -248,6 +248,10 @@ def inspect_tx_candidate(self, outpoint, n: int) -> Dict[str, str]: """ prev_txid, index = outpoint.split(':') spender_txid = self.adb.db.get_spent_outpoint(prev_txid, int(index)) + # discard local spenders + tx_mined_status = self.adb.get_tx_height(spender_txid) + if tx_mined_status.height in [TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE]: + spender_txid = None result = {outpoint:spender_txid} if n == 0: if spender_txid is None: @@ -263,7 +267,7 @@ def inspect_tx_candidate(self, outpoint, n: int) -> Dict[str, str]: # if tx input is not a first-stage HTLC, we can stop recursion if len(spender_tx.inputs()) != 1: return result - o = spender_tx.inputs()[0] + o = spender_tx.inputs()[0] # fixme? witness = o.witness_elements() if not witness: # This can happen if spender_tx is a local unsigned tx in the wallet history, e.g.: diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index e4a09d37028d..6bda80af0994 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -32,7 +32,7 @@ from .lnaddr import lndecode from .json_db import StoredObject, stored_in from . import constants -from .address_synchronizer import TX_HEIGHT_LOCAL +from .address_synchronizer import TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE from .i18n import _ from .bitcoin import construct_script @@ -346,6 +346,10 @@ async def _claim_swap(self, swap: SwapData) -> None: self._add_or_reindex_swap(swap) # to update _swaps_by_funding_outpoint funding_height = self.lnwatcher.adb.get_tx_height(txin.prevout.txid.hex()) spent_height = txin.spent_height + # discard local spenders + if spent_height in [TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE]: + spent_height = None + if spent_height is not None: swap.spending_txid = txin.spent_txid if spent_height > 0: @@ -353,22 +357,6 @@ async def _claim_swap(self, swap: SwapData) -> None: self.logger.info(f'stop watching swap {swap.lockup_address}') self.lnwatcher.remove_callback(swap.lockup_address) swap.is_redeemed = True - elif spent_height == TX_HEIGHT_LOCAL: - if funding_height.conf > 0 or (swap.is_reverse and self.wallet.config.LIGHTNING_ALLOW_INSTANT_SWAPS): - tx = self.lnwatcher.adb.get_transaction(txin.spent_txid) - try: - await self.network.broadcast_transaction(tx) - except TxBroadcastError: - self.logger.info(f'error broadcasting claim tx {txin.spent_txid}') - elif funding_height.height == TX_HEIGHT_LOCAL: - # the funding tx was double spent. - # this will remove both funding and child (spending tx) from adb - self.lnwatcher.adb.remove_transaction(swap.funding_txid) - swap.funding_txid = None - swap.spending_txid = None - else: - # spending tx is in mempool - pass if not swap.is_reverse: if swap.preimage is None and spent_height is not None: diff --git a/electrum/wallet.py b/electrum/wallet.py index 733da330fb6f..971733abac91 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -1151,9 +1151,8 @@ def get_onchain_history(self, *, domain=None): 'date': timestamp_to_datetime(hist_item.tx_mined_status.timestamp), 'label': self.get_label_for_txid(hist_item.txid), 'txpos_in_block': hist_item.tx_mined_status.txpos, + 'wanted_height': hist_item.tx_mined_status.wanted_height, } - if wanted_height := hist_item.tx_mined_status.wanted_height: - d['wanted_height'] = wanted_height yield d def create_invoice(self, *, outputs: List[PartialTxOutput], message, pr, URI) -> Invoice: @@ -1412,6 +1411,7 @@ def sort_key(x): parent['date'] = timestamp_to_datetime(tx_item['timestamp']) parent['height'] = tx_item['height'] parent['confirmations'] = tx_item['confirmations'] + parent['wanted_height'] = tx_item.get('wanted_height') parent['children'].append(tx_item) now = time.time() @@ -3320,8 +3320,10 @@ def add_sweep_info(self, sweep_info: 'SweepInfo'): # early return if it is spent, because self.processing is not persisted prevout = txin.prevout.to_str() prev_txid, index = prevout.split(':') - if self.adb.db.get_spent_outpoint(prev_txid, int(index)): - return + if spender_txid := self.adb.db.get_spent_outpoint(prev_txid, int(index)): + tx_mined_status = self.adb.get_tx_height(spender_txid) + if tx_mined_status.height not in [TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE]: + return self.processing.add(txin.prevout) self.logger.info(f'add_sweep_info: {sweep_info.name} {sweep_info.txin.prevout.to_str()}') self.batch_inputs[txin.prevout] = sweep_info @@ -3338,10 +3340,21 @@ def to_pay_after(self, tx): return [x for x in self.batch_payments if x not in tx.outputs()] def to_sweep_after(self, tx): - if not tx: - return self.batch_inputs - tx_prevouts = set(txin.prevout for txin in tx.inputs()) - return dict((k,v) for k,v in self.batch_inputs.items() if k not in tx_prevouts) + tx_prevouts = set(txin.prevout for txin in tx.inputs()) if tx else set() + result = [] + for k,v in self.batch_inputs.items(): + prevout = v.txin.prevout + prev_txid, index = prevout.to_str().split(':') + if not self.adb.db.get_transaction(prev_txid): + continue + if spender_txid := self.adb.db.get_spent_outpoint(prev_txid, int(index)): + tx_mined_status = self.adb.get_tx_height(spender_txid) + if tx_mined_status.height not in [TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE]: + continue + if prevout in tx_prevouts: + continue + result.append((k,v)) + return dict(result) def should_bump_fee(self, base_tx): # fixme: since batch_txs is not persisted, we do not bump after a restart @@ -3401,7 +3414,13 @@ async def manage_batch_payments(self): base_tx = self.batch_txs[-1] if self.batch_txs else None to_pay = self.to_pay_after(base_tx) to_sweep = self.to_sweep_after(base_tx) - to_sweep_now = dict([(k,v) for k,v in to_sweep.items() if self.can_broadcast(v)[0] is True]) + to_sweep_now = {} + for k, v in to_sweep.items(): + can_broadcast, wanted_height = self.can_broadcast(v) + if can_broadcast: + to_sweep_now[k] = v + else: + self.add_future_tx(v, wanted_height) if not to_pay and not to_sweep_now and not self.should_bump_fee(base_tx): continue try: @@ -3505,6 +3524,38 @@ async def maybe_broadcast_legacy_htlc_txs(self): self.adb.add_transaction(tx) self.batch_inputs.pop(sweep_info.txin.prevout) + def add_future_tx(self, sweep_info, wanted_height): + """ add local tx to provide user feedback """ + txin = copy.deepcopy(sweep_info.txin) + prevout = txin.prevout.to_str() + prev_txid, index = prevout.split(':') + if self.adb.db.get_spent_outpoint(prev_txid, int(index)): + return + name = sweep_info.name + prevout = txin.prevout.to_str() + new_tx = self.create_transaction( + inputs=[txin], + outputs=[], + password=None, + fee=0, + ) + # we may have a tx with a different fee, in which case it will be replaced + try: + tx_was_added = self.adb.add_transaction(new_tx)#, is_new=(old_tx is None)) + except Exception as e: + self.logger.info(f'could not add future tx: {name}. prevout: {prevout} {str(e)}') + tx_was_added = False + if tx_was_added: + self.logger.info(f'added future tx: {name}. prevout: {prevout}') + + # set future tx regardless of tx_was_added, because it is not persisted + # (and wanted_height can change if input of CSV was not mined before) + self.adb.set_future_tx(new_tx.txid(), wanted_height=wanted_height) + if tx_was_added: + self.set_label(new_tx.txid(), name) + #if old_tx and old_tx.txid() != new_tx.txid(): + # self.lnworker.wallet.set_label(old_tx.txid(), None) + util.trigger_callback('wallet_updated', self.lnworker.wallet) class Simple_Wallet(Abstract_Wallet): # wallet with a single keystore