electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit bb739f4de933580ed1a2555dbd87db392578bcd9
parent edc00b448f95fca71284ba65320164ea5aa67fc8
Author: ThomasV <thomasv@electrum.org>
Date:   Sat, 15 Feb 2020 17:31:14 +0100

Merge pull request #5951 from spesmilo/ln_backups

save wallet backups on channel creation
Diffstat:
Melectrum/gui/kivy/main_window.py | 119+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Aelectrum/gui/kivy/theming/light/eye1.png | 0
Melectrum/gui/kivy/tools/buildozer.spec | 2+-
Melectrum/gui/kivy/uix/dialogs/password_dialog.py | 99++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Melectrum/gui/kivy/uix/dialogs/settings.py | 32+++++++++++++++++++++++++-------
Melectrum/gui/kivy/uix/ui_screens/status.kv | 7++++---
Melectrum/gui/qt/__init__.py | 1+
Melectrum/gui/qt/channels_list.py | 17+++++++++++------
Melectrum/gui/qt/main_window.py | 34+++++++++++++++++++++-------------
Melectrum/gui/qt/settings_dialog.py | 15+++++++++++++++
Melectrum/lnpeer.py | 4++++
Melectrum/lnworker.py | 1+
Melectrum/tests/test_lnpeer.py | 2++
Melectrum/util.py | 15+++++++++++++++
Melectrum/wallet.py | 21+++++++++++++++++++--
15 files changed, 284 insertions(+), 85 deletions(-)

diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py @@ -7,7 +7,7 @@ import traceback from decimal import Decimal import threading 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.wallet_db import WalletDB @@ -31,6 +31,7 @@ from kivy.clock import Clock from kivy.factory import Factory from kivy.metrics import inch from kivy.lang import Builder +from .uix.dialogs.password_dialog import PasswordDialog ## lazy imports for factory so that widgets can be used in kv #Factory.register('InstallWizard', module='electrum.gui.kivy.uix.dialogs.installwizard') @@ -163,6 +164,10 @@ class ElectrumWindow(App): def on_use_rbf(self, instance, x): 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) def on_use_change(self, instance, x): if self.wallet: @@ -326,6 +331,7 @@ class ElectrumWindow(App): self.wallet = None # type: Optional[Abstract_Wallet] self.pause_time = 0 self.asyncio_loop = asyncio.get_event_loop() + self.password = None App.__init__(self)#, **kwargs) @@ -619,9 +625,15 @@ class ElectrumWindow(App): return wallet = self.daemon.load_wallet(path, None) if wallet: - if platform == 'android' and wallet.has_password(): - self.password_dialog(wallet=wallet, msg=_('Enter PIN code'), - on_success=lambda x: self.load_wallet(wallet), on_failure=self.stop) + if wallet.has_password(): + def on_success(x): + # 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: self.load_wallet(wallet) else: @@ -637,10 +649,13 @@ class ElectrumWindow(App): if not storage.is_encrypted_with_user_pw(): raise Exception("Kivy GUI does not support this type of encrypted wallet files.") def on_password(pw): + self.password = pw storage.decrypt(pw) self._on_decrypted_storage(storage) - self.password_dialog(wallet=storage, msg=_('Enter PIN code'), - on_success=on_password, on_failure=self.stop) + self.password_dialog( + check_password=storage.check_password, + on_success=on_password, + on_failure=self.stop) return self._on_decrypted_storage(storage) if not ask_if_wizard: @@ -934,7 +949,7 @@ class ElectrumWindow(App): def on_resume(self): now = time.time() 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: self.nfcscanner.nfc_enable() @@ -1096,12 +1111,12 @@ class ElectrumWindow(App): def on_fee(self, event, *arg): self.fee_status = self.electrum_config.get_fee_status() - def protected(self, msg, f, args): - if self.wallet.has_password(): - on_success = lambda pw: f(*(args + (pw,))) - self.password_dialog(wallet=self.wallet, msg=msg, on_success=on_success, on_failure=lambda: None) + def protected(self, f, args): + if self.electrum_config.get('pin_code'): + on_success = lambda pw: f(*(args + (self.password,))) + self.password_dialog(check_password=self.check_pin_code, on_success=on_success, on_failure=lambda: None, is_password=False) else: - f(*(args + (None,))) + f(*(args + (self.password,))) def toggle_lightning(self): if self.wallet.has_lightning(): @@ -1161,44 +1176,88 @@ class ElectrumWindow(App): self.load_wallet_by_name(new_path) 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): if self.wallet.has_password() and password is None: return keystore = self.wallet.keystore - try: - seed = keystore.get_seed(password) - passphrase = keystore.get_passphrase(password) - except: - self.show_error("Invalid PIN") - return + seed = keystore.get_seed(password) + passphrase = keystore.get_passphrase(password) label.data = seed if passphrase: label.data += '\n\n' + _('Passphrase') + ': ' + passphrase - def password_dialog(self, *, wallet: Union[Abstract_Wallet, WalletStorage], - msg: str, on_success: Callable = None, on_failure: Callable = None): - from .uix.dialogs.password_dialog import PasswordDialog + def has_pin_code(self): + return bool(self.electrum_config.get('pin_code')) + + 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: self._password_dialog = PasswordDialog() - self._password_dialog.init(self, wallet=wallet, msg=msg, - on_success=on_success, on_failure=on_failure) + self._password_dialog.init( + self, check_password = check_password, + on_success=on_success, on_failure=on_failure, + is_password=is_password) self._password_dialog.open() def change_password(self, cb): - from .uix.dialogs.password_dialog import PasswordDialog if self._password_dialog is None: self._password_dialog = PasswordDialog() - message = _("Changing PIN code.") + '\n' + _("Enter your current PIN:") def on_success(old_password, new_password): self.wallet.update_password(old_password, new_password) - self.show_info(_("Your PIN code was updated")) - on_failure = lambda: self.show_error(_("PIN codes do not match")) - self._password_dialog.init(self, wallet=self.wallet, msg=message, - on_success=on_success, on_failure=on_failure, is_change=1) + self.password = new_password + self.show_info(_("Your password was updated")) + on_failure = lambda: self.show_error(_("Password not updated")) + 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() + 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): if self.wallet.is_watching_only(): self.show_info(_('This is a watching-only wallet. It does not contain private keys.')) diff --git a/electrum/gui/kivy/theming/light/eye1.png b/electrum/gui/kivy/theming/light/eye1.png Binary files differ. diff --git a/electrum/gui/kivy/tools/buildozer.spec b/electrum/gui/kivy/tools/buildozer.spec @@ -67,7 +67,7 @@ fullscreen = False # # (list) Permissions -android.permissions = INTERNET, CAMERA +android.permissions = INTERNET, CAMERA, WRITE_EXTERNAL_STORAGE # (int) Android API to use android.api = 28 diff --git a/electrum/gui/kivy/uix/dialogs/password_dialog.py b/electrum/gui/kivy/uix/dialogs/password_dialog.py @@ -19,6 +19,7 @@ Builder.load_string(''' <PasswordDialog@Popup> id: popup + is_generic: False title: 'Electrum' message: '' BoxLayout: @@ -27,14 +28,45 @@ Builder.load_string(''' Widget: size_hint: 1, 0.05 Label: + size_hint: 0.70, None font_size: '20dp' text: root.message text_size: self.width, None - size: self.texture_size Widget: 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: - 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' text: '*'*len(kb.password) + '-'*(6-len(kb.password)) size: self.texture_size @@ -42,6 +74,7 @@ Builder.load_string(''' size_hint: 1, 0.05 GridLayout: id: kb + disabled: root.is_generic size_hint: 1, None height: self.minimum_height update_amount: popup.update_password @@ -79,31 +112,48 @@ Builder.load_string(''' class PasswordDialog(Factory.Popup): def init(self, app: 'ElectrumWindow', *, - wallet: Union['Abstract_Wallet', 'WalletStorage'] = None, - msg: str, on_success: Callable = None, on_failure: Callable = None, - is_change: int = 0): + check_password = None, + on_success: Callable = None, on_failure: Callable = None, + is_change: bool = False, + is_password: bool = False, + has_password: bool = False): self.app = app - self.wallet = wallet - self.message = msg + self.pw_check = check_password + self.message = '' self.on_success = on_success self.on_failure = on_failure - self.ids.kb.password = '' self.success = False self.is_change = is_change self.pw = 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): - if self.is_change > 1: + if self.level > 0: return True try: - self.wallet.check_password(password) + self.pw_check(password) return True except InvalidPassword as e: return False 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 self.on_failure: self.on_failure() @@ -126,25 +176,28 @@ class PasswordDialog(Factory.Popup): text += c 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.is_change == 0: + if self.is_change is False: self.success = True self.pw = pw self.message = _('Please wait...') self.dismiss() - elif self.is_change == 1: + elif self.level == 0: + self.level = 1 self.pw = pw - self.message = _('Enter new PIN') - self.ids.kb.password = '' - self.is_change = 2 - elif self.is_change == 2: + self.update_screen() + elif self.level == 1: + self.level = 2 self.new_password = pw - self.message = _('Confirm new PIN') - self.ids.kb.password = '' - self.is_change = 3 - elif self.is_change == 3: + self.update_screen() + elif self.level == 2: self.success = pw == self.new_password self.dismiss() else: diff --git a/electrum/gui/kivy/uix/dialogs/settings.py b/electrum/gui/kivy/uix/dialogs/settings.py @@ -18,7 +18,8 @@ Builder.load_string(''' <SettingsDialog@Popup> id: settings title: _('Electrum Settings') - disable_pin: False + disable_password: False + has_pin_code: False use_encryption: False BoxLayout: orientation: 'vertical' @@ -36,10 +37,10 @@ Builder.load_string(''' action: partial(root.language_dialog, self) CardSeparator SettingsItem: - disabled: root.disable_pin - title: _('PIN code') - description: _("Change your PIN code.") - action: partial(root.change_password, self) + status: 'ON' if root.has_pin_code else 'OFF' + title: _('PIN code') + ': ' + self.status + description: _("Change your PIN code.") if root.has_pin_code else _("Add PIN code") + action: partial(root.change_pin_code, self) CardSeparator SettingsItem: bu: app.base_unit @@ -82,6 +83,19 @@ Builder.load_string(''' description: _("Send your change to separate addresses.") message: _('Send excess coins to change addresses') 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 #CardSeparator @@ -112,15 +126,19 @@ class SettingsDialog(Factory.Popup): def update(self): 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.has_pin_code = self.app.has_pin_code() def get_language_name(self): 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) + def change_pin_code(self, label, dt): + self.app.change_pin_code(self.update) + def language_dialog(self, item, dt): if self._language_dialog is None: l = self.config.get('language', 'en_UK') diff --git a/electrum/gui/kivy/uix/ui_screens/status.kv b/electrum/gui/kivy/uix/ui_screens/status.kv @@ -83,13 +83,14 @@ Popup: Button: size_hint: 0.5, None height: '48dp' - text: _('Disable LN') if app.wallet.has_lightning() else _('Enable LN') + text: _('Save Backup') on_release: root.dismiss() - app.toggle_lightning() + app.save_backup() Button: size_hint: 0.5, None height: '48dp' - text: _('Close') + text: _('Disable LN') if app.wallet.has_lightning() else _('Enable LN') on_release: root.dismiss() + app.toggle_lightning() diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py @@ -233,6 +233,7 @@ class ElectrumGui(Logger): run_hook('on_new_window', w) w.warn_if_testnet() w.warn_if_watching_only() + w.warn_if_lightning_backup() return w def count_wizards_in_progress(func): diff --git a/electrum/gui/qt/channels_list.py b/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.i18n import _ -from electrum.lnchannel import Channel +from electrum.lnchannel import Channel, peer_states from electrum.wallet import Abstract_Wallet 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) def force_close(self, channel_id): - def task(): - coro = self.lnworker.force_close_channel(channel_id) - return self.network.run_from_another_thread(coro) - if self.parent.question('Force-close channel?\nReclaimed funds will not be immediately available.'): + 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(): + coro = self.lnworker.force_close_channel(channel_id) + return self.network.run_from_another_thread(coro) WaitingDialog(self, 'please wait..', task, self.on_success, self.on_failure) def remove_channel(self, channel_id): @@ -105,7 +109,8 @@ class ChannelsList(MyTreeView): menu.addAction(_("Details..."), lambda: self.details(channel_id)) self.add_copy_menu(menu, idx) if not chan.is_closed(): - menu.addAction(_("Close channel"), lambda: self.close_channel(channel_id)) + if chan.peer_state == peer_states.GOOD: + menu.addAction(_("Close channel"), lambda: self.close_channel(channel_id)) menu.addAction(_("Force-close channel"), lambda: self.force_close(channel_id)) else: menu.addAction(_("Remove"), lambda: self.remove_channel(channel_id)) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py @@ -513,6 +513,18 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): ]) 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): if not constants.net.TESTNET: return @@ -549,20 +561,16 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): return self.gui_object.new_window(filename) - 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: + try: + new_path = self.wallet.save_backup() + 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")) return - new_path = os.path.join(wallet_folder, filename) - if new_path != path: - try: - shutil.copy2(path, new_path) - self.show_message(_("A copy of your wallet file was created in")+" '%s'" % str(new_path), title=_("Wallet backup created")) - 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")) + 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): recent = self.config.get('recently_open', []) @@ -604,7 +612,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): self.recently_visited_menu = file_menu.addMenu(_("&Recently 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(_("&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.addSeparator() file_menu.addAction(_("&Quit"), self.close) diff --git a/electrum/gui/qt/settings_dialog.py b/electrum/gui/qt/settings_dialog.py @@ -145,6 +145,14 @@ class SettingsDialog(WindowModalDialog): # lightning 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 you close all your wallet windows. Your local watchtower will keep 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: 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): name = self.config.get('ssl_certfile', '') filename, __ = QFileDialog.getOpenFileName(self, "Select your SSL certificate file", name) diff --git a/electrum/lnpeer.py b/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.") await self.lnworker.force_close_channel(chan_id) 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 # note: chan.short_channel_id being set implies the funding txn is already at sufficient depth diff --git a/electrum/lnworker.py b/electrum/lnworker.py @@ -842,6 +842,7 @@ class LNWallet(LNWorker): with self.lock: self.channels[chan.channel_id] = chan self.lnwatcher.add_channel(chan.funding_outpoint.to_str(), chan.get_funding_address()) + self.wallet.save_backup() @log_exceptions async def add_peer(self, connect_str: str) -> Peer: diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py @@ -73,6 +73,8 @@ class MockWallet: pass def save_db(self): pass + def is_lightning_backup(self): + return False class MockLNWallet: def __init__(self, remote_keypair, local_keypair, chan, tx_queue): diff --git a/electrum/util.py b/electrum/util.py @@ -425,11 +425,26 @@ def profiler(func): 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(): import jnius PythonActivity = jnius.autoclass('org.kivy.android.PythonActivity') 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): # On modern Linux, no need to do anything. diff --git a/electrum/wallet.py b/electrum/wallet.py @@ -51,7 +51,7 @@ from .util import (NotEnoughFunds, UserCancelled, profiler, WalletFileException, BitcoinException, MultipleSpendMaxTxOutputs, InvalidPassword, format_time, timestamp_to_datetime, Satoshis, 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 .bitcoin import (COIN, is_address, address_to_script, is_minikey, relayfee, dust_threshold) @@ -263,6 +263,20 @@ class Abstract_Wallet(AddressSynchronizer, ABC): if 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): return bool(self.lnworker) @@ -285,6 +299,9 @@ class Abstract_Wallet(AddressSynchronizer, ABC): self.db.put('lightning_privkey2', None) self.save_db() + def is_lightning_backup(self): + return self.has_lightning() and self.db.get('is_backup') + def stop_threads(self): super().stop_threads() 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): 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() self.lnworker.start_network(network)