URI: 
       tMerge pull request #5809 from SomberNight/201911_invoice_paid_detection - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 7a080352f8864077875206658bb610113eded7e6
   DIR parent cfbd83c43230ad6b31e98120daff8336c3ac0b7a
  HTML Author: ThomasV <thomasv@electrum.org>
       Date:   Fri, 29 Nov 2019 18:24:08 +0100
       
       Merge pull request #5809 from SomberNight/201911_invoice_paid_detection
       
       wallet: better (outgoing) invoice "paid" detection
       Diffstat:
         M electrum/address_synchronizer.py    |      19 ++++++++++++++-----
         M electrum/gui/kivy/main_window.py    |       8 +-------
         M electrum/gui/kivy/uix/screens.py    |       6 +++---
         M electrum/gui/qt/invoice_list.py     |       3 ++-
         M electrum/gui/qt/main_window.py      |      37 +++++++++++++------------------
         M electrum/gui/qt/transaction_dialog… |      17 ++++++++---------
         M electrum/json_db.py                 |      51 ++++++++++++++++++++++++++++---
         M electrum/wallet.py                  |      77 +++++++++++++++++++++++++------
       
       8 files changed, 152 insertions(+), 66 deletions(-)
       ---
   DIR diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py
       t@@ -26,7 +26,7 @@ import threading
        import asyncio
        import itertools
        from collections import defaultdict
       -from typing import TYPE_CHECKING, Dict, Optional, Set, Tuple, NamedTuple, Sequence
       +from typing import TYPE_CHECKING, Dict, Optional, Set, Tuple, NamedTuple, Sequence, List
        
        from . import bitcoin
        from .bitcoin import COINBASE_MATURITY
       t@@ -207,7 +207,7 @@ class AddressSynchronizer(Logger):
                            conflicting_txns -= {tx_hash}
                    return conflicting_txns
        
       -    def add_transaction(self, tx: Transaction, allow_unrelated=False) -> bool:
       +    def add_transaction(self, tx: Transaction, *, allow_unrelated=False) -> bool:
                """Returns whether the tx was successfully added to the wallet history."""
                assert tx, tx
                assert tx.is_complete()
       t@@ -283,6 +283,8 @@ class AddressSynchronizer(Logger):
                    for n, txo in enumerate(tx.outputs()):
                        v = txo.value
                        ser = tx_hash + ':%d'%n
       +                scripthash = bitcoin.script_to_scripthash(txo.scriptpubkey.hex())
       +                self.db.add_prevout_by_scripthash(scripthash, prevout=TxOutpoint.from_str(ser), value=v)
                        addr = self.get_txout_address(txo)
                        if addr and self.is_mine(addr):
                            self.db.add_txo_addr(tx_hash, addr, n, v, is_coinbase)
       t@@ -299,7 +301,7 @@ class AddressSynchronizer(Logger):
                    self.db.add_num_inputs_to_tx(tx_hash, len(tx.inputs()))
                    return True
        
       -    def remove_transaction(self, tx_hash):
       +    def remove_transaction(self, tx_hash: str) -> None:
                def remove_from_spent_outpoints():
                    # undo spends in spent_outpoints
                    if tx is not None:
       t@@ -317,7 +319,7 @@ class AddressSynchronizer(Logger):
                            if spending_txid == tx_hash:
                                self.db.remove_spent_outpoint(prevout_hash, prevout_n)
        
       -        with self.transaction_lock:
       +        with self.lock, self.transaction_lock:
                    self.logger.info(f"removing tx from history {tx_hash}")
                    tx = self.db.remove_transaction(tx_hash)
                    remove_from_spent_outpoints()
       t@@ -327,6 +329,13 @@ class AddressSynchronizer(Logger):
                    self.db.remove_txi(tx_hash)
                    self.db.remove_txo(tx_hash)
                    self.db.remove_tx_fee(tx_hash)
       +            self.db.remove_verified_tx(tx_hash)
       +            self.unverified_tx.pop(tx_hash, None)
       +            if tx:
       +                for idx, txo in enumerate(tx.outputs()):
       +                    scripthash = bitcoin.script_to_scripthash(txo.scriptpubkey.hex())
       +                    prevout = TxOutpoint(bfh(tx_hash), idx)
       +                    self.db.remove_prevout_by_scripthash(scripthash, prevout=prevout, value=txo.value)
        
            def get_depending_transactions(self, tx_hash):
                """Returns all (grand-)children of tx_hash in this wallet."""
       t@@ -338,7 +347,7 @@ class AddressSynchronizer(Logger):
                        children |= self.get_depending_transactions(other_hash)
                    return children
        
       -    def receive_tx_callback(self, tx_hash, tx, tx_height):
       +    def receive_tx_callback(self, tx_hash: str, tx: Transaction, tx_height: int) -> None:
                self.add_unverified_tx(tx_hash, tx_height)
                self.add_transaction(tx, allow_unrelated=True)
        
   DIR diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py
       t@@ -1028,18 +1028,12 @@ class ElectrumWindow(App):
                    status, msg = True, tx.txid()
                Clock.schedule_once(lambda dt: on_complete(status, msg))
        
       -    def broadcast(self, tx, invoice=None):
       +    def broadcast(self, tx):
                def on_complete(ok, msg):
                    if ok:
                        self.show_info(_('Payment sent.'))
                        if self.send_screen:
                            self.send_screen.do_clear()
       -                if invoice:
       -                    key = invoice['id']
       -                    txid = tx.txid()
       -                    self.wallet.set_label(txid, invoice['message'])
       -                    self.wallet.set_paid(key, txid)
       -                    self.update_tab('invoices')
                    else:
                        msg = msg or ''
                        self.show_error(msg)
   DIR diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py
       t@@ -380,14 +380,14 @@ class SendScreen(CScreen):
                if fee > feerate_warning * tx.estimated_size() / 1000:
                    msg.append(_('Warning') + ': ' + _("The fee for this transaction seems unusually high."))
                msg.append(_("Enter your PIN code to proceed"))
       -        self.app.protected('\n'.join(msg), self.send_tx, (tx, invoice))
       +        self.app.protected('\n'.join(msg), self.send_tx, (tx,))
        
       -    def send_tx(self, tx, invoice, password):
       +    def send_tx(self, tx, password):
                if self.app.wallet.has_password() and password is None:
                    return
                def on_success(tx):
                    if tx.is_complete():
       -                self.app.broadcast(tx, invoice)
       +                self.app.broadcast(tx)
                    else:
                        self.app.tx_dialog(tx)
                def on_failure(error):
   DIR diff --git a/electrum/gui/qt/invoice_list.py b/electrum/gui/qt/invoice_list.py
       t@@ -96,7 +96,8 @@ class InvoiceList(MyTreeView):
                _list = self.parent.wallet.get_invoices()
                # filter out paid invoices unless we have the log
                lnworker_logs = self.parent.wallet.lnworker.logs if self.parent.wallet.lnworker else {}
       -        _list = [x for x in _list if x and x.get('status') != PR_PAID or x.get('rhash') in lnworker_logs]
       +        _list = [x for x in _list
       +                 if x and (x.get('status') != PR_PAID or x.get('rhash') in lnworker_logs)]
                self.model().clear()
                self.update_headers(self.__class__.headers)
                for idx, item in enumerate(_list):
   DIR diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
       t@@ -928,9 +928,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                d = address_dialog.AddressDialog(self, addr)
                d.exec_()
        
       -    def show_transaction(self, tx, *, invoice=None, tx_desc=None):
       +    def show_transaction(self, tx, *, tx_desc=None):
                '''tx_desc is set only for txs created in the Send tab'''
       -        show_transaction(tx, parent=self, invoice=invoice, desc=tx_desc)
       +        show_transaction(tx, parent=self, desc=tx_desc)
        
            def create_receive_tab(self):
                # A 4-column grid layout.  All the stretch is in the last column.
       t@@ -1472,7 +1472,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                    self.pay_lightning_invoice(invoice['invoice'], amount_sat=invoice['amount'])
                elif invoice['type'] == PR_TYPE_ONCHAIN:
                    outputs = invoice['outputs']
       -            self.pay_onchain_dialog(self.get_coins(), outputs, invoice=invoice)
       +            self.pay_onchain_dialog(self.get_coins(), outputs)
                else:
                    raise Exception('unknown invoice type')
        
       t@@ -1492,7 +1492,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
        
            def pay_onchain_dialog(self, inputs: Sequence[PartialTxInput],
                                   outputs: List[PartialTxOutput], *,
       -                           invoice=None, external_keypairs=None) -> None:
       +                           external_keypairs=None) -> None:
                # trustedcoin requires this
                if run_hook('abort_send', self):
                    return
       t@@ -1508,8 +1508,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                    return
                if self.config.get('advanced_preview'):
                    self.preview_tx_dialog(make_tx=make_tx,
       -                                   external_keypairs=external_keypairs,
       -                                   invoice=invoice)
       +                                   external_keypairs=external_keypairs)
                    return
        
                output_value = '!' if '!' in output_values else sum(output_values)
       t@@ -1524,27 +1523,26 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                if is_send:
                    def sign_done(success):
                        if success:
       -                    self.broadcast_or_show(tx, invoice=invoice)
       +                    self.broadcast_or_show(tx)
                    self.sign_tx_with_password(tx, callback=sign_done, password=password,
                                               external_keypairs=external_keypairs)
                else:
                    self.preview_tx_dialog(make_tx=make_tx,
       -                                   external_keypairs=external_keypairs,
       -                                   invoice=invoice)
       +                                   external_keypairs=external_keypairs)
        
       -    def preview_tx_dialog(self, *, make_tx, external_keypairs=None, invoice=None):
       +    def preview_tx_dialog(self, *, make_tx, external_keypairs=None):
                d = PreviewTxDialog(make_tx=make_tx, external_keypairs=external_keypairs,
       -                            window=self, invoice=invoice)
       +                            window=self)
                d.show()
        
       -    def broadcast_or_show(self, tx, *, invoice=None):
       +    def broadcast_or_show(self, tx: Transaction):
                if not self.network:
                    self.show_error(_("You can't broadcast a transaction without a live network connection."))
       -            self.show_transaction(tx, invoice=invoice)
       +            self.show_transaction(tx)
                elif not tx.is_complete():
       -            self.show_transaction(tx, invoice=invoice)
       +            self.show_transaction(tx)
                else:
       -            self.broadcast_transaction(tx, invoice=invoice)
       +            self.broadcast_transaction(tx)
        
            @protected
            def sign_tx(self, tx, *, callback, external_keypairs, password):
       t@@ -1568,7 +1566,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                msg = _('Signing transaction...')
                WaitingDialog(self, msg, task, on_success, on_failure)
        
       -    def broadcast_transaction(self, tx: Transaction, *, invoice=None, tx_desc=None):
       +    def broadcast_transaction(self, tx: Transaction):
        
                def broadcast_thread():
                    # non-GUI thread
       t@@ -1584,11 +1582,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                        return False, repr(e)
                    # success
                    txid = tx.txid()
       -            if tx_desc:
       -                self.wallet.set_label(txid, tx_desc)
       -            if invoice:
       -                self.wallet.set_paid(invoice['id'], txid)
       -                self.wallet.set_label(txid, invoice['message'])
                    if pr:
                        self.payment_request = None
                        refund_address = self.wallet.get_receiving_address()
       t@@ -2709,7 +2702,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                scriptpubkey = bfh(bitcoin.address_to_script(addr))
                outputs = [PartialTxOutput(scriptpubkey=scriptpubkey, value='!')]
                self.warn_if_watching_only()
       -        self.pay_onchain_dialog(coins, outputs, invoice=None, external_keypairs=keypairs)
       +        self.pay_onchain_dialog(coins, outputs, external_keypairs=keypairs)
        
            def _do_import(self, title, header_layout, func):
                text = text_dialog(self, title, header_layout, _('Import'), allow_multi=True)
   DIR diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py
       t@@ -75,9 +75,9 @@ _logger = get_logger(__name__)
        dialogs = []  # Otherwise python randomly garbage collects the dialogs...
        
        
       -def show_transaction(tx: Transaction, *, parent: 'ElectrumWindow', invoice=None, desc=None, prompt_if_unsaved=False):
       +def show_transaction(tx: Transaction, *, parent: 'ElectrumWindow', desc=None, prompt_if_unsaved=False):
            try:
       -        d = TxDialog(tx, parent=parent, invoice=invoice, desc=desc, prompt_if_unsaved=prompt_if_unsaved)
       +        d = TxDialog(tx, parent=parent, desc=desc, prompt_if_unsaved=prompt_if_unsaved)
            except SerializationError as e:
                _logger.exception('unable to deserialize the transaction')
                parent.show_critical(_("Electrum was unable to deserialize the transaction:") + "\n" + str(e))
       t@@ -88,7 +88,7 @@ def show_transaction(tx: Transaction, *, parent: 'ElectrumWindow', invoice=None,
        
        class BaseTxDialog(QDialog, MessageBoxMixin):
        
       -    def __init__(self, *, parent: 'ElectrumWindow', invoice, desc, prompt_if_unsaved, finalized: bool, external_keypairs=None):
       +    def __init__(self, *, parent: 'ElectrumWindow', desc, prompt_if_unsaved, finalized: bool, external_keypairs=None):
                '''Transactions in the wallet will show their description.
                Pass desc to give a description for txs not yet in the wallet.
                '''
       t@@ -103,7 +103,6 @@ class BaseTxDialog(QDialog, MessageBoxMixin):
                self.prompt_if_unsaved = prompt_if_unsaved
                self.saved = False
                self.desc = desc
       -        self.invoice = invoice
                self.setMinimumWidth(950)
                self.set_title()
        
       t@@ -213,7 +212,7 @@ class BaseTxDialog(QDialog, MessageBoxMixin):
            def do_broadcast(self):
                self.main_window.push_top_level_window(self)
                try:
       -            self.main_window.broadcast_transaction(self.tx, invoice=self.invoice, tx_desc=self.desc)
       +            self.main_window.broadcast_transaction(self.tx)
                finally:
                    self.main_window.pop_top_level_window(self)
                self.saved = True
       t@@ -592,8 +591,8 @@ class TxDetailLabel(QLabel):
        
        
        class TxDialog(BaseTxDialog):
       -    def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', invoice, desc, prompt_if_unsaved):
       -        BaseTxDialog.__init__(self, parent=parent, invoice=invoice, desc=desc, prompt_if_unsaved=prompt_if_unsaved, finalized=True)
       +    def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', desc, prompt_if_unsaved):
       +        BaseTxDialog.__init__(self, parent=parent, desc=desc, prompt_if_unsaved=prompt_if_unsaved, finalized=True)
                self.set_tx(tx)
                self.update()
        
       t@@ -601,9 +600,9 @@ class TxDialog(BaseTxDialog):
        
        class PreviewTxDialog(BaseTxDialog, TxEditor):
        
       -    def __init__(self, *, make_tx, external_keypairs, window: 'ElectrumWindow', invoice):
       +    def __init__(self, *, make_tx, external_keypairs, window: 'ElectrumWindow'):
                TxEditor.__init__(self, window=window, make_tx=make_tx, is_sweep=bool(external_keypairs))
       -        BaseTxDialog.__init__(self, parent=window, invoice=invoice, desc='', prompt_if_unsaved=False,
       +        BaseTxDialog.__init__(self, parent=window, desc='', prompt_if_unsaved=False,
                                      finalized=False, external_keypairs=external_keypairs)
                self.update_tx()
                self.update()
   DIR diff --git a/electrum/json_db.py b/electrum/json_db.py
       t@@ -31,16 +31,16 @@ from collections import defaultdict
        from typing import Dict, Optional, List, Tuple, Set, Iterable, NamedTuple, Sequence
        
        from . import util, bitcoin
       -from .util import profiler, WalletFileException, multisig_type, TxMinedInfo
       +from .util import profiler, WalletFileException, multisig_type, TxMinedInfo, bfh
        from .keystore import bip44_derivation
       -from .transaction import Transaction
       +from .transaction import Transaction, TxOutpoint
        from .logging import Logger
        
        # seed_version is now used for the version of the wallet file
        
        OLD_SEED_VERSION = 4        # electrum versions < 2.0
        NEW_SEED_VERSION = 11       # electrum versions >= 2.0
       -FINAL_SEED_VERSION = 21     # electrum >= 2.7 will set this to prevent
       +FINAL_SEED_VERSION = 22     # electrum >= 2.7 will set this to prevent
                                    # old versions from overwriting new format
        
        
       t@@ -215,6 +215,7 @@ class JsonDB(Logger):
                self._convert_version_19()
                self._convert_version_20()
                self._convert_version_21()
       +        self._convert_version_22()
                self.put('seed_version', FINAL_SEED_VERSION)  # just to be sure
        
                self._after_upgrade_tasks()
       t@@ -496,6 +497,24 @@ class JsonDB(Logger):
                    self.put('channels', channels)
                self.put('seed_version', 21)
        
       +    def _convert_version_22(self):
       +        # construct prevouts_by_scripthash
       +        if not self._is_upgrade_method_needed(21, 21):
       +            return
       +
       +        from .bitcoin import script_to_scripthash
       +        transactions = self.get('transactions', {})  # txid -> raw_tx
       +        prevouts_by_scripthash = defaultdict(list)
       +        for txid, raw_tx in transactions.items():
       +            tx = Transaction(raw_tx)
       +            for idx, txout in enumerate(tx.outputs()):
       +                outpoint = f"{txid}:{idx}"
       +                scripthash = script_to_scripthash(txout.scriptpubkey.hex())
       +                prevouts_by_scripthash[scripthash].append((outpoint, txout.value))
       +        self.put('prevouts_by_scripthash', prevouts_by_scripthash)
       +
       +        self.put('seed_version', 22)
       +
            def _convert_imported(self):
                if not self._is_upgrade_method_needed(0, 13):
                    return
       t@@ -661,6 +680,25 @@ class JsonDB(Logger):
                self.spent_outpoints[prevout_hash][prevout_n] = tx_hash
        
            @modifier
       +    def add_prevout_by_scripthash(self, scripthash: str, *, prevout: TxOutpoint, value: int) -> None:
       +        assert isinstance(prevout, TxOutpoint)
       +        if scripthash not in self._prevouts_by_scripthash:
       +            self._prevouts_by_scripthash[scripthash] = set()
       +        self._prevouts_by_scripthash[scripthash].add((prevout.to_str(), value))
       +
       +    @modifier
       +    def remove_prevout_by_scripthash(self, scripthash: str, *, prevout: TxOutpoint, value: int) -> None:
       +        assert isinstance(prevout, TxOutpoint)
       +        self._prevouts_by_scripthash[scripthash].discard((prevout.to_str(), value))
       +        if not self._prevouts_by_scripthash[scripthash]:
       +            self._prevouts_by_scripthash.pop(scripthash)
       +
       +    @locked
       +    def get_prevouts_by_scripthash(self, scripthash: str) -> Set[Tuple[TxOutpoint, int]]:
       +        prevouts_and_values = self._prevouts_by_scripthash.get(scripthash, set())
       +        return {(TxOutpoint.from_str(prevout), value) for prevout, value in prevouts_and_values}
       +
       +    @modifier
            def add_transaction(self, tx_hash: str, tx: Transaction) -> None:
                assert isinstance(tx, Transaction)
                self.transactions[tx_hash] = tx
       t@@ -863,14 +901,19 @@ class JsonDB(Logger):
                self.history = self.get_data_ref('addr_history')  # address -> list of (txid, height)
                self.verified_tx = self.get_data_ref('verified_tx3')  # txid -> (height, timestamp, txpos, header_hash)
                self.tx_fees = self.get_data_ref('tx_fees')  # type: Dict[str, TxFeesValue]
       +        # scripthash -> set of (outpoint, value)
       +        self._prevouts_by_scripthash = self.get_data_ref('prevouts_by_scripthash')  # type: Dict[str, Set[Tuple[str, int]]]
                # convert raw hex transactions to Transaction objects
                for tx_hash, raw_tx in self.transactions.items():
                    self.transactions[tx_hash] = Transaction(raw_tx)
       -        # convert list to set
       +        # convert txi, txo: list to set
                for t in self.txi, self.txo:
                    for d in t.values():
                        for addr, lst in d.items():
                            d[addr] = set([tuple(x) for x in lst])
       +        # convert prevouts_by_scripthash: list to set, list to tuple
       +        for scripthash, lst in self._prevouts_by_scripthash.items():
       +            self._prevouts_by_scripthash[scripthash] = {(prevout, value) for prevout, value in lst}
                # remove unreferenced tx
                for tx_hash in list(self.transactions.keys()):
                    if not self.get_txi_addresses(tx_hash) and not self.get_txo_addresses(tx_hash):
   DIR diff --git a/electrum/wallet.py b/electrum/wallet.py
       t@@ -36,9 +36,10 @@ import errno
        import traceback
        import operator
        from functools import partial
       +from collections import defaultdict
        from numbers import Number
        from decimal import Decimal
       -from typing import TYPE_CHECKING, List, Optional, Tuple, Union, NamedTuple, Sequence, Dict, Any
       +from typing import TYPE_CHECKING, List, Optional, Tuple, Union, NamedTuple, Sequence, Dict, Any, Set
        
        from .i18n import _
        from .bip32 import BIP32Node
       t@@ -252,6 +253,7 @@ class Abstract_Wallet(AddressSynchronizer):
                    if invoice.get('type') == PR_TYPE_ONCHAIN:
                        outputs = [PartialTxOutput.from_legacy_tuple(*output) for output in invoice.get('outputs')]
                        invoice['outputs'] = outputs
       +        self._prepare_onchain_invoice_paid_detection()
                self.calc_unused_change_addresses()
                # save wallet type the first time
                if self.storage.get('wallet_type') is None:
       t@@ -614,7 +616,10 @@ class Abstract_Wallet(AddressSynchronizer):
                elif invoice_type == PR_TYPE_ONCHAIN:
                    key = bh2u(sha256(repr(invoice))[0:16])
                    invoice['id'] = key
       -            invoice['txid'] = None
       +            outputs = invoice['outputs']  # type: List[PartialTxOutput]
       +            with self.transaction_lock:
       +                for txout in outputs:
       +                    self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(key)
                else:
                    raise Exception('Unsupported invoice type')
                self.invoices[key] = invoice
       t@@ -632,27 +637,73 @@ class Abstract_Wallet(AddressSynchronizer):
                out.sort(key=operator.itemgetter('time'))
                return out
        
       -    def set_paid(self, key, txid):
       -        if key not in self.invoices:
       -            return
       -        invoice = self.invoices[key]
       -        assert invoice.get('type') == PR_TYPE_ONCHAIN
       -        invoice['txid'] = txid
       -        self.storage.put('invoices', self.invoices)
       -
            def get_invoice(self, key):
                if key not in self.invoices:
                    return
                item = copy.copy(self.invoices[key])
                request_type = item.get('type')
                if request_type == PR_TYPE_ONCHAIN:
       -            item['status'] = PR_PAID if item.get('txid') is not None else PR_UNPAID
       +            item['status'] = PR_PAID if self._is_onchain_invoice_paid(item)[0] else PR_UNPAID
                elif self.lnworker and request_type == PR_TYPE_LN:
                    item['status'] = self.lnworker.get_payment_status(bfh(item['rhash']))
                else:
                    return
                return item
        
       +    def _get_relevant_invoice_keys_for_tx(self, tx: Transaction) -> Set[str]:
       +        relevant_invoice_keys = set()
       +        for txout in tx.outputs():
       +            for invoice_key in self._invoices_from_scriptpubkey_map.get(txout.scriptpubkey, set()):
       +                relevant_invoice_keys.add(invoice_key)
       +        return relevant_invoice_keys
       +
       +    def _prepare_onchain_invoice_paid_detection(self):
       +        # scriptpubkey -> list(invoice_keys)
       +        self._invoices_from_scriptpubkey_map = defaultdict(set)  # type: Dict[bytes, Set[str]]
       +        for invoice_key, invoice in self.invoices.items():
       +            if invoice.get('type') == PR_TYPE_ONCHAIN:
       +                outputs = invoice['outputs']  # type: List[PartialTxOutput]
       +                for txout in outputs:
       +                    self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(invoice_key)
       +
       +    def _is_onchain_invoice_paid(self, invoice) -> Tuple[bool, Sequence[str]]:
       +        """Returns whether on-chain invoice is satisfied, and list of relevant TXIDs."""
       +        assert invoice.get('type') == PR_TYPE_ONCHAIN
       +        invoice_amounts = defaultdict(int)  # type: Dict[bytes, int]  # scriptpubkey -> value_sats
       +        for txo in invoice['outputs']:  # type: PartialTxOutput
       +            invoice_amounts[txo.scriptpubkey] += 1 if txo.value == '!' else txo.value
       +        relevant_txs = []
       +        with self.transaction_lock:
       +            for invoice_scriptpubkey, invoice_amt in invoice_amounts.items():
       +                scripthash = bitcoin.script_to_scripthash(invoice_scriptpubkey.hex())
       +                prevouts_and_values = self.db.get_prevouts_by_scripthash(scripthash)
       +                relevant_txs += [prevout.txid.hex() for prevout, v in prevouts_and_values]
       +                total_received = sum([v for prevout, v in prevouts_and_values])
       +                if total_received < invoice_amt:
       +                    return False, []
       +        return True, relevant_txs
       +
       +    def _maybe_set_tx_label_based_on_invoices(self, tx: Transaction) -> bool:
       +        tx_hash = tx.txid()
       +        with self.transaction_lock:
       +            labels = []
       +            for invoice_key in self._get_relevant_invoice_keys_for_tx(tx):
       +                invoice = self.invoices.get(invoice_key)
       +                if invoice is None: continue
       +                assert invoice.get('type') == PR_TYPE_ONCHAIN
       +                if invoice['message']:
       +                    labels.append(invoice['message'])
       +        if labels:
       +            self.set_label(tx_hash, "; ".join(labels))
       +        return bool(labels)
       +
       +    def add_transaction(self, tx, *, allow_unrelated=False):
       +        tx_was_added = super().add_transaction(tx, allow_unrelated=allow_unrelated)
       +
       +        if tx_was_added:
       +            self._maybe_set_tx_label_based_on_invoices(tx)
       +        return tx_was_added
       +
            @profiler
            def get_full_history(self, fx=None, *, onchain_domain=None, include_lightning=True):
                transactions = OrderedDictWithIndex()
       t@@ -1877,10 +1928,6 @@ class Imported_Wallet(Simple_Wallet):
                    self.db.remove_addr_history(address)
                    for tx_hash in transactions_to_remove:
                        self.remove_transaction(tx_hash)
       -                self.db.remove_tx_fee(tx_hash)
       -                self.db.remove_verified_tx(tx_hash)
       -                self.unverified_tx.pop(tx_hash, None)
       -                self.db.remove_transaction(tx_hash)
                self.set_label(address, None)
                self.remove_payment_request(address)
                self.set_frozen_state_of_addresses([address], False)