URI: 
       tMerge pull request #5951 from spesmilo/ln_backups - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit bb739f4de933580ed1a2555dbd87db392578bcd9
   DIR parent edc00b448f95fca71284ba65320164ea5aa67fc8
  HTML 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:
         M electrum/gui/kivy/main_window.py    |     119 +++++++++++++++++++++++--------
         A electrum/gui/kivy/theming/light/ey… |       0 
         M electrum/gui/kivy/tools/buildozer.… |       2 +-
         M electrum/gui/kivy/uix/dialogs/pass… |      99 ++++++++++++++++++++++++-------
         M electrum/gui/kivy/uix/dialogs/sett… |      32 ++++++++++++++++++++++++-------
         M electrum/gui/kivy/uix/ui_screens/s… |       7 ++++---
         M electrum/gui/qt/__init__.py         |       1 +
         M electrum/gui/qt/channels_list.py    |      17 +++++++++++------
         M electrum/gui/qt/main_window.py      |      34 +++++++++++++++++++------------
         M electrum/gui/qt/settings_dialog.py  |      15 +++++++++++++++
         M electrum/lnpeer.py                  |       4 ++++
         M electrum/lnworker.py                |       1 +
         M electrum/tests/test_lnpeer.py       |       2 ++
         M electrum/util.py                    |      15 +++++++++++++++
         M electrum/wallet.py                  |      21 +++++++++++++++++++--
       
       15 files changed, 284 insertions(+), 85 deletions(-)
       ---
   DIR diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py
       t@@ -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
       t@@ -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')
       t@@ -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:
       t@@ -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)
        
       t@@ -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:
       t@@ -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:
       t@@ -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()
        
       t@@ -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():
       t@@ -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.'))
   DIR diff --git a/electrum/gui/kivy/theming/light/eye1.png b/electrum/gui/kivy/theming/light/eye1.png
       Binary files differ.
   DIR diff --git a/electrum/gui/kivy/tools/buildozer.spec b/electrum/gui/kivy/tools/buildozer.spec
       t@@ -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
   DIR diff --git a/electrum/gui/kivy/uix/dialogs/password_dialog.py b/electrum/gui/kivy/uix/dialogs/password_dialog.py
       t@@ -19,6 +19,7 @@ Builder.load_string('''
        
        <PasswordDialog@Popup>
            id: popup
       +    is_generic: False
            title: 'Electrum'
            message: ''
            BoxLayout:
       t@@ -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
       t@@ -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
       t@@ -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()
       t@@ -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:
   DIR diff --git a/electrum/gui/kivy/uix/dialogs/settings.py b/electrum/gui/kivy/uix/dialogs/settings.py
       t@@ -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'
       t@@ -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
       t@@ -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
       t@@ -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')
   DIR diff --git a/electrum/gui/kivy/uix/ui_screens/status.kv b/electrum/gui/kivy/uix/ui_screens/status.kv
       t@@ -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()
   DIR diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py
       t@@ -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):
   DIR diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py
       t@@ -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
        
       t@@ -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):
       t@@ -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))
   DIR diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
       t@@ -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
       t@@ -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', [])
       t@@ -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)
   DIR diff --git a/electrum/gui/qt/settings_dialog.py b/electrum/gui/qt/settings_dialog.py
       t@@ -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
       t@@ -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)
   DIR diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py
       t@@ -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
   DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py
       t@@ -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:
   DIR diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py
       t@@ -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):
   DIR diff --git a/electrum/util.py b/electrum/util.py
       t@@ -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.
   DIR diff --git a/electrum/wallet.py b/electrum/wallet.py
       t@@ -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)
       t@@ -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)
        
       t@@ -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()]):
       t@@ -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)