electrum

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

commit 73b11a700a7930899ce22ee248871b1d0ac2e5ef
parent e36f67aabcc87f2b0cc22705c7fbd69e69551df4
Author: SomberNight <somber.night@protonmail.com>
Date:   Tue,  1 May 2018 15:51:01 +0200

trezor/keepkey: merge qt_generic.py into qt.py

(remnants of separating trezor and keepkey)

Diffstat:
Mplugins/keepkey/qt.py | 594++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Dplugins/keepkey/qt_generic.py | 594-------------------------------------------------------------------------------
Mplugins/trezor/qt.py | 478++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Dplugins/trezor/qt_generic.py | 478-------------------------------------------------------------------------------
4 files changed, 1068 insertions(+), 1076 deletions(-)

diff --git a/plugins/keepkey/qt.py b/plugins/keepkey/qt.py @@ -1,5 +1,293 @@ -from .qt_generic import QtPlugin -from .keepkey import KeepKeyPlugin +from functools import partial +import threading + +from PyQt5.Qt import Qt +from PyQt5.Qt import QGridLayout, QInputDialog, QPushButton +from PyQt5.Qt import QVBoxLayout, QLabel +from electrum_gui.qt.util import * +from .keepkey import KeepKeyPlugin, TIM_NEW, TIM_RECOVER, TIM_MNEMONIC +from ..hw_wallet.qt import QtHandlerBase, QtPluginBase + +from electrum.i18n import _ +from electrum.plugins import hook, DeviceMgr +from electrum.util import PrintError, UserCancelled, bh2u +from electrum.wallet import Wallet, Standard_Wallet + +PASSPHRASE_HELP_SHORT =_( + "Passphrases allow you to access new wallets, each " + "hidden behind a particular case-sensitive passphrase.") +PASSPHRASE_HELP = PASSPHRASE_HELP_SHORT + " " + _( + "You need to create a separate Electrum wallet for each passphrase " + "you use as they each generate different addresses. Changing " + "your passphrase does not lose other wallets, each is still " + "accessible behind its own passphrase.") +RECOMMEND_PIN = _( + "You should enable PIN protection. Your PIN is the only protection " + "for your bitcoins if your device is lost or stolen.") +PASSPHRASE_NOT_PIN = _( + "If you forget a passphrase you will be unable to access any " + "bitcoins in the wallet behind it. A passphrase is not a PIN. " + "Only change this if you are sure you understand it.") +CHARACTER_RECOVERY = ( + "Use the recovery cipher shown on your device to input your seed words. " + "The cipher changes with every keypress.\n" + "After at most 4 letters the device will auto-complete a word.\n" + "Press SPACE or the Accept Word button to accept the device's auto-" + "completed word and advance to the next one.\n" + "Press BACKSPACE to go back a character or word.\n" + "Press ENTER or the Seed Entered button once the last word in your " + "seed is auto-completed.") + +class CharacterButton(QPushButton): + def __init__(self, text=None): + QPushButton.__init__(self, text) + + def keyPressEvent(self, event): + event.setAccepted(False) # Pass through Enter and Space keys + + +class CharacterDialog(WindowModalDialog): + + def __init__(self, parent): + super(CharacterDialog, self).__init__(parent) + self.setWindowTitle(_("KeepKey Seed Recovery")) + self.character_pos = 0 + self.word_pos = 0 + self.loop = QEventLoop() + self.word_help = QLabel() + self.char_buttons = [] + + vbox = QVBoxLayout(self) + vbox.addWidget(WWLabel(CHARACTER_RECOVERY)) + hbox = QHBoxLayout() + hbox.addWidget(self.word_help) + for i in range(4): + char_button = CharacterButton('*') + char_button.setMaximumWidth(36) + self.char_buttons.append(char_button) + hbox.addWidget(char_button) + self.accept_button = CharacterButton(_("Accept Word")) + self.accept_button.clicked.connect(partial(self.process_key, 32)) + self.rejected.connect(partial(self.loop.exit, 1)) + hbox.addWidget(self.accept_button) + hbox.addStretch(1) + vbox.addLayout(hbox) + + self.finished_button = QPushButton(_("Seed Entered")) + self.cancel_button = QPushButton(_("Cancel")) + self.finished_button.clicked.connect(partial(self.process_key, + Qt.Key_Return)) + self.cancel_button.clicked.connect(self.rejected) + buttons = Buttons(self.finished_button, self.cancel_button) + vbox.addSpacing(40) + vbox.addLayout(buttons) + self.refresh() + self.show() + + def refresh(self): + self.word_help.setText("Enter seed word %2d:" % (self.word_pos + 1)) + self.accept_button.setEnabled(self.character_pos >= 3) + self.finished_button.setEnabled((self.word_pos in (11, 17, 23) + and self.character_pos >= 3)) + for n, button in enumerate(self.char_buttons): + button.setEnabled(n == self.character_pos) + if n == self.character_pos: + button.setFocus() + + def is_valid_alpha_space(self, key): + # Auto-completion requires at least 3 characters + if key == ord(' ') and self.character_pos >= 3: + return True + # Firmware aborts protocol if the 5th character is non-space + if self.character_pos >= 4: + return False + return (key >= ord('a') and key <= ord('z') + or (key >= ord('A') and key <= ord('Z'))) + + def process_key(self, key): + self.data = None + if key == Qt.Key_Return and self.finished_button.isEnabled(): + self.data = {'done': True} + elif key == Qt.Key_Backspace and (self.word_pos or self.character_pos): + self.data = {'delete': True} + elif self.is_valid_alpha_space(key): + self.data = {'character': chr(key).lower()} + if self.data: + self.loop.exit(0) + + def keyPressEvent(self, event): + self.process_key(event.key()) + if not self.data: + QDialog.keyPressEvent(self, event) + + def get_char(self, word_pos, character_pos): + self.word_pos = word_pos + self.character_pos = character_pos + self.refresh() + if self.loop.exec_(): + self.data = None # User cancelled + + +class QtHandler(QtHandlerBase): + + char_signal = pyqtSignal(object) + pin_signal = pyqtSignal(object) + close_char_dialog_signal = pyqtSignal() + + def __init__(self, win, pin_matrix_widget_class, device): + super(QtHandler, self).__init__(win, device) + self.char_signal.connect(self.update_character_dialog) + self.pin_signal.connect(self.pin_dialog) + self.close_char_dialog_signal.connect(self._close_char_dialog) + self.pin_matrix_widget_class = pin_matrix_widget_class + self.character_dialog = None + + def get_char(self, msg): + self.done.clear() + self.char_signal.emit(msg) + self.done.wait() + data = self.character_dialog.data + if not data or 'done' in data: + self.close_char_dialog_signal.emit() + return data + + def _close_char_dialog(self): + if self.character_dialog: + self.character_dialog.accept() + self.character_dialog = None + + def get_pin(self, msg): + self.done.clear() + self.pin_signal.emit(msg) + self.done.wait() + return self.response + + def pin_dialog(self, msg): + # Needed e.g. when resetting a device + self.clear_dialog() + dialog = WindowModalDialog(self.top_level_window(), _("Enter PIN")) + matrix = self.pin_matrix_widget_class() + vbox = QVBoxLayout() + vbox.addWidget(QLabel(msg)) + vbox.addWidget(matrix) + vbox.addLayout(Buttons(CancelButton(dialog), OkButton(dialog))) + dialog.setLayout(vbox) + dialog.exec_() + self.response = str(matrix.get_value()) + self.done.set() + + def update_character_dialog(self, msg): + if not self.character_dialog: + self.character_dialog = CharacterDialog(self.top_level_window()) + self.character_dialog.get_char(msg.word_pos, msg.character_pos) + self.done.set() + + + +class QtPlugin(QtPluginBase): + # Derived classes must provide the following class-static variables: + # icon_file + # pin_matrix_widget_class + + def create_handler(self, window): + return QtHandler(window, self.pin_matrix_widget_class(), self.device) + + @hook + def receive_menu(self, menu, addrs, wallet): + if type(wallet) is not Standard_Wallet: + return + keystore = wallet.get_keystore() + if type(keystore) == self.keystore_class and len(addrs) == 1: + def show_address(): + keystore.thread.add(partial(self.show_address, wallet, addrs[0])) + menu.addAction(_("Show on {}").format(self.device), show_address) + + def show_settings_dialog(self, window, keystore): + device_id = self.choose_device(window, keystore) + if device_id: + SettingsDialog(window, self, keystore, device_id).exec_() + + def request_trezor_init_settings(self, wizard, method, device): + vbox = QVBoxLayout() + next_enabled = True + label = QLabel(_("Enter a label to name your device:")) + name = QLineEdit() + hl = QHBoxLayout() + hl.addWidget(label) + hl.addWidget(name) + hl.addStretch(1) + vbox.addLayout(hl) + + def clean_text(widget): + text = widget.toPlainText().strip() + return ' '.join(text.split()) + + if method in [TIM_NEW, TIM_RECOVER]: + gb = QGroupBox() + hbox1 = QHBoxLayout() + gb.setLayout(hbox1) + # KeepKey recovery doesn't need a word count + if method == TIM_NEW: + vbox.addWidget(gb) + gb.setTitle(_("Select your seed length:")) + bg = QButtonGroup() + for i, count in enumerate([12, 18, 24]): + rb = QRadioButton(gb) + rb.setText(_("{} words").format(count)) + bg.addButton(rb) + bg.setId(rb, i) + hbox1.addWidget(rb) + rb.setChecked(True) + cb_pin = QCheckBox(_('Enable PIN protection')) + cb_pin.setChecked(True) + else: + text = QTextEdit() + text.setMaximumHeight(60) + if method == TIM_MNEMONIC: + msg = _("Enter your BIP39 mnemonic:") + else: + msg = _("Enter the master private key beginning with xprv:") + def set_enabled(): + from electrum.keystore import is_xprv + wizard.next_button.setEnabled(is_xprv(clean_text(text))) + text.textChanged.connect(set_enabled) + next_enabled = False + + vbox.addWidget(QLabel(msg)) + vbox.addWidget(text) + pin = QLineEdit() + pin.setValidator(QRegExpValidator(QRegExp('[1-9]{0,9}'))) + pin.setMaximumWidth(100) + hbox_pin = QHBoxLayout() + hbox_pin.addWidget(QLabel(_("Enter your PIN (digits 1-9):"))) + hbox_pin.addWidget(pin) + hbox_pin.addStretch(1) + + if method in [TIM_NEW, TIM_RECOVER]: + vbox.addWidget(WWLabel(RECOMMEND_PIN)) + vbox.addWidget(cb_pin) + else: + vbox.addLayout(hbox_pin) + + passphrase_msg = WWLabel(PASSPHRASE_HELP_SHORT) + passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN) + passphrase_warning.setStyleSheet("color: red") + cb_phrase = QCheckBox(_('Enable passphrases')) + cb_phrase.setChecked(False) + vbox.addWidget(passphrase_msg) + vbox.addWidget(passphrase_warning) + vbox.addWidget(cb_phrase) + + wizard.exec_layout(vbox, next_enabled=next_enabled) + + if method in [TIM_NEW, TIM_RECOVER]: + item = bg.checkedId() + pin = cb_pin.isChecked() + else: + item = ' '.join(str(clean_text(text)).split()) + pin = str(pin.text()) + + return (item, name.text(), pin, cb_phrase.isChecked()) class Plugin(KeepKeyPlugin, QtPlugin): @@ -10,3 +298,305 @@ class Plugin(KeepKeyPlugin, QtPlugin): def pin_matrix_widget_class(self): from keepkeylib.qt.pinmatrix import PinMatrixWidget return PinMatrixWidget + + +class SettingsDialog(WindowModalDialog): + '''This dialog doesn't require a device be paired with a wallet. + We want users to be able to wipe a device even if they've forgotten + their PIN.''' + + def __init__(self, window, plugin, keystore, device_id): + title = _("{} Settings").format(plugin.device) + super(SettingsDialog, self).__init__(window, title) + self.setMaximumWidth(540) + + devmgr = plugin.device_manager() + config = devmgr.config + handler = keystore.handler + thread = keystore.thread + hs_rows, hs_cols = (64, 128) + + def invoke_client(method, *args, **kw_args): + unpair_after = kw_args.pop('unpair_after', False) + + def task(): + client = devmgr.client_by_id(device_id) + if not client: + raise RuntimeError("Device not connected") + if method: + getattr(client, method)(*args, **kw_args) + if unpair_after: + devmgr.unpair_id(device_id) + return client.features + + thread.add(task, on_success=update) + + def update(features): + self.features = features + set_label_enabled() + bl_hash = bh2u(features.bootloader_hash) + bl_hash = "\n".join([bl_hash[:32], bl_hash[32:]]) + noyes = [_("No"), _("Yes")] + endis = [_("Enable Passphrases"), _("Disable Passphrases")] + disen = [_("Disabled"), _("Enabled")] + setchange = [_("Set a PIN"), _("Change PIN")] + + version = "%d.%d.%d" % (features.major_version, + features.minor_version, + features.patch_version) + coins = ", ".join(coin.coin_name for coin in features.coins) + + device_label.setText(features.label) + pin_set_label.setText(noyes[features.pin_protection]) + passphrases_label.setText(disen[features.passphrase_protection]) + bl_hash_label.setText(bl_hash) + label_edit.setText(features.label) + device_id_label.setText(features.device_id) + initialized_label.setText(noyes[features.initialized]) + version_label.setText(version) + coins_label.setText(coins) + clear_pin_button.setVisible(features.pin_protection) + clear_pin_warning.setVisible(features.pin_protection) + pin_button.setText(setchange[features.pin_protection]) + pin_msg.setVisible(not features.pin_protection) + passphrase_button.setText(endis[features.passphrase_protection]) + language_label.setText(features.language) + + def set_label_enabled(): + label_apply.setEnabled(label_edit.text() != self.features.label) + + def rename(): + invoke_client('change_label', label_edit.text()) + + def toggle_passphrase(): + title = _("Confirm Toggle Passphrase Protection") + currently_enabled = self.features.passphrase_protection + if currently_enabled: + msg = _("After disabling passphrases, you can only pair this " + "Electrum wallet if it had an empty passphrase. " + "If its passphrase was not empty, you will need to " + "create a new wallet with the install wizard. You " + "can use this wallet again at any time by re-enabling " + "passphrases and entering its passphrase.") + else: + msg = _("Your current Electrum wallet can only be used with " + "an empty passphrase. You must create a separate " + "wallet with the install wizard for other passphrases " + "as each one generates a new set of addresses.") + msg += "\n\n" + _("Are you sure you want to proceed?") + if not self.question(msg, title=title): + return + invoke_client('toggle_passphrase', unpair_after=currently_enabled) + + def change_homescreen(): + from PIL import Image # FIXME + dialog = QFileDialog(self, _("Choose Homescreen")) + filename, __ = dialog.getOpenFileName() + if filename: + im = Image.open(str(filename)) + if im.size != (hs_cols, hs_rows): + raise Exception('Image must be 64 x 128 pixels') + im = im.convert('1') + pix = im.load() + img = '' + for j in range(hs_rows): + for i in range(hs_cols): + img += '1' if pix[i, j] else '0' + img = ''.join(chr(int(img[i:i + 8], 2)) + for i in range(0, len(img), 8)) + invoke_client('change_homescreen', img) + + def clear_homescreen(): + invoke_client('change_homescreen', '\x00') + + def set_pin(): + invoke_client('set_pin', remove=False) + + def clear_pin(): + invoke_client('set_pin', remove=True) + + def wipe_device(): + wallet = window.wallet + if wallet and sum(wallet.get_balance()): + title = _("Confirm Device Wipe") + msg = _("Are you SURE you want to wipe the device?\n" + "Your wallet still has bitcoins in it!") + if not self.question(msg, title=title, + icon=QMessageBox.Critical): + return + invoke_client('wipe_device', unpair_after=True) + + def slider_moved(): + mins = timeout_slider.sliderPosition() + timeout_minutes.setText(_("%2d minutes") % mins) + + def slider_released(): + config.set_session_timeout(timeout_slider.sliderPosition() * 60) + + # Information tab + info_tab = QWidget() + info_layout = QVBoxLayout(info_tab) + info_glayout = QGridLayout() + info_glayout.setColumnStretch(2, 1) + device_label = QLabel() + pin_set_label = QLabel() + passphrases_label = QLabel() + version_label = QLabel() + device_id_label = QLabel() + bl_hash_label = QLabel() + bl_hash_label.setWordWrap(True) + coins_label = QLabel() + coins_label.setWordWrap(True) + language_label = QLabel() + initialized_label = QLabel() + rows = [ + (_("Device Label"), device_label), + (_("PIN set"), pin_set_label), + (_("Passphrases"), passphrases_label), + (_("Firmware Version"), version_label), + (_("Device ID"), device_id_label), + (_("Bootloader Hash"), bl_hash_label), + (_("Supported Coins"), coins_label), + (_("Language"), language_label), + (_("Initialized"), initialized_label), + ] + for row_num, (label, widget) in enumerate(rows): + info_glayout.addWidget(QLabel(label), row_num, 0) + info_glayout.addWidget(widget, row_num, 1) + info_layout.addLayout(info_glayout) + + # Settings tab + settings_tab = QWidget() + settings_layout = QVBoxLayout(settings_tab) + settings_glayout = QGridLayout() + + # Settings tab - Label + label_msg = QLabel(_("Name this {}. If you have multiple devices " + "their labels help distinguish them.") + .format(plugin.device)) + label_msg.setWordWrap(True) + label_label = QLabel(_("Device Label")) + label_edit = QLineEdit() + label_edit.setMinimumWidth(150) + label_edit.setMaxLength(plugin.MAX_LABEL_LEN) + label_apply = QPushButton(_("Apply")) + label_apply.clicked.connect(rename) + label_edit.textChanged.connect(set_label_enabled) + settings_glayout.addWidget(label_label, 0, 0) + settings_glayout.addWidget(label_edit, 0, 1, 1, 2) + settings_glayout.addWidget(label_apply, 0, 3) + settings_glayout.addWidget(label_msg, 1, 1, 1, -1) + + # Settings tab - PIN + pin_label = QLabel(_("PIN Protection")) + pin_button = QPushButton() + pin_button.clicked.connect(set_pin) + settings_glayout.addWidget(pin_label, 2, 0) + settings_glayout.addWidget(pin_button, 2, 1) + pin_msg = QLabel(_("PIN protection is strongly recommended. " + "A PIN is your only protection against someone " + "stealing your bitcoins if they obtain physical " + "access to your {}.").format(plugin.device)) + pin_msg.setWordWrap(True) + pin_msg.setStyleSheet("color: red") + settings_glayout.addWidget(pin_msg, 3, 1, 1, -1) + + # Settings tab - Homescreen + if plugin.device != 'KeepKey': # Not yet supported by KK firmware + homescreen_layout = QHBoxLayout() + homescreen_label = QLabel(_("Homescreen")) + homescreen_change_button = QPushButton(_("Change...")) + homescreen_clear_button = QPushButton(_("Reset")) + homescreen_change_button.clicked.connect(change_homescreen) + homescreen_clear_button.clicked.connect(clear_homescreen) + homescreen_msg = QLabel(_("You can set the homescreen on your " + "device to personalize it. You must " + "choose a {} x {} monochrome black and " + "white image.").format(hs_rows, hs_cols)) + homescreen_msg.setWordWrap(True) + settings_glayout.addWidget(homescreen_label, 4, 0) + settings_glayout.addWidget(homescreen_change_button, 4, 1) + settings_glayout.addWidget(homescreen_clear_button, 4, 2) + settings_glayout.addWidget(homescreen_msg, 5, 1, 1, -1) + + # Settings tab - Session Timeout + timeout_label = QLabel(_("Session Timeout")) + timeout_minutes = QLabel() + timeout_slider = QSlider(Qt.Horizontal) + timeout_slider.setRange(1, 60) + timeout_slider.setSingleStep(1) + timeout_slider.setTickInterval(5) + timeout_slider.setTickPosition(QSlider.TicksBelow) + timeout_slider.setTracking(True) + timeout_msg = QLabel( + _("Clear the session after the specified period " + "of inactivity. Once a session has timed out, " + "your PIN and passphrase (if enabled) must be " + "re-entered to use the device.")) + timeout_msg.setWordWrap(True) + timeout_slider.setSliderPosition(config.get_session_timeout() // 60) + slider_moved() + timeout_slider.valueChanged.connect(slider_moved) + timeout_slider.sliderReleased.connect(slider_released) + settings_glayout.addWidget(timeout_label, 6, 0) + settings_glayout.addWidget(timeout_slider, 6, 1, 1, 3) + settings_glayout.addWidget(timeout_minutes, 6, 4) + settings_glayout.addWidget(timeout_msg, 7, 1, 1, -1) + settings_layout.addLayout(settings_glayout) + settings_layout.addStretch(1) + + # Advanced tab + advanced_tab = QWidget() + advanced_layout = QVBoxLayout(advanced_tab) + advanced_glayout = QGridLayout() + + # Advanced tab - clear PIN + clear_pin_button = QPushButton(_("Disable PIN")) + clear_pin_button.clicked.connect(clear_pin) + clear_pin_warning = QLabel( + _("If you disable your PIN, anyone with physical access to your " + "{} device can spend your bitcoins.").format(plugin.device)) + clear_pin_warning.setWordWrap(True) + clear_pin_warning.setStyleSheet("color: red") + advanced_glayout.addWidget(clear_pin_button, 0, 2) + advanced_glayout.addWidget(clear_pin_warning, 1, 0, 1, 5) + + # Advanced tab - toggle passphrase protection + passphrase_button = QPushButton() + passphrase_button.clicked.connect(toggle_passphrase) + passphrase_msg = WWLabel(PASSPHRASE_HELP) + passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN) + passphrase_warning.setStyleSheet("color: red") + advanced_glayout.addWidget(passphrase_button, 3, 2) + advanced_glayout.addWidget(passphrase_msg, 4, 0, 1, 5) + advanced_glayout.addWidget(passphrase_warning, 5, 0, 1, 5) + + # Advanced tab - wipe device + wipe_device_button = QPushButton(_("Wipe Device")) + wipe_device_button.clicked.connect(wipe_device) + wipe_device_msg = QLabel( + _("Wipe the device, removing all data from it. The firmware " + "is left unchanged.")) + wipe_device_msg.setWordWrap(True) + wipe_device_warning = QLabel( + _("Only wipe a device if you have the recovery seed written down " + "and the device wallet(s) are empty, otherwise the bitcoins " + "will be lost forever.")) + wipe_device_warning.setWordWrap(True) + wipe_device_warning.setStyleSheet("color: red") + advanced_glayout.addWidget(wipe_device_button, 6, 2) + advanced_glayout.addWidget(wipe_device_msg, 7, 0, 1, 5) + advanced_glayout.addWidget(wipe_device_warning, 8, 0, 1, 5) + advanced_layout.addLayout(advanced_glayout) + advanced_layout.addStretch(1) + + tabs = QTabWidget(self) + tabs.addTab(info_tab, _("Information")) + tabs.addTab(settings_tab, _("Settings")) + tabs.addTab(advanced_tab, _("Advanced")) + dialog_vbox = QVBoxLayout(self) + dialog_vbox.addWidget(tabs) + dialog_vbox.addLayout(Buttons(CloseButton(self))) + + # Update information + invoke_client(None) diff --git a/plugins/keepkey/qt_generic.py b/plugins/keepkey/qt_generic.py @@ -1,594 +0,0 @@ -from functools import partial -import threading - -from PyQt5.Qt import Qt -from PyQt5.Qt import QGridLayout, QInputDialog, QPushButton -from PyQt5.Qt import QVBoxLayout, QLabel -from electrum_gui.qt.util import * -from .keepkey import TIM_NEW, TIM_RECOVER, TIM_MNEMONIC -from ..hw_wallet.qt import QtHandlerBase, QtPluginBase - -from electrum.i18n import _ -from electrum.plugins import hook, DeviceMgr -from electrum.util import PrintError, UserCancelled, bh2u -from electrum.wallet import Wallet, Standard_Wallet - -PASSPHRASE_HELP_SHORT =_( - "Passphrases allow you to access new wallets, each " - "hidden behind a particular case-sensitive passphrase.") -PASSPHRASE_HELP = PASSPHRASE_HELP_SHORT + " " + _( - "You need to create a separate Electrum wallet for each passphrase " - "you use as they each generate different addresses. Changing " - "your passphrase does not lose other wallets, each is still " - "accessible behind its own passphrase.") -RECOMMEND_PIN = _( - "You should enable PIN protection. Your PIN is the only protection " - "for your bitcoins if your device is lost or stolen.") -PASSPHRASE_NOT_PIN = _( - "If you forget a passphrase you will be unable to access any " - "bitcoins in the wallet behind it. A passphrase is not a PIN. " - "Only change this if you are sure you understand it.") -CHARACTER_RECOVERY = ( - "Use the recovery cipher shown on your device to input your seed words. " - "The cipher changes with every keypress.\n" - "After at most 4 letters the device will auto-complete a word.\n" - "Press SPACE or the Accept Word button to accept the device's auto-" - "completed word and advance to the next one.\n" - "Press BACKSPACE to go back a character or word.\n" - "Press ENTER or the Seed Entered button once the last word in your " - "seed is auto-completed.") - -class CharacterButton(QPushButton): - def __init__(self, text=None): - QPushButton.__init__(self, text) - - def keyPressEvent(self, event): - event.setAccepted(False) # Pass through Enter and Space keys - - -class CharacterDialog(WindowModalDialog): - - def __init__(self, parent): - super(CharacterDialog, self).__init__(parent) - self.setWindowTitle(_("KeepKey Seed Recovery")) - self.character_pos = 0 - self.word_pos = 0 - self.loop = QEventLoop() - self.word_help = QLabel() - self.char_buttons = [] - - vbox = QVBoxLayout(self) - vbox.addWidget(WWLabel(CHARACTER_RECOVERY)) - hbox = QHBoxLayout() - hbox.addWidget(self.word_help) - for i in range(4): - char_button = CharacterButton('*') - char_button.setMaximumWidth(36) - self.char_buttons.append(char_button) - hbox.addWidget(char_button) - self.accept_button = CharacterButton(_("Accept Word")) - self.accept_button.clicked.connect(partial(self.process_key, 32)) - self.rejected.connect(partial(self.loop.exit, 1)) - hbox.addWidget(self.accept_button) - hbox.addStretch(1) - vbox.addLayout(hbox) - - self.finished_button = QPushButton(_("Seed Entered")) - self.cancel_button = QPushButton(_("Cancel")) - self.finished_button.clicked.connect(partial(self.process_key, - Qt.Key_Return)) - self.cancel_button.clicked.connect(self.rejected) - buttons = Buttons(self.finished_button, self.cancel_button) - vbox.addSpacing(40) - vbox.addLayout(buttons) - self.refresh() - self.show() - - def refresh(self): - self.word_help.setText("Enter seed word %2d:" % (self.word_pos + 1)) - self.accept_button.setEnabled(self.character_pos >= 3) - self.finished_button.setEnabled((self.word_pos in (11, 17, 23) - and self.character_pos >= 3)) - for n, button in enumerate(self.char_buttons): - button.setEnabled(n == self.character_pos) - if n == self.character_pos: - button.setFocus() - - def is_valid_alpha_space(self, key): - # Auto-completion requires at least 3 characters - if key == ord(' ') and self.character_pos >= 3: - return True - # Firmware aborts protocol if the 5th character is non-space - if self.character_pos >= 4: - return False - return (key >= ord('a') and key <= ord('z') - or (key >= ord('A') and key <= ord('Z'))) - - def process_key(self, key): - self.data = None - if key == Qt.Key_Return and self.finished_button.isEnabled(): - self.data = {'done': True} - elif key == Qt.Key_Backspace and (self.word_pos or self.character_pos): - self.data = {'delete': True} - elif self.is_valid_alpha_space(key): - self.data = {'character': chr(key).lower()} - if self.data: - self.loop.exit(0) - - def keyPressEvent(self, event): - self.process_key(event.key()) - if not self.data: - QDialog.keyPressEvent(self, event) - - def get_char(self, word_pos, character_pos): - self.word_pos = word_pos - self.character_pos = character_pos - self.refresh() - if self.loop.exec_(): - self.data = None # User cancelled - - -class QtHandler(QtHandlerBase): - - char_signal = pyqtSignal(object) - pin_signal = pyqtSignal(object) - close_char_dialog_signal = pyqtSignal() - - def __init__(self, win, pin_matrix_widget_class, device): - super(QtHandler, self).__init__(win, device) - self.char_signal.connect(self.update_character_dialog) - self.pin_signal.connect(self.pin_dialog) - self.close_char_dialog_signal.connect(self._close_char_dialog) - self.pin_matrix_widget_class = pin_matrix_widget_class - self.character_dialog = None - - def get_char(self, msg): - self.done.clear() - self.char_signal.emit(msg) - self.done.wait() - data = self.character_dialog.data - if not data or 'done' in data: - self.close_char_dialog_signal.emit() - return data - - def _close_char_dialog(self): - if self.character_dialog: - self.character_dialog.accept() - self.character_dialog = None - - def get_pin(self, msg): - self.done.clear() - self.pin_signal.emit(msg) - self.done.wait() - return self.response - - def pin_dialog(self, msg): - # Needed e.g. when resetting a device - self.clear_dialog() - dialog = WindowModalDialog(self.top_level_window(), _("Enter PIN")) - matrix = self.pin_matrix_widget_class() - vbox = QVBoxLayout() - vbox.addWidget(QLabel(msg)) - vbox.addWidget(matrix) - vbox.addLayout(Buttons(CancelButton(dialog), OkButton(dialog))) - dialog.setLayout(vbox) - dialog.exec_() - self.response = str(matrix.get_value()) - self.done.set() - - def update_character_dialog(self, msg): - if not self.character_dialog: - self.character_dialog = CharacterDialog(self.top_level_window()) - self.character_dialog.get_char(msg.word_pos, msg.character_pos) - self.done.set() - - - -class QtPlugin(QtPluginBase): - # Derived classes must provide the following class-static variables: - # icon_file - # pin_matrix_widget_class - - def create_handler(self, window): - return QtHandler(window, self.pin_matrix_widget_class(), self.device) - - @hook - def receive_menu(self, menu, addrs, wallet): - if type(wallet) is not Standard_Wallet: - return - keystore = wallet.get_keystore() - if type(keystore) == self.keystore_class and len(addrs) == 1: - def show_address(): - keystore.thread.add(partial(self.show_address, wallet, addrs[0])) - menu.addAction(_("Show on {}").format(self.device), show_address) - - def show_settings_dialog(self, window, keystore): - device_id = self.choose_device(window, keystore) - if device_id: - SettingsDialog(window, self, keystore, device_id).exec_() - - def request_trezor_init_settings(self, wizard, method, device): - vbox = QVBoxLayout() - next_enabled = True - label = QLabel(_("Enter a label to name your device:")) - name = QLineEdit() - hl = QHBoxLayout() - hl.addWidget(label) - hl.addWidget(name) - hl.addStretch(1) - vbox.addLayout(hl) - - def clean_text(widget): - text = widget.toPlainText().strip() - return ' '.join(text.split()) - - if method in [TIM_NEW, TIM_RECOVER]: - gb = QGroupBox() - hbox1 = QHBoxLayout() - gb.setLayout(hbox1) - # KeepKey recovery doesn't need a word count - if method == TIM_NEW: - vbox.addWidget(gb) - gb.setTitle(_("Select your seed length:")) - bg = QButtonGroup() - for i, count in enumerate([12, 18, 24]): - rb = QRadioButton(gb) - rb.setText(_("{} words").format(count)) - bg.addButton(rb) - bg.setId(rb, i) - hbox1.addWidget(rb) - rb.setChecked(True) - cb_pin = QCheckBox(_('Enable PIN protection')) - cb_pin.setChecked(True) - else: - text = QTextEdit() - text.setMaximumHeight(60) - if method == TIM_MNEMONIC: - msg = _("Enter your BIP39 mnemonic:") - else: - msg = _("Enter the master private key beginning with xprv:") - def set_enabled(): - from electrum.keystore import is_xprv - wizard.next_button.setEnabled(is_xprv(clean_text(text))) - text.textChanged.connect(set_enabled) - next_enabled = False - - vbox.addWidget(QLabel(msg)) - vbox.addWidget(text) - pin = QLineEdit() - pin.setValidator(QRegExpValidator(QRegExp('[1-9]{0,9}'))) - pin.setMaximumWidth(100) - hbox_pin = QHBoxLayout() - hbox_pin.addWidget(QLabel(_("Enter your PIN (digits 1-9):"))) - hbox_pin.addWidget(pin) - hbox_pin.addStretch(1) - - if method in [TIM_NEW, TIM_RECOVER]: - vbox.addWidget(WWLabel(RECOMMEND_PIN)) - vbox.addWidget(cb_pin) - else: - vbox.addLayout(hbox_pin) - - passphrase_msg = WWLabel(PASSPHRASE_HELP_SHORT) - passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN) - passphrase_warning.setStyleSheet("color: red") - cb_phrase = QCheckBox(_('Enable passphrases')) - cb_phrase.setChecked(False) - vbox.addWidget(passphrase_msg) - vbox.addWidget(passphrase_warning) - vbox.addWidget(cb_phrase) - - wizard.exec_layout(vbox, next_enabled=next_enabled) - - if method in [TIM_NEW, TIM_RECOVER]: - item = bg.checkedId() - pin = cb_pin.isChecked() - else: - item = ' '.join(str(clean_text(text)).split()) - pin = str(pin.text()) - - return (item, name.text(), pin, cb_phrase.isChecked()) - - - - -class SettingsDialog(WindowModalDialog): - '''This dialog doesn't require a device be paired with a wallet. - We want users to be able to wipe a device even if they've forgotten - their PIN.''' - - def __init__(self, window, plugin, keystore, device_id): - title = _("{} Settings").format(plugin.device) - super(SettingsDialog, self).__init__(window, title) - self.setMaximumWidth(540) - - devmgr = plugin.device_manager() - config = devmgr.config - handler = keystore.handler - thread = keystore.thread - hs_rows, hs_cols = (64, 128) - - def invoke_client(method, *args, **kw_args): - unpair_after = kw_args.pop('unpair_after', False) - - def task(): - client = devmgr.client_by_id(device_id) - if not client: - raise RuntimeError("Device not connected") - if method: - getattr(client, method)(*args, **kw_args) - if unpair_after: - devmgr.unpair_id(device_id) - return client.features - - thread.add(task, on_success=update) - - def update(features): - self.features = features - set_label_enabled() - bl_hash = bh2u(features.bootloader_hash) - bl_hash = "\n".join([bl_hash[:32], bl_hash[32:]]) - noyes = [_("No"), _("Yes")] - endis = [_("Enable Passphrases"), _("Disable Passphrases")] - disen = [_("Disabled"), _("Enabled")] - setchange = [_("Set a PIN"), _("Change PIN")] - - version = "%d.%d.%d" % (features.major_version, - features.minor_version, - features.patch_version) - coins = ", ".join(coin.coin_name for coin in features.coins) - - device_label.setText(features.label) - pin_set_label.setText(noyes[features.pin_protection]) - passphrases_label.setText(disen[features.passphrase_protection]) - bl_hash_label.setText(bl_hash) - label_edit.setText(features.label) - device_id_label.setText(features.device_id) - initialized_label.setText(noyes[features.initialized]) - version_label.setText(version) - coins_label.setText(coins) - clear_pin_button.setVisible(features.pin_protection) - clear_pin_warning.setVisible(features.pin_protection) - pin_button.setText(setchange[features.pin_protection]) - pin_msg.setVisible(not features.pin_protection) - passphrase_button.setText(endis[features.passphrase_protection]) - language_label.setText(features.language) - - def set_label_enabled(): - label_apply.setEnabled(label_edit.text() != self.features.label) - - def rename(): - invoke_client('change_label', label_edit.text()) - - def toggle_passphrase(): - title = _("Confirm Toggle Passphrase Protection") - currently_enabled = self.features.passphrase_protection - if currently_enabled: - msg = _("After disabling passphrases, you can only pair this " - "Electrum wallet if it had an empty passphrase. " - "If its passphrase was not empty, you will need to " - "create a new wallet with the install wizard. You " - "can use this wallet again at any time by re-enabling " - "passphrases and entering its passphrase.") - else: - msg = _("Your current Electrum wallet can only be used with " - "an empty passphrase. You must create a separate " - "wallet with the install wizard for other passphrases " - "as each one generates a new set of addresses.") - msg += "\n\n" + _("Are you sure you want to proceed?") - if not self.question(msg, title=title): - return - invoke_client('toggle_passphrase', unpair_after=currently_enabled) - - def change_homescreen(): - from PIL import Image # FIXME - dialog = QFileDialog(self, _("Choose Homescreen")) - filename, __ = dialog.getOpenFileName() - if filename: - im = Image.open(str(filename)) - if im.size != (hs_cols, hs_rows): - raise Exception('Image must be 64 x 128 pixels') - im = im.convert('1') - pix = im.load() - img = '' - for j in range(hs_rows): - for i in range(hs_cols): - img += '1' if pix[i, j] else '0' - img = ''.join(chr(int(img[i:i + 8], 2)) - for i in range(0, len(img), 8)) - invoke_client('change_homescreen', img) - - def clear_homescreen(): - invoke_client('change_homescreen', '\x00') - - def set_pin(): - invoke_client('set_pin', remove=False) - - def clear_pin(): - invoke_client('set_pin', remove=True) - - def wipe_device(): - wallet = window.wallet - if wallet and sum(wallet.get_balance()): - title = _("Confirm Device Wipe") - msg = _("Are you SURE you want to wipe the device?\n" - "Your wallet still has bitcoins in it!") - if not self.question(msg, title=title, - icon=QMessageBox.Critical): - return - invoke_client('wipe_device', unpair_after=True) - - def slider_moved(): - mins = timeout_slider.sliderPosition() - timeout_minutes.setText(_("%2d minutes") % mins) - - def slider_released(): - config.set_session_timeout(timeout_slider.sliderPosition() * 60) - - # Information tab - info_tab = QWidget() - info_layout = QVBoxLayout(info_tab) - info_glayout = QGridLayout() - info_glayout.setColumnStretch(2, 1) - device_label = QLabel() - pin_set_label = QLabel() - passphrases_label = QLabel() - version_label = QLabel() - device_id_label = QLabel() - bl_hash_label = QLabel() - bl_hash_label.setWordWrap(True) - coins_label = QLabel() - coins_label.setWordWrap(True) - language_label = QLabel() - initialized_label = QLabel() - rows = [ - (_("Device Label"), device_label), - (_("PIN set"), pin_set_label), - (_("Passphrases"), passphrases_label), - (_("Firmware Version"), version_label), - (_("Device ID"), device_id_label), - (_("Bootloader Hash"), bl_hash_label), - (_("Supported Coins"), coins_label), - (_("Language"), language_label), - (_("Initialized"), initialized_label), - ] - for row_num, (label, widget) in enumerate(rows): - info_glayout.addWidget(QLabel(label), row_num, 0) - info_glayout.addWidget(widget, row_num, 1) - info_layout.addLayout(info_glayout) - - # Settings tab - settings_tab = QWidget() - settings_layout = QVBoxLayout(settings_tab) - settings_glayout = QGridLayout() - - # Settings tab - Label - label_msg = QLabel(_("Name this {}. If you have multiple devices " - "their labels help distinguish them.") - .format(plugin.device)) - label_msg.setWordWrap(True) - label_label = QLabel(_("Device Label")) - label_edit = QLineEdit() - label_edit.setMinimumWidth(150) - label_edit.setMaxLength(plugin.MAX_LABEL_LEN) - label_apply = QPushButton(_("Apply")) - label_apply.clicked.connect(rename) - label_edit.textChanged.connect(set_label_enabled) - settings_glayout.addWidget(label_label, 0, 0) - settings_glayout.addWidget(label_edit, 0, 1, 1, 2) - settings_glayout.addWidget(label_apply, 0, 3) - settings_glayout.addWidget(label_msg, 1, 1, 1, -1) - - # Settings tab - PIN - pin_label = QLabel(_("PIN Protection")) - pin_button = QPushButton() - pin_button.clicked.connect(set_pin) - settings_glayout.addWidget(pin_label, 2, 0) - settings_glayout.addWidget(pin_button, 2, 1) - pin_msg = QLabel(_("PIN protection is strongly recommended. " - "A PIN is your only protection against someone " - "stealing your bitcoins if they obtain physical " - "access to your {}.").format(plugin.device)) - pin_msg.setWordWrap(True) - pin_msg.setStyleSheet("color: red") - settings_glayout.addWidget(pin_msg, 3, 1, 1, -1) - - # Settings tab - Homescreen - if plugin.device != 'KeepKey': # Not yet supported by KK firmware - homescreen_layout = QHBoxLayout() - homescreen_label = QLabel(_("Homescreen")) - homescreen_change_button = QPushButton(_("Change...")) - homescreen_clear_button = QPushButton(_("Reset")) - homescreen_change_button.clicked.connect(change_homescreen) - homescreen_clear_button.clicked.connect(clear_homescreen) - homescreen_msg = QLabel(_("You can set the homescreen on your " - "device to personalize it. You must " - "choose a {} x {} monochrome black and " - "white image.").format(hs_rows, hs_cols)) - homescreen_msg.setWordWrap(True) - settings_glayout.addWidget(homescreen_label, 4, 0) - settings_glayout.addWidget(homescreen_change_button, 4, 1) - settings_glayout.addWidget(homescreen_clear_button, 4, 2) - settings_glayout.addWidget(homescreen_msg, 5, 1, 1, -1) - - # Settings tab - Session Timeout - timeout_label = QLabel(_("Session Timeout")) - timeout_minutes = QLabel() - timeout_slider = QSlider(Qt.Horizontal) - timeout_slider.setRange(1, 60) - timeout_slider.setSingleStep(1) - timeout_slider.setTickInterval(5) - timeout_slider.setTickPosition(QSlider.TicksBelow) - timeout_slider.setTracking(True) - timeout_msg = QLabel( - _("Clear the session after the specified period " - "of inactivity. Once a session has timed out, " - "your PIN and passphrase (if enabled) must be " - "re-entered to use the device.")) - timeout_msg.setWordWrap(True) - timeout_slider.setSliderPosition(config.get_session_timeout() // 60) - slider_moved() - timeout_slider.valueChanged.connect(slider_moved) - timeout_slider.sliderReleased.connect(slider_released) - settings_glayout.addWidget(timeout_label, 6, 0) - settings_glayout.addWidget(timeout_slider, 6, 1, 1, 3) - settings_glayout.addWidget(timeout_minutes, 6, 4) - settings_glayout.addWidget(timeout_msg, 7, 1, 1, -1) - settings_layout.addLayout(settings_glayout) - settings_layout.addStretch(1) - - # Advanced tab - advanced_tab = QWidget() - advanced_layout = QVBoxLayout(advanced_tab) - advanced_glayout = QGridLayout() - - # Advanced tab - clear PIN - clear_pin_button = QPushButton(_("Disable PIN")) - clear_pin_button.clicked.connect(clear_pin) - clear_pin_warning = QLabel( - _("If you disable your PIN, anyone with physical access to your " - "{} device can spend your bitcoins.").format(plugin.device)) - clear_pin_warning.setWordWrap(True) - clear_pin_warning.setStyleSheet("color: red") - advanced_glayout.addWidget(clear_pin_button, 0, 2) - advanced_glayout.addWidget(clear_pin_warning, 1, 0, 1, 5) - - # Advanced tab - toggle passphrase protection - passphrase_button = QPushButton() - passphrase_button.clicked.connect(toggle_passphrase) - passphrase_msg = WWLabel(PASSPHRASE_HELP) - passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN) - passphrase_warning.setStyleSheet("color: red") - advanced_glayout.addWidget(passphrase_button, 3, 2) - advanced_glayout.addWidget(passphrase_msg, 4, 0, 1, 5) - advanced_glayout.addWidget(passphrase_warning, 5, 0, 1, 5) - - # Advanced tab - wipe device - wipe_device_button = QPushButton(_("Wipe Device")) - wipe_device_button.clicked.connect(wipe_device) - wipe_device_msg = QLabel( - _("Wipe the device, removing all data from it. The firmware " - "is left unchanged.")) - wipe_device_msg.setWordWrap(True) - wipe_device_warning = QLabel( - _("Only wipe a device if you have the recovery seed written down " - "and the device wallet(s) are empty, otherwise the bitcoins " - "will be lost forever.")) - wipe_device_warning.setWordWrap(True) - wipe_device_warning.setStyleSheet("color: red") - advanced_glayout.addWidget(wipe_device_button, 6, 2) - advanced_glayout.addWidget(wipe_device_msg, 7, 0, 1, 5) - advanced_glayout.addWidget(wipe_device_warning, 8, 0, 1, 5) - advanced_layout.addLayout(advanced_glayout) - advanced_layout.addStretch(1) - - tabs = QTabWidget(self) - tabs.addTab(info_tab, _("Information")) - tabs.addTab(settings_tab, _("Settings")) - tabs.addTab(advanced_tab, _("Advanced")) - dialog_vbox = QVBoxLayout(self) - dialog_vbox.addWidget(tabs) - dialog_vbox.addLayout(Buttons(CloseButton(self))) - - # Update information - invoke_client(None) diff --git a/plugins/trezor/qt.py b/plugins/trezor/qt.py @@ -1,5 +1,168 @@ -from ..trezor.qt_generic import QtPlugin -from .trezor import TrezorPlugin +from functools import partial +import threading + +from PyQt5.Qt import Qt +from PyQt5.Qt import QGridLayout, QInputDialog, QPushButton +from PyQt5.Qt import QVBoxLayout, QLabel +from electrum_gui.qt.util import * +from .trezor import TrezorPlugin, TIM_NEW, TIM_RECOVER, TIM_MNEMONIC +from ..hw_wallet.qt import QtHandlerBase, QtPluginBase + +from electrum.i18n import _ +from electrum.plugins import hook, DeviceMgr +from electrum.util import PrintError, UserCancelled, bh2u +from electrum.wallet import Wallet, Standard_Wallet + +PASSPHRASE_HELP_SHORT =_( + "Passphrases allow you to access new wallets, each " + "hidden behind a particular case-sensitive passphrase.") +PASSPHRASE_HELP = PASSPHRASE_HELP_SHORT + " " + _( + "You need to create a separate Electrum wallet for each passphrase " + "you use as they each generate different addresses. Changing " + "your passphrase does not lose other wallets, each is still " + "accessible behind its own passphrase.") +RECOMMEND_PIN = _( + "You should enable PIN protection. Your PIN is the only protection " + "for your bitcoins if your device is lost or stolen.") +PASSPHRASE_NOT_PIN = _( + "If you forget a passphrase you will be unable to access any " + "bitcoins in the wallet behind it. A passphrase is not a PIN. " + "Only change this if you are sure you understand it.") + + +class QtHandler(QtHandlerBase): + + pin_signal = pyqtSignal(object) + + def __init__(self, win, pin_matrix_widget_class, device): + super(QtHandler, self).__init__(win, device) + self.pin_signal.connect(self.pin_dialog) + self.pin_matrix_widget_class = pin_matrix_widget_class + + def get_pin(self, msg): + self.done.clear() + self.pin_signal.emit(msg) + self.done.wait() + return self.response + + def pin_dialog(self, msg): + # Needed e.g. when resetting a device + self.clear_dialog() + dialog = WindowModalDialog(self.top_level_window(), _("Enter PIN")) + matrix = self.pin_matrix_widget_class() + vbox = QVBoxLayout() + vbox.addWidget(QLabel(msg)) + vbox.addWidget(matrix) + vbox.addLayout(Buttons(CancelButton(dialog), OkButton(dialog))) + dialog.setLayout(vbox) + dialog.exec_() + self.response = str(matrix.get_value()) + self.done.set() + + +class QtPlugin(QtPluginBase): + # Derived classes must provide the following class-static variables: + # icon_file + # pin_matrix_widget_class + + def create_handler(self, window): + return QtHandler(window, self.pin_matrix_widget_class(), self.device) + + @hook + def receive_menu(self, menu, addrs, wallet): + if len(addrs) != 1: + return + for keystore in wallet.get_keystores(): + if type(keystore) == self.keystore_class: + def show_address(): + keystore.thread.add(partial(self.show_address, wallet, addrs[0], keystore)) + menu.addAction(_("Show on {}").format(self.device), show_address) + break + + def show_settings_dialog(self, window, keystore): + device_id = self.choose_device(window, keystore) + if device_id: + SettingsDialog(window, self, keystore, device_id).exec_() + + def request_trezor_init_settings(self, wizard, method, device): + vbox = QVBoxLayout() + next_enabled = True + label = QLabel(_("Enter a label to name your device:")) + name = QLineEdit() + hl = QHBoxLayout() + hl.addWidget(label) + hl.addWidget(name) + hl.addStretch(1) + vbox.addLayout(hl) + + def clean_text(widget): + text = widget.toPlainText().strip() + return ' '.join(text.split()) + + if method in [TIM_NEW, TIM_RECOVER]: + gb = QGroupBox() + hbox1 = QHBoxLayout() + gb.setLayout(hbox1) + vbox.addWidget(gb) + gb.setTitle(_("Select your seed length:")) + bg = QButtonGroup() + for i, count in enumerate([12, 18, 24]): + rb = QRadioButton(gb) + rb.setText(_("%d words") % count) + bg.addButton(rb) + bg.setId(rb, i) + hbox1.addWidget(rb) + rb.setChecked(True) + cb_pin = QCheckBox(_('Enable PIN protection')) + cb_pin.setChecked(True) + else: + text = QTextEdit() + text.setMaximumHeight(60) + if method == TIM_MNEMONIC: + msg = _("Enter your BIP39 mnemonic:") + else: + msg = _("Enter the master private key beginning with xprv:") + def set_enabled(): + from electrum.keystore import is_xprv + wizard.next_button.setEnabled(is_xprv(clean_text(text))) + text.textChanged.connect(set_enabled) + next_enabled = False + + vbox.addWidget(QLabel(msg)) + vbox.addWidget(text) + pin = QLineEdit() + pin.setValidator(QRegExpValidator(QRegExp('[1-9]{0,9}'))) + pin.setMaximumWidth(100) + hbox_pin = QHBoxLayout() + hbox_pin.addWidget(QLabel(_("Enter your PIN (digits 1-9):"))) + hbox_pin.addWidget(pin) + hbox_pin.addStretch(1) + + if method in [TIM_NEW, TIM_RECOVER]: + vbox.addWidget(WWLabel(RECOMMEND_PIN)) + vbox.addWidget(cb_pin) + else: + vbox.addLayout(hbox_pin) + + passphrase_msg = WWLabel(PASSPHRASE_HELP_SHORT) + passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN) + passphrase_warning.setStyleSheet("color: red") + cb_phrase = QCheckBox(_('Enable passphrases')) + cb_phrase.setChecked(False) + vbox.addWidget(passphrase_msg) + vbox.addWidget(passphrase_warning) + vbox.addWidget(cb_phrase) + + wizard.exec_layout(vbox, next_enabled=next_enabled) + + if method in [TIM_NEW, TIM_RECOVER]: + item = bg.checkedId() + pin = cb_pin.isChecked() + else: + item = ' '.join(str(clean_text(text)).split()) + pin = str(pin.text()) + + return (item, name.text(), pin, cb_phrase.isChecked()) class Plugin(TrezorPlugin, QtPlugin): @@ -10,3 +173,314 @@ class Plugin(TrezorPlugin, QtPlugin): def pin_matrix_widget_class(self): from trezorlib.qt.pinmatrix import PinMatrixWidget return PinMatrixWidget + + +class SettingsDialog(WindowModalDialog): + '''This dialog doesn't require a device be paired with a wallet. + We want users to be able to wipe a device even if they've forgotten + their PIN.''' + + def __init__(self, window, plugin, keystore, device_id): + title = _("{} Settings").format(plugin.device) + super(SettingsDialog, self).__init__(window, title) + self.setMaximumWidth(540) + + devmgr = plugin.device_manager() + config = devmgr.config + handler = keystore.handler + thread = keystore.thread + hs_rows, hs_cols = (64, 128) + + def invoke_client(method, *args, **kw_args): + unpair_after = kw_args.pop('unpair_after', False) + + def task(): + client = devmgr.client_by_id(device_id) + if not client: + raise RuntimeError("Device not connected") + if method: + getattr(client, method)(*args, **kw_args) + if unpair_after: + devmgr.unpair_id(device_id) + return client.features + + thread.add(task, on_success=update) + + def update(features): + self.features = features + set_label_enabled() + if features.bootloader_hash: + bl_hash = bh2u(features.bootloader_hash) + bl_hash = "\n".join([bl_hash[:32], bl_hash[32:]]) + else: + bl_hash = "N/A" + noyes = [_("No"), _("Yes")] + endis = [_("Enable Passphrases"), _("Disable Passphrases")] + disen = [_("Disabled"), _("Enabled")] + setchange = [_("Set a PIN"), _("Change PIN")] + + version = "%d.%d.%d" % (features.major_version, + features.minor_version, + features.patch_version) + coins = ", ".join(coin.coin_name for coin in features.coins) + + device_label.setText(features.label) + pin_set_label.setText(noyes[features.pin_protection]) + passphrases_label.setText(disen[features.passphrase_protection]) + bl_hash_label.setText(bl_hash) + label_edit.setText(features.label) + device_id_label.setText(features.device_id) + initialized_label.setText(noyes[features.initialized]) + version_label.setText(version) + coins_label.setText(coins) + clear_pin_button.setVisible(features.pin_protection) + clear_pin_warning.setVisible(features.pin_protection) + pin_button.setText(setchange[features.pin_protection]) + pin_msg.setVisible(not features.pin_protection) + passphrase_button.setText(endis[features.passphrase_protection]) + language_label.setText(features.language) + + def set_label_enabled(): + label_apply.setEnabled(label_edit.text() != self.features.label) + + def rename(): + invoke_client('change_label', label_edit.text()) + + def toggle_passphrase(): + title = _("Confirm Toggle Passphrase Protection") + currently_enabled = self.features.passphrase_protection + if currently_enabled: + msg = _("After disabling passphrases, you can only pair this " + "Electrum wallet if it had an empty passphrase. " + "If its passphrase was not empty, you will need to " + "create a new wallet with the install wizard. You " + "can use this wallet again at any time by re-enabling " + "passphrases and entering its passphrase.") + else: + msg = _("Your current Electrum wallet can only be used with " + "an empty passphrase. You must create a separate " + "wallet with the install wizard for other passphrases " + "as each one generates a new set of addresses.") + msg += "\n\n" + _("Are you sure you want to proceed?") + if not self.question(msg, title=title): + return + invoke_client('toggle_passphrase', unpair_after=currently_enabled) + + def change_homescreen(): + dialog = QFileDialog(self, _("Choose Homescreen")) + filename, __ = dialog.getOpenFileName() + + if filename.endswith('.toif'): + img = open(filename, 'rb').read() + if img[:8] != b'TOIf\x90\x00\x90\x00': + raise Exception('File is not a TOIF file with size of 144x144') + else: + from PIL import Image # FIXME + im = Image.open(filename) + if im.size != (128, 64): + raise Exception('Image must be 128 x 64 pixels') + im = im.convert('1') + pix = im.load() + img = bytearray(1024) + for j in range(64): + for i in range(128): + if pix[i, j]: + o = (i + j * 128) + img[o // 8] |= (1 << (7 - o % 8)) + img = bytes(img) + invoke_client('change_homescreen', img) + + def clear_homescreen(): + invoke_client('change_homescreen', b'\x00') + + def set_pin(): + invoke_client('set_pin', remove=False) + + def clear_pin(): + invoke_client('set_pin', remove=True) + + def wipe_device(): + wallet = window.wallet + if wallet and sum(wallet.get_balance()): + title = _("Confirm Device Wipe") + msg = _("Are you SURE you want to wipe the device?\n" + "Your wallet still has bitcoins in it!") + if not self.question(msg, title=title, + icon=QMessageBox.Critical): + return + invoke_client('wipe_device', unpair_after=True) + + def slider_moved(): + mins = timeout_slider.sliderPosition() + timeout_minutes.setText(_("%2d minutes") % mins) + + def slider_released(): + config.set_session_timeout(timeout_slider.sliderPosition() * 60) + + # Information tab + info_tab = QWidget() + info_layout = QVBoxLayout(info_tab) + info_glayout = QGridLayout() + info_glayout.setColumnStretch(2, 1) + device_label = QLabel() + pin_set_label = QLabel() + passphrases_label = QLabel() + version_label = QLabel() + device_id_label = QLabel() + bl_hash_label = QLabel() + bl_hash_label.setWordWrap(True) + coins_label = QLabel() + coins_label.setWordWrap(True) + language_label = QLabel() + initialized_label = QLabel() + rows = [ + (_("Device Label"), device_label), + (_("PIN set"), pin_set_label), + (_("Passphrases"), passphrases_label), + (_("Firmware Version"), version_label), + (_("Device ID"), device_id_label), + (_("Bootloader Hash"), bl_hash_label), + (_("Supported Coins"), coins_label), + (_("Language"), language_label), + (_("Initialized"), initialized_label), + ] + for row_num, (label, widget) in enumerate(rows): + info_glayout.addWidget(QLabel(label), row_num, 0) + info_glayout.addWidget(widget, row_num, 1) + info_layout.addLayout(info_glayout) + + # Settings tab + settings_tab = QWidget() + settings_layout = QVBoxLayout(settings_tab) + settings_glayout = QGridLayout() + + # Settings tab - Label + label_msg = QLabel(_("Name this {}. If you have multiple devices " + "their labels help distinguish them.") + .format(plugin.device)) + label_msg.setWordWrap(True) + label_label = QLabel(_("Device Label")) + label_edit = QLineEdit() + label_edit.setMinimumWidth(150) + label_edit.setMaxLength(plugin.MAX_LABEL_LEN) + label_apply = QPushButton(_("Apply")) + label_apply.clicked.connect(rename) + label_edit.textChanged.connect(set_label_enabled) + settings_glayout.addWidget(label_label, 0, 0) + settings_glayout.addWidget(label_edit, 0, 1, 1, 2) + settings_glayout.addWidget(label_apply, 0, 3) + settings_glayout.addWidget(label_msg, 1, 1, 1, -1) + + # Settings tab - PIN + pin_label = QLabel(_("PIN Protection")) + pin_button = QPushButton() + pin_button.clicked.connect(set_pin) + settings_glayout.addWidget(pin_label, 2, 0) + settings_glayout.addWidget(pin_button, 2, 1) + pin_msg = QLabel(_("PIN protection is strongly recommended. " + "A PIN is your only protection against someone " + "stealing your bitcoins if they obtain physical " + "access to your {}.").format(plugin.device)) + pin_msg.setWordWrap(True) + pin_msg.setStyleSheet("color: red") + settings_glayout.addWidget(pin_msg, 3, 1, 1, -1) + + # Settings tab - Homescreen + if plugin.device != 'KeepKey': # Not yet supported by KK firmware + homescreen_layout = QHBoxLayout() + homescreen_label = QLabel(_("Homescreen")) + homescreen_change_button = QPushButton(_("Change...")) + homescreen_clear_button = QPushButton(_("Reset")) + homescreen_change_button.clicked.connect(change_homescreen) + homescreen_clear_button.clicked.connect(clear_homescreen) + homescreen_msg = QLabel(_("You can set the homescreen on your " + "device to personalize it. You must " + "choose a {} x {} monochrome black and " + "white image.").format(hs_rows, hs_cols)) + homescreen_msg.setWordWrap(True) + settings_glayout.addWidget(homescreen_label, 4, 0) + settings_glayout.addWidget(homescreen_change_button, 4, 1) + settings_glayout.addWidget(homescreen_clear_button, 4, 2) + settings_glayout.addWidget(homescreen_msg, 5, 1, 1, -1) + + # Settings tab - Session Timeout + timeout_label = QLabel(_("Session Timeout")) + timeout_minutes = QLabel() + timeout_slider = QSlider(Qt.Horizontal) + timeout_slider.setRange(1, 60) + timeout_slider.setSingleStep(1) + timeout_slider.setTickInterval(5) + timeout_slider.setTickPosition(QSlider.TicksBelow) + timeout_slider.setTracking(True) + timeout_msg = QLabel( + _("Clear the session after the specified period " + "of inactivity. Once a session has timed out, " + "your PIN and passphrase (if enabled) must be " + "re-entered to use the device.")) + timeout_msg.setWordWrap(True) + timeout_slider.setSliderPosition(config.get_session_timeout() // 60) + slider_moved() + timeout_slider.valueChanged.connect(slider_moved) + timeout_slider.sliderReleased.connect(slider_released) + settings_glayout.addWidget(timeout_label, 6, 0) + settings_glayout.addWidget(timeout_slider, 6, 1, 1, 3) + settings_glayout.addWidget(timeout_minutes, 6, 4) + settings_glayout.addWidget(timeout_msg, 7, 1, 1, -1) + settings_layout.addLayout(settings_glayout) + settings_layout.addStretch(1) + + # Advanced tab + advanced_tab = QWidget() + advanced_layout = QVBoxLayout(advanced_tab) + advanced_glayout = QGridLayout() + + # Advanced tab - clear PIN + clear_pin_button = QPushButton(_("Disable PIN")) + clear_pin_button.clicked.connect(clear_pin) + clear_pin_warning = QLabel( + _("If you disable your PIN, anyone with physical access to your " + "{} device can spend your bitcoins.").format(plugin.device)) + clear_pin_warning.setWordWrap(True) + clear_pin_warning.setStyleSheet("color: red") + advanced_glayout.addWidget(clear_pin_button, 0, 2) + advanced_glayout.addWidget(clear_pin_warning, 1, 0, 1, 5) + + # Advanced tab - toggle passphrase protection + passphrase_button = QPushButton() + passphrase_button.clicked.connect(toggle_passphrase) + passphrase_msg = WWLabel(PASSPHRASE_HELP) + passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN) + passphrase_warning.setStyleSheet("color: red") + advanced_glayout.addWidget(passphrase_button, 3, 2) + advanced_glayout.addWidget(passphrase_msg, 4, 0, 1, 5) + advanced_glayout.addWidget(passphrase_warning, 5, 0, 1, 5) + + # Advanced tab - wipe device + wipe_device_button = QPushButton(_("Wipe Device")) + wipe_device_button.clicked.connect(wipe_device) + wipe_device_msg = QLabel( + _("Wipe the device, removing all data from it. The firmware " + "is left unchanged.")) + wipe_device_msg.setWordWrap(True) + wipe_device_warning = QLabel( + _("Only wipe a device if you have the recovery seed written down " + "and the device wallet(s) are empty, otherwise the bitcoins " + "will be lost forever.")) + wipe_device_warning.setWordWrap(True) + wipe_device_warning.setStyleSheet("color: red") + advanced_glayout.addWidget(wipe_device_button, 6, 2) + advanced_glayout.addWidget(wipe_device_msg, 7, 0, 1, 5) + advanced_glayout.addWidget(wipe_device_warning, 8, 0, 1, 5) + advanced_layout.addLayout(advanced_glayout) + advanced_layout.addStretch(1) + + tabs = QTabWidget(self) + tabs.addTab(info_tab, _("Information")) + tabs.addTab(settings_tab, _("Settings")) + tabs.addTab(advanced_tab, _("Advanced")) + dialog_vbox = QVBoxLayout(self) + dialog_vbox.addWidget(tabs) + dialog_vbox.addLayout(Buttons(CloseButton(self))) + + # Update information + invoke_client(None) diff --git a/plugins/trezor/qt_generic.py b/plugins/trezor/qt_generic.py @@ -1,478 +0,0 @@ -from functools import partial -import threading - -from PyQt5.Qt import Qt -from PyQt5.Qt import QGridLayout, QInputDialog, QPushButton -from PyQt5.Qt import QVBoxLayout, QLabel -from electrum_gui.qt.util import * -from .trezor import TIM_NEW, TIM_RECOVER, TIM_MNEMONIC -from ..hw_wallet.qt import QtHandlerBase, QtPluginBase - -from electrum.i18n import _ -from electrum.plugins import hook, DeviceMgr -from electrum.util import PrintError, UserCancelled, bh2u -from electrum.wallet import Wallet, Standard_Wallet - -PASSPHRASE_HELP_SHORT =_( - "Passphrases allow you to access new wallets, each " - "hidden behind a particular case-sensitive passphrase.") -PASSPHRASE_HELP = PASSPHRASE_HELP_SHORT + " " + _( - "You need to create a separate Electrum wallet for each passphrase " - "you use as they each generate different addresses. Changing " - "your passphrase does not lose other wallets, each is still " - "accessible behind its own passphrase.") -RECOMMEND_PIN = _( - "You should enable PIN protection. Your PIN is the only protection " - "for your bitcoins if your device is lost or stolen.") -PASSPHRASE_NOT_PIN = _( - "If you forget a passphrase you will be unable to access any " - "bitcoins in the wallet behind it. A passphrase is not a PIN. " - "Only change this if you are sure you understand it.") - - -class QtHandler(QtHandlerBase): - - pin_signal = pyqtSignal(object) - - def __init__(self, win, pin_matrix_widget_class, device): - super(QtHandler, self).__init__(win, device) - self.pin_signal.connect(self.pin_dialog) - self.pin_matrix_widget_class = pin_matrix_widget_class - - def get_pin(self, msg): - self.done.clear() - self.pin_signal.emit(msg) - self.done.wait() - return self.response - - def pin_dialog(self, msg): - # Needed e.g. when resetting a device - self.clear_dialog() - dialog = WindowModalDialog(self.top_level_window(), _("Enter PIN")) - matrix = self.pin_matrix_widget_class() - vbox = QVBoxLayout() - vbox.addWidget(QLabel(msg)) - vbox.addWidget(matrix) - vbox.addLayout(Buttons(CancelButton(dialog), OkButton(dialog))) - dialog.setLayout(vbox) - dialog.exec_() - self.response = str(matrix.get_value()) - self.done.set() - - -class QtPlugin(QtPluginBase): - # Derived classes must provide the following class-static variables: - # icon_file - # pin_matrix_widget_class - - def create_handler(self, window): - return QtHandler(window, self.pin_matrix_widget_class(), self.device) - - @hook - def receive_menu(self, menu, addrs, wallet): - if len(addrs) != 1: - return - for keystore in wallet.get_keystores(): - if type(keystore) == self.keystore_class: - def show_address(): - keystore.thread.add(partial(self.show_address, wallet, addrs[0], keystore)) - menu.addAction(_("Show on {}").format(self.device), show_address) - break - - def show_settings_dialog(self, window, keystore): - device_id = self.choose_device(window, keystore) - if device_id: - SettingsDialog(window, self, keystore, device_id).exec_() - - def request_trezor_init_settings(self, wizard, method, device): - vbox = QVBoxLayout() - next_enabled = True - label = QLabel(_("Enter a label to name your device:")) - name = QLineEdit() - hl = QHBoxLayout() - hl.addWidget(label) - hl.addWidget(name) - hl.addStretch(1) - vbox.addLayout(hl) - - def clean_text(widget): - text = widget.toPlainText().strip() - return ' '.join(text.split()) - - if method in [TIM_NEW, TIM_RECOVER]: - gb = QGroupBox() - hbox1 = QHBoxLayout() - gb.setLayout(hbox1) - vbox.addWidget(gb) - gb.setTitle(_("Select your seed length:")) - bg = QButtonGroup() - for i, count in enumerate([12, 18, 24]): - rb = QRadioButton(gb) - rb.setText(_("%d words") % count) - bg.addButton(rb) - bg.setId(rb, i) - hbox1.addWidget(rb) - rb.setChecked(True) - cb_pin = QCheckBox(_('Enable PIN protection')) - cb_pin.setChecked(True) - else: - text = QTextEdit() - text.setMaximumHeight(60) - if method == TIM_MNEMONIC: - msg = _("Enter your BIP39 mnemonic:") - else: - msg = _("Enter the master private key beginning with xprv:") - def set_enabled(): - from electrum.keystore import is_xprv - wizard.next_button.setEnabled(is_xprv(clean_text(text))) - text.textChanged.connect(set_enabled) - next_enabled = False - - vbox.addWidget(QLabel(msg)) - vbox.addWidget(text) - pin = QLineEdit() - pin.setValidator(QRegExpValidator(QRegExp('[1-9]{0,9}'))) - pin.setMaximumWidth(100) - hbox_pin = QHBoxLayout() - hbox_pin.addWidget(QLabel(_("Enter your PIN (digits 1-9):"))) - hbox_pin.addWidget(pin) - hbox_pin.addStretch(1) - - if method in [TIM_NEW, TIM_RECOVER]: - vbox.addWidget(WWLabel(RECOMMEND_PIN)) - vbox.addWidget(cb_pin) - else: - vbox.addLayout(hbox_pin) - - passphrase_msg = WWLabel(PASSPHRASE_HELP_SHORT) - passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN) - passphrase_warning.setStyleSheet("color: red") - cb_phrase = QCheckBox(_('Enable passphrases')) - cb_phrase.setChecked(False) - vbox.addWidget(passphrase_msg) - vbox.addWidget(passphrase_warning) - vbox.addWidget(cb_phrase) - - wizard.exec_layout(vbox, next_enabled=next_enabled) - - if method in [TIM_NEW, TIM_RECOVER]: - item = bg.checkedId() - pin = cb_pin.isChecked() - else: - item = ' '.join(str(clean_text(text)).split()) - pin = str(pin.text()) - - return (item, name.text(), pin, cb_phrase.isChecked()) - - - - -class SettingsDialog(WindowModalDialog): - '''This dialog doesn't require a device be paired with a wallet. - We want users to be able to wipe a device even if they've forgotten - their PIN.''' - - def __init__(self, window, plugin, keystore, device_id): - title = _("{} Settings").format(plugin.device) - super(SettingsDialog, self).__init__(window, title) - self.setMaximumWidth(540) - - devmgr = plugin.device_manager() - config = devmgr.config - handler = keystore.handler - thread = keystore.thread - hs_rows, hs_cols = (64, 128) - - def invoke_client(method, *args, **kw_args): - unpair_after = kw_args.pop('unpair_after', False) - - def task(): - client = devmgr.client_by_id(device_id) - if not client: - raise RuntimeError("Device not connected") - if method: - getattr(client, method)(*args, **kw_args) - if unpair_after: - devmgr.unpair_id(device_id) - return client.features - - thread.add(task, on_success=update) - - def update(features): - self.features = features - set_label_enabled() - if features.bootloader_hash: - bl_hash = bh2u(features.bootloader_hash) - bl_hash = "\n".join([bl_hash[:32], bl_hash[32:]]) - else: - bl_hash = "N/A" - noyes = [_("No"), _("Yes")] - endis = [_("Enable Passphrases"), _("Disable Passphrases")] - disen = [_("Disabled"), _("Enabled")] - setchange = [_("Set a PIN"), _("Change PIN")] - - version = "%d.%d.%d" % (features.major_version, - features.minor_version, - features.patch_version) - coins = ", ".join(coin.coin_name for coin in features.coins) - - device_label.setText(features.label) - pin_set_label.setText(noyes[features.pin_protection]) - passphrases_label.setText(disen[features.passphrase_protection]) - bl_hash_label.setText(bl_hash) - label_edit.setText(features.label) - device_id_label.setText(features.device_id) - initialized_label.setText(noyes[features.initialized]) - version_label.setText(version) - coins_label.setText(coins) - clear_pin_button.setVisible(features.pin_protection) - clear_pin_warning.setVisible(features.pin_protection) - pin_button.setText(setchange[features.pin_protection]) - pin_msg.setVisible(not features.pin_protection) - passphrase_button.setText(endis[features.passphrase_protection]) - language_label.setText(features.language) - - def set_label_enabled(): - label_apply.setEnabled(label_edit.text() != self.features.label) - - def rename(): - invoke_client('change_label', label_edit.text()) - - def toggle_passphrase(): - title = _("Confirm Toggle Passphrase Protection") - currently_enabled = self.features.passphrase_protection - if currently_enabled: - msg = _("After disabling passphrases, you can only pair this " - "Electrum wallet if it had an empty passphrase. " - "If its passphrase was not empty, you will need to " - "create a new wallet with the install wizard. You " - "can use this wallet again at any time by re-enabling " - "passphrases and entering its passphrase.") - else: - msg = _("Your current Electrum wallet can only be used with " - "an empty passphrase. You must create a separate " - "wallet with the install wizard for other passphrases " - "as each one generates a new set of addresses.") - msg += "\n\n" + _("Are you sure you want to proceed?") - if not self.question(msg, title=title): - return - invoke_client('toggle_passphrase', unpair_after=currently_enabled) - - def change_homescreen(): - dialog = QFileDialog(self, _("Choose Homescreen")) - filename, __ = dialog.getOpenFileName() - - if filename.endswith('.toif'): - img = open(filename, 'rb').read() - if img[:8] != b'TOIf\x90\x00\x90\x00': - raise Exception('File is not a TOIF file with size of 144x144') - else: - from PIL import Image # FIXME - im = Image.open(filename) - if im.size != (128, 64): - raise Exception('Image must be 128 x 64 pixels') - im = im.convert('1') - pix = im.load() - img = bytearray(1024) - for j in range(64): - for i in range(128): - if pix[i, j]: - o = (i + j * 128) - img[o // 8] |= (1 << (7 - o % 8)) - img = bytes(img) - invoke_client('change_homescreen', img) - - def clear_homescreen(): - invoke_client('change_homescreen', b'\x00') - - def set_pin(): - invoke_client('set_pin', remove=False) - - def clear_pin(): - invoke_client('set_pin', remove=True) - - def wipe_device(): - wallet = window.wallet - if wallet and sum(wallet.get_balance()): - title = _("Confirm Device Wipe") - msg = _("Are you SURE you want to wipe the device?\n" - "Your wallet still has bitcoins in it!") - if not self.question(msg, title=title, - icon=QMessageBox.Critical): - return - invoke_client('wipe_device', unpair_after=True) - - def slider_moved(): - mins = timeout_slider.sliderPosition() - timeout_minutes.setText(_("%2d minutes") % mins) - - def slider_released(): - config.set_session_timeout(timeout_slider.sliderPosition() * 60) - - # Information tab - info_tab = QWidget() - info_layout = QVBoxLayout(info_tab) - info_glayout = QGridLayout() - info_glayout.setColumnStretch(2, 1) - device_label = QLabel() - pin_set_label = QLabel() - passphrases_label = QLabel() - version_label = QLabel() - device_id_label = QLabel() - bl_hash_label = QLabel() - bl_hash_label.setWordWrap(True) - coins_label = QLabel() - coins_label.setWordWrap(True) - language_label = QLabel() - initialized_label = QLabel() - rows = [ - (_("Device Label"), device_label), - (_("PIN set"), pin_set_label), - (_("Passphrases"), passphrases_label), - (_("Firmware Version"), version_label), - (_("Device ID"), device_id_label), - (_("Bootloader Hash"), bl_hash_label), - (_("Supported Coins"), coins_label), - (_("Language"), language_label), - (_("Initialized"), initialized_label), - ] - for row_num, (label, widget) in enumerate(rows): - info_glayout.addWidget(QLabel(label), row_num, 0) - info_glayout.addWidget(widget, row_num, 1) - info_layout.addLayout(info_glayout) - - # Settings tab - settings_tab = QWidget() - settings_layout = QVBoxLayout(settings_tab) - settings_glayout = QGridLayout() - - # Settings tab - Label - label_msg = QLabel(_("Name this {}. If you have multiple devices " - "their labels help distinguish them.") - .format(plugin.device)) - label_msg.setWordWrap(True) - label_label = QLabel(_("Device Label")) - label_edit = QLineEdit() - label_edit.setMinimumWidth(150) - label_edit.setMaxLength(plugin.MAX_LABEL_LEN) - label_apply = QPushButton(_("Apply")) - label_apply.clicked.connect(rename) - label_edit.textChanged.connect(set_label_enabled) - settings_glayout.addWidget(label_label, 0, 0) - settings_glayout.addWidget(label_edit, 0, 1, 1, 2) - settings_glayout.addWidget(label_apply, 0, 3) - settings_glayout.addWidget(label_msg, 1, 1, 1, -1) - - # Settings tab - PIN - pin_label = QLabel(_("PIN Protection")) - pin_button = QPushButton() - pin_button.clicked.connect(set_pin) - settings_glayout.addWidget(pin_label, 2, 0) - settings_glayout.addWidget(pin_button, 2, 1) - pin_msg = QLabel(_("PIN protection is strongly recommended. " - "A PIN is your only protection against someone " - "stealing your bitcoins if they obtain physical " - "access to your {}.").format(plugin.device)) - pin_msg.setWordWrap(True) - pin_msg.setStyleSheet("color: red") - settings_glayout.addWidget(pin_msg, 3, 1, 1, -1) - - # Settings tab - Homescreen - if plugin.device != 'KeepKey': # Not yet supported by KK firmware - homescreen_layout = QHBoxLayout() - homescreen_label = QLabel(_("Homescreen")) - homescreen_change_button = QPushButton(_("Change...")) - homescreen_clear_button = QPushButton(_("Reset")) - homescreen_change_button.clicked.connect(change_homescreen) - homescreen_clear_button.clicked.connect(clear_homescreen) - homescreen_msg = QLabel(_("You can set the homescreen on your " - "device to personalize it. You must " - "choose a {} x {} monochrome black and " - "white image.").format(hs_rows, hs_cols)) - homescreen_msg.setWordWrap(True) - settings_glayout.addWidget(homescreen_label, 4, 0) - settings_glayout.addWidget(homescreen_change_button, 4, 1) - settings_glayout.addWidget(homescreen_clear_button, 4, 2) - settings_glayout.addWidget(homescreen_msg, 5, 1, 1, -1) - - # Settings tab - Session Timeout - timeout_label = QLabel(_("Session Timeout")) - timeout_minutes = QLabel() - timeout_slider = QSlider(Qt.Horizontal) - timeout_slider.setRange(1, 60) - timeout_slider.setSingleStep(1) - timeout_slider.setTickInterval(5) - timeout_slider.setTickPosition(QSlider.TicksBelow) - timeout_slider.setTracking(True) - timeout_msg = QLabel( - _("Clear the session after the specified period " - "of inactivity. Once a session has timed out, " - "your PIN and passphrase (if enabled) must be " - "re-entered to use the device.")) - timeout_msg.setWordWrap(True) - timeout_slider.setSliderPosition(config.get_session_timeout() // 60) - slider_moved() - timeout_slider.valueChanged.connect(slider_moved) - timeout_slider.sliderReleased.connect(slider_released) - settings_glayout.addWidget(timeout_label, 6, 0) - settings_glayout.addWidget(timeout_slider, 6, 1, 1, 3) - settings_glayout.addWidget(timeout_minutes, 6, 4) - settings_glayout.addWidget(timeout_msg, 7, 1, 1, -1) - settings_layout.addLayout(settings_glayout) - settings_layout.addStretch(1) - - # Advanced tab - advanced_tab = QWidget() - advanced_layout = QVBoxLayout(advanced_tab) - advanced_glayout = QGridLayout() - - # Advanced tab - clear PIN - clear_pin_button = QPushButton(_("Disable PIN")) - clear_pin_button.clicked.connect(clear_pin) - clear_pin_warning = QLabel( - _("If you disable your PIN, anyone with physical access to your " - "{} device can spend your bitcoins.").format(plugin.device)) - clear_pin_warning.setWordWrap(True) - clear_pin_warning.setStyleSheet("color: red") - advanced_glayout.addWidget(clear_pin_button, 0, 2) - advanced_glayout.addWidget(clear_pin_warning, 1, 0, 1, 5) - - # Advanced tab - toggle passphrase protection - passphrase_button = QPushButton() - passphrase_button.clicked.connect(toggle_passphrase) - passphrase_msg = WWLabel(PASSPHRASE_HELP) - passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN) - passphrase_warning.setStyleSheet("color: red") - advanced_glayout.addWidget(passphrase_button, 3, 2) - advanced_glayout.addWidget(passphrase_msg, 4, 0, 1, 5) - advanced_glayout.addWidget(passphrase_warning, 5, 0, 1, 5) - - # Advanced tab - wipe device - wipe_device_button = QPushButton(_("Wipe Device")) - wipe_device_button.clicked.connect(wipe_device) - wipe_device_msg = QLabel( - _("Wipe the device, removing all data from it. The firmware " - "is left unchanged.")) - wipe_device_msg.setWordWrap(True) - wipe_device_warning = QLabel( - _("Only wipe a device if you have the recovery seed written down " - "and the device wallet(s) are empty, otherwise the bitcoins " - "will be lost forever.")) - wipe_device_warning.setWordWrap(True) - wipe_device_warning.setStyleSheet("color: red") - advanced_glayout.addWidget(wipe_device_button, 6, 2) - advanced_glayout.addWidget(wipe_device_msg, 7, 0, 1, 5) - advanced_glayout.addWidget(wipe_device_warning, 8, 0, 1, 5) - advanced_layout.addLayout(advanced_glayout) - advanced_layout.addStretch(1) - - tabs = QTabWidget(self) - tabs.addTab(info_tab, _("Information")) - tabs.addTab(settings_tab, _("Settings")) - tabs.addTab(advanced_tab, _("Advanced")) - dialog_vbox = QVBoxLayout(self) - dialog_vbox.addWidget(tabs) - dialog_vbox.addLayout(Buttons(CloseButton(self))) - - # Update information - invoke_client(None)