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 26import os 27import sys 28import copy 29import traceback 30from functools import partial 31from typing import List, TYPE_CHECKING, Tuple, NamedTuple, Any, Dict, Optional, Union 32 33from . import bitcoin 34from . import keystore 35from . import mnemonic 36from .bip32 import is_bip32_derivation, xpub_type, normalize_bip32_derivation, BIP32Node 37from .keystore import bip44_derivation, purpose48_derivation, Hardware_KeyStore, KeyStore, bip39_to_seed 38from .wallet import (Imported_Wallet, Standard_Wallet, Multisig_Wallet, 39 wallet_types, Wallet, Abstract_Wallet) 40from .storage import WalletStorage, StorageEncryptionVersion 41from .wallet_db import WalletDB 42from .i18n import _ 43from .util import UserCancelled, InvalidPassword, WalletFileException, UserFacingException 44from .simple_config import SimpleConfig 45from .plugin import Plugins, HardwarePluginLibraryUnavailable 46from .logging import Logger 47from .plugins.hw_wallet.plugin import OutdatedHwFirmwareException, HW_PluginBase 48 49if TYPE_CHECKING: 50 from .plugin import DeviceInfo, BasePlugin 51 52 53# hardware device setup purpose 54HWD_SETUP_NEW_WALLET, HWD_SETUP_DECRYPT_WALLET = range(0, 2) 55 56 57class ScriptTypeNotSupported(Exception): pass 58 59 60class GoBack(Exception): pass 61 62 63class ReRunDialog(Exception): pass 64 65 66class ChooseHwDeviceAgain(Exception): pass 67 68 69class WizardStackItem(NamedTuple): 70 action: Any 71 args: Any 72 kwargs: Dict[str, Any] 73 db_data: dict 74 75 76class WizardWalletPasswordSetting(NamedTuple): 77 password: Optional[str] 78 encrypt_storage: bool 79 storage_enc_version: StorageEncryptionVersion 80 encrypt_keystore: bool 81 82 83class 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 GoBack: 381 raise ChooseHwDeviceAgain() 382 except (UserCancelled, ReRunDialog): 383 raise 384 except UserFacingException as e: 385 self.show_error(str(e)) 386 raise ChooseHwDeviceAgain() 387 except BaseException as e: 388 self.logger.exception('') 389 self.show_error(str(e)) 390 raise ChooseHwDeviceAgain() 391 392 if purpose == HWD_SETUP_NEW_WALLET: 393 def f(derivation, script_type): 394 derivation = normalize_bip32_derivation(derivation) 395 self.run('on_hw_derivation', name, device_info, derivation, script_type) 396 self.derivation_and_script_type_dialog(f) 397 elif purpose == HWD_SETUP_DECRYPT_WALLET: 398 password = client.get_password_for_storage_encryption() 399 try: 400 storage.decrypt(password) 401 except InvalidPassword: 402 # try to clear session so that user can type another passphrase 403 if hasattr(client, 'clear_session'): # FIXME not all hw wallet plugins have this 404 client.clear_session() 405 raise 406 else: 407 raise Exception('unknown purpose: %s' % purpose) 408 409 def derivation_and_script_type_dialog(self, f, *, get_account_xpub=None): 410 message1 = _('Choose the type of addresses in your wallet.') 411 message2 = ' '.join([ 412 _('You can override the suggested derivation path.'), 413 _('If you are not sure what this is, leave this field unchanged.') 414 ]) 415 hide_choices = False 416 if self.wallet_type == 'multisig': 417 # There is no general standard for HD multisig. 418 # For legacy, this is partially compatible with BIP45; assumes index=0 419 # For segwit, a custom path is used, as there is no standard at all. 420 default_choice_idx = 2 421 choices = [ 422 ('standard', 'legacy multisig (p2sh)', normalize_bip32_derivation("m/45'/0")), 423 ('p2wsh-p2sh', 'p2sh-segwit multisig (p2wsh-p2sh)', purpose48_derivation(0, xtype='p2wsh-p2sh')), 424 ('p2wsh', 'native segwit multisig (p2wsh)', purpose48_derivation(0, xtype='p2wsh')), 425 ] 426 # if this is not the first cosigner, pre-select the expected script type, 427 # and hide the choices 428 script_type = self.get_script_type_of_wallet() 429 if script_type is not None: 430 script_types = [*zip(*choices)][0] 431 chosen_idx = script_types.index(script_type) 432 default_choice_idx = chosen_idx 433 hide_choices = True 434 else: 435 default_choice_idx = 2 436 choices = [ 437 ('standard', 'legacy (p2pkh)', bip44_derivation(0, bip43_purpose=44)), 438 ('p2wpkh-p2sh', 'p2sh-segwit (p2wpkh-p2sh)', bip44_derivation(0, bip43_purpose=49)), 439 ('p2wpkh', 'native segwit (p2wpkh)', bip44_derivation(0, bip43_purpose=84)), 440 ] 441 while True: 442 try: 443 self.derivation_and_script_type_gui_specific_dialog( 444 run_next=f, 445 title=_('Script type and Derivation path'), 446 message1=message1, 447 message2=message2, 448 choices=choices, 449 test_text=is_bip32_derivation, 450 default_choice_idx=default_choice_idx, 451 get_account_xpub=get_account_xpub, 452 hide_choices=hide_choices, 453 ) 454 return 455 except ScriptTypeNotSupported as e: 456 self.show_error(e) 457 # let the user choose again 458 459 def on_hw_derivation(self, name, device_info: 'DeviceInfo', derivation, xtype): 460 from .keystore import hardware_keystore 461 devmgr = self.plugins.device_manager 462 assert isinstance(self.plugin, HW_PluginBase) 463 try: 464 xpub = self.plugin.get_xpub(device_info.device.id_, derivation, xtype, self) 465 client = devmgr.client_by_id(device_info.device.id_, scan_now=False) 466 if not client: raise Exception("failed to find client for device id") 467 root_fingerprint = client.request_root_fingerprint_from_device() 468 label = client.label() # use this as device_info.label might be outdated! 469 soft_device_id = client.get_soft_device_id() # use this as device_info.device_id might be outdated! 470 except ScriptTypeNotSupported: 471 raise # this is handled in derivation_dialog 472 except BaseException as e: 473 self.logger.exception('') 474 self.show_error(e) 475 raise ChooseHwDeviceAgain() 476 d = { 477 'type': 'hardware', 478 'hw_type': name, 479 'derivation': derivation, 480 'root_fingerprint': root_fingerprint, 481 'xpub': xpub, 482 'label': label, 483 'soft_device_id': soft_device_id, 484 } 485 try: 486 client.manipulate_keystore_dict_during_wizard_setup(d) 487 except Exception as e: 488 self.logger.exception('') 489 self.show_error(e) 490 raise ChooseHwDeviceAgain() 491 k = hardware_keystore(d) 492 self.on_keystore(k) 493 494 def passphrase_dialog(self, run_next, is_restoring=False): 495 title = _('Seed extension') 496 message = '\n'.join([ 497 _('You may extend your seed with custom words.'), 498 _('Your seed extension must be saved together with your seed.'), 499 ]) 500 warning = '\n'.join([ 501 _('Note that this is NOT your encryption password.'), 502 _('If you do not know what this is, leave this field empty.'), 503 ]) 504 warn_issue4566 = is_restoring and self.seed_type == 'bip39' 505 self.line_dialog(title=title, message=message, warning=warning, 506 default='', test=lambda x:True, run_next=run_next, 507 warn_issue4566=warn_issue4566) 508 509 def restore_from_seed(self): 510 self.opt_bip39 = True 511 self.opt_slip39 = True 512 self.opt_ext = True 513 is_cosigning_seed = lambda x: mnemonic.seed_type(x) in ['standard', 'segwit'] 514 test = mnemonic.is_seed if self.wallet_type == 'standard' else is_cosigning_seed 515 f = lambda *args: self.run('on_restore_seed', *args) 516 self.restore_seed_dialog(run_next=f, test=test) 517 518 def on_restore_seed(self, seed, seed_type, is_ext): 519 self.seed_type = seed_type if seed_type != 'electrum' else mnemonic.seed_type(seed) 520 if self.seed_type == 'bip39': 521 def f(passphrase): 522 root_seed = bip39_to_seed(seed, passphrase) 523 self.on_restore_bip43(root_seed) 524 self.passphrase_dialog(run_next=f, is_restoring=True) if is_ext else f('') 525 elif self.seed_type == 'slip39': 526 def f(passphrase): 527 root_seed = seed.decrypt(passphrase) 528 self.on_restore_bip43(root_seed) 529 self.passphrase_dialog(run_next=f, is_restoring=True) if is_ext else f('') 530 elif self.seed_type in ['standard', 'segwit']: 531 f = lambda passphrase: self.run('create_keystore', seed, passphrase) 532 self.passphrase_dialog(run_next=f, is_restoring=True) if is_ext else f('') 533 elif self.seed_type == 'old': 534 self.run('create_keystore', seed, '') 535 elif mnemonic.is_any_2fa_seed_type(self.seed_type): 536 self.load_2fa() 537 self.run('on_restore_seed', seed, is_ext) 538 else: 539 raise Exception('Unknown seed type', self.seed_type) 540 541 def on_restore_bip43(self, root_seed): 542 def f(derivation, script_type): 543 derivation = normalize_bip32_derivation(derivation) 544 self.run('on_bip43', root_seed, derivation, script_type) 545 if self.wallet_type == 'standard': 546 def get_account_xpub(account_path): 547 root_node = BIP32Node.from_rootseed(root_seed, xtype="standard") 548 account_node = root_node.subkey_at_private_derivation(account_path) 549 account_xpub = account_node.to_xpub() 550 return account_xpub 551 else: 552 get_account_xpub = None 553 self.derivation_and_script_type_dialog(f, get_account_xpub=get_account_xpub) 554 555 def create_keystore(self, seed, passphrase): 556 k = keystore.from_seed(seed, passphrase, self.wallet_type == 'multisig') 557 if k.can_have_deterministic_lightning_xprv(): 558 self.data['lightning_xprv'] = k.get_lightning_xprv(None) 559 self.on_keystore(k) 560 561 def on_bip43(self, root_seed, derivation, script_type): 562 k = keystore.from_bip43_rootseed(root_seed, derivation, xtype=script_type) 563 self.on_keystore(k) 564 565 def get_script_type_of_wallet(self) -> Optional[str]: 566 if len(self.keystores) > 0: 567 ks = self.keystores[0] 568 if isinstance(ks, keystore.Xpub): 569 return xpub_type(ks.xpub) 570 return None 571 572 def on_keystore(self, k: KeyStore): 573 has_xpub = isinstance(k, keystore.Xpub) 574 if has_xpub: 575 t1 = xpub_type(k.xpub) 576 if self.wallet_type == 'standard': 577 if has_xpub and t1 not in ['standard', 'p2wpkh', 'p2wpkh-p2sh']: 578 self.show_error(_('Wrong key type') + ' %s'%t1) 579 self.run('choose_keystore') 580 return 581 self.keystores.append(k) 582 self.run('create_wallet') 583 elif self.wallet_type == 'multisig': 584 assert has_xpub 585 if t1 not in ['standard', 'p2wsh', 'p2wsh-p2sh']: 586 self.show_error(_('Wrong key type') + ' %s'%t1) 587 self.run('choose_keystore') 588 return 589 if k.xpub in map(lambda x: x.xpub, self.keystores): 590 self.show_error(_('Error: duplicate master public key')) 591 self.run('choose_keystore') 592 return 593 if len(self.keystores)>0: 594 t2 = xpub_type(self.keystores[0].xpub) 595 if t1 != t2: 596 self.show_error(_('Cannot add this cosigner:') + '\n' + "Their key type is '%s', we are '%s'"%(t1, t2)) 597 self.run('choose_keystore') 598 return 599 if len(self.keystores) == 0: 600 xpub = k.get_master_public_key() 601 self.reset_stack() 602 self.keystores.append(k) 603 self.run('show_xpub_and_add_cosigners', xpub) 604 return 605 self.reset_stack() 606 self.keystores.append(k) 607 if len(self.keystores) < self.n: 608 self.run('choose_keystore') 609 else: 610 self.run('create_wallet') 611 612 def create_wallet(self): 613 encrypt_keystore = any(k.may_have_password() for k in self.keystores) 614 # note: the following condition ("if") is duplicated logic from 615 # wallet.get_available_storage_encryption_version() 616 if self.wallet_type == 'standard' and isinstance(self.keystores[0], Hardware_KeyStore): 617 # offer encrypting with a pw derived from the hw device 618 k = self.keystores[0] # type: Hardware_KeyStore 619 assert isinstance(self.plugin, HW_PluginBase) 620 try: 621 k.handler = self.plugin.create_handler(self) 622 password = k.get_password_for_storage_encryption() 623 except UserCancelled: 624 devmgr = self.plugins.device_manager 625 devmgr.unpair_xpub(k.xpub) 626 raise ChooseHwDeviceAgain() 627 except BaseException as e: 628 self.logger.exception('') 629 self.show_error(str(e)) 630 raise ChooseHwDeviceAgain() 631 self.request_storage_encryption( 632 run_next=lambda encrypt_storage: self.on_password( 633 password, 634 encrypt_storage=encrypt_storage, 635 storage_enc_version=StorageEncryptionVersion.XPUB_PASSWORD, 636 encrypt_keystore=False)) 637 else: 638 # reset stack to disable 'back' button in password dialog 639 self.reset_stack() 640 # prompt the user to set an arbitrary password 641 self.request_password( 642 run_next=lambda password, encrypt_storage: self.on_password( 643 password, 644 encrypt_storage=encrypt_storage, 645 storage_enc_version=StorageEncryptionVersion.USER_PASSWORD, 646 encrypt_keystore=encrypt_keystore), 647 force_disable_encrypt_cb=not encrypt_keystore) 648 649 def on_password(self, password, *, encrypt_storage: bool, 650 storage_enc_version=StorageEncryptionVersion.USER_PASSWORD, 651 encrypt_keystore: bool): 652 for k in self.keystores: 653 if k.may_have_password(): 654 k.update_password(None, password) 655 if self.wallet_type == 'standard': 656 self.data['seed_type'] = self.seed_type 657 keys = self.keystores[0].dump() 658 self.data['keystore'] = keys 659 elif self.wallet_type == 'multisig': 660 for i, k in enumerate(self.keystores): 661 self.data['x%d/'%(i+1)] = k.dump() 662 elif self.wallet_type == 'imported': 663 if len(self.keystores) > 0: 664 keys = self.keystores[0].dump() 665 self.data['keystore'] = keys 666 else: 667 raise Exception('Unknown wallet type') 668 self.pw_args = WizardWalletPasswordSetting(password=password, 669 encrypt_storage=encrypt_storage, 670 storage_enc_version=storage_enc_version, 671 encrypt_keystore=encrypt_keystore) 672 self.terminate() 673 674 def create_storage(self, path) -> Tuple[WalletStorage, WalletDB]: 675 if os.path.exists(path): 676 raise Exception('file already exists at path') 677 assert self.pw_args, f"pw_args not set?!" 678 pw_args = self.pw_args 679 self.pw_args = None # clean-up so that it can get GC-ed 680 storage = WalletStorage(path) 681 if pw_args.encrypt_storage: 682 storage.set_password(pw_args.password, enc_version=pw_args.storage_enc_version) 683 db = WalletDB('', manual_upgrades=False) 684 db.set_keystore_encryption(bool(pw_args.password) and pw_args.encrypt_keystore) 685 for key, value in self.data.items(): 686 db.put(key, value) 687 db.load_plugins() 688 db.write(storage) 689 return storage, db 690 691 def terminate(self, *, storage: WalletStorage = None, 692 db: WalletDB = None, 693 aborted: bool = False) -> None: 694 raise NotImplementedError() # implemented by subclasses 695 696 def show_xpub_and_add_cosigners(self, xpub): 697 self.show_xpub_dialog(xpub=xpub, run_next=lambda x: self.run('choose_keystore')) 698 699 def choose_seed_type(self): 700 seed_type = 'standard' if self.config.get('nosegwit') else 'segwit' 701 self.create_seed(seed_type) 702 703 def create_seed(self, seed_type): 704 from . import mnemonic 705 self.seed_type = seed_type 706 seed = mnemonic.Mnemonic('en').make_seed(seed_type=self.seed_type) 707 self.opt_bip39 = False 708 self.opt_ext = True 709 self.opt_slip39 = False 710 f = lambda x: self.request_passphrase(seed, x) 711 self.show_seed_dialog(run_next=f, seed_text=seed) 712 713 def request_passphrase(self, seed, opt_passphrase): 714 if opt_passphrase: 715 f = lambda x: self.confirm_seed(seed, x) 716 self.passphrase_dialog(run_next=f) 717 else: 718 self.run('confirm_seed', seed, '') 719 720 def confirm_seed(self, seed, passphrase): 721 f = lambda x: self.confirm_passphrase(seed, passphrase) 722 self.confirm_seed_dialog(run_next=f, seed=seed if self.config.get('debug_seed') else '', test=lambda x: x==seed) 723 724 def confirm_passphrase(self, seed, passphrase): 725 f = lambda x: self.run('create_keystore', seed, x) 726 if passphrase: 727 title = _('Confirm Seed Extension') 728 message = '\n'.join([ 729 _('Your seed extension must be saved together with your seed.'), 730 _('Please type it here.'), 731 ]) 732 self.line_dialog(run_next=f, title=title, message=message, default='', test=lambda x: x==passphrase) 733 else: 734 f('') 735 736 def show_error(self, msg: Union[str, BaseException]) -> None: 737 raise NotImplementedError() 738