URI: 
       tUse attr.s classes for invoices and requests: - storage upgrade - fixes #6192 - add can_pay_invoice, can_receive_invoice to lnworker - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 6058829870fde0ef17b2e08a567110ecc381ab94
   DIR parent 5f527720cf2ae4c7aef1cfdcf4244dbceb54a5bc
  HTML Author: ThomasV <thomasv@electrum.org>
       Date:   Sun, 31 May 2020 12:49:49 +0200
       
       Use attr.s classes for invoices and requests:
        - storage upgrade
        - fixes #6192
        - add can_pay_invoice, can_receive_invoice to lnworker
       
       Diffstat:
         M electrum/commands.py                |      41 +++++++++++++++++--------------
         M electrum/daemon.py                  |       8 ++++----
         M electrum/gui/kivy/main_window.py    |      13 ++++++-------
         M electrum/gui/kivy/uix/dialogs/invo… |      17 +++++++++--------
         M electrum/gui/kivy/uix/dialogs/requ… |      17 +++++++++--------
         M electrum/gui/kivy/uix/screens.py    |      67 ++++++++++++++++---------------
         M electrum/gui/kivy/uix/ui_screens/r… |       4 ++--
         M electrum/gui/kivy/uix/ui_screens/s… |       4 ++--
         M electrum/gui/qt/invoice_list.py     |      42 ++++++++++++++++---------------
         M electrum/gui/qt/main_window.py      |      37 ++++++++++++++-----------------
         M electrum/gui/qt/request_list.py     |      52 ++++++++++++++++---------------
         M electrum/gui/qt/util.py             |       2 +-
         A electrum/invoices.py                |     115 +++++++++++++++++++++++++++++++
         M electrum/lnaddr.py                  |      15 ---------------
         M electrum/lnchannel.py               |       3 ++-
         M electrum/lnworker.py                |      23 +++++++++++------------
         M electrum/paymentrequest.py          |       2 +-
         M electrum/util.py                    |      61 +------------------------------
         M electrum/wallet.py                  |     282 ++++++++++++++++++-------------
         M electrum/wallet_db.py               |      54 ++++++++++++++++++++++++-------
       
       20 files changed, 489 insertions(+), 370 deletions(-)
       ---
   DIR diff --git a/electrum/commands.py b/electrum/commands.py
       t@@ -47,7 +47,7 @@ from .bip32 import BIP32Node
        from .i18n import _
        from .transaction import (Transaction, multisig_script, TxOutput, PartialTransaction, PartialTxOutput,
                                  tx_from_any, PartialTxInput, TxOutpoint)
       -from .paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED
       +from .invoices import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED
        from .synchronizer import Notifier
        from .wallet import Abstract_Wallet, create_new_wallet, restore_wallet_from_text, Deterministic_Wallet
        from .address_synchronizer import TX_HEIGHT_LOCAL
       t@@ -59,7 +59,7 @@ from .lnpeer import channel_id_from_funding_tx
        from .plugin import run_hook
        from .version import ELECTRUM_VERSION
        from .simple_config import SimpleConfig
       -from .lnaddr import parse_lightning_invoice
       +from .invoices import LNInvoice
        
        
        if TYPE_CHECKING:
       t@@ -761,19 +761,13 @@ class Commands:
                decrypted = wallet.decrypt_message(pubkey, encrypted, password)
                return decrypted.decode('utf-8')
        
       -    def _format_request(self, out):
       -        from .util import get_request_status
       -        out['amount_BTC'] = format_satoshis(out.get('amount'))
       -        out['status'], out['status_str'] = get_request_status(out)
       -        return out
       -
            @command('w')
            async def getrequest(self, key, wallet: Abstract_Wallet = None):
                """Return a payment request"""
                r = wallet.get_request(key)
                if not r:
                    raise Exception("Request not found")
       -        return self._format_request(r)
       +        return wallet.export_request(r)
        
            #@command('w')
            #async def ackrequest(self, serialized):
       t@@ -783,8 +777,6 @@ class Commands:
            @command('w')
            async def list_requests(self, pending=False, expired=False, paid=False, wallet: Abstract_Wallet = None):
                """List the payment requests you made."""
       -        out = wallet.get_sorted_requests()
       -        out = list(map(self._format_request, out))
                if pending:
                    f = PR_UNPAID
                elif expired:
       t@@ -793,9 +785,10 @@ class Commands:
                    f = PR_PAID
                else:
                    f = None
       +        out = wallet.get_sorted_requests()
                if f is not None:
       -            out = list(filter(lambda x: x.get('status')==f, out))
       -        return out
       +            out = list(filter(lambda x: x.status==f, out))
       +        return [wallet.export_request(x) for x in out]
        
            @command('w')
            async def createnewaddress(self, wallet: Abstract_Wallet = None):
       t@@ -847,14 +840,13 @@ class Commands:
                expiration = int(expiration) if expiration else None
                req = wallet.make_payment_request(addr, amount, memo, expiration)
                wallet.add_payment_request(req)
       -        out = wallet.get_request(addr)
       -        return self._format_request(out)
       +        return wallet.export_request(req)
        
            @command('wn')
            async def add_lightning_request(self, amount, memo='', expiration=3600, wallet: Abstract_Wallet = None):
                amount_sat = int(satoshis(amount))
                key = await wallet.lnworker._add_request_coro(amount_sat, memo, expiration)
       -        return wallet.get_request(key)
       +        return wallet.get_formatted_request(key)
        
            @command('w')
            async def addtransaction(self, tx, wallet: Abstract_Wallet = None):
       t@@ -996,14 +988,24 @@ class Commands:
        
            @command('')
            async def decode_invoice(self, invoice):
       -        return parse_lightning_invoice(invoice)
       +        from .lnaddr import lndecode
       +        lnaddr = lndecode(invoice)
       +        return {
       +            'pubkey': lnaddr.pubkey.serialize().hex(),
       +            'amount_BTC': lnaddr.amount,
       +            'rhash': lnaddr.paymenthash.hex(),
       +            'description': lnaddr.get_description(),
       +            'exp': lnaddr.get_expiry(),
       +            'time': lnaddr.date,
       +            #'tags': str(lnaddr.tags),
       +        }
        
            @command('wn')
            async def lnpay(self, invoice, attempts=1, timeout=30, wallet: Abstract_Wallet = None):
                lnworker = wallet.lnworker
                lnaddr = lnworker._check_invoice(invoice, None)
                payment_hash = lnaddr.paymenthash
       -        wallet.save_invoice(parse_lightning_invoice(invoice))
       +        wallet.save_invoice(LNInvoice.from_bech32(invoice))
                success, log = await lnworker._pay(invoice, attempts=attempts)
                return {
                    'payment_hash': payment_hash.hex(),
       t@@ -1061,7 +1063,8 @@ class Commands:
        
            @command('w')
            async def list_invoices(self, wallet: Abstract_Wallet = None):
       -        return wallet.get_invoices()
       +        l = wallet.get_invoices()
       +        return [wallet.export_invoice(x) for x in l]
        
            @command('wn')
            async def close_channel(self, channel_point, force=False, wallet: Abstract_Wallet = None):
   DIR diff --git a/electrum/daemon.py b/electrum/daemon.py
       t@@ -46,7 +46,7 @@ from aiorpcx import TaskGroup
        from . import util
        from .network import Network
        from .util import (json_decode, to_bytes, to_string, profiler, standardize_path, constant_time_compare)
       -from .util import PR_PAID, PR_EXPIRED, get_request_status
       +from .invoices import PR_PAID, PR_EXPIRED
        from .util import log_exceptions, ignore_exceptions, randrange
        from .wallet import Wallet, Abstract_Wallet
        from .storage import WalletStorage
       t@@ -344,13 +344,13 @@ class PayServer(Logger):
        
            async def get_request(self, r):
                key = r.query_string
       -        request = self.wallet.get_request(key)
       +        request = self.wallet.get_formatted_request(key)
                return web.json_response(request)
        
            async def get_bip70_request(self, r):
                from .paymentrequest import make_request
                key = r.match_info['key']
       -        request = self.wallet.get_request(key)
       +        request = self.wallet.get_formatted_request(key)
                if not request:
                    return web.HTTPNotFound()
                pr = make_request(self.config, request)
       t@@ -360,7 +360,7 @@ class PayServer(Logger):
                ws = web.WebSocketResponse()
                await ws.prepare(request)
                key = request.query_string
       -        info = self.wallet.get_request(key)
       +        info = self.wallet.get_formatted_request(key)
                if not info:
                    await ws.send_str('unknown invoice')
                    await ws.close()
   DIR diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py
       t@@ -16,7 +16,8 @@ from electrum.plugin import run_hook
        from electrum import util
        from electrum.util import (profiler, InvalidPassword, send_exception_to_crash_reporter,
                                   format_satoshis, format_satoshis_plain, format_fee_satoshis,
       -                           PR_PAID, PR_FAILED, maybe_extract_bolt11_invoice)
       +                           maybe_extract_bolt11_invoice)
       +from electrum.invoices import PR_PAID, PR_FAILED
        from electrum import blockchain
        from electrum.network import Network, TxBroadcastError, BestEffortRequestFailed
        from electrum.interface import PREFERRED_NETWORK_PROTOCOL, ServerAddr
       t@@ -242,7 +243,7 @@ class ElectrumWindow(App):
                req = self.wallet.get_invoice(key)
                if req is None:
                    return
       -        status = req['status']
       +        status = self.wallet.get_invoice_status(req)
                # todo: update single item
                self.update_tab('send')
                if self.invoice_popup and self.invoice_popup.key == key:
       t@@ -393,7 +394,7 @@ class ElectrumWindow(App):
                if pr.verify(self.wallet.contacts):
                    key = pr.get_id()
                    invoice = self.wallet.get_invoice(key)  # FIXME wrong key...
       -            if invoice and invoice['status'] == PR_PAID:
       +            if invoice and self.wallet.get_invoice_status(invoice) == PR_PAID:
                        self.show_error("invoice already paid")
                        self.send_screen.do_clear()
                    elif pr.has_expired():
       t@@ -451,9 +452,7 @@ class ElectrumWindow(App):
        
            def show_request(self, is_lightning, key):
                from .uix.dialogs.request_dialog import RequestDialog
       -        request = self.wallet.get_request(key)
       -        data = request['invoice'] if is_lightning else request['URI']
       -        self.request_popup = RequestDialog('Request', data, key, is_lightning=is_lightning)
       +        self.request_popup = RequestDialog('Request', key)
                self.request_popup.open()
        
            def show_invoice(self, is_lightning, key):
       t@@ -461,7 +460,7 @@ class ElectrumWindow(App):
                invoice = self.wallet.get_invoice(key)
                if not invoice:
                    return
       -        data = invoice['invoice'] if is_lightning else key
       +        data = invoice.invoice if is_lightning else key
                self.invoice_popup = InvoiceDialog('Invoice', data, key)
                self.invoice_popup.open()
        
   DIR diff --git a/electrum/gui/kivy/uix/dialogs/invoice_dialog.py b/electrum/gui/kivy/uix/dialogs/invoice_dialog.py
       t@@ -7,8 +7,8 @@ from kivy.app import App
        from kivy.clock import Clock
        
        from electrum.gui.kivy.i18n import _
       -from electrum.util import pr_tooltips, pr_color, get_request_status
       -from electrum.util import PR_UNKNOWN, PR_UNPAID, PR_FAILED, PR_TYPE_LN
       +from electrum.invoices import pr_tooltips, pr_color
       +from electrum.invoices import PR_UNKNOWN, PR_UNPAID, PR_FAILED, PR_TYPE_LN
        
        if TYPE_CHECKING:
            from electrum.gui.kivy.main_window import ElectrumWindow
       t@@ -92,16 +92,17 @@ class InvoiceDialog(Factory.Popup):
                self.title = title
                self.data = data
                self.key = key
       -        r = self.app.wallet.get_invoice(key)
       -        self.amount = r.get('amount')
       -        self.description = r.get('message') or r.get('memo','')
       -        self.is_lightning = r.get('type') == PR_TYPE_LN
       +        invoice = self.app.wallet.get_invoice(key)
       +        self.amount = invoice.amount
       +        self.description = invoice.message
       +        self.is_lightning = invoice.type == PR_TYPE_LN
                self.update_status()
                self.log = self.app.wallet.lnworker.logs[self.key] if self.is_lightning else []
        
            def update_status(self):
       -        req = self.app.wallet.get_invoice(self.key)
       -        self.status, self.status_str = get_request_status(req)
       +        invoice = self.app.wallet.get_invoice(self.key)
       +        self.status = self.app.wallet.get_invoice_status(invoice)
       +        self.status_str = invoice.get_status_str(self.status)
                self.status_color = pr_color[self.status]
                self.can_pay = self.status in [PR_UNPAID, PR_FAILED]
                if self.can_pay and self.is_lightning and self.app.wallet.lnworker:
   DIR diff --git a/electrum/gui/kivy/uix/dialogs/request_dialog.py b/electrum/gui/kivy/uix/dialogs/request_dialog.py
       t@@ -7,8 +7,8 @@ from kivy.app import App
        from kivy.clock import Clock
        
        from electrum.gui.kivy.i18n import _
       -from electrum.util import pr_tooltips, pr_color, get_request_status
       -from electrum.util import PR_UNKNOWN, PR_UNPAID, PR_FAILED, PR_TYPE_LN
       +from electrum.invoices import pr_tooltips, pr_color
       +from electrum.invoices import PR_UNKNOWN, PR_UNPAID, PR_FAILED, PR_TYPE_LN
        
        if TYPE_CHECKING:
            from ...main_window import ElectrumWindow
       t@@ -86,17 +86,17 @@ Builder.load_string('''
        
        class RequestDialog(Factory.Popup):
        
       -    def __init__(self, title, data, key, *, is_lightning=False):
       +    def __init__(self, title, key):
                self.status = PR_UNKNOWN
                Factory.Popup.__init__(self)
                self.app = App.get_running_app()  # type: ElectrumWindow
                self.title = title
       -        self.data = data
                self.key = key
                r = self.app.wallet.get_request(key)
       -        self.amount = r.get('amount')
       -        self.description = r.get('message', '')
       -        self.is_lightning = r.get('type') == PR_TYPE_LN
       +        self.is_lightning = r.is_lightning()
       +        self.data = r.invoice if self.is_lightning else self.app.wallet.get_request_URI(r)
       +        self.amount = r.amount
       +        self.description = r.message
                self.update_status()
        
            def on_open(self):
       t@@ -109,7 +109,8 @@ class RequestDialog(Factory.Popup):
        
            def update_status(self):
                req = self.app.wallet.get_request(self.key)
       -        self.status, self.status_str = get_request_status(req)
       +        self.status = self.app.wallet.get_request_status(self.key)
       +        self.status_str = req.get_status_str(self.status)
                self.status_color = pr_color[self.status]
                if self.status == PR_UNPAID and self.is_lightning and self.app.wallet.lnworker:
                    if self.amount and self.amount > self.app.wallet.lnworker.num_sats_can_receive():
   DIR diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py
       t@@ -24,17 +24,17 @@ from kivy.utils import platform
        from kivy.logger import Logger
        
        from electrum.util import profiler, parse_URI, format_time, InvalidPassword, NotEnoughFunds, Fiat
       -from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN, PR_DEFAULT_EXPIRATION_WHEN_CREATING
       +from electrum.invoices import (PR_TYPE_ONCHAIN, PR_TYPE_LN, PR_DEFAULT_EXPIRATION_WHEN_CREATING,
       +                               PR_PAID, PR_UNKNOWN, PR_EXPIRED, PR_INFLIGHT,
       +                               LNInvoice, pr_expiration_values)
        from electrum import bitcoin, constants
        from electrum.transaction import Transaction, tx_from_any, PartialTransaction, PartialTxOutput
       -from electrum.util import (parse_URI, InvalidBitcoinURI, PR_PAID, PR_UNKNOWN, PR_EXPIRED,
       -                           PR_INFLIGHT, TxMinedInfo, get_request_status, pr_expiration_values,
       -                           maybe_extract_bolt11_invoice)
       +from electrum.util import parse_URI, InvalidBitcoinURI, TxMinedInfo, maybe_extract_bolt11_invoice
        from electrum.plugin import run_hook
        from electrum.wallet import InternalAddressCorruption
        from electrum import simple_config
        from electrum.simple_config import FEERATE_WARNING_HIGH_FEE, FEE_RATIO_HIGH_WARNING
       -from electrum.lnaddr import lndecode, parse_lightning_invoice
       +from electrum.lnaddr import lndecode
        from electrum.lnutil import RECEIVED, SENT, PaymentFailure
        
        from .dialogs.question import Question
       t@@ -225,26 +225,27 @@ class SendScreen(CScreen):
                self.app.show_invoice(obj.is_lightning, obj.key)
        
            def get_card(self, item):
       -        invoice_type = item['type']
       -        status, status_str = get_request_status(item) # convert to str
       -        if invoice_type == PR_TYPE_LN:
       -            key = item['rhash']
       +        status = self.app.wallet.get_invoice_status(item)
       +        status_str = item.get_status_str(status)
       +        is_lightning = item.type == PR_TYPE_LN
       +        if is_lightning:
       +            key = item.rhash
                    log = self.app.wallet.lnworker.logs.get(key)
       -            if item['status'] == PR_INFLIGHT and log:
       +            if status == PR_INFLIGHT and log:
                        status_str += '... (%d)'%len(log)
       -        elif invoice_type == PR_TYPE_ONCHAIN:
       -            key = item['id']
       +            is_bip70 = False
                else:
       -            raise Exception('unknown invoice type')
       +            key = item.id
       +            is_bip70 = bool(item.bip70)
                return {
       -            'is_lightning': invoice_type == PR_TYPE_LN,
       -            'is_bip70': 'bip70' in item,
       +            'is_lightning': is_lightning,
       +            'is_bip70': is_bip70,
                    'screen': self,
                    'status': status,
                    'status_str': status_str,
                    'key': key,
       -            'memo': item['message'],
       -            'amount': self.app.format_amount_and_units(item['amount'] or 0),
       +            'memo': item.message,
       +            'amount': self.app.format_amount_and_units(item.amount or 0),
                }
        
            def do_clear(self):
       t@@ -300,7 +301,7 @@ class SendScreen(CScreen):
                    return
                message = self.message
                if self.is_lightning:
       -            return parse_lightning_invoice(address)
       +            return LNInvoice.from_bech32(address)
                else:  # on-chain
                    if self.payment_request:
                        outputs = self.payment_request.get_outputs()
       t@@ -329,26 +330,27 @@ class SendScreen(CScreen):
                self.do_pay_invoice(invoice)
        
            def do_pay_invoice(self, invoice):
       -        if invoice['type'] == PR_TYPE_LN:
       +        if invoice.is_lightning():
                    self._do_pay_lightning(invoice)
                    return
       -        elif invoice['type'] == PR_TYPE_ONCHAIN:
       +        else:
                    do_pay = lambda rbf: self._do_pay_onchain(invoice, rbf)
                    if self.app.electrum_config.get('use_rbf'):
                        d = Question(_('Should this transaction be replaceable?'), do_pay)
                        d.open()
                    else:
                        do_pay(False)
       -        else:
       -            raise Exception('unknown invoice type')
        
            def _do_pay_lightning(self, invoice):
                attempts = 10
       -        threading.Thread(target=self.app.wallet.lnworker.pay, args=(invoice['invoice'], invoice['amount'], attempts)).start()
       +        threading.Thread(
       +            target=self.app.wallet.lnworker.pay,
       +            args=(invoice.invoice, invoice.amount),
       +            kwargs={'attempts':10}).start()
        
            def _do_pay_onchain(self, invoice, rbf):
                # make unsigned transaction
       -        outputs = invoice['outputs']  # type: List[PartialTxOutput]
       +        outputs = invoice.outputs  # type: List[PartialTxOutput]
                coins = self.app.wallet.get_spendable_coins(None)
                try:
                    tx = self.app.wallet.make_unsigned_transaction(coins=coins, outputs=outputs)
       t@@ -405,7 +407,7 @@ class SendScreen(CScreen):
                def callback(c):
                    if c:
                        for req in invoices:
       -                    key = req['key']
       +                    key = req.rhash if req.is_lightning() else req.get_address()
                            self.app.wallet.delete_invoice(key)
                        self.update()
                n = len(invoices)
       t@@ -477,16 +479,17 @@ class ReceiveScreen(CScreen):
                self.app.show_request(lightning, key)
        
            def get_card(self, req):
       -        is_lightning = req.get('type') == PR_TYPE_LN
       +        is_lightning = req.is_lightning()
                if not is_lightning:
       -            address = req['address']
       +            address = req.get_address()
                    key = address
                else:
       -            key = req['rhash']
       -            address = req['invoice']
       -        amount = req.get('amount')
       -        description = req.get('message') or req.get('memo', '')  # TODO: a db upgrade would be needed to simplify that.
       -        status, status_str = get_request_status(req)
       +            key = req.rhash
       +            address = req.invoice
       +        amount = req.amount
       +        description = req.message
       +        status = self.app.wallet.get_request_status(key)
       +        status_str = req.get_status_str(status)
                ci = {}
                ci['screen'] = self
                ci['address'] = address
   DIR diff --git a/electrum/gui/kivy/uix/ui_screens/receive.kv b/electrum/gui/kivy/uix/ui_screens/receive.kv
       t@@ -1,6 +1,6 @@
        #:import _ electrum.gui.kivy.i18n._
       -#:import pr_color electrum.util.pr_color
       -#:import PR_UNKNOWN electrum.util.PR_UNKNOWN
       +#:import pr_color electrum.invoices.pr_color
       +#:import PR_UNKNOWN electrum.invoices.PR_UNKNOWN
        #:import Factory kivy.factory.Factory
        #:import Decimal decimal.Decimal
        #:set btc_symbol chr(171)
   DIR diff --git a/electrum/gui/kivy/uix/ui_screens/send.kv b/electrum/gui/kivy/uix/ui_screens/send.kv
       t@@ -1,6 +1,6 @@
        #:import _ electrum.gui.kivy.i18n._
       -#:import pr_color electrum.util.pr_color
       -#:import PR_UNKNOWN electrum.util.PR_UNKNOWN
       +#:import pr_color electrum.invoices.pr_color
       +#:import PR_UNKNOWN electrum.invoices.PR_UNKNOWN
        #:import Factory kivy.factory.Factory
        #:import Decimal decimal.Decimal
        #:set btc_symbol chr(171)
   DIR diff --git a/electrum/gui/qt/invoice_list.py b/electrum/gui/qt/invoice_list.py
       t@@ -32,9 +32,8 @@ from PyQt5.QtWidgets import QAbstractItemView
        from PyQt5.QtWidgets import QMenu, QVBoxLayout, QTreeWidget, QTreeWidgetItem, QHeaderView
        
        from electrum.i18n import _
       -from electrum.util import format_time, PR_UNPAID, PR_PAID, PR_INFLIGHT, PR_FAILED
       -from electrum.util import get_request_status
       -from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN
       +from electrum.util import format_time
       +from electrum.invoices import Invoice, PR_UNPAID, PR_PAID, PR_INFLIGHT, PR_FAILED, PR_TYPE_ONCHAIN, PR_TYPE_LN
        from electrum.lnutil import PaymentAttemptLog
        
        from .util import (MyTreeView, read_QIcon, MySortModel,
       t@@ -77,7 +76,7 @@ class InvoiceList(MyTreeView):
                self.setSelectionMode(QAbstractItemView.ExtendedSelection)
                self.update()
        
       -    def update_item(self, key, req):
       +    def update_item(self, key, invoice: Invoice):
                model = self.std_model
                for row in range(0, model.rowCount()):
                    item = model.item(row, 0)
       t@@ -86,7 +85,8 @@ class InvoiceList(MyTreeView):
                else:
                    return
                status_item = model.item(row, self.Columns.STATUS)
       -        status, status_str = get_request_status(req)
       +        status = self.parent.wallet.get_invoice_status(invoice)
       +        status_str = invoice.get_status_str(status)
                if self.parent.wallet.lnworker:
                    log = self.parent.wallet.lnworker.logs.get(key)
                    if log and status == PR_INFLIGHT:
       t@@ -100,21 +100,21 @@ class InvoiceList(MyTreeView):
                self.std_model.clear()
                self.update_headers(self.__class__.headers)
                for idx, item in enumerate(self.parent.wallet.get_invoices()):
       -            invoice_type = item['type']
       -            if invoice_type == PR_TYPE_LN:
       -                key = item['rhash']
       +            if item.type == PR_TYPE_LN:
       +                key = item.rhash
                        icon_name = 'lightning.png'
       -            elif invoice_type == PR_TYPE_ONCHAIN:
       -                key = item['id']
       +            elif item.type == PR_TYPE_ONCHAIN:
       +                key = item.id
                        icon_name = 'bitcoin.png'
       -                if item.get('bip70'):
       +                if item.bip70:
                            icon_name = 'seal.png'
                    else:
                        raise Exception('Unsupported type')
       -            status, status_str = get_request_status(item)
       -            message = item['message']
       -            amount = item['amount']
       -            timestamp = item.get('time', 0)
       +            status = self.parent.wallet.get_invoice_status(item)
       +            status_str = item.get_status_str(status)
       +            message = item.message
       +            amount = item.amount
       +            timestamp = item.time or 0
                    date_str = format_time(timestamp) if timestamp else _('Unknown')
                    amount_str = self.parent.format_amount(amount, whitespaces=True)
                    labels = [date_str, message, amount_str, status_str]
       t@@ -123,7 +123,7 @@ class InvoiceList(MyTreeView):
                    items[self.Columns.DATE].setIcon(read_QIcon(icon_name))
                    items[self.Columns.STATUS].setIcon(read_QIcon(pr_icons.get(status)))
                    items[self.Columns.DATE].setData(key, role=ROLE_REQUEST_ID)
       -            items[self.Columns.DATE].setData(invoice_type, role=ROLE_REQUEST_TYPE)
       +            items[self.Columns.DATE].setData(item.type, role=ROLE_REQUEST_TYPE)
                    items[self.Columns.DATE].setData(timestamp, role=ROLE_SORT_ORDER)
                    self.std_model.insertRow(idx, items)
                self.filter()
       t@@ -143,11 +143,12 @@ class InvoiceList(MyTreeView):
                export_meta_gui(self.parent, _('invoices'), self.parent.invoices.export_file)
        
            def create_menu(self, position):
       +        wallet = self.parent.wallet
                items = self.selected_in_column(0)
                if len(items)>1:
                    keys = [ item.data(ROLE_REQUEST_ID)  for item in items]
       -            invoices = [ self.parent.wallet.get_invoice(key) for key in keys]
       -            can_batch_pay = all([ invoice['status'] == PR_UNPAID and invoice['type'] == PR_TYPE_ONCHAIN for invoice in invoices])
       +            invoices = [ wallet.invoices.get(key) for key in keys]
       +            can_batch_pay = all([i.type == PR_TYPE_ONCHAIN and wallet.get_invoice_status(i) == PR_UNPAID for i in invoices])
                    menu = QMenu(self)
                    if can_batch_pay:
                        menu.addAction(_("Batch pay invoices"), lambda: self.parent.pay_multiple_invoices(invoices))
       t@@ -164,9 +165,10 @@ class InvoiceList(MyTreeView):
                self.add_copy_menu(menu, idx)
                invoice = self.parent.wallet.get_invoice(key)
                menu.addAction(_("Details"), lambda: self.parent.show_invoice(key))
       -        if invoice['status'] == PR_UNPAID:
       +        status = wallet.get_invoice_status(invoice)
       +        if status == PR_UNPAID:
                    menu.addAction(_("Pay"), lambda: self.parent.do_pay_invoice(invoice))
       -        if invoice['status'] == PR_FAILED:
       +        if status == PR_FAILED:
                    menu.addAction(_("Retry"), lambda: self.parent.do_pay_invoice(invoice))
                if self.parent.wallet.lnworker:
                    log = self.parent.wallet.lnworker.logs.get(key)
   DIR diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
       t@@ -62,7 +62,8 @@ from electrum.util import (format_time, format_satoshis, format_fee_satoshis,
                                   get_new_wallet_name, send_exception_to_crash_reporter,
                                   InvalidBitcoinURI, maybe_extract_bolt11_invoice, NotEnoughFunds,
                                   NoDynamicFeeEstimates, MultipleSpendMaxTxOutputs)
       -from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN, PR_DEFAULT_EXPIRATION_WHEN_CREATING
       +from electrum.invoices import PR_TYPE_ONCHAIN, PR_TYPE_LN, PR_DEFAULT_EXPIRATION_WHEN_CREATING
       +from electrum.invoices import PR_PAID, PR_FAILED, pr_expiration_values, LNInvoice
        from electrum.transaction import (Transaction, PartialTxInput,
                                          PartialTransaction, PartialTxOutput)
        from electrum.address_synchronizer import AddTransactionException
       t@@ -73,10 +74,7 @@ from electrum.network import Network, TxBroadcastError, BestEffortRequestFailed,
        from electrum.exchange_rate import FxThread
        from electrum.simple_config import SimpleConfig
        from electrum.logging import Logger
       -from electrum.util import PR_PAID, PR_FAILED
       -from electrum.util import pr_expiration_values
        from electrum.lnutil import ln_dummy_address
       -from electrum.lnaddr import parse_lightning_invoice
        
        from .exception_window import Exception_Hook
        from .amountedit import AmountEdit, BTCAmountEdit, FreezableLineEdit, FeerateEdit
       t@@ -1192,7 +1190,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                self.receive_message_e.setText('')
                # copy to clipboard
                r = self.wallet.get_request(key)
       -        content = r.get('invoice', '') if is_lightning else r.get('address', '')
       +        content = r.invoice if r.is_lightning() else r.get_address()
                title = _('Invoice') if is_lightning else _('Address')
                self.do_copy(content, title=title)
        
       t@@ -1231,7 +1229,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
            def export_payment_request(self, addr):
                r = self.wallet.receive_requests.get(addr)
                pr = paymentrequest.serialize_request(r).SerializeToString()
       -        name = r['id'] + '.bip70'
       +        name = r.id + '.bip70'
                fileName = self.getSaveFileName(_("Select where to save your payment request"), name, "*.bip70")
                if fileName:
                    with open(fileName, "wb+") as f:
       t@@ -1505,21 +1503,21 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                if self.check_send_tab_payto_line_and_show_errors():
                    return
                if not self._is_onchain:
       -            invoice = self.payto_e.lightning_invoice
       -            if not invoice:
       +            invoice_str = self.payto_e.lightning_invoice
       +            if not invoice_str:
                        return
                    if not self.wallet.lnworker:
                        self.show_error(_('Lightning is disabled'))
                        return
       -            invoice_dict = parse_lightning_invoice(invoice)
       -            if invoice_dict.get('amount') is None:
       +            invoice = LNInvoice.from_bech32(invoice_str)
       +            if invoice.amount is None:
                        amount = self.amount_e.get_amount()
                        if amount:
       -                    invoice_dict['amount'] = amount
       +                    invoice.amount = amount
                        else:
                            self.show_error(_('No amount'))
                            return
       -            return invoice_dict
       +            return invoice
                else:
                    outputs = self.read_outputs()
                    if self.check_send_tab_onchain_outputs_and_show_errors(outputs):
       t@@ -1547,15 +1545,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
            def pay_multiple_invoices(self, invoices):
                outputs = []
                for invoice in invoices:
       -            outputs += invoice['outputs']
       +            outputs += invoice.outputs
                self.pay_onchain_dialog(self.get_coins(), outputs)
        
            def do_pay_invoice(self, invoice):
       -        if invoice['type'] == PR_TYPE_LN:
       -            self.pay_lightning_invoice(invoice['invoice'], invoice['amount'])
       -        elif invoice['type'] == PR_TYPE_ONCHAIN:
       -            outputs = invoice['outputs']
       -            self.pay_onchain_dialog(self.get_coins(), outputs)
       +        if invoice.type == PR_TYPE_LN:
       +            self.pay_lightning_invoice(invoice.invoice, invoice.amount)
       +        elif invoice.type == PR_TYPE_ONCHAIN:
       +            self.pay_onchain_dialog(self.get_coins(), invoice.outputs)
                else:
                    raise Exception('unknown invoice type')
        
       t@@ -1775,7 +1772,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                    return
                key = pr.get_id()
                invoice = self.wallet.get_invoice(key)
       -        if invoice and invoice['status'] == PR_PAID:
       +        if invoice and self.wallet.get_invoice_status() == PR_PAID:
                    self.show_message("invoice already paid")
                    self.do_clear()
                    self.payment_request = None
       t@@ -1970,7 +1967,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                if invoice is None:
                    self.show_error('Cannot find payment request in wallet.')
                    return
       -        bip70 = invoice.get('bip70')
       +        bip70 = invoice.bip70
                if bip70:
                    pr = paymentrequest.PaymentRequest(bytes.fromhex(bip70))
                    pr.verify(self.contacts)
   DIR diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py
       t@@ -31,8 +31,8 @@ from PyQt5.QtWidgets import QMenu, QAbstractItemView
        from PyQt5.QtCore import Qt, QItemSelectionModel, QModelIndex
        
        from electrum.i18n import _
       -from electrum.util import format_time, get_request_status
       -from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN
       +from electrum.util import format_time
       +from electrum.invoices import PR_TYPE_ONCHAIN, PR_TYPE_LN
        from electrum.plugin import run_hook
        
        from .util import MyTreeView, pr_icons, read_QIcon, webopen, MySortModel
       t@@ -90,18 +90,17 @@ class RequestList(MyTreeView):
                    return
                # TODO use siblingAtColumn when min Qt version is >=5.11
                item = self.item_from_index(idx.sibling(idx.row(), self.Columns.DATE))
       -        request_type = item.data(ROLE_REQUEST_TYPE)
                key = item.data(ROLE_KEY)
                req = self.wallet.get_request(key)
                if req is None:
                    self.update()
                    return
       -        if request_type == PR_TYPE_LN:
       -            self.parent.receive_payreq_e.setText(req.get('invoice'))
       -            self.parent.receive_address_e.setText(req.get('invoice'))
       +        if req.is_lightning():
       +            self.parent.receive_payreq_e.setText(req.invoice)
       +            self.parent.receive_address_e.setText(req.invoice)
                else:
       -            self.parent.receive_payreq_e.setText(req.get('URI'))
       -            self.parent.receive_address_e.setText(req['address'])
       +            self.parent.receive_payreq_e.setText(self.parent.wallet.get_request_URI(req))
       +            self.parent.receive_address_e.setText(req.get_address())
                self.parent.receive_payreq_e.repaint()  # macOS hack (similar to #4777)
                self.parent.receive_address_e.repaint()  # macOS hack (similar to #4777)
        
       t@@ -119,7 +118,8 @@ class RequestList(MyTreeView):
                    key = date_item.data(ROLE_KEY)
                    req = self.wallet.get_request(key)
                    if req:
       -                status, status_str = get_request_status(req)
       +                status = self.parent.wallet.get_request_status(key)
       +                status_str = req.get_status_str(status)
                        status_item.setText(status_str)
                        status_item.setIcon(read_QIcon(pr_icons.get(status)))
        
       t@@ -130,20 +130,22 @@ class RequestList(MyTreeView):
                self.std_model.clear()
                self.update_headers(self.__class__.headers)
                for req in self.wallet.get_sorted_requests():
       -            status, status_str = get_request_status(req)
       -            request_type = req['type']
       -            timestamp = req.get('time', 0)
       -            amount = req.get('amount')
       -            message = req.get('message') or req.get('memo')
       +            key = req.rhash if req.is_lightning() else req.id
       +            status = self.parent.wallet.get_request_status(key)
       +            status_str = req.get_status_str(status)
       +            request_type = req.type
       +            timestamp = req.time
       +            amount = req.amount
       +            message = req.message
                    date = format_time(timestamp)
                    amount_str = self.parent.format_amount(amount) if amount else ""
                    labels = [date, message, amount_str, status_str]
       -            if request_type == PR_TYPE_LN:
       -                key = req['rhash']
       +            if req.is_lightning():
       +                key = req.rhash
                        icon = read_QIcon("lightning.png")
                        tooltip = 'lightning request'
       -            elif request_type == PR_TYPE_ONCHAIN:
       -                key = req['address']
       +            else:
       +                key = req.get_address()
                        icon = read_QIcon("bitcoin.png")
                        tooltip = 'onchain request'
                    items = [QStandardItem(e) for e in labels]
       t@@ -182,20 +184,20 @@ class RequestList(MyTreeView):
                if not item:
                    return
                key = item.data(ROLE_KEY)
       -        request_type = item.data(ROLE_REQUEST_TYPE)
                req = self.wallet.get_request(key)
                if req is None:
                    self.update()
                    return
                menu = QMenu(self)
                self.add_copy_menu(menu, idx)
       -        if request_type == PR_TYPE_LN:
       -            menu.addAction(_("Copy Request"), lambda: self.parent.do_copy(req['invoice'], title='Lightning Request'))
       +        if req.is_lightning():
       +            menu.addAction(_("Copy Request"), lambda: self.parent.do_copy(req.invoice, title='Lightning Request'))
                else:
       -            menu.addAction(_("Copy Request"), lambda: self.parent.do_copy(req['URI'], title='Bitcoin URI'))
       -            menu.addAction(_("Copy Address"), lambda: self.parent.do_copy(req['address'], title='Bitcoin Address'))
       -        if 'view_url' in req:
       -            menu.addAction(_("View in web browser"), lambda: webopen(req['view_url']))
       +            URI = self.wallet.get_request_URI(req)
       +            menu.addAction(_("Copy Request"), lambda: self.parent.do_copy(URI, title='Bitcoin URI'))
       +            menu.addAction(_("Copy Address"), lambda: self.parent.do_copy(req.get_address(), title='Bitcoin Address'))
       +        #if 'view_url' in req:
       +        #    menu.addAction(_("View in web browser"), lambda: webopen(req['view_url']))
                menu.addAction(_("Delete"), lambda: self.parent.delete_requests([key]))
                run_hook('receive_list_menu', menu, key)
                menu.exec_(self.viewport().mapToGlobal(position))
   DIR diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py
       t@@ -26,7 +26,7 @@ from PyQt5.QtWidgets import (QPushButton, QLabel, QMessageBox, QHBoxLayout,
        
        from electrum.i18n import _, languages
        from electrum.util import FileImportFailed, FileExportFailed, make_aiohttp_session, resource_path
       -from electrum.util import PR_UNPAID, PR_PAID, PR_EXPIRED, PR_INFLIGHT, PR_UNKNOWN, PR_FAILED, PR_ROUTING
       +from electrum.invoices import PR_UNPAID, PR_PAID, PR_EXPIRED, PR_INFLIGHT, PR_UNKNOWN, PR_FAILED, PR_ROUTING
        
        if TYPE_CHECKING:
            from .main_window import ElectrumWindow
   DIR diff --git a/electrum/invoices.py b/electrum/invoices.py
       t@@ -0,0 +1,115 @@
       +import attr
       +import time
       +
       +from .json_db import StoredObject
       +from .i18n import _
       +from .util import age
       +from .lnaddr import lndecode
       +from . import constants
       +from .bitcoin import COIN
       +from .transaction import PartialTxOutput
       +
       +# convention: 'invoices' = outgoing , 'request' = incoming
       +
       +# types of payment requests
       +PR_TYPE_ONCHAIN = 0
       +PR_TYPE_LN = 2
       +
       +# status of payment requests
       +PR_UNPAID   = 0
       +PR_EXPIRED  = 1
       +PR_UNKNOWN  = 2     # sent but not propagated
       +PR_PAID     = 3     # send and propagated
       +PR_INFLIGHT = 4     # unconfirmed
       +PR_FAILED   = 5
       +PR_ROUTING  = 6
       +
       +pr_color = {
       +    PR_UNPAID:   (.7, .7, .7, 1),
       +    PR_PAID:     (.2, .9, .2, 1),
       +    PR_UNKNOWN:  (.7, .7, .7, 1),
       +    PR_EXPIRED:  (.9, .2, .2, 1),
       +    PR_INFLIGHT: (.9, .6, .3, 1),
       +    PR_FAILED:   (.9, .2, .2, 1),
       +    PR_ROUTING: (.9, .6, .3, 1),
       +}
       +
       +pr_tooltips = {
       +    PR_UNPAID:_('Pending'),
       +    PR_PAID:_('Paid'),
       +    PR_UNKNOWN:_('Unknown'),
       +    PR_EXPIRED:_('Expired'),
       +    PR_INFLIGHT:_('In progress'),
       +    PR_FAILED:_('Failed'),
       +    PR_ROUTING: _('Computing route...'),
       +}
       +
       +PR_DEFAULT_EXPIRATION_WHEN_CREATING = 24*60*60  # 1 day
       +pr_expiration_values = {
       +    0: _('Never'),
       +    10*60: _('10 minutes'),
       +    60*60: _('1 hour'),
       +    24*60*60: _('1 day'),
       +    7*24*60*60: _('1 week'),
       +}
       +assert PR_DEFAULT_EXPIRATION_WHEN_CREATING in pr_expiration_values
       +
       +outputs_decoder = lambda _list: [PartialTxOutput.from_legacy_tuple(*x) for x in _list]
       +
       +@attr.s
       +class Invoice(StoredObject):
       +    type = attr.ib(type=int)
       +    message = attr.ib(type=str)
       +    amount = attr.ib(type=int)
       +    exp = attr.ib(type=int)
       +    time = attr.ib(type=int)
       +
       +    def is_lightning(self):
       +        return self.type == PR_TYPE_LN
       +
       +    def get_status_str(self, status):
       +        status_str = pr_tooltips[status]
       +        if status == PR_UNPAID:
       +            if self.exp > 0:
       +                expiration = self.exp + self.time
       +                status_str = _('Expires') + ' ' + age(expiration, include_seconds=True)
       +            else:
       +                status_str = _('Pending')
       +        return status_str
       +
       +@attr.s
       +class OnchainInvoice(Invoice):
       +    id = attr.ib(type=str)
       +    outputs = attr.ib(type=list, converter=outputs_decoder)
       +    bip70 = attr.ib(type=str) # may be None
       +    requestor = attr.ib(type=str) # may be None
       +
       +    def get_address(self):
       +        assert len(self.outputs) == 1
       +        return self.outputs[0].address
       +
       +@attr.s
       +class LNInvoice(Invoice):
       +    rhash = attr.ib(type=str)
       +    invoice = attr.ib(type=str)
       +
       +    @classmethod
       +    def from_bech32(klass, invoice: str):
       +        lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
       +        amount = int(lnaddr.amount * COIN) if lnaddr.amount else None
       +        return LNInvoice(
       +            type = PR_TYPE_LN,
       +            amount = amount,
       +            message = lnaddr.get_description(),
       +            time = lnaddr.date,
       +            exp = lnaddr.get_expiry(),
       +            rhash = lnaddr.paymenthash.hex(),
       +            invoice = invoice,
       +        )
       +
       +
       +def invoice_from_json(x: dict) -> Invoice:
       +    if x.get('type') == PR_TYPE_LN:
       +        return LNInvoice(**x)
       +    else:
       +        return OnchainInvoice(**x)
   DIR diff --git a/electrum/lnaddr.py b/electrum/lnaddr.py
       t@@ -13,7 +13,6 @@ from .bitcoin import hash160_to_b58_address, b58_address_to_hash160
        from .segwit_addr import bech32_encode, bech32_decode, CHARSET
        from . import constants
        from . import ecc
       -from .util import PR_TYPE_LN
        from .bitcoin import COIN
        
        
       t@@ -470,20 +469,6 @@ def lndecode(invoice: str, *, verbose=False, expected_hrp=None) -> LnAddr:
        
        
        
       -def parse_lightning_invoice(invoice):
       -    lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
       -    amount = int(lnaddr.amount * COIN) if lnaddr.amount else None
       -    return {
       -        'type': PR_TYPE_LN,
       -        'invoice': invoice,
       -        'amount': amount,
       -        'message': lnaddr.get_description(),
       -        'time': lnaddr.date,
       -        'exp': lnaddr.get_expiry(),
       -        'pubkey': lnaddr.pubkey.serialize().hex(),
       -        'rhash': lnaddr.paymenthash.hex(),
       -    }
       -
        if __name__ == '__main__':
            # run using
            # python3 -m electrum.lnaddr <invoice> <expected hrp>
   DIR diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py
       t@@ -35,7 +35,8 @@ import attr
        
        from . import ecc
        from . import constants, util
       -from .util import bfh, bh2u, chunks, TxMinedInfo, PR_PAID
       +from .util import bfh, bh2u, chunks, TxMinedInfo
       +from .invoices import PR_PAID
        from .bitcoin import redeem_script_to_address
        from .crypto import sha256, sha256d
        from .transaction import Transaction, PartialTransaction, TxInput
   DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py
       t@@ -24,8 +24,8 @@ from aiorpcx import run_in_thread
        from . import constants, util
        from . import keystore
        from .util import profiler
       -from .util import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_INFLIGHT, PR_FAILED, PR_ROUTING
       -from .util import PR_TYPE_LN, NetworkRetryManager
       +from .invoices import PR_TYPE_LN, PR_UNPAID, PR_EXPIRED, PR_PAID, PR_INFLIGHT, PR_FAILED, PR_ROUTING, LNInvoice
       +from .util import NetworkRetryManager
        from .lnutil import LN_MAX_FUNDING_SAT
        from .keystore import BIP32_KeyStore
        from .bitcoin import COIN
       t@@ -1102,15 +1102,7 @@ class LNWallet(LNWorker):
                                payment_secret=derive_payment_secret_from_payment_preimage(payment_preimage))
                invoice = lnencode(lnaddr, self.node_keypair.privkey)
                key = bh2u(lnaddr.paymenthash)
       -        req = {
       -            'type': PR_TYPE_LN,
       -            'amount': amount_sat,
       -            'time': lnaddr.date,
       -            'exp': expiry,
       -            'message': message,
       -            'rhash': key,
       -            'invoice': invoice
       -        }
       +        req = LNInvoice.from_bech32(invoice)
                self.save_preimage(payment_hash, payment_preimage)
                self.save_payment_info(info)
                self.wallet.add_payment_request(req)
       t@@ -1145,7 +1137,8 @@ class LNWallet(LNWorker):
                info = self.get_payment_info(payment_hash)
                return info.status if info else PR_UNPAID
        
       -    def get_invoice_status(self, key):
       +    def get_invoice_status(self, invoice):
       +        key = invoice.rhash
                log = self.logs[key]
                if key in self.is_routing:
                    return PR_ROUTING
       t@@ -1285,6 +1278,12 @@ class LNWallet(LNWorker):
                    return Decimal(max(chan.available_to_spend(REMOTE) if chan.is_open() else 0
                                       for chan in self.channels.values()))/1000 if self.channels else 0
        
       +    def can_pay_invoice(self, invoice):
       +        return invoice.amount <= self.num_sats_can_send()
       +
       +    def can_receive_invoice(self, invoice):
       +        return invoice.amount <= self.num_sats_can_receive()
       +
            async def close_channel(self, chan_id):
                chan = self._channels[chan_id]
                peer = self._peers[chan.node_id]
   DIR diff --git a/electrum/paymentrequest.py b/electrum/paymentrequest.py
       t@@ -41,7 +41,7 @@ except ImportError:
        
        from . import bitcoin, ecc, util, transaction, x509, rsakey
        from .util import bh2u, bfh, export_meta, import_meta, make_aiohttp_session
       -from .util import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_UNKNOWN, PR_INFLIGHT
       +from .invoices import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_UNKNOWN, PR_INFLIGHT
        from .crypto import sha256
        from .bitcoin import address_to_script
        from .transaction import PartialTxOutput
   DIR diff --git a/electrum/util.py b/electrum/util.py
       t@@ -43,6 +43,7 @@ from typing import NamedTuple, Optional
        import ssl
        import ipaddress
        import random
       +import attr
        
        import aiohttp
        from aiohttp_socks import ProxyConnector, ProxyType
       t@@ -77,66 +78,6 @@ base_units_list = ['BTC', 'mBTC', 'bits', 'sat']  # list(dict) does not guarante
        
        DECIMAL_POINT_DEFAULT = 5  # mBTC
        
       -# types of payment requests
       -PR_TYPE_ONCHAIN = 0
       -PR_TYPE_LN = 2
       -
       -# status of payment requests
       -PR_UNPAID   = 0
       -PR_EXPIRED  = 1
       -PR_UNKNOWN  = 2     # sent but not propagated
       -PR_PAID     = 3     # send and propagated
       -PR_INFLIGHT = 4     # unconfirmed
       -PR_FAILED   = 5
       -PR_ROUTING  = 6
       -
       -pr_color = {
       -    PR_UNPAID:   (.7, .7, .7, 1),
       -    PR_PAID:     (.2, .9, .2, 1),
       -    PR_UNKNOWN:  (.7, .7, .7, 1),
       -    PR_EXPIRED:  (.9, .2, .2, 1),
       -    PR_INFLIGHT: (.9, .6, .3, 1),
       -    PR_FAILED:   (.9, .2, .2, 1),
       -    PR_ROUTING: (.9, .6, .3, 1),
       -}
       -
       -pr_tooltips = {
       -    PR_UNPAID:_('Pending'),
       -    PR_PAID:_('Paid'),
       -    PR_UNKNOWN:_('Unknown'),
       -    PR_EXPIRED:_('Expired'),
       -    PR_INFLIGHT:_('In progress'),
       -    PR_FAILED:_('Failed'),
       -    PR_ROUTING: _('Computing route...'),
       -}
       -
       -PR_DEFAULT_EXPIRATION_WHEN_CREATING = 24*60*60  # 1 day
       -pr_expiration_values = {
       -    0: _('Never'),
       -    10*60: _('10 minutes'),
       -    60*60: _('1 hour'),
       -    24*60*60: _('1 day'),
       -    7*24*60*60: _('1 week'),
       -}
       -assert PR_DEFAULT_EXPIRATION_WHEN_CREATING in pr_expiration_values
       -
       -
       -def get_request_status(req):
       -    status = req['status']
       -    exp = req.get('exp', 0) or 0
       -    if req.get('type') == PR_TYPE_LN and exp == 0:
       -        status = PR_EXPIRED  # for BOLT-11 invoices, exp==0 means 0 seconds
       -    if req['status'] == PR_UNPAID and exp > 0 and req['time'] + req['exp'] < time.time():
       -        status = PR_EXPIRED
       -    status_str = pr_tooltips[status]
       -    if status == PR_UNPAID:
       -        if exp > 0:
       -            expiration = exp + req['time']
       -            status_str = _('Expires') + ' ' + age(expiration, include_seconds=True)
       -        else:
       -            status_str = _('Pending')
       -    return status, status_str
       -
        
        class UnknownBaseUnit(Exception): pass
        
   DIR diff --git a/electrum/wallet.py b/electrum/wallet.py
       t@@ -52,10 +52,10 @@ from .util import (NotEnoughFunds, UserCancelled, profiler,
                           WalletFileException, BitcoinException, MultipleSpendMaxTxOutputs,
                           InvalidPassword, format_time, timestamp_to_datetime, Satoshis,
                           Fiat, bfh, bh2u, TxMinedInfo, quantize_feerate, create_bip21_uri, OrderedDictWithIndex)
       -from .util import PR_TYPE_ONCHAIN, PR_TYPE_LN, get_backup_dir
       +from .util import get_backup_dir
        from .simple_config import SimpleConfig
       -from .bitcoin import (COIN, is_address, address_to_script,
       -                      is_minikey, relayfee, dust_threshold)
       +from .bitcoin import COIN, TYPE_ADDRESS
       +from .bitcoin import is_address, address_to_script, is_minikey, relayfee, dust_threshold
        from .crypto import sha256d
        from . import keystore
        from .keystore import load_keystore, Hardware_KeyStore, KeyStore, KeyStoreWithMPK, AddressIndexGeneric
       t@@ -68,7 +68,8 @@ from .transaction import (Transaction, TxInput, UnknownTxinType, TxOutput,
        from .plugin import run_hook
        from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL,
                                           TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE)
       -from .util import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, PR_INFLIGHT
       +from .invoices import Invoice, OnchainInvoice, invoice_from_json
       +from .invoices import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, PR_INFLIGHT, PR_TYPE_ONCHAIN, PR_TYPE_LN
        from .contacts import Contacts
        from .interface import NetworkException
        from .mnemonic import Mnemonic
       t@@ -660,39 +661,43 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
                    amount = '!'
                else:
                    amount = sum(x.value for x in outputs)
       -        invoice = {
       -            'type': PR_TYPE_ONCHAIN,
       -            'message': message,
       -            'outputs': outputs,
       -            'amount': amount,
       -        }
       +        outputs = [x.to_legacy_tuple() for x in outputs]
                if pr:
       -            invoice['bip70'] = pr.raw.hex()
       -            invoice['time'] = pr.get_time()
       -            invoice['exp'] = pr.get_expiration_date() - pr.get_time()
       -            invoice['requestor'] = pr.get_requestor()
       -            invoice['message'] = pr.get_memo()
       -        elif URI:
       -            timestamp = URI.get('time')
       -            if timestamp: invoice['time'] = timestamp
       -            exp = URI.get('exp')
       -            if exp: invoice['exp'] = exp
       -        if 'time' not in invoice:
       -            invoice['time'] = int(time.time())
       +            invoice = OnchainInvoice(
       +                type = PR_TYPE_ONCHAIN,
       +                amount = amount,
       +                outputs = outputs,
       +                message = pr.get_memo(),
       +                id = pr.get_id(),
       +                time = pr.get_time(),
       +                exp = pr.get_expiration_date() - pr.get_time(),
       +                bip70 = pr.raw.hex() if pr else None,
       +                requestor = pr.get_requestor(),
       +            )
       +        else:
       +            invoice = OnchainInvoice(
       +                type = PR_TYPE_ONCHAIN,
       +                amount = amount,
       +                outputs = outputs,
       +                message = message,
       +                id = bh2u(sha256(repr(outputs))[0:16]),
       +                time = URI.get('time') if URI else int(time.time()),
       +                exp = URI.get('exp') if URI else 0,
       +                bip70 = None,
       +                requestor = None,
       +            )
                return invoice
        
       -    def save_invoice(self, invoice):
       -        invoice_type = invoice['type']
       +    def save_invoice(self, invoice: Invoice):
       +        invoice_type = invoice.type
                if invoice_type == PR_TYPE_LN:
       -            key = invoice['rhash']
       +            key = invoice.rhash
                elif invoice_type == PR_TYPE_ONCHAIN:
       +            key = invoice.id
                    if self.is_onchain_invoice_paid(invoice):
                        self.logger.info("saving invoice... but it is already paid!")
       -            key = bh2u(sha256(repr(invoice))[0:16])
       -            invoice['id'] = key
       -            outputs = invoice['outputs']  # type: List[PartialTxOutput]
                    with self.transaction_lock:
       -                for txout in outputs:
       +                for txout in invoice.outputs:
                            self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(key)
                else:
                    raise Exception('Unsupported invoice type')
       t@@ -704,26 +709,13 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
                self.save_db()
        
            def get_invoices(self):
       -        out = [self.get_invoice(key) for key in self.invoices.keys()]
       -        out = list(filter(None, out))
       -        out.sort(key=operator.itemgetter('time'))
       +        out = list(self.invoices.values())
       +        #out = list(filter(None, out)) filter out ln
       +        out.sort(key=lambda x:x.time)
                return out
        
            def get_invoice(self, key):
       -        if key not in self.invoices:
       -            return
       -        # convert StoredDict to dict
       -        item = dict(self.invoices[key])
       -        request_type = item.get('type')
       -        if request_type == PR_TYPE_ONCHAIN:
       -            item['status'] = PR_PAID if self.is_onchain_invoice_paid(item) else PR_UNPAID
       -        elif self.lnworker and request_type == PR_TYPE_LN:
       -            item['status'] = self.lnworker.get_invoice_status(key)
       -        else:
       -            return
       -        # unique handle
       -        item['key'] = key
       -        return item
       +        return self.invoices.get(key)
        
            def _get_relevant_invoice_keys_for_tx(self, tx: Transaction) -> Set[str]:
                relevant_invoice_keys = set()
       t@@ -736,16 +728,15 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
                # 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:
       +            if invoice.type == PR_TYPE_ONCHAIN:
       +                for txout in invoice.outputs:
                            self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(invoice_key)
        
       -    def _is_onchain_invoice_paid(self, invoice: dict) -> Tuple[bool, Sequence[str]]:
       +    def _is_onchain_invoice_paid(self, invoice: Invoice) -> Tuple[bool, Sequence[str]]:
                """Returns whether on-chain invoice is satisfied, and list of relevant TXIDs."""
       -        assert invoice.get('type') == PR_TYPE_ONCHAIN
       +        assert invoice.type == PR_TYPE_ONCHAIN
                invoice_amounts = defaultdict(int)  # type: Dict[bytes, int]  # scriptpubkey -> value_sats
       -        for txo in invoice['outputs']:  # type: PartialTxOutput
       +        for txo in invoice.outputs:  # type: PartialTxOutput
                    invoice_amounts[txo.scriptpubkey] += 1 if txo.value == '!' else txo.value
                relevant_txs = []
                with self.transaction_lock:
       t@@ -762,7 +753,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
                            return False, []
                return True, relevant_txs
        
       -    def is_onchain_invoice_paid(self, invoice: dict) -> bool:
       +    def is_onchain_invoice_paid(self, invoice: Invoice) -> bool:
                return self._is_onchain_invoice_paid(invoice)[0]
        
            def _maybe_set_tx_label_based_on_invoices(self, tx: Transaction) -> bool:
       t@@ -1550,7 +1541,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
        
            def get_unused_addresses(self) -> Sequence[str]:
                domain = self.get_receiving_addresses()
       -        in_use_by_request = [k for k in self.receive_requests.keys() if self.get_request_status(k)[0] != PR_EXPIRED]
       +        in_use_by_request = [k for k in self.receive_requests.keys() if self.get_request_status(k) != PR_EXPIRED] # we should index receive_requests by id
                return [addr for addr in domain if not self.is_used(addr)
                        and addr not in in_use_by_request]
        
       t@@ -1608,60 +1599,84 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
                        return True, conf
                return False, None
        
       -    def get_request_URI(self, addr):
       -        req = self.receive_requests[addr]
       +    def get_request_URI(self, req: Invoice):
       +        addr = req.get_address()
                message = self.labels.get(addr, '')
       -        amount = req['amount']
       +        amount = req.amount
                extra_query_params = {}
       -        if req.get('time'):
       -            extra_query_params['time'] = str(int(req.get('time')))
       -        if req.get('exp'):
       -            extra_query_params['exp'] = str(int(req.get('exp')))
       -        if req.get('name') and req.get('sig'):
       -            sig = bfh(req.get('sig'))
       -            sig = bitcoin.base_encode(sig, base=58)
       -            extra_query_params['name'] = req['name']
       -            extra_query_params['sig'] = sig
       +        if req.time:
       +            extra_query_params['time'] = str(int(req.time))
       +        if req.exp:
       +            extra_query_params['exp'] = str(int(req.exp))
       +        #if req.get('name') and req.get('sig'):
       +        #    sig = bfh(req.get('sig'))
       +        #    sig = bitcoin.base_encode(sig, base=58)
       +        #    extra_query_params['name'] = req['name']
       +        #    extra_query_params['sig'] = sig
                uri = create_bip21_uri(addr, amount, message, extra_query_params=extra_query_params)
                return str(uri)
        
       -    def get_request_status(self, address):
       -        r = self.receive_requests.get(address)
       +    def check_expired_status(self, r, status):
       +        if r.is_lightning() and r.exp == 0:
       +            status = PR_EXPIRED  # for BOLT-11 invoices, exp==0 means 0 seconds
       +        if status == PR_UNPAID and r.exp > 0 and r.time + r.exp < time.time():
       +            status = PR_EXPIRED
       +        return status
       +
       +    def get_invoice_status(self, invoice):
       +        if invoice.is_lightning():
       +            status = self.lnworker.get_invoice_status(invoice)
       +        else:
       +            status = PR_PAID if self.is_onchain_invoice_paid(invoice) else PR_UNPAID
       +        return self.check_expired_status(invoice, status)
       +
       +    def get_request_status(self, key):
       +        r = self.get_request(key)
                if r is None:
                    return PR_UNKNOWN
       -        amount = r.get('amount', 0) or 0
       -        timestamp = r.get('time', 0)
       -        if timestamp and type(timestamp) != int:
       -            timestamp = 0
       -        exp = r.get('exp', 0) or 0
       -        paid, conf = self.get_payment_status(address, amount)
       -        if not paid:
       -            if exp > 0 and time.time() > timestamp + exp:
       -                status = PR_EXPIRED
       -            else:
       -                status = PR_UNPAID
       +        if r.is_lightning():
       +            status = self.lnworker.get_payment_status(bfh(r.rhash))
                else:
       -            status = PR_PAID
       -        return status, conf
       +            paid, conf = self.get_payment_status(r.get_address(), r.amount)
       +            status = PR_PAID if paid else PR_UNPAID
       +        return self.check_expired_status(r, status)
        
            def get_request(self, key):
       -        req = self.receive_requests.get(key)
       -        if not req:
       -            return
       -        # convert StoredDict to dict
       -        req = dict(req)
       -        _type = req.get('type')
       -        if _type == PR_TYPE_ONCHAIN:
       -            addr = req['address']
       -            req['URI'] = self.get_request_URI(addr)
       -            status, conf = self.get_request_status(addr)
       -            req['status'] = status
       -            if conf is not None:
       -                req['confirmations'] = conf
       -        elif self.lnworker and _type == PR_TYPE_LN:
       -            req['status'] = self.lnworker.get_payment_status(bfh(key))
       +        return self.receive_requests.get(key)
       +
       +    def get_formatted_request(self, key):
       +        x = self.receive_requests.get(key)
       +        if x:
       +            return self.export_request(x)
       +
       +    def export_request(self, x):
       +        key = x.rhash if x.is_lightning() else x.get_address()
       +        status = self.get_request_status(key)
       +        status_str = x.get_status_str(status)
       +        is_lightning = x.is_lightning()
       +        d = {
       +            'is_lightning': is_lightning,
       +            'amount': x.amount,
       +            'amount_BTC': format_satoshis(x.amount),
       +            'message': x.message,
       +            'timestamp': x.time,
       +            'expiration': x.exp,
       +            'status': status,
       +            'status_str': status_str,
       +        }
       +        if is_lightning:
       +            d['rhash'] = x.rhash
       +            d['invoice'] = x.invoice
       +            if self.lnworker and status == PR_UNPAID:
       +                d['can_receive'] = self.lnworker.can_receive_invoice(x)
                else:
       -            return
       +            #key = x.id
       +            addr = x.get_address()
       +            paid, conf = self.get_payment_status(addr, x.amount)
       +            d['address'] = addr
       +            d['URI'] = self.get_request_URI(x)
       +            if conf is not None:
       +                d['confirmations'] = conf
                # add URL if we are running a payserver
                payserver = self.config.get_netaddress('payserver_address')
                if payserver:
       t@@ -1669,32 +1684,58 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
                    use_ssl = bool(self.config.get('ssl_keyfile'))
                    protocol = 'https' if use_ssl else 'http'
                    base = '%s://%s:%d'%(protocol, payserver.host, payserver.port)
       -            req['view_url'] = base + root + '/pay?id=' + key
       -            if use_ssl and 'URI' in req:
       +            d['view_url'] = base + root + '/pay?id=' + key
       +            if use_ssl and 'URI' in d:
                        request_url = base + '/bip70/' + key + '.bip70'
       -                req['bip70_url'] = request_url
       -        return req
       +                d['bip70_url'] = request_url
       +        return d
       +
       +    def export_invoice(self, x):
       +        status = self.get_invoice_status(x)
       +        status_str = x.get_status_str(status)
       +        is_lightning = x.is_lightning()
       +        d = {
       +            'is_lightning': is_lightning,
       +            'amount': x.amount,
       +            'amount_BTC': format_satoshis(x.amount),
       +            'message': x.message,
       +            'timestamp': x.time,
       +            'expiration': x.exp,
       +            'status': status,
       +            'status_str': status_str,
       +        }
       +        if is_lightning:
       +            d['invoice'] = x.invoice
       +            if status == PR_UNPAID:
       +                d['can_pay'] = self.lnworker.can_pay_invoice(x)
       +        else:
       +            d['outputs'] = [y.to_legacy_tuple() for y in x.outputs]
       +            if x.bip70:
       +                d['bip70'] = x.bip70
       +                d['requestor'] = x.requestor
       +        return d
        
            def receive_tx_callback(self, tx_hash, tx, tx_height):
                super().receive_tx_callback(tx_hash, tx, tx_height)
                for txo in tx.outputs():
                    addr = self.get_txout_address(txo)
                    if addr in self.receive_requests:
       -                status, conf = self.get_request_status(addr)
       +                status = self.get_request_status(addr)
                        util.trigger_callback('request_status', addr, status)
        
       -    def make_payment_request(self, addr, amount, message, expiration):
       +    def make_payment_request(self, address, amount, message, expiration):
                timestamp = int(time.time())
       -        _id = bh2u(sha256d(addr + "%d"%timestamp))[0:10]
       -        return {
       -            'type': PR_TYPE_ONCHAIN,
       -            'time':timestamp,
       -            'amount':amount,
       -            'exp':expiration,
       -            'address':addr,
       -            'memo':message,
       -            'id':_id,
       -        }
       +        _id = bh2u(sha256d(address + "%d"%timestamp))[0:10]
       +        return OnchainInvoice(
       +            type = PR_TYPE_ONCHAIN,
       +            outputs = [(TYPE_ADDRESS, address, amount)],
       +            message = message,
       +            time = timestamp,
       +            amount = amount,
       +            exp = expiration,
       +            id = _id,
       +            bip70 = None,
       +            requestor = None)
        
            def sign_payment_request(self, key, alias, alias_addr, password):
                req = self.receive_requests.get(key)
       t@@ -1706,20 +1747,17 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
                self.receive_requests[key] = req
        
            def add_payment_request(self, req):
       -        if req['type'] == PR_TYPE_ONCHAIN:
       -            addr = req['address']
       +        if not req.is_lightning():
       +            addr = req.get_address()
                    if not bitcoin.is_address(addr):
                        raise Exception(_('Invalid Bitcoin address.'))
                    if not self.is_mine(addr):
                        raise Exception(_('Address not in wallet.'))
                    key = addr
       -            message = req['memo']
       -        elif req['type'] == PR_TYPE_LN:
       -            key = req['rhash']
       -            message = req['message']
       +            message = req.message
                else:
       -            raise Exception('Unknown request type')
       -        amount = req.get('amount')
       +            key = req.rhash
       +        message = req.message
                self.receive_requests[key] = req
                self.set_label(key, message) # should be a default label
                return req
       t@@ -1748,7 +1786,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
                """ sorted by timestamp """
                out = [self.get_request(x) for x in self.receive_requests.keys()]
                out = [x for x in out if x is not None]
       -        out.sort(key=operator.itemgetter('time'))
       +        out.sort(key=lambda x: x.time)
                return out
        
            @abstractmethod
   DIR diff --git a/electrum/wallet_db.py b/electrum/wallet_db.py
       t@@ -32,7 +32,8 @@ from typing import Dict, Optional, List, Tuple, Set, Iterable, NamedTuple, Seque
        import binascii
        
        from . import util, bitcoin
       -from .util import profiler, WalletFileException, multisig_type, TxMinedInfo, bfh, PR_TYPE_ONCHAIN
       +from .util import profiler, WalletFileException, multisig_type, TxMinedInfo, bfh
       +from .invoices import PR_TYPE_ONCHAIN, invoice_from_json
        from .keystore import bip44_derivation
        from .transaction import Transaction, TxOutpoint, tx_from_any, PartialTransaction, PartialTxOutput
        from .logging import Logger
       t@@ -50,7 +51,7 @@ if TYPE_CHECKING:
        
        OLD_SEED_VERSION = 4        # electrum versions < 2.0
        NEW_SEED_VERSION = 11       # electrum versions >= 2.0
       -FINAL_SEED_VERSION = 28     # electrum >= 2.7 will set this to prevent
       +FINAL_SEED_VERSION = 29     # electrum >= 2.7 will set this to prevent
                                    # old versions from overwriting new format
        
        
       t@@ -174,6 +175,7 @@ class WalletDB(JsonDB):
                self._convert_version_26()
                self._convert_version_27()
                self._convert_version_28()
       +        self._convert_version_29()
                self.put('seed_version', FINAL_SEED_VERSION)  # just to be sure
        
                self._after_upgrade_tasks()
       t@@ -605,6 +607,41 @@ class WalletDB(JsonDB):
                    c['local_config']['channel_seed'] = None
                self.data['seed_version'] = 28
        
       +    def _convert_version_29(self):
       +        if not self._is_upgrade_method_needed(28, 28):
       +            return
       +        requests = self.data.get('payment_requests', {})
       +        invoices = self.data.get('invoices', {})
       +        for d in [invoices, requests]:
       +            for key, r in list(d.items()):
       +                _type = r.get('type', 0)
       +                item = {
       +                    'type': _type,
       +                    'message': r.get('message') or r.get('memo', ''),
       +                    'amount': r.get('amount'),
       +                    'exp': r.get('exp', 0),
       +                    'time': r.get('time', 0),
       +                }
       +                if _type == PR_TYPE_ONCHAIN:
       +                    address = r.pop('address', None)
       +                    if address:
       +                        outputs = [(0, address, r.get('amount'))]
       +                    else:
       +                        outputs = r.get('outputs')
       +                    item.update({
       +                        'outputs': outputs,
       +                        'id': r.get('id'),
       +                        'bip70': r.get('bip70'),
       +                        'requestor': r.get('requestor'),
       +                    })
       +                else:
       +                    item.update({
       +                        'rhash': r['rhash'],
       +                        'invoice': r['invoice'],
       +                    })
       +                d[key] = item
       +        self.data['seed_version'] = 29
       +
            def _convert_imported(self):
                if not self._is_upgrade_method_needed(0, 13):
                    return
       t@@ -1072,15 +1109,6 @@ class WalletDB(JsonDB):
                        if spending_txid not in self.transactions:
                            self.logger.info("removing unreferenced spent outpoint")
                            d.pop(prevout_n)
       -        # convert invoices
       -        # TODO invoices being these contextual dicts even internally,
       -        #      where certain keys are only present depending on values of other keys...
       -        #      it's horrible. we need to change this, at least for the internal representation,
       -        #      to something that can be typed.
       -        self.invoices = self.get_dict('invoices')
       -        for invoice_key, invoice in self.invoices.items():
       -            if invoice.get('type') == PR_TYPE_ONCHAIN:
       -                invoice['outputs'] = [PartialTxOutput.from_legacy_tuple(*output) for output in invoice.get('outputs')]
        
            @modifier
            def clear_history(self):
       t@@ -1097,6 +1125,10 @@ class WalletDB(JsonDB):
                if key == 'transactions':
                    # note: for performance, "deserialize=False" so that we will deserialize these on-demand
                    v = dict((k, tx_from_any(x, deserialize=False)) for k, x in v.items())
       +        if key == 'invoices':
       +            v = dict((k, invoice_from_json(x)) for k, x in v.items())
       +        if key == 'payment_requests':
       +            v = dict((k, invoice_from_json(x)) for k, x in v.items())
                elif key == 'adds':
                    v = dict((k, UpdateAddHtlc.from_tuple(*x)) for k, x in v.items())
                elif key == 'fee_updates':