base_wizard.py (32657B)
1 # -*- coding: utf-8 -*- 2 # 3 # Electrum - lightweight Bitcoin client 4 # Copyright (C) 2016 Thomas Voegtlin 5 # 6 # Permission is hereby granted, free of charge, to any person 7 # obtaining a copy of this software and associated documentation files 8 # (the "Software"), to deal in the Software without restriction, 9 # including without limitation the rights to use, copy, modify, merge, 10 # publish, distribute, sublicense, and/or sell copies of the Software, 11 # and to permit persons to whom the Software is furnished to do so, 12 # subject to the following conditions: 13 # 14 # The above copyright notice and this permission notice shall be 15 # included in all copies or substantial portions of the Software. 16 # 17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 21 # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 22 # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 23 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 # SOFTWARE. 25 26 import os 27 import sys 28 import copy 29 import traceback 30 from functools import partial 31 from typing import List, TYPE_CHECKING, Tuple, NamedTuple, Any, Dict, Optional, Union 32 33 from . import bitcoin 34 from . import keystore 35 from . import mnemonic 36 from .bip32 import is_bip32_derivation, xpub_type, normalize_bip32_derivation, BIP32Node 37 from .keystore import bip44_derivation, purpose48_derivation, Hardware_KeyStore, KeyStore, bip39_to_seed 38 from .wallet import (Imported_Wallet, Standard_Wallet, Multisig_Wallet, 39 wallet_types, Wallet, Abstract_Wallet) 40 from .storage import WalletStorage, StorageEncryptionVersion 41 from .wallet_db import WalletDB 42 from .i18n import _ 43 from .util import UserCancelled, InvalidPassword, WalletFileException, UserFacingException 44 from .simple_config import SimpleConfig 45 from .plugin import Plugins, HardwarePluginLibraryUnavailable 46 from .logging import Logger 47 from .plugins.hw_wallet.plugin import OutdatedHwFirmwareException, HW_PluginBase 48 49 if TYPE_CHECKING: 50 from .plugin import DeviceInfo, BasePlugin 51 52 53 # hardware device setup purpose 54 HWD_SETUP_NEW_WALLET, HWD_SETUP_DECRYPT_WALLET = range(0, 2) 55 56 57 class ScriptTypeNotSupported(Exception): pass 58 59 60 class GoBack(Exception): pass 61 62 63 class ReRunDialog(Exception): pass 64 65 66 class ChooseHwDeviceAgain(Exception): pass 67 68 69 class WizardStackItem(NamedTuple): 70 action: Any 71 args: Any 72 kwargs: Dict[str, Any] 73 db_data: dict 74 75 76 class WizardWalletPasswordSetting(NamedTuple): 77 password: Optional[str] 78 encrypt_storage: bool 79 storage_enc_version: StorageEncryptionVersion 80 encrypt_keystore: bool 81 82 83 class BaseWizard(Logger): 84 85 def __init__(self, config: SimpleConfig, plugins: Plugins): 86 super(BaseWizard, self).__init__() 87 Logger.__init__(self) 88 self.config = config 89 self.plugins = plugins 90 self.data = {} 91 self.pw_args = None # type: Optional[WizardWalletPasswordSetting] 92 self._stack = [] # type: List[WizardStackItem] 93 self.plugin = None # type: Optional[BasePlugin] 94 self.keystores = [] # type: List[KeyStore] 95 self.is_kivy = config.get('gui') == 'kivy' 96 self.seed_type = None 97 98 def set_icon(self, icon): 99 pass 100 101 def run(self, *args, **kwargs): 102 action = args[0] 103 args = args[1:] 104 db_data = copy.deepcopy(self.data) 105 self._stack.append(WizardStackItem(action, args, kwargs, db_data)) 106 if not action: 107 return 108 if type(action) is tuple: 109 self.plugin, action = action 110 if self.plugin and hasattr(self.plugin, action): 111 f = getattr(self.plugin, action) 112 f(self, *args, **kwargs) 113 elif hasattr(self, action): 114 f = getattr(self, action) 115 f(*args, **kwargs) 116 else: 117 raise Exception("unknown action", action) 118 119 def can_go_back(self): 120 return len(self._stack) > 1 121 122 def go_back(self, *, rerun_previous: bool = True) -> None: 123 if not self.can_go_back(): 124 return 125 # pop 'current' frame 126 self._stack.pop() 127 prev_frame = self._stack[-1] 128 # try to undo side effects since we last entered 'previous' frame 129 # FIXME only self.data is properly restored 130 self.data = copy.deepcopy(prev_frame.db_data) 131 132 if rerun_previous: 133 # pop 'previous' frame 134 self._stack.pop() 135 # rerun 'previous' frame 136 self.run(prev_frame.action, *prev_frame.args, **prev_frame.kwargs) 137 138 def reset_stack(self): 139 self._stack = [] 140 141 def new(self): 142 title = _("Create new wallet") 143 message = '\n'.join([ 144 _("What kind of wallet do you want to create?") 145 ]) 146 wallet_kinds = [ 147 ('standard', _("Standard wallet")), 148 ('2fa', _("Wallet with two-factor authentication")), 149 ('multisig', _("Multi-signature wallet")), 150 ('imported', _("Import Bitcoin addresses or private keys")), 151 ] 152 choices = [pair for pair in wallet_kinds if pair[0] in wallet_types] 153 self.choice_dialog(title=title, message=message, choices=choices, run_next=self.on_wallet_type) 154 155 def upgrade_db(self, storage, db): 156 exc = None # type: Optional[Exception] 157 def on_finished(): 158 if exc is None: 159 self.terminate(storage=storage, db=db) 160 else: 161 raise exc 162 def do_upgrade(): 163 nonlocal exc 164 try: 165 db.upgrade() 166 except Exception as e: 167 exc = e 168 self.waiting_dialog(do_upgrade, _('Upgrading wallet format...'), on_finished=on_finished) 169 170 def run_task_without_blocking_gui(self, task, *, msg: str = None) -> Any: 171 """Perform a task in a thread without blocking the GUI. 172 Returns the result of 'task', or raises the same exception. 173 This method blocks until 'task' is finished. 174 """ 175 raise NotImplementedError() 176 177 def load_2fa(self): 178 self.data['wallet_type'] = '2fa' 179 self.data['use_trustedcoin'] = True 180 self.plugin = self.plugins.load_plugin('trustedcoin') 181 182 def on_wallet_type(self, choice): 183 self.data['wallet_type'] = self.wallet_type = choice 184 if choice == 'standard': 185 action = 'choose_keystore' 186 elif choice == 'multisig': 187 action = 'choose_multisig' 188 elif choice == '2fa': 189 self.load_2fa() 190 action = self.plugin.get_action(self.data) 191 elif choice == 'imported': 192 action = 'import_addresses_or_keys' 193 self.run(action) 194 195 def choose_multisig(self): 196 def on_multisig(m, n): 197 multisig_type = "%dof%d" % (m, n) 198 self.data['wallet_type'] = multisig_type 199 self.n = n 200 self.run('choose_keystore') 201 self.multisig_dialog(run_next=on_multisig) 202 203 def choose_keystore(self): 204 assert self.wallet_type in ['standard', 'multisig'] 205 i = len(self.keystores) 206 title = _('Add cosigner') + ' (%d of %d)'%(i+1, self.n) if self.wallet_type=='multisig' else _('Keystore') 207 if self.wallet_type =='standard' or i==0: 208 message = _('Do you want to create a new seed, or to restore a wallet using an existing seed?') 209 choices = [ 210 ('choose_seed_type', _('Create a new seed')), 211 ('restore_from_seed', _('I already have a seed')), 212 ('restore_from_key', _('Use a master key')), 213 ] 214 if not self.is_kivy: 215 choices.append(('choose_hw_device', _('Use a hardware device'))) 216 else: 217 message = _('Add a cosigner to your multi-sig wallet') 218 choices = [ 219 ('restore_from_key', _('Enter cosigner key')), 220 ('restore_from_seed', _('Enter cosigner seed')), 221 ] 222 if not self.is_kivy: 223 choices.append(('choose_hw_device', _('Cosign with hardware device'))) 224 225 self.choice_dialog(title=title, message=message, choices=choices, run_next=self.run) 226 227 def import_addresses_or_keys(self): 228 v = lambda x: keystore.is_address_list(x) or keystore.is_private_key_list(x, raise_on_error=True) 229 title = _("Import Bitcoin Addresses") 230 message = _("Enter a list of Bitcoin addresses (this will create a watching-only wallet), or a list of private keys.") 231 self.add_xpub_dialog(title=title, message=message, run_next=self.on_import, 232 is_valid=v, allow_multi=True, show_wif_help=True) 233 234 def on_import(self, text): 235 # text is already sanitized by is_address_list and is_private_keys_list 236 if keystore.is_address_list(text): 237 self.data['addresses'] = {} 238 for addr in text.split(): 239 assert bitcoin.is_address(addr) 240 self.data['addresses'][addr] = {} 241 elif keystore.is_private_key_list(text): 242 self.data['addresses'] = {} 243 k = keystore.Imported_KeyStore({}) 244 keys = keystore.get_private_keys(text) 245 for pk in keys: 246 assert bitcoin.is_private_key(pk) 247 txin_type, pubkey = k.import_privkey(pk, None) 248 addr = bitcoin.pubkey_to_address(txin_type, pubkey) 249 self.data['addresses'][addr] = {'type':txin_type, 'pubkey':pubkey} 250 self.keystores.append(k) 251 else: 252 return self.terminate(aborted=True) 253 return self.run('create_wallet') 254 255 def restore_from_key(self): 256 if self.wallet_type == 'standard': 257 v = keystore.is_master_key 258 title = _("Create keystore from a master key") 259 message = ' '.join([ 260 _("To create a watching-only wallet, please enter your master public key (xpub/ypub/zpub)."), 261 _("To create a spending wallet, please enter a master private key (xprv/yprv/zprv).") 262 ]) 263 self.add_xpub_dialog(title=title, message=message, run_next=self.on_restore_from_key, is_valid=v) 264 else: 265 i = len(self.keystores) + 1 266 self.add_cosigner_dialog(index=i, run_next=self.on_restore_from_key, is_valid=keystore.is_bip32_key) 267 268 def on_restore_from_key(self, text): 269 k = keystore.from_master_key(text) 270 self.on_keystore(k) 271 272 def choose_hw_device(self, purpose=HWD_SETUP_NEW_WALLET, *, storage: WalletStorage = None): 273 while True: 274 try: 275 self._choose_hw_device(purpose=purpose, storage=storage) 276 except ChooseHwDeviceAgain: 277 pass 278 else: 279 break 280 281 def _choose_hw_device(self, *, purpose, storage: WalletStorage = None): 282 title = _('Hardware Keystore') 283 # check available plugins 284 supported_plugins = self.plugins.get_hardware_support() 285 devices = [] # type: List[Tuple[str, DeviceInfo]] 286 devmgr = self.plugins.device_manager 287 debug_msg = '' 288 289 def failed_getting_device_infos(name, e): 290 nonlocal debug_msg 291 err_str_oneline = ' // '.join(str(e).splitlines()) 292 self.logger.warning(f'error getting device infos for {name}: {err_str_oneline}') 293 indented_error_msg = ' '.join([''] + str(e).splitlines(keepends=True)) 294 debug_msg += f' {name}: (error getting device infos)\n{indented_error_msg}\n' 295 296 # scan devices 297 try: 298 scanned_devices = self.run_task_without_blocking_gui(task=devmgr.scan_devices, 299 msg=_("Scanning devices...")) 300 except BaseException as e: 301 self.logger.info('error scanning devices: {}'.format(repr(e))) 302 debug_msg = ' {}:\n {}'.format(_('Error scanning devices'), e) 303 else: 304 for splugin in supported_plugins: 305 name, plugin = splugin.name, splugin.plugin 306 # plugin init errored? 307 if not plugin: 308 e = splugin.exception 309 indented_error_msg = ' '.join([''] + str(e).splitlines(keepends=True)) 310 debug_msg += f' {name}: (error during plugin init)\n' 311 debug_msg += ' {}\n'.format(_('You might have an incompatible library.')) 312 debug_msg += f'{indented_error_msg}\n' 313 continue 314 # see if plugin recognizes 'scanned_devices' 315 try: 316 # FIXME: side-effect: unpaired_device_info sets client.handler 317 device_infos = devmgr.unpaired_device_infos(None, plugin, devices=scanned_devices, 318 include_failing_clients=True) 319 except HardwarePluginLibraryUnavailable as e: 320 failed_getting_device_infos(name, e) 321 continue 322 except BaseException as e: 323 self.logger.exception('') 324 failed_getting_device_infos(name, e) 325 continue 326 device_infos_failing = list(filter(lambda di: di.exception is not None, device_infos)) 327 for di in device_infos_failing: 328 failed_getting_device_infos(name, di.exception) 329 device_infos_working = list(filter(lambda di: di.exception is None, device_infos)) 330 devices += list(map(lambda x: (name, x), device_infos_working)) 331 if not debug_msg: 332 debug_msg = ' {}'.format(_('No exceptions encountered.')) 333 if not devices: 334 msg = (_('No hardware device detected.') + '\n' + 335 _('To trigger a rescan, press \'Next\'.') + '\n\n') 336 if sys.platform == 'win32': 337 msg += _('If your device is not detected on Windows, go to "Settings", "Devices", "Connected devices", ' 338 'and do "Remove device". Then, plug your device again.') + '\n' 339 msg += _('While this is less than ideal, it might help if you run Electrum as Administrator.') + '\n' 340 else: 341 msg += _('On Linux, you might have to add a new permission to your udev rules.') + '\n' 342 msg += '\n\n' 343 msg += _('Debug message') + '\n' + debug_msg 344 self.confirm_dialog(title=title, message=msg, 345 run_next=lambda x: None) 346 raise ChooseHwDeviceAgain() 347 # select device 348 self.devices = devices 349 choices = [] 350 for name, info in devices: 351 state = _("initialized") if info.initialized else _("wiped") 352 label = info.label or _("An unnamed {}").format(name) 353 try: transport_str = info.device.transport_ui_string[:20] 354 except: transport_str = 'unknown transport' 355 descr = f"{label} [{info.model_name or name}, {state}, {transport_str}]" 356 choices.append(((name, info), descr)) 357 msg = _('Select a device') + ':' 358 self.choice_dialog(title=title, message=msg, choices=choices, 359 run_next=lambda *args: self.on_device(*args, purpose=purpose, storage=storage)) 360 361 def on_device(self, name, device_info: 'DeviceInfo', *, purpose, storage: WalletStorage = None): 362 self.plugin = self.plugins.get_plugin(name) 363 assert isinstance(self.plugin, HW_PluginBase) 364 devmgr = self.plugins.device_manager 365 try: 366 client = self.plugin.setup_device(device_info, self, purpose) 367 except OSError as e: 368 self.show_error(_('We encountered an error while connecting to your device:') 369 + '\n' + str(e) + '\n' 370 + _('To try to fix this, we will now re-pair with your device.') + '\n' 371 + _('Please try again.')) 372 devmgr.unpair_id(device_info.device.id_) 373 raise ChooseHwDeviceAgain() 374 except OutdatedHwFirmwareException as e: 375 if self.question(e.text_ignore_old_fw_and_continue(), title=_("Outdated device firmware")): 376 self.plugin.set_ignore_outdated_fw() 377 # will need to re-pair 378 devmgr.unpair_id(device_info.device.id_) 379 raise ChooseHwDeviceAgain() 380 except (UserCancelled, GoBack): 381 raise ChooseHwDeviceAgain() 382 except UserFacingException as e: 383 self.show_error(str(e)) 384 raise ChooseHwDeviceAgain() 385 except BaseException as e: 386 self.logger.exception('') 387 self.show_error(str(e)) 388 raise ChooseHwDeviceAgain() 389 390 if purpose == HWD_SETUP_NEW_WALLET: 391 def f(derivation, script_type): 392 derivation = normalize_bip32_derivation(derivation) 393 self.run('on_hw_derivation', name, device_info, derivation, script_type) 394 self.derivation_and_script_type_dialog(f) 395 elif purpose == HWD_SETUP_DECRYPT_WALLET: 396 password = client.get_password_for_storage_encryption() 397 try: 398 storage.decrypt(password) 399 except InvalidPassword: 400 # try to clear session so that user can type another passphrase 401 if hasattr(client, 'clear_session'): # FIXME not all hw wallet plugins have this 402 client.clear_session() 403 raise 404 else: 405 raise Exception('unknown purpose: %s' % purpose) 406 407 def derivation_and_script_type_dialog(self, f, *, get_account_xpub=None): 408 message1 = _('Choose the type of addresses in your wallet.') 409 message2 = ' '.join([ 410 _('You can override the suggested derivation path.'), 411 _('If you are not sure what this is, leave this field unchanged.') 412 ]) 413 hide_choices = False 414 if self.wallet_type == 'multisig': 415 # There is no general standard for HD multisig. 416 # For legacy, this is partially compatible with BIP45; assumes index=0 417 # For segwit, a custom path is used, as there is no standard at all. 418 default_choice_idx = 2 419 choices = [ 420 ('standard', 'legacy multisig (p2sh)', normalize_bip32_derivation("m/45'/0")), 421 ('p2wsh-p2sh', 'p2sh-segwit multisig (p2wsh-p2sh)', purpose48_derivation(0, xtype='p2wsh-p2sh')), 422 ('p2wsh', 'native segwit multisig (p2wsh)', purpose48_derivation(0, xtype='p2wsh')), 423 ] 424 # if this is not the first cosigner, pre-select the expected script type, 425 # and hide the choices 426 script_type = self.get_script_type_of_wallet() 427 if script_type is not None: 428 script_types = [*zip(*choices)][0] 429 chosen_idx = script_types.index(script_type) 430 default_choice_idx = chosen_idx 431 hide_choices = True 432 else: 433 default_choice_idx = 2 434 choices = [ 435 ('standard', 'legacy (p2pkh)', bip44_derivation(0, bip43_purpose=44)), 436 ('p2wpkh-p2sh', 'p2sh-segwit (p2wpkh-p2sh)', bip44_derivation(0, bip43_purpose=49)), 437 ('p2wpkh', 'native segwit (p2wpkh)', bip44_derivation(0, bip43_purpose=84)), 438 ] 439 while True: 440 try: 441 self.derivation_and_script_type_gui_specific_dialog( 442 run_next=f, 443 title=_('Script type and Derivation path'), 444 message1=message1, 445 message2=message2, 446 choices=choices, 447 test_text=is_bip32_derivation, 448 default_choice_idx=default_choice_idx, 449 get_account_xpub=get_account_xpub, 450 hide_choices=hide_choices, 451 ) 452 return 453 except ScriptTypeNotSupported as e: 454 self.show_error(e) 455 # let the user choose again 456 457 def on_hw_derivation(self, name, device_info: 'DeviceInfo', derivation, xtype): 458 from .keystore import hardware_keystore 459 devmgr = self.plugins.device_manager 460 assert isinstance(self.plugin, HW_PluginBase) 461 try: 462 xpub = self.plugin.get_xpub(device_info.device.id_, derivation, xtype, self) 463 client = devmgr.client_by_id(device_info.device.id_, scan_now=False) 464 if not client: raise Exception("failed to find client for device id") 465 root_fingerprint = client.request_root_fingerprint_from_device() 466 label = client.label() # use this as device_info.label might be outdated! 467 soft_device_id = client.get_soft_device_id() # use this as device_info.device_id might be outdated! 468 except ScriptTypeNotSupported: 469 raise # this is handled in derivation_dialog 470 except BaseException as e: 471 self.logger.exception('') 472 self.show_error(e) 473 raise ChooseHwDeviceAgain() 474 d = { 475 'type': 'hardware', 476 'hw_type': name, 477 'derivation': derivation, 478 'root_fingerprint': root_fingerprint, 479 'xpub': xpub, 480 'label': label, 481 'soft_device_id': soft_device_id, 482 } 483 try: 484 client.manipulate_keystore_dict_during_wizard_setup(d) 485 except Exception as e: 486 self.logger.exception('') 487 self.show_error(e) 488 raise ChooseHwDeviceAgain() 489 k = hardware_keystore(d) 490 self.on_keystore(k) 491 492 def passphrase_dialog(self, run_next, is_restoring=False): 493 title = _('Seed extension') 494 message = '\n'.join([ 495 _('You may extend your seed with custom words.'), 496 _('Your seed extension must be saved together with your seed.'), 497 ]) 498 warning = '\n'.join([ 499 _('Note that this is NOT your encryption password.'), 500 _('If you do not know what this is, leave this field empty.'), 501 ]) 502 warn_issue4566 = is_restoring and self.seed_type == 'bip39' 503 self.line_dialog(title=title, message=message, warning=warning, 504 default='', test=lambda x:True, run_next=run_next, 505 warn_issue4566=warn_issue4566) 506 507 def restore_from_seed(self): 508 self.opt_bip39 = True 509 self.opt_ext = True 510 is_cosigning_seed = lambda x: mnemonic.seed_type(x) in ['standard', 'segwit'] 511 test = mnemonic.is_seed if self.wallet_type == 'standard' else is_cosigning_seed 512 f = lambda *args: self.run('on_restore_seed', *args) 513 self.restore_seed_dialog(run_next=f, test=test) 514 515 def on_restore_seed(self, seed, is_bip39, is_ext): 516 self.seed_type = 'bip39' if is_bip39 else mnemonic.seed_type(seed) 517 if self.seed_type == 'bip39': 518 f = lambda passphrase: self.run('on_restore_bip39', seed, passphrase) 519 self.passphrase_dialog(run_next=f, is_restoring=True) if is_ext else f('') 520 elif self.seed_type in ['standard', 'segwit']: 521 f = lambda passphrase: self.run('create_keystore', seed, passphrase) 522 self.passphrase_dialog(run_next=f, is_restoring=True) if is_ext else f('') 523 elif self.seed_type == 'old': 524 self.run('create_keystore', seed, '') 525 elif mnemonic.is_any_2fa_seed_type(self.seed_type): 526 self.load_2fa() 527 self.run('on_restore_seed', seed, is_ext) 528 else: 529 raise Exception('Unknown seed type', self.seed_type) 530 531 def on_restore_bip39(self, seed, passphrase): 532 def f(derivation, script_type): 533 derivation = normalize_bip32_derivation(derivation) 534 self.run('on_bip43', seed, passphrase, derivation, script_type) 535 if self.wallet_type == 'standard': 536 def get_account_xpub(account_path): 537 root_seed = bip39_to_seed(seed, passphrase) 538 root_node = BIP32Node.from_rootseed(root_seed, xtype="standard") 539 account_node = root_node.subkey_at_private_derivation(account_path) 540 account_xpub = account_node.to_xpub() 541 return account_xpub 542 else: 543 get_account_xpub = None 544 self.derivation_and_script_type_dialog(f, get_account_xpub=get_account_xpub) 545 546 def create_keystore(self, seed, passphrase): 547 k = keystore.from_seed(seed, passphrase, self.wallet_type == 'multisig') 548 self.on_keystore(k) 549 550 def on_bip43(self, seed, passphrase, derivation, script_type): 551 k = keystore.from_bip39_seed(seed, passphrase, derivation, xtype=script_type) 552 self.on_keystore(k) 553 554 def get_script_type_of_wallet(self) -> Optional[str]: 555 if len(self.keystores) > 0: 556 ks = self.keystores[0] 557 if isinstance(ks, keystore.Xpub): 558 return xpub_type(ks.xpub) 559 return None 560 561 def on_keystore(self, k: KeyStore): 562 has_xpub = isinstance(k, keystore.Xpub) 563 if has_xpub: 564 t1 = xpub_type(k.xpub) 565 if self.wallet_type == 'standard': 566 if has_xpub and t1 not in ['standard', 'p2wpkh', 'p2wpkh-p2sh']: 567 self.show_error(_('Wrong key type') + ' %s'%t1) 568 self.run('choose_keystore') 569 return 570 self.keystores.append(k) 571 self.run('create_wallet') 572 elif self.wallet_type == 'multisig': 573 assert has_xpub 574 if t1 not in ['standard', 'p2wsh', 'p2wsh-p2sh']: 575 self.show_error(_('Wrong key type') + ' %s'%t1) 576 self.run('choose_keystore') 577 return 578 if k.xpub in map(lambda x: x.xpub, self.keystores): 579 self.show_error(_('Error: duplicate master public key')) 580 self.run('choose_keystore') 581 return 582 if len(self.keystores)>0: 583 t2 = xpub_type(self.keystores[0].xpub) 584 if t1 != t2: 585 self.show_error(_('Cannot add this cosigner:') + '\n' + "Their key type is '%s', we are '%s'"%(t1, t2)) 586 self.run('choose_keystore') 587 return 588 if len(self.keystores) == 0: 589 xpub = k.get_master_public_key() 590 self.reset_stack() 591 self.keystores.append(k) 592 self.run('show_xpub_and_add_cosigners', xpub) 593 return 594 self.reset_stack() 595 self.keystores.append(k) 596 if len(self.keystores) < self.n: 597 self.run('choose_keystore') 598 else: 599 self.run('create_wallet') 600 601 def create_wallet(self): 602 encrypt_keystore = any(k.may_have_password() for k in self.keystores) 603 # note: the following condition ("if") is duplicated logic from 604 # wallet.get_available_storage_encryption_version() 605 if self.wallet_type == 'standard' and isinstance(self.keystores[0], Hardware_KeyStore): 606 # offer encrypting with a pw derived from the hw device 607 k = self.keystores[0] # type: Hardware_KeyStore 608 assert isinstance(self.plugin, HW_PluginBase) 609 try: 610 k.handler = self.plugin.create_handler(self) 611 password = k.get_password_for_storage_encryption() 612 except UserCancelled: 613 devmgr = self.plugins.device_manager 614 devmgr.unpair_xpub(k.xpub) 615 raise ChooseHwDeviceAgain() 616 except BaseException as e: 617 self.logger.exception('') 618 self.show_error(str(e)) 619 raise ChooseHwDeviceAgain() 620 self.request_storage_encryption( 621 run_next=lambda encrypt_storage: self.on_password( 622 password, 623 encrypt_storage=encrypt_storage, 624 storage_enc_version=StorageEncryptionVersion.XPUB_PASSWORD, 625 encrypt_keystore=False)) 626 else: 627 # reset stack to disable 'back' button in password dialog 628 self.reset_stack() 629 # prompt the user to set an arbitrary password 630 self.request_password( 631 run_next=lambda password, encrypt_storage: self.on_password( 632 password, 633 encrypt_storage=encrypt_storage, 634 storage_enc_version=StorageEncryptionVersion.USER_PASSWORD, 635 encrypt_keystore=encrypt_keystore), 636 force_disable_encrypt_cb=not encrypt_keystore) 637 638 def on_password(self, password, *, encrypt_storage: bool, 639 storage_enc_version=StorageEncryptionVersion.USER_PASSWORD, 640 encrypt_keystore: bool): 641 for k in self.keystores: 642 if k.may_have_password(): 643 k.update_password(None, password) 644 if self.wallet_type == 'standard': 645 self.data['seed_type'] = self.seed_type 646 keys = self.keystores[0].dump() 647 self.data['keystore'] = keys 648 elif self.wallet_type == 'multisig': 649 for i, k in enumerate(self.keystores): 650 self.data['x%d/'%(i+1)] = k.dump() 651 elif self.wallet_type == 'imported': 652 if len(self.keystores) > 0: 653 keys = self.keystores[0].dump() 654 self.data['keystore'] = keys 655 else: 656 raise Exception('Unknown wallet type') 657 self.pw_args = WizardWalletPasswordSetting(password=password, 658 encrypt_storage=encrypt_storage, 659 storage_enc_version=storage_enc_version, 660 encrypt_keystore=encrypt_keystore) 661 self.terminate() 662 663 def create_storage(self, path) -> Tuple[WalletStorage, WalletDB]: 664 if os.path.exists(path): 665 raise Exception('file already exists at path') 666 assert self.pw_args, f"pw_args not set?!" 667 pw_args = self.pw_args 668 self.pw_args = None # clean-up so that it can get GC-ed 669 storage = WalletStorage(path) 670 if pw_args.encrypt_storage: 671 storage.set_password(pw_args.password, enc_version=pw_args.storage_enc_version) 672 db = WalletDB('', manual_upgrades=False) 673 db.set_keystore_encryption(bool(pw_args.password) and pw_args.encrypt_keystore) 674 for key, value in self.data.items(): 675 db.put(key, value) 676 db.load_plugins() 677 db.write(storage) 678 return storage, db 679 680 def terminate(self, *, storage: WalletStorage = None, 681 db: WalletDB = None, 682 aborted: bool = False) -> None: 683 raise NotImplementedError() # implemented by subclasses 684 685 def show_xpub_and_add_cosigners(self, xpub): 686 self.show_xpub_dialog(xpub=xpub, run_next=lambda x: self.run('choose_keystore')) 687 688 def choose_seed_type(self): 689 seed_type = 'standard' if self.config.get('nosegwit') else 'segwit' 690 self.create_seed(seed_type) 691 692 def create_seed(self, seed_type): 693 from . import mnemonic 694 self.seed_type = seed_type 695 seed = mnemonic.Mnemonic('en').make_seed(seed_type=self.seed_type) 696 self.opt_bip39 = False 697 self.opt_ext = True 698 f = lambda x: self.request_passphrase(seed, x) 699 self.show_seed_dialog(run_next=f, seed_text=seed) 700 701 def request_passphrase(self, seed, opt_passphrase): 702 if opt_passphrase: 703 f = lambda x: self.confirm_seed(seed, x) 704 self.passphrase_dialog(run_next=f) 705 else: 706 self.run('confirm_seed', seed, '') 707 708 def confirm_seed(self, seed, passphrase): 709 f = lambda x: self.confirm_passphrase(seed, passphrase) 710 self.confirm_seed_dialog(run_next=f, seed=seed if self.config.get('debug_seed') else '', test=lambda x: x==seed) 711 712 def confirm_passphrase(self, seed, passphrase): 713 f = lambda x: self.run('create_keystore', seed, x) 714 if passphrase: 715 title = _('Confirm Seed Extension') 716 message = '\n'.join([ 717 _('Your seed extension must be saved together with your seed.'), 718 _('Please type it here.'), 719 ]) 720 self.line_dialog(run_next=f, title=title, message=message, default='', test=lambda x: x==passphrase) 721 else: 722 f('') 723 724 def show_error(self, msg: Union[str, BaseException]) -> None: 725 raise NotImplementedError()