electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

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()