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
This commit is contained in:
@@ -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.'))
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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
|
||||||
self.show_message(_("A copy of your wallet file was created in")+" '%s'" % str(new_path), title=_("Wallet backup created"))
|
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', [])
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user