mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-27 15:31:31 +00:00
Merge pull request #5951 from spesmilo/ln_backups
save wallet backups on channel creation
This commit is contained in:
commit
bb739f4de9
15 changed files with 287 additions and 88 deletions
|
@ -7,7 +7,7 @@ import traceback
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
import threading
|
import threading
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import TYPE_CHECKING, Optional, Union, Callable
|
from typing import TYPE_CHECKING, Optional, Union, Callable, Sequence
|
||||||
|
|
||||||
from electrum.storage import WalletStorage, StorageReadWriteError
|
from electrum.storage import WalletStorage, StorageReadWriteError
|
||||||
from electrum.wallet_db import WalletDB
|
from electrum.wallet_db import WalletDB
|
||||||
|
@ -31,6 +31,7 @@ from kivy.clock import Clock
|
||||||
from kivy.factory import Factory
|
from kivy.factory import Factory
|
||||||
from kivy.metrics import inch
|
from kivy.metrics import inch
|
||||||
from kivy.lang import Builder
|
from kivy.lang import Builder
|
||||||
|
from .uix.dialogs.password_dialog import PasswordDialog
|
||||||
|
|
||||||
## lazy imports for factory so that widgets can be used in kv
|
## lazy imports for factory so that widgets can be used in kv
|
||||||
#Factory.register('InstallWizard', module='electrum.gui.kivy.uix.dialogs.installwizard')
|
#Factory.register('InstallWizard', module='electrum.gui.kivy.uix.dialogs.installwizard')
|
||||||
|
@ -163,6 +164,10 @@ class ElectrumWindow(App):
|
||||||
def on_use_rbf(self, instance, x):
|
def on_use_rbf(self, instance, x):
|
||||||
self.electrum_config.set_key('use_rbf', self.use_rbf, True)
|
self.electrum_config.set_key('use_rbf', self.use_rbf, True)
|
||||||
|
|
||||||
|
android_backups = BooleanProperty(False)
|
||||||
|
def on_android_backups(self, instance, x):
|
||||||
|
self.electrum_config.set_key('android_backups', self.android_backups, True)
|
||||||
|
|
||||||
use_change = BooleanProperty(False)
|
use_change = BooleanProperty(False)
|
||||||
def on_use_change(self, instance, x):
|
def on_use_change(self, instance, x):
|
||||||
if self.wallet:
|
if self.wallet:
|
||||||
|
@ -326,6 +331,7 @@ class ElectrumWindow(App):
|
||||||
self.wallet = None # type: Optional[Abstract_Wallet]
|
self.wallet = None # type: Optional[Abstract_Wallet]
|
||||||
self.pause_time = 0
|
self.pause_time = 0
|
||||||
self.asyncio_loop = asyncio.get_event_loop()
|
self.asyncio_loop = asyncio.get_event_loop()
|
||||||
|
self.password = None
|
||||||
|
|
||||||
App.__init__(self)#, **kwargs)
|
App.__init__(self)#, **kwargs)
|
||||||
|
|
||||||
|
@ -619,9 +625,15 @@ class ElectrumWindow(App):
|
||||||
return
|
return
|
||||||
wallet = self.daemon.load_wallet(path, None)
|
wallet = self.daemon.load_wallet(path, None)
|
||||||
if wallet:
|
if wallet:
|
||||||
if platform == 'android' and wallet.has_password():
|
if wallet.has_password():
|
||||||
self.password_dialog(wallet=wallet, msg=_('Enter PIN code'),
|
def on_success(x):
|
||||||
on_success=lambda x: self.load_wallet(wallet), on_failure=self.stop)
|
# save pin_code so that we can create backups
|
||||||
|
self.password = x
|
||||||
|
self.load_wallet(wallet)
|
||||||
|
self.password_dialog(
|
||||||
|
check_password=wallet.check_password,
|
||||||
|
on_success=on_success,
|
||||||
|
on_failure=self.stop)
|
||||||
else:
|
else:
|
||||||
self.load_wallet(wallet)
|
self.load_wallet(wallet)
|
||||||
else:
|
else:
|
||||||
|
@ -637,10 +649,13 @@ class ElectrumWindow(App):
|
||||||
if not storage.is_encrypted_with_user_pw():
|
if not storage.is_encrypted_with_user_pw():
|
||||||
raise Exception("Kivy GUI does not support this type of encrypted wallet files.")
|
raise Exception("Kivy GUI does not support this type of encrypted wallet files.")
|
||||||
def on_password(pw):
|
def on_password(pw):
|
||||||
|
self.password = pw
|
||||||
storage.decrypt(pw)
|
storage.decrypt(pw)
|
||||||
self._on_decrypted_storage(storage)
|
self._on_decrypted_storage(storage)
|
||||||
self.password_dialog(wallet=storage, msg=_('Enter PIN code'),
|
self.password_dialog(
|
||||||
on_success=on_password, on_failure=self.stop)
|
check_password=storage.check_password,
|
||||||
|
on_success=on_password,
|
||||||
|
on_failure=self.stop)
|
||||||
return
|
return
|
||||||
self._on_decrypted_storage(storage)
|
self._on_decrypted_storage(storage)
|
||||||
if not ask_if_wizard:
|
if not ask_if_wizard:
|
||||||
|
@ -934,7 +949,7 @@ class ElectrumWindow(App):
|
||||||
def on_resume(self):
|
def on_resume(self):
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if self.wallet and self.wallet.has_password() and now - self.pause_time > 60:
|
if self.wallet and self.wallet.has_password() and now - self.pause_time > 60:
|
||||||
self.password_dialog(wallet=self.wallet, msg=_('Enter PIN'), on_success=None, on_failure=self.stop)
|
self.password_dialog(check_password=self.check_pin_code, on_success=None, on_failure=self.stop, is_password=False)
|
||||||
if self.nfcscanner:
|
if self.nfcscanner:
|
||||||
self.nfcscanner.nfc_enable()
|
self.nfcscanner.nfc_enable()
|
||||||
|
|
||||||
|
@ -1096,12 +1111,12 @@ class ElectrumWindow(App):
|
||||||
def on_fee(self, event, *arg):
|
def on_fee(self, event, *arg):
|
||||||
self.fee_status = self.electrum_config.get_fee_status()
|
self.fee_status = self.electrum_config.get_fee_status()
|
||||||
|
|
||||||
def protected(self, msg, f, args):
|
def protected(self, f, args):
|
||||||
if self.wallet.has_password():
|
if self.electrum_config.get('pin_code'):
|
||||||
on_success = lambda pw: f(*(args + (pw,)))
|
on_success = lambda pw: f(*(args + (self.password,)))
|
||||||
self.password_dialog(wallet=self.wallet, msg=msg, on_success=on_success, on_failure=lambda: None)
|
self.password_dialog(check_password=self.check_pin_code, on_success=on_success, on_failure=lambda: None, is_password=False)
|
||||||
else:
|
else:
|
||||||
f(*(args + (None,)))
|
f(*(args + (self.password,)))
|
||||||
|
|
||||||
def toggle_lightning(self):
|
def toggle_lightning(self):
|
||||||
if self.wallet.has_lightning():
|
if self.wallet.has_lightning():
|
||||||
|
@ -1161,44 +1176,88 @@ class ElectrumWindow(App):
|
||||||
self.load_wallet_by_name(new_path)
|
self.load_wallet_by_name(new_path)
|
||||||
|
|
||||||
def show_seed(self, label):
|
def show_seed(self, label):
|
||||||
self.protected(_("Enter your PIN code in order to decrypt your seed"), self._show_seed, (label,))
|
self.protected(self._show_seed, (label,))
|
||||||
|
|
||||||
def _show_seed(self, label, password):
|
def _show_seed(self, label, password):
|
||||||
if self.wallet.has_password() and password is None:
|
if self.wallet.has_password() and password is None:
|
||||||
return
|
return
|
||||||
keystore = self.wallet.keystore
|
keystore = self.wallet.keystore
|
||||||
try:
|
seed = keystore.get_seed(password)
|
||||||
seed = keystore.get_seed(password)
|
passphrase = keystore.get_passphrase(password)
|
||||||
passphrase = keystore.get_passphrase(password)
|
|
||||||
except:
|
|
||||||
self.show_error("Invalid PIN")
|
|
||||||
return
|
|
||||||
label.data = seed
|
label.data = seed
|
||||||
if passphrase:
|
if passphrase:
|
||||||
label.data += '\n\n' + _('Passphrase') + ': ' + passphrase
|
label.data += '\n\n' + _('Passphrase') + ': ' + passphrase
|
||||||
|
|
||||||
def password_dialog(self, *, wallet: Union[Abstract_Wallet, WalletStorage],
|
def has_pin_code(self):
|
||||||
msg: str, on_success: Callable = None, on_failure: Callable = None):
|
return bool(self.electrum_config.get('pin_code'))
|
||||||
from .uix.dialogs.password_dialog import PasswordDialog
|
|
||||||
|
def check_pin_code(self, pin):
|
||||||
|
if pin != self.electrum_config.get('pin_code'):
|
||||||
|
raise InvalidPassword
|
||||||
|
|
||||||
|
def password_dialog(self, *, check_password: Callable = None,
|
||||||
|
on_success: Callable = None, on_failure: Callable = None,
|
||||||
|
is_password=True):
|
||||||
if self._password_dialog is None:
|
if self._password_dialog is None:
|
||||||
self._password_dialog = PasswordDialog()
|
self._password_dialog = PasswordDialog()
|
||||||
self._password_dialog.init(self, wallet=wallet, msg=msg,
|
self._password_dialog.init(
|
||||||
on_success=on_success, on_failure=on_failure)
|
self, check_password = check_password,
|
||||||
|
on_success=on_success, on_failure=on_failure,
|
||||||
|
is_password=is_password)
|
||||||
self._password_dialog.open()
|
self._password_dialog.open()
|
||||||
|
|
||||||
def change_password(self, cb):
|
def change_password(self, cb):
|
||||||
from .uix.dialogs.password_dialog import PasswordDialog
|
|
||||||
if self._password_dialog is None:
|
if self._password_dialog is None:
|
||||||
self._password_dialog = PasswordDialog()
|
self._password_dialog = PasswordDialog()
|
||||||
message = _("Changing PIN code.") + '\n' + _("Enter your current PIN:")
|
|
||||||
def on_success(old_password, new_password):
|
def on_success(old_password, new_password):
|
||||||
self.wallet.update_password(old_password, new_password)
|
self.wallet.update_password(old_password, new_password)
|
||||||
self.show_info(_("Your PIN code was updated"))
|
self.password = new_password
|
||||||
on_failure = lambda: self.show_error(_("PIN codes do not match"))
|
self.show_info(_("Your password was updated"))
|
||||||
self._password_dialog.init(self, wallet=self.wallet, msg=message,
|
on_failure = lambda: self.show_error(_("Password not updated"))
|
||||||
on_success=on_success, on_failure=on_failure, is_change=1)
|
self._password_dialog.init(
|
||||||
|
self, check_password = self.wallet.check_password,
|
||||||
|
on_success=on_success, on_failure=on_failure,
|
||||||
|
is_change=True, is_password=True,
|
||||||
|
has_password=self.wallet.has_password())
|
||||||
self._password_dialog.open()
|
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):
|
def export_private_keys(self, pk_label, addr):
|
||||||
if self.wallet.is_watching_only():
|
if self.wallet.is_watching_only():
|
||||||
self.show_info(_('This is a watching-only wallet. It does not contain private keys.'))
|
self.show_info(_('This is a watching-only wallet. It does not contain private keys.'))
|
||||||
|
|
BIN
electrum/gui/kivy/theming/light/eye1.png
Normal file
BIN
electrum/gui/kivy/theming/light/eye1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
|
@ -67,7 +67,7 @@ fullscreen = False
|
||||||
#
|
#
|
||||||
|
|
||||||
# (list) Permissions
|
# (list) Permissions
|
||||||
android.permissions = INTERNET, CAMERA
|
android.permissions = INTERNET, CAMERA, WRITE_EXTERNAL_STORAGE
|
||||||
|
|
||||||
# (int) Android API to use
|
# (int) Android API to use
|
||||||
android.api = 28
|
android.api = 28
|
||||||
|
|
|
@ -19,6 +19,7 @@ Builder.load_string('''
|
||||||
|
|
||||||
<PasswordDialog@Popup>
|
<PasswordDialog@Popup>
|
||||||
id: popup
|
id: popup
|
||||||
|
is_generic: False
|
||||||
title: 'Electrum'
|
title: 'Electrum'
|
||||||
message: ''
|
message: ''
|
||||||
BoxLayout:
|
BoxLayout:
|
||||||
|
@ -27,14 +28,45 @@ Builder.load_string('''
|
||||||
Widget:
|
Widget:
|
||||||
size_hint: 1, 0.05
|
size_hint: 1, 0.05
|
||||||
Label:
|
Label:
|
||||||
|
size_hint: 0.70, None
|
||||||
font_size: '20dp'
|
font_size: '20dp'
|
||||||
text: root.message
|
text: root.message
|
||||||
text_size: self.width, None
|
text_size: self.width, None
|
||||||
size: self.texture_size
|
|
||||||
Widget:
|
Widget:
|
||||||
size_hint: 1, 0.05
|
size_hint: 1, 0.05
|
||||||
|
BoxLayout:
|
||||||
|
orientation: 'horizontal'
|
||||||
|
id: box_generic_password
|
||||||
|
visible: root.is_generic
|
||||||
|
size_hint_y: 0.05
|
||||||
|
opacity: 1 if self.visible else 0
|
||||||
|
disabled: not self.visible
|
||||||
|
WizardTextInput:
|
||||||
|
id: textinput_generic_password
|
||||||
|
valign: 'center'
|
||||||
|
multiline: False
|
||||||
|
on_text_validate:
|
||||||
|
popup.on_password(self.text)
|
||||||
|
password: True
|
||||||
|
size_hint: 0.9, None
|
||||||
|
unfocus_on_touch: False
|
||||||
|
focus: root.is_generic
|
||||||
|
Button:
|
||||||
|
size_hint: 0.1, None
|
||||||
|
valign: 'center'
|
||||||
|
background_normal: 'atlas://electrum/gui/kivy/theming/light/eye1'
|
||||||
|
background_down: self.background_normal
|
||||||
|
height: '50dp'
|
||||||
|
width: '50dp'
|
||||||
|
padding: '5dp', '5dp'
|
||||||
|
on_release:
|
||||||
|
textinput_generic_password.password = False if textinput_generic_password.password else True
|
||||||
Label:
|
Label:
|
||||||
id: a
|
id: label_pin
|
||||||
|
visible: not root.is_generic
|
||||||
|
size_hint_y: 0.05
|
||||||
|
opacity: 1 if self.visible else 0
|
||||||
|
disabled: not self.visible
|
||||||
font_size: '50dp'
|
font_size: '50dp'
|
||||||
text: '*'*len(kb.password) + '-'*(6-len(kb.password))
|
text: '*'*len(kb.password) + '-'*(6-len(kb.password))
|
||||||
size: self.texture_size
|
size: self.texture_size
|
||||||
|
@ -42,6 +74,7 @@ Builder.load_string('''
|
||||||
size_hint: 1, 0.05
|
size_hint: 1, 0.05
|
||||||
GridLayout:
|
GridLayout:
|
||||||
id: kb
|
id: kb
|
||||||
|
disabled: root.is_generic
|
||||||
size_hint: 1, None
|
size_hint: 1, None
|
||||||
height: self.minimum_height
|
height: self.minimum_height
|
||||||
update_amount: popup.update_password
|
update_amount: popup.update_password
|
||||||
|
@ -79,31 +112,48 @@ Builder.load_string('''
|
||||||
class PasswordDialog(Factory.Popup):
|
class PasswordDialog(Factory.Popup):
|
||||||
|
|
||||||
def init(self, app: 'ElectrumWindow', *,
|
def init(self, app: 'ElectrumWindow', *,
|
||||||
wallet: Union['Abstract_Wallet', 'WalletStorage'] = None,
|
check_password = None,
|
||||||
msg: str, on_success: Callable = None, on_failure: Callable = None,
|
on_success: Callable = None, on_failure: Callable = None,
|
||||||
is_change: int = 0):
|
is_change: bool = False,
|
||||||
|
is_password: bool = False,
|
||||||
|
has_password: bool = False):
|
||||||
self.app = app
|
self.app = app
|
||||||
self.wallet = wallet
|
self.pw_check = check_password
|
||||||
self.message = msg
|
self.message = ''
|
||||||
self.on_success = on_success
|
self.on_success = on_success
|
||||||
self.on_failure = on_failure
|
self.on_failure = on_failure
|
||||||
self.ids.kb.password = ''
|
|
||||||
self.success = False
|
self.success = False
|
||||||
self.is_change = is_change
|
self.is_change = is_change
|
||||||
self.pw = None
|
self.pw = None
|
||||||
self.new_password = None
|
self.new_password = None
|
||||||
self.title = 'Electrum' + (' - ' + self.wallet.basename() if self.wallet else '')
|
self.title = 'Electrum'
|
||||||
|
self.level = 1 if is_change and not has_password else 0
|
||||||
|
self.is_generic = is_password
|
||||||
|
self.update_screen()
|
||||||
|
|
||||||
|
def update_screen(self):
|
||||||
|
self.ids.kb.password = ''
|
||||||
|
self.ids.textinput_generic_password.text = ''
|
||||||
|
if self.level == 0:
|
||||||
|
self.message = _('Enter your password') if self.is_generic else _('Enter your PIN')
|
||||||
|
elif self.level == 1:
|
||||||
|
self.message = _('Enter new password') if self.is_generic else _('Enter new PIN')
|
||||||
|
elif self.level == 2:
|
||||||
|
self.message = _('Confirm new password') if self.is_generic else _('Confirm new PIN')
|
||||||
|
|
||||||
def check_password(self, password):
|
def check_password(self, password):
|
||||||
if self.is_change > 1:
|
if self.level > 0:
|
||||||
return True
|
return True
|
||||||
try:
|
try:
|
||||||
self.wallet.check_password(password)
|
self.pw_check(password)
|
||||||
return True
|
return True
|
||||||
except InvalidPassword as e:
|
except InvalidPassword as e:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def on_dismiss(self):
|
def on_dismiss(self):
|
||||||
|
if self.level == 1 and not self.is_generic and self.on_success:
|
||||||
|
self.on_success(self.pw, None)
|
||||||
|
return False
|
||||||
if not self.success:
|
if not self.success:
|
||||||
if self.on_failure:
|
if self.on_failure:
|
||||||
self.on_failure()
|
self.on_failure()
|
||||||
|
@ -126,25 +176,28 @@ class PasswordDialog(Factory.Popup):
|
||||||
text += c
|
text += c
|
||||||
kb.password = text
|
kb.password = text
|
||||||
|
|
||||||
def on_password(self, pw):
|
|
||||||
if len(pw) == 6:
|
def on_password(self, pw: str):
|
||||||
|
if self.is_generic:
|
||||||
|
if len(pw) < 6:
|
||||||
|
self.app.show_error(_('Password is too short (min {} characters)').format(6))
|
||||||
|
return
|
||||||
|
if len(pw) >= 6:
|
||||||
if self.check_password(pw):
|
if self.check_password(pw):
|
||||||
if self.is_change == 0:
|
if self.is_change is False:
|
||||||
self.success = True
|
self.success = True
|
||||||
self.pw = pw
|
self.pw = pw
|
||||||
self.message = _('Please wait...')
|
self.message = _('Please wait...')
|
||||||
self.dismiss()
|
self.dismiss()
|
||||||
elif self.is_change == 1:
|
elif self.level == 0:
|
||||||
|
self.level = 1
|
||||||
self.pw = pw
|
self.pw = pw
|
||||||
self.message = _('Enter new PIN')
|
self.update_screen()
|
||||||
self.ids.kb.password = ''
|
elif self.level == 1:
|
||||||
self.is_change = 2
|
self.level = 2
|
||||||
elif self.is_change == 2:
|
|
||||||
self.new_password = pw
|
self.new_password = pw
|
||||||
self.message = _('Confirm new PIN')
|
self.update_screen()
|
||||||
self.ids.kb.password = ''
|
elif self.level == 2:
|
||||||
self.is_change = 3
|
|
||||||
elif self.is_change == 3:
|
|
||||||
self.success = pw == self.new_password
|
self.success = pw == self.new_password
|
||||||
self.dismiss()
|
self.dismiss()
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -18,7 +18,8 @@ Builder.load_string('''
|
||||||
<SettingsDialog@Popup>
|
<SettingsDialog@Popup>
|
||||||
id: settings
|
id: settings
|
||||||
title: _('Electrum Settings')
|
title: _('Electrum Settings')
|
||||||
disable_pin: False
|
disable_password: False
|
||||||
|
has_pin_code: False
|
||||||
use_encryption: False
|
use_encryption: False
|
||||||
BoxLayout:
|
BoxLayout:
|
||||||
orientation: 'vertical'
|
orientation: 'vertical'
|
||||||
|
@ -36,10 +37,10 @@ Builder.load_string('''
|
||||||
action: partial(root.language_dialog, self)
|
action: partial(root.language_dialog, self)
|
||||||
CardSeparator
|
CardSeparator
|
||||||
SettingsItem:
|
SettingsItem:
|
||||||
disabled: root.disable_pin
|
status: 'ON' if root.has_pin_code else 'OFF'
|
||||||
title: _('PIN code')
|
title: _('PIN code') + ': ' + self.status
|
||||||
description: _("Change your PIN code.")
|
description: _("Change your PIN code.") if root.has_pin_code else _("Add PIN code")
|
||||||
action: partial(root.change_password, self)
|
action: partial(root.change_pin_code, self)
|
||||||
CardSeparator
|
CardSeparator
|
||||||
SettingsItem:
|
SettingsItem:
|
||||||
bu: app.base_unit
|
bu: app.base_unit
|
||||||
|
@ -82,6 +83,19 @@ Builder.load_string('''
|
||||||
description: _("Send your change to separate addresses.")
|
description: _("Send your change to separate addresses.")
|
||||||
message: _('Send excess coins to change addresses')
|
message: _('Send excess coins to change addresses')
|
||||||
action: partial(root.boolean_dialog, 'use_change', _('Use change addresses'), self.message)
|
action: partial(root.boolean_dialog, 'use_change', _('Use change addresses'), self.message)
|
||||||
|
CardSeparator
|
||||||
|
SettingsItem:
|
||||||
|
disabled: root.disable_password
|
||||||
|
title: _('Password')
|
||||||
|
description: _("Change wallet password.")
|
||||||
|
action: root.change_password
|
||||||
|
CardSeparator
|
||||||
|
SettingsItem:
|
||||||
|
status: _('Yes') if app.android_backups else _('No')
|
||||||
|
title: _('Backups') + ': ' + self.status
|
||||||
|
description: _("Backup wallet to external storage.")
|
||||||
|
message: _("If this option is checked, a backup of your wallet will be written to external storage everytime you create a new channel. Make sure your wallet is protected with a strong password before you enable this option.")
|
||||||
|
action: partial(root.boolean_dialog, 'android_backups', _('Backups'), self.message)
|
||||||
|
|
||||||
# disabled: there is currently only one coin selection policy
|
# disabled: there is currently only one coin selection policy
|
||||||
#CardSeparator
|
#CardSeparator
|
||||||
|
@ -112,15 +126,19 @@ class SettingsDialog(Factory.Popup):
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
self.wallet = self.app.wallet
|
self.wallet = self.app.wallet
|
||||||
self.disable_pin = self.wallet.is_watching_only() if self.wallet else True
|
self.disable_password = self.wallet.is_watching_only() if self.wallet else True
|
||||||
self.use_encryption = self.wallet.has_password() if self.wallet else False
|
self.use_encryption = self.wallet.has_password() if self.wallet else False
|
||||||
|
self.has_pin_code = self.app.has_pin_code()
|
||||||
|
|
||||||
def get_language_name(self):
|
def get_language_name(self):
|
||||||
return languages.get(self.config.get('language', 'en_UK'), '')
|
return languages.get(self.config.get('language', 'en_UK'), '')
|
||||||
|
|
||||||
def change_password(self, item, dt):
|
def change_password(self, dt):
|
||||||
self.app.change_password(self.update)
|
self.app.change_password(self.update)
|
||||||
|
|
||||||
|
def change_pin_code(self, label, dt):
|
||||||
|
self.app.change_pin_code(self.update)
|
||||||
|
|
||||||
def language_dialog(self, item, dt):
|
def language_dialog(self, item, dt):
|
||||||
if self._language_dialog is None:
|
if self._language_dialog is None:
|
||||||
l = self.config.get('language', 'en_UK')
|
l = self.config.get('language', 'en_UK')
|
||||||
|
|
|
@ -80,6 +80,13 @@ Popup:
|
||||||
on_release:
|
on_release:
|
||||||
root.dismiss()
|
root.dismiss()
|
||||||
app.delete_wallet()
|
app.delete_wallet()
|
||||||
|
Button:
|
||||||
|
size_hint: 0.5, None
|
||||||
|
height: '48dp'
|
||||||
|
text: _('Save Backup')
|
||||||
|
on_release:
|
||||||
|
root.dismiss()
|
||||||
|
app.save_backup()
|
||||||
Button:
|
Button:
|
||||||
size_hint: 0.5, None
|
size_hint: 0.5, None
|
||||||
height: '48dp'
|
height: '48dp'
|
||||||
|
@ -87,9 +94,3 @@ Popup:
|
||||||
on_release:
|
on_release:
|
||||||
root.dismiss()
|
root.dismiss()
|
||||||
app.toggle_lightning()
|
app.toggle_lightning()
|
||||||
Button:
|
|
||||||
size_hint: 0.5, None
|
|
||||||
height: '48dp'
|
|
||||||
text: _('Close')
|
|
||||||
on_release:
|
|
||||||
root.dismiss()
|
|
||||||
|
|
|
@ -233,6 +233,7 @@ class ElectrumGui(Logger):
|
||||||
run_hook('on_new_window', w)
|
run_hook('on_new_window', w)
|
||||||
w.warn_if_testnet()
|
w.warn_if_testnet()
|
||||||
w.warn_if_watching_only()
|
w.warn_if_watching_only()
|
||||||
|
w.warn_if_lightning_backup()
|
||||||
return w
|
return w
|
||||||
|
|
||||||
def count_wizards_in_progress(func):
|
def count_wizards_in_progress(func):
|
||||||
|
|
|
@ -8,7 +8,7 @@ from PyQt5.QtWidgets import QMenu, QHBoxLayout, QLabel, QVBoxLayout, QGridLayout
|
||||||
|
|
||||||
from electrum.util import bh2u, NotEnoughFunds, NoDynamicFeeEstimates
|
from electrum.util import bh2u, NotEnoughFunds, NoDynamicFeeEstimates
|
||||||
from electrum.i18n import _
|
from electrum.i18n import _
|
||||||
from electrum.lnchannel import Channel
|
from electrum.lnchannel import Channel, peer_states
|
||||||
from electrum.wallet import Abstract_Wallet
|
from electrum.wallet import Abstract_Wallet
|
||||||
from electrum.lnutil import LOCAL, REMOTE, format_short_channel_id, LN_MAX_FUNDING_SAT
|
from electrum.lnutil import LOCAL, REMOTE, format_short_channel_id, LN_MAX_FUNDING_SAT
|
||||||
|
|
||||||
|
@ -84,10 +84,14 @@ class ChannelsList(MyTreeView):
|
||||||
WaitingDialog(self, 'please wait..', task, self.on_success, self.on_failure)
|
WaitingDialog(self, 'please wait..', task, self.on_success, self.on_failure)
|
||||||
|
|
||||||
def force_close(self, channel_id):
|
def force_close(self, channel_id):
|
||||||
def task():
|
if self.lnworker.wallet.is_lightning_backup():
|
||||||
coro = self.lnworker.force_close_channel(channel_id)
|
msg = _('WARNING: force-closing from an old state might result in fund loss.\nAre you sure?')
|
||||||
return self.network.run_from_another_thread(coro)
|
else:
|
||||||
if self.parent.question('Force-close channel?\nReclaimed funds will not be immediately available.'):
|
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)
|
WaitingDialog(self, 'please wait..', task, self.on_success, self.on_failure)
|
||||||
|
|
||||||
def remove_channel(self, channel_id):
|
def remove_channel(self, channel_id):
|
||||||
|
@ -105,7 +109,8 @@ class ChannelsList(MyTreeView):
|
||||||
menu.addAction(_("Details..."), lambda: self.details(channel_id))
|
menu.addAction(_("Details..."), lambda: self.details(channel_id))
|
||||||
self.add_copy_menu(menu, idx)
|
self.add_copy_menu(menu, idx)
|
||||||
if not chan.is_closed():
|
if not chan.is_closed():
|
||||||
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))
|
menu.addAction(_("Force-close channel"), lambda: self.force_close(channel_id))
|
||||||
else:
|
else:
|
||||||
menu.addAction(_("Remove"), lambda: self.remove_channel(channel_id))
|
menu.addAction(_("Remove"), lambda: self.remove_channel(channel_id))
|
||||||
|
|
|
@ -513,6 +513,18 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
||||||
])
|
])
|
||||||
self.show_warning(msg, title=_('Watch-only wallet'))
|
self.show_warning(msg, title=_('Watch-only wallet'))
|
||||||
|
|
||||||
|
def warn_if_lightning_backup(self):
|
||||||
|
if self.wallet.is_lightning_backup():
|
||||||
|
msg = '\n\n'.join([
|
||||||
|
_("This file is a backup of a lightning wallet."),
|
||||||
|
_("You will not be able to perform lightning payments using this file, and the lightning balance displayed in this wallet might be outdated.") + ' ' + \
|
||||||
|
_("If you have lost the original wallet file, you can use this file to trigger a forced closure of your channels."),
|
||||||
|
_("Do you want to have your channels force-closed?")
|
||||||
|
])
|
||||||
|
if self.question(msg, title=_('Lightning Backup')):
|
||||||
|
self.network.maybe_init_lightning()
|
||||||
|
self.wallet.lnworker.start_network(self.network)
|
||||||
|
|
||||||
def warn_if_testnet(self):
|
def warn_if_testnet(self):
|
||||||
if not constants.net.TESTNET:
|
if not constants.net.TESTNET:
|
||||||
return
|
return
|
||||||
|
@ -549,20 +561,16 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
||||||
return
|
return
|
||||||
self.gui_object.new_window(filename)
|
self.gui_object.new_window(filename)
|
||||||
|
|
||||||
|
|
||||||
def backup_wallet(self):
|
def backup_wallet(self):
|
||||||
path = self.wallet.storage.path
|
try:
|
||||||
wallet_folder = os.path.dirname(path)
|
new_path = self.wallet.save_backup()
|
||||||
filename, __ = QFileDialog.getSaveFileName(self, _('Enter a filename for the copy of your wallet'), wallet_folder)
|
except BaseException as reason:
|
||||||
if not filename:
|
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
|
||||||
new_path = os.path.join(wallet_folder, filename)
|
if new_path:
|
||||||
if new_path != path:
|
self.show_message(_("A copy of your wallet file was created in")+" '%s'" % str(new_path), title=_("Wallet backup created"))
|
||||||
try:
|
else:
|
||||||
shutil.copy2(path, new_path)
|
self.show_message(_("You need to configure a backup directory in your preferences"), title=_("Backup not created"))
|
||||||
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"))
|
|
||||||
|
|
||||||
def update_recently_visited(self, filename):
|
def update_recently_visited(self, filename):
|
||||||
recent = self.config.get('recently_open', [])
|
recent = self.config.get('recently_open', [])
|
||||||
|
@ -604,7 +612,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
||||||
self.recently_visited_menu = file_menu.addMenu(_("&Recently open"))
|
self.recently_visited_menu = file_menu.addMenu(_("&Recently open"))
|
||||||
file_menu.addAction(_("&Open"), self.open_wallet).setShortcut(QKeySequence.Open)
|
file_menu.addAction(_("&Open"), self.open_wallet).setShortcut(QKeySequence.Open)
|
||||||
file_menu.addAction(_("&New/Restore"), self.new_wallet).setShortcut(QKeySequence.New)
|
file_menu.addAction(_("&New/Restore"), self.new_wallet).setShortcut(QKeySequence.New)
|
||||||
file_menu.addAction(_("&Save Copy"), self.backup_wallet).setShortcut(QKeySequence.SaveAs)
|
file_menu.addAction(_("&Save backup"), self.backup_wallet).setShortcut(QKeySequence.SaveAs)
|
||||||
file_menu.addAction(_("Delete"), self.remove_wallet)
|
file_menu.addAction(_("Delete"), self.remove_wallet)
|
||||||
file_menu.addSeparator()
|
file_menu.addSeparator()
|
||||||
file_menu.addAction(_("&Quit"), self.close)
|
file_menu.addAction(_("&Quit"), self.close)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -848,6 +848,10 @@ class Peer(Logger):
|
||||||
self.logger.warning(f"channel_reestablish: we are ahead of remote! trying to force-close.")
|
self.logger.warning(f"channel_reestablish: we are ahead of remote! trying to force-close.")
|
||||||
await self.lnworker.force_close_channel(chan_id)
|
await self.lnworker.force_close_channel(chan_id)
|
||||||
return
|
return
|
||||||
|
elif self.lnworker.wallet.is_lightning_backup():
|
||||||
|
self.logger.warning(f"channel_reestablish: force-closing because we are a recent backup")
|
||||||
|
await self.lnworker.force_close_channel(chan_id)
|
||||||
|
return
|
||||||
|
|
||||||
chan.peer_state = peer_states.GOOD
|
chan.peer_state = peer_states.GOOD
|
||||||
# note: chan.short_channel_id being set implies the funding txn is already at sufficient depth
|
# note: chan.short_channel_id being set implies the funding txn is already at sufficient depth
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -73,6 +73,8 @@ class MockWallet:
|
||||||
pass
|
pass
|
||||||
def save_db(self):
|
def save_db(self):
|
||||||
pass
|
pass
|
||||||
|
def is_lightning_backup(self):
|
||||||
|
return False
|
||||||
|
|
||||||
class MockLNWallet:
|
class MockLNWallet:
|
||||||
def __init__(self, remote_keypair, local_keypair, chan, tx_queue):
|
def __init__(self, remote_keypair, local_keypair, chan, tx_queue):
|
||||||
|
|
|
@ -425,11 +425,26 @@ def profiler(func):
|
||||||
return lambda *args, **kw_args: do_profile(args, kw_args)
|
return lambda *args, **kw_args: do_profile(args, kw_args)
|
||||||
|
|
||||||
|
|
||||||
|
def android_ext_dir():
|
||||||
|
from android.storage import primary_external_storage_path
|
||||||
|
return primary_external_storage_path()
|
||||||
|
|
||||||
|
def android_backup_dir():
|
||||||
|
d = os.path.join(android_ext_dir(), 'org.electrum.electrum')
|
||||||
|
if not os.path.exists(d):
|
||||||
|
os.mkdir(d)
|
||||||
|
return d
|
||||||
|
|
||||||
def android_data_dir():
|
def android_data_dir():
|
||||||
import jnius
|
import jnius
|
||||||
PythonActivity = jnius.autoclass('org.kivy.android.PythonActivity')
|
PythonActivity = jnius.autoclass('org.kivy.android.PythonActivity')
|
||||||
return PythonActivity.mActivity.getFilesDir().getPath() + '/data'
|
return PythonActivity.mActivity.getFilesDir().getPath() + '/data'
|
||||||
|
|
||||||
|
def get_backup_dir(config):
|
||||||
|
if 'ANDROID_DATA' in os.environ:
|
||||||
|
return android_backup_dir() if config.get('android_backups') else None
|
||||||
|
else:
|
||||||
|
return config.get('backup_dir')
|
||||||
|
|
||||||
def ensure_sparse_file(filename):
|
def ensure_sparse_file(filename):
|
||||||
# On modern Linux, no need to do anything.
|
# On modern Linux, no need to do anything.
|
||||||
|
|
|
@ -51,7 +51,7 @@ from .util import (NotEnoughFunds, UserCancelled, profiler,
|
||||||
WalletFileException, BitcoinException, MultipleSpendMaxTxOutputs,
|
WalletFileException, BitcoinException, MultipleSpendMaxTxOutputs,
|
||||||
InvalidPassword, format_time, timestamp_to_datetime, Satoshis,
|
InvalidPassword, format_time, timestamp_to_datetime, Satoshis,
|
||||||
Fiat, bfh, bh2u, TxMinedInfo, quantize_feerate, create_bip21_uri, OrderedDictWithIndex)
|
Fiat, bfh, bh2u, TxMinedInfo, quantize_feerate, create_bip21_uri, OrderedDictWithIndex)
|
||||||
from .util import PR_TYPE_ONCHAIN, PR_TYPE_LN
|
from .util import PR_TYPE_ONCHAIN, PR_TYPE_LN, get_backup_dir
|
||||||
from .simple_config import SimpleConfig
|
from .simple_config import SimpleConfig
|
||||||
from .bitcoin import (COIN, is_address, address_to_script,
|
from .bitcoin import (COIN, is_address, address_to_script,
|
||||||
is_minikey, relayfee, dust_threshold)
|
is_minikey, relayfee, dust_threshold)
|
||||||
|
@ -263,6 +263,20 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
||||||
if self.storage:
|
if self.storage:
|
||||||
self.db.write(self.storage)
|
self.db.write(self.storage)
|
||||||
|
|
||||||
|
def save_backup(self):
|
||||||
|
backup_dir = get_backup_dir(self.config)
|
||||||
|
if backup_dir is None:
|
||||||
|
return
|
||||||
|
new_db = WalletDB(self.db.dump(), manual_upgrades=False)
|
||||||
|
new_db.put('is_backup', True)
|
||||||
|
new_path = os.path.join(backup_dir, self.basename() + '.backup')
|
||||||
|
new_storage = WalletStorage(new_path)
|
||||||
|
new_storage._encryption_version = self.storage._encryption_version
|
||||||
|
new_storage.pubkey = self.storage.pubkey
|
||||||
|
new_db.set_modified(True)
|
||||||
|
new_db.write(new_storage)
|
||||||
|
return new_path
|
||||||
|
|
||||||
def has_lightning(self):
|
def has_lightning(self):
|
||||||
return bool(self.lnworker)
|
return bool(self.lnworker)
|
||||||
|
|
||||||
|
@ -285,6 +299,9 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
||||||
self.db.put('lightning_privkey2', None)
|
self.db.put('lightning_privkey2', None)
|
||||||
self.save_db()
|
self.save_db()
|
||||||
|
|
||||||
|
def is_lightning_backup(self):
|
||||||
|
return self.has_lightning() and self.db.get('is_backup')
|
||||||
|
|
||||||
def stop_threads(self):
|
def stop_threads(self):
|
||||||
super().stop_threads()
|
super().stop_threads()
|
||||||
if any([ks.is_requesting_to_be_rewritten_to_wallet_file for ks in self.get_keystores()]):
|
if any([ks.is_requesting_to_be_rewritten_to_wallet_file for ks in self.get_keystores()]):
|
||||||
|
@ -301,7 +318,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
||||||
|
|
||||||
def start_network(self, network):
|
def start_network(self, network):
|
||||||
AddressSynchronizer.start_network(self, network)
|
AddressSynchronizer.start_network(self, network)
|
||||||
if self.lnworker and network:
|
if self.lnworker and network and not self.is_lightning_backup():
|
||||||
network.maybe_init_lightning()
|
network.maybe_init_lightning()
|
||||||
self.lnworker.start_network(network)
|
self.lnworker.start_network(network)
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue