Browse Source

Automate backups:

- backup wallet file on each channel creation
 - on android, a backup password is entered in settings
 - on desktop, the backup path is in settings
hard-fail-on-bad-server-string
ThomasV 5 years ago
parent
commit
2dad87cbb4
  1. 20
      electrum/gui/kivy/main_window.py
  2. 8
      electrum/gui/kivy/uix/dialogs/settings.py
  3. 15
      electrum/gui/qt/main_window.py
  4. 15
      electrum/gui/qt/settings_dialog.py
  5. 1
      electrum/lnworker.py
  6. 2
      electrum/util.py
  7. 26
      electrum/wallet.py

20
electrum/gui/kivy/main_window.py

@ -1199,21 +1199,31 @@ class ElectrumWindow(App):
on_success=on_success, on_failure=on_failure, is_change=1) on_success=on_success, on_failure=on_failure, is_change=1)
self._password_dialog.open() self._password_dialog.open()
def save_backup(self): def change_backup_password(self):
from .uix.dialogs.password_dialog import PasswordDialog from .uix.dialogs.password_dialog import PasswordDialog
from electrum.util import get_backup_dir from electrum.util import get_backup_dir
from electrum.storage import WalletStorage
if self._password_dialog is None: if self._password_dialog is None:
self._password_dialog = PasswordDialog() self._password_dialog = PasswordDialog()
message = _("Create backup.") + '\n' + _("Enter your current PIN:") message = _("Create backup.") + '\n' + _("Enter your current PIN:")
def on_success(old_password, new_password): def on_success(old_password, new_password):
new_path = os.path.join(get_backup_dir(self.electrum_config), self.wallet.basename() + '.backup') backup_pubkey = WalletStorage.get_eckey_from_password(new_password).get_public_key_hex()
self.wallet.save_backup(new_path, old_password=old_password, new_password=new_password) # TODO: use a unique PIN for all wallets
self.show_info(_("Backup saved:") + f"\n{new_path}") self.electrum_config.set_key('pin_code', old_password)
on_failure = lambda: self.show_error(_("PIN codes do not match")) self.electrum_config.set_key('backup_pubkey', backup_pubkey)
self.show_info(_("Backup password set"))
on_failure = lambda: self.show_error(_("Passwords do not match"))
self._password_dialog.init(self, wallet=self.wallet, msg=message, self._password_dialog.init(self, wallet=self.wallet, msg=message,
on_success=on_success, on_failure=on_failure, is_change=1, is_backup=True) on_success=on_success, on_failure=on_failure, is_change=1, is_backup=True)
self._password_dialog.open() self._password_dialog.open()
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 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.'))

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

@ -82,6 +82,11 @@ 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:
title: _('Backups')
description: _("Set password for encrypted backups.")
action: root.change_backup_password
# disabled: there is currently only one coin selection policy # disabled: there is currently only one coin selection policy
#CardSeparator #CardSeparator
@ -121,6 +126,9 @@ class SettingsDialog(Factory.Popup):
def change_password(self, item, dt): def change_password(self, item, dt):
self.app.change_password(self.update) self.app.change_password(self.update)
def change_backup_password(self, dt):
self.app.change_backup_password()
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')

15
electrum/gui/qt/main_window.py

@ -562,20 +562,15 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
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:
return
try: try:
self.wallet.save_backup(new_path) new_path = self.wallet.save_backup()
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 return
if new_path:
self.show_message(_("A copy of your wallet file was created in")+" '%s'" % str(new_path), title=_("Wallet backup created")) 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', [])
@ -617,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)

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/util.py

@ -442,7 +442,7 @@ def android_data_dir():
return PythonActivity.mActivity.getFilesDir().getPath() + '/data' return PythonActivity.mActivity.getFilesDir().getPath() + '/data'
def get_backup_dir(config): def get_backup_dir(config):
return android_backup_dir() if 'ANDROID_DATA' in os.environ else config.path return android_backup_dir() if 'ANDROID_DATA' in os.environ else config.get('backup_dir')
def ensure_sparse_file(filename): def ensure_sparse_file(filename):

26
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,15 +263,25 @@ 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, path, *, old_password=None, new_password=None): def save_backup(self):
new_db = WalletDB(self.db.dump(), manual_upgrades=False) new_db = WalletDB(self.db.dump(), manual_upgrades=False)
new_db.put('is_backup', True) new_db.put('is_backup', True)
new_storage = WalletStorage(path) new_path = os.path.join(get_backup_dir(self.config), self.basename() + '.backup')
new_storage._encryption_version = StorageEncryptionVersion.PLAINTEXT if new_path is None:
w2 = Wallet(new_db, new_storage, config=self.config) return
if new_password: new_storage = WalletStorage(new_path)
w2.update_password(old_password, new_password, encrypt_storage=True) if 'ANDROID_DATA' in os.environ:
w2.save_db() pin_code = self.config.get('pin_code')
w2 = Wallet(new_db, None, config=self.config)
w2.update_password(pin_code, None)
new_storage._encryption_version = StorageEncryptionVersion.USER_PASSWORD
new_storage.pubkey = self.config.get('backup_pubkey')
else:
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)

Loading…
Cancel
Save