Browse Source

Merge pull request #5951 from spesmilo/ln_backups

save wallet backups on channel creation
hard-fail-on-bad-server-string
ThomasV 5 years ago
committed by GitHub
parent
commit
bb739f4de9
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 115
      electrum/gui/kivy/main_window.py
  2. BIN
      electrum/gui/kivy/theming/light/eye1.png
  3. 2
      electrum/gui/kivy/tools/buildozer.spec
  4. 99
      electrum/gui/kivy/uix/dialogs/password_dialog.py
  5. 32
      electrum/gui/kivy/uix/dialogs/settings.py
  6. 7
      electrum/gui/kivy/uix/ui_screens/status.kv
  7. 1
      electrum/gui/qt/__init__.py
  8. 9
      electrum/gui/qt/channels_list.py
  9. 30
      electrum/gui/qt/main_window.py
  10. 15
      electrum/gui/qt/settings_dialog.py
  11. 4
      electrum/lnpeer.py
  12. 1
      electrum/lnworker.py
  13. 2
      electrum/tests/test_lnpeer.py
  14. 15
      electrum/util.py
  15. 21
      electrum/wallet.py

115
electrum/gui/kivy/main_window.py

@ -7,7 +7,7 @@ import traceback
from decimal import Decimal from decimal import Decimal
import threading import threading
import asyncio import asyncio
from typing import TYPE_CHECKING, Optional, Union, Callable from typing import TYPE_CHECKING, Optional, Union, Callable, Sequence
from electrum.storage import WalletStorage, StorageReadWriteError from electrum.storage import WalletStorage, StorageReadWriteError
from electrum.wallet_db import WalletDB from electrum.wallet_db import WalletDB
@ -31,6 +31,7 @@ from kivy.clock import Clock
from kivy.factory import Factory from kivy.factory import Factory
from kivy.metrics import inch from kivy.metrics import inch
from kivy.lang import Builder from kivy.lang import Builder
from .uix.dialogs.password_dialog import PasswordDialog
## lazy imports for factory so that widgets can be used in kv ## lazy imports for factory so that widgets can be used in kv
#Factory.register('InstallWizard', module='electrum.gui.kivy.uix.dialogs.installwizard') #Factory.register('InstallWizard', module='electrum.gui.kivy.uix.dialogs.installwizard')
@ -163,6 +164,10 @@ class ElectrumWindow(App):
def on_use_rbf(self, instance, x): def on_use_rbf(self, instance, x):
self.electrum_config.set_key('use_rbf', self.use_rbf, True) self.electrum_config.set_key('use_rbf', self.use_rbf, True)
android_backups = BooleanProperty(False)
def on_android_backups(self, instance, x):
self.electrum_config.set_key('android_backups', self.android_backups, True)
use_change = BooleanProperty(False) use_change = BooleanProperty(False)
def on_use_change(self, instance, x): def on_use_change(self, instance, x):
if self.wallet: if self.wallet:
@ -326,6 +331,7 @@ class ElectrumWindow(App):
self.wallet = None # type: Optional[Abstract_Wallet] self.wallet = None # type: Optional[Abstract_Wallet]
self.pause_time = 0 self.pause_time = 0
self.asyncio_loop = asyncio.get_event_loop() self.asyncio_loop = asyncio.get_event_loop()
self.password = None
App.__init__(self)#, **kwargs) App.__init__(self)#, **kwargs)
@ -619,9 +625,15 @@ class ElectrumWindow(App):
return return
wallet = self.daemon.load_wallet(path, None) wallet = self.daemon.load_wallet(path, None)
if wallet: if wallet:
if platform == 'android' and wallet.has_password(): if wallet.has_password():
self.password_dialog(wallet=wallet, msg=_('Enter PIN code'), def on_success(x):
on_success=lambda x: self.load_wallet(wallet), on_failure=self.stop) # save pin_code so that we can create backups
self.password = x
self.load_wallet(wallet)
self.password_dialog(
check_password=wallet.check_password,
on_success=on_success,
on_failure=self.stop)
else: else:
self.load_wallet(wallet) self.load_wallet(wallet)
else: else:
@ -637,10 +649,13 @@ class ElectrumWindow(App):
if not storage.is_encrypted_with_user_pw(): if not storage.is_encrypted_with_user_pw():
raise Exception("Kivy GUI does not support this type of encrypted wallet files.") raise Exception("Kivy GUI does not support this type of encrypted wallet files.")
def on_password(pw): def on_password(pw):
self.password = pw
storage.decrypt(pw) storage.decrypt(pw)
self._on_decrypted_storage(storage) self._on_decrypted_storage(storage)
self.password_dialog(wallet=storage, msg=_('Enter PIN code'), self.password_dialog(
on_success=on_password, on_failure=self.stop) check_password=storage.check_password,
on_success=on_password,
on_failure=self.stop)
return return
self._on_decrypted_storage(storage) self._on_decrypted_storage(storage)
if not ask_if_wizard: if not ask_if_wizard:
@ -934,7 +949,7 @@ class ElectrumWindow(App):
def on_resume(self): def on_resume(self):
now = time.time() now = time.time()
if self.wallet and self.wallet.has_password() and now - self.pause_time > 60: if self.wallet and self.wallet.has_password() and now - self.pause_time > 60:
self.password_dialog(wallet=self.wallet, msg=_('Enter PIN'), on_success=None, on_failure=self.stop) self.password_dialog(check_password=self.check_pin_code, on_success=None, on_failure=self.stop, is_password=False)
if self.nfcscanner: if self.nfcscanner:
self.nfcscanner.nfc_enable() self.nfcscanner.nfc_enable()
@ -1096,12 +1111,12 @@ class ElectrumWindow(App):
def on_fee(self, event, *arg): def on_fee(self, event, *arg):
self.fee_status = self.electrum_config.get_fee_status() self.fee_status = self.electrum_config.get_fee_status()
def protected(self, msg, f, args): def protected(self, f, args):
if self.wallet.has_password(): if self.electrum_config.get('pin_code'):
on_success = lambda pw: f(*(args + (pw,))) on_success = lambda pw: f(*(args + (self.password,)))
self.password_dialog(wallet=self.wallet, msg=msg, on_success=on_success, on_failure=lambda: None) self.password_dialog(check_password=self.check_pin_code, on_success=on_success, on_failure=lambda: None, is_password=False)
else: else:
f(*(args + (None,))) f(*(args + (self.password,)))
def toggle_lightning(self): def toggle_lightning(self):
if self.wallet.has_lightning(): if self.wallet.has_lightning():
@ -1161,44 +1176,88 @@ class ElectrumWindow(App):
self.load_wallet_by_name(new_path) self.load_wallet_by_name(new_path)
def show_seed(self, label): def show_seed(self, label):
self.protected(_("Enter your PIN code in order to decrypt your seed"), self._show_seed, (label,)) self.protected(self._show_seed, (label,))
def _show_seed(self, label, password): def _show_seed(self, label, password):
if self.wallet.has_password() and password is None: if self.wallet.has_password() and password is None:
return return
keystore = self.wallet.keystore keystore = self.wallet.keystore
try:
seed = keystore.get_seed(password) seed = keystore.get_seed(password)
passphrase = keystore.get_passphrase(password) passphrase = keystore.get_passphrase(password)
except:
self.show_error("Invalid PIN")
return
label.data = seed label.data = seed
if passphrase: if passphrase:
label.data += '\n\n' + _('Passphrase') + ': ' + passphrase label.data += '\n\n' + _('Passphrase') + ': ' + passphrase
def password_dialog(self, *, wallet: Union[Abstract_Wallet, WalletStorage], def has_pin_code(self):
msg: str, on_success: Callable = None, on_failure: Callable = None): return bool(self.electrum_config.get('pin_code'))
from .uix.dialogs.password_dialog import PasswordDialog
def check_pin_code(self, pin):
if pin != self.electrum_config.get('pin_code'):
raise InvalidPassword
def password_dialog(self, *, check_password: Callable = None,
on_success: Callable = None, on_failure: Callable = None,
is_password=True):
if self._password_dialog is None: if self._password_dialog is None:
self._password_dialog = PasswordDialog() self._password_dialog = PasswordDialog()
self._password_dialog.init(self, wallet=wallet, msg=msg, self._password_dialog.init(
on_success=on_success, on_failure=on_failure) self, check_password = check_password,
on_success=on_success, on_failure=on_failure,
is_password=is_password)
self._password_dialog.open() self._password_dialog.open()
def change_password(self, cb): def change_password(self, cb):
from .uix.dialogs.password_dialog import PasswordDialog
if self._password_dialog is None: if self._password_dialog is None:
self._password_dialog = PasswordDialog() self._password_dialog = PasswordDialog()
message = _("Changing PIN code.") + '\n' + _("Enter your current PIN:")
def on_success(old_password, new_password): def on_success(old_password, new_password):
self.wallet.update_password(old_password, new_password) self.wallet.update_password(old_password, new_password)
self.show_info(_("Your PIN code was updated")) self.password = new_password
on_failure = lambda: self.show_error(_("PIN codes do not match")) self.show_info(_("Your password was updated"))
self._password_dialog.init(self, wallet=self.wallet, msg=message, on_failure = lambda: self.show_error(_("Password not updated"))
on_success=on_success, on_failure=on_failure, is_change=1) self._password_dialog.init(
self, check_password = self.wallet.check_password,
on_success=on_success, on_failure=on_failure,
is_change=True, is_password=True,
has_password=self.wallet.has_password())
self._password_dialog.open()
def change_pin_code(self, cb):
if self._password_dialog is None:
self._password_dialog = PasswordDialog()
def on_success(old_password, new_password):
self.electrum_config.set_key('pin_code', new_password)
cb()
self.show_info(_("PIN updated") if new_password else _('PIN disabled'))
on_failure = lambda: self.show_error(_("PIN not updated"))
self._password_dialog.init(
self, check_password=self.check_pin_code,
on_success=on_success, on_failure=on_failure,
is_change=True, is_password=False,
has_password = self.has_pin_code())
self._password_dialog.open() self._password_dialog.open()
def save_backup(self):
if platform != 'android':
self._save_backup()
return
from android.permissions import request_permissions, Permission
def cb(permissions, grant_results: Sequence[bool]):
if not grant_results or not grant_results[0]:
self.show_error(_("Cannot save backup without STORAGE permission"))
return
# note: Clock.schedule_once is a hack so that we get called on a non-daemon thread
# (needed for WalletDB.write)
Clock.schedule_once(lambda dt: self._save_backup())
request_permissions([Permission.WRITE_EXTERNAL_STORAGE], cb)
def _save_backup(self):
new_path = self.wallet.save_backup()
if new_path:
self.show_info(_("Backup saved:") + f"\n{new_path}")
else:
self.show_error(_("Backup NOT saved. Backup directory not configured."))
def export_private_keys(self, pk_label, addr): def export_private_keys(self, pk_label, addr):
if self.wallet.is_watching_only(): if self.wallet.is_watching_only():
self.show_info(_('This is a watching-only wallet. It does not contain private keys.')) self.show_info(_('This is a watching-only wallet. It does not contain private keys.'))

BIN
electrum/gui/kivy/theming/light/eye1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

2
electrum/gui/kivy/tools/buildozer.spec

@ -67,7 +67,7 @@ fullscreen = False
# #
# (list) Permissions # (list) Permissions
android.permissions = INTERNET, CAMERA android.permissions = INTERNET, CAMERA, WRITE_EXTERNAL_STORAGE
# (int) Android API to use # (int) Android API to use
android.api = 28 android.api = 28

99
electrum/gui/kivy/uix/dialogs/password_dialog.py

@ -19,6 +19,7 @@ Builder.load_string('''
<PasswordDialog@Popup> <PasswordDialog@Popup>
id: popup id: popup
is_generic: False
title: 'Electrum' title: 'Electrum'
message: '' message: ''
BoxLayout: BoxLayout:
@ -27,14 +28,45 @@ Builder.load_string('''
Widget: Widget:
size_hint: 1, 0.05 size_hint: 1, 0.05
Label: Label:
size_hint: 0.70, None
font_size: '20dp' font_size: '20dp'
text: root.message text: root.message
text_size: self.width, None text_size: self.width, None
size: self.texture_size
Widget: Widget:
size_hint: 1, 0.05 size_hint: 1, 0.05
BoxLayout:
orientation: 'horizontal'
id: box_generic_password
visible: root.is_generic
size_hint_y: 0.05
opacity: 1 if self.visible else 0
disabled: not self.visible
WizardTextInput:
id: textinput_generic_password
valign: 'center'
multiline: False
on_text_validate:
popup.on_password(self.text)
password: True
size_hint: 0.9, None
unfocus_on_touch: False
focus: root.is_generic
Button:
size_hint: 0.1, None
valign: 'center'
background_normal: 'atlas://electrum/gui/kivy/theming/light/eye1'
background_down: self.background_normal
height: '50dp'
width: '50dp'
padding: '5dp', '5dp'
on_release:
textinput_generic_password.password = False if textinput_generic_password.password else True
Label: Label:
id: a id: label_pin
visible: not root.is_generic
size_hint_y: 0.05
opacity: 1 if self.visible else 0
disabled: not self.visible
font_size: '50dp' font_size: '50dp'
text: '*'*len(kb.password) + '-'*(6-len(kb.password)) text: '*'*len(kb.password) + '-'*(6-len(kb.password))
size: self.texture_size size: self.texture_size
@ -42,6 +74,7 @@ Builder.load_string('''
size_hint: 1, 0.05 size_hint: 1, 0.05
GridLayout: GridLayout:
id: kb id: kb
disabled: root.is_generic
size_hint: 1, None size_hint: 1, None
height: self.minimum_height height: self.minimum_height
update_amount: popup.update_password update_amount: popup.update_password
@ -79,31 +112,48 @@ Builder.load_string('''
class PasswordDialog(Factory.Popup): class PasswordDialog(Factory.Popup):
def init(self, app: 'ElectrumWindow', *, def init(self, app: 'ElectrumWindow', *,
wallet: Union['Abstract_Wallet', 'WalletStorage'] = None, check_password = None,
msg: str, on_success: Callable = None, on_failure: Callable = None, on_success: Callable = None, on_failure: Callable = None,
is_change: int = 0): is_change: bool = False,
is_password: bool = False,
has_password: bool = False):
self.app = app self.app = app
self.wallet = wallet self.pw_check = check_password
self.message = msg self.message = ''
self.on_success = on_success self.on_success = on_success
self.on_failure = on_failure self.on_failure = on_failure
self.ids.kb.password = ''
self.success = False self.success = False
self.is_change = is_change self.is_change = is_change
self.pw = None self.pw = None
self.new_password = None self.new_password = None
self.title = 'Electrum' + (' - ' + self.wallet.basename() if self.wallet else '') self.title = 'Electrum'
self.level = 1 if is_change and not has_password else 0
self.is_generic = is_password
self.update_screen()
def update_screen(self):
self.ids.kb.password = ''
self.ids.textinput_generic_password.text = ''
if self.level == 0:
self.message = _('Enter your password') if self.is_generic else _('Enter your PIN')
elif self.level == 1:
self.message = _('Enter new password') if self.is_generic else _('Enter new PIN')
elif self.level == 2:
self.message = _('Confirm new password') if self.is_generic else _('Confirm new PIN')
def check_password(self, password): def check_password(self, password):
if self.is_change > 1: if self.level > 0:
return True return True
try: try:
self.wallet.check_password(password) self.pw_check(password)
return True return True
except InvalidPassword as e: except InvalidPassword as e:
return False return False
def on_dismiss(self): def on_dismiss(self):
if self.level == 1 and not self.is_generic and self.on_success:
self.on_success(self.pw, None)
return False
if not self.success: if not self.success:
if self.on_failure: if self.on_failure:
self.on_failure() self.on_failure()
@ -126,25 +176,28 @@ class PasswordDialog(Factory.Popup):
text += c text += c
kb.password = text kb.password = text
def on_password(self, pw):
if len(pw) == 6: def on_password(self, pw: str):
if self.is_generic:
if len(pw) < 6:
self.app.show_error(_('Password is too short (min {} characters)').format(6))
return
if len(pw) >= 6:
if self.check_password(pw): if self.check_password(pw):
if self.is_change == 0: if self.is_change is False:
self.success = True self.success = True
self.pw = pw self.pw = pw
self.message = _('Please wait...') self.message = _('Please wait...')
self.dismiss() self.dismiss()
elif self.is_change == 1: elif self.level == 0:
self.level = 1
self.pw = pw self.pw = pw
self.message = _('Enter new PIN') self.update_screen()
self.ids.kb.password = '' elif self.level == 1:
self.is_change = 2 self.level = 2
elif self.is_change == 2:
self.new_password = pw self.new_password = pw
self.message = _('Confirm new PIN') self.update_screen()
self.ids.kb.password = '' elif self.level == 2:
self.is_change = 3
elif self.is_change == 3:
self.success = pw == self.new_password self.success = pw == self.new_password
self.dismiss() self.dismiss()
else: else:

32
electrum/gui/kivy/uix/dialogs/settings.py

@ -18,7 +18,8 @@ Builder.load_string('''
<SettingsDialog@Popup> <SettingsDialog@Popup>
id: settings id: settings
title: _('Electrum Settings') title: _('Electrum Settings')
disable_pin: False disable_password: False
has_pin_code: False
use_encryption: False use_encryption: False
BoxLayout: BoxLayout:
orientation: 'vertical' orientation: 'vertical'
@ -36,10 +37,10 @@ Builder.load_string('''
action: partial(root.language_dialog, self) action: partial(root.language_dialog, self)
CardSeparator CardSeparator
SettingsItem: SettingsItem:
disabled: root.disable_pin status: 'ON' if root.has_pin_code else 'OFF'
title: _('PIN code') title: _('PIN code') + ': ' + self.status
description: _("Change your PIN code.") description: _("Change your PIN code.") if root.has_pin_code else _("Add PIN code")
action: partial(root.change_password, self) action: partial(root.change_pin_code, self)
CardSeparator CardSeparator
SettingsItem: SettingsItem:
bu: app.base_unit bu: app.base_unit
@ -82,6 +83,19 @@ Builder.load_string('''
description: _("Send your change to separate addresses.") description: _("Send your change to separate addresses.")
message: _('Send excess coins to change addresses') message: _('Send excess coins to change addresses')
action: partial(root.boolean_dialog, 'use_change', _('Use change addresses'), self.message) action: partial(root.boolean_dialog, 'use_change', _('Use change addresses'), self.message)
CardSeparator
SettingsItem:
disabled: root.disable_password
title: _('Password')
description: _("Change wallet password.")
action: root.change_password
CardSeparator
SettingsItem:
status: _('Yes') if app.android_backups else _('No')
title: _('Backups') + ': ' + self.status
description: _("Backup wallet to external storage.")
message: _("If this option is checked, a backup of your wallet will be written to external storage everytime you create a new channel. Make sure your wallet is protected with a strong password before you enable this option.")
action: partial(root.boolean_dialog, 'android_backups', _('Backups'), self.message)
# disabled: there is currently only one coin selection policy # disabled: there is currently only one coin selection policy
#CardSeparator #CardSeparator
@ -112,15 +126,19 @@ class SettingsDialog(Factory.Popup):
def update(self): def update(self):
self.wallet = self.app.wallet self.wallet = self.app.wallet
self.disable_pin = self.wallet.is_watching_only() if self.wallet else True self.disable_password = self.wallet.is_watching_only() if self.wallet else True
self.use_encryption = self.wallet.has_password() if self.wallet else False self.use_encryption = self.wallet.has_password() if self.wallet else False
self.has_pin_code = self.app.has_pin_code()
def get_language_name(self): def get_language_name(self):
return languages.get(self.config.get('language', 'en_UK'), '') return languages.get(self.config.get('language', 'en_UK'), '')
def change_password(self, item, dt): def change_password(self, dt):
self.app.change_password(self.update) self.app.change_password(self.update)
def change_pin_code(self, label, dt):
self.app.change_pin_code(self.update)
def language_dialog(self, item, dt): def language_dialog(self, item, dt):
if self._language_dialog is None: if self._language_dialog is None:
l = self.config.get('language', 'en_UK') l = self.config.get('language', 'en_UK')

7
electrum/gui/kivy/uix/ui_screens/status.kv

@ -83,13 +83,14 @@ Popup:
Button: Button:
size_hint: 0.5, None size_hint: 0.5, None
height: '48dp' height: '48dp'
text: _('Disable LN') if app.wallet.has_lightning() else _('Enable LN') text: _('Save Backup')
on_release: on_release:
root.dismiss() root.dismiss()
app.toggle_lightning() app.save_backup()
Button: Button:
size_hint: 0.5, None size_hint: 0.5, None
height: '48dp' height: '48dp'
text: _('Close') text: _('Disable LN') if app.wallet.has_lightning() else _('Enable LN')
on_release: on_release:
root.dismiss() root.dismiss()
app.toggle_lightning()

1
electrum/gui/qt/__init__.py

@ -233,6 +233,7 @@ class ElectrumGui(Logger):
run_hook('on_new_window', w) run_hook('on_new_window', w)
w.warn_if_testnet() w.warn_if_testnet()
w.warn_if_watching_only() w.warn_if_watching_only()
w.warn_if_lightning_backup()
return w return w
def count_wizards_in_progress(func): def count_wizards_in_progress(func):

9
electrum/gui/qt/channels_list.py

@ -8,7 +8,7 @@ from PyQt5.QtWidgets import QMenu, QHBoxLayout, QLabel, QVBoxLayout, QGridLayout
from electrum.util import bh2u, NotEnoughFunds, NoDynamicFeeEstimates from electrum.util import bh2u, NotEnoughFunds, NoDynamicFeeEstimates
from electrum.i18n import _ from electrum.i18n import _
from electrum.lnchannel import Channel from electrum.lnchannel import Channel, peer_states
from electrum.wallet import Abstract_Wallet from electrum.wallet import Abstract_Wallet
from electrum.lnutil import LOCAL, REMOTE, format_short_channel_id, LN_MAX_FUNDING_SAT from electrum.lnutil import LOCAL, REMOTE, format_short_channel_id, LN_MAX_FUNDING_SAT
@ -84,10 +84,14 @@ class ChannelsList(MyTreeView):
WaitingDialog(self, 'please wait..', task, self.on_success, self.on_failure) WaitingDialog(self, 'please wait..', task, self.on_success, self.on_failure)
def force_close(self, channel_id): def force_close(self, channel_id):
if self.lnworker.wallet.is_lightning_backup():
msg = _('WARNING: force-closing from an old state might result in fund loss.\nAre you sure?')
else:
msg = _('Force-close channel?\nReclaimed funds will not be immediately available.')
if self.parent.question(msg):
def task(): def task():
coro = self.lnworker.force_close_channel(channel_id) coro = self.lnworker.force_close_channel(channel_id)
return self.network.run_from_another_thread(coro) return self.network.run_from_another_thread(coro)
if self.parent.question('Force-close channel?\nReclaimed funds will not be immediately available.'):
WaitingDialog(self, 'please wait..', task, self.on_success, self.on_failure) WaitingDialog(self, 'please wait..', task, self.on_success, self.on_failure)
def remove_channel(self, channel_id): def remove_channel(self, channel_id):
@ -105,6 +109,7 @@ class ChannelsList(MyTreeView):
menu.addAction(_("Details..."), lambda: self.details(channel_id)) menu.addAction(_("Details..."), lambda: self.details(channel_id))
self.add_copy_menu(menu, idx) self.add_copy_menu(menu, idx)
if not chan.is_closed(): if not chan.is_closed():
if chan.peer_state == peer_states.GOOD:
menu.addAction(_("Close channel"), lambda: self.close_channel(channel_id)) menu.addAction(_("Close channel"), lambda: self.close_channel(channel_id))
menu.addAction(_("Force-close channel"), lambda: self.force_close(channel_id)) menu.addAction(_("Force-close channel"), lambda: self.force_close(channel_id))
else: else:

30
electrum/gui/qt/main_window.py

@ -513,6 +513,18 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
]) ])
self.show_warning(msg, title=_('Watch-only wallet')) self.show_warning(msg, title=_('Watch-only wallet'))
def warn_if_lightning_backup(self):
if self.wallet.is_lightning_backup():
msg = '\n\n'.join([
_("This file is a backup of a lightning wallet."),
_("You will not be able to perform lightning payments using this file, and the lightning balance displayed in this wallet might be outdated.") + ' ' + \
_("If you have lost the original wallet file, you can use this file to trigger a forced closure of your channels."),
_("Do you want to have your channels force-closed?")
])
if self.question(msg, title=_('Lightning Backup')):
self.network.maybe_init_lightning()
self.wallet.lnworker.start_network(self.network)
def warn_if_testnet(self): def warn_if_testnet(self):
if not constants.net.TESTNET: if not constants.net.TESTNET:
return return
@ -549,20 +561,16 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
return return
self.gui_object.new_window(filename) self.gui_object.new_window(filename)
def backup_wallet(self): def backup_wallet(self):
path = self.wallet.storage.path
wallet_folder = os.path.dirname(path)
filename, __ = QFileDialog.getSaveFileName(self, _('Enter a filename for the copy of your wallet'), wallet_folder)
if not filename:
return
new_path = os.path.join(wallet_folder, filename)
if new_path != path:
try: try:
shutil.copy2(path, new_path) new_path = self.wallet.save_backup()
self.show_message(_("A copy of your wallet file was created in")+" '%s'" % str(new_path), title=_("Wallet backup created"))
except BaseException as reason: except BaseException as reason:
self.show_critical(_("Electrum was unable to copy your wallet file to the specified location.") + "\n" + str(reason), title=_("Unable to create backup")) self.show_critical(_("Electrum was unable to copy your wallet file to the specified location.") + "\n" + str(reason), title=_("Unable to create backup"))
return
if new_path:
self.show_message(_("A copy of your wallet file was created in")+" '%s'" % str(new_path), title=_("Wallet backup created"))
else:
self.show_message(_("You need to configure a backup directory in your preferences"), title=_("Backup not created"))
def update_recently_visited(self, filename): def update_recently_visited(self, filename):
recent = self.config.get('recently_open', []) recent = self.config.get('recently_open', [])
@ -604,7 +612,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.recently_visited_menu = file_menu.addMenu(_("&Recently open")) self.recently_visited_menu = file_menu.addMenu(_("&Recently open"))
file_menu.addAction(_("&Open"), self.open_wallet).setShortcut(QKeySequence.Open) file_menu.addAction(_("&Open"), self.open_wallet).setShortcut(QKeySequence.Open)
file_menu.addAction(_("&New/Restore"), self.new_wallet).setShortcut(QKeySequence.New) file_menu.addAction(_("&New/Restore"), self.new_wallet).setShortcut(QKeySequence.New)
file_menu.addAction(_("&Save Copy"), self.backup_wallet).setShortcut(QKeySequence.SaveAs) file_menu.addAction(_("&Save backup"), self.backup_wallet).setShortcut(QKeySequence.SaveAs)
file_menu.addAction(_("Delete"), self.remove_wallet) file_menu.addAction(_("Delete"), self.remove_wallet)
file_menu.addSeparator() file_menu.addSeparator()
file_menu.addAction(_("&Quit"), self.close) file_menu.addAction(_("&Quit"), self.close)

15
electrum/gui/qt/settings_dialog.py

@ -145,6 +145,14 @@ class SettingsDialog(WindowModalDialog):
# lightning # lightning
lightning_widgets = [] lightning_widgets = []
backup_help = _("""A backup of your wallet file will be saved to that directory everytime you create a new channel. The backup cannot be used to perform lightning transactions; it may only be used to retrieve the funds in your open channels, using data loss protect (channels will be force closed).""")
backup_dir = self.config.get('backup_dir')
backup_dir_label = HelpLabel(_('Backup directory') + ':', backup_help)
self.backup_dir_e = QPushButton(backup_dir)
self.backup_dir_e.clicked.connect(self.select_backup_dir)
lightning_widgets.append((backup_dir_label, self.backup_dir_e))
help_persist = _("""If this option is checked, Electrum will persist as a daemon after 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 you close all your wallet windows. Your local watchtower will keep
running, and it will protect your channels even if your wallet is not running, and it will protect your channels even if your wallet is not
@ -546,6 +554,13 @@ that is always connected to the internet. Configure a port if you want it to be
if alias: if alias:
self.window.fetch_alias() self.window.fetch_alias()
def select_backup_dir(self, b):
name = self.config.get('backup_dir', '')
dirname = QFileDialog.getExistingDirectory(self, "Select your SSL certificate file", name)
if dirname:
self.config.set_key('backup_dir', dirname)
self.backup_dir_e.setText(dirname)
def select_ssl_certfile(self, b): def select_ssl_certfile(self, b):
name = self.config.get('ssl_certfile', '') name = self.config.get('ssl_certfile', '')
filename, __ = QFileDialog.getOpenFileName(self, "Select your SSL certificate file", name) filename, __ = QFileDialog.getOpenFileName(self, "Select your SSL certificate file", name)

4
electrum/lnpeer.py

@ -848,6 +848,10 @@ class Peer(Logger):
self.logger.warning(f"channel_reestablish: we are ahead of remote! trying to force-close.") self.logger.warning(f"channel_reestablish: we are ahead of remote! trying to force-close.")
await self.lnworker.force_close_channel(chan_id) await self.lnworker.force_close_channel(chan_id)
return return
elif self.lnworker.wallet.is_lightning_backup():
self.logger.warning(f"channel_reestablish: force-closing because we are a recent backup")
await self.lnworker.force_close_channel(chan_id)
return
chan.peer_state = peer_states.GOOD chan.peer_state = peer_states.GOOD
# note: chan.short_channel_id being set implies the funding txn is already at sufficient depth # note: chan.short_channel_id being set implies the funding txn is already at sufficient depth

1
electrum/lnworker.py

@ -842,6 +842,7 @@ class LNWallet(LNWorker):
with self.lock: with self.lock:
self.channels[chan.channel_id] = chan self.channels[chan.channel_id] = chan
self.lnwatcher.add_channel(chan.funding_outpoint.to_str(), chan.get_funding_address()) self.lnwatcher.add_channel(chan.funding_outpoint.to_str(), chan.get_funding_address())
self.wallet.save_backup()
@log_exceptions @log_exceptions
async def add_peer(self, connect_str: str) -> Peer: async def add_peer(self, connect_str: str) -> Peer:

2
electrum/tests/test_lnpeer.py

@ -73,6 +73,8 @@ class MockWallet:
pass pass
def save_db(self): def save_db(self):
pass pass
def is_lightning_backup(self):
return False
class MockLNWallet: class MockLNWallet:
def __init__(self, remote_keypair, local_keypair, chan, tx_queue): def __init__(self, remote_keypair, local_keypair, chan, tx_queue):

15
electrum/util.py

@ -425,11 +425,26 @@ def profiler(func):
return lambda *args, **kw_args: do_profile(args, kw_args) return lambda *args, **kw_args: do_profile(args, kw_args)
def android_ext_dir():
from android.storage import primary_external_storage_path
return primary_external_storage_path()
def android_backup_dir():
d = os.path.join(android_ext_dir(), 'org.electrum.electrum')
if not os.path.exists(d):
os.mkdir(d)
return d
def android_data_dir(): def android_data_dir():
import jnius import jnius
PythonActivity = jnius.autoclass('org.kivy.android.PythonActivity') PythonActivity = jnius.autoclass('org.kivy.android.PythonActivity')
return PythonActivity.mActivity.getFilesDir().getPath() + '/data' return PythonActivity.mActivity.getFilesDir().getPath() + '/data'
def get_backup_dir(config):
if 'ANDROID_DATA' in os.environ:
return android_backup_dir() if config.get('android_backups') else None
else:
return config.get('backup_dir')
def ensure_sparse_file(filename): def ensure_sparse_file(filename):
# On modern Linux, no need to do anything. # On modern Linux, no need to do anything.

21
electrum/wallet.py

@ -51,7 +51,7 @@ from .util import (NotEnoughFunds, UserCancelled, profiler,
WalletFileException, BitcoinException, MultipleSpendMaxTxOutputs, WalletFileException, BitcoinException, MultipleSpendMaxTxOutputs,
InvalidPassword, format_time, timestamp_to_datetime, Satoshis, InvalidPassword, format_time, timestamp_to_datetime, Satoshis,
Fiat, bfh, bh2u, TxMinedInfo, quantize_feerate, create_bip21_uri, OrderedDictWithIndex) Fiat, bfh, bh2u, TxMinedInfo, quantize_feerate, create_bip21_uri, OrderedDictWithIndex)
from .util import PR_TYPE_ONCHAIN, PR_TYPE_LN from .util import PR_TYPE_ONCHAIN, PR_TYPE_LN, get_backup_dir
from .simple_config import SimpleConfig from .simple_config import SimpleConfig
from .bitcoin import (COIN, is_address, address_to_script, from .bitcoin import (COIN, is_address, address_to_script,
is_minikey, relayfee, dust_threshold) is_minikey, relayfee, dust_threshold)
@ -263,6 +263,20 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
if self.storage: if self.storage:
self.db.write(self.storage) self.db.write(self.storage)
def save_backup(self):
backup_dir = get_backup_dir(self.config)
if backup_dir is None:
return
new_db = WalletDB(self.db.dump(), manual_upgrades=False)
new_db.put('is_backup', True)
new_path = os.path.join(backup_dir, self.basename() + '.backup')
new_storage = WalletStorage(new_path)
new_storage._encryption_version = self.storage._encryption_version
new_storage.pubkey = self.storage.pubkey
new_db.set_modified(True)
new_db.write(new_storage)
return new_path
def has_lightning(self): def has_lightning(self):
return bool(self.lnworker) return bool(self.lnworker)
@ -285,6 +299,9 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
self.db.put('lightning_privkey2', None) self.db.put('lightning_privkey2', None)
self.save_db() self.save_db()
def is_lightning_backup(self):
return self.has_lightning() and self.db.get('is_backup')
def stop_threads(self): def stop_threads(self):
super().stop_threads() super().stop_threads()
if any([ks.is_requesting_to_be_rewritten_to_wallet_file for ks in self.get_keystores()]): if any([ks.is_requesting_to_be_rewritten_to_wallet_file for ks in self.get_keystores()]):
@ -301,7 +318,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
def start_network(self, network): def start_network(self, network):
AddressSynchronizer.start_network(self, network) AddressSynchronizer.start_network(self, network)
if self.lnworker and network: if self.lnworker and network and not self.is_lightning_backup():
network.maybe_init_lightning() network.maybe_init_lightning()
self.lnworker.start_network(network) self.lnworker.start_network(network)

Loading…
Cancel
Save