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 (21197B)
       ---
            1 from functools import partial
            2 import threading
            3 
            4 from PyQt5.QtCore import Qt, pyqtSignal, QRegExp
            5 from PyQt5.QtGui import QRegExpValidator
            6 from PyQt5.QtWidgets import (QVBoxLayout, QLabel, QGridLayout, QPushButton,
            7                              QHBoxLayout, QButtonGroup, QGroupBox,
            8                              QTextEdit, QLineEdit, QRadioButton, QCheckBox, QWidget,
            9                              QMessageBox, QFileDialog, QSlider, QTabWidget)
           10 
           11 from electrum.gui.qt.util import (WindowModalDialog, WWLabel, Buttons, CancelButton,
           12                                   OkButton, CloseButton, getOpenFileName)
           13 from electrum.i18n import _
           14 from electrum.plugin import hook
           15 from electrum.util import bh2u
           16 
           17 from ..hw_wallet.qt import QtHandlerBase, QtPluginBase
           18 from ..hw_wallet.plugin import only_hook_if_libraries_available
           19 from .safe_t import SafeTPlugin, TIM_NEW, TIM_RECOVER, TIM_MNEMONIC
           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 
           38 
           39 class QtHandler(QtHandlerBase):
           40 
           41     pin_signal = pyqtSignal(object, object)
           42 
           43     def __init__(self, win, pin_matrix_widget_class, device):
           44         super(QtHandler, self).__init__(win, device)
           45         self.pin_signal.connect(self.pin_dialog)
           46         self.pin_matrix_widget_class = pin_matrix_widget_class
           47 
           48     def get_pin(self, msg, *, show_strength=True):
           49         self.done.clear()
           50         self.pin_signal.emit(msg, show_strength)
           51         self.done.wait()
           52         return self.response
           53 
           54     def pin_dialog(self, msg, show_strength):
           55         # Needed e.g. when resetting a device
           56         self.clear_dialog()
           57         dialog = WindowModalDialog(self.top_level_window(), _("Enter PIN"))
           58         matrix = self.pin_matrix_widget_class(show_strength)
           59         vbox = QVBoxLayout()
           60         vbox.addWidget(QLabel(msg))
           61         vbox.addWidget(matrix)
           62         vbox.addLayout(Buttons(CancelButton(dialog), OkButton(dialog)))
           63         dialog.setLayout(vbox)
           64         dialog.exec_()
           65         self.response = str(matrix.get_value())
           66         self.done.set()
           67 
           68 
           69 class QtPlugin(QtPluginBase):
           70     # Derived classes must provide the following class-static variables:
           71     #   icon_file
           72     #   pin_matrix_widget_class
           73 
           74     @only_hook_if_libraries_available
           75     @hook
           76     def receive_menu(self, menu, addrs, wallet):
           77         if len(addrs) != 1:
           78             return
           79         for keystore in wallet.get_keystores():
           80             if type(keystore) == self.keystore_class:
           81                 def show_address(keystore=keystore):
           82                     keystore.thread.add(partial(self.show_address, wallet, addrs[0], keystore))
           83                 device_name = "{} ({})".format(self.device, keystore.label)
           84                 menu.addAction(_("Show on {}").format(device_name), show_address)
           85 
           86     def show_settings_dialog(self, window, keystore):
           87         def connect():
           88             device_id = self.choose_device(window, keystore)
           89             return device_id
           90         def show_dialog(device_id):
           91             if device_id:
           92                 SettingsDialog(window, self, keystore, device_id).exec_()
           93         keystore.thread.add(connect, on_success=show_dialog)
           94 
           95     def request_safe_t_init_settings(self, wizard, method, device):
           96         vbox = QVBoxLayout()
           97         next_enabled = True
           98         label = QLabel(_("Enter a label to name your device:"))
           99         name = QLineEdit()
          100         hl = QHBoxLayout()
          101         hl.addWidget(label)
          102         hl.addWidget(name)
          103         hl.addStretch(1)
          104         vbox.addLayout(hl)
          105 
          106         def clean_text(widget):
          107             text = widget.toPlainText().strip()
          108             return ' '.join(text.split())
          109 
          110         if method in [TIM_NEW, TIM_RECOVER]:
          111             gb = QGroupBox()
          112             hbox1 = QHBoxLayout()
          113             gb.setLayout(hbox1)
          114             vbox.addWidget(gb)
          115             gb.setTitle(_("Select your seed length:"))
          116             bg = QButtonGroup()
          117             for i, count in enumerate([12, 18, 24]):
          118                 rb = QRadioButton(gb)
          119                 rb.setText(_("{:d} words").format(count))
          120                 bg.addButton(rb)
          121                 bg.setId(rb, i)
          122                 hbox1.addWidget(rb)
          123                 rb.setChecked(True)
          124             cb_pin = QCheckBox(_('Enable PIN protection'))
          125             cb_pin.setChecked(True)
          126         else:
          127             text = QTextEdit()
          128             text.setMaximumHeight(60)
          129             if method == TIM_MNEMONIC:
          130                 msg = _("Enter your BIP39 mnemonic:")
          131             else:
          132                 msg = _("Enter the master private key beginning with xprv:")
          133                 def set_enabled():
          134                     from electrum.bip32 import is_xprv
          135                     wizard.next_button.setEnabled(is_xprv(clean_text(text)))
          136                 text.textChanged.connect(set_enabled)
          137                 next_enabled = False
          138 
          139             vbox.addWidget(QLabel(msg))
          140             vbox.addWidget(text)
          141             pin = QLineEdit()
          142             pin.setValidator(QRegExpValidator(QRegExp('[1-9]{0,9}')))
          143             pin.setMaximumWidth(100)
          144             hbox_pin = QHBoxLayout()
          145             hbox_pin.addWidget(QLabel(_("Enter your PIN (digits 1-9):")))
          146             hbox_pin.addWidget(pin)
          147             hbox_pin.addStretch(1)
          148 
          149         if method in [TIM_NEW, TIM_RECOVER]:
          150             vbox.addWidget(WWLabel(RECOMMEND_PIN))
          151             vbox.addWidget(cb_pin)
          152         else:
          153             vbox.addLayout(hbox_pin)
          154 
          155         passphrase_msg = WWLabel(PASSPHRASE_HELP_SHORT)
          156         passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN)
          157         passphrase_warning.setStyleSheet("color: red")
          158         cb_phrase = QCheckBox(_('Enable passphrases'))
          159         cb_phrase.setChecked(False)
          160         vbox.addWidget(passphrase_msg)
          161         vbox.addWidget(passphrase_warning)
          162         vbox.addWidget(cb_phrase)
          163 
          164         wizard.exec_layout(vbox, next_enabled=next_enabled)
          165 
          166         if method in [TIM_NEW, TIM_RECOVER]:
          167             item = bg.checkedId()
          168             pin = cb_pin.isChecked()
          169         else:
          170             item = ' '.join(str(clean_text(text)).split())
          171             pin = str(pin.text())
          172 
          173         return (item, name.text(), pin, cb_phrase.isChecked())
          174 
          175 
          176 class Plugin(SafeTPlugin, QtPlugin):
          177     icon_unpaired = "safe-t_unpaired.png"
          178     icon_paired = "safe-t.png"
          179 
          180     def create_handler(self, window):
          181         return QtHandler(window, self.pin_matrix_widget_class(), self.device)
          182 
          183     @classmethod
          184     def pin_matrix_widget_class(self):
          185         from safetlib.qt.pinmatrix import PinMatrixWidget
          186         return PinMatrixWidget
          187 
          188 
          189 class SettingsDialog(WindowModalDialog):
          190     '''This dialog doesn't require a device be paired with a wallet.
          191     We want users to be able to wipe a device even if they've forgotten
          192     their PIN.'''
          193 
          194     def __init__(self, window, plugin, keystore, device_id):
          195         title = _("{} Settings").format(plugin.device)
          196         super(SettingsDialog, self).__init__(window, title)
          197         self.setMaximumWidth(540)
          198 
          199         devmgr = plugin.device_manager()
          200         config = devmgr.config
          201         handler = keystore.handler
          202         thread = keystore.thread
          203         hs_cols, hs_rows = (128, 64)
          204 
          205         def invoke_client(method, *args, **kw_args):
          206             unpair_after = kw_args.pop('unpair_after', False)
          207 
          208             def task():
          209                 client = devmgr.client_by_id(device_id)
          210                 if not client:
          211                     raise RuntimeError("Device not connected")
          212                 if method:
          213                     getattr(client, method)(*args, **kw_args)
          214                 if unpair_after:
          215                     devmgr.unpair_id(device_id)
          216                 return client.features
          217 
          218             thread.add(task, on_success=update)
          219 
          220         def update(features):
          221             self.features = features
          222             set_label_enabled()
          223             if features.bootloader_hash:
          224                 bl_hash = bh2u(features.bootloader_hash)
          225                 bl_hash = "\n".join([bl_hash[:32], bl_hash[32:]])
          226             else:
          227                 bl_hash = "N/A"
          228             noyes = [_("No"), _("Yes")]
          229             endis = [_("Enable Passphrases"), _("Disable Passphrases")]
          230             disen = [_("Disabled"), _("Enabled")]
          231             setchange = [_("Set a PIN"), _("Change PIN")]
          232 
          233             version = "%d.%d.%d" % (features.major_version,
          234                                     features.minor_version,
          235                                     features.patch_version)
          236 
          237             device_label.setText(features.label)
          238             pin_set_label.setText(noyes[features.pin_protection])
          239             passphrases_label.setText(disen[features.passphrase_protection])
          240             bl_hash_label.setText(bl_hash)
          241             label_edit.setText(features.label)
          242             device_id_label.setText(features.device_id)
          243             initialized_label.setText(noyes[features.initialized])
          244             version_label.setText(version)
          245             clear_pin_button.setVisible(features.pin_protection)
          246             clear_pin_warning.setVisible(features.pin_protection)
          247             pin_button.setText(setchange[features.pin_protection])
          248             pin_msg.setVisible(not features.pin_protection)
          249             passphrase_button.setText(endis[features.passphrase_protection])
          250             language_label.setText(features.language)
          251 
          252         def set_label_enabled():
          253             label_apply.setEnabled(label_edit.text() != self.features.label)
          254 
          255         def rename():
          256             invoke_client('change_label', label_edit.text())
          257 
          258         def toggle_passphrase():
          259             title = _("Confirm Toggle Passphrase Protection")
          260             currently_enabled = self.features.passphrase_protection
          261             if currently_enabled:
          262                 msg = _("After disabling passphrases, you can only pair this "
          263                         "Electrum wallet if it had an empty passphrase.  "
          264                         "If its passphrase was not empty, you will need to "
          265                         "create a new wallet with the install wizard.  You "
          266                         "can use this wallet again at any time by re-enabling "
          267                         "passphrases and entering its passphrase.")
          268             else:
          269                 msg = _("Your current Electrum wallet can only be used with "
          270                         "an empty passphrase.  You must create a separate "
          271                         "wallet with the install wizard for other passphrases "
          272                         "as each one generates a new set of addresses.")
          273             msg += "\n\n" + _("Are you sure you want to proceed?")
          274             if not self.question(msg, title=title):
          275                 return
          276             invoke_client('toggle_passphrase', unpair_after=currently_enabled)
          277 
          278         def change_homescreen():
          279             filename = getOpenFileName(
          280                 parent=self,
          281                 title=_("Choose Homescreen"),
          282                 config=config,
          283             )
          284             if not filename:
          285                 return  # user cancelled
          286 
          287             if filename.endswith('.toif'):
          288                 img = open(filename, 'rb').read()
          289                 if img[:8] != b'TOIf\x90\x00\x90\x00':
          290                     handler.show_error('File is not a TOIF file with size of 144x144')
          291                     return
          292             else:
          293                 from PIL import Image # FIXME
          294                 im = Image.open(filename)
          295                 if im.size != (128, 64):
          296                     handler.show_error('Image must be 128 x 64 pixels')
          297                     return
          298                 im = im.convert('1')
          299                 pix = im.load()
          300                 img = bytearray(1024)
          301                 for j in range(64):
          302                     for i in range(128):
          303                         if pix[i, j]:
          304                             o = (i + j * 128)
          305                             img[o // 8] |= (1 << (7 - o % 8))
          306                 img = bytes(img)
          307             invoke_client('change_homescreen', img)
          308 
          309         def clear_homescreen():
          310             invoke_client('change_homescreen', b'\x00')
          311 
          312         def set_pin():
          313             invoke_client('set_pin', remove=False)
          314 
          315         def clear_pin():
          316             invoke_client('set_pin', remove=True)
          317 
          318         def wipe_device():
          319             wallet = window.wallet
          320             if wallet and sum(wallet.get_balance()):
          321                 title = _("Confirm Device Wipe")
          322                 msg = _("Are you SURE you want to wipe the device?\n"
          323                         "Your wallet still has bitcoins in it!")
          324                 if not self.question(msg, title=title,
          325                                      icon=QMessageBox.Critical):
          326                     return
          327             invoke_client('wipe_device', unpair_after=True)
          328 
          329         def slider_moved():
          330             mins = timeout_slider.sliderPosition()
          331             timeout_minutes.setText(_("{:2d} minutes").format(mins))
          332 
          333         def slider_released():
          334             config.set_session_timeout(timeout_slider.sliderPosition() * 60)
          335 
          336         # Information tab
          337         info_tab = QWidget()
          338         info_layout = QVBoxLayout(info_tab)
          339         info_glayout = QGridLayout()
          340         info_glayout.setColumnStretch(2, 1)
          341         device_label = QLabel()
          342         pin_set_label = QLabel()
          343         passphrases_label = QLabel()
          344         version_label = QLabel()
          345         device_id_label = QLabel()
          346         bl_hash_label = QLabel()
          347         bl_hash_label.setWordWrap(True)
          348         language_label = QLabel()
          349         initialized_label = QLabel()
          350         rows = [
          351             (_("Device Label"), device_label),
          352             (_("PIN set"), pin_set_label),
          353             (_("Passphrases"), passphrases_label),
          354             (_("Firmware Version"), version_label),
          355             (_("Device ID"), device_id_label),
          356             (_("Bootloader Hash"), bl_hash_label),
          357             (_("Language"), language_label),
          358             (_("Initialized"), initialized_label),
          359         ]
          360         for row_num, (label, widget) in enumerate(rows):
          361             info_glayout.addWidget(QLabel(label), row_num, 0)
          362             info_glayout.addWidget(widget, row_num, 1)
          363         info_layout.addLayout(info_glayout)
          364 
          365         # Settings tab
          366         settings_tab = QWidget()
          367         settings_layout = QVBoxLayout(settings_tab)
          368         settings_glayout = QGridLayout()
          369 
          370         # Settings tab - Label
          371         label_msg = QLabel(_("Name this {}.  If you have multiple devices "
          372                              "their labels help distinguish them.")
          373                            .format(plugin.device))
          374         label_msg.setWordWrap(True)
          375         label_label = QLabel(_("Device Label"))
          376         label_edit = QLineEdit()
          377         label_edit.setMinimumWidth(150)
          378         label_edit.setMaxLength(plugin.MAX_LABEL_LEN)
          379         label_apply = QPushButton(_("Apply"))
          380         label_apply.clicked.connect(rename)
          381         label_edit.textChanged.connect(set_label_enabled)
          382         settings_glayout.addWidget(label_label, 0, 0)
          383         settings_glayout.addWidget(label_edit, 0, 1, 1, 2)
          384         settings_glayout.addWidget(label_apply, 0, 3)
          385         settings_glayout.addWidget(label_msg, 1, 1, 1, -1)
          386 
          387         # Settings tab - PIN
          388         pin_label = QLabel(_("PIN Protection"))
          389         pin_button = QPushButton()
          390         pin_button.clicked.connect(set_pin)
          391         settings_glayout.addWidget(pin_label, 2, 0)
          392         settings_glayout.addWidget(pin_button, 2, 1)
          393         pin_msg = QLabel(_("PIN protection is strongly recommended.  "
          394                            "A PIN is your only protection against someone "
          395                            "stealing your bitcoins if they obtain physical "
          396                            "access to your {}.").format(plugin.device))
          397         pin_msg.setWordWrap(True)
          398         pin_msg.setStyleSheet("color: red")
          399         settings_glayout.addWidget(pin_msg, 3, 1, 1, -1)
          400 
          401         # Settings tab - Homescreen
          402         homescreen_label = QLabel(_("Homescreen"))
          403         homescreen_change_button = QPushButton(_("Change..."))
          404         homescreen_clear_button = QPushButton(_("Reset"))
          405         homescreen_change_button.clicked.connect(change_homescreen)
          406         try:
          407             import PIL
          408         except ImportError:
          409             homescreen_change_button.setDisabled(True)
          410             homescreen_change_button.setToolTip(
          411                 _("Required package 'PIL' is not available - Please install it.")
          412             )
          413         homescreen_clear_button.clicked.connect(clear_homescreen)
          414         homescreen_msg = QLabel(_("You can set the homescreen on your "
          415                                   "device to personalize it.  You must "
          416                                   "choose a {} x {} monochrome black and "
          417                                   "white image.").format(hs_cols, hs_rows))
          418         homescreen_msg.setWordWrap(True)
          419         settings_glayout.addWidget(homescreen_label, 4, 0)
          420         settings_glayout.addWidget(homescreen_change_button, 4, 1)
          421         settings_glayout.addWidget(homescreen_clear_button, 4, 2)
          422         settings_glayout.addWidget(homescreen_msg, 5, 1, 1, -1)
          423 
          424         # Settings tab - Session Timeout
          425         timeout_label = QLabel(_("Session Timeout"))
          426         timeout_minutes = QLabel()
          427         timeout_slider = QSlider(Qt.Horizontal)
          428         timeout_slider.setRange(1, 60)
          429         timeout_slider.setSingleStep(1)
          430         timeout_slider.setTickInterval(5)
          431         timeout_slider.setTickPosition(QSlider.TicksBelow)
          432         timeout_slider.setTracking(True)
          433         timeout_msg = QLabel(
          434             _("Clear the session after the specified period "
          435               "of inactivity.  Once a session has timed out, "
          436               "your PIN and passphrase (if enabled) must be "
          437               "re-entered to use the device."))
          438         timeout_msg.setWordWrap(True)
          439         timeout_slider.setSliderPosition(config.get_session_timeout() // 60)
          440         slider_moved()
          441         timeout_slider.valueChanged.connect(slider_moved)
          442         timeout_slider.sliderReleased.connect(slider_released)
          443         settings_glayout.addWidget(timeout_label, 6, 0)
          444         settings_glayout.addWidget(timeout_slider, 6, 1, 1, 3)
          445         settings_glayout.addWidget(timeout_minutes, 6, 4)
          446         settings_glayout.addWidget(timeout_msg, 7, 1, 1, -1)
          447         settings_layout.addLayout(settings_glayout)
          448         settings_layout.addStretch(1)
          449 
          450         # Advanced tab
          451         advanced_tab = QWidget()
          452         advanced_layout = QVBoxLayout(advanced_tab)
          453         advanced_glayout = QGridLayout()
          454 
          455         # Advanced tab - clear PIN
          456         clear_pin_button = QPushButton(_("Disable PIN"))
          457         clear_pin_button.clicked.connect(clear_pin)
          458         clear_pin_warning = QLabel(
          459             _("If you disable your PIN, anyone with physical access to your "
          460               "{} device can spend your bitcoins.").format(plugin.device))
          461         clear_pin_warning.setWordWrap(True)
          462         clear_pin_warning.setStyleSheet("color: red")
          463         advanced_glayout.addWidget(clear_pin_button, 0, 2)
          464         advanced_glayout.addWidget(clear_pin_warning, 1, 0, 1, 5)
          465 
          466         # Advanced tab - toggle passphrase protection
          467         passphrase_button = QPushButton()
          468         passphrase_button.clicked.connect(toggle_passphrase)
          469         passphrase_msg = WWLabel(PASSPHRASE_HELP)
          470         passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN)
          471         passphrase_warning.setStyleSheet("color: red")
          472         advanced_glayout.addWidget(passphrase_button, 3, 2)
          473         advanced_glayout.addWidget(passphrase_msg, 4, 0, 1, 5)
          474         advanced_glayout.addWidget(passphrase_warning, 5, 0, 1, 5)
          475 
          476         # Advanced tab - wipe device
          477         wipe_device_button = QPushButton(_("Wipe Device"))
          478         wipe_device_button.clicked.connect(wipe_device)
          479         wipe_device_msg = QLabel(
          480             _("Wipe the device, removing all data from it.  The firmware "
          481               "is left unchanged."))
          482         wipe_device_msg.setWordWrap(True)
          483         wipe_device_warning = QLabel(
          484             _("Only wipe a device if you have the recovery seed written down "
          485               "and the device wallet(s) are empty, otherwise the bitcoins "
          486               "will be lost forever."))
          487         wipe_device_warning.setWordWrap(True)
          488         wipe_device_warning.setStyleSheet("color: red")
          489         advanced_glayout.addWidget(wipe_device_button, 6, 2)
          490         advanced_glayout.addWidget(wipe_device_msg, 7, 0, 1, 5)
          491         advanced_glayout.addWidget(wipe_device_warning, 8, 0, 1, 5)
          492         advanced_layout.addLayout(advanced_glayout)
          493         advanced_layout.addStretch(1)
          494 
          495         tabs = QTabWidget(self)
          496         tabs.addTab(info_tab, _("Information"))
          497         tabs.addTab(settings_tab, _("Settings"))
          498         tabs.addTab(advanced_tab, _("Advanced"))
          499         dialog_vbox = QVBoxLayout(self)
          500         dialog_vbox.addWidget(tabs)
          501         dialog_vbox.addLayout(Buttons(CloseButton(self)))
          502 
          503         # Update information
          504         invoke_client(None)