electrum

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

installwizard.py (32681B)


      1 # Copyright (C) 2018 The Electrum developers
      2 # Distributed under the MIT software license, see the accompanying
      3 # file LICENCE or http://www.opensource.org/licenses/mit-license.php
      4 
      5 import os
      6 import json
      7 import sys
      8 import threading
      9 import traceback
     10 from typing import Tuple, List, Callable, NamedTuple, Optional, TYPE_CHECKING
     11 from functools import partial
     12 
     13 from PyQt5.QtCore import QRect, QEventLoop, Qt, pyqtSignal
     14 from PyQt5.QtGui import QPalette, QPen, QPainter, QPixmap
     15 from PyQt5.QtWidgets import (QWidget, QDialog, QLabel, QHBoxLayout, QMessageBox,
     16                              QVBoxLayout, QLineEdit, QFileDialog, QPushButton,
     17                              QGridLayout, QSlider, QScrollArea, QApplication)
     18 
     19 from electrum.wallet import Wallet, Abstract_Wallet
     20 from electrum.storage import WalletStorage, StorageReadWriteError
     21 from electrum.util import UserCancelled, InvalidPassword, WalletFileException, get_new_wallet_name
     22 from electrum.base_wizard import BaseWizard, HWD_SETUP_DECRYPT_WALLET, GoBack, ReRunDialog
     23 from electrum.network import Network
     24 from electrum.i18n import _
     25 
     26 from .seed_dialog import SeedLayout, KeysLayout
     27 from .network_dialog import NetworkChoiceLayout
     28 from .util import (MessageBoxMixin, Buttons, icon_path, ChoicesLayout, WWLabel,
     29                    InfoButton, char_width_in_lineedit, PasswordLineEdit)
     30 from .password_dialog import PasswordLayout, PasswordLayoutForHW, PW_NEW
     31 from .bip39_recovery_dialog import Bip39RecoveryDialog
     32 from electrum.plugin import run_hook, Plugins
     33 
     34 if TYPE_CHECKING:
     35     from electrum.simple_config import SimpleConfig
     36     from electrum.wallet_db import WalletDB
     37     from . import ElectrumGui
     38 
     39 
     40 MSG_ENTER_PASSWORD = _("Choose a password to encrypt your wallet keys.") + '\n'\
     41                      + _("Leave this field empty if you want to disable encryption.")
     42 MSG_HW_STORAGE_ENCRYPTION = _("Set wallet file encryption.") + '\n'\
     43                           + _("Your wallet file does not contain secrets, mostly just metadata. ") \
     44                           + _("It also contains your master public key that allows watching your addresses.") + '\n\n'\
     45                           + _("Note: If you enable this setting, you will need your hardware device to open your wallet.")
     46 WIF_HELP_TEXT = (_('WIF keys are typed in Electrum, based on script type.') + '\n\n' +
     47                  _('A few examples') + ':\n' +
     48                  'p2pkh:KxZcY47uGp9a...       \t-> 1DckmggQM...\n' +
     49                  'p2wpkh-p2sh:KxZcY47uGp9a... \t-> 3NhNeZQXF...\n' +
     50                  'p2wpkh:KxZcY47uGp9a...      \t-> bc1q3fjfk...')
     51 # note: full key is KxZcY47uGp9aVQAb6VVvuBs8SwHKgkSR2DbZUzjDzXf2N2GPhG9n
     52 MSG_PASSPHRASE_WARN_ISSUE4566 = _("Warning") + ": "\
     53                               + _("You have multiple consecutive whitespaces or leading/trailing "
     54                                   "whitespaces in your passphrase.") + " " \
     55                               + _("This is discouraged.") + " " \
     56                               + _("Due to a bug, old versions of Electrum will NOT be creating the "
     57                                   "same wallet as newer versions or other software.")
     58 
     59 
     60 class CosignWidget(QWidget):
     61     size = 120
     62 
     63     def __init__(self, m, n):
     64         QWidget.__init__(self)
     65         self.R = QRect(0, 0, self.size, self.size)
     66         self.setGeometry(self.R)
     67         self.setMinimumHeight(self.size)
     68         self.setMaximumHeight(self.size)
     69         self.m = m
     70         self.n = n
     71 
     72     def set_n(self, n):
     73         self.n = n
     74         self.update()
     75 
     76     def set_m(self, m):
     77         self.m = m
     78         self.update()
     79 
     80     def paintEvent(self, event):
     81         bgcolor = self.palette().color(QPalette.Background)
     82         pen = QPen(bgcolor, 7, Qt.SolidLine)
     83         qp = QPainter()
     84         qp.begin(self)
     85         qp.setPen(pen)
     86         qp.setRenderHint(QPainter.Antialiasing)
     87         qp.setBrush(Qt.gray)
     88         for i in range(self.n):
     89             alpha = int(16* 360 * i/self.n)
     90             alpha2 = int(16* 360 * 1/self.n)
     91             qp.setBrush(Qt.green if i<self.m else Qt.gray)
     92             qp.drawPie(self.R, alpha, alpha2)
     93         qp.end()
     94 
     95 
     96 
     97 def wizard_dialog(func):
     98     def func_wrapper(*args, **kwargs):
     99         run_next = kwargs['run_next']
    100         wizard = args[0]  # type: InstallWizard
    101         while True:
    102             #wizard.logger.debug(f"dialog stack. len: {len(wizard._stack)}. stack: {wizard._stack}")
    103             wizard.back_button.setText(_('Back') if wizard.can_go_back() else _('Cancel'))
    104             # current dialog
    105             try:
    106                 out = func(*args, **kwargs)
    107                 if type(out) is not tuple:
    108                     out = (out,)
    109             except GoBack:
    110                 if not wizard.can_go_back():
    111                     wizard.close()
    112                     raise UserCancelled
    113                 else:
    114                     # to go back from the current dialog, we just let the caller unroll the stack:
    115                     raise
    116             # next dialog
    117             try:
    118                 while True:
    119                     try:
    120                         run_next(*out)
    121                     except ReRunDialog:
    122                         # restore state, and then let the loop re-run next
    123                         wizard.go_back(rerun_previous=False)
    124                     else:
    125                         break
    126             except GoBack as e:
    127                 # to go back from the next dialog, we ask the wizard to restore state
    128                 wizard.go_back(rerun_previous=False)
    129                 # and we re-run the current dialog
    130                 if wizard.can_go_back():
    131                     # also rerun any calculations that might have populated the inputs to the current dialog,
    132                     # by going back to just after the *previous* dialog finished
    133                     raise ReRunDialog() from e
    134                 else:
    135                     continue
    136             else:
    137                 break
    138     return func_wrapper
    139 
    140 
    141 class WalletAlreadyOpenInMemory(Exception):
    142     def __init__(self, wallet: Abstract_Wallet):
    143         super().__init__()
    144         self.wallet = wallet
    145 
    146 
    147 # WindowModalDialog must come first as it overrides show_error
    148 class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
    149 
    150     accept_signal = pyqtSignal()
    151 
    152     def __init__(self, config: 'SimpleConfig', app: QApplication, plugins: 'Plugins', *, gui_object: 'ElectrumGui'):
    153         QDialog.__init__(self, None)
    154         BaseWizard.__init__(self, config, plugins)
    155         self.setWindowTitle('Electrum  -  ' + _('Install Wizard'))
    156         self.app = app
    157         self.config = config
    158         self.gui_thread = gui_object.gui_thread
    159         self.setMinimumSize(600, 400)
    160         self.accept_signal.connect(self.accept)
    161         self.title = QLabel()
    162         self.main_widget = QWidget()
    163         self.back_button = QPushButton(_("Back"), self)
    164         self.back_button.setText(_('Back') if self.can_go_back() else _('Cancel'))
    165         self.next_button = QPushButton(_("Next"), self)
    166         self.next_button.setDefault(True)
    167         self.logo = QLabel()
    168         self.please_wait = QLabel(_("Please wait..."))
    169         self.please_wait.setAlignment(Qt.AlignCenter)
    170         self.icon_filename = None
    171         self.loop = QEventLoop()
    172         self.rejected.connect(lambda: self.loop.exit(0))
    173         self.back_button.clicked.connect(lambda: self.loop.exit(1))
    174         self.next_button.clicked.connect(lambda: self.loop.exit(2))
    175         outer_vbox = QVBoxLayout(self)
    176         inner_vbox = QVBoxLayout()
    177         inner_vbox.addWidget(self.title)
    178         inner_vbox.addWidget(self.main_widget)
    179         inner_vbox.addStretch(1)
    180         inner_vbox.addWidget(self.please_wait)
    181         inner_vbox.addStretch(1)
    182         scroll_widget = QWidget()
    183         scroll_widget.setLayout(inner_vbox)
    184         scroll = QScrollArea()
    185         scroll.setWidget(scroll_widget)
    186         scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
    187         scroll.setWidgetResizable(True)
    188         icon_vbox = QVBoxLayout()
    189         icon_vbox.addWidget(self.logo)
    190         icon_vbox.addStretch(1)
    191         hbox = QHBoxLayout()
    192         hbox.addLayout(icon_vbox)
    193         hbox.addSpacing(5)
    194         hbox.addWidget(scroll)
    195         hbox.setStretchFactor(scroll, 1)
    196         outer_vbox.addLayout(hbox)
    197         outer_vbox.addLayout(Buttons(self.back_button, self.next_button))
    198         self.set_icon('electrum.png')
    199         self.show()
    200         self.raise_()
    201         self.refresh_gui()  # Need for QT on MacOSX.  Lame.
    202 
    203     def select_storage(self, path, get_wallet_from_daemon) -> Tuple[str, Optional[WalletStorage]]:
    204 
    205         vbox = QVBoxLayout()
    206         hbox = QHBoxLayout()
    207         hbox.addWidget(QLabel(_('Wallet') + ':'))
    208         name_e = QLineEdit()
    209         hbox.addWidget(name_e)
    210         button = QPushButton(_('Choose...'))
    211         hbox.addWidget(button)
    212         vbox.addLayout(hbox)
    213 
    214         msg_label = WWLabel('')
    215         vbox.addWidget(msg_label)
    216         hbox2 = QHBoxLayout()
    217         pw_e = PasswordLineEdit('', self)
    218         pw_e.setFixedWidth(17 * char_width_in_lineedit())
    219         pw_label = QLabel(_('Password') + ':')
    220         hbox2.addWidget(pw_label)
    221         hbox2.addWidget(pw_e)
    222         hbox2.addStretch()
    223         vbox.addLayout(hbox2)
    224 
    225         vbox.addSpacing(50)
    226         vbox_create_new = QVBoxLayout()
    227         vbox_create_new.addWidget(QLabel(_('Alternatively') + ':'), alignment=Qt.AlignLeft)
    228         button_create_new = QPushButton(_('Create New Wallet'))
    229         button_create_new.setMinimumWidth(120)
    230         vbox_create_new.addWidget(button_create_new, alignment=Qt.AlignLeft)
    231         widget_create_new = QWidget()
    232         widget_create_new.setLayout(vbox_create_new)
    233         vbox_create_new.setContentsMargins(0, 0, 0, 0)
    234         vbox.addWidget(widget_create_new)
    235 
    236         self.set_layout(vbox, title=_('Electrum wallet'))
    237 
    238         temp_storage = None  # type: Optional[WalletStorage]
    239         wallet_folder = os.path.dirname(path)
    240 
    241         def on_choose():
    242             path, __ = QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder)
    243             if path:
    244                 name_e.setText(path)
    245 
    246         def on_filename(filename):
    247             # FIXME? "filename" might contain ".." (etc) and hence sketchy path traversals are possible
    248             nonlocal temp_storage
    249             temp_storage = None
    250             msg = None
    251             if filename:
    252                 path = os.path.join(wallet_folder, filename)
    253                 wallet_from_memory = get_wallet_from_daemon(path)
    254                 try:
    255                     if wallet_from_memory:
    256                         temp_storage = wallet_from_memory.storage  # type: Optional[WalletStorage]
    257                     else:
    258                         temp_storage = WalletStorage(path)
    259                 except (StorageReadWriteError, WalletFileException) as e:
    260                     msg = _('Cannot read file') + f'\n{repr(e)}'
    261                 except Exception as e:
    262                     self.logger.exception('')
    263                     msg = _('Cannot read file') + f'\n{repr(e)}'
    264             else:
    265                 msg = _('')
    266             self.next_button.setEnabled(temp_storage is not None)
    267             user_needs_to_enter_password = False
    268             if temp_storage:
    269                 if not temp_storage.file_exists():
    270                     msg =_("This file does not exist.") + '\n' \
    271                           + _("Press 'Next' to create this wallet, or choose another file.")
    272                 elif not wallet_from_memory:
    273                     if temp_storage.is_encrypted_with_user_pw():
    274                         msg = _("This file is encrypted with a password.") + '\n' \
    275                               + _('Enter your password or choose another file.')
    276                         user_needs_to_enter_password = True
    277                     elif temp_storage.is_encrypted_with_hw_device():
    278                         msg = _("This file is encrypted using a hardware device.") + '\n' \
    279                               + _("Press 'Next' to choose device to decrypt.")
    280                     else:
    281                         msg = _("Press 'Next' to open this wallet.")
    282                 else:
    283                     msg = _("This file is already open in memory.") + "\n" \
    284                         + _("Press 'Next' to create/focus window.")
    285             if msg is None:
    286                 msg = _('Cannot read file')
    287             msg_label.setText(msg)
    288             widget_create_new.setVisible(bool(temp_storage and temp_storage.file_exists()))
    289             if user_needs_to_enter_password:
    290                 pw_label.show()
    291                 pw_e.show()
    292                 pw_e.setFocus()
    293             else:
    294                 pw_label.hide()
    295                 pw_e.hide()
    296 
    297         button.clicked.connect(on_choose)
    298         button_create_new.clicked.connect(
    299             partial(
    300                 name_e.setText,
    301                 get_new_wallet_name(wallet_folder)))
    302         name_e.textChanged.connect(on_filename)
    303         name_e.setText(os.path.basename(path))
    304 
    305         def run_user_interaction_loop():
    306             while True:
    307                 if self.loop.exec_() != 2:  # 2 = next
    308                     raise UserCancelled()
    309                 assert temp_storage
    310                 if temp_storage.file_exists() and not temp_storage.is_encrypted():
    311                     break
    312                 if not temp_storage.file_exists():
    313                     break
    314                 wallet_from_memory = get_wallet_from_daemon(temp_storage.path)
    315                 if wallet_from_memory:
    316                     raise WalletAlreadyOpenInMemory(wallet_from_memory)
    317                 if temp_storage.file_exists() and temp_storage.is_encrypted():
    318                     if temp_storage.is_encrypted_with_user_pw():
    319                         password = pw_e.text()
    320                         try:
    321                             temp_storage.decrypt(password)
    322                             break
    323                         except InvalidPassword as e:
    324                             self.show_message(title=_('Error'), msg=str(e))
    325                             continue
    326                         except BaseException as e:
    327                             self.logger.exception('')
    328                             self.show_message(title=_('Error'), msg=repr(e))
    329                             raise UserCancelled()
    330                     elif temp_storage.is_encrypted_with_hw_device():
    331                         try:
    332                             self.run('choose_hw_device', HWD_SETUP_DECRYPT_WALLET, storage=temp_storage)
    333                         except InvalidPassword as e:
    334                             self.show_message(title=_('Error'),
    335                                               msg=_('Failed to decrypt using this hardware device.') + '\n' +
    336                                                   _('If you use a passphrase, make sure it is correct.'))
    337                             self.reset_stack()
    338                             return self.select_storage(path, get_wallet_from_daemon)
    339                         except (UserCancelled, GoBack):
    340                             raise
    341                         except BaseException as e:
    342                             self.logger.exception('')
    343                             self.show_message(title=_('Error'), msg=repr(e))
    344                             raise UserCancelled()
    345                         if temp_storage.is_past_initial_decryption():
    346                             break
    347                         else:
    348                             raise UserCancelled()
    349                     else:
    350                         raise Exception('Unexpected encryption version')
    351 
    352         try:
    353             run_user_interaction_loop()
    354         finally:
    355             try:
    356                 pw_e.clear()
    357             except RuntimeError:  # wrapped C/C++ object has been deleted.
    358                 pass              # happens when decrypting with hw device
    359 
    360         return temp_storage.path, (temp_storage if temp_storage.file_exists() else None)
    361 
    362     def run_upgrades(self, storage: WalletStorage, db: 'WalletDB') -> None:
    363         path = storage.path
    364         if db.requires_split():
    365             self.hide()
    366             msg = _("The wallet '{}' contains multiple accounts, which are no longer supported since Electrum 2.7.\n\n"
    367                     "Do you want to split your wallet into multiple files?").format(path)
    368             if not self.question(msg):
    369                 return
    370             file_list = db.split_accounts(path)
    371             msg = _('Your accounts have been moved to') + ':\n' + '\n'.join(file_list) + '\n\n'+ _('Do you want to delete the old file') + ':\n' + path
    372             if self.question(msg):
    373                 os.remove(path)
    374                 self.show_warning(_('The file was removed'))
    375             # raise now, to avoid having the old storage opened
    376             raise UserCancelled()
    377 
    378         action = db.get_action()
    379         if action and db.requires_upgrade():
    380             raise WalletFileException('Incomplete wallet files cannot be upgraded.')
    381         if action:
    382             self.hide()
    383             msg = _("The file '{}' contains an incompletely created wallet.\n"
    384                     "Do you want to complete its creation now?").format(path)
    385             if not self.question(msg):
    386                 if self.question(_("Do you want to delete '{}'?").format(path)):
    387                     os.remove(path)
    388                     self.show_warning(_('The file was removed'))
    389                 return
    390             self.show()
    391             self.data = json.loads(storage.read())
    392             self.run(action)
    393             for k, v in self.data.items():
    394                 db.put(k, v)
    395             db.write(storage)
    396             return
    397 
    398         if db.requires_upgrade():
    399             self.upgrade_db(storage, db)
    400 
    401     def on_error(self, exc_info):
    402         if not isinstance(exc_info[1], UserCancelled):
    403             self.logger.error("on_error", exc_info=exc_info)
    404             self.show_error(str(exc_info[1]))
    405 
    406     def set_icon(self, filename):
    407         prior_filename, self.icon_filename = self.icon_filename, filename
    408         self.logo.setPixmap(QPixmap(icon_path(filename))
    409                             .scaledToWidth(60, mode=Qt.SmoothTransformation))
    410         return prior_filename
    411 
    412     def set_layout(self, layout, title=None, next_enabled=True):
    413         self.title.setText("<b>%s</b>"%title if title else "")
    414         self.title.setVisible(bool(title))
    415         # Get rid of any prior layout by assigning it to a temporary widget
    416         prior_layout = self.main_widget.layout()
    417         if prior_layout:
    418             QWidget().setLayout(prior_layout)
    419         self.main_widget.setLayout(layout)
    420         self.back_button.setEnabled(True)
    421         self.next_button.setEnabled(next_enabled)
    422         if next_enabled:
    423             self.next_button.setFocus()
    424         self.main_widget.setVisible(True)
    425         self.please_wait.setVisible(False)
    426 
    427     def exec_layout(self, layout, title=None, raise_on_cancel=True,
    428                         next_enabled=True, focused_widget=None):
    429         self.set_layout(layout, title, next_enabled)
    430         if focused_widget:
    431             focused_widget.setFocus()
    432         result = self.loop.exec_()
    433         if not result and raise_on_cancel:
    434             raise UserCancelled()
    435         if result == 1:
    436             raise GoBack from None
    437         self.title.setVisible(False)
    438         self.back_button.setEnabled(False)
    439         self.next_button.setEnabled(False)
    440         self.main_widget.setVisible(False)
    441         self.please_wait.setVisible(True)
    442         self.refresh_gui()
    443         return result
    444 
    445     def refresh_gui(self):
    446         # For some reason, to refresh the GUI this needs to be called twice
    447         self.app.processEvents()
    448         self.app.processEvents()
    449 
    450     def remove_from_recently_open(self, filename):
    451         self.config.remove_from_recently_open(filename)
    452 
    453     def text_input(self, title, message, is_valid, allow_multi=False):
    454         slayout = KeysLayout(parent=self, header_layout=message, is_valid=is_valid,
    455                              allow_multi=allow_multi, config=self.config)
    456         self.exec_layout(slayout, title, next_enabled=False)
    457         return slayout.get_text()
    458 
    459     def seed_input(self, title, message, is_seed, options):
    460         slayout = SeedLayout(
    461             title=message,
    462             is_seed=is_seed,
    463             options=options,
    464             parent=self,
    465             config=self.config,
    466         )
    467         self.exec_layout(slayout, title, next_enabled=False)
    468         return slayout.get_seed(), slayout.is_bip39, slayout.is_ext
    469 
    470     @wizard_dialog
    471     def add_xpub_dialog(self, title, message, is_valid, run_next, allow_multi=False, show_wif_help=False):
    472         header_layout = QHBoxLayout()
    473         label = WWLabel(message)
    474         label.setMinimumWidth(400)
    475         header_layout.addWidget(label)
    476         if show_wif_help:
    477             header_layout.addWidget(InfoButton(WIF_HELP_TEXT), alignment=Qt.AlignRight)
    478         return self.text_input(title, header_layout, is_valid, allow_multi)
    479 
    480     @wizard_dialog
    481     def add_cosigner_dialog(self, run_next, index, is_valid):
    482         title = _("Add Cosigner") + " %d"%index
    483         message = ' '.join([
    484             _('Please enter the master public key (xpub) of your cosigner.'),
    485             _('Enter their master private key (xprv) if you want to be able to sign for them.')
    486         ])
    487         return self.text_input(title, message, is_valid)
    488 
    489     @wizard_dialog
    490     def restore_seed_dialog(self, run_next, test):
    491         options = []
    492         if self.opt_ext:
    493             options.append('ext')
    494         if self.opt_bip39:
    495             options.append('bip39')
    496         title = _('Enter Seed')
    497         message = _('Please enter your seed phrase in order to restore your wallet.')
    498         return self.seed_input(title, message, test, options)
    499 
    500     @wizard_dialog
    501     def confirm_seed_dialog(self, run_next, seed, test):
    502         self.app.clipboard().clear()
    503         title = _('Confirm Seed')
    504         message = ' '.join([
    505             _('Your seed is important!'),
    506             _('If you lose your seed, your money will be permanently lost.'),
    507             _('To make sure that you have properly saved your seed, please retype it here.')
    508         ])
    509         seed, is_bip39, is_ext = self.seed_input(title, message, test, None)
    510         return seed
    511 
    512     @wizard_dialog
    513     def show_seed_dialog(self, run_next, seed_text):
    514         title = _("Your wallet generation seed is:")
    515         slayout = SeedLayout(
    516             seed=seed_text,
    517             title=title,
    518             msg=True,
    519             options=['ext'],
    520             config=self.config,
    521         )
    522         self.exec_layout(slayout)
    523         return slayout.is_ext
    524 
    525     def pw_layout(self, msg, kind, force_disable_encrypt_cb):
    526         pw_layout = PasswordLayout(
    527             msg=msg, kind=kind, OK_button=self.next_button,
    528             force_disable_encrypt_cb=force_disable_encrypt_cb)
    529         pw_layout.encrypt_cb.setChecked(True)
    530         try:
    531             self.exec_layout(pw_layout.layout(), focused_widget=pw_layout.new_pw)
    532             return pw_layout.new_password(), pw_layout.encrypt_cb.isChecked()
    533         finally:
    534             pw_layout.clear_password_fields()
    535 
    536     @wizard_dialog
    537     def request_password(self, run_next, force_disable_encrypt_cb=False):
    538         """Request the user enter a new password and confirm it.  Return
    539         the password or None for no password."""
    540         return self.pw_layout(MSG_ENTER_PASSWORD, PW_NEW, force_disable_encrypt_cb)
    541 
    542     @wizard_dialog
    543     def request_storage_encryption(self, run_next):
    544         playout = PasswordLayoutForHW(MSG_HW_STORAGE_ENCRYPTION)
    545         playout.encrypt_cb.setChecked(True)
    546         self.exec_layout(playout.layout())
    547         return playout.encrypt_cb.isChecked()
    548 
    549     @wizard_dialog
    550     def confirm_dialog(self, title, message, run_next):
    551         self.confirm(message, title)
    552 
    553     def confirm(self, message, title):
    554         label = WWLabel(message)
    555         vbox = QVBoxLayout()
    556         vbox.addWidget(label)
    557         self.exec_layout(vbox, title)
    558 
    559     @wizard_dialog
    560     def action_dialog(self, action, run_next):
    561         self.run(action)
    562 
    563     def terminate(self, **kwargs):
    564         self.accept_signal.emit()
    565 
    566     def waiting_dialog(self, task, msg, on_finished=None):
    567         label = WWLabel(msg)
    568         vbox = QVBoxLayout()
    569         vbox.addSpacing(100)
    570         label.setMinimumWidth(300)
    571         label.setAlignment(Qt.AlignCenter)
    572         vbox.addWidget(label)
    573         self.set_layout(vbox, next_enabled=False)
    574         self.back_button.setEnabled(False)
    575 
    576         t = threading.Thread(target=task)
    577         t.start()
    578         while True:
    579             t.join(1.0/60)
    580             if t.is_alive():
    581                 self.refresh_gui()
    582             else:
    583                 break
    584         if on_finished:
    585             on_finished()
    586 
    587     def run_task_without_blocking_gui(self, task, *, msg=None):
    588         assert self.gui_thread == threading.current_thread(), 'must be called from GUI thread'
    589         if msg is None:
    590             msg = _("Please wait...")
    591 
    592         exc = None  # type: Optional[Exception]
    593         res = None
    594         def task_wrapper():
    595             nonlocal exc
    596             nonlocal res
    597             try:
    598                 res = task()
    599             except Exception as e:
    600                 exc = e
    601         self.waiting_dialog(task_wrapper, msg=msg)
    602         if exc is None:
    603             return res
    604         else:
    605             raise exc
    606 
    607     @wizard_dialog
    608     def choice_dialog(self, title, message, choices, run_next):
    609         c_values = [x[0] for x in choices]
    610         c_titles = [x[1] for x in choices]
    611         clayout = ChoicesLayout(message, c_titles)
    612         vbox = QVBoxLayout()
    613         vbox.addLayout(clayout.layout())
    614         self.exec_layout(vbox, title)
    615         action = c_values[clayout.selected_index()]
    616         return action
    617 
    618     def query_choice(self, msg, choices):
    619         """called by hardware wallets"""
    620         clayout = ChoicesLayout(msg, choices)
    621         vbox = QVBoxLayout()
    622         vbox.addLayout(clayout.layout())
    623         self.exec_layout(vbox, '')
    624         return clayout.selected_index()
    625 
    626     @wizard_dialog
    627     def derivation_and_script_type_gui_specific_dialog(
    628             self,
    629             *,
    630             title: str,
    631             message1: str,
    632             choices: List[Tuple[str, str, str]],
    633             hide_choices: bool = False,
    634             message2: str,
    635             test_text: Callable[[str], int],
    636             run_next,
    637             default_choice_idx: int = 0,
    638             get_account_xpub=None,
    639     ) -> Tuple[str, str]:
    640         vbox = QVBoxLayout()
    641 
    642         if get_account_xpub:
    643             button = QPushButton(_("Detect Existing Accounts"))
    644             def on_account_select(account):
    645                 script_type = account["script_type"]
    646                 if script_type == "p2pkh":
    647                     script_type = "standard"
    648                 button_index = c_values.index(script_type)
    649                 button = clayout.group.buttons()[button_index]
    650                 button.setChecked(True)
    651                 line.setText(account["derivation_path"])
    652             button.clicked.connect(lambda: Bip39RecoveryDialog(self, get_account_xpub, on_account_select))
    653             vbox.addWidget(button, alignment=Qt.AlignLeft)
    654             vbox.addWidget(QLabel(_("Or")))
    655 
    656         c_values = [x[0] for x in choices]
    657         c_titles = [x[1] for x in choices]
    658         c_default_text = [x[2] for x in choices]
    659         def on_choice_click(clayout):
    660             idx = clayout.selected_index()
    661             line.setText(c_default_text[idx])
    662         clayout = ChoicesLayout(message1, c_titles, on_choice_click,
    663                                 checked_index=default_choice_idx)
    664         if not hide_choices:
    665             vbox.addLayout(clayout.layout())
    666 
    667         vbox.addWidget(WWLabel(message2))
    668 
    669         line = QLineEdit()
    670         def on_text_change(text):
    671             self.next_button.setEnabled(test_text(text))
    672         line.textEdited.connect(on_text_change)
    673         on_choice_click(clayout)  # set default text for "line"
    674         vbox.addWidget(line)
    675 
    676         self.exec_layout(vbox, title)
    677         choice = c_values[clayout.selected_index()]
    678         return str(line.text()), choice
    679 
    680     @wizard_dialog
    681     def line_dialog(self, run_next, title, message, default, test, warning='',
    682                     presets=(), warn_issue4566=False):
    683         vbox = QVBoxLayout()
    684         vbox.addWidget(WWLabel(message))
    685         line = QLineEdit()
    686         line.setText(default)
    687         def f(text):
    688             self.next_button.setEnabled(test(text))
    689             if warn_issue4566:
    690                 text_whitespace_normalised = ' '.join(text.split())
    691                 warn_issue4566_label.setVisible(text != text_whitespace_normalised)
    692         line.textEdited.connect(f)
    693         vbox.addWidget(line)
    694         vbox.addWidget(WWLabel(warning))
    695 
    696         warn_issue4566_label = WWLabel(MSG_PASSPHRASE_WARN_ISSUE4566)
    697         warn_issue4566_label.setVisible(False)
    698         vbox.addWidget(warn_issue4566_label)
    699 
    700         for preset in presets:
    701             button = QPushButton(preset[0])
    702             button.clicked.connect(lambda __, text=preset[1]: line.setText(text))
    703             button.setMinimumWidth(150)
    704             hbox = QHBoxLayout()
    705             hbox.addWidget(button, alignment=Qt.AlignCenter)
    706             vbox.addLayout(hbox)
    707 
    708         self.exec_layout(vbox, title, next_enabled=test(default))
    709         return line.text()
    710 
    711     @wizard_dialog
    712     def show_xpub_dialog(self, xpub, run_next):
    713         msg = ' '.join([
    714             _("Here is your master public key."),
    715             _("Please share it with your cosigners.")
    716         ])
    717         vbox = QVBoxLayout()
    718         layout = SeedLayout(
    719             xpub,
    720             title=msg,
    721             icon=False,
    722             for_seed_words=False,
    723             config=self.config,
    724         )
    725         vbox.addLayout(layout.layout())
    726         self.exec_layout(vbox, _('Master Public Key'))
    727         return None
    728 
    729     def init_network(self, network: 'Network'):
    730         message = _("Electrum communicates with remote servers to get "
    731                   "information about your transactions and addresses. The "
    732                   "servers all fulfill the same purpose only differing in "
    733                   "hardware. In most cases you simply want to let Electrum "
    734                   "pick one at random.  However if you prefer feel free to "
    735                   "select a server manually.")
    736         choices = [_("Auto connect"), _("Select server manually")]
    737         title = _("How do you want to connect to a server? ")
    738         clayout = ChoicesLayout(message, choices)
    739         self.back_button.setText(_('Cancel'))
    740         self.exec_layout(clayout.layout(), title)
    741         r = clayout.selected_index()
    742         if r == 1:
    743             nlayout = NetworkChoiceLayout(network, self.config, wizard=True)
    744             if self.exec_layout(nlayout.layout()):
    745                 nlayout.accept()
    746                 self.config.set_key('auto_connect', network.auto_connect, True)
    747         else:
    748             network.auto_connect = True
    749             self.config.set_key('auto_connect', True, True)
    750 
    751     @wizard_dialog
    752     def multisig_dialog(self, run_next):
    753         cw = CosignWidget(2, 2)
    754         m_edit = QSlider(Qt.Horizontal, self)
    755         n_edit = QSlider(Qt.Horizontal, self)
    756         n_edit.setMinimum(2)
    757         n_edit.setMaximum(15)
    758         m_edit.setMinimum(1)
    759         m_edit.setMaximum(2)
    760         n_edit.setValue(2)
    761         m_edit.setValue(2)
    762         n_label = QLabel()
    763         m_label = QLabel()
    764         grid = QGridLayout()
    765         grid.addWidget(n_label, 0, 0)
    766         grid.addWidget(n_edit, 0, 1)
    767         grid.addWidget(m_label, 1, 0)
    768         grid.addWidget(m_edit, 1, 1)
    769         def on_m(m):
    770             m_label.setText(_('Require {0} signatures').format(m))
    771             cw.set_m(m)
    772             backup_warning_label.setVisible(cw.m != cw.n)
    773         def on_n(n):
    774             n_label.setText(_('From {0} cosigners').format(n))
    775             cw.set_n(n)
    776             m_edit.setMaximum(n)
    777             backup_warning_label.setVisible(cw.m != cw.n)
    778         n_edit.valueChanged.connect(on_n)
    779         m_edit.valueChanged.connect(on_m)
    780         vbox = QVBoxLayout()
    781         vbox.addWidget(cw)
    782         vbox.addWidget(WWLabel(_("Choose the number of signatures needed to unlock funds in your wallet:")))
    783         vbox.addLayout(grid)
    784         vbox.addSpacing(2 * char_width_in_lineedit())
    785         backup_warning_label = WWLabel(_("Warning: to be able to restore a multisig wallet, "
    786                                          "you should include the master public key for each cosigner "
    787                                          "in all of your backups."))
    788         vbox.addWidget(backup_warning_label)
    789         on_n(2)
    790         on_m(2)
    791         self.exec_layout(vbox, _("Multi-Signature Wallet"))
    792         m = int(m_edit.value())
    793         n = int(n_edit.value())
    794         return (m, n)