URI: 
       tinstallwizard.py - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
       tinstallwizard.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)