commit 90ce9f195b97978becdabd62e618dda9b8daba42
parent a201ed44df7c6c530b27709eb0e73b36237ae766
Author: ThomasV <thomasv@electrum.org>
Date: Sun, 13 Oct 2019 20:34:38 +0200
Allow user to enable lightning in the GUI. Make it a per-wallet setting.
Diffstat:
12 files changed, 90 insertions(+), 76 deletions(-)
diff --git a/electrum/commands.py b/electrum/commands.py
@@ -623,6 +623,12 @@ class Commands:
return json_encode(wallet.get_detailed_history(**kwargs))
@command('w')
+ async def init_lightning(self, wallet: Abstract_Wallet = None):
+ """Enable lightning payments"""
+ wallet.init_lightning()
+ return "Lightning keys have been created."
+
+ @command('w')
async def lightning_history(self, show_fiat=False, wallet: Abstract_Wallet = None):
""" lightning history """
lightning_history = wallet.lnworker.get_history() if wallet.lnworker else []
@@ -1127,8 +1133,6 @@ def add_global_options(parser):
group.add_argument("--testnet", action="store_true", dest="testnet", default=False, help="Use Testnet")
group.add_argument("--regtest", action="store_true", dest="regtest", default=False, help="Use Regtest")
group.add_argument("--simnet", action="store_true", dest="simnet", default=False, help="Use Simnet")
- group.add_argument("--lightning", action="store_true", dest="lightning", default=False, help="Enable lightning")
- group.add_argument("--reckless", action="store_true", dest="reckless", default=False, help="Allow to enable lightning on mainnet")
group.add_argument("-o", "--offline", action="store_true", dest="offline", default=False, help="Run offline")
def add_wallet_option(parser):
diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py
@@ -341,9 +341,6 @@ class ElectrumWindow(App):
self.gui_object = kwargs.get('gui_object', None) # type: ElectrumGui
self.daemon = self.gui_object.daemon
self.fx = self.daemon.fx
-
- self.is_lightning_enabled = bool(config.get('lightning'))
-
self.use_rbf = config.get('use_rbf', True)
self.use_unconfirmed = not config.get('confirmed_only', False)
@@ -558,7 +555,7 @@ class ElectrumWindow(App):
self.network.register_callback(self.on_fee_histogram, ['fee_histogram'])
self.network.register_callback(self.on_quotes, ['on_quotes'])
self.network.register_callback(self.on_history, ['on_history'])
- self.network.register_callback(self.on_channels, ['channels'])
+ self.network.register_callback(self.on_channels, ['channels_updated'])
self.network.register_callback(self.on_channel, ['channel'])
self.network.register_callback(self.on_invoice_status, ['invoice_status'])
self.network.register_callback(self.on_request_status, ['request_status'])
@@ -687,7 +684,7 @@ class ElectrumWindow(App):
if self._channels_dialog:
Clock.schedule_once(lambda dt: self._channels_dialog.update())
- def on_channels(self, evt):
+ def on_channels(self, evt, wallet):
if self._channels_dialog:
Clock.schedule_once(lambda dt: self._channels_dialog.update())
diff --git a/electrum/gui/kivy/uix/ui_screens/receive.kv b/electrum/gui/kivy/uix/ui_screens/receive.kv
@@ -91,7 +91,7 @@ ReceiveScreen:
id: address_label
text: _('Lightning') if root.is_lightning else (s.address if s.address else _('Bitcoin Address'))
shorten: True
- on_release: root.is_lightning = not root.is_lightning if app.is_lightning_enabled else False
+ on_release: root.is_lightning = not root.is_lightning if app.wallet.has_lightning() else False
CardSeparator:
opacity: message_selection.opacity
color: blue_bottom.foreground_color
diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py
@@ -155,9 +155,8 @@ class ElectrumGui(Logger):
else:
m = self.tray.contextMenu()
m.clear()
- if self.config.get('lightning'):
- m.addAction(_("Lightning"), self.show_lightning_dialog)
- m.addAction(_("Watchtower"), self.show_watchtower_dialog)
+ m.addAction(_("Lightning"), self.show_lightning_dialog)
+ m.addAction(_("Watchtower"), self.show_watchtower_dialog)
for window in self.windows:
name = window.wallet.basename()
submenu = m.addMenu(name)
diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py
@@ -10,6 +10,7 @@ from PyQt5.QtWidgets import QMenu, QHBoxLayout, QLabel, QVBoxLayout, QGridLayout
from electrum.util import inv_dict, bh2u, bfh
from electrum.i18n import _
from electrum.lnchannel import Channel
+from electrum.wallet import Abstract_Wallet
from electrum.lnutil import LOCAL, REMOTE, ConnStringFormatError, format_short_channel_id
from .util import MyTreeView, WindowModalDialog, Buttons, OkButton, CancelButton, EnterButton, WWLabel, WaitingDialog
@@ -21,7 +22,7 @@ ROLE_CHANNEL_ID = Qt.UserRole
class ChannelsList(MyTreeView):
- update_rows = QtCore.pyqtSignal()
+ update_rows = QtCore.pyqtSignal(Abstract_Wallet)
update_single_row = QtCore.pyqtSignal(Channel)
class Columns(IntEnum):
@@ -121,8 +122,10 @@ class ChannelsList(MyTreeView):
for column, v in enumerate(self.format_fields(chan)):
self.model().item(row, column).setData(v, QtCore.Qt.DisplayRole)
- @QtCore.pyqtSlot()
- def do_update_rows(self):
+ @QtCore.pyqtSlot(Abstract_Wallet)
+ def do_update_rows(self, wallet):
+ if wallet != self.parent.wallet:
+ return
self.model().clear()
self.update_headers(self.headers)
for chan in self.parent.wallet.lnworker.channels.values():
diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
@@ -196,7 +196,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
tabs.addTab(tab, icon, description.replace("&", ""))
add_optional_tab(tabs, self.addresses_tab, read_QIcon("tab_addresses.png"), _("&Addresses"), "addresses")
- if self.config.get('lightning'):
+ if self.wallet.has_lightning():
add_optional_tab(tabs, self.channels_tab, read_QIcon("lightning.png"), _("Channels"), "channels")
add_optional_tab(tabs, self.utxo_tab, read_QIcon("tab_coins.png"), _("Co&ins"), "utxo")
add_optional_tab(tabs, self.contacts_tab, read_QIcon("tab_contacts.png"), _("Con&tacts"), "contacts")
@@ -232,7 +232,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
interests = ['wallet_updated', 'network_updated', 'blockchain_updated',
'new_transaction', 'status',
'banner', 'verified', 'fee', 'fee_histogram', 'on_quotes',
- 'on_history', 'channel', 'channels',
+ 'on_history', 'channel', 'channels_updated',
'invoice_status', 'request_status']
# To avoid leaking references to "self" that prevent the
# window from being GC-ed when closed, callbacks should be
@@ -377,8 +377,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.on_fx_quotes()
elif event == 'on_history':
self.on_fx_history()
- elif event == 'channels':
- self.channels_list.update_rows.emit()
+ elif event == 'channels_updated':
+ self.channels_list.update_rows.emit(*args)
elif event == 'channel':
self.channels_list.update_single_row.emit(*args)
self.update_status()
@@ -583,7 +583,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
file_menu.addAction(_("&Quit"), self.close)
wallet_menu = menubar.addMenu(_("&Wallet"))
- wallet_menu.addAction(_("&Information"), self.show_master_public_keys)
+ wallet_menu.addAction(_("&Information"), self.show_wallet_info)
wallet_menu.addSeparator()
self.password_menu = wallet_menu.addAction(_("&Password"), self.change_password_dialog)
self.seed_menu = wallet_menu.addAction(_("&Seed"), self.show_seed_dialog)
@@ -623,7 +623,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
view_menu = menubar.addMenu(_("&View"))
add_toggle_action(view_menu, self.addresses_tab)
add_toggle_action(view_menu, self.utxo_tab)
- if self.config.get('lightning'):
+ if self.wallet.has_lightning():
add_toggle_action(view_menu, self.channels_tab)
add_toggle_action(view_menu, self.contacts_tab)
add_toggle_action(view_menu, self.console_tab)
@@ -633,7 +633,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
# Settings / Preferences are all reserved keywords in macOS using this as work around
tools_menu.addAction(_("Electrum preferences") if sys.platform == 'darwin' else _("Preferences"), self.settings_dialog)
tools_menu.addAction(_("&Network"), lambda: self.gui_object.show_network_dialog(self))
- if self.config.get('lightning'):
+ if self.wallet.has_lightning():
tools_menu.addAction(_("&Lightning"), self.gui_object.show_lightning_dialog)
tools_menu.addAction(_("&Watchtower"), self.gui_object.show_watchtower_dialog)
tools_menu.addAction(_("&Plugins"), self.plugins_dialog)
@@ -985,7 +985,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
buttons.addStretch(1)
buttons.addWidget(self.clear_invoice_button)
buttons.addWidget(self.create_invoice_button)
- if self.config.get('lightning'):
+ if self.wallet.has_lightning():
self.create_lightning_invoice_button = QPushButton(_('Lightning'))
self.create_lightning_invoice_button.setIcon(read_QIcon("lightning.png"))
self.create_lightning_invoice_button.clicked.connect(lambda: self.create_invoice(True))
@@ -2300,7 +2300,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
sb.addPermanentWidget(StatusBarButton(read_QIcon("preferences.png"), _("Preferences"), self.settings_dialog ) )
self.seed_button = StatusBarButton(read_QIcon("seed.png"), _("Seed"), self.show_seed_dialog )
sb.addPermanentWidget(self.seed_button)
- if self.config.get('lightning'):
+ if self.wallet.has_lightning():
self.lightning_button = StatusBarButton(read_QIcon("lightning.png"), _("Lightning Network"), self.gui_object.show_lightning_dialog)
sb.addPermanentWidget(self.lightning_button)
self.status_button = StatusBarButton(read_QIcon("status_disconnected.png"), _("Network"), lambda: self.gui_object.show_network_dialog(self))
@@ -2390,7 +2390,16 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
if d.exec_():
self.set_contact(line2.text(), line1.text())
- def show_master_public_keys(self):
+ def enable_lightning(self):
+ warning1 = _("Lightning support in Electrum is experimental. Do not put large amounts in lightning channels.")
+ warning2 = _("Funds stored in lightning channels are not recoverable from your seed. You must backup your wallet file everytime you crate a new channel.")
+ r = self.question(_('Enable Lightning payments?') + '\n\n' + _('WARNINGS') + ': ' + '\n\n' + warning1 + '\n\n' + warning2)
+ if not r:
+ return
+ self.wallet.init_lightning()
+ self.show_warning(_('Lightning keys have been initialized. Please restart Electrum'))
+
+ def show_wallet_info(self):
dialog = WindowModalDialog(self, _("Wallet Information"))
dialog.setMinimumSize(500, 100)
mpk_list = self.wallet.get_master_public_keys()
@@ -2414,6 +2423,17 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
grid.addWidget(QLabel(_("Keystore type") + ':'), 4, 0)
ks_type = str(keystore_types[0]) if keystore_types else _('No keystore')
grid.addWidget(QLabel(ks_type), 4, 1)
+ # lightning
+ if self.wallet.has_lightning():
+ lightning_b = None
+ lightning_label = QLabel(_('Enabled'))
+ else:
+ lightning_b = QPushButton(_('Enable'))
+ lightning_b.clicked.connect(self.enable_lightning)
+ lightning_label = QLabel(_('Disabled'))
+ grid.addWidget(QLabel(_('Lightning')), 5, 0)
+ grid.addWidget(lightning_label, 5, 1)
+ grid.addWidget(lightning_b, 5, 2)
vbox.addLayout(grid)
if self.wallet.is_deterministic():
diff --git a/electrum/gui/qt/settings_dialog.py b/electrum/gui/qt/settings_dialog.py
@@ -171,18 +171,7 @@ class SettingsDialog(WindowModalDialog):
fee_widgets.append((batch_rbf_cb, None))
# lightning
- help_lightning = _("""Enable Lightning Network payments. Note that funds stored in
-lightning channels are not recoverable from your seed. You must backup
-your wallet file after every channel creation.""")
lightning_widgets = []
- lightning_cb = QCheckBox(_("Enable Lightning"))
- lightning_cb.setToolTip(help_lightning)
- lightning_cb.setChecked(bool(self.config.get('lightning', False)))
- def on_lightning_checked(x):
- self.config.set_key('lightning', bool(x))
- lightning_cb.stateChanged.connect(on_lightning_checked)
- lightning_widgets.append((lightning_cb, None))
-
help_persist = _("""If this option is checked, Electrum will persist as a daemon after
you close all your wallet windows. Your local watchtower will keep
running, and it will protect your channels even if your wallet is not
diff --git a/electrum/lnworker.py b/electrum/lnworker.py
@@ -307,19 +307,11 @@ class LNGossip(LNWorker):
class LNWallet(LNWorker):
- def __init__(self, wallet: 'Abstract_Wallet'):
+ def __init__(self, wallet: 'Abstract_Wallet', xprv):
Logger.__init__(self)
self.wallet = wallet
self.storage = wallet.storage
self.config = wallet.config
- xprv = self.storage.get('lightning_privkey2')
- if xprv is None:
- # TODO derive this deterministically from wallet.keystore at keystore generation time
- # probably along a hardened path ( lnd-equivalent would be m/1017'/coinType'/ )
- seed = os.urandom(32)
- node = BIP32Node.from_rootseed(seed, xtype='standard')
- xprv = node.to_xprv()
- self.storage.put('lightning_privkey2', xprv)
LNWorker.__init__(self, xprv)
self.ln_keystore = keystore.from_xprv(xprv)
self.localfeatures |= LnLocalFeatures.OPTION_DATA_LOSS_PROTECT_REQ
@@ -789,7 +781,7 @@ class LNWallet(LNWorker):
return chan
def on_channels_updated(self):
- self.network.trigger_callback('channels')
+ self.network.trigger_callback('channels_updated', self.wallet)
@log_exceptions
async def add_peer(self, connect_str: str) -> Peer:
@@ -1211,7 +1203,7 @@ class LNWallet(LNWorker):
with self.lock:
self.channels.pop(chan_id)
self.save_channels()
- self.network.trigger_callback('channels', self.wallet)
+ self.network.trigger_callback('channels_updated', self.wallet)
self.network.trigger_callback('wallet_updated', self.wallet)
async def reestablish_peer_for_given_channel(self, chan):
diff --git a/electrum/network.py b/electrum/network.py
@@ -302,7 +302,12 @@ class Network(Logger):
self._set_status('disconnected')
# lightning network
- if self.config.get('lightning'):
+ self.channel_db = None # type: Optional[ChannelDB]
+ self.lngossip = None # type: Optional[LNGossip]
+ self.local_watchtower = None # type: Optional[WatchTower]
+
+ def maybe_init_lightning(self):
+ if self.channel_db is None:
from . import lnwatcher
from . import lnworker
from . import lnrouter
@@ -311,10 +316,10 @@ class Network(Logger):
self.path_finder = lnrouter.LNPathFinder(self.channel_db)
self.lngossip = lnworker.LNGossip(self)
self.local_watchtower = lnwatcher.WatchTower(self) if self.config.get('local_watchtower', False) else None
- else:
- self.channel_db = None # type: Optional[ChannelDB]
- self.lngossip = None # type: Optional[LNGossip]
- self.local_watchtower = None # type: Optional[WatchTower]
+ self.lngossip.start_network(self)
+ if self.local_watchtower:
+ self.local_watchtower.start_network(self)
+ asyncio.ensure_future(self.local_watchtower.start_watching)
def run_from_another_thread(self, coro, *, timeout=None):
assert self._loop_thread != threading.current_thread(), 'must not be called from network thread'
@@ -1158,12 +1163,6 @@ class Network(Logger):
self._set_oneserver(self.config.get('oneserver', False))
self._start_interface(self.default_server)
- if self.lngossip:
- self.lngossip.start_network(self)
- if self.local_watchtower:
- self.local_watchtower.start_network(self)
- await self.local_watchtower.start_watching()
-
async def main():
try:
await self._init_headers_file()
diff --git a/electrum/tests/regtest/regtest.sh b/electrum/tests/regtest/regtest.sh
@@ -4,9 +4,9 @@ set -eu
# alice -> bob -> carol
-alice="./run_electrum --regtest --lightning -D /tmp/alice"
-bob="./run_electrum --regtest --lightning -D /tmp/bob"
-carol="./run_electrum --regtest --lightning -D /tmp/carol"
+alice="./run_electrum --regtest -D /tmp/alice"
+bob="./run_electrum --regtest -D /tmp/bob"
+carol="./run_electrum --regtest -D /tmp/carol"
bitcoin_cli="bitcoin-cli -rpcuser=doggman -rpcpassword=donkey -rpcport=18554 -regtest"
@@ -18,7 +18,7 @@ function new_blocks()
function wait_for_balance()
{
msg="wait until $1's balance reaches $2"
- cmd="./run_electrum --regtest --lightning -D /tmp/$1"
+ cmd="./run_electrum --regtest -D /tmp/$1"
while balance=$($cmd getbalance | jq '[.confirmed, .unconfirmed] | to_entries | map(select(.value != null).value) | map(tonumber) | add ') && (( $(echo "$balance < $2" | bc -l) )); do
sleep 1
msg="$msg."
@@ -30,7 +30,7 @@ function wait_for_balance()
function wait_until_channel_open()
{
msg="wait until $1 sees channel open"
- cmd="./run_electrum --regtest --lightning -D /tmp/$1"
+ cmd="./run_electrum --regtest -D /tmp/$1"
while channel_state=$($cmd list_channels | jq '.[0] | .state' | tr -d '"') && [ $channel_state != "OPEN" ]; do
sleep 1
msg="$msg."
@@ -42,7 +42,7 @@ function wait_until_channel_open()
function wait_until_channel_closed()
{
msg="wait until $1 sees channel closed"
- cmd="./run_electrum --regtest --lightning -D /tmp/$1"
+ cmd="./run_electrum --regtest -D /tmp/$1"
while [[ $($cmd list_channels | jq '.[0].state' | tr -d '"') != "CLOSED" ]]; do
sleep 1
msg="$msg."
@@ -73,6 +73,9 @@ if [[ $1 == "init" ]]; then
$alice create --offline > /dev/null
$bob create --offline > /dev/null
$carol create --offline > /dev/null
+ $alice -o init_lightning
+ $bob -o init_lightning
+ $carol -o init_lightning
$alice setconfig --offline log_to_file True
$bob setconfig --offline log_to_file True
$carol setconfig --offline log_to_file True
diff --git a/electrum/wallet.py b/electrum/wallet.py
@@ -41,6 +41,7 @@ from decimal import Decimal
from typing import TYPE_CHECKING, List, Optional, Tuple, Union, NamedTuple, Sequence
from .i18n import _
+from .bip32 import BIP32Node
from .crypto import sha256
from .util import (NotEnoughFunds, UserCancelled, profiler,
format_satoshis, format_fee_satoshis, NoDynamicFeeEstimates,
@@ -229,22 +230,36 @@ class Abstract_Wallet(AddressSynchronizer):
self.fiat_value = storage.get('fiat_value', {})
self.receive_requests = storage.get('payment_requests', {})
self.invoices = storage.get('invoices', {})
-
# convert invoices
for invoice_key, invoice in self.invoices.items():
if invoice.get('type') == PR_TYPE_ONCHAIN:
outputs = [TxOutput(*output) for output in invoice.get('outputs')]
invoice['outputs'] = outputs
-
self.calc_unused_change_addresses()
-
# save wallet type the first time
if self.storage.get('wallet_type') is None:
self.storage.put('wallet_type', self.wallet_type)
-
self.contacts = Contacts(self.storage)
self._coin_price_cache = {}
- self.lnworker = LNWallet(self) if self.config.get('lightning') else None
+ # lightning
+ ln_xprv = self.storage.get('lightning_privkey2')
+ self.lnworker = LNWallet(self, ln_xprv) if ln_xprv else None
+
+ def has_lightning(self):
+ return bool(self.lnworker)
+
+ def init_lightning(self):
+ if self.storage.get('lightning_privkey2'):
+ return
+ if not is_using_fast_ecc():
+ raise Exception('libsecp256k1 library not available. '
+ 'Verifying Lightning channels is too computationally expensive without libsecp256k1, aborting.')
+ # TODO derive this deterministically from wallet.keystore at keystore generation time
+ # probably along a hardened path ( lnd-equivalent would be m/1017'/coinType'/ )
+ seed = os.urandom(32)
+ node = BIP32Node.from_rootseed(seed, xtype='standard')
+ ln_xprv = node.to_xprv()
+ self.storage.put('lightning_privkey2', ln_xprv)
def stop_threads(self):
super().stop_threads()
@@ -261,6 +276,7 @@ class Abstract_Wallet(AddressSynchronizer):
def start_network(self, network):
AddressSynchronizer.start_network(self, network)
if self.lnworker:
+ network.maybe_init_lightning()
self.lnworker.start_network(network)
def load_and_cleanup(self):
diff --git a/run_electrum b/run_electrum
@@ -333,14 +333,6 @@ if __name__ == '__main__':
constants.set_regtest()
elif config.get('simnet'):
constants.set_simnet()
- elif config.get('lightning') and not config.get('reckless'):
- raise Exception('lightning option not available on mainnet')
-
- if config.get('lightning'):
- from electrum.ecc_fast import is_using_fast_ecc
- if not is_using_fast_ecc():
- raise Exception('libsecp256k1 library not available. '
- 'Verifying Lightning channels is too computationally expensive without libsecp256k1, aborting.')
cmdname = config.get('cmd')