URI: 
       tRestructure invoices and requests (WIP) - Terminology: use 'invoices' for outgoing payments, 'requests' for incoming payments - At the GUI level, try to handle invoices in a generic way. - Display ongoing payments in send tab. - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit a50f935aecc1a47dffce195808ed3b1839c8e3a0
   DIR parent 3902d774f7ced2311ab07e5309dac2eeff2af0c7
  HTML Author: ThomasV <thomasv@electrum.org>
       Date:   Sun, 11 Aug 2019 14:47:06 +0200
       
       Restructure invoices and requests (WIP)
        - Terminology: use 'invoices' for outgoing payments, 'requests' for incoming payments
        - At the GUI level, try to handle invoices in a generic way.
        - Display ongoing payments in send tab.
       
       Diffstat:
         M electrum/gui/kivy/main_window.py    |      47 +++++++++++++++++++++++--------
         A electrum/gui/kivy/uix/dialogs/invo… |      97 ++++++++++++++++++++++++++++++
         M electrum/gui/kivy/uix/screens.py    |     183 ++++++++++++++++++-------------
         M electrum/gui/kivy/uix/ui_screens/s… |     108 ++++++++++++++++++++++---------
         M electrum/gui/qt/history_list.py     |       6 +++---
         M electrum/gui/qt/invoice_list.py     |      80 +++++++++++++------------------
         M electrum/gui/qt/main_window.py      |      74 ++++++++++++++-----------------
         M electrum/gui/qt/request_list.py     |       9 ++++++---
         M electrum/lnchannel.py               |      13 ++++++++-----
         M electrum/lnworker.py                |     117 ++++++++++++++++++++++---------
         M electrum/paymentrequest.py          |     104 +++----------------------------
         M electrum/util.py                    |      19 +++++++++++++------
         M electrum/wallet.py                  |      65 ++++++++++++++++++++++++++++---
       
       13 files changed, 563 insertions(+), 359 deletions(-)
       ---
   DIR diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py
       t@@ -11,11 +11,10 @@ import asyncio
        from electrum.bitcoin import TYPE_ADDRESS
        from electrum.storage import WalletStorage
        from electrum.wallet import Wallet, InternalAddressCorruption
       -from electrum.paymentrequest import InvoiceStore
        from electrum.util import profiler, InvalidPassword, send_exception_to_crash_reporter
        from electrum.plugin import run_hook
        from electrum.util import format_satoshis, format_satoshis_plain, format_fee_satoshis
       -from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED
       +from electrum.util import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED
        from electrum import blockchain
        from electrum.network import Network, TxBroadcastError, BestEffortRequestFailed
        from .i18n import _
       t@@ -201,6 +200,19 @@ class ElectrumWindow(App):
                if status == PR_PAID:
                    self.show_info(_('Payment Received') + '\n' + key)
        
       +    def on_payment_status(self, event, key, status, *args):
       +        self.update_tab('send')
       +        if status == 'success':
       +            self.show_info(_('Payment was sent'))
       +            self._trigger_update_history()
       +        elif status == 'progress':
       +            pass
       +        elif status == 'failure':
       +            self.show_info(_('Payment failed'))
       +        elif status == 'error':
       +            e = args[0]
       +            self.show_error(_('Error') + '\n' + str(e))
       +
            def _get_bu(self):
                decimal_point = self.electrum_config.get('decimal_point', DECIMAL_POINT_DEFAULT)
                try:
       t@@ -343,19 +355,16 @@ class ElectrumWindow(App):
                    self.show_error(_('No wallet loaded.'))
                    return
                if pr.verify(self.wallet.contacts):
       -            key = self.wallet.invoices.add(pr)
       -            if self.invoices_screen:
       -                self.invoices_screen.update()
       -            status = self.wallet.invoices.get_status(key)
       -            if status == PR_PAID:
       +            key = pr.get_id()
       +            invoice = self.wallet.get_invoice(key)
       +            if invoice and invoice['status'] == PR_PAID:
                        self.show_error("invoice already paid")
                        self.send_screen.do_clear()
       +            elif pr.has_expired():
       +                self.show_error(_('Payment request has expired'))
                    else:
       -                if pr.has_expired():
       -                    self.show_error(_('Payment request has expired'))
       -                else:
       -                    self.switch_to('send')
       -                    self.send_screen.set_request(pr)
       +                self.switch_to('send')
       +                self.send_screen.set_request(pr)
                else:
                    self.show_error("invoice error:" + pr.error)
                    self.send_screen.do_clear()
       t@@ -418,6 +427,19 @@ class ElectrumWindow(App):
                self.request_popup.set_status(status)
                self.request_popup.open()
        
       +    def show_invoice(self, is_lightning, key):
       +        from .uix.dialogs.invoice_dialog import InvoiceDialog
       +        invoice = self.wallet.get_invoice(key)
       +        if not invoice:
       +            return
       +        status = invoice['status']
       +        if is_lightning:
       +            data = invoice['invoice']
       +        else:
       +            data = key
       +        self.invoice_popup = InvoiceDialog('Invoice', data, key)
       +        self.invoice_popup.open()
       +
            def qr_dialog(self, title, data, show_text=False, text_for_clipboard=None):
                from .uix.dialogs.qr_dialog import QRDialog
                def on_qr_failure():
       t@@ -519,6 +541,7 @@ class ElectrumWindow(App):
                    self.network.register_callback(self.on_payment_received, ['payment_received'])
                    self.network.register_callback(self.on_channels, ['channels'])
                    self.network.register_callback(self.on_channel, ['channel'])
       +            self.network.register_callback(self.on_payment_status, ['payment_status'])
                # load wallet
                self.load_wallet_by_name(self.electrum_config.get_wallet_path())
                # URI passed in config
   DIR diff --git a/electrum/gui/kivy/uix/dialogs/invoice_dialog.py b/electrum/gui/kivy/uix/dialogs/invoice_dialog.py
       t@@ -0,0 +1,97 @@
       +from kivy.factory import Factory
       +from kivy.lang import Builder
       +from kivy.core.clipboard import Clipboard
       +from kivy.app import App
       +from kivy.clock import Clock
       +
       +from electrum.gui.kivy.i18n import _
       +from electrum.util import pr_tooltips
       +
       +
       +Builder.load_string('''
       +<InvoiceDialog@Popup>
       +    id: popup
       +    title: ''
       +    data: ''
       +    status: 'unknown'
       +    shaded: False
       +    show_text: False
       +    AnchorLayout:
       +        anchor_x: 'center'
       +        BoxLayout:
       +            orientation: 'vertical'
       +            size_hint: 1, 1
       +            padding: '10dp'
       +            spacing: '10dp'
       +            TopLabel:
       +                text: root.data
       +            TopLabel:
       +                text: _('Status') + ': ' + root.status
       +            Widget:
       +                size_hint: 1, 0.2
       +            BoxLayout:
       +                size_hint: 1, None
       +                height: '48dp'
       +                Button:
       +                    size_hint: 1, None
       +                    height: '48dp'
       +                    text: _('Delete')
       +                    on_release: root.delete_dialog()
       +                IconButton:
       +                    icon: 'atlas://electrum/gui/kivy/theming/light/copy'
       +                    size_hint: 0.5, None
       +                    height: '48dp'
       +                    on_release: root.copy_to_clipboard()
       +                IconButton:
       +                    icon: 'atlas://electrum/gui/kivy/theming/light/share'
       +                    size_hint: 0.5, None
       +                    height: '48dp'
       +                    on_release: root.do_share()
       +                Button:
       +                    size_hint: 1, None
       +                    height: '48dp'
       +                    text: _('Pay')
       +                    on_release: root.do_pay()
       +''')
       +
       +class InvoiceDialog(Factory.Popup):
       +
       +    def __init__(self, title, data, key):
       +        Factory.Popup.__init__(self)
       +        self.app = App.get_running_app()
       +        self.title = title
       +        self.data = data
       +        self.key = key
       +
       +    #def on_open(self):
       +    #    self.ids.qr.set_data(self.data)
       +
       +    def set_status(self, status):
       +        self.status = pr_tooltips[status]
       +
       +    def on_dismiss(self):
       +        self.app.request_popup = None
       +
       +    def copy_to_clipboard(self):
       +        Clipboard.copy(self.data)
       +        msg = _('Text copied to clipboard.')
       +        Clock.schedule_once(lambda dt: self.app.show_info(msg))
       +
       +    def do_share(self):
       +        self.app.do_share(self.data, _("Share Invoice"))
       +        self.dismiss()
       +
       +    def do_pay(self):
       +        invoice = self.app.wallet.get_invoice(self.key)
       +        self.app.send_screen.do_pay_invoice(invoice)
       +        self.dismiss()
       +
       +    def delete_dialog(self):
       +        from .question import Question
       +        def cb(result):
       +            if result:
       +                self.app.wallet.delete_invoice(self.key)
       +                self.dismiss()
       +                self.app.send_screen.update()
       +        d = Question(_('Delete invoice?'), cb)
       +        d.open()
   DIR diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py
       t@@ -4,7 +4,6 @@ from decimal import Decimal
        import re
        import threading
        import traceback, sys
       -from enum import Enum, auto
        
        from kivy.app import App
        from kivy.cache import Cache
       t@@ -23,6 +22,7 @@ from kivy.factory import Factory
        from kivy.utils import platform
        
        from electrum.util import profiler, parse_URI, format_time, InvalidPassword, NotEnoughFunds, Fiat
       +from electrum.util import PR_TYPE_ADDRESS, PR_TYPE_LN, PR_TYPE_BIP70
        from electrum import bitcoin, constants
        from electrum.transaction import TxOutput, Transaction, tx_from_str
        from electrum.util import send_exception_to_crash_reporter, parse_URI, InvalidBitcoinURI
       t@@ -38,10 +38,6 @@ from .dialogs.lightning_open_channel import LightningOpenChannelDialog
        
        from electrum.gui.kivy.i18n import _
        
       -class Destination(Enum):
       -    Address = auto()
       -    PR = auto()
       -    LN = auto()
        
        class HistoryRecycleView(RecycleView):
            pass
       t@@ -49,6 +45,9 @@ class HistoryRecycleView(RecycleView):
        class RequestRecycleView(RecycleView):
            pass
        
       +class PaymentRecycleView(RecycleView):
       +    pass
       +
        class CScreen(Factory.Screen):
            __events__ = ('on_activate', 'on_deactivate', 'on_enter', 'on_leave')
            action_view = ObjectProperty(None)
       t@@ -119,14 +118,12 @@ class HistoryScreen(CScreen):
                super(HistoryScreen, self).__init__(**kwargs)
        
            def show_item(self, obj):
       -        print(obj)
                key = obj.key
                tx = self.app.wallet.db.get_transaction(key)
                if not tx:
                    return
                self.app.tx_dialog(tx)
        
       -
            def get_card(self, tx_item): #tx_hash, tx_mined_status, value, balance):
                is_lightning = tx_item.get('lightning', False)
                timestamp = tx_item['timestamp']
       t@@ -192,7 +189,7 @@ class SendScreen(CScreen):
                self.screen.message = uri.get('message', '')
                self.screen.amount = self.app.format_amount_and_units(amount) if amount else ''
                self.payment_request = None
       -        self.screen.destinationtype = Destination.Address
       +        self.screen.destinationtype = PR_TYPE_ADDRESS
        
            def set_ln_invoice(self, invoice):
                try:
       t@@ -204,19 +201,47 @@ class SendScreen(CScreen):
                self.screen.message = dict(lnaddr.tags).get('d', None)
                self.screen.amount = self.app.format_amount_and_units(lnaddr.amount * bitcoin.COIN) if lnaddr.amount else ''
                self.payment_request = None
       -        self.screen.destinationtype = Destination.LN
       +        self.screen.destinationtype = PR_TYPE_LN
        
            def update(self):
       +        if not self.loaded:
       +            return
                if self.app.wallet and self.payment_request_queued:
                    self.set_URI(self.payment_request_queued)
                    self.payment_request_queued = None
       +        _list = self.app.wallet.get_invoices()
       +        payments_container = self.screen.ids.payments_container
       +        payments_container.data = [self.get_card(item) for item in _list if item['status'] != PR_PAID]
       +
       +    def show_item(self, obj):
       +        self.app.show_invoice(obj.is_lightning, obj.key)
       +
       +    def get_card(self, item):
       +        invoice_type = item['type']
       +        if invoice_type == PR_TYPE_LN:
       +            key = item['rhash']
       +            status = get_request_status(item) # convert to str
       +        elif invoice_type == PR_TYPE_BIP70:
       +            key = item['id']
       +            status = get_request_status(item) # convert to str
       +        elif invoice_type == PR_TYPE_ADDRESS:
       +            key = item['address']
       +            status = get_request_status(item) # convert to str
       +        return {
       +            'is_lightning': invoice_type == PR_TYPE_LN,
       +            'screen': self,
       +            'status': status,
       +            'key': key,
       +            'memo': item['message'],
       +            'amount': self.app.format_amount_and_units(item['amount'] or 0),
       +        }
        
            def do_clear(self):
                self.screen.amount = ''
                self.screen.message = ''
                self.screen.address = ''
                self.payment_request = None
       -        self.screen.destinationtype = Destination.Address
       +        self.screen.destinationtype = PR_TYPE_ADDRESS
        
            def set_request(self, pr):
                self.screen.address = pr.get_requestor()
       t@@ -224,32 +249,10 @@ class SendScreen(CScreen):
                self.screen.amount = self.app.format_amount_and_units(amount) if amount else ''
                self.screen.message = pr.get_memo()
                if pr.is_pr():
       -            self.screen.destinationtype = Destination.PR
       -            self.payment_request = pr
       -        else:
       -            self.screen.destinationtype = Destination.Address
       -            self.payment_request = None
       -
       -    def save_invoice(self):
       -        if not self.screen.address:
       -            return
       -        if self.screen.destinationtype == Destination.PR:
       -            # it should be already saved
       -            return
       -        # save address as invoice
       -        from electrum.paymentrequest import make_unsigned_request, PaymentRequest
       -        req = {'address':self.screen.address, 'memo':self.screen.message}
       -        amount = self.app.get_amount(self.screen.amount) if self.screen.amount else 0
       -        req['amount'] = amount
       -        pr = make_unsigned_request(req).SerializeToString()
       -        pr = PaymentRequest(pr)
       -        self.app.wallet.invoices.add(pr)
       -        #self.app.show_info(_("Invoice saved"))
       -        if pr.is_pr():
       -            self.screen.destinationtype = Destination.PR
       +            self.screen.destinationtype = PR_TYPE_BIP70
                    self.payment_request = pr
                else:
       -            self.screen.destinationtype = Destination.Address
       +            self.screen.destinationtype = PR_TYPE_ADDRESS
                    self.payment_request = None
        
            def do_paste(self):
       t@@ -275,63 +278,87 @@ class SendScreen(CScreen):
                    self.set_ln_invoice(lower)
                else:
                    self.set_URI(data)
       -            # save automatically
       -            self.save_invoice()
        
       -    def _do_send_lightning(self):
       +    def read_invoice(self):
       +        address = str(self.screen.address)
       +        if not address:
       +            self.app.show_error(_('Recipient not specified.') + ' ' + _('Please scan a Bitcoin address or a payment request'))
       +            return
                if not self.screen.amount:
       -            self.app.show_error(_('Since the invoice contained no amount, you must enter one'))
       +            self.app.show_error(_('Please enter an amount'))
                    return
       -        invoice = self.screen.address
       -        amount_sat = self.app.get_amount(self.screen.amount)
       -        threading.Thread(target=self._lnpay_thread, args=(invoice, amount_sat)).start()
       -
       -    def _lnpay_thread(self, invoice, amount_sat):
       -        self.do_clear()
       -        self.app.show_info(_('Payment in progress..'))
                try:
       -            success = self.app.wallet.lnworker.pay(invoice, attempts=10, amount_sat=amount_sat, timeout=60)
       -        except PaymentFailure as e:
       -            self.app.show_error(_('Payment failure') + '\n' + str(e))
       -            return
       -        if success:
       -            self.app.show_info(_('Payment was sent'))
       -            self.app._trigger_update_history()
       -        else:
       -            self.app.show_error(_('Payment failed'))
       -
       -    def do_send(self):
       -        if self.screen.destinationtype == Destination.LN:
       -            self._do_send_lightning()
       +            amount = self.app.get_amount(self.screen.amount)
       +        except:
       +            self.app.show_error(_('Invalid amount') + ':\n' + self.screen.amount)
                    return
       -        elif self.screen.destinationtype == Destination.PR:
       -            if self.payment_request.has_expired():
       -                self.app.show_error(_('Payment request has expired'))
       -                return
       -            outputs = self.payment_request.get_outputs()
       -        else:
       -            address = str(self.screen.address)
       -            if not address:
       -                self.app.show_error(_('Recipient not specified.') + ' ' + _('Please scan a Bitcoin address or a payment request'))
       -                return
       +        message = self.screen.message
       +        if self.screen.destinationtype == PR_TYPE_LN:
       +            return {
       +                'type': PR_TYPE_LN,
       +                'invoice': address,
       +                'amount': amount,
       +                'message': message,
       +            }
       +        elif self.screen.destinationtype == PR_TYPE_ADDRESS:
                    if not bitcoin.is_address(address):
                        self.app.show_error(_('Invalid Bitcoin Address') + ':\n' + address)
                        return
       -            try:
       -                amount = self.app.get_amount(self.screen.amount)
       -            except:
       -                self.app.show_error(_('Invalid amount') + ':\n' + self.screen.amount)
       +            return {
       +                'type': PR_TYPE_ADDRESS,
       +                'address': address,
       +                'amount': amount,
       +                'message': message,
       +            }
       +        elif self.screen.destinationtype == PR_TYPE_BIP70:
       +            if self.payment_request.has_expired():
       +                self.app.show_error(_('Payment request has expired'))
                        return
       +            return self.payment_request.get_dict()
       +        else:
       +            raise Exception('Unknown invoice type')
       +
       +    def do_save(self):
       +        invoice = self.read_invoice()
       +        if not invoice:
       +            return
       +        self.app.wallet.save_invoice(invoice)
       +        self.do_clear()
       +        self.update()
       +
       +    def do_pay(self):
       +        invoice = self.read_invoice()
       +        if not invoice:
       +            return
       +        self.app.wallet.save_invoice(invoice)
       +        self.do_clear()
       +        self.update()
       +        self.do_pay_invoice(invoice)
       +
       +    def do_pay_invoice(self, invoice):
       +        if invoice['type'] == PR_TYPE_LN:
       +            self._do_send_lightning(invoice['invoice'], invoice['amount'])
       +            return
       +        elif invoice['type'] == PR_TYPE_ADDRESS:
       +            address = invoice['address']
       +            amount = invoice['amount']
       +            message = invoice['message']
                    outputs = [TxOutput(bitcoin.TYPE_ADDRESS, address, amount)]
       -        message = self.screen.message
       -        amount = sum(map(lambda x:x[2], outputs))
       +        elif invoice['type'] == PR_TYPE_BIP70:
       +            outputs = invoice['outputs']
       +            amount = sum(map(lambda x:x[2], outputs))
       +        # onchain payment
                if self.app.electrum_config.get('use_rbf'):
       -            d = Question(_('Should this transaction be replaceable?'), lambda b: self._do_send(amount, message, outputs, b))
       +            d = Question(_('Should this transaction be replaceable?'), lambda b: self._do_send_onchain(amount, message, outputs, b))
                    d.open()
                else:
       -            self._do_send(amount, message, outputs, False)
       +            self._do_send_onchain(amount, message, outputs, False)
       +
       +    def _do_send_lightning(self, invoice, amount):
       +        attempts = 10
       +        threading.Thread(target=self.app.wallet.lnworker.pay, args=(invoice, amount, attempts)).start()
        
       -    def _do_send(self, amount, message, outputs, rbf):
       +    def _do_send_onchain(self, amount, message, outputs, rbf):
                # make unsigned transaction
                config = self.app.electrum_config
                coins = self.app.wallet.get_spendable_coins(None, config)
       t@@ -447,7 +474,7 @@ class ReceiveScreen(CScreen):
                self.app.show_request(lightning, key)
        
            def get_card(self, req):
       -        is_lightning = req.get('lightning', False)
       +        is_lightning = req.get('type') == PR_TYPE_LN
                if not is_lightning:
                    address = req['address']
                    key = address
   DIR diff --git a/electrum/gui/kivy/uix/ui_screens/send.kv b/electrum/gui/kivy/uix/ui_screens/send.kv
       t@@ -1,10 +1,66 @@
        #:import _ electrum.gui.kivy.i18n._
       -#:import Destination electrum.gui.kivy.uix.screens.Destination
       +#:import Factory kivy.factory.Factory
       +#:import PR_TYPE_ADDRESS electrum.util.PR_TYPE_ADDRESS
       +#:import PR_TYPE_LN electrum.util.PR_TYPE_LN
       +#:import PR_TYPE_BIP70 electrum.util.PR_TYPE_BIP70
        #:import Decimal decimal.Decimal
        #:set btc_symbol chr(171)
        #:set mbtc_symbol chr(187)
        #:set font_light 'electrum/gui/kivy/data/fonts/Roboto-Condensed.ttf'
        
       +<PaymentLabel@Label>
       +    #color: .305, .309, .309, 1
       +    text_size: self.width, None
       +    halign: 'left'
       +    valign: 'top'
       +
       +<PaymentItem@CardItem>
       +    key: ''
       +    memo: ''
       +    amount: ''
       +    status: ''
       +    date: ''
       +    BoxLayout:
       +        spacing: '8dp'
       +        height: '32dp'
       +        orientation: 'vertical'
       +        Widget
       +        PaymentLabel:
       +            text: root.memo
       +            shorten: True
       +            shorten_from: 'right'
       +        Widget
       +        PaymentLabel:
       +            text: root.key
       +            color: .699, .699, .699, 1
       +            font_size: '13sp'
       +            shorten: True
       +        Widget
       +    BoxLayout:
       +        spacing: '8dp'
       +        height: '32dp'
       +        orientation: 'vertical'
       +        Widget
       +        PaymentLabel:
       +            text: root.amount
       +            halign: 'right'
       +            font_size: '15sp'
       +        Widget
       +        PaymentLabel:
       +            text: root.status
       +            halign: 'right'
       +            font_size: '13sp'
       +            color: .699, .699, .699, 1
       +        Widget
       +
       +<PaymentRecycleView>:
       +    viewclass: 'PaymentItem'
       +    RecycleBoxLayout:
       +        default_size: None, dp(56)
       +        default_size_hint: 1, None
       +        size_hint: 1, None
       +        height: self.minimum_height
       +        orientation: 'vertical'
        
        SendScreen:
            id: s
       t@@ -12,7 +68,7 @@ SendScreen:
            address: ''
            amount: ''
            message: ''
       -    destinationtype: Destination.Address
       +    destinationtype: PR_TYPE_ADDRESS
            BoxLayout
                padding: '12dp', '12dp', '12dp', '12dp'
                spacing: '12dp'
       t@@ -26,7 +82,7 @@ SendScreen:
                        height: blue_bottom.item_height
                        spacing: '5dp'
                        Image:
       -                    source: 'atlas://electrum/gui/kivy/theming/light/globe'
       +                    source: 'atlas://electrum/gui/kivy/theming/light/globe' if root.destinationtype != PR_TYPE_LN else 'atlas://electrum/gui/kivy/theming/light/lightning'
                            size_hint: None, None
                            size: '22dp', '22dp'
                            pos_hint: {'center_y': .5}
       t@@ -37,7 +93,7 @@ SendScreen:
                            on_release: Clock.schedule_once(lambda dt: app.show_info(_('Copy and paste the recipient address using the Paste button, or use the camera to scan a QR code.')))
                            #on_release: Clock.schedule_once(lambda dt: app.popup_dialog('contacts'))
                    CardSeparator:
       -                opacity: int(root.destinationtype == Destination.Address)
       +                opacity: int(root.destinationtype == PR_TYPE_ADDRESS)
                        color: blue_bottom.foreground_color
                    BoxLayout:
                        size_hint: 1, None
       t@@ -53,10 +109,10 @@ SendScreen:
                            id: amount_e
                            default_text: _('Amount')
                            text: s.amount if s.amount else _('Amount')
       -                    disabled: root.destinationtype == Destination.PR or root.destinationtype == Destination.LN and not s.amount
       +                    disabled: root.destinationtype == PR_TYPE_BIP70 or root.destinationtype == PR_TYPE_LN and not s.amount
                            on_release: Clock.schedule_once(lambda dt: app.amount_dialog(s, True))
                    CardSeparator:
       -                opacity: int(root.destinationtype == Destination.Address)
       +                opacity: int(root.destinationtype == PR_TYPE_ADDRESS)
                        color: blue_bottom.foreground_color
                    BoxLayout:
                        id: message_selection
       t@@ -70,37 +126,40 @@ SendScreen:
                            pos_hint: {'center_y': .5}
                        BlueButton:
                            id: description
       -                    text: s.message if s.message else ({Destination.LN: _('No description'), Destination.Address: _('Description'), Destination.PR: _('No Description')}[root.destinationtype])
       -                    disabled: root.destinationtype != Destination.Address
       +                    text: s.message if s.message else ({PR_TYPE_LN: _('No description'), PR_TYPE_ADDRESS: _('Description'), PR_TYPE_BIP70: _('No Description')}[root.destinationtype])
       +                    disabled: root.destinationtype != PR_TYPE_ADDRESS
                            on_release: Clock.schedule_once(lambda dt: app.description_dialog(s))
                    CardSeparator:
       -                opacity: int(root.destinationtype == Destination.Address)
       +                opacity: int(root.destinationtype == PR_TYPE_ADDRESS)
                        color: blue_bottom.foreground_color
                    BoxLayout:
                        size_hint: 1, None
       -                height: blue_bottom.item_height if root.destinationtype != Destination.LN else 0
       +                height: blue_bottom.item_height
                        spacing: '5dp'
                        Image:
                            source: 'atlas://electrum/gui/kivy/theming/light/star_big_inactive'
       -                    opacity: 0.7 if root.destinationtype != Destination.LN else 0
                            size_hint: None, None
                            size: '22dp', '22dp'
                            pos_hint: {'center_y': .5}
                        BlueButton:
                            id: fee_e
                            default_text: _('Fee')
       -                    text: app.fee_status if root.destinationtype != Destination.LN else ''
       -                    on_release: Clock.schedule_once(lambda dt: app.fee_dialog(s, True)) if root.destinationtype != Destination.LN else None
       +                    text: app.fee_status if root.destinationtype != PR_TYPE_LN else ''
       +                    on_release: Clock.schedule_once(lambda dt: app.fee_dialog(s, True)) if root.destinationtype != PR_TYPE_LN else None
                BoxLayout:
                    size_hint: 1, None
                    height: '48dp'
                    IconButton:
                        size_hint: 0.5, 1
       +                on_release: s.parent.do_save()
       +                icon: 'atlas://electrum/gui/kivy/theming/light/save'
       +            IconButton:
       +                size_hint: 0.5, 1
                        icon: 'atlas://electrum/gui/kivy/theming/light/copy'
                        on_release: s.parent.do_paste()
                    IconButton:
                        id: qr
       -                size_hint: 0.5, 1
       +                size_hint: 1, 1
                        on_release: Clock.schedule_once(lambda dt: app.scan_qr(on_complete=app.on_qr))
                        icon: 'atlas://electrum/gui/kivy/theming/light/camera'
                    Button:
       t@@ -110,19 +169,10 @@ SendScreen:
                    Button:
                        text: _('Pay')
                        size_hint: 1, 1
       -                on_release: s.parent.do_send()
       +                on_release: s.parent.do_pay()
                Widget:
       -            size_hint: 1, 1
       -        #BoxLayout:
       -        #    size_hint: 1, None
       -        #    height: '48dp'
       -            #IconButton:
       -            #    size_hint: 0.5, 1
       -            #    on_release: s.parent.do_save()
       -            #    icon: 'atlas://electrum/gui/kivy/theming/light/save'
       -            #IconButton:
       -            #    size_hint: 0.5, 1
       -            #    icon: 'atlas://electrum/gui/kivy/theming/light/list'
       -            #    on_release: Clock.schedule_once(lambda dt: app.invoices_dialog(s))
       -            #Widget:
       -            #    size_hint: 2.5, 1
       +            size_hint: 1, 0.1
       +        PaymentRecycleView:
       +            id: payments_container
       +            scroll_type: ['bars', 'content']
       +            bar_width: '25dp'
   DIR diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py
       t@@ -196,9 +196,9 @@ class HistoryModel(QAbstractItemModel, Logger):
                    elif col != HistoryColumns.STATUS and role == Qt.FontRole:
                        monospace_font = QFont(MONOSPACE_FONT)
                        return QVariant(monospace_font)
       -            elif col == HistoryColumns.DESCRIPTION and role == Qt.DecorationRole and not is_lightning\
       -                    and self.parent.wallet.invoices.paid.get(tx_hash):
       -                return QVariant(read_QIcon("seal"))
       +            #elif col == HistoryColumns.DESCRIPTION and role == Qt.DecorationRole and not is_lightning\
       +            #        and self.parent.wallet.invoices.paid.get(tx_hash):
       +            #    return QVariant(read_QIcon("seal"))
                    elif col in (HistoryColumns.DESCRIPTION, HistoryColumns.AMOUNT) \
                            and role == Qt.ForegroundRole and not is_lightning and tx_item['value'].value < 0:
                        red_brush = QBrush(QColor("#BC1E1E"))
   DIR diff --git a/electrum/gui/qt/invoice_list.py b/electrum/gui/qt/invoice_list.py
       t@@ -30,22 +30,20 @@ from PyQt5.QtGui import QStandardItemModel, QStandardItem, QFont
        from PyQt5.QtWidgets import QHeaderView, QMenu
        
        from electrum.i18n import _
       -from electrum.util import format_time, pr_tooltips, PR_UNPAID
       +from electrum.util import format_time, PR_UNPAID, PR_PAID, get_request_status
       +from electrum.util import PR_TYPE_ADDRESS, PR_TYPE_LN, PR_TYPE_BIP70
        from electrum.lnutil import lndecode, RECEIVED
        from electrum.bitcoin import COIN
        from electrum import constants
        
       -from .util import (MyTreeView, read_QIcon, MONOSPACE_FONT, PR_UNPAID,
       +from .util import (MyTreeView, read_QIcon, MONOSPACE_FONT,
                           import_meta_gui, export_meta_gui, pr_icons)
        
        
       -REQUEST_TYPE_BITCOIN = 0
       -REQUEST_TYPE_LN = 1
        
        ROLE_REQUEST_TYPE = Qt.UserRole
        ROLE_REQUEST_ID = Qt.UserRole + 1
        
       -from electrum.paymentrequest import PR_PAID
        
        class InvoiceList(MyTreeView):
        
       t@@ -56,7 +54,7 @@ class InvoiceList(MyTreeView):
                STATUS = 3
        
            headers = {
       -        Columns.DATE: _('Expires'),
       +        Columns.DATE: _('Date'),
                Columns.DESCRIPTION: _('Description'),
                Columns.AMOUNT: _('Amount'),
                Columns.STATUS: _('Status'),
       t@@ -72,48 +70,38 @@ class InvoiceList(MyTreeView):
                self.update()
        
            def update(self):
       -        inv_list = self.parent.invoices.unpaid_invoices()
       +        _list = self.parent.wallet.get_invoices()
                self.model().clear()
                self.update_headers(self.__class__.headers)
       -        for idx, pr in enumerate(inv_list):
       -            key = pr.get_id()
       -            status = self.parent.invoices.get_status(key)
       -            if status is None:
       -                continue
       -            requestor = pr.get_requestor()
       -            exp = pr.get_time()
       -            date_str = format_time(exp) if exp else _('Never')
       -            labels = [date_str, '[%s] '%requestor + pr.memo, self.parent.format_amount(pr.get_amount(), whitespaces=True), pr_tooltips.get(status,'')]
       +        for idx, item in enumerate(_list):
       +            invoice_type = item['type']
       +            if invoice_type == PR_TYPE_LN:
       +                key = item['rhash']
       +                icon_name = 'lightning.png'
       +            elif invoice_type == PR_TYPE_ADDRESS:
       +                key = item['address']
       +                icon_name = 'bitcoin.png'
       +            elif invoice_type == PR_TYPE_BIP70:
       +                key = item['id']
       +                icon_name = 'seal.png'
       +            else:
       +                raise Exception('Unsupported type')
       +            status = item['status']
       +            status_str = get_request_status(item) # convert to str
       +            message = item['message']
       +            amount = item['amount']
       +            timestamp = item.get('time', 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]
                    items = [QStandardItem(e) for e in labels]
                    self.set_editability(items)
       -            items[self.Columns.DATE].setIcon(read_QIcon('bitcoin.png'))
       +            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(REQUEST_TYPE_BITCOIN, role=ROLE_REQUEST_TYPE)
       +            items[self.Columns.DATE].setData(invoice_type, role=ROLE_REQUEST_TYPE)
                    self.model().insertRow(idx, items)
        
       -        lnworker = self.parent.wallet.lnworker
       -        items = list(lnworker.invoices.items()) if lnworker else []
       -        for key, (invoice, direction, is_paid) in items:
       -            if direction == RECEIVED:
       -                continue
       -            status = lnworker.get_invoice_status(key)
       -            if status == PR_PAID:
       -                continue
       -            lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
       -            amount_sat = lnaddr.amount*COIN if lnaddr.amount else None
       -            amount_str = self.parent.format_amount(amount_sat) if amount_sat else ''
       -            description = lnaddr.get_description()
       -            date_str = format_time(lnaddr.date)
       -            labels = [date_str, description, amount_str, pr_tooltips.get(status,'')]
       -            items = [QStandardItem(e) for e in labels]
       -            self.set_editability(items)
       -            items[self.Columns.DATE].setIcon(read_QIcon('lightning.png'))
       -            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(REQUEST_TYPE_LN, role=ROLE_REQUEST_TYPE)
       -            self.model().insertRow(self.model().rowCount(), items)
       -
                self.selectionModel().select(self.model().index(0,0), QItemSelectionModel.SelectCurrent)
                # sort requests by date
                self.model().sort(self.Columns.DATE)
       t@@ -138,7 +126,7 @@ class InvoiceList(MyTreeView):
                    return
                key = item_col0.data(ROLE_REQUEST_ID)
                request_type = item_col0.data(ROLE_REQUEST_TYPE)
       -        assert request_type in [REQUEST_TYPE_BITCOIN, REQUEST_TYPE_LN]
       +        assert request_type in [PR_TYPE_ADDRESS, PR_TYPE_BIP70, PR_TYPE_LN]
                column = idx.column()
                column_title = self.model().horizontalHeaderItem(column).text()
                column_data = item.text()
       t@@ -147,17 +135,17 @@ class InvoiceList(MyTreeView):
                    if column == self.Columns.AMOUNT:
                        column_data = column_data.strip()
                    menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data))
       -        if request_type == REQUEST_TYPE_BITCOIN:
       +        if request_type in [PR_TYPE_BIP70, PR_TYPE_ADDRESS]:
                    self.create_menu_bitcoin_payreq(menu, key)
       -        elif request_type == REQUEST_TYPE_LN:
       +        elif request_type == PR_TYPE_LN:
                    self.create_menu_ln_payreq(menu, key)
                menu.exec_(self.viewport().mapToGlobal(position))
        
            def create_menu_bitcoin_payreq(self, menu, payreq_key):
       -        status = self.parent.invoices.get_status(payreq_key)
       +        #status = self.parent.wallet.get_invoice_status(payreq_key)
                menu.addAction(_("Details"), lambda: self.parent.show_invoice(payreq_key))
       -        if status == PR_UNPAID:
       -            menu.addAction(_("Pay Now"), lambda: self.parent.do_pay_invoice(payreq_key))
       +        #if status == PR_UNPAID:
       +        menu.addAction(_("Pay Now"), lambda: self.parent.do_pay_invoice(payreq_key))
                menu.addAction(_("Delete"), lambda: self.parent.delete_invoice(payreq_key))
        
            def create_menu_ln_payreq(self, menu, payreq_key):
   DIR diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
       t@@ -120,7 +120,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
            payment_request_ok_signal = pyqtSignal()
            payment_request_error_signal = pyqtSignal()
            network_signal = pyqtSignal(str, object)
       -    ln_payment_attempt_signal = pyqtSignal(str)
       +    #ln_payment_attempt_signal = pyqtSignal(str)
            alias_received_signal = pyqtSignal()
            computing_privkeys_signal = pyqtSignal()
            show_privkeys_signal = pyqtSignal()
       t@@ -138,7 +138,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                assert wallet, "no wallet"
                self.wallet = wallet
                self.fx = gui_object.daemon.fx  # type: FxThread
       -        self.invoices = wallet.invoices
       +        #self.invoices = wallet.invoices
                self.contacts = wallet.contacts
                self.tray = gui_object.tray
                self.app = gui_object.app
       t@@ -225,7 +225,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                                 'new_transaction', 'status',
                                 'banner', 'verified', 'fee', 'fee_histogram', 'on_quotes',
                                 'on_history', 'channel', 'channels', 'payment_received',
       -                         'ln_payment_completed', 'ln_payment_attempt']
       +                         'payment_status']
                    # To avoid leaking references to "self" that prevent the
                    # window from being GC-ed when closed, callbacks should be
                    # methods of this class only, and specifically not be
       t@@ -374,14 +374,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                elif event == 'channel':
                    self.channels_list.update_single_row.emit(*args)
                    self.update_status()
       -        elif event == 'ln_payment_attempt':
       -            msg = _('Sending lightning payment') + '... (%d/%d)'%(args[0]+1, LN_NUM_PAYMENT_ATTEMPTS)
       -            self.ln_payment_attempt_signal.emit(msg)
       -        elif event == 'ln_payment_completed':
       -            # FIXME it is really inefficient to force update the whole GUI
       -            # just for a single LN payment. individual rows in lists should be updated instead.
       -            # consider: history tab, invoice list, request list
       -            self.need_update.set()
       +        elif event == 'payment_status':
       +            self.on_payment_status(*args)
                elif event == 'status':
                    self.update_status()
                elif event == 'banner':
       t@@ -1671,33 +1665,32 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                self.do_send(preview = True)
        
            def pay_lightning_invoice(self, invoice):
       -        amount = self.amount_e.get_amount()
       -        def on_success(result):
       -            self.logger.info(f'ln payment success. {result}')
       -            self.show_error(_('Payment succeeded'))
       -            self.do_clear()
       -        def on_failure(exc_info):
       -            type_, e, traceback = exc_info
       -            if isinstance(e, PaymentFailure):
       -                self.show_error(_('Payment failed. {}').format(e))
       -            elif isinstance(e, InvoiceError):
       -                self.show_error(_('InvoiceError: {}').format(e))
       -            else:
       -                raise e
       +        amount_sat = self.amount_e.get_amount()
       +        attempts = LN_NUM_PAYMENT_ATTEMPTS
                def task():
       -            success = self.wallet.lnworker.pay(invoice, attempts=LN_NUM_PAYMENT_ATTEMPTS, amount_sat=amount, timeout=60)
       -            if not success:
       -                raise PaymentFailure(f'Failed after {LN_NUM_PAYMENT_ATTEMPTS} attempts')
       +            self.wallet.lnworker.pay(invoice, amount_sat, attempts)
       +        self.do_clear()
       +        self.wallet.thread.add(task)
       +        self.invoice_list.update()
        
       -        msg = _('Sending lightning payment...')
       -        d = WaitingDialog(self, msg, task, on_success, on_failure)
       -        self.ln_payment_attempt_signal.connect(d.update)
       +    def on_payment_status(self, key, status, *args):
       +        # todo: check that key is in this wallet's invoice list
       +        self.invoice_list.update()
       +        if status == 'success':
       +            self.show_message(_('Payment succeeded'))
       +            self.need_update.set()
       +        elif status == 'progress':
       +            print('on_payment_status', key, status, args)
       +        elif status == 'failure':
       +            self.show_info(_('Payment failed'))
       +        elif status == 'error':
       +            e = args[0]
       +            self.show_error(_('Error') + '\n' + str(e))
        
            def do_send(self, preview = False):
                if self.payto_e.is_lightning:
                    self.pay_lightning_invoice(self.payto_e.lightning_invoice)
                    return
       -        #
                if run_hook('abort_send', self):
                    return
                outputs, fee_estimator, tx_desc, coins = self.read_send_tab()
       t@@ -1817,8 +1810,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                    else:
                        status, msg = True, tx.txid()
                    if pr and status is True:
       -                self.invoices.set_paid(pr, tx.txid())
       -                self.invoices.save()
       +                key = pr.get_id()
       +                self.wallet.set_invoice_paid(key, tx.txid())
                        self.payment_request = None
                        refund_address = self.wallet.get_receiving_address()
                        coro = pr.send_payment_and_receive_paymentack(str(tx), refund_address)
       t@@ -1889,17 +1882,16 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                return True
        
            def delete_invoice(self, key):
       -        self.invoices.remove(key)
       +        self.wallet.delete_invoice(key)
                self.invoice_list.update()
        
            def payment_request_ok(self):
                pr = self.payment_request
                if not pr:
                    return
       -        key = self.invoices.add(pr)
       -        status = self.invoices.get_status(key)
       -        self.invoice_list.update()
       -        if status == PR_PAID:
       +        key = pr.get_id()
       +        invoice = self.wallet.get_invoice(key)
       +        if invoice and invoice['status'] == PR_PAID:
                    self.show_message("invoice already paid")
                    self.do_clear()
                    self.payment_request = None
       t@@ -2106,7 +2098,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                self.update_completions()
        
            def show_invoice(self, key):
       -        pr = self.invoices.get(key)
       +        pr = self.wallet.get_invoice(key)
                if pr is None:
                    self.show_error('Cannot find payment request in wallet.')
                    return
       t@@ -2143,7 +2135,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                exportButton = EnterButton(_('Save'), do_export)
                def do_delete():
                    if self.question(_('Delete invoice?')):
       -                self.invoices.remove(key)
       +                self.wallet.delete_invoices(key)
                        self.history_list.update()
                        self.invoice_list.update()
                        d.close()
       t@@ -2152,7 +2144,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                d.exec_()
        
            def do_pay_invoice(self, key):
       -        pr = self.invoices.get(key)
       +        pr = self.wallet.get_invoice(key)
                self.payment_request = pr
                self.prepare_for_payment_request()
                pr.error = None  # this forces verify() to re-run
   DIR diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py
       t@@ -31,6 +31,7 @@ from PyQt5.QtCore import Qt, QItemSelectionModel
        
        from electrum.i18n import _
        from electrum.util import format_time, age, get_request_status
       +from electrum.util import PR_TYPE_ADDRESS, PR_TYPE_LN, PR_TYPE_BIP70
        from electrum.util import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_UNKNOWN, PR_INFLIGHT, pr_tooltips
        from electrum.lnutil import SENT, RECEIVED
        from electrum.plugin import run_hook
       t@@ -104,9 +105,10 @@ class RequestList(MyTreeView):
                    is_lightning = date_item.data(ROLE_REQUEST_TYPE) == REQUEST_TYPE_LN
                    req = self.wallet.get_request(key, is_lightning)
                    if req:
       +                status = req['status']
                        status_str = get_request_status(req)
                        status_item.setText(status_str)
       -                status_item.setIcon(read_QIcon(pr_icons.get(req['status'])))
       +                status_item.setIcon(read_QIcon(pr_icons.get(status)))
        
            def update(self):
                self.wallet = self.parent.wallet
       t@@ -118,10 +120,11 @@ class RequestList(MyTreeView):
                    status = req.get('status')
                    if status == PR_PAID:
                        continue
       -            request_type = REQUEST_TYPE_LN if req.get('lightning', False) else REQUEST_TYPE_BITCOIN
       +            is_lightning = req['type'] == PR_TYPE_LN
       +            request_type = REQUEST_TYPE_LN if is_lightning else REQUEST_TYPE_BITCOIN
                    timestamp = req.get('time', 0)
                    amount = req.get('amount')
       -            message = req['memo']
       +            message = req['message'] if is_lightning else req['memo']
                    date = format_time(timestamp)
                    amount_str = self.parent.format_amount(amount) if amount else ""
                    status_str = get_request_status(req)
   DIR diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py
       t@@ -31,7 +31,7 @@ from typing import Optional, Dict, List, Tuple, NamedTuple, Set, Callable, Itera
        import time
        
        from . import ecc
       -from .util import bfh, bh2u
       +from .util import bfh, bh2u, PR_PAID, PR_FAILED
        from .bitcoin import TYPE_SCRIPT, TYPE_ADDRESS
        from .bitcoin import redeem_script_to_address
        from .crypto import sha256, sha256d
       t@@ -165,8 +165,11 @@ class Channel(Logger):
                    log = self.hm.log[subject]
                    for htlc_id, htlc in log.get('adds', {}).items():
                        if htlc_id in log.get('fails',{}):
       -                    continue
       -                status = 'settled' if htlc_id in log.get('settles',{}) else 'inflight'
       +                    status = 'failed'
       +                elif htlc_id in log.get('settles',{}):
       +                    status = 'settled'
       +                else:
       +                    status = 'inflight'
                        direction = SENT if subject is LOCAL else RECEIVED
                        rhash = bh2u(htlc.payment_hash)
                        out[rhash] = (self.channel_id, htlc, direction, status)
       t@@ -563,7 +566,7 @@ class Channel(Logger):
                assert htlc_id not in log['settles']
                self.hm.send_settle(htlc_id)
                if self.lnworker:
       -            self.lnworker.set_paid(htlc.payment_hash)
       +            self.lnworker.set_invoice_status(htlc.payment_hash, PR_PAID)
        
            def receive_htlc_settle(self, preimage, htlc_id):
                self.logger.info("receive_htlc_settle")
       t@@ -574,7 +577,7 @@ class Channel(Logger):
                self.hm.recv_settle(htlc_id)
                if self.lnworker:
                    self.lnworker.save_preimage(htlc.payment_hash, preimage)
       -            self.lnworker.set_paid(htlc.payment_hash)
       +            self.lnworker.set_invoice_status(htlc.payment_hash, PR_PAID)
        
            def fail_htlc(self, htlc_id):
                self.logger.info("fail_htlc")
   DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py
       t@@ -21,7 +21,8 @@ import dns.exception
        
        from . import constants
        from . import keystore
       -from .util import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_UNKNOWN, PR_INFLIGHT
       +from .util import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_UNKNOWN, PR_INFLIGHT, profiler
       +from .util import PR_TYPE_LN
        from .keystore import BIP32_KeyStore
        from .bitcoin import COIN
        from .transaction import Transaction
       t@@ -396,14 +397,12 @@ class LNWallet(LNWorker):
            def get_invoice_status(self, key):
                if key not in self.invoices:
                    return PR_UNKNOWN
       -        invoice, direction, is_paid = self.invoices[key]
       +        invoice, direction, status = self.invoices[key]
                lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
       -        if is_paid:
       -            return PR_PAID
       -        elif lnaddr.is_expired():
       +        if status == PR_UNPAID and lnaddr.is_expired():
                    return PR_EXPIRED
                else:
       -            return PR_UNPAID
       +            return status
        
            def get_payments(self):
                # return one item per payment_hash
       t@@ -415,11 +414,35 @@ class LNWallet(LNWorker):
                        out[k].append(v)
                return out
        
       +    def get_unsettled_payments(self):
       +        out = []
       +        for payment_hash, plist in self.get_payments().items():
       +            if len(plist) != 1:
       +                continue
       +            chan_id, htlc, _direction, status = plist[0]
       +            if _direction != SENT:
       +                continue
       +            if status == 'settled':
       +                continue
       +            amount = htlc.amount_msat//1000
       +            item = {
       +                'is_lightning': True,
       +                'status': status,
       +                'key': payment_hash,
       +                'amount': amount,
       +                'timestamp': htlc.timestamp,
       +                'label': self.wallet.get_label(payment_hash)
       +            }
       +            out.append(item)
       +        return out
       +
            def get_history(self):
                out = []
                for payment_hash, plist in self.get_payments().items():
                    if len(plist) == 1:
                        chan_id, htlc, _direction, status = plist[0]
       +                if status != 'settled':
       +                    continue
                        direction = 'sent' if _direction == SENT else 'received'
                        amount_msat= int(_direction) * htlc.amount_msat
                        timestamp = htlc.timestamp
       t@@ -751,17 +774,23 @@ class LNWallet(LNWorker):
                    raise Exception(_("open_channel timed out"))
                return chan
        
       -    def pay(self, invoice, attempts=1, amount_sat=None, timeout=10):
       +    def pay(self, invoice, amount_sat=None, attempts=1):
                """
                Can be called from other threads
       -        Raises exception after timeout
                """
       -        coro = self._pay(invoice, attempts, amount_sat)
       +        addr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
       +        key = bh2u(addr.paymenthash)
       +        coro = self._pay(invoice, amount_sat, attempts)
                fut = asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop)
                try:
       -            return fut.result(timeout=timeout)
       -        except concurrent.futures.TimeoutError:
       -            raise PaymentFailure(_("Payment timed out"))
       +            success = fut.result()
       +        except Exception as e:
       +            self.network.trigger_callback('payment_status', key, 'error', e)
       +            return
       +        if success:
       +            self.network.trigger_callback('payment_status', key, 'success')
       +        else:
       +            self.network.trigger_callback('payment_status', key, 'failure')
        
            def get_channel_by_short_id(self, short_channel_id):
                with self.lock:
       t@@ -770,20 +799,22 @@ class LNWallet(LNWorker):
                            return chan
        
            @log_exceptions
       -    async def _pay(self, invoice, attempts=1, amount_sat=None):
       +    async def _pay(self, invoice, amount_sat=None, attempts=1):
                addr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
       -        status = self.get_invoice_status(bh2u(addr.paymenthash))
       +        key = bh2u(addr.paymenthash)
       +        status = self.get_invoice_status(key)
                if status == PR_PAID:
       +            # fixme: use lightning_preimaages, because invoices are not permanently stored
                    raise PaymentFailure(_("This invoice has been paid already"))
                self._check_invoice(invoice, amount_sat)
       -        self.save_invoice(addr.paymenthash, invoice, SENT, is_paid=False)
       -        self.wallet.set_label(bh2u(addr.paymenthash), addr.get_description())
       +        self.save_invoice(addr.paymenthash, invoice, SENT, PR_INFLIGHT)
       +        self.wallet.set_label(key, addr.get_description())
                for i in range(attempts):
                    route = await self._create_route_from_invoice(decoded_invoice=addr)
                    if not self.get_channel_by_short_id(route[0].short_channel_id):
                        scid = format_short_channel_id(route[0].short_channel_id)
                        raise Exception(f"Got route with unknown first channel: {scid}")
       -            self.network.trigger_callback('ln_payment_attempt', i)
       +            self.network.trigger_callback('payment_status', key, 'progress', i)
                    if await self._pay_to_route(route, addr, invoice):
                        return True
                return False
       t@@ -895,7 +926,7 @@ class LNWallet(LNWorker):
                                                ('x', expiry)]
                                               + routing_hints),
                                   self.node_keypair.privkey)
       -        self.save_invoice(payment_hash, invoice, RECEIVED, is_paid=False)
       +        self.save_invoice(payment_hash, invoice, RECEIVED, PR_UNPAID)
                self.save_preimage(payment_hash, payment_preimage)
                self.wallet.set_label(bh2u(payment_hash), message)
                return payment_hash
       t@@ -915,20 +946,24 @@ class LNWallet(LNWorker):
                except KeyError as e:
                    raise UnknownPaymentHash(payment_hash) from e
        
       -    def save_invoice(self, payment_hash:bytes, invoice, direction, *, is_paid=False):
       +    def save_new_invoice(self, invoice):
       +        addr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
       +        self.save_invoice(addr.paymenthash, invoice, SENT, PR_UNPAID)
       +
       +    def save_invoice(self, payment_hash:bytes, invoice, direction, status):
                key = bh2u(payment_hash)
       -        self.invoices[key] = invoice, direction, is_paid
       +        self.invoices[key] = invoice, direction, status
                self.storage.put('lightning_invoices', self.invoices)
                self.storage.write()
        
       -    def set_paid(self, payment_hash):
       +    def set_invoice_status(self, payment_hash, status):
                key = bh2u(payment_hash)
                if key not in self.invoices:
                    # if we are forwarding
                    return
                invoice, direction, _ = self.invoices[key]
       -        self.save_invoice(payment_hash, invoice, direction, is_paid=True)
       -        if direction == RECEIVED:
       +        self.save_invoice(payment_hash, invoice, direction, status)
       +        if direction == RECEIVED and status == PR_PAID:
                    self.network.trigger_callback('payment_received', self.wallet, key, PR_PAID)
        
            def get_invoice(self, payment_hash: bytes) -> LnAddr:
       t@@ -939,6 +974,9 @@ class LNWallet(LNWorker):
                    raise UnknownPaymentHash(payment_hash) from e
        
            def get_request(self, key):
       +        if key not in self.invoices:
       +            return
       +        # todo: parse invoices when saving
                invoice, direction, is_paid = self.invoices[key]
                status = self.get_invoice_status(key)
                lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
       t@@ -946,23 +984,32 @@ class LNWallet(LNWorker):
                description = lnaddr.get_description()
                timestamp = lnaddr.date
                return {
       -            'lightning':True,
       -            'status':status,
       -            'amount':amount_sat,
       -            'time':timestamp,
       -            'exp':lnaddr.get_expiry(),
       -            'memo':description,
       -            'rhash':key,
       +            'type': PR_TYPE_LN,
       +            'status': status,
       +            'amount': amount_sat,
       +            'time': timestamp,
       +            'exp': lnaddr.get_expiry(),
       +            'message': description,
       +            'rhash': key,
                    'invoice': invoice
                }
        
       +    @profiler
            def get_invoices(self):
       -        items = self.invoices.items()
       +        # invoices = outgoing
                out = []
       -        for key, (invoice, direction, is_paid) in items:
       -            if direction == SENT:
       -                continue
       -            out.append(self.get_request(key))
       +        for key, (invoice, direction, status) in self.invoices.items():
       +            if direction == SENT and status != PR_PAID:
       +                out.append(self.get_request(key))
       +        return out
       +
       +    @profiler
       +    def get_requests(self):
       +        # requests = incoming
       +        out = []
       +        for key, (invoice, direction, status) in self.invoices.items():
       +            if direction == RECEIVED and status != PR_PAID:
       +                out.append(self.get_request(key))
                return out
        
            async def _calc_routing_hints_for_invoice(self, amount_sat):
   DIR diff --git a/electrum/paymentrequest.py b/electrum/paymentrequest.py
       t@@ -41,6 +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 .util import PR_TYPE_BIP70
        from .crypto import sha256
        from .bitcoin import TYPE_ADDRESS
        from .transaction import TxOutput
       t@@ -270,13 +271,15 @@ class PaymentRequest:
        
            def get_dict(self):
                return {
       +            'type': PR_TYPE_BIP70,
       +            'id': self.get_id(),
                    'requestor': self.get_requestor(),
       -            'memo':self.get_memo(),
       -            'exp': self.get_expiration_date(),
       +            'message': self.get_memo(),
       +            'time': self.get_time(),
       +            'exp': self.get_expiration_date() - self.get_time(),
                    'amount': self.get_amount(),
       -            'signature': self.get_verify_status(),
       -            'txid': self.tx,
       -            'outputs': self.get_outputs()
       +            'outputs': self.get_outputs(),
       +            'hex': self.raw.hex(),
                }
        
            def get_id(self):
       t@@ -475,94 +478,3 @@ def make_request(config, req):
            if key_path and cert_path:
                sign_request_with_x509(pr, key_path, cert_path)
            return pr
       -
       -
       -
       -class InvoiceStore(Logger):
       -
       -    def __init__(self, storage):
       -        Logger.__init__(self)
       -        self.storage = storage
       -        self.invoices = {}
       -        self.paid = {}
       -        d = self.storage.get('invoices', {})
       -        self.load(d)
       -
       -    def set_paid(self, pr, txid):
       -        pr.tx = txid
       -        pr_id = pr.get_id()
       -        self.paid[txid] = pr_id
       -        if pr_id not in self.invoices:
       -            # in case the user had deleted it previously
       -            self.add(pr)
       -
       -    def load(self, d):
       -        for k, v in d.items():
       -            try:
       -                pr = PaymentRequest(bfh(v.get('hex')))
       -                pr.tx = v.get('txid')
       -                pr.requestor = v.get('requestor')
       -                self.invoices[k] = pr
       -                if pr.tx:
       -                    self.paid[pr.tx] = k
       -            except:
       -                continue
       -
       -    def import_file(self, path):
       -        def validate(data):
       -            return data  # TODO
       -        import_meta(path, validate, self.on_import)
       -
       -    def on_import(self, data):
       -        self.load(data)
       -        self.save()
       -
       -    def export_file(self, filename):
       -        export_meta(self.dump(), filename)
       -
       -    def dump(self):
       -        d = {}
       -        for k, pr in self.invoices.items():
       -            d[k] = {
       -                'hex': bh2u(pr.raw),
       -                'requestor': pr.requestor,
       -                'txid': pr.tx
       -            }
       -        return d
       -
       -    def save(self):
       -        self.storage.put('invoices', self.dump())
       -
       -    def get_status(self, key):
       -        pr = self.get(key)
       -        if pr is None:
       -            self.logger.info(f"get_status() can't find pr for {key}")
       -            return
       -        if pr.tx is not None:
       -            return PR_PAID
       -        if pr.has_expired():
       -            return PR_EXPIRED
       -        return PR_UNPAID
       -
       -    def add(self, pr):
       -        key = pr.get_id()
       -        self.invoices[key] = pr
       -        self.save()
       -        return key
       -
       -    def remove(self, key):
       -        self.invoices.pop(key)
       -        self.save()
       -
       -    def get(self, k):
       -        return self.invoices.get(k)
       -
       -    def sorted_list(self):
       -        # sort
       -        return self.invoices.values()
       -
       -    def unpaid_invoices(self):
       -        return [self.invoices[k] for k in
       -                filter(lambda x: self.get_status(x) not in (PR_PAID, None),
       -                       self.invoices.keys())
       -                ]
   DIR diff --git a/electrum/util.py b/electrum/util.py
       t@@ -73,19 +73,26 @@ base_units_list = ['BTC', 'mBTC', 'bits', 'sat']  # list(dict) does not guarante
        
        DECIMAL_POINT_DEFAULT = 5  # mBTC
        
       +# types of payment requests
       +PR_TYPE_ADDRESS = 0
       +PR_TYPE_BIP70= 1
       +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_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_tooltips = {
            PR_UNPAID:_('Pending'),
            PR_PAID:_('Paid'),
            PR_UNKNOWN:_('Unknown'),
            PR_EXPIRED:_('Expired'),
       -    PR_INFLIGHT:_('Paid (unconfirmed)')
       +    PR_INFLIGHT:_('In progress'),
       +    PR_FAILED:_('Failed'),
        }
        
        pr_expiration_values = {
   DIR diff --git a/electrum/wallet.py b/electrum/wallet.py
       t@@ -47,6 +47,7 @@ from .util import (NotEnoughFunds, UserCancelled, profiler,
                           InvalidPassword, format_time, timestamp_to_datetime, Satoshis,
                           Fiat, bfh, bh2u, TxMinedInfo, quantize_feerate, create_bip21_uri, OrderedDictWithIndex)
        from .util import age
       +from .util import PR_TYPE_ADDRESS, PR_TYPE_BIP70, PR_TYPE_LN
        from .simple_config import get_config
        from .bitcoin import (COIN, TYPE_ADDRESS, is_address, address_to_script,
                              is_minikey, relayfee, dust_threshold)
       t@@ -60,14 +61,14 @@ from .transaction import Transaction, TxOutput, TxOutputHwInfo
        from .plugin import run_hook
        from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL,
                                           TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE)
       -from .paymentrequest import (PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, PR_INFLIGHT,
       -                             InvoiceStore)
       +from .util import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, PR_INFLIGHT
        from .contacts import Contacts
        from .interface import NetworkException
        from .ecc_fast import is_using_fast_ecc
        from .mnemonic import Mnemonic
        from .logging import get_logger
        from .lnworker import LNWallet
       +from .paymentrequest import PaymentRequest
        
        if TYPE_CHECKING:
            from .network import Network
       t@@ -225,6 +226,7 @@ class Abstract_Wallet(AddressSynchronizer):
                self.frozen_coins          = set(storage.get('frozen_coins', []))  # set of txid:vout strings
                self.fiat_value            = storage.get('fiat_value', {})
                self.receive_requests      = storage.get('payment_requests', {})
       +        self.invoices              = storage.get('invoices', {})
        
                self.calc_unused_change_addresses()
        
       t@@ -232,8 +234,7 @@ class Abstract_Wallet(AddressSynchronizer):
                if self.storage.get('wallet_type') is None:
                    self.storage.put('wallet_type', self.wallet_type)
        
       -        # invoices and contacts
       -        self.invoices = InvoiceStore(self.storage)
       +        # contacts
                self.contacts = Contacts(self.storage)
                self._coin_price_cache = {}
                self.lnworker = LNWallet(self) if get_config().get('lightning') else None
       t@@ -498,6 +499,51 @@ class Abstract_Wallet(AddressSynchronizer):
                        'txpos_in_block': tx_mined_status.txpos,
                    }
        
       +    def save_invoice(self, invoice):
       +        invoice_type = invoice['type']
       +        if invoice_type == PR_TYPE_LN:
       +            self.lnworker.save_new_invoice(invoice['invoice'])
       +        else:
       +            if invoice_type == PR_TYPE_ADDRESS:
       +                key = invoice['address']
       +                invoice['time'] = int(time.time())
       +            elif invoice_type == PR_TYPE_BIP70:
       +                key = invoice['id']
       +                invoice['txid'] = None
       +            else:
       +                raise Exception('Unsupported invoice type')
       +            self.invoices[key] = invoice
       +            self.storage.put('invoices', self.invoices)
       +            self.storage.write()
       +
       +    def get_invoices(self):
       +        out = [self.get_invoice(key) for key in self.invoices.keys()]
       +        out = [x for x in out if x and x.get('status') != PR_PAID]
       +        if self.lnworker:
       +            out += self.lnworker.get_invoices()
       +        out.sort(key=operator.itemgetter('time'))
       +        return out
       +
       +    def get_invoice(self, key):
       +        if key in self.invoices:
       +            item = copy.copy(self.invoices[key])
       +            request_type = item.get('type')
       +            if request_type is None:
       +                # todo: convert old bip70 invoices
       +                return
       +            # add status
       +            if item.get('txid'):
       +                status = PR_PAID
       +            elif 'exp' in item and item['time'] + item['exp'] < time.time():
       +                status = PR_EXPIRED
       +            else:
       +                status = PR_UNPAID
       +            item['status'] = status
       +            return item
       +        if self.lnworker:
       +            return self.lnworker.get_request(key)
       +
       +
            @profiler
            def get_full_history(self, fx=None):
                transactions = OrderedDictWithIndex()
       t@@ -1221,6 +1267,7 @@ class Abstract_Wallet(AddressSynchronizer):
                if not r:
                    return
                out = copy.copy(r)
       +        out['type'] = PR_TYPE_ADDRESS
                out['URI'] = 'bitcoin:' + addr + '?amount=' + format_satoshis(out.get('amount'))
                status, conf = self.get_request_status(addr)
                out['status'] = status
       t@@ -1363,6 +1410,14 @@ class Abstract_Wallet(AddressSynchronizer):
                elif self.lnworker:
                    self.lnworker.delete_invoice(key)
        
       +    def delete_invoice(self, key):
       +        """ lightning or on-chain """
       +        if key in self.invoices:
       +            self.invoices.pop(key)
       +            self.storage.put('invoices', self.invoices)
       +        elif self.lnworker:
       +            self.lnworker.delete_invoice(key)
       +
            def remove_payment_request(self, addr, config):
                if addr not in self.receive_requests:
                    return False
       t@@ -1381,7 +1436,7 @@ class Abstract_Wallet(AddressSynchronizer):
                """ sorted by timestamp """
                out = [self.get_payment_request(x, config) for x in self.receive_requests.keys()]
                if self.lnworker:
       -            out += self.lnworker.get_invoices()
       +            out += self.lnworker.get_requests()
                out.sort(key=operator.itemgetter('time'))
                return out