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)