diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
index e59e55da1..11c2fbc20 100644
--- a/electrum/gui/qt/main_window.py
+++ b/electrum/gui/qt/main_window.py
@@ -73,7 +73,6 @@ from .transaction_dialog import show_transaction
from .fee_slider import FeeSlider
from .util import *
from .installwizard import WIF_HELP_TEXT
-from .lightning_invoice_list import LightningInvoiceList
from .lightning_channels_list import LightningChannelsList
@@ -162,11 +161,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
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
@@ -809,10 +805,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
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
@@ -842,16 +834,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
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.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)
@@ -886,23 +868,30 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
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.new_request_button = QPushButton(_('New'))
- self.new_request_button.clicked.connect(self.new_payment_request)
+ self.save_request_button = QPushButton(_('Create'))
+ self.save_request_button.clicked.connect(self.create_invoice)
+
+ 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.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
@@ -913,14 +902,19 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
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)
@@ -939,8 +933,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
message = self.wallet.labels.get(addr, '')
amount = req['amount']
URI = util.create_URI(addr, amount, message)
- if req.get('time'):
- URI += "&time=%d"%req.get('time')
+ #if req.get('time'):
+ # URI += "&time=%d"%req.get('time')
if req.get('exp'):
URI += "&exp=%d"%req.get('exp')
if req.get('name') and req.get('sig'):
@@ -971,15 +965,34 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
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)
@@ -990,7 +1003,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
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):
@@ -1016,26 +1028,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
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('')
@@ -1078,14 +1070,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
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_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.set_content(addr, amount, message, uri)
+ self.qr_window.set_content(uri, '', '', uri)
def set_feerounding_text(self, num_satoshis_added):
self.feerounding_text = (_('Additional {} satoshis are going to be added.')
@@ -1764,6 +1752,23 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
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
index 6235b85cc..2b6bfce7b 100644
--- a/electrum/gui/qt/paytoedit.py
+++ b/electrum/gui/qt/paytoedit.py
@@ -58,10 +58,8 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, PrintError):
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):
@@ -129,7 +127,10 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, PrintError):
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)
@@ -203,7 +204,7 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, PrintError):
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
index 19ec59703..ed786e726 100644
--- a/electrum/gui/qt/request_list.py
+++ b/electrum/gui/qt/request_list.py
@@ -25,7 +25,7 @@
from PyQt5.QtGui import *
from PyQt5.QtCore import *
-from PyQt5.QtWidgets import QTreeWidgetItem, QMenu
+from PyQt5.QtWidgets import QTreeWidgetItem, QMenu, QHeaderView
from electrum.i18n import _
from electrum.util import format_time, age
@@ -35,38 +35,47 @@ from electrum.paymentrequest import PR_UNKNOWN
from .util import MyTreeWidget, pr_tooltips, pr_icons
-class RequestList(MyTreeWidget):
- filter_columns = [0, 1, 2, 3, 4] # Date, Account, Address, Description, Amount
+class RequestList(MyTreeWidget):
+ filter_columns = [0, 2, 3, 4] # Date, Address, Description, Amount
def __init__(self, parent):
- MyTreeWidget.__init__(self, parent, self.create_menu, [_('Date'), _('Address'), '', _('Description'), _('Amount'), _('Status')], 3)
+ MyTreeWidget.__init__(self, parent, self.create_menu, [_('Date'), _('Type'), _('Address'), _('Description'), _('Amount'), _('Status')], 3)
self.currentItemChanged.connect(self.item_changed)
self.itemClicked.connect(self.item_changed)
self.setSortingEnabled(True)
self.setColumnWidth(0, 180)
- self.hideColumn(1)
+ self.setColumnWidth(2, 250)
+
+ def update_headers(self, headers):
+ self.setColumnCount(len(headers))
+ self.setHeaderLabels(headers)
+ self.header().setStretchLastSection(False)
+ for col in range(len(headers)):
+ if col in [2]: continue
+ sm = QHeaderView.Stretch if col == self.stretch_column else QHeaderView.ResizeToContents
+ self.header().setSectionResizeMode(col, sm)
def item_changed(self, item):
if item is None:
return
if not item.isSelected():
return
- addr = str(item.text(1))
- 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, '')
+ addr = str(item.text(2))
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 on_update(self):
self.wallet = self.parent.wallet
@@ -79,12 +88,12 @@ class RequestList(MyTreeWidget):
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()
- addr = self.wallet.get_unused_address()
- if not current_address in domain and addr:
- self.parent.set_receive_address(addr)
- self.parent.new_request_button.setEnabled(addr != current_address)
+ #addr = self.wallet.get_unused_address()
+ #if not current_address in domain and addr:
+ # self.parent.set_receive_address(addr)
+ #self.parent.new_request_button.setEnabled(addr != current_address)
# clear the list and fill it again
self.clear()
@@ -101,13 +110,29 @@ class RequestList(MyTreeWidget):
signature = req.get('sig')
requestor = req.get('name', '')
amount_str = self.parent.format_amount(amount) if amount else ""
- item = QTreeWidgetItem([date, address, '', message, amount_str, pr_tooltips.get(status,'')])
+ URI = self.parent.get_request_URI(address)
+ item = QTreeWidgetItem([date, '', URI, message, amount_str, pr_tooltips.get(status,'')])
if signature is not None:
item.setIcon(2, self.icon_cache.get(":icons/seal.png"))
item.setToolTip(2, 'signed by '+ requestor)
if status is not PR_UNKNOWN:
item.setIcon(6, self.icon_cache.get(pr_icons.get(status)))
self.addTopLevelItem(item)
+ # 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 = ''
+ item = QTreeWidgetItem([date, '', r, description, amount_str, ''])
+ item.setIcon(1, QIcon(":icons/lightning.png"))
+ self.addTopLevelItem(item)
def create_menu(self, position):
diff --git a/gui/qt/lightning_channels_list.py b/gui/qt/lightning_channels_list.py
index 7b75321e3..09e0d4812 100644
--- 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.qrc b/icons.qrc
index 19c298adc..67db4fe5b 100644
--- a/icons.qrc
+++ b/icons.qrc
@@ -22,6 +22,7 @@
icons/key.png
icons/ledger.png
icons/ledger_unpaired.png
+ icons/lightning.png
icons/lock.png
icons/microphone.png
icons/network.png
diff --git a/icons/lightning.png b/icons/lightning.png
new file mode 100644
index 000000000..797d3ca93
Binary files /dev/null and b/icons/lightning.png differ
diff --git a/lib/lnworker.py b/lib/lnworker.py
index 701e98bb2..c8fa43f09 100644
--- 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]