URI: 
       tlightning GUI: use existing receive and send tabs with lightning invoices - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 12d3877873128fa3872070a3df5be5501e2fd871
   DIR parent 7d2a6d83d5fce332674375586acd8a43641aaa60
  HTML Author: ThomasV <thomasv@electrum.org>
       Date:   Thu, 31 May 2018 12:38:02 +0200
       
       lightning GUI: use existing receive and send tabs with lightning invoices
       
       Diffstat:
         M electrum/gui/qt/main_window.py      |     129 ++++++++++++++++---------------
         M electrum/gui/qt/paytoedit.py        |       9 +++++----
         M electrum/gui/qt/request_list.py     |      40 +++++++++++++++++++++++++-------
         M gui/qt/lightning_channels_list.py   |      22 +++++++++++-----------
         A icons/lightning.png                 |       0 
         M lib/lnworker.py                     |      32 ++++++++++++++++++++++++++++---
       
       6 files changed, 144 insertions(+), 88 deletions(-)
       ---
   DIR diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
       t@@ -88,7 +88,6 @@ from .util import (read_QIcon, ColorScheme, text_dialog, icon_path, WaitingDialo
        from .installwizard import WIF_HELP_TEXT
        from .history_list import HistoryList, HistoryModel
        from .update_checker import UpdateCheck, UpdateCheckThread
       -from .lightning_invoice_list import LightningInvoiceList
        from .lightning_channels_list import LightningChannelsList
        
        
       t@@ -177,11 +176,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                tabs.addTab(self.send_tab, QIcon(":icons/tab_send.png"), _('Send'))
                tabs.addTab(self.receive_tab, QIcon(":icons/tab_receive.png"), _('Receive'))
                if config.get("lnbase", False):
       -            self.lightning_invoices_tab = self.create_lightning_invoices_tab(wallet)
       -            tabs.addTab(self.lightning_invoices_tab, _("Lightning Invoices"))
       -
                    self.lightning_channels_tab = self.create_lightning_channels_tab(wallet)
       -            tabs.addTab(self.lightning_channels_tab, _("Lightning Channels"))
       +            tabs.addTab(self.lightning_channels_tab, QIcon(":icons/lightning.png"), _("Channels"))
        
                def add_optional_tab(tabs, tab, icon, description, name):
                    tab.tab_icon = icon
       t@@ -881,10 +877,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                self.invoice_list.update()
                self.update_completions()
        
       -    def create_lightning_invoices_tab(self, wallet):
       -        self.lightning_invoice_list = LightningInvoiceList(self, wallet.lnworker)
       -        return self.lightning_invoice_list
       -
            def create_lightning_channels_tab(self, wallet):
                self.lightning_channels_list = LightningChannelsList(self, wallet.lnworker)
                return self.lightning_channels_list
       t@@ -915,17 +907,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                grid.setSpacing(8)
                grid.setColumnStretch(3, 1)
        
       -        self.receive_address_e = ButtonsLineEdit()
       -        self.receive_address_e.addCopyButton(self.app)
       -        self.receive_address_e.setReadOnly(True)
       -        msg = _('Bitcoin address where the payment should be received. Note that each payment request uses a different Bitcoin address.')
       -        self.receive_address_label = HelpLabel(_('Receiving address'), msg)
       -        self.receive_address_e.textChanged.connect(self.update_receive_qr)
       -        self.receive_address_e.textChanged.connect(self.update_receive_address_styling)
       -        self.receive_address_e.setFocusPolicy(Qt.ClickFocus)
       -        grid.addWidget(self.receive_address_label, 0, 0)
       -        grid.addWidget(self.receive_address_e, 0, 1, 1, -1)
       -
                self.receive_message_e = QLineEdit()
                grid.addWidget(QLabel(_('Description')), 1, 0)
                grid.addWidget(self.receive_message_e, 1, 1, 1, -1)
       t@@ -960,23 +941,31 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                self.expires_label.hide()
                grid.addWidget(self.expires_label, 3, 1)
        
       -        self.save_request_button = QPushButton(_('Save'))
       -        self.save_request_button.clicked.connect(self.save_payment_request)
       +        self.receive_type = QComboBox()
       +        self.receive_type.addItems([_('Bitcoin address'), _('Lightning')])
       +        grid.addWidget(QLabel(_('Type')), 4, 0)
       +        grid.addWidget(self.receive_type, 4, 1)
       +
       +        self.save_request_button = QPushButton(_('Create'))
       +        self.save_request_button.clicked.connect(self.create_invoice)
        
       -        self.new_request_button = QPushButton(_('New'))
       -        self.new_request_button.clicked.connect(self.new_payment_request)
       +        self.receive_buttons = buttons = QHBoxLayout()
       +        buttons.addWidget(self.save_request_button)
       +        buttons.addStretch(1)
       +        grid.addLayout(buttons, 4, 2, 1, 2)
       +
       +        self.receive_address_e = ButtonsTextEdit()
       +        self.receive_address_e.addCopyButton(self.app)
       +        self.receive_address_e.setReadOnly(True)
       +        self.receive_address_e.textChanged.connect(self.update_receive_qr)
       +        self.receive_address_e.textChanged.connect(self.update_receive_address_styling)
       +        self.receive_address_e.setFocusPolicy(Qt.ClickFocus)
        
                self.receive_qr = QRCodeWidget(fixedSize=200)
                self.receive_qr.mouseReleaseEvent = lambda x: self.toggle_qr_window()
                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_buttons = buttons = QHBoxLayout()
       -        buttons.addStretch(1)
       -        buttons.addWidget(self.save_request_button)
       -        buttons.addWidget(self.new_request_button)
       -        grid.addLayout(buttons, 4, 1, 1, 2)
       -
                self.receive_requests_label = QLabel(_('Requests'))
        
                from .request_list import RequestList
       t@@ -987,14 +976,19 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                vbox_g.addLayout(grid)
                vbox_g.addStretch()
        
       +        hbox_r = QHBoxLayout()
       +        hbox_r.addWidget(self.receive_qr)
       +        hbox_r.addWidget(self.receive_address_e)
       +
                hbox = QHBoxLayout()
                hbox.addLayout(vbox_g)
       -        hbox.addWidget(self.receive_qr)
       +        hbox.addLayout(hbox_r)
        
                w = QWidget()
                w.searchable_list = self.request_list
                vbox = QVBoxLayout(w)
                vbox.addLayout(hbox)
       +
                vbox.addStretch(1)
                vbox.addWidget(self.receive_requests_label)
                vbox.addWidget(self.request_list)
       t@@ -1047,15 +1041,34 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                        else:
                            return
        
       -    def save_payment_request(self):
       -        addr = str(self.receive_address_e.text())
       +    def create_invoice(self):
                amount = self.receive_amount_e.get_amount()
                message = self.receive_message_e.text()
       -        if not message and not amount:
       -            self.show_error(_('No message or amount'))
       -            return False
                i = self.expires_combo.currentIndex()
                expiration = list(map(lambda x: x[1], expiration_values))[i]
       +        if self.receive_type.currentIndex() == 1:
       +            self.create_lightning_request(amount, message, expiration)
       +        else:
       +            self.create_bitcoin_request(amount, message, expiration)
       +        self.request_list.update()
       +
       +    def create_lightning_request(self, amount, message, expiration):
       +        req = self.wallet.lnworker.add_invoice(amount)
       +
       +    def create_bitcoin_request(self, amount, message, expiration):
       +        addr = self.wallet.get_unused_address()
       +        if addr is None:
       +            if not self.wallet.is_deterministic():
       +                msg = [
       +                    _('No more addresses in your wallet.'),
       +                    _('You are using a non-deterministic wallet, which cannot create new addresses.'),
       +                    _('If you want to create new addresses, use a deterministic wallet instead.')
       +                   ]
       +                self.show_message(' '.join(msg))
       +                return
       +            if not self.question(_("Warning: The next address will not be recovered automatically if you restore your wallet from seed; you may need to add it manually.\n\nThis occurs because you have too many unused addresses in your wallet. To avoid this situation, use the existing addresses first.\n\nCreate anyway?")):
       +                return
       +            addr = self.wallet.create_new_address(False)
                req = self.wallet.make_payment_request(addr, amount, message, expiration)
                try:
                    self.wallet.add_payment_request(req, self.config)
       t@@ -1066,7 +1079,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                    self.sign_payment_request(addr)
                    self.save_request_button.setEnabled(False)
                finally:
       -            self.request_list.update()
                    self.address_list.update()
        
            def view_and_paste(self, title, msg, data):
       t@@ -1092,26 +1104,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                    self.show_message(_("Request saved successfully"))
                    self.saved = True
        
       -    def new_payment_request(self):
       -        addr = self.wallet.get_unused_address()
       -        if addr is None:
       -            if not self.wallet.is_deterministic():
       -                msg = [
       -                    _('No more addresses in your wallet.'),
       -                    _('You are using a non-deterministic wallet, which cannot create new addresses.'),
       -                    _('If you want to create new addresses, use a deterministic wallet instead.')
       -                   ]
       -                self.show_message(' '.join(msg))
       -                return
       -            if not self.question(_("Warning: The next address will not be recovered automatically if you restore your wallet from seed; you may need to add it manually.\n\nThis occurs because you have too many unused addresses in your wallet. To avoid this situation, use the existing addresses first.\n\nCreate anyway?")):
       -                return
       -            addr = self.wallet.create_new_address(False)
       -        self.set_receive_address(addr)
       -        self.expires_label.hide()
       -        self.expires_combo.show()
       -        self.new_request_button.setEnabled(False)
       -        self.receive_message_e.setFocus(1)
       -
            def set_receive_address(self, addr):
                self.receive_address_e.setText(addr)
                self.receive_message_e.setText('')
       t@@ -1158,11 +1150,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                self.new_request_button.setEnabled(True)
        
            def update_receive_qr(self):
       -        addr = str(self.receive_address_e.text())
       -        amount = self.receive_amount_e.get_amount()
       -        message = self.receive_message_e.text()
       -        self.save_request_button.setEnabled((amount is not None) or (message != ""))
       -        uri = util.create_bip21_uri(addr, amount, message)
       +        uri = str(self.receive_address_e.text())
                self.receive_qr.setData(uri)
                if self.qr_window and self.qr_window.isVisible():
                    self.qr_window.qrw.setData(uri)
       t@@ -1876,6 +1864,23 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                else:
                    self.payment_request_error_signal.emit()
        
       +    def parse_lightning_invoice(self, invoice):
       +        from electrum.lightning_payencode.lnaddr import lndecode
       +        lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
       +        pubkey = bh2u(lnaddr.pubkey.serialize())
       +        for k,v in lnaddr.tags:
       +            if k == 'd':
       +                description = v
       +                break
       +        else:
       +             description = ''
       +        self.payto_e.setFrozen(True)
       +        self.payto_e.setGreen()
       +        self.payto_e.setText(pubkey)
       +        self.message_e.setText(description)
       +        self.amount_e.setAmount(lnaddr.amount)
       +        #self.amount_e.textEdited.emit("")
       +
            def pay_to_URI(self, URI):
                if not URI:
                    return
   DIR diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py
       t@@ -61,10 +61,8 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
                self.errors = []
                self.is_pr = False
                self.is_alias = False
       -        self.scan_f = win.pay_to_URI
                self.update_size()
                self.payto_address = None
       -
                self.previous_payto = ''
        
            def setFrozen(self, b):
       t@@ -130,7 +128,10 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
                if len(lines) == 1:
                    data = lines[0]
                    if data.startswith("bitcoin:"):
       -                self.scan_f(data)
       +                self.win.pay_to_URI(data)
       +                return
       +            if data.startswith("ln"):
       +                self.win.parse_lightning_invoice(data)
                        return
                    try:
                        self.payto_address = self.parse_output(data)
       t@@ -204,7 +205,7 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
            def qr_input(self):
                data = super(PayToEdit,self).qr_input()
                if data.startswith("bitcoin:"):
       -            self.scan_f(data)
       +            self.win.pay_to_URI(data)
                    # TODO: update fee
        
            def resolve(self):
   DIR diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py
       t@@ -79,12 +79,19 @@ class RequestList(MyTreeView):
                amount = req['amount']
                message = req['memo']
                self.parent.receive_address_e.setText(addr)
       -        self.parent.receive_message_e.setText(message)
       -        self.parent.receive_amount_e.setAmount(amount)
       -        self.parent.expires_combo.hide()
       -        self.parent.expires_label.show()
       -        self.parent.expires_label.setText(expires)
       -        self.parent.new_request_button.setEnabled(True)
       +        #req = self.wallet.receive_requests.get(addr)
       +        #if req is None:
       +        #    self.update()
       +        #    return
       +        #expires = age(req['time'] + req['exp']) if req.get('exp') else _('Never')
       +        #amount = req['amount']
       +        #message = self.wallet.labels.get(addr, '')
       +        #self.parent.receive_message_e.setText(message)
       +        #self.parent.receive_amount_e.setAmount(amount)
       +        #self.parent.expires_combo.hide()
       +        #self.parent.expires_label.show()
       +        #self.parent.expires_label.setText(expires)
       +        #self.parent.new_request_button.setEnabled(True)
        
            def update(self):
                self.wallet = self.parent.wallet
       t@@ -98,7 +105,7 @@ class RequestList(MyTreeView):
                        self.parent.expires_combo.show()
        
                # update the receive address if necessary
       -        current_address = self.parent.receive_address_e.text()
       +        #current_address = self.parent.receive_address_e.text()
                domain = self.wallet.get_receiving_addresses()
                try:
                    addr = self.wallet.get_unused_address()
       t@@ -126,7 +133,8 @@ class RequestList(MyTreeView):
                    signature = req.get('sig')
                    requestor = req.get('name', '')
                    amount_str = self.parent.format_amount(amount) if amount else ""
       -            labels = [date, address, '', message, amount_str, pr_tooltips.get(status,'')]
       +            URI = self.parent.get_request_URI(address)
       +            labels = [date, URI, '', message, amount_str, pr_tooltips.get(status,'')]
                    items = [QStandardItem(e) for e in labels]
                    self.set_editability(items)
                    if signature is not None:
       t@@ -137,6 +145,22 @@ class RequestList(MyTreeView):
                    items[self.Columns.DESCRIPTION].setData(address, Qt.UserRole)
                    self.model().insertRow(self.model().rowCount(), items)
                self.filter()
       +        # lightning
       +        for k, r in self.wallet.lnworker.invoices.items():
       +            from electrum.lightning_payencode.lnaddr import lndecode
       +            import electrum.constants as constants
       +            lnaddr = lndecode(r, expected_hrp=constants.net.SEGWIT_HRP)
       +            amount_str = self.parent.format_amount(lnaddr.amount*100000000)
       +            for k,v in lnaddr.tags:
       +                if k == 'd':
       +                    description = v
       +                    break
       +                else:
       +                    description = ''
       +            labels = [date, r, '', description, amount_str, '']
       +            items = [QStandardItem(e) for e in labels]
       +            items.setIcon(2, QIcon(":icons/lightning.png"))
       +            self.model().insertRow(self.model().rowCount(), items)
        
            def create_menu(self, position):
                idx = self.indexAt(position)
   DIR diff --git a/gui/qt/lightning_channels_list.py b/gui/qt/lightning_channels_list.py
       t@@ -47,6 +47,14 @@ class LightningChannelsList(QtWidgets.QWidget):
                assert local_amt >= push_amt
                obj = self.lnworker.open_channel(node_id, local_amt, push_amt, password)
        
       +    def create_menu(self, position):
       +        menu = QtWidgets.QMenu()
       +        cur = self._tv.currentItem()
       +        def close():
       +            print("closechannel result", lnworker.close_channel_from_other_thread(cur.di))
       +        menu.addAction("Close channel", close)
       +        menu.exec_(self._tv.viewport().mapToGlobal(position))
       +
            @QtCore.pyqtSlot(dict)
            def do_update_single_row(self, new):
                try:
       t@@ -60,14 +68,6 @@ class LightningChannelsList(QtWidgets.QWidget):
                        except KeyError:
                            obj[k] = v
        
       -    def create_menu(self, position):
       -        menu = QtWidgets.QMenu()
       -        cur = self._tv.currentItem()
       -        def close():
       -            print("closechannel result", lnworker.close_channel_from_other_thread(cur.di))
       -        menu.addAction("Close channel", close)
       -        menu.exec_(self._tv.viewport().mapToGlobal(position))
       -
            @QtCore.pyqtSlot(dict)
            def do_update_rows(self, obj):
                self._tv.clear()
       t@@ -82,9 +82,8 @@ class LightningChannelsList(QtWidgets.QWidget):
                self.update_single_row.connect(self.do_update_single_row)
        
                self.lnworker = lnworker
       -
       -        #lnworker.subscribe_channel_list_updates_from_other_thread(self.update_rows.emit)
       -        #lnworker.subscribe_single_channel_update_from_other_thread(self.update_single_row.emit)
       +        lnworker.register_callback(self.update_rows.emit, ['channels_updated'])
       +        lnworker.register_callback(self.update_single_row.emit, ['channel_updated'])
        
                self._tv=QtWidgets.QTreeWidget(self)
                self._tv.setHeaderLabels([mapping[i] for i in range(len(mapping))])
       t@@ -122,3 +121,4 @@ class LightningChannelsList(QtWidgets.QWidget):
                l.addWidget(self._tv)
        
                self.resize(2500,1000)
       +        lnworker.on_channels_updated()
   DIR diff --git a/icons/lightning.png b/icons/lightning.png
       Binary files differ.
   DIR diff --git a/lib/lnworker.py b/lib/lnworker.py
       t@@ -8,7 +8,8 @@ import os
        from decimal import Decimal
        import binascii
        import asyncio
       -
       +import threading
       +from collections import defaultdict
        
        from . import constants
        from .bitcoin import sha256, COIN
       t@@ -109,6 +110,8 @@ class LNWorker(PrintError):
                self.channel_state = {chan.channel_id: "OPENING" for chan in self.channels}
                for host, port, pubkey in peer_list:
                    self.add_peer(host, int(port), pubkey)
       +
       +        self.callbacks = defaultdict(list)
                # wait until we see confirmations
                self.network.register_callback(self.on_network_update, ['updated', 'verified']) # thread safe
                self.on_network_update('updated') # shortcut (don't block) if funding tx locked and verified
       t@@ -119,6 +122,7 @@ class LNWorker(PrintError):
                peer = Peer(host, int(port), node_id, self.privkey, self.network, self.channel_db, self.path_finder, self.channel_state, channels, self.invoices, request_initial_sync=True)
                self.network.futures.append(asyncio.run_coroutine_threadsafe(peer.main_loop(), asyncio.get_event_loop()))
                self.peers[node_id] = peer
       +        self.lock = threading.Lock()
        
            def save_channel(self, openchannel):
                if openchannel.channel_id not in self.channel_state:
       t@@ -127,6 +131,7 @@ class LNWorker(PrintError):
                dumped = serialize_channels(self.channels)
                self.wallet.storage.put("channels", dumped)
                self.wallet.storage.write()
       +        self.trigger_callback('channel_updated', {"chan_id": openchannel.channel_id})
        
            def save_short_chan_id(self, chan):
                """
       t@@ -176,6 +181,11 @@ class LNWorker(PrintError):
                openingchannel = await peer.channel_establishment_flow(self.wallet, self.config, password, amount_sat, push_sat * 1000, temp_channel_id=os.urandom(32))
                self.print_error("SAVING OPENING CHANNEL")
                self.save_channel(openingchannel)
       +        self.on_channels_updated()
       +
       +    def on_channels_updated(self):
       +        std_chan = [{"chan_id": chan.channel_id} for chan in self.channels]
       +        self.trigger_callback('channels_updated', {'channels':std_chan})
        
            def open_channel(self, node_id, local_amt_sat, push_amt_sat, pw):
                coro = self._open_channel_coroutine(node_id, local_amt_sat, push_amt_sat, None if pw == "" else pw)
       t@@ -199,8 +209,8 @@ class LNWorker(PrintError):
            def add_invoice(self, amount_sat, message='one cup of coffee'):
                is_open = lambda chan: self.channel_state[chan] == "OPEN"
                # TODO doesn't account for fees!!!
       -        if not any(openchannel.remote_state.amount_msat >= amount_sat * 1000 for openchannel in self.channels if is_open(chan)):
       -            return "Not making invoice, no channel has enough balance"
       +        #if not any(openchannel.remote_state.amount_msat >= amount_sat * 1000 for openchannel in self.channels if is_open(chan)):
       +        #    return "Not making invoice, no channel has enough balance"
                payment_preimage = os.urandom(32)
                RHASH = sha256(payment_preimage)
                pay_req = lnencode(LnAddr(RHASH, amount_sat/Decimal(COIN), tags=[('d', message)]), self.privkey)
       t@@ -213,3 +223,19 @@ class LNWorker(PrintError):
        
            def list_channels(self):
                return serialize_channels(self.channels)
       +
       +    def register_callback(self, callback, events):
       +        with self.lock:
       +            for event in events:
       +                self.callbacks[event].append(callback)
       +
       +    def unregister_callback(self, callback):
       +        with self.lock:
       +            for callbacks in self.callbacks.values():
       +                if callback in callbacks:
       +                    callbacks.remove(callback)
       +
       +    def trigger_callback(self, event, *args):
       +        with self.lock:
       +            callbacks = self.callbacks[event][:]
       +        [callback(*args) for callback in callbacks]