electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 12d3877873128fa3872070a3df5be5501e2fd871
parent 7d2a6d83d5fce332674375586acd8a43641aaa60
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:
Melectrum/gui/qt/main_window.py | 129+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Melectrum/gui/qt/paytoedit.py | 9+++++----
Melectrum/gui/qt/request_list.py | 40++++++++++++++++++++++++++++++++--------
Mgui/qt/lightning_channels_list.py | 22+++++++++++-----------
Aicons/lightning.png | 0
Mlib/lnworker.py | 32+++++++++++++++++++++++++++++---
6 files changed, 144 insertions(+), 88 deletions(-)

diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py @@ -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 @@ -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 @@ -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 @@ -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) @@ -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 @@ -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) @@ -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) @@ -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): @@ -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('') @@ -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) @@ -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 diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py @@ -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): @@ -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) @@ -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): diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py @@ -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 @@ -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() @@ -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: @@ -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) diff --git a/gui/qt/lightning_channels_list.py b/gui/qt/lightning_channels_list.py @@ -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: @@ -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() @@ -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))]) @@ -122,3 +121,4 @@ class LightningChannelsList(QtWidgets.QWidget): l.addWidget(self._tv) self.resize(2500,1000) + lnworker.on_channels_updated() diff --git a/icons/lightning.png b/icons/lightning.png Binary files differ. diff --git a/lib/lnworker.py b/lib/lnworker.py @@ -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 @@ -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 @@ -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: @@ -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): """ @@ -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) @@ -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) @@ -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]