URI: 
       tSimplify invoices and requests. - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit aaed594772dde4b6240267e21f067e89bed5fa15
   DIR parent 1b332748c3ef641ecb94e8cbaefd4c2483b6993f
  HTML Author: ThomasV <thomasv@electrum.org>
       Date:   Sun,  8 Sep 2019 11:59:03 +0200
       
       Simplify invoices and requests.
       
        - We need only two types: PR_TYPE_ONCHAIN and PR_TYPE_LN
        - BIP70 is no longer a type, but an optional field in the dict
        - Invoices in the wallet are indexed by a hash of their serialized list of outputs.
        - Requests are still indexed by address, because we never generate Paytomany requests.
        - Add 'clear_invoices' command to CLI
        - Add 'save invoice' button to Qt
       
       Diffstat:
         M electrum/commands.py                |       8 +++++++-
         M electrum/gui/kivy/uix/screens.py    |      65 +++++++++++++-------------------
         M electrum/gui/kivy/uix/ui_screens/s… |      25 ++++++++++++-------------
         M electrum/gui/qt/invoice_list.py     |      33 ++++++++++---------------------
         M electrum/gui/qt/main_window.py      |     151 ++++++++++++++++++++++---------
         M electrum/gui/qt/paytoedit.py        |       5 +++--
         M electrum/gui/qt/request_list.py     |      39 +++++++++++++------------------
         M electrum/paymentrequest.py          |      18 ------------------
         M electrum/util.py                    |       3 +--
         M electrum/wallet.py                  |      51 +++++++++++++++++++++++--------
       
       10 files changed, 221 insertions(+), 177 deletions(-)
       ---
   DIR diff --git a/electrum/commands.py b/electrum/commands.py
       t@@ -795,11 +795,17 @@ class Commands:
                return wallet.remove_payment_request(address)
        
            @command('w')
       -    async def clearrequests(self, wallet=None):
       +    async def clear_requests(self, wallet=None):
                """Remove all payment requests"""
                for k in list(wallet.receive_requests.keys()):
                    wallet.remove_payment_request(k)
        
       +    @command('w')
       +    async def clear_invoices(self, wallet=None):
       +        """Remove all invoices"""
       +        wallet.clear_invoices()
       +        return True
       +
            @command('n')
            async def notify(self, address: str, URL: str):
                """Watch an address. Every time the address changes, a http POST is sent to the URL."""
   DIR diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py
       t@@ -21,8 +21,9 @@ from kivy.lang import Builder
        from kivy.factory import Factory
        from kivy.utils import platform
        
       +from electrum.bitcoin import TYPE_ADDRESS
        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.util import PR_TYPE_ONCHAIN, PR_TYPE_LN
        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@@ -180,6 +181,7 @@ class SendScreen(CScreen):
            kvname = 'send'
            payment_request = None
            payment_request_queued = None
       +    parsed_URI = None
        
            def set_URI(self, text):
                if not self.app.wallet:
       t@@ -190,12 +192,13 @@ class SendScreen(CScreen):
                except InvalidBitcoinURI as e:
                    self.app.show_info(_("Error parsing URI") + f":\n{e}")
                    return
       +        self.parsed_URI = uri
                amount = uri.get('amount')
                self.screen.address = uri.get('address', '')
                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 = PR_TYPE_ADDRESS
       +        self.screen.is_lightning = False
        
            def set_ln_invoice(self, invoice):
                try:
       t@@ -207,7 +210,7 @@ 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 = PR_TYPE_LN
       +        self.screen.is_lightning = True
        
            def update(self):
                if not self.loaded:
       t@@ -227,14 +230,14 @@ class SendScreen(CScreen):
                if invoice_type == PR_TYPE_LN:
                    key = item['rhash']
                    status = get_request_status(item) # convert to str
       -        elif invoice_type == PR_TYPE_BIP70:
       +        elif invoice_type == PR_TYPE_ONCHAIN:
                    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
       +        else:
       +            raise Exception('unknown invoice type')
                return {
                    'is_lightning': invoice_type == PR_TYPE_LN,
       +            'is_bip70': 'bip70' in item,
                    'screen': self,
                    'status': status,
                    'key': key,
       t@@ -247,19 +250,16 @@ class SendScreen(CScreen):
                self.screen.message = ''
                self.screen.address = ''
                self.payment_request = None
       -        self.screen.destinationtype = PR_TYPE_ADDRESS
       +        self.screen.locked = False
       +        self.parsed_URI = None
        
            def set_request(self, pr):
                self.screen.address = pr.get_requestor()
                amount = pr.get_amount()
                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 = PR_TYPE_BIP70
       -            self.payment_request = pr
       -        else:
       -            self.screen.destinationtype = PR_TYPE_ADDRESS
       -            self.payment_request = None
       +        self.screen.locked = True
       +        self.payment_request = pr
        
            def do_paste(self):
                data = self.app._clipboard.paste().strip()
       t@@ -299,30 +299,19 @@ class SendScreen(CScreen):
                    self.app.show_error(_('Invalid amount') + ':\n' + self.screen.amount)
                    return
                message = self.screen.message
       -        if self.screen.destinationtype == PR_TYPE_LN:
       +        if self.screen.is_lightning:
                    return {
                        'type': PR_TYPE_LN,
                        'invoice': address,
                        'amount': amount,
                        'message': message,
                    }
       -        elif self.screen.destinationtype == PR_TYPE_ADDRESS:
       +        else:
                    if not bitcoin.is_address(address):
                        self.app.show_error(_('Invalid Bitcoin Address') + ':\n' + address)
                        return
       -            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')
       +            outputs = [(TYPE_ADDRESS, address, amount)]
       +            return self.app.wallet.create_invoice(outputs, message, self.payment_request, self.parsed_URI)
        
            def do_save(self):
                invoice = self.read_invoice()
       t@@ -345,20 +334,18 @@ class SendScreen(CScreen):
                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']
       +        elif invoice['type'] == PR_TYPE_ONCHAIN:
                    message = invoice['message']
       -            outputs = [TxOutput(bitcoin.TYPE_ADDRESS, address, amount)]
       -        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_onchain(amount, message, outputs, b))
       -            d.open()
       +            do_pay = lambda rbf: self._do_send_onchain(amount, message, outputs, 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:
       -            self._do_send_onchain(amount, message, outputs, False)
       +            raise Exception('unknown invoice type')
        
            def _do_send_lightning(self, invoice, amount):
                attempts = 10
   DIR diff --git a/electrum/gui/kivy/uix/ui_screens/send.kv b/electrum/gui/kivy/uix/ui_screens/send.kv
       t@@ -1,8 +1,5 @@
        #:import _ electrum.gui.kivy.i18n._
        #: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)
       t@@ -68,7 +65,9 @@ SendScreen:
            address: ''
            amount: ''
            message: ''
       -    destinationtype: PR_TYPE_ADDRESS
       +    is_bip70: False
       +    is_lightning: False
       +    is_locked: self.is_lightning or self.is_bip70
            BoxLayout
                padding: '12dp', '12dp', '12dp', '12dp'
                spacing: '12dp'
       t@@ -82,7 +81,7 @@ SendScreen:
                        height: blue_bottom.item_height
                        spacing: '5dp'
                        Image:
       -                    source: 'atlas://electrum/gui/kivy/theming/light/globe' if root.destinationtype != PR_TYPE_LN else 'atlas://electrum/gui/kivy/theming/light/lightning'
       +                    source: 'atlas://electrum/gui/kivy/theming/light/lightning' if root.is_lightning else 'atlas://electrum/gui/kivy/theming/light/globe'
                            size_hint: None, None
                            size: '22dp', '22dp'
                            pos_hint: {'center_y': .5}
       t@@ -93,7 +92,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 == PR_TYPE_ADDRESS)
       +                opacity: int(not root.is_locked)
                        color: blue_bottom.foreground_color
                    BoxLayout:
                        size_hint: 1, None
       t@@ -109,10 +108,10 @@ SendScreen:
                            id: amount_e
                            default_text: _('Amount')
                            text: s.amount if s.amount else _('Amount')
       -                    disabled: root.destinationtype == PR_TYPE_BIP70 or root.destinationtype == PR_TYPE_LN and not s.amount
       +                    disabled: root.is_bip70 or (root.is_lightning and not s.amount)
                            on_release: Clock.schedule_once(lambda dt: app.amount_dialog(s, True))
                    CardSeparator:
       -                opacity: int(root.destinationtype == PR_TYPE_ADDRESS)
       +                opacity: int(not root.is_locked)
                        color: blue_bottom.foreground_color
                    BoxLayout:
                        id: message_selection
       t@@ -126,11 +125,11 @@ SendScreen:
                            pos_hint: {'center_y': .5}
                        BlueButton:
                            id: description
       -                    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
       +                    text: s.message if s.message else (_('No Description') if root.is_locked else _('Description'))
       +                    disabled: root.is_locked
                            on_release: Clock.schedule_once(lambda dt: app.description_dialog(s))
                    CardSeparator:
       -                opacity: int(root.destinationtype == PR_TYPE_ADDRESS)
       +                opacity: int(not root.is_locked)
                        color: blue_bottom.foreground_color
                    BoxLayout:
                        size_hint: 1, None
       t@@ -144,8 +143,8 @@ SendScreen:
                        BlueButton:
                            id: fee_e
                            default_text: _('Fee')
       -                    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
       +                    text: app.fee_status if not root.is_lightning else ''
       +                    on_release: Clock.schedule_once(lambda dt: app.fee_dialog(s, True)) if not root.is_lightning else None
                BoxLayout:
                    size_hint: 1, None
                    height: '48dp'
   DIR diff --git a/electrum/gui/qt/invoice_list.py b/electrum/gui/qt/invoice_list.py
       t@@ -31,7 +31,7 @@ from PyQt5.QtWidgets import QHeaderView, QMenu
        
        from electrum.i18n import _
        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.util import PR_TYPE_ONCHAIN, PR_TYPE_LN
        from electrum.lnutil import lndecode, RECEIVED
        from electrum.bitcoin import COIN
        from electrum import constants
       t@@ -78,12 +78,11 @@ class InvoiceList(MyTreeView):
                    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:
       +            elif invoice_type == PR_TYPE_ONCHAIN:
                        key = item['id']
       -                icon_name = 'seal.png'
       +                icon_name = 'bitcoin.png'
       +                if item.get('bip70'):
       +                    icon_name = 'seal.png'
                    else:
                        raise Exception('Unsupported type')
                    status = item['status']
       t@@ -126,7 +125,6 @@ class InvoiceList(MyTreeView):
                    return
                key = item_col0.data(ROLE_REQUEST_ID)
                request_type = item_col0.data(ROLE_REQUEST_TYPE)
       -        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@@ -135,20 +133,9 @@ 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 in [PR_TYPE_BIP70, PR_TYPE_ADDRESS]:
       -            self.create_menu_bitcoin_payreq(menu, key)
       -        elif request_type == PR_TYPE_LN:
       -            self.create_menu_ln_payreq(menu, key)
       +        invoice = self.parent.wallet.get_invoice(key)
       +        menu.addAction(_("Details"), lambda: self.parent.show_invoice(key))
       +        if invoice['status'] == PR_UNPAID:
       +            menu.addAction(_("Pay Now"), lambda: self.parent.do_pay_invoice(invoice))
       +        menu.addAction(_("Delete"), lambda: self.parent.delete_invoice(key))
                menu.exec_(self.viewport().mapToGlobal(position))
       -
       -    def create_menu_bitcoin_payreq(self, menu, 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))
       -        menu.addAction(_("Delete"), lambda: self.parent.delete_invoice(payreq_key))
       -
       -    def create_menu_ln_payreq(self, menu, payreq_key):
       -        req = self.parent.wallet.lnworker.invoices[payreq_key][0]
       -        menu.addAction(_("Copy Lightning invoice"), lambda: self.parent.do_copy('Lightning invoice', req))
       -        menu.addAction(_("Delete"), lambda: self.parent.delete_invoice(payreq_key))
   DIR diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
       t@@ -62,6 +62,7 @@ from electrum.util import (format_time, format_satoshis, format_fee_satoshis,
                                   UnknownBaseUnit, DECIMAL_POINT_DEFAULT, UserFacingException,
                                   get_new_wallet_name, send_exception_to_crash_reporter,
                                   InvalidBitcoinURI, InvoiceError)
       +from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN
        from electrum.lnutil import PaymentFailure, SENT, RECEIVED
        from electrum.transaction import Transaction, TxOutput
        from electrum.address_synchronizer import AddTransactionException
       t@@ -142,7 +143,6 @@ 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.contacts = wallet.contacts
                self.tray = gui_object.tray
                self.app = gui_object.app
       t@@ -171,6 +171,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
        
                self.completions = QStringListModel()
        
       +        self.send_tab_is_onchain = False
       +
                self.tabs = tabs = QTabWidget(self)
                self.send_tab = self.create_send_tab()
                self.receive_tab = self.create_receive_tab()
       t@@ -1001,7 +1003,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                self.receive_qr.enterEvent = lambda x: self.app.setOverrideCursor(QCursor(Qt.PointingHandCursor))
                self.receive_qr.leaveEvent = lambda x: self.app.setOverrideCursor(QCursor(Qt.ArrowCursor))
        
       -        self.receive_requests_label = QLabel(_('Incoming invoices'))
       +        self.receive_requests_label = QLabel(_('Incoming payments'))
        
                from .request_list import RequestList
                self.request_list = RequestList(self)
       t@@ -1076,6 +1078,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                    self.address_list.update()
                self.request_list.update()
                self.request_list.select_key(key)
       +        # clear request fields
       +        self.receive_amount_e.setText('')
       +        self.receive_message_e.setText('')
        
            def create_bitcoin_request(self, amount, message, expiration):
                addr = self.wallet.get_unused_address()
       t@@ -1206,34 +1211,34 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                self.message_e = MyLineEdit()
                grid.addWidget(self.message_e, 2, 1, 1, -1)
        
       -        self.from_label = QLabel(_('From'))
       -        grid.addWidget(self.from_label, 3, 0)
       -        self.from_list = FromList(self, self.from_list_menu)
       -        grid.addWidget(self.from_list, 3, 1, 1, -1)
       -        self.set_pay_from([])
       -
                msg = _('Amount to be sent.') + '\n\n' \
                      + _('The amount will be displayed in red if you do not have enough funds in your wallet.') + ' ' \
                      + _('Note that if you have frozen some of your addresses, the available funds will be lower than your total balance.') + '\n\n' \
                      + _('Keyboard shortcut: type "!" to send all your coins.')
                amount_label = HelpLabel(_('Amount'), msg)
       -        grid.addWidget(amount_label, 4, 0)
       -        grid.addWidget(self.amount_e, 4, 1)
       +        grid.addWidget(amount_label, 3, 0)
       +        grid.addWidget(self.amount_e, 3, 1)
        
                self.fiat_send_e = AmountEdit(self.fx.get_currency if self.fx else '')
                if not self.fx or not self.fx.is_enabled():
                    self.fiat_send_e.setVisible(False)
       -        grid.addWidget(self.fiat_send_e, 4, 2)
       +        grid.addWidget(self.fiat_send_e, 3, 2)
                self.amount_e.frozen.connect(
                    lambda: self.fiat_send_e.setFrozen(self.amount_e.isReadOnly()))
        
                self.max_button = EnterButton(_("Max"), self.spend_max)
                self.max_button.setFixedWidth(self.amount_e.width())
                self.max_button.setCheckable(True)
       -        grid.addWidget(self.max_button, 4, 3)
       +        grid.addWidget(self.max_button, 3, 3)
                hbox = QHBoxLayout()
                hbox.addStretch(1)
       -        grid.addLayout(hbox, 4, 4)
       +        grid.addLayout(hbox, 3, 4)
       +
       +        self.from_label = QLabel(_('From'))
       +        grid.addWidget(self.from_label, 4, 0)
       +        self.from_list = FromList(self, self.from_list_menu)
       +        grid.addWidget(self.from_list, 4, 1, 1, -1)
       +        self.set_pay_from([])
        
                msg = _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
                      + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
       t@@ -1337,12 +1342,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                if not self.config.get('show_fee', False):
                    self.fee_adv_controls.setVisible(False)
        
       +        self.save_button = EnterButton(_("Save"), self.do_save_invoice)
                self.preview_button = EnterButton(_("Preview"), self.do_preview)
                self.preview_button.setToolTip(_('Display the details of your transaction before signing it.'))
       -        self.send_button = EnterButton(_("Send"), self.do_send)
       +        self.send_button = EnterButton(_("Send"), self.do_pay)
                self.clear_button = EnterButton(_("Clear"), self.do_clear)
                buttons = QHBoxLayout()
                buttons.addStretch(1)
       +        buttons.addWidget(self.save_button)
                buttons.addWidget(self.clear_button)
                buttons.addWidget(self.preview_button)
                buttons.addWidget(self.send_button)
       t@@ -1355,7 +1362,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                def reset_max(text):
                    self.max_button.setChecked(False)
                    enable = not bool(text) and not self.amount_e.isReadOnly()
       -            self.max_button.setEnabled(enable)
       +            #self.max_button.setEnabled(enable)
                self.amount_e.textEdited.connect(reset_max)
                self.fiat_send_e.textEdited.connect(reset_max)
        
       t@@ -1398,7 +1405,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                self.fee_e.textChanged.connect(entry_changed)
                self.feerate_e.textChanged.connect(entry_changed)
        
       -        self.invoices_label = QLabel(_('Outgoing invoices'))
       +        self.set_onchain(False)
       +
       +        self.invoices_label = QLabel(_('Outgoing payments'))
                from .invoice_list import InvoiceList
                self.invoice_list = InvoiceList(self)
        
       t@@ -1436,7 +1445,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                '''Recalculate the fee.  If the fee was manually input, retain it, but
                still build the TX to see if there are enough funds.
                '''
       -        if self.payto_e.is_lightning:
       +        if not self.is_onchain:
                    return
                freeze_fee = self.is_send_fee_frozen()
                freeze_feerate = self.is_send_feerate_frozen()
       t@@ -1448,7 +1457,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                    self.statusBar().showMessage('')
                    return
        
       -        outputs, fee_estimator, tx_desc, coins = self.read_send_tab()
       +        outputs = self.read_outputs()
       +        fee_estimator = self.get_send_fee_estimator()
       +        coins = self.get_coins()
       +
                if not outputs:
                    _type, addr = self.get_payto_or_dummy()
                    outputs = [TxOutput(_type, addr, amount)]
       t@@ -1607,15 +1619,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                    fee_estimator = None
                return fee_estimator
        
       -    def read_send_tab(self):
       -        label = self.message_e.text()
       +    def read_outputs(self):
                if self.payment_request:
                    outputs = self.payment_request.get_outputs()
                else:
                    outputs = self.payto_e.get_outputs(self.max_button.isChecked())
       -        fee_estimator = self.get_send_fee_estimator()
       -        coins = self.get_coins()
       -        return outputs, fee_estimator, label, coins
       +        return outputs
        
            def check_send_tab_outputs_and_show_errors(self, outputs) -> bool:
                """Returns whether there are errors with outputs.
       t@@ -1658,9 +1667,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
        
                return False  # no errors
        
       -    def do_preview(self):
       -        self.do_send(preview = True)
       -
            def pay_lightning_invoice(self, invoice):
                amount_sat = self.amount_e.get_amount()
                attempts = LN_NUM_PAYMENT_ATTEMPTS
       t@@ -1684,15 +1690,60 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                    e = args[0]
                    self.show_error(_('Error') + '\n' + str(e))
        
       -    def do_send(self, preview = False):
       -        if self.payto_e.is_lightning:
       +    def read_invoice(self):
       +        message = self.message_e.text()
       +        amount = self.amount_e.get_amount()
       +        if not self.is_onchain:
       +            return {
       +                'type': PR_TYPE_LN,
       +                'invoice': self.payto_e.lightning_invoice,
       +                'amount': amount,
       +                'message': message,
       +            }
       +        else:
       +            outputs = self.read_outputs()
       +            if self.check_send_tab_outputs_and_show_errors(outputs):
       +                return
       +            return self.wallet.create_invoice(outputs, message, self.payment_request, self.payto_URI)
       +
       +    def do_save_invoice(self):
       +        invoice = self.read_invoice()
       +        if not invoice:
       +            return
       +        self.wallet.save_invoice(invoice)
       +        self.do_clear()
       +        self.invoice_list.update()
       +
       +    def do_preview(self):
       +        self.do_pay(preview=True)
       +
       +    def do_pay(self, preview=False):
       +        invoice = self.read_invoice()
       +        if not invoice:
       +            return
       +        if not preview:
       +            self.wallet.save_invoice(invoice)
       +            self.do_clear()
       +            self.invoice_list.update()
       +        self.do_pay_invoice(invoice, preview)
       +
       +    def do_pay_invoice(self, invoice, preview=False):
       +        if invoice['type'] == PR_TYPE_LN:
                    self.pay_lightning_invoice(self.payto_e.lightning_invoice)
                    return
       +        elif invoice['type'] == PR_TYPE_ONCHAIN:
       +            message = invoice['message']
       +            outputs = invoice['outputs']
       +            amount = sum(map(lambda x:x[2], outputs))
       +        else:
       +            raise Exception('unknowwn invoicce type')
       +
                if run_hook('abort_send', self):
                    return
       -        outputs, fee_estimator, tx_desc, coins = self.read_send_tab()
       -        if self.check_send_tab_outputs_and_show_errors(outputs):
       -            return
       +
       +        outputs = [TxOutput(*x) for x in outputs]
       +        fee_estimator = self.get_send_fee_estimator()
       +        coins = self.get_coins()
                try:
                    is_sweep = bool(self.tx_external_keypairs)
                    tx = self.wallet.make_unsigned_transaction(
       t@@ -1724,7 +1775,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                    return
        
                if preview:
       -            self.show_transaction(tx, tx_desc)
       +            self.show_transaction(tx, message)
                    return
        
                if not self.network:
       t@@ -1764,7 +1815,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                            self.show_transaction(tx)
                            self.do_clear()
                        else:
       -                    self.broadcast_transaction(tx, tx_desc)
       +                    self.broadcast_transaction(tx, message)
                self.sign_tx_with_password(tx, sign_done, password)
        
            @protected
       t@@ -1935,8 +1986,13 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                if lnaddr.amount is not None:
                    self.amount_e.setAmount(lnaddr.amount * COIN)
                #self.amount_e.textEdited.emit("")
       -        self.payto_e.is_lightning = True
       -        self.show_send_tab_onchain_fees(False)
       +        self.set_onchain(False)
       +
       +    def set_onchain(self, b):
       +        self.is_onchain = b
       +        self.preview_button.setEnabled(b)
       +        self.max_button.setEnabled(b)
       +        self.show_send_tab_onchain_fees(b)
        
            def show_send_tab_onchain_fees(self, b: bool):
                self.feecontrol_fields.setVisible(b)
       t@@ -1951,6 +2007,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                    self.show_error(_("Error parsing URI") + f":\n{e}")
                    return
                self.show_send_tab()
       +        self.payto_URI = out
                r = out.get('r')
                sig = out.get('sig')
                name = out.get('name')
       t@@ -1977,9 +2034,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                self.max_button.setChecked(False)
                self.not_enough_funds = False
                self.payment_request = None
       +        self.payto_URI = None
                self.payto_e.is_pr = False
       -        self.payto_e.is_lightning = False
       -        self.show_send_tab_onchain_fees(True)
       +        self.is_onchain = False
       +        self.set_onchain(False)
                for e in [self.payto_e, self.message_e, self.amount_e, self.fiat_send_e,
                          self.fee_e, self.feerate_e]:
                    e.setText('')
       t@@ -1993,6 +2051,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                self.update_status()
                run_hook('do_clear', self)
        
       +
            def set_frozen_state_of_addresses(self, addrs, freeze: bool):
                self.wallet.set_frozen_state_of_addresses(addrs, freeze)
                self.address_list.update()
       t@@ -2048,6 +2107,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
        
            def spend_coins(self, coins):
                self.set_pay_from(coins)
       +        self.set_onchain(len(coins) > 0)
                self.show_send_tab()
                self.update_fee()
        
       t@@ -2095,16 +2155,19 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                self.update_completions()
        
            def show_invoice(self, key):
       -        pr = self.wallet.get_invoice(key)
       -        if pr is None:
       +        invoice = self.wallet.get_invoice(key)
       +        if invoice is None:
                    self.show_error('Cannot find payment request in wallet.')
                    return
       -        pr.verify(self.contacts)
       -        self.show_pr_details(pr)
       +        bip70 = invoice.get('bip70')
       +        if bip70:
       +            pr = paymentrequest.PaymentRequest(bytes.fromhex(bip70))
       +            pr.verify(self.contacts)
       +            self.show_bip70_details(pr)
        
       -    def show_pr_details(self, pr):
       +    def show_bip70_details(self, pr):
                key = pr.get_id()
       -        d = WindowModalDialog(self, _("Invoice"))
       +        d = WindowModalDialog(self, _("BIP70 Invoice"))
                vbox = QVBoxLayout(d)
                grid = QGridLayout()
                grid.addWidget(QLabel(_("Requestor") + ':'), 0, 0)
       t@@ -2140,7 +2203,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                vbox.addLayout(Buttons(exportButton, deleteButton, CloseButton(d)))
                d.exec_()
        
       -    def do_pay_invoice(self, key):
       +    def pay_bip70_invoice(self, key):
                pr = self.wallet.get_invoice(key)
                self.payment_request = pr
                self.prepare_for_payment_request()
   DIR diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py
       t@@ -61,7 +61,6 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
                self.errors = []
                self.is_pr = False
                self.is_alias = False
       -        self.is_lightning = False
                self.update_size()
                self.payto_address = None
                self.previous_payto = ''
       t@@ -143,6 +142,7 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
                    except:
                        pass
                    if self.payto_address:
       +                self.win.set_onchain(True)
                        self.win.lock_amount(False)
                        return
        
       t@@ -153,12 +153,13 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
                    except:
                        self.errors.append((i, line.strip()))
                        continue
       -
                    outputs.append(output)
                    if output.value == '!':
                        is_max = True
                    else:
                        total += output.value
       +        if outputs:
       +            self.win.set_onchain(True)
        
                self.win.max_button.setChecked(is_max)
                self.outputs = outputs
   DIR diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py
       t@@ -31,7 +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_TYPE_ONCHAIN, PR_TYPE_LN
        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@@ -118,35 +118,30 @@ class RequestList(MyTreeView):
                    status = req.get('status')
                    if status == PR_PAID:
                        continue
       -            is_lightning = req['type'] == PR_TYPE_LN
                    request_type = req['type']
                    timestamp = req.get('time', 0)
       +            expiration = req.get('exp', None)
                    amount = req.get('amount')
       -            message = req['message'] if is_lightning else req['memo']
       +            message = req.get('message') or req.get('memo')
                    date = format_time(timestamp)
                    amount_str = self.parent.format_amount(amount) if amount else ""
                    status_str = get_request_status(req)
                    labels = [date, message, amount_str, status_str]
       +            if request_type == PR_TYPE_LN:
       +                key = req['rhash']
       +                icon = read_QIcon("lightning.png")
       +                tooltip = 'lightning request'
       +            elif request_type == PR_TYPE_ONCHAIN:
       +                key = req['address']
       +                icon = read_QIcon("bitcoin.png")
       +                tooltip = 'onchain request'
                    items = [QStandardItem(e) for e in labels]
                    self.set_editability(items)
                    items[self.Columns.DATE].setData(request_type, ROLE_REQUEST_TYPE)
       +            items[self.Columns.DATE].setData(key, ROLE_KEY)
       +            items[self.Columns.DATE].setIcon(icon)
                    items[self.Columns.STATUS].setIcon(read_QIcon(pr_icons.get(status)))
       -            if request_type == PR_TYPE_LN:
       -                items[self.Columns.DATE].setData(req['rhash'], ROLE_KEY)
       -                items[self.Columns.DATE].setIcon(read_QIcon("lightning.png"))
       -            elif request_type == PR_TYPE_ADDRESS:
       -                address = req['address']
       -                if address not in domain:
       -                    continue
       -                expiration = req.get('exp', None)
       -                signature = req.get('sig')
       -                requestor = req.get('name', '')
       -                items[self.Columns.DATE].setData(address, ROLE_KEY)
       -                if signature is not None:
       -                    items[self.Columns.DATE].setIcon(read_QIcon("seal.png"))
       -                    items[self.Columns.DATE].setToolTip(f'signed by {requestor}')
       -                else:
       -                    items[self.Columns.DATE].setIcon(read_QIcon("bitcoin.png"))
       +            items[self.Columns.DATE].setToolTip(tooltip)
                    self.model().insertRow(self.model().rowCount(), items)
                self.filter()
                # sort requests by date
       t@@ -177,12 +172,10 @@ class RequestList(MyTreeView):
                if column == self.Columns.AMOUNT:
                    column_data = column_data.strip()
                menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.do_copy(column_title, column_data))
       -        if request_type == PR_TYPE_ADDRESS:
       -            menu.addAction(_("Copy Address"), lambda: self.parent.do_copy('Address', key))
                if request_type == PR_TYPE_LN:
       -            menu.addAction(_("Copy lightning payment request"), lambda: self.parent.do_copy('Request', req['invoice']))
       +            menu.addAction(_("Copy Request"), lambda: self.parent.do_copy('Lightning Request', req['invoice']))
                else:
       -            menu.addAction(_("Copy URI"), lambda: self.parent.do_copy('URI', req['URI']))
       +            menu.addAction(_("Copy Request"), lambda: self.parent.do_copy('Bitcoin URI', req['URI']))
                if 'view_url' in req:
                    menu.addAction(_("View in web browser"), lambda: webopen(req['view_url']))
                menu.addAction(_("Delete"), lambda: self.parent.delete_request(key))
   DIR diff --git a/electrum/paymentrequest.py b/electrum/paymentrequest.py
       t@@ -41,7 +41,6 @@ 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@@ -151,10 +150,6 @@ class PaymentRequest:
                self.memo = self.details.memo
                self.payment_url = self.details.payment_url
        
       -    def is_pr(self):
       -        return self.get_amount() != 0
       -        #return self.get_outputs() != [(TYPE_ADDRESS, self.get_requestor(), self.get_amount())]
       -
            def verify(self, contacts):
                if self.error:
                    return False
       t@@ -269,19 +264,6 @@ class PaymentRequest:
            def get_memo(self):
                return self.memo
        
       -    def get_dict(self):
       -        return {
       -            'type': PR_TYPE_BIP70,
       -            'id': self.get_id(),
       -            'requestor': self.get_requestor(),
       -            'message': self.get_memo(),
       -            'time': self.get_time(),
       -            'exp': self.get_expiration_date() - self.get_time(),
       -            'amount': self.get_amount(),
       -            'outputs': self.get_outputs(),
       -            'hex': self.raw.hex(),
       -        }
       -
            def get_id(self):
                return self.id if self.requestor else self.get_address()
        
   DIR diff --git a/electrum/util.py b/electrum/util.py
       t@@ -74,8 +74,7 @@ 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_ONCHAIN = 0
        PR_TYPE_LN = 2
        
        # status of payment requests
   DIR diff --git a/electrum/wallet.py b/electrum/wallet.py
       t@@ -41,12 +41,13 @@ from decimal import Decimal
        from typing import TYPE_CHECKING, List, Optional, Tuple, Union, NamedTuple, Sequence
        
        from .i18n import _
       +from .crypto import sha256
        from .util import (NotEnoughFunds, UserCancelled, profiler,
                           format_satoshis, format_fee_satoshis, NoDynamicFeeEstimates,
                           WalletFileException, BitcoinException,
                           InvalidPassword, format_time, timestamp_to_datetime, Satoshis,
                           Fiat, bfh, bh2u, TxMinedInfo, quantize_feerate, create_bip21_uri, OrderedDictWithIndex)
       -from .util import PR_TYPE_ADDRESS, PR_TYPE_BIP70, PR_TYPE_LN
       +from .util import PR_TYPE_ONCHAIN, PR_TYPE_LN
        from .simple_config import SimpleConfig
        from .bitcoin import (COIN, TYPE_ADDRESS, is_address, address_to_script,
                              is_minikey, relayfee, dust_threshold)
       t@@ -505,22 +506,47 @@ class Abstract_Wallet(AddressSynchronizer):
                        'txpos_in_block': hist_item.tx_mined_status.txpos,
                    }
        
       +    def create_invoice(self, outputs, message, pr, URI):
       +        amount = sum(x[2] for x in outputs)
       +        invoice = {
       +            'type': PR_TYPE_ONCHAIN,
       +            'message': message,
       +            'outputs': outputs,
       +            'amount': amount,
       +        }
       +        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())
       +        return invoice
       +
            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')
       +        elif invoice_type == PR_TYPE_ONCHAIN:
       +            key = bh2u(sha256(repr(invoice))[0:16])
       +            invoice['id'] = key
       +            invoice['txid'] = None
                    self.invoices[key] = invoice
                    self.storage.put('invoices', self.invoices)
                    self.storage.write()
       +        else:
       +            raise Exception('Unsupported invoice type')
       +
       +    def clear_invoices(self):
       +        self.invoices = {}
       +        self.storage.put('invoices', self.invoices)
       +        self.storage.write()
        
            def get_invoices(self):
                out = [self.get_invoice(key) for key in self.invoices.keys()]
       t@@ -1284,7 +1310,7 @@ class Abstract_Wallet(AddressSynchronizer):
                if not r:
                    return
                out = copy.copy(r)
       -        out['type'] = PR_TYPE_ADDRESS
       +        out['type'] = PR_TYPE_ONCHAIN
                out['URI'] = self.get_request_URI(addr)
                status, conf = self.get_request_status(addr)
                out['status'] = status
       t@@ -1362,9 +1388,10 @@ class Abstract_Wallet(AddressSynchronizer):
                        self.network.trigger_callback('payment_received', self, addr, status)
        
            def make_payment_request(self, addr, amount, message, expiration):
       +        from .bitcoin import TYPE_ADDRESS
                timestamp = int(time.time())
                _id = bh2u(sha256d(addr + "%d"%timestamp))[0:10]
       -        r = {'time':timestamp, 'amount':amount, 'exp':expiration, 'address':addr, 'memo':message, 'id':_id}
       +        r = {'time':timestamp, 'amount':amount, 'exp':expiration, 'address':addr, 'memo':message, 'id':_id, 'outputs': [(TYPE_ADDRESS, addr, amount)]}
                return r
        
            def sign_payment_request(self, key, alias, alias_addr, password):