49 changed files with 4354 additions and 2922 deletions
@ -1,738 +0,0 @@
|
||||
# -*- coding: utf-8 -*- |
||||
# |
||||
# Electrum - lightweight Bitcoin client |
||||
# Copyright (C) 2016 Thomas Voegtlin |
||||
# |
||||
# Permission is hereby granted, free of charge, to any person |
||||
# obtaining a copy of this software and associated documentation files |
||||
# (the "Software"), to deal in the Software without restriction, |
||||
# including without limitation the rights to use, copy, modify, merge, |
||||
# publish, distribute, sublicense, and/or sell copies of the Software, |
||||
# and to permit persons to whom the Software is furnished to do so, |
||||
# subject to the following conditions: |
||||
# |
||||
# The above copyright notice and this permission notice shall be |
||||
# included in all copies or substantial portions of the Software. |
||||
# |
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
# SOFTWARE. |
||||
|
||||
import os |
||||
import sys |
||||
import copy |
||||
import traceback |
||||
from functools import partial |
||||
from typing import List, TYPE_CHECKING, Tuple, NamedTuple, Any, Dict, Optional, Union |
||||
|
||||
from . import bitcoin |
||||
from . import keystore |
||||
from . import mnemonic |
||||
from .bip32 import is_bip32_derivation, xpub_type, normalize_bip32_derivation, BIP32Node |
||||
from .keystore import bip44_derivation, purpose48_derivation, Hardware_KeyStore, KeyStore, bip39_to_seed |
||||
from .wallet import (Imported_Wallet, Standard_Wallet, Multisig_Wallet, |
||||
wallet_types, Wallet, Abstract_Wallet) |
||||
from .storage import WalletStorage, StorageEncryptionVersion |
||||
from .wallet_db import WalletDB |
||||
from .i18n import _ |
||||
from .util import UserCancelled, InvalidPassword, WalletFileException, UserFacingException |
||||
from .simple_config import SimpleConfig |
||||
from .plugin import Plugins, HardwarePluginLibraryUnavailable |
||||
from .logging import Logger |
||||
from .plugins.hw_wallet.plugin import OutdatedHwFirmwareException, HW_PluginBase |
||||
|
||||
if TYPE_CHECKING: |
||||
from .plugin import DeviceInfo, BasePlugin |
||||
|
||||
|
||||
# hardware device setup purpose |
||||
HWD_SETUP_NEW_WALLET, HWD_SETUP_DECRYPT_WALLET = range(0, 2) |
||||
|
||||
|
||||
class ScriptTypeNotSupported(Exception): pass |
||||
|
||||
|
||||
class GoBack(Exception): pass |
||||
|
||||
|
||||
class ReRunDialog(Exception): pass |
||||
|
||||
|
||||
class ChooseHwDeviceAgain(Exception): pass |
||||
|
||||
|
||||
class WizardStackItem(NamedTuple): |
||||
action: Any |
||||
args: Any |
||||
kwargs: Dict[str, Any] |
||||
db_data: dict |
||||
|
||||
|
||||
class WizardWalletPasswordSetting(NamedTuple): |
||||
password: Optional[str] |
||||
encrypt_storage: bool |
||||
storage_enc_version: StorageEncryptionVersion |
||||
encrypt_keystore: bool |
||||
|
||||
|
||||
class BaseWizard(Logger): |
||||
|
||||
def __init__(self, config: SimpleConfig, plugins: Plugins): |
||||
super(BaseWizard, self).__init__() |
||||
Logger.__init__(self) |
||||
self.config = config |
||||
self.plugins = plugins |
||||
self.data = {} |
||||
self.pw_args = None # type: Optional[WizardWalletPasswordSetting] |
||||
self._stack = [] # type: List[WizardStackItem] |
||||
self.plugin = None # type: Optional[BasePlugin] |
||||
self.keystores = [] # type: List[KeyStore] |
||||
self.seed_type = None |
||||
|
||||
def set_icon(self, icon): |
||||
pass |
||||
|
||||
def run(self, *args, **kwargs): |
||||
action = args[0] |
||||
args = args[1:] |
||||
db_data = copy.deepcopy(self.data) |
||||
self._stack.append(WizardStackItem(action, args, kwargs, db_data)) |
||||
if not action: |
||||
return |
||||
if type(action) is tuple: |
||||
self.plugin, action = action |
||||
if self.plugin and hasattr(self.plugin, action): |
||||
f = getattr(self.plugin, action) |
||||
f(self, *args, **kwargs) |
||||
elif hasattr(self, action): |
||||
f = getattr(self, action) |
||||
f(*args, **kwargs) |
||||
else: |
||||
raise Exception("unknown action", action) |
||||
|
||||
def can_go_back(self): |
||||
return len(self._stack) > 1 |
||||
|
||||
def go_back(self, *, rerun_previous: bool = True) -> None: |
||||
if not self.can_go_back(): |
||||
return |
||||
# pop 'current' frame |
||||
self._stack.pop() |
||||
prev_frame = self._stack[-1] |
||||
# try to undo side effects since we last entered 'previous' frame |
||||
# FIXME only self.data is properly restored |
||||
self.data = copy.deepcopy(prev_frame.db_data) |
||||
|
||||
if rerun_previous: |
||||
# pop 'previous' frame |
||||
self._stack.pop() |
||||
# rerun 'previous' frame |
||||
self.run(prev_frame.action, *prev_frame.args, **prev_frame.kwargs) |
||||
|
||||
def reset_stack(self): |
||||
self._stack = [] |
||||
|
||||
def new(self): |
||||
title = _("Create new wallet") |
||||
message = '\n'.join([ |
||||
_("What kind of wallet do you want to create?") |
||||
]) |
||||
wallet_kinds = [ |
||||
('standard', _("Standard wallet")), |
||||
('2fa', _("Wallet with two-factor authentication")), |
||||
('multisig', _("Multi-signature wallet")), |
||||
('imported', _("Import Bitcoin addresses or private keys")), |
||||
] |
||||
choices = [pair for pair in wallet_kinds if pair[0] in wallet_types] |
||||
self.choice_dialog(title=title, message=message, choices=choices, run_next=self.on_wallet_type) |
||||
|
||||
def upgrade_db(self, storage, db): |
||||
exc = None # type: Optional[Exception] |
||||
def on_finished(): |
||||
if exc is None: |
||||
self.terminate(storage=storage, db=db) |
||||
else: |
||||
raise exc |
||||
def do_upgrade(): |
||||
nonlocal exc |
||||
try: |
||||
db.upgrade() |
||||
except Exception as e: |
||||
exc = e |
||||
self.waiting_dialog(do_upgrade, _('Upgrading wallet format...'), on_finished=on_finished) |
||||
|
||||
def run_task_without_blocking_gui(self, task, *, msg: str = None) -> Any: |
||||
"""Perform a task in a thread without blocking the GUI. |
||||
Returns the result of 'task', or raises the same exception. |
||||
This method blocks until 'task' is finished. |
||||
""" |
||||
raise NotImplementedError() |
||||
|
||||
def load_2fa(self): |
||||
self.data['wallet_type'] = '2fa' |
||||
self.data['use_trustedcoin'] = True |
||||
self.plugin = self.plugins.load_plugin('trustedcoin') |
||||
|
||||
def on_wallet_type(self, choice): |
||||
self.data['wallet_type'] = self.wallet_type = choice |
||||
if choice == 'standard': |
||||
action = 'choose_keystore' |
||||
elif choice == 'multisig': |
||||
action = 'choose_multisig' |
||||
elif choice == '2fa': |
||||
self.load_2fa() |
||||
action = self.plugin.get_action(self.data) |
||||
elif choice == 'imported': |
||||
action = 'import_addresses_or_keys' |
||||
self.run(action) |
||||
|
||||
def choose_multisig(self): |
||||
def on_multisig(m, n): |
||||
multisig_type = "%dof%d" % (m, n) |
||||
self.data['wallet_type'] = multisig_type |
||||
self.n = n |
||||
self.run('choose_keystore') |
||||
self.multisig_dialog(run_next=on_multisig) |
||||
|
||||
def choose_keystore(self): |
||||
assert self.wallet_type in ['standard', 'multisig'] |
||||
i = len(self.keystores) |
||||
title = _('Add cosigner') + ' (%d of %d)'%(i+1, self.n) if self.wallet_type=='multisig' else _('Keystore') |
||||
if self.wallet_type =='standard' or i==0: |
||||
message = _('Do you want to create a new seed, or to restore a wallet using an existing seed?') |
||||
choices = [ |
||||
('choose_seed_type', _('Create a new seed')), |
||||
('restore_from_seed', _('I already have a seed')), |
||||
('restore_from_key', _('Use a master key')), |
||||
('choose_hw_device', _('Use a hardware device')), |
||||
] |
||||
else: |
||||
message = _('Add a cosigner to your multi-sig wallet') |
||||
choices = [ |
||||
('restore_from_key', _('Enter cosigner key')), |
||||
('restore_from_seed', _('Enter cosigner seed')), |
||||
('choose_hw_device', _('Cosign with hardware device')), |
||||
] |
||||
|
||||
self.choice_dialog(title=title, message=message, choices=choices, run_next=self.run) |
||||
|
||||
def import_addresses_or_keys(self): |
||||
v = lambda x: keystore.is_address_list(x) or keystore.is_private_key_list(x, raise_on_error=True) |
||||
title = _("Import Bitcoin Addresses") |
||||
message = _("Enter a list of Bitcoin addresses (this will create a watching-only wallet), or a list of private keys.") |
||||
self.add_xpub_dialog(title=title, message=message, run_next=self.on_import, |
||||
is_valid=v, allow_multi=True, show_wif_help=True) |
||||
|
||||
def on_import(self, text): |
||||
# text is already sanitized by is_address_list and is_private_keys_list |
||||
if keystore.is_address_list(text): |
||||
self.data['addresses'] = {} |
||||
for addr in text.split(): |
||||
assert bitcoin.is_address(addr) |
||||
self.data['addresses'][addr] = {} |
||||
elif keystore.is_private_key_list(text): |
||||
self.data['addresses'] = {} |
||||
k = keystore.Imported_KeyStore({}) |
||||
keys = keystore.get_private_keys(text) |
||||
for pk in keys: |
||||
assert bitcoin.is_private_key(pk) |
||||
txin_type, pubkey = k.import_privkey(pk, None) |
||||
addr = bitcoin.pubkey_to_address(txin_type, pubkey) |
||||
self.data['addresses'][addr] = {'type':txin_type, 'pubkey':pubkey} |
||||
self.keystores.append(k) |
||||
else: |
||||
return self.terminate(aborted=True) |
||||
return self.run('create_wallet') |
||||
|
||||
def restore_from_key(self): |
||||
if self.wallet_type == 'standard': |
||||
v = keystore.is_master_key |
||||
title = _("Create keystore from a master key") |
||||
message = ' '.join([ |
||||
_("To create a watching-only wallet, please enter your master public key (xpub/ypub/zpub)."), |
||||
_("To create a spending wallet, please enter a master private key (xprv/yprv/zprv).") |
||||
]) |
||||
self.add_xpub_dialog(title=title, message=message, run_next=self.on_restore_from_key, is_valid=v) |
||||
else: |
||||
i = len(self.keystores) + 1 |
||||
self.add_cosigner_dialog(index=i, run_next=self.on_restore_from_key, is_valid=keystore.is_bip32_key) |
||||
|
||||
def on_restore_from_key(self, text): |
||||
k = keystore.from_master_key(text) |
||||
self.on_keystore(k) |
||||
|
||||
def choose_hw_device(self, purpose=HWD_SETUP_NEW_WALLET, *, storage: WalletStorage = None): |
||||
while True: |
||||
try: |
||||
self._choose_hw_device(purpose=purpose, storage=storage) |
||||
except ChooseHwDeviceAgain: |
||||
pass |
||||
else: |
||||
break |
||||
|
||||
def _choose_hw_device(self, *, purpose, storage: WalletStorage = None): |
||||
title = _('Hardware Keystore') |
||||
# check available plugins |
||||
supported_plugins = self.plugins.get_hardware_support() |
||||
devices = [] # type: List[Tuple[str, DeviceInfo]] |
||||
devmgr = self.plugins.device_manager |
||||
debug_msg = '' |
||||
|
||||
def failed_getting_device_infos(name, e): |
||||
nonlocal debug_msg |
||||
err_str_oneline = ' // '.join(str(e).splitlines()) |
||||
self.logger.warning(f'error getting device infos for {name}: {err_str_oneline}') |
||||
indented_error_msg = ' '.join([''] + str(e).splitlines(keepends=True)) |
||||
debug_msg += f' {name}: (error getting device infos)\n{indented_error_msg}\n' |
||||
|
||||
# scan devices |
||||
try: |
||||
scanned_devices = self.run_task_without_blocking_gui(task=devmgr.scan_devices, |
||||
msg=_("Scanning devices...")) |
||||
except BaseException as e: |
||||
self.logger.info('error scanning devices: {}'.format(repr(e))) |
||||
debug_msg = ' {}:\n {}'.format(_('Error scanning devices'), e) |
||||
else: |
||||
for splugin in supported_plugins: |
||||
name, plugin = splugin.name, splugin.plugin |
||||
# plugin init errored? |
||||
if not plugin: |
||||
e = splugin.exception |
||||
indented_error_msg = ' '.join([''] + str(e).splitlines(keepends=True)) |
||||
debug_msg += f' {name}: (error during plugin init)\n' |
||||
debug_msg += ' {}\n'.format(_('You might have an incompatible library.')) |
||||
debug_msg += f'{indented_error_msg}\n' |
||||
continue |
||||
# see if plugin recognizes 'scanned_devices' |
||||
try: |
||||
# FIXME: side-effect: this sets client.handler |
||||
device_infos = devmgr.list_pairable_device_infos( |
||||
handler=None, plugin=plugin, devices=scanned_devices, include_failing_clients=True) |
||||
except HardwarePluginLibraryUnavailable as e: |
||||
failed_getting_device_infos(name, e) |
||||
continue |
||||
except BaseException as e: |
||||
self.logger.exception('') |
||||
failed_getting_device_infos(name, e) |
||||
continue |
||||
device_infos_failing = list(filter(lambda di: di.exception is not None, device_infos)) |
||||
for di in device_infos_failing: |
||||
failed_getting_device_infos(name, di.exception) |
||||
device_infos_working = list(filter(lambda di: di.exception is None, device_infos)) |
||||
devices += list(map(lambda x: (name, x), device_infos_working)) |
||||
if not debug_msg: |
||||
debug_msg = ' {}'.format(_('No exceptions encountered.')) |
||||
if not devices: |
||||
msg = (_('No hardware device detected.') + '\n' + |
||||
_('To trigger a rescan, press \'Next\'.') + '\n\n') |
||||
if sys.platform == 'win32': |
||||
msg += _('If your device is not detected on Windows, go to "Settings", "Devices", "Connected devices", ' |
||||
'and do "Remove device". Then, plug your device again.') + '\n' |
||||
msg += _('While this is less than ideal, it might help if you run Electrum as Administrator.') + '\n' |
||||
else: |
||||
msg += _('On Linux, you might have to add a new permission to your udev rules.') + '\n' |
||||
msg += '\n\n' |
||||
msg += _('Debug message') + '\n' + debug_msg |
||||
self.confirm_dialog(title=title, message=msg, |
||||
run_next=lambda x: None) |
||||
raise ChooseHwDeviceAgain() |
||||
# select device |
||||
self.devices = devices |
||||
choices = [] |
||||
for name, info in devices: |
||||
state = _("initialized") if info.initialized else _("wiped") |
||||
label = info.label or _("An unnamed {}").format(name) |
||||
try: transport_str = info.device.transport_ui_string[:20] |
||||
except Exception: transport_str = 'unknown transport' |
||||
descr = f"{label} [{info.model_name or name}, {state}, {transport_str}]" |
||||
choices.append(((name, info), descr)) |
||||
msg = _('Select a device') + ':' |
||||
self.choice_dialog(title=title, message=msg, choices=choices, |
||||
run_next=lambda *args: self.on_device(*args, purpose=purpose, storage=storage)) |
||||
|
||||
def on_device(self, name, device_info: 'DeviceInfo', *, purpose, storage: WalletStorage = None): |
||||
self.plugin = self.plugins.get_plugin(name) |
||||
assert isinstance(self.plugin, HW_PluginBase) |
||||
devmgr = self.plugins.device_manager |
||||
try: |
||||
client = self.plugin.setup_device(device_info, self, purpose) |
||||
except OSError as e: |
||||
self.show_error(_('We encountered an error while connecting to your device:') |
||||
+ '\n' + str(e) + '\n' |
||||
+ _('To try to fix this, we will now re-pair with your device.') + '\n' |
||||
+ _('Please try again.')) |
||||
devmgr.unpair_id(device_info.device.id_) |
||||
raise ChooseHwDeviceAgain() |
||||
except OutdatedHwFirmwareException as e: |
||||
if self.question(e.text_ignore_old_fw_and_continue(), title=_("Outdated device firmware")): |
||||
self.plugin.set_ignore_outdated_fw() |
||||
# will need to re-pair |
||||
devmgr.unpair_id(device_info.device.id_) |
||||
raise ChooseHwDeviceAgain() |
||||
except GoBack: |
||||
raise ChooseHwDeviceAgain() |
||||
except (UserCancelled, ReRunDialog): |
||||
raise |
||||
except UserFacingException as e: |
||||
self.show_error(str(e)) |
||||
raise ChooseHwDeviceAgain() |
||||
except BaseException as e: |
||||
self.logger.exception('') |
||||
self.show_error(str(e)) |
||||
raise ChooseHwDeviceAgain() |
||||
|
||||
if purpose == HWD_SETUP_NEW_WALLET: |
||||
def f(derivation, script_type): |
||||
derivation = normalize_bip32_derivation(derivation) |
||||
self.run('on_hw_derivation', name, device_info, derivation, script_type) |
||||
self.derivation_and_script_type_dialog(f) |
||||
elif purpose == HWD_SETUP_DECRYPT_WALLET: |
||||
password = client.get_password_for_storage_encryption() |
||||
try: |
||||
storage.decrypt(password) |
||||
except InvalidPassword: |
||||
# try to clear session so that user can type another passphrase |
||||
if hasattr(client, 'clear_session'): # FIXME not all hw wallet plugins have this |
||||
client.clear_session() |
||||
raise |
||||
else: |
||||
raise Exception('unknown purpose: %s' % purpose) |
||||
|
||||
def derivation_and_script_type_dialog(self, f, *, get_account_xpub=None): |
||||
message1 = _('Choose the type of addresses in your wallet.') |
||||
message2 = ' '.join([ |
||||
_('You can override the suggested derivation path.'), |
||||
_('If you are not sure what this is, leave this field unchanged.') |
||||
]) |
||||
hide_choices = False |
||||
if self.wallet_type == 'multisig': |
||||
# There is no general standard for HD multisig. |
||||
# For legacy, this is partially compatible with BIP45; assumes index=0 |
||||
# For segwit, a custom path is used, as there is no standard at all. |
||||
default_choice_idx = 2 |
||||
choices = [ |
||||
('standard', 'legacy multisig (p2sh)', normalize_bip32_derivation("m/45'/0")), |
||||
('p2wsh-p2sh', 'p2sh-segwit multisig (p2wsh-p2sh)', purpose48_derivation(0, xtype='p2wsh-p2sh')), |
||||
('p2wsh', 'native segwit multisig (p2wsh)', purpose48_derivation(0, xtype='p2wsh')), |
||||
] |
||||
# if this is not the first cosigner, pre-select the expected script type, |
||||
# and hide the choices |
||||
script_type = self.get_script_type_of_wallet() |
||||
if script_type is not None: |
||||
script_types = [*zip(*choices)][0] |
||||
chosen_idx = script_types.index(script_type) |
||||
default_choice_idx = chosen_idx |
||||
hide_choices = True |
||||
else: |
||||
default_choice_idx = 2 |
||||
choices = [ |
||||
('standard', 'legacy (p2pkh)', bip44_derivation(0, bip43_purpose=44)), |
||||
('p2wpkh-p2sh', 'p2sh-segwit (p2wpkh-p2sh)', bip44_derivation(0, bip43_purpose=49)), |
||||
('p2wpkh', 'native segwit (p2wpkh)', bip44_derivation(0, bip43_purpose=84)), |
||||
] |
||||
while True: |
||||
try: |
||||
self.derivation_and_script_type_gui_specific_dialog( |
||||
run_next=f, |
||||
title=_('Script type and Derivation path'), |
||||
message1=message1, |
||||
message2=message2, |
||||
choices=choices, |
||||
test_text=is_bip32_derivation, |
||||
default_choice_idx=default_choice_idx, |
||||
get_account_xpub=get_account_xpub, |
||||
hide_choices=hide_choices, |
||||
) |
||||
return |
||||
except ScriptTypeNotSupported as e: |
||||
self.show_error(e) |
||||
# let the user choose again |
||||
|
||||
def on_hw_derivation(self, name, device_info: 'DeviceInfo', derivation, xtype): |
||||
from .keystore import hardware_keystore |
||||
devmgr = self.plugins.device_manager |
||||
assert isinstance(self.plugin, HW_PluginBase) |
||||
try: |
||||
xpub = self.plugin.get_xpub(device_info.device.id_, derivation, xtype, self) |
||||
client = devmgr.client_by_id(device_info.device.id_, scan_now=False) |
||||
if not client: raise Exception("failed to find client for device id") |
||||
root_fingerprint = client.request_root_fingerprint_from_device() |
||||
label = client.label() # use this as device_info.label might be outdated! |
||||
soft_device_id = client.get_soft_device_id() # use this as device_info.device_id might be outdated! |
||||
except ScriptTypeNotSupported: |
||||
raise # this is handled in derivation_dialog |
||||
except BaseException as e: |
||||
self.logger.exception('') |
||||
self.show_error(e) |
||||
raise ChooseHwDeviceAgain() |
||||
d = { |
||||
'type': 'hardware', |
||||
'hw_type': name, |
||||
'derivation': derivation, |
||||
'root_fingerprint': root_fingerprint, |
||||
'xpub': xpub, |
||||
'label': label, |
||||
'soft_device_id': soft_device_id, |
||||
} |
||||
try: |
||||
client.manipulate_keystore_dict_during_wizard_setup(d) |
||||
except Exception as e: |
||||
self.logger.exception('') |
||||
self.show_error(e) |
||||
raise ChooseHwDeviceAgain() |
||||
k = hardware_keystore(d) |
||||
self.on_keystore(k) |
||||
|
||||
def passphrase_dialog(self, run_next, is_restoring=False): |
||||
title = _('Seed extension') |
||||
message = '\n'.join([ |
||||
_('You may extend your seed with custom words.'), |
||||
_('Your seed extension must be saved together with your seed.'), |
||||
]) |
||||
warning = '\n'.join([ |
||||
_('Note that this is NOT your encryption password.'), |
||||
_('If you do not know what this is, leave this field empty.'), |
||||
]) |
||||
warn_issue4566 = is_restoring and self.seed_type == 'bip39' |
||||
self.line_dialog(title=title, message=message, warning=warning, |
||||
default='', test=lambda x:True, run_next=run_next, |
||||
warn_issue4566=warn_issue4566) |
||||
|
||||
def restore_from_seed(self): |
||||
self.opt_bip39 = True |
||||
self.opt_slip39 = True |
||||
self.opt_ext = True |
||||
is_cosigning_seed = lambda x: mnemonic.seed_type(x) in ['standard', 'segwit'] |
||||
test = mnemonic.is_seed if self.wallet_type == 'standard' else is_cosigning_seed |
||||
f = lambda *args: self.run('on_restore_seed', *args) |
||||
self.restore_seed_dialog(run_next=f, test=test) |
||||
|
||||
def on_restore_seed(self, seed, seed_type, is_ext): |
||||
self.seed_type = seed_type if seed_type != 'electrum' else mnemonic.seed_type(seed) |
||||
if self.seed_type == 'bip39': |
||||
def f(passphrase): |
||||
root_seed = bip39_to_seed(seed, passphrase) |
||||
self.on_restore_bip43(root_seed) |
||||
self.passphrase_dialog(run_next=f, is_restoring=True) if is_ext else f('') |
||||
elif self.seed_type == 'slip39': |
||||
def f(passphrase): |
||||
root_seed = seed.decrypt(passphrase) |
||||
self.on_restore_bip43(root_seed) |
||||
self.passphrase_dialog(run_next=f, is_restoring=True) if is_ext else f('') |
||||
elif self.seed_type in ['standard', 'segwit']: |
||||
f = lambda passphrase: self.run('create_keystore', seed, passphrase) |
||||
self.passphrase_dialog(run_next=f, is_restoring=True) if is_ext else f('') |
||||
elif self.seed_type == 'old': |
||||
self.run('create_keystore', seed, '') |
||||
elif mnemonic.is_any_2fa_seed_type(self.seed_type): |
||||
self.load_2fa() |
||||
self.run('on_restore_seed', seed, is_ext) |
||||
else: |
||||
raise Exception('Unknown seed type', self.seed_type) |
||||
|
||||
def on_restore_bip43(self, root_seed): |
||||
def f(derivation, script_type): |
||||
derivation = normalize_bip32_derivation(derivation) |
||||
self.run('on_bip43', root_seed, derivation, script_type) |
||||
if self.wallet_type == 'standard': |
||||
def get_account_xpub(account_path): |
||||
root_node = BIP32Node.from_rootseed(root_seed, xtype="standard") |
||||
account_node = root_node.subkey_at_private_derivation(account_path) |
||||
account_xpub = account_node.to_xpub() |
||||
return account_xpub |
||||
else: |
||||
get_account_xpub = None |
||||
self.derivation_and_script_type_dialog(f, get_account_xpub=get_account_xpub) |
||||
|
||||
def create_keystore(self, seed, passphrase): |
||||
k = keystore.from_seed(seed, passphrase, self.wallet_type == 'multisig') |
||||
if k.can_have_deterministic_lightning_xprv(): |
||||
self.data['lightning_xprv'] = k.get_lightning_xprv(None) |
||||
self.on_keystore(k) |
||||
|
||||
def on_bip43(self, root_seed, derivation, script_type): |
||||
k = keystore.from_bip43_rootseed(root_seed, derivation, xtype=script_type) |
||||
self.on_keystore(k) |
||||
|
||||
def get_script_type_of_wallet(self) -> Optional[str]: |
||||
if len(self.keystores) > 0: |
||||
ks = self.keystores[0] |
||||
if isinstance(ks, keystore.Xpub): |
||||
return xpub_type(ks.xpub) |
||||
return None |
||||
|
||||
def on_keystore(self, k: KeyStore): |
||||
has_xpub = isinstance(k, keystore.Xpub) |
||||
if has_xpub: |
||||
t1 = xpub_type(k.xpub) |
||||
if self.wallet_type == 'standard': |
||||
if has_xpub and t1 not in ['standard', 'p2wpkh', 'p2wpkh-p2sh']: |
||||
self.show_error(_('Wrong key type') + ' %s'%t1) |
||||
self.run('choose_keystore') |
||||
return |
||||
self.keystores.append(k) |
||||
self.run('create_wallet') |
||||
elif self.wallet_type == 'multisig': |
||||
assert has_xpub |
||||
if t1 not in ['standard', 'p2wsh', 'p2wsh-p2sh']: |
||||
self.show_error(_('Wrong key type') + ' %s'%t1) |
||||
self.run('choose_keystore') |
||||
return |
||||
if k.xpub in map(lambda x: x.xpub, self.keystores): |
||||
self.show_error(_('Error: duplicate master public key')) |
||||
self.run('choose_keystore') |
||||
return |
||||
if len(self.keystores)>0: |
||||
t2 = xpub_type(self.keystores[0].xpub) |
||||
if t1 != t2: |
||||
self.show_error(_('Cannot add this cosigner:') + '\n' + "Their key type is '%s', we are '%s'"%(t1, t2)) |
||||
self.run('choose_keystore') |
||||
return |
||||
if len(self.keystores) == 0: |
||||
xpub = k.get_master_public_key() |
||||
self.reset_stack() |
||||
self.keystores.append(k) |
||||
self.run('show_xpub_and_add_cosigners', xpub) |
||||
return |
||||
self.reset_stack() |
||||
self.keystores.append(k) |
||||
if len(self.keystores) < self.n: |
||||
self.run('choose_keystore') |
||||
else: |
||||
self.run('create_wallet') |
||||
|
||||
def create_wallet(self): |
||||
encrypt_keystore = any(k.may_have_password() for k in self.keystores) |
||||
# note: the following condition ("if") is duplicated logic from |
||||
# wallet.get_available_storage_encryption_version() |
||||
if self.wallet_type == 'standard' and isinstance(self.keystores[0], Hardware_KeyStore): |
||||
# offer encrypting with a pw derived from the hw device |
||||
k = self.keystores[0] # type: Hardware_KeyStore |
||||
assert isinstance(self.plugin, HW_PluginBase) |
||||
try: |
||||
k.handler = self.plugin.create_handler(self) |
||||
password = k.get_password_for_storage_encryption() |
||||
except UserCancelled: |
||||
devmgr = self.plugins.device_manager |
||||
devmgr.unpair_pairing_code(k.pairing_code()) |
||||
raise ChooseHwDeviceAgain() |
||||
except BaseException as e: |
||||
self.logger.exception('') |
||||
self.show_error(str(e)) |
||||
raise ChooseHwDeviceAgain() |
||||
self.request_storage_encryption( |
||||
run_next=lambda encrypt_storage: self.on_password( |
||||
password, |
||||
encrypt_storage=encrypt_storage, |
||||
storage_enc_version=StorageEncryptionVersion.XPUB_PASSWORD, |
||||
encrypt_keystore=False)) |
||||
else: |
||||
# reset stack to disable 'back' button in password dialog |
||||
self.reset_stack() |
||||
# prompt the user to set an arbitrary password |
||||
self.request_password( |
||||
run_next=lambda password, encrypt_storage: self.on_password( |
||||
password, |
||||
encrypt_storage=encrypt_storage, |
||||
storage_enc_version=StorageEncryptionVersion.USER_PASSWORD, |
||||
encrypt_keystore=encrypt_keystore), |
||||
force_disable_encrypt_cb=not encrypt_keystore) |
||||
|
||||
def on_password(self, password, *, encrypt_storage: bool, |
||||
storage_enc_version=StorageEncryptionVersion.USER_PASSWORD, |
||||
encrypt_keystore: bool): |
||||
for k in self.keystores: |
||||
if k.may_have_password(): |
||||
k.update_password(None, password) |
||||
if self.wallet_type == 'standard': |
||||
self.data['seed_type'] = self.seed_type |
||||
keys = self.keystores[0].dump() |
||||
self.data['keystore'] = keys |
||||
elif self.wallet_type == 'multisig': |
||||
for i, k in enumerate(self.keystores): |
||||
self.data['x%d/'%(i+1)] = k.dump() |
||||
elif self.wallet_type == 'imported': |
||||
if len(self.keystores) > 0: |
||||
keys = self.keystores[0].dump() |
||||
self.data['keystore'] = keys |
||||
else: |
||||
raise Exception('Unknown wallet type') |
||||
self.pw_args = WizardWalletPasswordSetting(password=password, |
||||
encrypt_storage=encrypt_storage, |
||||
storage_enc_version=storage_enc_version, |
||||
encrypt_keystore=encrypt_keystore) |
||||
self.terminate() |
||||
|
||||
def create_storage(self, path) -> Tuple[WalletStorage, WalletDB]: |
||||
if os.path.exists(path): |
||||
raise Exception('file already exists at path') |
||||
assert self.pw_args, f"pw_args not set?!" |
||||
pw_args = self.pw_args |
||||
self.pw_args = None # clean-up so that it can get GC-ed |
||||
storage = WalletStorage(path) |
||||
if pw_args.encrypt_storage: |
||||
storage.set_password(pw_args.password, enc_version=pw_args.storage_enc_version) |
||||
db = WalletDB('', storage=storage, manual_upgrades=False) |
||||
db.set_keystore_encryption(bool(pw_args.password) and pw_args.encrypt_keystore) |
||||
for key, value in self.data.items(): |
||||
db.put(key, value) |
||||
db.load_plugins() |
||||
db.write() |
||||
return storage, db |
||||
|
||||
def terminate(self, *, storage: WalletStorage = None, |
||||
db: WalletDB = None, |
||||
aborted: bool = False) -> None: |
||||
raise NotImplementedError() # implemented by subclasses |
||||
|
||||
def show_xpub_and_add_cosigners(self, xpub): |
||||
self.show_xpub_dialog(xpub=xpub, run_next=lambda x: self.run('choose_keystore')) |
||||
|
||||
def choose_seed_type(self): |
||||
seed_type = 'standard' if self.config.WIZARD_DONT_CREATE_SEGWIT else 'segwit' |
||||
self.create_seed(seed_type) |
||||
|
||||
def create_seed(self, seed_type): |
||||
from . import mnemonic |
||||
self.seed_type = seed_type |
||||
seed = mnemonic.Mnemonic('en').make_seed(seed_type=self.seed_type) |
||||
self.opt_bip39 = False |
||||
self.opt_ext = True |
||||
self.opt_slip39 = False |
||||
f = lambda x: self.request_passphrase(seed, x) |
||||
self.show_seed_dialog(run_next=f, seed_text=seed) |
||||
|
||||
def request_passphrase(self, seed, opt_passphrase): |
||||
if opt_passphrase: |
||||
f = lambda x: self.confirm_seed(seed, x) |
||||
self.passphrase_dialog(run_next=f) |
||||
else: |
||||
self.run('confirm_seed', seed, '') |
||||
|
||||
def confirm_seed(self, seed, passphrase): |
||||
f = lambda x: self.confirm_passphrase(seed, passphrase) |
||||
self.confirm_seed_dialog( |
||||
run_next=f, |
||||
seed=seed if self.config.get('debug_seed') else '', |
||||
test=lambda x: mnemonic.is_matching_seed(seed=seed, seed_again=x), |
||||
) |
||||
|
||||
def confirm_passphrase(self, seed, passphrase): |
||||
f = lambda x: self.run('create_keystore', seed, x) |
||||
if passphrase: |
||||
title = _('Confirm Seed Extension') |
||||
message = '\n'.join([ |
||||
_('Your seed extension must be saved together with your seed.'), |
||||
_('Please type it here.'), |
||||
]) |
||||
self.line_dialog(run_next=f, title=title, message=message, default='', test=lambda x: x==passphrase) |
||||
else: |
||||
f('') |
||||
|
||||
def show_error(self, msg: Union[str, BaseException]) -> None: |
||||
raise NotImplementedError() |
||||
@ -1,797 +0,0 @@
|
||||
# Copyright (C) 2018 The Electrum developers |
||||
# Distributed under the MIT software license, see the accompanying |
||||
# file LICENCE or http://www.opensource.org/licenses/mit-license.php |
||||
|
||||
import os |
||||
import json |
||||
import sys |
||||
import threading |
||||
import traceback |
||||
from typing import Tuple, List, Callable, NamedTuple, Optional, TYPE_CHECKING |
||||
from functools import partial |
||||
|
||||
from PyQt5.QtCore import QRect, QEventLoop, Qt, pyqtSignal |
||||
from PyQt5.QtGui import QPalette, QPen, QPainter, QPixmap |
||||
from PyQt5.QtWidgets import (QWidget, QDialog, QLabel, QHBoxLayout, QMessageBox, |
||||
QVBoxLayout, QLineEdit, QFileDialog, QPushButton, |
||||
QGridLayout, QSlider, QScrollArea, QApplication) |
||||
|
||||
from electrum.wallet import Wallet, Abstract_Wallet |
||||
from electrum.storage import WalletStorage, StorageReadWriteError |
||||
from electrum.util import UserCancelled, InvalidPassword, WalletFileException, get_new_wallet_name |
||||
from electrum.base_wizard import BaseWizard, HWD_SETUP_DECRYPT_WALLET, GoBack, ReRunDialog |
||||
from electrum.network import Network |
||||
from electrum.i18n import _ |
||||
|
||||
from .seed_dialog import SeedLayout, KeysLayout |
||||
from .network_dialog import NetworkChoiceLayout |
||||
from .util import (MessageBoxMixin, Buttons, icon_path, ChoicesLayout, WWLabel, |
||||
InfoButton, char_width_in_lineedit, PasswordLineEdit, font_height) |
||||
from .password_dialog import PasswordLayout, PasswordLayoutForHW, PW_NEW |
||||
from .bip39_recovery_dialog import Bip39RecoveryDialog |
||||
from electrum.plugin import run_hook, Plugins |
||||
|
||||
if TYPE_CHECKING: |
||||
from electrum.simple_config import SimpleConfig |
||||
from electrum.wallet_db import WalletDB |
||||
from . import ElectrumGui |
||||
|
||||
|
||||
MSG_ENTER_PASSWORD = _("Choose a password to encrypt your wallet keys.") + '\n'\ |
||||
+ _("Leave this field empty if you want to disable encryption.") |
||||
MSG_HW_STORAGE_ENCRYPTION = _("Set wallet file encryption.") + '\n'\ |
||||
+ _("Your wallet file does not contain secrets, mostly just metadata. ") \ |
||||
+ _("It also contains your master public key that allows watching your addresses.") + '\n\n'\ |
||||
+ _("Note: If you enable this setting, you will need your hardware device to open your wallet.") |
||||
WIF_HELP_TEXT = (_('WIF keys are typed in Electrum, based on script type.') + '\n\n' + |
||||
_('A few examples') + ':\n' + |
||||
'p2pkh:KxZcY47uGp9a... \t-> 1DckmggQM...\n' + |
||||
'p2wpkh-p2sh:KxZcY47uGp9a... \t-> 3NhNeZQXF...\n' + |
||||
'p2wpkh:KxZcY47uGp9a... \t-> bc1q3fjfk...') |
||||
# note: full key is KxZcY47uGp9aVQAb6VVvuBs8SwHKgkSR2DbZUzjDzXf2N2GPhG9n |
||||
MSG_PASSPHRASE_WARN_ISSUE4566 = _("Warning") + ": "\ |
||||
+ _("You have multiple consecutive whitespaces or leading/trailing " |
||||
"whitespaces in your passphrase.") + " " \ |
||||
+ _("This is discouraged.") + " " \ |
||||
+ _("Due to a bug, old versions of Electrum will NOT be creating the " |
||||
"same wallet as newer versions or other software.") |
||||
|
||||
|
||||
class CosignWidget(QWidget): |
||||
|
||||
def __init__(self, m, n): |
||||
QWidget.__init__(self) |
||||
self.size = max(120, 9 * font_height()) |
||||
self.R = QRect(0, 0, self.size, self.size) |
||||
self.setGeometry(self.R) |
||||
self.setMinimumHeight(self.size) |
||||
self.setMaximumHeight(self.size) |
||||
self.m = m |
||||
self.n = n |
||||
|
||||
def set_n(self, n): |
||||
self.n = n |
||||
self.update() |
||||
|
||||
def set_m(self, m): |
||||
self.m = m |
||||
self.update() |
||||
|
||||
def paintEvent(self, event): |
||||
bgcolor = self.palette().color(QPalette.Background) |
||||
pen = QPen(bgcolor, 7, Qt.SolidLine) |
||||
qp = QPainter() |
||||
qp.begin(self) |
||||
qp.setPen(pen) |
||||
qp.setRenderHint(QPainter.Antialiasing) |
||||
qp.setBrush(Qt.gray) |
||||
for i in range(self.n): |
||||
alpha = int(16* 360 * i/self.n) |
||||
alpha2 = int(16* 360 * 1/self.n) |
||||
qp.setBrush(Qt.green if i<self.m else Qt.gray) |
||||
qp.drawPie(self.R, alpha, alpha2) |
||||
qp.end() |
||||
|
||||
|
||||
|
||||
def wizard_dialog(func): |
||||
def func_wrapper(*args, **kwargs): |
||||
run_next = kwargs['run_next'] |
||||
wizard = args[0] # type: InstallWizard |
||||
while True: |
||||
#wizard.logger.debug(f"dialog stack. len: {len(wizard._stack)}. stack: {wizard._stack}") |
||||
wizard.back_button.setText(_('Back') if wizard.can_go_back() else _('Cancel')) |
||||
# current dialog |
||||
try: |
||||
out = func(*args, **kwargs) |
||||
if type(out) is not tuple: |
||||
out = (out,) |
||||
except GoBack: |
||||
if not wizard.can_go_back(): |
||||
wizard.close() |
||||
raise UserCancelled |
||||
else: |
||||
# to go back from the current dialog, we just let the caller unroll the stack: |
||||
raise |
||||
# next dialog |
||||
try: |
||||
while True: |
||||
try: |
||||
run_next(*out) |
||||
except ReRunDialog: |
||||
# restore state, and then let the loop re-run next |
||||
wizard.go_back(rerun_previous=False) |
||||
else: |
||||
break |
||||
except GoBack as e: |
||||
# to go back from the next dialog, we ask the wizard to restore state |
||||
wizard.go_back(rerun_previous=False) |
||||
# and we re-run the current dialog |
||||
if wizard.can_go_back(): |
||||
# also rerun any calculations that might have populated the inputs to the current dialog, |
||||
# by going back to just after the *previous* dialog finished |
||||
raise ReRunDialog() from e |
||||
else: |
||||
continue |
||||
else: |
||||
break |
||||
return func_wrapper |
||||
|
||||
|
||||
class WalletAlreadyOpenInMemory(Exception): |
||||
def __init__(self, wallet: Abstract_Wallet): |
||||
super().__init__() |
||||
self.wallet = wallet |
||||
|
||||
|
||||
# WindowModalDialog must come first as it overrides show_error |
||||
class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): |
||||
|
||||
accept_signal = pyqtSignal() |
||||
|
||||
def __init__(self, config: 'SimpleConfig', app: QApplication, plugins: 'Plugins', *, gui_object: 'ElectrumGui'): |
||||
QDialog.__init__(self, None) |
||||
BaseWizard.__init__(self, config, plugins) |
||||
self.setWindowTitle('Electrum - ' + _('Install Wizard')) |
||||
self.app = app |
||||
self.config = config |
||||
self.gui_thread = gui_object.gui_thread |
||||
self.setMinimumSize(600, 400) |
||||
self.accept_signal.connect(self.accept) |
||||
self.title = QLabel() |
||||
self.main_widget = QWidget() |
||||
self.back_button = QPushButton(_("Back"), self) |
||||
self.back_button.setText(_('Back') if self.can_go_back() else _('Cancel')) |
||||
self.next_button = QPushButton(_("Next"), self) |
||||
self.next_button.setDefault(True) |
||||
self.logo = QLabel() |
||||
self.please_wait = QLabel(_("Please wait...")) |
||||
self.please_wait.setAlignment(Qt.AlignCenter) |
||||
self.icon_filename = None |
||||
self.loop = QEventLoop() |
||||
self.rejected.connect(lambda: self.loop.exit(0)) |
||||
self.back_button.clicked.connect(lambda: self.loop.exit(1)) |
||||
self.next_button.clicked.connect(lambda: self.loop.exit(2)) |
||||
outer_vbox = QVBoxLayout(self) |
||||
inner_vbox = QVBoxLayout() |
||||
inner_vbox.addWidget(self.title) |
||||
inner_vbox.addWidget(self.main_widget) |
||||
inner_vbox.addStretch(1) |
||||
inner_vbox.addWidget(self.please_wait) |
||||
inner_vbox.addStretch(1) |
||||
scroll_widget = QWidget() |
||||
scroll_widget.setLayout(inner_vbox) |
||||
scroll = QScrollArea() |
||||
scroll.setFocusPolicy(Qt.NoFocus) |
||||
scroll.setWidget(scroll_widget) |
||||
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) |
||||
scroll.setWidgetResizable(True) |
||||
icon_vbox = QVBoxLayout() |
||||
icon_vbox.addWidget(self.logo) |
||||
icon_vbox.addStretch(1) |
||||
hbox = QHBoxLayout() |
||||
hbox.addLayout(icon_vbox) |
||||
hbox.addSpacing(5) |
||||
hbox.addWidget(scroll) |
||||
hbox.setStretchFactor(scroll, 1) |
||||
outer_vbox.addLayout(hbox) |
||||
outer_vbox.addLayout(Buttons(self.back_button, self.next_button)) |
||||
self.set_icon('electrum.png') |
||||
self.show() |
||||
self.raise_() |
||||
self.refresh_gui() # Need for QT on MacOSX. Lame. |
||||
|
||||
def select_storage(self, path, get_wallet_from_daemon) -> Tuple[str, Optional[WalletStorage]]: |
||||
if os.path.isdir(path): |
||||
raise Exception("wallet path cannot point to a directory") |
||||
|
||||
vbox = QVBoxLayout() |
||||
hbox = QHBoxLayout() |
||||
hbox.addWidget(QLabel(_('Wallet') + ':')) |
||||
name_e = QLineEdit() |
||||
hbox.addWidget(name_e) |
||||
button = QPushButton(_('Choose...')) |
||||
hbox.addWidget(button) |
||||
vbox.addLayout(hbox) |
||||
|
||||
msg_label = WWLabel('') |
||||
vbox.addWidget(msg_label) |
||||
hbox2 = QHBoxLayout() |
||||
pw_e = PasswordLineEdit('', self) |
||||
pw_e.setFixedWidth(17 * char_width_in_lineedit()) |
||||
pw_label = QLabel(_('Password') + ':') |
||||
hbox2.addWidget(pw_label) |
||||
hbox2.addWidget(pw_e) |
||||
hbox2.addStretch() |
||||
vbox.addLayout(hbox2) |
||||
|
||||
vbox.addSpacing(50) |
||||
vbox_create_new = QVBoxLayout() |
||||
vbox_create_new.addWidget(QLabel(_('Alternatively') + ':'), alignment=Qt.AlignLeft) |
||||
button_create_new = QPushButton(_('Create New Wallet')) |
||||
button_create_new.setMinimumWidth(120) |
||||
vbox_create_new.addWidget(button_create_new, alignment=Qt.AlignLeft) |
||||
widget_create_new = QWidget() |
||||
widget_create_new.setLayout(vbox_create_new) |
||||
vbox_create_new.setContentsMargins(0, 0, 0, 0) |
||||
vbox.addWidget(widget_create_new) |
||||
|
||||
self.set_layout(vbox, title=_('Electrum wallet')) |
||||
|
||||
temp_storage = None # type: Optional[WalletStorage] |
||||
wallet_folder = os.path.dirname(path) |
||||
|
||||
def on_choose(): |
||||
path, __ = QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) |
||||
if path: |
||||
name_e.setText(path) |
||||
|
||||
def on_filename(filename): |
||||
# FIXME? "filename" might contain ".." (etc) and hence sketchy path traversals are possible |
||||
nonlocal temp_storage |
||||
temp_storage = None |
||||
msg = None |
||||
if filename: |
||||
path = os.path.join(wallet_folder, filename) |
||||
wallet_from_memory = get_wallet_from_daemon(path) |
||||
try: |
||||
if wallet_from_memory: |
||||
temp_storage = wallet_from_memory.storage # type: Optional[WalletStorage] |
||||
else: |
||||
temp_storage = WalletStorage(path) |
||||
except (StorageReadWriteError, WalletFileException) as e: |
||||
msg = _('Cannot read file') + f'\n{repr(e)}' |
||||
except Exception as e: |
||||
self.logger.exception('') |
||||
msg = _('Cannot read file') + f'\n{repr(e)}' |
||||
else: |
||||
msg = "" |
||||
self.next_button.setEnabled(temp_storage is not None) |
||||
user_needs_to_enter_password = False |
||||
if temp_storage: |
||||
if not temp_storage.file_exists(): |
||||
msg =_("This file does not exist.") + '\n' \ |
||||
+ _("Press 'Next' to create this wallet, or choose another file.") |
||||
elif not wallet_from_memory: |
||||
if temp_storage.is_encrypted_with_user_pw(): |
||||
msg = _("This file is encrypted with a password.") + '\n' \ |
||||
+ _('Enter your password or choose another file.') |
||||
user_needs_to_enter_password = True |
||||
elif temp_storage.is_encrypted_with_hw_device(): |
||||
msg = _("This file is encrypted using a hardware device.") + '\n' \ |
||||
+ _("Press 'Next' to choose device to decrypt.") |
||||
else: |
||||
msg = _("Press 'Next' to open this wallet.") |
||||
else: |
||||
msg = _("This file is already open in memory.") + "\n" \ |
||||
+ _("Press 'Next' to create/focus window.") |
||||
if msg is None: |
||||
msg = _('Cannot read file') |
||||
msg_label.setText(msg) |
||||
widget_create_new.setVisible(bool(temp_storage and temp_storage.file_exists())) |
||||
if user_needs_to_enter_password: |
||||
pw_label.show() |
||||
pw_e.show() |
||||
pw_e.setFocus() |
||||
else: |
||||
pw_label.hide() |
||||
pw_e.hide() |
||||
|
||||
button.clicked.connect(on_choose) |
||||
button_create_new.clicked.connect( |
||||
lambda: name_e.setText(get_new_wallet_name(wallet_folder))) # FIXME get_new_wallet_name might raise |
||||
name_e.textChanged.connect(on_filename) |
||||
name_e.setText(os.path.basename(path)) |
||||
|
||||
def run_user_interaction_loop(): |
||||
while True: |
||||
if self.loop.exec_() != 2: # 2 = next |
||||
raise UserCancelled() |
||||
assert temp_storage |
||||
if temp_storage.file_exists() and not temp_storage.is_encrypted(): |
||||
break |
||||
if not temp_storage.file_exists(): |
||||
break |
||||
wallet_from_memory = get_wallet_from_daemon(temp_storage.path) |
||||
if wallet_from_memory: |
||||
raise WalletAlreadyOpenInMemory(wallet_from_memory) |
||||
if temp_storage.file_exists() and temp_storage.is_encrypted(): |
||||
if temp_storage.is_encrypted_with_user_pw(): |
||||
password = pw_e.text() |
||||
try: |
||||
temp_storage.decrypt(password) |
||||
break |
||||
except InvalidPassword as e: |
||||
self.show_message(title=_('Error'), msg=str(e)) |
||||
continue |
||||
except BaseException as e: |
||||
self.logger.exception('') |
||||
self.show_message(title=_('Error'), msg=repr(e)) |
||||
raise UserCancelled() |
||||
elif temp_storage.is_encrypted_with_hw_device(): |
||||
try: |
||||
self.run('choose_hw_device', HWD_SETUP_DECRYPT_WALLET, storage=temp_storage) |
||||
except InvalidPassword as e: |
||||
self.show_message(title=_('Error'), |
||||
msg=_('Failed to decrypt using this hardware device.') + '\n' + |
||||
_('If you use a passphrase, make sure it is correct.')) |
||||
self.reset_stack() |
||||
return self.select_storage(path, get_wallet_from_daemon) |
||||
except (UserCancelled, GoBack): |
||||
raise |
||||
except BaseException as e: |
||||
self.logger.exception('') |
||||
self.show_message(title=_('Error'), msg=repr(e)) |
||||
raise UserCancelled() |
||||
if temp_storage.is_past_initial_decryption(): |
||||
break |
||||
else: |
||||
raise UserCancelled() |
||||
else: |
||||
raise Exception('Unexpected encryption version') |
||||
|
||||
try: |
||||
run_user_interaction_loop() |
||||
finally: |
||||
try: |
||||
pw_e.clear() |
||||
except RuntimeError: # wrapped C/C++ object has been deleted. |
||||
pass # happens when decrypting with hw device |
||||
|
||||
return temp_storage.path, (temp_storage if temp_storage.file_exists() else None) |
||||
|
||||
def run_upgrades(self, storage: WalletStorage, db: 'WalletDB') -> None: |
||||
path = storage.path |
||||
if db.requires_split(): |
||||
self.hide() |
||||
msg = _("The wallet '{}' contains multiple accounts, which are no longer supported since Electrum 2.7.\n\n" |
||||
"Do you want to split your wallet into multiple files?").format(path) |
||||
if not self.question(msg): |
||||
return |
||||
file_list = db.split_accounts(path) |
||||
msg = _('Your accounts have been moved to') + ':\n' + '\n'.join(file_list) + '\n\n'+ _('Do you want to delete the old file') + ':\n' + path |
||||
if self.question(msg): |
||||
os.remove(path) |
||||
self.show_warning(_('The file was removed')) |
||||
# raise now, to avoid having the old storage opened |
||||
raise UserCancelled() |
||||
|
||||
action = db.get_action() |
||||
if action and db.requires_upgrade(): |
||||
raise WalletFileException('Incomplete wallet files cannot be upgraded.') |
||||
if action: |
||||
self.hide() |
||||
msg = _("The file '{}' contains an incompletely created wallet.\n" |
||||
"Do you want to complete its creation now?").format(path) |
||||
if not self.question(msg): |
||||
if self.question(_("Do you want to delete '{}'?").format(path)): |
||||
os.remove(path) |
||||
self.show_warning(_('The file was removed')) |
||||
return |
||||
self.show() |
||||
self.data = json.loads(storage.read()) |
||||
self.run(action) |
||||
for k, v in self.data.items(): |
||||
db.put(k, v) |
||||
db.write() |
||||
return |
||||
|
||||
if db.requires_upgrade(): |
||||
self.upgrade_db(storage, db) |
||||
|
||||
def on_error(self, exc_info): |
||||
if not isinstance(exc_info[1], UserCancelled): |
||||
self.logger.error("on_error", exc_info=exc_info) |
||||
self.show_error(str(exc_info[1])) |
||||
|
||||
def set_icon(self, filename): |
||||
prior_filename, self.icon_filename = self.icon_filename, filename |
||||
self.logo.setPixmap(QPixmap(icon_path(filename)) |
||||
.scaledToWidth(60, mode=Qt.SmoothTransformation)) |
||||
return prior_filename |
||||
|
||||
def set_layout(self, layout, title=None, next_enabled=True): |
||||
self.title.setText("<b>%s</b>"%title if title else "") |
||||
self.title.setVisible(bool(title)) |
||||
# Get rid of any prior layout by assigning it to a temporary widget |
||||
prior_layout = self.main_widget.layout() |
||||
if prior_layout: |
||||
QWidget().setLayout(prior_layout) |
||||
self.main_widget.setLayout(layout) |
||||
self.back_button.setEnabled(True) |
||||
self.next_button.setEnabled(next_enabled) |
||||
if next_enabled: |
||||
self.next_button.setFocus() |
||||
self.main_widget.setVisible(True) |
||||
self.please_wait.setVisible(False) |
||||
|
||||
def exec_layout(self, layout, title=None, raise_on_cancel=True, |
||||
next_enabled=True, focused_widget=None): |
||||
self.set_layout(layout, title, next_enabled) |
||||
if focused_widget: |
||||
focused_widget.setFocus() |
||||
result = self.loop.exec_() |
||||
if not result and raise_on_cancel: |
||||
raise UserCancelled() |
||||
if result == 1: |
||||
raise GoBack from None |
||||
self.title.setVisible(False) |
||||
self.back_button.setEnabled(False) |
||||
self.next_button.setEnabled(False) |
||||
self.main_widget.setVisible(False) |
||||
self.please_wait.setVisible(True) |
||||
self.refresh_gui() |
||||
return result |
||||
|
||||
def refresh_gui(self): |
||||
# For some reason, to refresh the GUI this needs to be called twice |
||||
self.app.processEvents() |
||||
self.app.processEvents() |
||||
|
||||
def remove_from_recently_open(self, filename): |
||||
self.config.remove_from_recently_open(filename) |
||||
|
||||
def text_input(self, title, message, is_valid, allow_multi=False): |
||||
slayout = KeysLayout(parent=self, header_layout=message, is_valid=is_valid, |
||||
allow_multi=allow_multi, config=self.config) |
||||
self.exec_layout(slayout, title, next_enabled=False) |
||||
return slayout.get_text() |
||||
|
||||
def seed_input(self, title, message, is_seed, options): |
||||
slayout = SeedLayout( |
||||
title=message, |
||||
is_seed=is_seed, |
||||
options=options, |
||||
parent=self, |
||||
config=self.config, |
||||
) |
||||
self.exec_layout(slayout, title, next_enabled=False) |
||||
return slayout.get_seed(), slayout.seed_type, slayout.is_ext |
||||
|
||||
@wizard_dialog |
||||
def add_xpub_dialog(self, title, message, is_valid, run_next, allow_multi=False, show_wif_help=False): |
||||
header_layout = QHBoxLayout() |
||||
label = WWLabel(message) |
||||
label.setMinimumWidth(400) |
||||
header_layout.addWidget(label) |
||||
if show_wif_help: |
||||
header_layout.addWidget(InfoButton(WIF_HELP_TEXT), alignment=Qt.AlignRight) |
||||
return self.text_input(title, header_layout, is_valid, allow_multi) |
||||
|
||||
@wizard_dialog |
||||
def add_cosigner_dialog(self, run_next, index, is_valid): |
||||
title = _("Add Cosigner") + " %d"%index |
||||
message = ' '.join([ |
||||
_('Please enter the master public key (xpub) of your cosigner.'), |
||||
_('Enter their master private key (xprv) if you want to be able to sign for them.') |
||||
]) |
||||
return self.text_input(title, message, is_valid) |
||||
|
||||
@wizard_dialog |
||||
def restore_seed_dialog(self, run_next, test): |
||||
options = [] |
||||
if self.opt_ext: |
||||
options.append('ext') |
||||
if self.opt_bip39: |
||||
options.append('bip39') |
||||
if self.opt_slip39: |
||||
options.append('slip39') |
||||
title = _('Enter Seed') |
||||
message = _('Please enter your seed phrase in order to restore your wallet.') |
||||
return self.seed_input(title, message, test, options) |
||||
|
||||
@wizard_dialog |
||||
def confirm_seed_dialog(self, run_next, seed, test): |
||||
self.app.clipboard().clear() |
||||
title = _('Confirm Seed') |
||||
message = ' '.join([ |
||||
_('Your seed is important!'), |
||||
_('If you lose your seed, your money will be permanently lost.'), |
||||
_('To make sure that you have properly saved your seed, please retype it here.') |
||||
]) |
||||
seed, seed_type, is_ext = self.seed_input(title, message, test, None) |
||||
return seed |
||||
|
||||
@wizard_dialog |
||||
def show_seed_dialog(self, run_next, seed_text): |
||||
title = _("Your wallet generation seed is:") |
||||
slayout = SeedLayout( |
||||
seed=seed_text, |
||||
title=title, |
||||
msg=True, |
||||
options=['ext'], |
||||
config=self.config, |
||||
) |
||||
self.exec_layout(slayout) |
||||
return slayout.is_ext |
||||
|
||||
def pw_layout(self, msg, kind, force_disable_encrypt_cb): |
||||
pw_layout = PasswordLayout( |
||||
msg=msg, kind=kind, OK_button=self.next_button, |
||||
force_disable_encrypt_cb=force_disable_encrypt_cb) |
||||
pw_layout.encrypt_cb.setChecked(True) |
||||
try: |
||||
self.exec_layout(pw_layout.layout(), focused_widget=pw_layout.new_pw) |
||||
return pw_layout.new_password(), pw_layout.encrypt_cb.isChecked() |
||||
finally: |
||||
pw_layout.clear_password_fields() |
||||
|
||||
@wizard_dialog |
||||
def request_password(self, run_next, force_disable_encrypt_cb=False): |
||||
"""Request the user enter a new password and confirm it. Return |
||||
the password or None for no password.""" |
||||
return self.pw_layout(MSG_ENTER_PASSWORD, PW_NEW, force_disable_encrypt_cb) |
||||
|
||||
@wizard_dialog |
||||
def request_storage_encryption(self, run_next): |
||||
playout = PasswordLayoutForHW(MSG_HW_STORAGE_ENCRYPTION) |
||||
playout.encrypt_cb.setChecked(True) |
||||
self.exec_layout(playout.layout()) |
||||
return playout.encrypt_cb.isChecked() |
||||
|
||||
@wizard_dialog |
||||
def confirm_dialog(self, title, message, run_next): |
||||
self.confirm(message, title) |
||||
|
||||
def confirm(self, message, title): |
||||
label = WWLabel(message) |
||||
vbox = QVBoxLayout() |
||||
vbox.addWidget(label) |
||||
self.exec_layout(vbox, title) |
||||
|
||||
@wizard_dialog |
||||
def action_dialog(self, action, run_next): |
||||
self.run(action) |
||||
|
||||
def terminate(self, **kwargs): |
||||
self.accept_signal.emit() |
||||
|
||||
def waiting_dialog(self, task, msg, on_finished=None): |
||||
label = WWLabel(msg) |
||||
vbox = QVBoxLayout() |
||||
vbox.addSpacing(100) |
||||
label.setMinimumWidth(300) |
||||
label.setAlignment(Qt.AlignCenter) |
||||
vbox.addWidget(label) |
||||
self.set_layout(vbox, next_enabled=False) |
||||
self.back_button.setEnabled(False) |
||||
|
||||
t = threading.Thread(target=task) |
||||
t.start() |
||||
while True: |
||||
t.join(1.0/60) |
||||
if t.is_alive(): |
||||
self.refresh_gui() |
||||
else: |
||||
break |
||||
if on_finished: |
||||
on_finished() |
||||
|
||||
def run_task_without_blocking_gui(self, task, *, msg=None): |
||||
assert self.gui_thread == threading.current_thread(), 'must be called from GUI thread' |
||||
if msg is None: |
||||
msg = _("Please wait...") |
||||
|
||||
exc = None # type: Optional[Exception] |
||||
res = None |
||||
def task_wrapper(): |
||||
nonlocal exc |
||||
nonlocal res |
||||
try: |
||||
res = task() |
||||
except Exception as e: |
||||
exc = e |
||||
self.waiting_dialog(task_wrapper, msg=msg) |
||||
if exc is None: |
||||
return res |
||||
else: |
||||
raise exc |
||||
|
||||
@wizard_dialog |
||||
def choice_dialog(self, title, message, choices, run_next): |
||||
c_values = [x[0] for x in choices] |
||||
c_titles = [x[1] for x in choices] |
||||
clayout = ChoicesLayout(message, c_titles) |
||||
vbox = QVBoxLayout() |
||||
vbox.addLayout(clayout.layout()) |
||||
self.exec_layout(vbox, title) |
||||
action = c_values[clayout.selected_index()] |
||||
return action |
||||
|
||||
def query_choice(self, msg, choices): |
||||
"""called by hardware wallets""" |
||||
clayout = ChoicesLayout(msg, choices) |
||||
vbox = QVBoxLayout() |
||||
vbox.addLayout(clayout.layout()) |
||||
self.exec_layout(vbox, '') |
||||
return clayout.selected_index() |
||||
|
||||
@wizard_dialog |
||||
def derivation_and_script_type_gui_specific_dialog( |
||||
self, |
||||
*, |
||||
title: str, |
||||
message1: str, |
||||
choices: List[Tuple[str, str, str]], |
||||
hide_choices: bool = False, |
||||
message2: str, |
||||
test_text: Callable[[str], int], |
||||
run_next, |
||||
default_choice_idx: int = 0, |
||||
get_account_xpub=None, |
||||
) -> Tuple[str, str]: |
||||
vbox = QVBoxLayout() |
||||
|
||||
if get_account_xpub: |
||||
button = QPushButton(_("Detect Existing Accounts")) |
||||
def on_account_select(account): |
||||
script_type = account["script_type"] |
||||
if script_type == "p2pkh": |
||||
script_type = "standard" |
||||
button_index = c_values.index(script_type) |
||||
button = clayout.group.buttons()[button_index] |
||||
button.setChecked(True) |
||||
line.setText(account["derivation_path"]) |
||||
button.clicked.connect(lambda: Bip39RecoveryDialog(self, get_account_xpub, on_account_select)) |
||||
vbox.addWidget(button, alignment=Qt.AlignLeft) |
||||
vbox.addWidget(QLabel(_("Or"))) |
||||
|
||||
c_values = [x[0] for x in choices] |
||||
c_titles = [x[1] for x in choices] |
||||
c_default_text = [x[2] for x in choices] |
||||
def on_choice_click(clayout): |
||||
idx = clayout.selected_index() |
||||
line.setText(c_default_text[idx]) |
||||
clayout = ChoicesLayout(message1, c_titles, on_choice_click, |
||||
checked_index=default_choice_idx) |
||||
if not hide_choices: |
||||
vbox.addLayout(clayout.layout()) |
||||
|
||||
vbox.addWidget(WWLabel(message2)) |
||||
|
||||
line = QLineEdit() |
||||
def on_text_change(text): |
||||
self.next_button.setEnabled(test_text(text)) |
||||
line.textEdited.connect(on_text_change) |
||||
on_choice_click(clayout) # set default text for "line" |
||||
vbox.addWidget(line) |
||||
|
||||
self.exec_layout(vbox, title) |
||||
choice = c_values[clayout.selected_index()] |
||||
return str(line.text()), choice |
||||
|
||||
@wizard_dialog |
||||
def line_dialog(self, run_next, title, message, default, test, warning='', |
||||
presets=(), warn_issue4566=False): |
||||
vbox = QVBoxLayout() |
||||
vbox.addWidget(WWLabel(message)) |
||||
line = QLineEdit() |
||||
line.setText(default) |
||||
def f(text): |
||||
self.next_button.setEnabled(test(text)) |
||||
if warn_issue4566: |
||||
text_whitespace_normalised = ' '.join(text.split()) |
||||
warn_issue4566_label.setVisible(text != text_whitespace_normalised) |
||||
line.textEdited.connect(f) |
||||
vbox.addWidget(line) |
||||
vbox.addWidget(WWLabel(warning)) |
||||
|
||||
warn_issue4566_label = WWLabel(MSG_PASSPHRASE_WARN_ISSUE4566) |
||||
warn_issue4566_label.setVisible(False) |
||||
vbox.addWidget(warn_issue4566_label) |
||||
|
||||
for preset in presets: |
||||
button = QPushButton(preset[0]) |
||||
button.clicked.connect(lambda __, text=preset[1]: line.setText(text)) |
||||
button.setMinimumWidth(150) |
||||
hbox = QHBoxLayout() |
||||
hbox.addWidget(button, alignment=Qt.AlignCenter) |
||||
vbox.addLayout(hbox) |
||||
|
||||
self.exec_layout(vbox, title, next_enabled=test(default)) |
||||
return line.text() |
||||
|
||||
@wizard_dialog |
||||
def show_xpub_dialog(self, xpub, run_next): |
||||
msg = ' '.join([ |
||||
_("Here is your master public key."), |
||||
_("Please share it with your cosigners.") |
||||
]) |
||||
vbox = QVBoxLayout() |
||||
layout = SeedLayout( |
||||
xpub, |
||||
title=msg, |
||||
icon=False, |
||||
for_seed_words=False, |
||||
config=self.config, |
||||
) |
||||
vbox.addLayout(layout.layout()) |
||||
self.exec_layout(vbox, _('Master Public Key')) |
||||
return None |
||||
|
||||
def init_network(self, network: 'Network'): |
||||
message = _("Electrum communicates with remote servers to get " |
||||
"information about your transactions and addresses. The " |
||||
"servers all fulfill the same purpose only differing in " |
||||
"hardware. In most cases you simply want to let Electrum " |
||||
"pick one at random. However if you prefer feel free to " |
||||
"select a server manually.") |
||||
choices = [_("Auto connect"), _("Select server manually")] |
||||
title = _("How do you want to connect to a server? ") |
||||
clayout = ChoicesLayout(message, choices) |
||||
self.back_button.setText(_('Cancel')) |
||||
self.exec_layout(clayout.layout(), title) |
||||
r = clayout.selected_index() |
||||
if r == 1: |
||||
nlayout = NetworkChoiceLayout(network, self.config, wizard=True) |
||||
if self.exec_layout(nlayout.layout()): |
||||
nlayout.accept() |
||||
self.config.NETWORK_AUTO_CONNECT = network.auto_connect |
||||
else: |
||||
network.auto_connect = True |
||||
self.config.NETWORK_AUTO_CONNECT = True |
||||
|
||||
@wizard_dialog |
||||
def multisig_dialog(self, run_next): |
||||
cw = CosignWidget(2, 2) |
||||
n_edit = QSlider(Qt.Horizontal, self) |
||||
m_edit = QSlider(Qt.Horizontal, self) |
||||
n_edit.setMinimum(2) |
||||
n_edit.setMaximum(15) |
||||
m_edit.setMinimum(1) |
||||
m_edit.setMaximum(2) |
||||
n_edit.setValue(2) |
||||
m_edit.setValue(2) |
||||
n_label = QLabel() |
||||
m_label = QLabel() |
||||
grid = QGridLayout() |
||||
grid.addWidget(n_label, 0, 0) |
||||
grid.addWidget(n_edit, 0, 1) |
||||
grid.addWidget(m_label, 1, 0) |
||||
grid.addWidget(m_edit, 1, 1) |
||||
def on_m(m): |
||||
m_label.setText(_('Require {0} signatures').format(m)) |
||||
cw.set_m(m) |
||||
backup_warning_label.setVisible(cw.m != cw.n) |
||||
def on_n(n): |
||||
n_label.setText(_('From {0} cosigners').format(n)) |
||||
cw.set_n(n) |
||||
m_edit.setMaximum(n) |
||||
backup_warning_label.setVisible(cw.m != cw.n) |
||||
n_edit.valueChanged.connect(on_n) |
||||
m_edit.valueChanged.connect(on_m) |
||||
vbox = QVBoxLayout() |
||||
vbox.addWidget(cw) |
||||
vbox.addWidget(WWLabel(_("Choose the number of signatures needed to unlock funds in your wallet:"))) |
||||
vbox.addLayout(grid) |
||||
vbox.addSpacing(2 * char_width_in_lineedit()) |
||||
backup_warning_label = WWLabel(_("Warning: to be able to restore a multisig wallet, " |
||||
"you should include the master public key for each cosigner " |
||||
"in all of your backups.")) |
||||
vbox.addWidget(backup_warning_label) |
||||
on_n(2) |
||||
on_m(2) |
||||
self.exec_layout(vbox, _("Multi-Signature Wallet")) |
||||
m = int(m_edit.value()) |
||||
n = int(n_edit.value()) |
||||
return (m, n) |
||||
@ -0,0 +1,93 @@
|
||||
from typing import TYPE_CHECKING |
||||
|
||||
from electrum.i18n import _ |
||||
from electrum.wizard import ServerConnectWizard |
||||
from electrum.gui.qt.network_dialog import ProxyWidget, ServerWidget |
||||
from electrum.gui.qt.util import ChoiceWidget |
||||
from .wizard import QEAbstractWizard, WizardComponent |
||||
|
||||
if TYPE_CHECKING: |
||||
from electrum.simple_config import SimpleConfig |
||||
from electrum.plugin import Plugins |
||||
from electrum.daemon import Daemon |
||||
from electrum.gui.qt import QElectrumApplication |
||||
|
||||
|
||||
class QEServerConnectWizard(ServerConnectWizard, QEAbstractWizard): |
||||
|
||||
def __init__(self, config: 'SimpleConfig', app: 'QElectrumApplication', plugins: 'Plugins', daemon: 'Daemon', parent=None): |
||||
ServerConnectWizard.__init__(self, daemon) |
||||
QEAbstractWizard.__init__(self, config, app) |
||||
|
||||
self.setWindowTitle(_('Network and server configuration')) |
||||
|
||||
# attach gui classes |
||||
self.navmap_merge({ |
||||
'autoconnect': { 'gui': WCAutoConnect }, |
||||
'proxy_ask': { 'gui': WCProxyAsk }, |
||||
'proxy_config': { 'gui': WCProxyConfig }, |
||||
'server_config': { 'gui': WCServerConfig }, |
||||
}) |
||||
|
||||
|
||||
class WCAutoConnect(WizardComponent): |
||||
def __init__(self, parent, wizard): |
||||
WizardComponent.__init__(self, parent, wizard, title=_("How do you want to connect to a server? ")) |
||||
message = _("Electrum communicates with remote servers to get " |
||||
"information about your transactions and addresses. The " |
||||
"servers all fulfill the same purpose only differing in " |
||||
"hardware. In most cases you simply want to let Electrum " |
||||
"pick one at random. However if you prefer feel free to " |
||||
"select a server manually.") |
||||
choices = [('autoconnect', _("Auto connect")), |
||||
('select', _("Select server manually"))] |
||||
self.choice_w = ChoiceWidget(message=message, choices=choices) |
||||
self.choice_w.itemSelected.connect(self.on_updated) |
||||
self.layout().addWidget(self.choice_w) |
||||
self.layout().addStretch(1) |
||||
self._valid = True |
||||
|
||||
def apply(self): |
||||
self.wizard_data['autoconnect'] = (self.choice_w.selected_item[0] == 'autoconnect') |
||||
|
||||
|
||||
class WCProxyAsk(WizardComponent): |
||||
def __init__(self, parent, wizard): |
||||
WizardComponent.__init__(self, parent, wizard, title=_("Proxy")) |
||||
message = _("Do you use a local proxy service such as TOR to reach the internet?") |
||||
choices = [('yes', _("Yes")), |
||||
('no', _("No"))] |
||||
self.choice_w = ChoiceWidget(message=message, choices=choices) |
||||
self.layout().addWidget(self.choice_w) |
||||
self.layout().addStretch(1) |
||||
self._valid = True |
||||
|
||||
def apply(self): |
||||
self.wizard_data['want_proxy'] = (self.choice_w.selected_item[0] == 'yes') |
||||
|
||||
|
||||
class WCProxyConfig(WizardComponent): |
||||
def __init__(self, parent, wizard): |
||||
WizardComponent.__init__(self, parent, wizard, title=_("Proxy")) |
||||
self.pw = ProxyWidget(self) |
||||
self.pw.proxy_cb.setChecked(True) |
||||
self.pw.proxy_host.setText('localhost') |
||||
self.pw.proxy_port.setText('9050') |
||||
self.layout().addWidget(self.pw) |
||||
self.layout().addStretch(1) |
||||
self._valid = True |
||||
|
||||
def apply(self): |
||||
self.wizard_data['proxy'] = self.pw.get_proxy_settings() |
||||
|
||||
|
||||
class WCServerConfig(WizardComponent): |
||||
def __init__(self, parent, wizard): |
||||
WizardComponent.__init__(self, parent, wizard, title=_("Server")) |
||||
self.sw = ServerWidget(wizard._daemon.network, self) |
||||
self.layout().addWidget(self.sw) |
||||
self._valid = True |
||||
|
||||
def apply(self): |
||||
self.wizard_data['autoconnect'] = self.sw.autoconnect_cb.isChecked() |
||||
self.wizard_data['server'] = self.sw.server_e.text() |
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,287 @@
|
||||
import copy |
||||
import threading |
||||
from abc import abstractmethod |
||||
from typing import TYPE_CHECKING |
||||
|
||||
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, pyqtSlot, QSize |
||||
from PyQt5.QtGui import QPixmap |
||||
from PyQt5.QtWidgets import (QDialog, QPushButton, QWidget, QLabel, QVBoxLayout, QScrollArea, |
||||
QHBoxLayout, QLayout, QStackedWidget) |
||||
|
||||
from electrum.i18n import _ |
||||
from electrum.logging import get_logger |
||||
from electrum.gui.qt.util import Buttons, icon_path, MessageBoxMixin, WWLabel |
||||
|
||||
if TYPE_CHECKING: |
||||
from electrum.simple_config import SimpleConfig |
||||
from electrum.gui.qt import QElectrumApplication |
||||
from electrum.wizard import WizardViewState |
||||
|
||||
|
||||
class QEAbstractWizard(QDialog, MessageBoxMixin): |
||||
_logger = get_logger(__name__) |
||||
|
||||
requestNext = pyqtSignal() |
||||
requestPrev = pyqtSignal() |
||||
|
||||
def __init__(self, config: 'SimpleConfig', app: 'QElectrumApplication', *, start_viewstate: 'WizardViewState' = None): |
||||
QDialog.__init__(self, None) |
||||
self.app = app |
||||
self.config = config |
||||
|
||||
# compat |
||||
self.gui_thread = threading.current_thread() |
||||
|
||||
self.setMinimumSize(600, 400) |
||||
|
||||
self.title = QLabel() |
||||
|
||||
self.main_widget = QStackedWidget(self) |
||||
|
||||
self.back_button = QPushButton(_("Back"), self) |
||||
self.back_button.clicked.connect(self.on_back_button_clicked) |
||||
self.next_button = QPushButton(_("Next"), self) |
||||
self.next_button.clicked.connect(self.on_next_button_clicked) |
||||
self.next_button.setDefault(True) |
||||
self.requestPrev.connect(self.on_back_button_clicked) |
||||
self.requestNext.connect(self.on_next_button_clicked) |
||||
self.logo = QLabel() |
||||
|
||||
please_wait_layout = QVBoxLayout() |
||||
please_wait_layout.addStretch(1) |
||||
self.please_wait_l = QLabel(_("Please wait...")) |
||||
self.please_wait_l.setAlignment(Qt.AlignCenter) |
||||
please_wait_layout.addWidget(self.please_wait_l) |
||||
please_wait_layout.addStretch(1) |
||||
self.please_wait = QWidget() |
||||
self.please_wait.setVisible(False) |
||||
self.please_wait.setLayout(please_wait_layout) |
||||
|
||||
error_layout = QVBoxLayout() |
||||
error_layout.addStretch(1) |
||||
# error_l = QLabel(_("Error!")) |
||||
# error_l.setAlignment(Qt.AlignCenter) |
||||
# error_layout.addWidget(error_l) |
||||
self.error_msg = WWLabel() |
||||
self.error_msg.setAlignment(Qt.AlignCenter) |
||||
error_layout.addWidget(self.error_msg) |
||||
error_layout.addStretch(1) |
||||
self.error = QWidget() |
||||
self.error.setVisible(False) |
||||
self.error.setLayout(error_layout) |
||||
|
||||
outer_vbox = QVBoxLayout(self) |
||||
inner_vbox = QVBoxLayout() |
||||
inner_vbox.addWidget(self.title) |
||||
inner_vbox.addWidget(self.main_widget) |
||||
inner_vbox.addWidget(self.please_wait) |
||||
inner_vbox.addWidget(self.error) |
||||
|
||||
scroll_widget = QWidget() |
||||
scroll_widget.setLayout(inner_vbox) |
||||
scroll = QScrollArea() |
||||
scroll.setFocusPolicy(Qt.NoFocus) |
||||
scroll.setWidget(scroll_widget) |
||||
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) |
||||
scroll.setWidgetResizable(True) |
||||
icon_vbox = QVBoxLayout() |
||||
icon_vbox.addWidget(self.logo) |
||||
icon_vbox.addStretch(1) |
||||
hbox = QHBoxLayout() |
||||
hbox.addLayout(icon_vbox) |
||||
hbox.addSpacing(5) |
||||
hbox.addWidget(scroll) |
||||
hbox.setStretchFactor(scroll, 1) |
||||
outer_vbox.addLayout(hbox) |
||||
outer_vbox.addLayout(Buttons(self.back_button, self.next_button)) |
||||
|
||||
self.icon_filename = None |
||||
self.set_icon('electrum.png') |
||||
|
||||
self.start_viewstate = start_viewstate |
||||
|
||||
self.show() |
||||
self.raise_() |
||||
|
||||
QTimer.singleShot(40, self.strt) |
||||
|
||||
# TODO: re-test if needed on macOS |
||||
# self.refresh_gui() # Need for QT on MacOSX. Lame. |
||||
|
||||
# def refresh_gui(self): |
||||
# # For some reason, to refresh the GUI this needs to be called twice |
||||
# self.app.processEvents() |
||||
# self.app.processEvents() |
||||
|
||||
def sizeHint(self) -> QSize: |
||||
return QSize(800, 600) |
||||
|
||||
def strt(self): |
||||
if self.start_viewstate is not None: |
||||
viewstate = self._current = self.start_viewstate |
||||
else: |
||||
viewstate = self.start_wizard() |
||||
self.load_next_component(viewstate.view, viewstate.wizard_data) |
||||
|
||||
def load_next_component(self, view, wdata=None, params=None): |
||||
if wdata is None: |
||||
wdata = {} |
||||
if params is None: |
||||
params = {} |
||||
|
||||
comp = self.view_to_component(view) |
||||
try: |
||||
page = comp(self.main_widget, self) |
||||
except Exception as e: |
||||
self._logger.error(f'not a class: {comp!r}') |
||||
raise e |
||||
page.wizard_data = copy.deepcopy(wdata) |
||||
page.params = params |
||||
page.updated.connect(self.on_page_updated) |
||||
self._logger.debug(f'{page!r}') |
||||
|
||||
# add to stack and update wizard |
||||
self.main_widget.setCurrentIndex(self.main_widget.addWidget(page)) |
||||
page.on_ready() |
||||
page.apply() |
||||
self.update() |
||||
|
||||
@pyqtSlot(object) |
||||
def on_page_updated(self, page): |
||||
page.apply() |
||||
if page == self.main_widget.currentWidget(): |
||||
self.update() |
||||
|
||||
def set_icon(self, filename): |
||||
prior_filename, self.icon_filename = self.icon_filename, filename |
||||
self.logo.setPixmap(QPixmap(icon_path(filename)) |
||||
.scaledToWidth(60, mode=Qt.SmoothTransformation)) |
||||
return prior_filename |
||||
|
||||
def can_go_back(self) -> bool: |
||||
return len(self._stack) > 0 |
||||
|
||||
def update(self): |
||||
page = self.main_widget.currentWidget() |
||||
self.title.setText(f'<b>{page.title}</b>' if page.title else '') |
||||
self.back_button.setText(_('Back') if self.can_go_back() else _('Cancel')) |
||||
self.back_button.setEnabled(not page.busy) |
||||
self.next_button.setText(_('Next') if not self.is_last(page.wizard_data) else _('Finish')) |
||||
self.next_button.setEnabled(not page.busy and page.valid) |
||||
self.main_widget.setVisible(not page.busy and not bool(page.error)) |
||||
self.please_wait.setVisible(page.busy) |
||||
self.please_wait_l.setText(page.busy_msg if page.busy_msg else _("Please wait...")) |
||||
self.error_msg.setText(str(page.error)) |
||||
self.error.setVisible(not page.busy and bool(page.error)) |
||||
icon = page.params.get('icon', icon_path('electrum.png')) |
||||
if icon != self.icon_filename: |
||||
self.set_icon(icon) |
||||
|
||||
def on_back_button_clicked(self): |
||||
if self.can_go_back(): |
||||
self.prev() |
||||
widget = self.main_widget.currentWidget() |
||||
self.main_widget.removeWidget(widget) |
||||
widget.deleteLater() |
||||
self.update() |
||||
else: |
||||
self.close() |
||||
|
||||
def on_next_button_clicked(self): |
||||
page = self.main_widget.currentWidget() |
||||
page.apply() |
||||
wd = page.wizard_data.copy() |
||||
if self.is_last(wd): |
||||
self.submit(wd) |
||||
if self.is_finalized(wd): |
||||
self.accept() |
||||
else: |
||||
self.prev() # rollback the submit above |
||||
else: |
||||
next = self.submit(wd) |
||||
self.load_next_component(next.view, next.wizard_data, next.params) |
||||
|
||||
def start_wizard(self) -> 'WizardViewState': |
||||
self.start() |
||||
return self._current |
||||
|
||||
def view_to_component(self, view) -> QWidget: |
||||
return self.navmap[view]['gui'] |
||||
|
||||
def submit(self, wizard_data) -> dict: |
||||
wdata = wizard_data.copy() |
||||
view = self.resolve_next(self._current.view, wdata) |
||||
return view |
||||
|
||||
def prev(self) -> dict: |
||||
viewstate = self.resolve_prev() |
||||
return viewstate.wizard_data |
||||
|
||||
def is_last(self, wizard_data: dict) -> bool: |
||||
wdata = wizard_data.copy() |
||||
return self.is_last_view(self._current.view, wdata) |
||||
|
||||
def is_finalized(self, wizard_data: dict) -> bool: |
||||
''' Final check before closing the wizard. ''' |
||||
return True |
||||
|
||||
|
||||
class WizardComponent(QWidget): |
||||
updated = pyqtSignal(object) |
||||
|
||||
def __init__(self, parent: QWidget, wizard: QEAbstractWizard, *, title: str = None, layout: QLayout = None): |
||||
super().__init__(parent) |
||||
self.setLayout(layout if layout else QVBoxLayout(self)) |
||||
self.wizard_data = {} |
||||
self.title = title if title is not None else 'No title' |
||||
self.busy_msg = '' |
||||
self.wizard = wizard |
||||
self._error = '' |
||||
self._valid = False |
||||
self._busy = False |
||||
|
||||
@property |
||||
def valid(self): |
||||
return self._valid |
||||
|
||||
@valid.setter |
||||
def valid(self, is_valid): |
||||
if self._valid != is_valid: |
||||
self._valid = is_valid |
||||
self.on_updated() |
||||
|
||||
@property |
||||
def busy(self): |
||||
return self._busy |
||||
|
||||
@busy.setter |
||||
def busy(self, is_busy): |
||||
if self._busy != is_busy: |
||||
self._busy = is_busy |
||||
self.on_updated() |
||||
|
||||
@property |
||||
def error(self): |
||||
return self._error |
||||
|
||||
@error.setter |
||||
def error(self, error): |
||||
if self._error != error: |
||||
self._error = error |
||||
self.on_updated() |
||||
|
||||
@abstractmethod |
||||
def apply(self): |
||||
# called to apply UI component values to wizard_data |
||||
pass |
||||
|
||||
def on_ready(self): |
||||
# called when wizard_data is available |
||||
pass |
||||
|
||||
@pyqtSlot() |
||||
def on_updated(self, *args): |
||||
try: |
||||
self.updated.emit(self) |
||||
except RuntimeError: |
||||
pass |
||||
@ -0,0 +1,257 @@
|
||||
import threading |
||||
import socket |
||||
import base64 |
||||
from typing import TYPE_CHECKING |
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot |
||||
|
||||
from electrum.i18n import _ |
||||
from electrum.bip32 import BIP32Node |
||||
|
||||
from .trustedcoin import (server, ErrorConnectingServer, MOBILE_DISCLAIMER, TrustedCoinException) |
||||
from electrum.gui.common_qt.plugins import PluginQObject |
||||
|
||||
if TYPE_CHECKING: |
||||
from electrum.wizard import NewWalletWizard |
||||
|
||||
|
||||
class TrustedcoinPluginQObject(PluginQObject): |
||||
canSignWithoutServerChanged = pyqtSignal() |
||||
termsAndConditionsRetrieved = pyqtSignal([str], arguments=['message']) |
||||
termsAndConditionsError = pyqtSignal([str], arguments=['message']) |
||||
otpError = pyqtSignal([str], arguments=['message']) |
||||
otpSuccess = pyqtSignal() |
||||
disclaimerChanged = pyqtSignal() |
||||
keystoreChanged = pyqtSignal() |
||||
otpSecretChanged = pyqtSignal() |
||||
shortIdChanged = pyqtSignal() |
||||
billingModelChanged = pyqtSignal() |
||||
|
||||
remoteKeyStateChanged = pyqtSignal() |
||||
remoteKeyError = pyqtSignal([str], arguments=['message']) |
||||
|
||||
requestOtp = pyqtSignal() |
||||
|
||||
def __init__(self, plugin, wizard: 'NewWalletWizard', parent): |
||||
super().__init__(plugin, parent) |
||||
self.wizard = wizard |
||||
self._canSignWithoutServer = False |
||||
self._otpSecret = '' |
||||
self._shortId = '' |
||||
self._billingModel = [] |
||||
self._remoteKeyState = '' |
||||
self._verifyingOtp = False |
||||
|
||||
@pyqtProperty(str, notify=disclaimerChanged) |
||||
def disclaimer(self): |
||||
return '\n\n'.join(MOBILE_DISCLAIMER) |
||||
|
||||
@pyqtProperty(bool, notify=canSignWithoutServerChanged) |
||||
def canSignWithoutServer(self): |
||||
return self._canSignWithoutServer |
||||
|
||||
@pyqtProperty('QVariantMap', notify=keystoreChanged) |
||||
def keystore(self): |
||||
return self._keystore |
||||
|
||||
@pyqtProperty(str, notify=otpSecretChanged) |
||||
def otpSecret(self): |
||||
return self._otpSecret |
||||
|
||||
@pyqtProperty(str, notify=shortIdChanged) |
||||
def shortId(self): |
||||
return self._shortId |
||||
|
||||
@pyqtSlot(str) |
||||
def otpSubmit(self, otp): |
||||
self._plugin.on_otp(otp) |
||||
|
||||
@pyqtProperty(str, notify=remoteKeyStateChanged) |
||||
def remoteKeyState(self): |
||||
return self._remoteKeyState |
||||
|
||||
@remoteKeyState.setter |
||||
def remoteKeyState(self, new_state): |
||||
if self._remoteKeyState != new_state: |
||||
self._remoteKeyState = new_state |
||||
self.remoteKeyStateChanged.emit() |
||||
|
||||
@pyqtProperty('QVariantList', notify=billingModelChanged) |
||||
def billingModel(self): |
||||
return self._billingModel |
||||
|
||||
def updateBillingInfo(self, wallet): |
||||
billingModel = [] |
||||
|
||||
price_per_tx = wallet.price_per_tx |
||||
for k, v in sorted(price_per_tx.items()): |
||||
if k == 1: |
||||
continue |
||||
item = { |
||||
'text': 'Pay every %d transactions' % k, |
||||
'value': k, |
||||
'sats_per_tx': v / k |
||||
} |
||||
billingModel.append(item) |
||||
|
||||
self._billingModel = billingModel |
||||
self.billingModelChanged.emit() |
||||
|
||||
@pyqtSlot() |
||||
def fetchTermsAndConditions(self): |
||||
def fetch_task(): |
||||
try: |
||||
self.plugin.logger.debug('TOS') |
||||
tos = server.get_terms_of_service() |
||||
except ErrorConnectingServer as e: |
||||
self.termsAndConditionsError.emit(_('Error connecting to server')) |
||||
except Exception as e: |
||||
self.termsAndConditionsError.emit('%s: %s' % (_('Error'), repr(e))) |
||||
else: |
||||
self.termsAndConditionsRetrieved.emit(tos) |
||||
finally: |
||||
self._busy = False |
||||
self.busyChanged.emit() |
||||
|
||||
self._busy = True |
||||
self.busyChanged.emit() |
||||
t = threading.Thread(target=fetch_task) |
||||
t.daemon = True |
||||
t.start() |
||||
|
||||
@pyqtSlot(str) |
||||
def createKeystore(self, email): |
||||
self.remoteKeyState = '' |
||||
self._otpSecret = '' |
||||
self.otpSecretChanged.emit() |
||||
|
||||
wizard_data = self.wizard.get_wizard_data() |
||||
|
||||
xprv1, xpub1, xprv2, xpub2, xpub3, short_id = self.plugin.create_keys(wizard_data) |
||||
|
||||
def create_remote_key_task(): |
||||
try: |
||||
self.plugin.logger.debug('create remote key') |
||||
r = server.create(xpub1, xpub2, email) |
||||
|
||||
otp_secret = r['otp_secret'] |
||||
_xpub3 = r['xpubkey_cosigner'] |
||||
_id = r['id'] |
||||
except (socket.error, ErrorConnectingServer) as e: |
||||
self.remoteKeyState = 'error' |
||||
self.remoteKeyError.emit(f'Network error: {str(e)}') |
||||
except TrustedCoinException as e: |
||||
if e.status_code == 409: |
||||
self.remoteKeyState = 'wallet_known' |
||||
self._shortId = short_id |
||||
self.shortIdChanged.emit() |
||||
else: |
||||
self.remoteKeyState = 'error' |
||||
self.logger.warning(str(e)) |
||||
self.remoteKeyError.emit(f'Service error: {str(e)}') |
||||
except (KeyError, TypeError) as e: # catch any assumptions |
||||
self.remoteKeyState = 'error' |
||||
self.remoteKeyError.emit(f'Error: {str(e)}') |
||||
self.logger.error(str(e)) |
||||
else: |
||||
if short_id != _id: |
||||
self.remoteKeyState = 'error' |
||||
self.logger.error("unexpected trustedcoin short_id: expected {}, received {}".format(short_id, _id)) |
||||
self.remoteKeyError.emit('Unexpected short_id') |
||||
return |
||||
if xpub3 != _xpub3: |
||||
self.remoteKeyState = 'error' |
||||
self.logger.error("unexpected trustedcoin xpub3: expected {}, received {}".format(xpub3, _xpub3)) |
||||
self.remoteKeyError.emit('Unexpected trustedcoin xpub3') |
||||
return |
||||
self.remoteKeyState = 'new' |
||||
self._otpSecret = otp_secret |
||||
self.otpSecretChanged.emit() |
||||
self._shortId = short_id |
||||
self.shortIdChanged.emit() |
||||
finally: |
||||
self._busy = False |
||||
self.busyChanged.emit() |
||||
|
||||
self._busy = True |
||||
self.busyChanged.emit() |
||||
|
||||
t = threading.Thread(target=create_remote_key_task) |
||||
t.daemon = True |
||||
t.start() |
||||
|
||||
@pyqtSlot() |
||||
def resetOtpSecret(self): |
||||
self.remoteKeyState = '' |
||||
|
||||
wizard_data = self.wizard.get_wizard_data() |
||||
|
||||
xprv1, xpub1, xprv2, xpub2, xpub3, short_id = self.plugin.create_keys(wizard_data) |
||||
|
||||
def reset_otp_task(): |
||||
try: |
||||
self.plugin.logger.debug('reset_otp') |
||||
r = server.get_challenge(short_id) |
||||
challenge = r.get('challenge') |
||||
message = 'TRUSTEDCOIN CHALLENGE: ' + challenge |
||||
|
||||
def f(xprv): |
||||
rootnode = BIP32Node.from_xkey(xprv) |
||||
key = rootnode.subkey_at_private_derivation((0, 0)).eckey |
||||
sig = key.sign_message(message, True) |
||||
return base64.b64encode(sig).decode() |
||||
|
||||
signatures = [f(x) for x in [xprv1, xprv2]] |
||||
r = server.reset_auth(short_id, challenge, signatures) |
||||
otp_secret = r.get('otp_secret') |
||||
except (socket.error, ErrorConnectingServer) as e: |
||||
self.remoteKeyState = 'error' |
||||
self.remoteKeyError.emit(f'Network error: {str(e)}') |
||||
except Exception as e: |
||||
self.remoteKeyState = 'error' |
||||
self.remoteKeyError.emit(f'Error: {str(e)}') |
||||
else: |
||||
self.remoteKeyState = 'reset' |
||||
self._otpSecret = otp_secret |
||||
self.otpSecretChanged.emit() |
||||
finally: |
||||
self._busy = False |
||||
self.busyChanged.emit() |
||||
|
||||
self._busy = True |
||||
self.busyChanged.emit() |
||||
|
||||
t = threading.Thread(target=reset_otp_task, daemon=True) |
||||
t.start() |
||||
|
||||
@pyqtSlot(str, int) |
||||
def checkOtp(self, short_id, otp): |
||||
assert type(otp) is int # make sure this doesn't fail subtly |
||||
|
||||
def check_otp_task(): |
||||
try: |
||||
self.plugin.logger.debug(f'check OTP, shortId={short_id}, otp={otp}') |
||||
server.auth(short_id, otp) |
||||
except TrustedCoinException as e: |
||||
if e.status_code == 400: # invalid OTP |
||||
self.plugin.logger.debug('Invalid one-time password.') |
||||
self.otpError.emit(_('Invalid one-time password.')) |
||||
else: |
||||
self.plugin.logger.error(str(e)) |
||||
self.otpError.emit(f'Service error: {str(e)}') |
||||
except Exception as e: |
||||
self.plugin.logger.error(str(e)) |
||||
self.otpError.emit(f'Error: {str(e)}') |
||||
else: |
||||
self.plugin.logger.debug('OTP verify success') |
||||
self.otpSuccess.emit() |
||||
finally: |
||||
self._busy = False |
||||
self.busyChanged.emit() |
||||
self._verifyingOtp = False |
||||
|
||||
self._verifyingOtp = True |
||||
self._busy = True |
||||
self.busyChanged.emit() |
||||
t = threading.Thread(target=check_otp_task, daemon=True) |
||||
t.start() |
||||
Loading…
Reference in new issue