electrum

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

qt.py (30784B)


      1 from functools import partial
      2 import threading
      3 
      4 from PyQt5.QtCore import Qt, QEventLoop, pyqtSignal
      5 from PyQt5.QtWidgets import (QVBoxLayout, QLabel, QGridLayout, QPushButton,
      6                              QHBoxLayout, QButtonGroup, QGroupBox, QDialog,
      7                              QLineEdit, QRadioButton, QCheckBox, QWidget,
      8                              QMessageBox, QFileDialog, QSlider, QTabWidget)
      9 
     10 from electrum.gui.qt.util import (WindowModalDialog, WWLabel, Buttons, CancelButton,
     11                                   OkButton, CloseButton, PasswordLineEdit, getOpenFileName)
     12 from electrum.i18n import _
     13 from electrum.plugin import hook
     14 from electrum.util import bh2u
     15 
     16 from ..hw_wallet.qt import QtHandlerBase, QtPluginBase
     17 from ..hw_wallet.plugin import only_hook_if_libraries_available
     18 from .trezor import (TrezorPlugin, TIM_NEW, TIM_RECOVER, TrezorInitSettings,
     19                      PASSPHRASE_ON_DEVICE, Capability, BackupType, RecoveryDeviceType)
     20 
     21 
     22 PASSPHRASE_HELP_SHORT =_(
     23     "Passphrases allow you to access new wallets, each "
     24     "hidden behind a particular case-sensitive passphrase.")
     25 PASSPHRASE_HELP = PASSPHRASE_HELP_SHORT + "  " + _(
     26     "You need to create a separate Electrum wallet for each passphrase "
     27     "you use as they each generate different addresses.  Changing "
     28     "your passphrase does not lose other wallets, each is still "
     29     "accessible behind its own passphrase.")
     30 RECOMMEND_PIN = _(
     31     "You should enable PIN protection.  Your PIN is the only protection "
     32     "for your bitcoins if your device is lost or stolen.")
     33 PASSPHRASE_NOT_PIN = _(
     34     "If you forget a passphrase you will be unable to access any "
     35     "bitcoins in the wallet behind it.  A passphrase is not a PIN. "
     36     "Only change this if you are sure you understand it.")
     37 MATRIX_RECOVERY = _(
     38     "Enter the recovery words by pressing the buttons according to what "
     39     "the device shows on its display.  You can also use your NUMPAD.\n"
     40     "Press BACKSPACE to go back a choice or word.\n")
     41 SEEDLESS_MODE_WARNING = _(
     42     "In seedless mode, the mnemonic seed words are never shown to the user.\n"
     43     "There is no backup, and the user has a proof of this.\n"
     44     "This is an advanced feature, only suggested to be used in redundant multisig setups.")
     45 
     46 
     47 class MatrixDialog(WindowModalDialog):
     48 
     49     def __init__(self, parent):
     50         super(MatrixDialog, self).__init__(parent)
     51         self.setWindowTitle(_("Trezor Matrix Recovery"))
     52         self.num = 9
     53         self.loop = QEventLoop()
     54 
     55         vbox = QVBoxLayout(self)
     56         vbox.addWidget(WWLabel(MATRIX_RECOVERY))
     57 
     58         grid = QGridLayout()
     59         grid.setSpacing(0)
     60         self.char_buttons = []
     61         for y in range(3):
     62             for x in range(3):
     63                 button = QPushButton('?')
     64                 button.clicked.connect(partial(self.process_key, ord('1') + y * 3 + x))
     65                 grid.addWidget(button, 3 - y, x)
     66                 self.char_buttons.append(button)
     67         vbox.addLayout(grid)
     68 
     69         self.backspace_button = QPushButton("<=")
     70         self.backspace_button.clicked.connect(partial(self.process_key, Qt.Key_Backspace))
     71         self.cancel_button = QPushButton(_("Cancel"))
     72         self.cancel_button.clicked.connect(partial(self.process_key, Qt.Key_Escape))
     73         buttons = Buttons(self.backspace_button, self.cancel_button)
     74         vbox.addSpacing(40)
     75         vbox.addLayout(buttons)
     76         self.refresh()
     77         self.show()
     78 
     79     def refresh(self):
     80         for y in range(3):
     81             self.char_buttons[3 * y + 1].setEnabled(self.num == 9)
     82 
     83     def is_valid(self, key):
     84         return key >= ord('1') and key <= ord('9')
     85 
     86     def process_key(self, key):
     87         self.data = None
     88         if key == Qt.Key_Backspace:
     89             self.data = '\010'
     90         elif key == Qt.Key_Escape:
     91             self.data = 'x'
     92         elif self.is_valid(key):
     93             self.char_buttons[key - ord('1')].setFocus()
     94             self.data = '%c' % key
     95         if self.data:
     96             self.loop.exit(0)
     97 
     98     def keyPressEvent(self, event):
     99         self.process_key(event.key())
    100         if not self.data:
    101             QDialog.keyPressEvent(self, event)
    102 
    103     def get_matrix(self, num):
    104         self.num = num
    105         self.refresh()
    106         self.loop.exec_()
    107 
    108 
    109 class QtHandler(QtHandlerBase):
    110 
    111     pin_signal = pyqtSignal(object, object)
    112     matrix_signal = pyqtSignal(object)
    113     close_matrix_dialog_signal = pyqtSignal()
    114 
    115     def __init__(self, win, pin_matrix_widget_class, device):
    116         super(QtHandler, self).__init__(win, device)
    117         self.pin_signal.connect(self.pin_dialog)
    118         self.matrix_signal.connect(self.matrix_recovery_dialog)
    119         self.close_matrix_dialog_signal.connect(self._close_matrix_dialog)
    120         self.pin_matrix_widget_class = pin_matrix_widget_class
    121         self.matrix_dialog = None
    122         self.passphrase_on_device = False
    123 
    124     def get_pin(self, msg, *, show_strength=True):
    125         self.done.clear()
    126         self.pin_signal.emit(msg, show_strength)
    127         self.done.wait()
    128         return self.response
    129 
    130     def get_matrix(self, msg):
    131         self.done.clear()
    132         self.matrix_signal.emit(msg)
    133         self.done.wait()
    134         data = self.matrix_dialog.data
    135         if data == 'x':
    136             self.close_matrix_dialog()
    137         return data
    138 
    139     def _close_matrix_dialog(self):
    140         if self.matrix_dialog:
    141             self.matrix_dialog.accept()
    142             self.matrix_dialog = None
    143 
    144     def close_matrix_dialog(self):
    145         self.close_matrix_dialog_signal.emit()
    146 
    147     def pin_dialog(self, msg, show_strength):
    148         # Needed e.g. when resetting a device
    149         self.clear_dialog()
    150         dialog = WindowModalDialog(self.top_level_window(), _("Enter PIN"))
    151         matrix = self.pin_matrix_widget_class(show_strength)
    152         vbox = QVBoxLayout()
    153         vbox.addWidget(QLabel(msg))
    154         vbox.addWidget(matrix)
    155         vbox.addLayout(Buttons(CancelButton(dialog), OkButton(dialog)))
    156         dialog.setLayout(vbox)
    157         dialog.exec_()
    158         self.response = str(matrix.get_value())
    159         self.done.set()
    160 
    161     def matrix_recovery_dialog(self, msg):
    162         if not self.matrix_dialog:
    163             self.matrix_dialog = MatrixDialog(self.top_level_window())
    164         self.matrix_dialog.get_matrix(msg)
    165         self.done.set()
    166 
    167     def passphrase_dialog(self, msg, confirm):
    168         # If confirm is true, require the user to enter the passphrase twice
    169         parent = self.top_level_window()
    170         d = WindowModalDialog(parent, _('Enter Passphrase'))
    171 
    172         OK_button = OkButton(d, _('Enter Passphrase'))
    173         OnDevice_button = QPushButton(_('Enter Passphrase on Device'))
    174 
    175         new_pw = PasswordLineEdit()
    176         conf_pw = PasswordLineEdit()
    177 
    178         vbox = QVBoxLayout()
    179         label = QLabel(msg + "\n")
    180         label.setWordWrap(True)
    181 
    182         grid = QGridLayout()
    183         grid.setSpacing(8)
    184         grid.setColumnMinimumWidth(0, 150)
    185         grid.setColumnMinimumWidth(1, 100)
    186         grid.setColumnStretch(1,1)
    187 
    188         vbox.addWidget(label)
    189 
    190         grid.addWidget(QLabel(_('Passphrase:')), 0, 0)
    191         grid.addWidget(new_pw, 0, 1)
    192 
    193         if confirm:
    194             grid.addWidget(QLabel(_('Confirm Passphrase:')), 1, 0)
    195             grid.addWidget(conf_pw, 1, 1)
    196 
    197         vbox.addLayout(grid)
    198 
    199         def enable_OK():
    200             if not confirm:
    201                 ok = True
    202             else:
    203                 ok = new_pw.text() == conf_pw.text()
    204             OK_button.setEnabled(ok)
    205 
    206         new_pw.textChanged.connect(enable_OK)
    207         conf_pw.textChanged.connect(enable_OK)
    208 
    209         vbox.addWidget(OK_button)
    210 
    211         if self.passphrase_on_device:
    212             vbox.addWidget(OnDevice_button)
    213 
    214         d.setLayout(vbox)
    215 
    216         self.passphrase = None
    217 
    218         def ok_clicked():
    219             self.passphrase = new_pw.text()
    220 
    221         def on_device_clicked():
    222             self.passphrase = PASSPHRASE_ON_DEVICE
    223 
    224         OK_button.clicked.connect(ok_clicked)
    225         OnDevice_button.clicked.connect(on_device_clicked)
    226         OnDevice_button.clicked.connect(d.accept)
    227 
    228         d.exec_()
    229         self.done.set()
    230 
    231 
    232 class QtPlugin(QtPluginBase):
    233     # Derived classes must provide the following class-static variables:
    234     #   icon_file
    235     #   pin_matrix_widget_class
    236 
    237     @only_hook_if_libraries_available
    238     @hook
    239     def receive_menu(self, menu, addrs, wallet):
    240         if len(addrs) != 1:
    241             return
    242         for keystore in wallet.get_keystores():
    243             if type(keystore) == self.keystore_class:
    244                 def show_address(keystore=keystore):
    245                     keystore.thread.add(partial(self.show_address, wallet, addrs[0], keystore))
    246                 device_name = "{} ({})".format(self.device, keystore.label)
    247                 menu.addAction(_("Show on {}").format(device_name), show_address)
    248 
    249     def show_settings_dialog(self, window, keystore):
    250         def connect():
    251             device_id = self.choose_device(window, keystore)
    252             return device_id
    253         def show_dialog(device_id):
    254             if device_id:
    255                 SettingsDialog(window, self, keystore, device_id).exec_()
    256         keystore.thread.add(connect, on_success=show_dialog)
    257 
    258     def request_trezor_init_settings(self, wizard, method, device_id):
    259         vbox = QVBoxLayout()
    260         next_enabled = True
    261 
    262         devmgr = self.device_manager()
    263         client = devmgr.client_by_id(device_id)
    264         if not client:
    265             raise Exception(_("The device was disconnected."))
    266         model = client.get_trezor_model()
    267         fw_version = client.client.version
    268         capabilities = client.client.features.capabilities
    269         have_shamir = Capability.Shamir in capabilities
    270 
    271         # label
    272         label = QLabel(_("Enter a label to name your device:"))
    273         name = QLineEdit()
    274         hl = QHBoxLayout()
    275         hl.addWidget(label)
    276         hl.addWidget(name)
    277         hl.addStretch(1)
    278         vbox.addLayout(hl)
    279 
    280         # Backup type
    281         gb_backuptype = QGroupBox()
    282         hbox_backuptype = QHBoxLayout()
    283         gb_backuptype.setLayout(hbox_backuptype)
    284         vbox.addWidget(gb_backuptype)
    285         gb_backuptype.setTitle(_('Select backup type:'))
    286         bg_backuptype = QButtonGroup()
    287 
    288         rb_single = QRadioButton(gb_backuptype)
    289         rb_single.setText(_('Single seed (BIP39)'))
    290         bg_backuptype.addButton(rb_single)
    291         bg_backuptype.setId(rb_single, BackupType.Bip39)
    292         hbox_backuptype.addWidget(rb_single)
    293         rb_single.setChecked(True)
    294 
    295         rb_shamir = QRadioButton(gb_backuptype)
    296         rb_shamir.setText(_('Shamir'))
    297         bg_backuptype.addButton(rb_shamir)
    298         bg_backuptype.setId(rb_shamir, BackupType.Slip39_Basic)
    299         hbox_backuptype.addWidget(rb_shamir)
    300         rb_shamir.setEnabled(Capability.Shamir in capabilities)
    301         rb_shamir.setVisible(False)  # visible with "expert settings"
    302 
    303         rb_shamir_groups = QRadioButton(gb_backuptype)
    304         rb_shamir_groups.setText(_('Super Shamir'))
    305         bg_backuptype.addButton(rb_shamir_groups)
    306         bg_backuptype.setId(rb_shamir_groups, BackupType.Slip39_Advanced)
    307         hbox_backuptype.addWidget(rb_shamir_groups)
    308         rb_shamir_groups.setEnabled(Capability.ShamirGroups in capabilities)
    309         rb_shamir_groups.setVisible(False)  # visible with "expert settings"
    310 
    311         # word count
    312         word_count_buttons = {}
    313 
    314         gb_numwords = QGroupBox()
    315         hbox1 = QHBoxLayout()
    316         gb_numwords.setLayout(hbox1)
    317         vbox.addWidget(gb_numwords)
    318         gb_numwords.setTitle(_("Select seed/share length:"))
    319         bg_numwords = QButtonGroup()
    320         for count in (12, 18, 20, 24, 33):
    321             rb = QRadioButton(gb_numwords)
    322             word_count_buttons[count] = rb
    323             rb.setText(_("{:d} words").format(count))
    324             bg_numwords.addButton(rb)
    325             bg_numwords.setId(rb, count)
    326             hbox1.addWidget(rb)
    327             rb.setChecked(True)
    328 
    329         def configure_word_counts():
    330             if model == "1":
    331                 checked_wordcount = 24
    332             else:
    333                 checked_wordcount = 12
    334 
    335             if method == TIM_RECOVER:
    336                 if have_shamir:
    337                     valid_word_counts = (12, 18, 20, 24, 33)
    338                 else:
    339                     valid_word_counts = (12, 18, 24)
    340             elif rb_single.isChecked():
    341                 valid_word_counts = (12, 18, 24)
    342                 gb_numwords.setTitle(_('Select seed length:'))
    343             else:
    344                 valid_word_counts = (20, 33)
    345                 checked_wordcount = 20
    346                 gb_numwords.setTitle(_('Select share length:'))
    347 
    348             word_count_buttons[checked_wordcount].setChecked(True)
    349             for c, btn in word_count_buttons.items():
    350                 btn.setVisible(c in valid_word_counts)
    351 
    352         bg_backuptype.buttonClicked.connect(configure_word_counts)
    353         configure_word_counts()
    354 
    355         # set up conditional visibility:
    356         # 1. backup_type is only visible when creating new seed
    357         gb_backuptype.setVisible(method == TIM_NEW)
    358         # 2. word_count is not visible when recovering on TT
    359         if method == TIM_RECOVER and model != "1":
    360             gb_numwords.setVisible(False)
    361 
    362         # PIN
    363         cb_pin = QCheckBox(_('Enable PIN protection'))
    364         cb_pin.setChecked(True)
    365         vbox.addWidget(WWLabel(RECOMMEND_PIN))
    366         vbox.addWidget(cb_pin)
    367 
    368         # "expert settings" button
    369         expert_vbox = QVBoxLayout()
    370         expert_widget = QWidget()
    371         expert_widget.setLayout(expert_vbox)
    372         expert_widget.setVisible(False)
    373         expert_button = QPushButton(_("Show expert settings"))
    374         def show_expert_settings():
    375             expert_button.setVisible(False)
    376             expert_widget.setVisible(True)
    377             rb_shamir.setVisible(True)
    378             rb_shamir_groups.setVisible(True)
    379         expert_button.clicked.connect(show_expert_settings)
    380         vbox.addWidget(expert_button)
    381 
    382         # passphrase
    383         passphrase_msg = WWLabel(PASSPHRASE_HELP_SHORT)
    384         passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN)
    385         passphrase_warning.setStyleSheet("color: red")
    386         cb_phrase = QCheckBox(_('Enable passphrases'))
    387         cb_phrase.setChecked(False)
    388         expert_vbox.addWidget(passphrase_msg)
    389         expert_vbox.addWidget(passphrase_warning)
    390         expert_vbox.addWidget(cb_phrase)
    391 
    392         # ask for recovery type (random word order OR matrix)
    393         bg_rectype = None
    394         if method == TIM_RECOVER and model == '1':
    395             gb_rectype = QGroupBox()
    396             hbox_rectype = QHBoxLayout()
    397             gb_rectype.setLayout(hbox_rectype)
    398             expert_vbox.addWidget(gb_rectype)
    399             gb_rectype.setTitle(_("Select recovery type:"))
    400             bg_rectype = QButtonGroup()
    401 
    402             rb1 = QRadioButton(gb_rectype)
    403             rb1.setText(_('Scrambled words'))
    404             bg_rectype.addButton(rb1)
    405             bg_rectype.setId(rb1, RecoveryDeviceType.ScrambledWords)
    406             hbox_rectype.addWidget(rb1)
    407             rb1.setChecked(True)
    408 
    409             rb2 = QRadioButton(gb_rectype)
    410             rb2.setText(_('Matrix'))
    411             bg_rectype.addButton(rb2)
    412             bg_rectype.setId(rb2, RecoveryDeviceType.Matrix)
    413             hbox_rectype.addWidget(rb2)
    414 
    415         # no backup
    416         cb_no_backup = None
    417         if method == TIM_NEW:
    418             cb_no_backup = QCheckBox(f'''{_('Enable seedless mode')}''')
    419             cb_no_backup.setChecked(False)
    420             if (model == '1' and fw_version >= (1, 7, 1)
    421                     or model == 'T' and fw_version >= (2, 0, 9)):
    422                 cb_no_backup.setToolTip(SEEDLESS_MODE_WARNING)
    423             else:
    424                 cb_no_backup.setEnabled(False)
    425                 cb_no_backup.setToolTip(_('Firmware version too old.'))
    426             expert_vbox.addWidget(cb_no_backup)
    427 
    428         vbox.addWidget(expert_widget)
    429         wizard.exec_layout(vbox, next_enabled=next_enabled)
    430 
    431         return TrezorInitSettings(
    432             word_count=bg_numwords.checkedId(),
    433             label=name.text(),
    434             pin_enabled=cb_pin.isChecked(),
    435             passphrase_enabled=cb_phrase.isChecked(),
    436             recovery_type=bg_rectype.checkedId() if bg_rectype else None,
    437             backup_type=bg_backuptype.checkedId(),
    438             no_backup=cb_no_backup.isChecked() if cb_no_backup else False,
    439         )
    440 
    441 
    442 class Plugin(TrezorPlugin, QtPlugin):
    443     icon_unpaired = "trezor_unpaired.png"
    444     icon_paired = "trezor.png"
    445 
    446     def create_handler(self, window):
    447         return QtHandler(window, self.pin_matrix_widget_class(), self.device)
    448 
    449     @classmethod
    450     def pin_matrix_widget_class(self):
    451         from trezorlib.qt.pinmatrix import PinMatrixWidget
    452         return PinMatrixWidget
    453 
    454 
    455 class SettingsDialog(WindowModalDialog):
    456     '''This dialog doesn't require a device be paired with a wallet.
    457     We want users to be able to wipe a device even if they've forgotten
    458     their PIN.'''
    459 
    460     def __init__(self, window, plugin, keystore, device_id):
    461         title = _("{} Settings").format(plugin.device)
    462         super(SettingsDialog, self).__init__(window, title)
    463         self.setMaximumWidth(540)
    464 
    465         devmgr = plugin.device_manager()
    466         config = devmgr.config
    467         handler = keystore.handler
    468         thread = keystore.thread
    469         hs_cols, hs_rows = (128, 64)
    470 
    471         def invoke_client(method, *args, **kw_args):
    472             unpair_after = kw_args.pop('unpair_after', False)
    473 
    474             def task():
    475                 client = devmgr.client_by_id(device_id)
    476                 if not client:
    477                     raise RuntimeError("Device not connected")
    478                 if method:
    479                     getattr(client, method)(*args, **kw_args)
    480                 if unpair_after:
    481                     devmgr.unpair_id(device_id)
    482                 return client.features
    483 
    484             thread.add(task, on_success=update)
    485 
    486         def update(features):
    487             self.features = features
    488             set_label_enabled()
    489             if features.bootloader_hash:
    490                 bl_hash = bh2u(features.bootloader_hash)
    491                 bl_hash = "\n".join([bl_hash[:32], bl_hash[32:]])
    492             else:
    493                 bl_hash = "N/A"
    494             noyes = [_("No"), _("Yes")]
    495             endis = [_("Enable Passphrases"), _("Disable Passphrases")]
    496             disen = [_("Disabled"), _("Enabled")]
    497             setchange = [_("Set a PIN"), _("Change PIN")]
    498 
    499             version = "%d.%d.%d" % (features.major_version,
    500                                     features.minor_version,
    501                                     features.patch_version)
    502 
    503             device_label.setText(features.label)
    504             pin_set_label.setText(noyes[features.pin_protection])
    505             passphrases_label.setText(disen[features.passphrase_protection])
    506             bl_hash_label.setText(bl_hash)
    507             label_edit.setText(features.label)
    508             device_id_label.setText(features.device_id)
    509             initialized_label.setText(noyes[features.initialized])
    510             version_label.setText(version)
    511             clear_pin_button.setVisible(features.pin_protection)
    512             clear_pin_warning.setVisible(features.pin_protection)
    513             pin_button.setText(setchange[features.pin_protection])
    514             pin_msg.setVisible(not features.pin_protection)
    515             passphrase_button.setText(endis[features.passphrase_protection])
    516             language_label.setText(features.language)
    517 
    518         def set_label_enabled():
    519             label_apply.setEnabled(label_edit.text() != self.features.label)
    520 
    521         def rename():
    522             invoke_client('change_label', label_edit.text())
    523 
    524         def toggle_passphrase():
    525             title = _("Confirm Toggle Passphrase Protection")
    526             currently_enabled = self.features.passphrase_protection
    527             if currently_enabled:
    528                 msg = _("After disabling passphrases, you can only pair this "
    529                         "Electrum wallet if it had an empty passphrase.  "
    530                         "If its passphrase was not empty, you will need to "
    531                         "create a new wallet with the install wizard.  You "
    532                         "can use this wallet again at any time by re-enabling "
    533                         "passphrases and entering its passphrase.")
    534             else:
    535                 msg = _("Your current Electrum wallet can only be used with "
    536                         "an empty passphrase.  You must create a separate "
    537                         "wallet with the install wizard for other passphrases "
    538                         "as each one generates a new set of addresses.")
    539             msg += "\n\n" + _("Are you sure you want to proceed?")
    540             if not self.question(msg, title=title):
    541                 return
    542             invoke_client('toggle_passphrase', unpair_after=currently_enabled)
    543 
    544         def change_homescreen():
    545             filename = getOpenFileName(
    546                 parent=self,
    547                 title=_("Choose Homescreen"),
    548                 config=config,
    549             )
    550             if not filename:
    551                 return  # user cancelled
    552 
    553             if filename.endswith('.toif'):
    554                 img = open(filename, 'rb').read()
    555                 if img[:8] != b'TOIf\x90\x00\x90\x00':
    556                     handler.show_error('File is not a TOIF file with size of 144x144')
    557                     return
    558             else:
    559                 from PIL import Image # FIXME
    560                 im = Image.open(filename)
    561                 if im.size != (128, 64):
    562                     handler.show_error('Image must be 128 x 64 pixels')
    563                     return
    564                 im = im.convert('1')
    565                 pix = im.load()
    566                 img = bytearray(1024)
    567                 for j in range(64):
    568                     for i in range(128):
    569                         if pix[i, j]:
    570                             o = (i + j * 128)
    571                             img[o // 8] |= (1 << (7 - o % 8))
    572                 img = bytes(img)
    573             invoke_client('change_homescreen', img)
    574 
    575         def clear_homescreen():
    576             invoke_client('change_homescreen', b'\x00')
    577 
    578         def set_pin():
    579             invoke_client('set_pin', remove=False)
    580 
    581         def clear_pin():
    582             invoke_client('set_pin', remove=True)
    583 
    584         def wipe_device():
    585             wallet = window.wallet
    586             if wallet and sum(wallet.get_balance()):
    587                 title = _("Confirm Device Wipe")
    588                 msg = _("Are you SURE you want to wipe the device?\n"
    589                         "Your wallet still has bitcoins in it!")
    590                 if not self.question(msg, title=title,
    591                                      icon=QMessageBox.Critical):
    592                     return
    593             invoke_client('wipe_device', unpair_after=True)
    594 
    595         def slider_moved():
    596             mins = timeout_slider.sliderPosition()
    597             timeout_minutes.setText(_("{:2d} minutes").format(mins))
    598 
    599         def slider_released():
    600             config.set_session_timeout(timeout_slider.sliderPosition() * 60)
    601 
    602         # Information tab
    603         info_tab = QWidget()
    604         info_layout = QVBoxLayout(info_tab)
    605         info_glayout = QGridLayout()
    606         info_glayout.setColumnStretch(2, 1)
    607         device_label = QLabel()
    608         pin_set_label = QLabel()
    609         passphrases_label = QLabel()
    610         version_label = QLabel()
    611         device_id_label = QLabel()
    612         bl_hash_label = QLabel()
    613         bl_hash_label.setWordWrap(True)
    614         language_label = QLabel()
    615         initialized_label = QLabel()
    616         rows = [
    617             (_("Device Label"), device_label),
    618             (_("PIN set"), pin_set_label),
    619             (_("Passphrases"), passphrases_label),
    620             (_("Firmware Version"), version_label),
    621             (_("Device ID"), device_id_label),
    622             (_("Bootloader Hash"), bl_hash_label),
    623             (_("Language"), language_label),
    624             (_("Initialized"), initialized_label),
    625         ]
    626         for row_num, (label, widget) in enumerate(rows):
    627             info_glayout.addWidget(QLabel(label), row_num, 0)
    628             info_glayout.addWidget(widget, row_num, 1)
    629         info_layout.addLayout(info_glayout)
    630 
    631         # Settings tab
    632         settings_tab = QWidget()
    633         settings_layout = QVBoxLayout(settings_tab)
    634         settings_glayout = QGridLayout()
    635 
    636         # Settings tab - Label
    637         label_msg = QLabel(_("Name this {}.  If you have multiple devices "
    638                              "their labels help distinguish them.")
    639                            .format(plugin.device))
    640         label_msg.setWordWrap(True)
    641         label_label = QLabel(_("Device Label"))
    642         label_edit = QLineEdit()
    643         label_edit.setMinimumWidth(150)
    644         label_edit.setMaxLength(plugin.MAX_LABEL_LEN)
    645         label_apply = QPushButton(_("Apply"))
    646         label_apply.clicked.connect(rename)
    647         label_edit.textChanged.connect(set_label_enabled)
    648         settings_glayout.addWidget(label_label, 0, 0)
    649         settings_glayout.addWidget(label_edit, 0, 1, 1, 2)
    650         settings_glayout.addWidget(label_apply, 0, 3)
    651         settings_glayout.addWidget(label_msg, 1, 1, 1, -1)
    652 
    653         # Settings tab - PIN
    654         pin_label = QLabel(_("PIN Protection"))
    655         pin_button = QPushButton()
    656         pin_button.clicked.connect(set_pin)
    657         settings_glayout.addWidget(pin_label, 2, 0)
    658         settings_glayout.addWidget(pin_button, 2, 1)
    659         pin_msg = QLabel(_("PIN protection is strongly recommended.  "
    660                            "A PIN is your only protection against someone "
    661                            "stealing your bitcoins if they obtain physical "
    662                            "access to your {}.").format(plugin.device))
    663         pin_msg.setWordWrap(True)
    664         pin_msg.setStyleSheet("color: red")
    665         settings_glayout.addWidget(pin_msg, 3, 1, 1, -1)
    666 
    667         # Settings tab - Homescreen
    668         homescreen_label = QLabel(_("Homescreen"))
    669         homescreen_change_button = QPushButton(_("Change..."))
    670         homescreen_clear_button = QPushButton(_("Reset"))
    671         homescreen_change_button.clicked.connect(change_homescreen)
    672         try:
    673             import PIL
    674         except ImportError:
    675             homescreen_change_button.setDisabled(True)
    676             homescreen_change_button.setToolTip(
    677                 _("Required package 'PIL' is not available - Please install it or use the Trezor website instead.")
    678             )
    679         homescreen_clear_button.clicked.connect(clear_homescreen)
    680         homescreen_msg = QLabel(_("You can set the homescreen on your "
    681                                   "device to personalize it.  You must "
    682                                   "choose a {} x {} monochrome black and "
    683                                   "white image.").format(hs_cols, hs_rows))
    684         homescreen_msg.setWordWrap(True)
    685         settings_glayout.addWidget(homescreen_label, 4, 0)
    686         settings_glayout.addWidget(homescreen_change_button, 4, 1)
    687         settings_glayout.addWidget(homescreen_clear_button, 4, 2)
    688         settings_glayout.addWidget(homescreen_msg, 5, 1, 1, -1)
    689 
    690         # Settings tab - Session Timeout
    691         timeout_label = QLabel(_("Session Timeout"))
    692         timeout_minutes = QLabel()
    693         timeout_slider = QSlider(Qt.Horizontal)
    694         timeout_slider.setRange(1, 60)
    695         timeout_slider.setSingleStep(1)
    696         timeout_slider.setTickInterval(5)
    697         timeout_slider.setTickPosition(QSlider.TicksBelow)
    698         timeout_slider.setTracking(True)
    699         timeout_msg = QLabel(
    700             _("Clear the session after the specified period "
    701               "of inactivity.  Once a session has timed out, "
    702               "your PIN and passphrase (if enabled) must be "
    703               "re-entered to use the device."))
    704         timeout_msg.setWordWrap(True)
    705         timeout_slider.setSliderPosition(config.get_session_timeout() // 60)
    706         slider_moved()
    707         timeout_slider.valueChanged.connect(slider_moved)
    708         timeout_slider.sliderReleased.connect(slider_released)
    709         settings_glayout.addWidget(timeout_label, 6, 0)
    710         settings_glayout.addWidget(timeout_slider, 6, 1, 1, 3)
    711         settings_glayout.addWidget(timeout_minutes, 6, 4)
    712         settings_glayout.addWidget(timeout_msg, 7, 1, 1, -1)
    713         settings_layout.addLayout(settings_glayout)
    714         settings_layout.addStretch(1)
    715 
    716         # Advanced tab
    717         advanced_tab = QWidget()
    718         advanced_layout = QVBoxLayout(advanced_tab)
    719         advanced_glayout = QGridLayout()
    720 
    721         # Advanced tab - clear PIN
    722         clear_pin_button = QPushButton(_("Disable PIN"))
    723         clear_pin_button.clicked.connect(clear_pin)
    724         clear_pin_warning = QLabel(
    725             _("If you disable your PIN, anyone with physical access to your "
    726               "{} device can spend your bitcoins.").format(plugin.device))
    727         clear_pin_warning.setWordWrap(True)
    728         clear_pin_warning.setStyleSheet("color: red")
    729         advanced_glayout.addWidget(clear_pin_button, 0, 2)
    730         advanced_glayout.addWidget(clear_pin_warning, 1, 0, 1, 5)
    731 
    732         # Advanced tab - toggle passphrase protection
    733         passphrase_button = QPushButton()
    734         passphrase_button.clicked.connect(toggle_passphrase)
    735         passphrase_msg = WWLabel(PASSPHRASE_HELP)
    736         passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN)
    737         passphrase_warning.setStyleSheet("color: red")
    738         advanced_glayout.addWidget(passphrase_button, 3, 2)
    739         advanced_glayout.addWidget(passphrase_msg, 4, 0, 1, 5)
    740         advanced_glayout.addWidget(passphrase_warning, 5, 0, 1, 5)
    741 
    742         # Advanced tab - wipe device
    743         wipe_device_button = QPushButton(_("Wipe Device"))
    744         wipe_device_button.clicked.connect(wipe_device)
    745         wipe_device_msg = QLabel(
    746             _("Wipe the device, removing all data from it.  The firmware "
    747               "is left unchanged."))
    748         wipe_device_msg.setWordWrap(True)
    749         wipe_device_warning = QLabel(
    750             _("Only wipe a device if you have the recovery seed written down "
    751               "and the device wallet(s) are empty, otherwise the bitcoins "
    752               "will be lost forever."))
    753         wipe_device_warning.setWordWrap(True)
    754         wipe_device_warning.setStyleSheet("color: red")
    755         advanced_glayout.addWidget(wipe_device_button, 6, 2)
    756         advanced_glayout.addWidget(wipe_device_msg, 7, 0, 1, 5)
    757         advanced_glayout.addWidget(wipe_device_warning, 8, 0, 1, 5)
    758         advanced_layout.addLayout(advanced_glayout)
    759         advanced_layout.addStretch(1)
    760 
    761         tabs = QTabWidget(self)
    762         tabs.addTab(info_tab, _("Information"))
    763         tabs.addTab(settings_tab, _("Settings"))
    764         tabs.addTab(advanced_tab, _("Advanced"))
    765         dialog_vbox = QVBoxLayout(self)
    766         dialog_vbox.addWidget(tabs)
    767         dialog_vbox.addLayout(Buttons(CloseButton(self)))
    768 
    769         # Update information
    770         invoke_client(None)