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