Browse Source

wip. trezor works for standard wallet, also for cosigners

master
Sander van Grieken 2 years ago
parent
commit
b7ed4c569b
  1. 1
      electrum/gui/qt/password_dialog.py
  2. 217
      electrum/gui/qt/wizard/wallet.py
  3. 21
      electrum/gui/qt/wizard/wizard.py
  4. 278
      electrum/plugins/trezor/qt.py
  5. 27
      electrum/plugins/trezor/trezor.py
  6. 78
      electrum/wizard.py

1
electrum/gui/qt/password_dialog.py

@ -136,6 +136,7 @@ class PasswordLayout(object):
and not force_disable_encrypt_cb)
self.new_pw.textChanged.connect(enable_OK)
self.conf_pw.textChanged.connect(enable_OK)
enable_OK()
self.vbox = vbox

217
electrum/gui/qt/wizard/wallet.py

@ -1,7 +1,10 @@
import os
import sys
import threading
from typing import TYPE_CHECKING
from PyQt5.QtCore import Qt, QTimer, QRect
from PyQt5.QtCore import Qt, QTimer, QRect, pyqtSignal
from PyQt5.QtGui import QPen, QPainter, QPalette
from PyQt5.QtWidgets import (QApplication, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QWidget,
QFileDialog, QSlider, QGridLayout)
@ -10,16 +13,16 @@ from electrum.bip32 import is_bip32_derivation, BIP32Node, normalize_bip32_deriv
from electrum.daemon import Daemon
from electrum.i18n import _
from electrum.keystore import bip44_derivation, bip39_to_seed, purpose48_derivation
from electrum.plugin import run_hook
from electrum.plugin import run_hook, HardwarePluginLibraryUnavailable
from electrum.storage import StorageReadWriteError
from electrum.util import WalletFileException, get_new_wallet_name
from electrum.wallet import wallet_types
from .wizard import QEAbstractWizard, WizardComponent
from electrum.logging import get_logger
from electrum.logging import get_logger, Logger
from electrum import WalletStorage, mnemonic, keystore
from electrum.wizard import NewWalletWizard
from ..bip39_recovery_dialog import Bip39RecoveryDialog
from ..password_dialog import PasswordLayout, PW_NEW, MSG_ENTER_PASSWORD
from ..password_dialog import PasswordLayout, PW_NEW, MSG_ENTER_PASSWORD, PasswordLayoutForHW
from ..seed_dialog import SeedLayout, MSG_PASSPHRASE_WARN_ISSUE4566, KeysLayout
from ..util import ChoicesLayout, PasswordLineEdit, char_width_in_lineedit, WWLabel, InfoButton, font_height
@ -35,12 +38,17 @@ WIF_HELP_TEXT = (_('WIF keys are typed in Electrum, based on script type.') + '\
'p2wpkh-p2sh:KxZcY47uGp9a... \t-> 3NhNeZQXF...\n' +
'p2wpkh:KxZcY47uGp9a... \t-> bc1q3fjfk...')
MSG_HW_STORAGE_ENCRYPTION = _("Set wallet file encryption.") + '\n'\
+ _("Your wallet file does not contain secrets, mostly just metadata. ") \
+ _("It also contains your master public key that allows watching your addresses.") + '\n\n'\
+ _("Note: If you enable this setting, you will need your hardware device to open your wallet.")
class QENewWalletWizard(NewWalletWizard, QEAbstractWizard):
_logger = get_logger(__name__)
def __init__(self, config: 'SimpleConfig', app: 'QElectrumApplication', plugins: 'Plugins', daemon: Daemon, path, parent=None):
NewWalletWizard.__init__(self, daemon)
NewWalletWizard.__init__(self, daemon, plugins)
QEAbstractWizard.__init__(self, config, app, plugins, daemon)
self._daemon = daemon # TODO: dedupe
self._path = path
@ -53,15 +61,18 @@ class QENewWalletWizard(NewWalletWizard, QEAbstractWizard):
'create_seed': { 'gui': WCCreateSeed },
'confirm_seed': { 'gui': WCConfirmSeed },
'have_seed': { 'gui': WCHaveSeed },
'choose_hardware_device': { 'gui': WCChooseHWDevice },
'script_and_derivation': { 'gui': WCScriptAndDerivation},
'have_master_key': { 'gui': WCHaveMasterKey },
'multisig': { 'gui': WCMultisig },
'multisig_cosigner_keystore': { 'gui': WCCosignerKeystore },
'multisig_cosigner_key': { 'gui': WCHaveMasterKey },
'multisig_cosigner_seed': { 'gui': WCHaveSeed },
'multisig_cosigner_hardware': { 'gui': WCChooseHWDevice },
'multisig_cosigner_script_and_derivation': { 'gui': WCScriptAndDerivation},
'imported': { 'gui': WCImport },
'wallet_password': { 'gui': WCWalletPassword }
'wallet_password': { 'gui': WCWalletPassword },
'wallet_password_hardware': { 'gui': WCWalletPasswordHardware }
})
# modify default flow, insert seed extension entry/confirm as separate views
@ -101,7 +112,7 @@ class QENewWalletWizard(NewWalletWizard, QEAbstractWizard):
'next': self.on_have_cosigner_seed,
'last': lambda d: self.is_single_password() and self.last_cosigner(d) and not self.needs_derivation_path(d),
'gui': WCEnterExt
}
},
})
run_hook('init_wallet_wizard', self)
@ -565,7 +576,7 @@ class WCScriptAndDerivation(WizardComponent):
('p2wpkh', 'native segwit (p2wpkh)', bip44_derivation(0, bip43_purpose=84)),
]
if self.wizard_data['wallet_type'] == 'standard':
if self.wizard_data['wallet_type'] == 'standard' and not self.wizard_data['keystore_type'] == 'hardware':
button = QPushButton(_("Detect Existing Accounts"))
passphrase = self.wizard_data['seed_extra_words'] if self.wizard_data['seed_extend'] else ''
@ -619,7 +630,12 @@ class WCScriptAndDerivation(WizardComponent):
cosigner_data = self._current_cosigner(self.wizard_data)
derivation_valid = is_bip32_derivation(cosigner_data['derivation_path'])
if derivation_valid and self.wizard_data['wallet_type'] == 'multisig':
# TODO: refactor to wizard.current_cosigner_is_hardware
cosigner_is_hardware = cosigner_data == self.wizard_data and self.wizard_data['keystore_type'] == 'hardware'
if 'cosigner_keystore_type' in self.wizard_data and self.wizard_data['cosigner_keystore_type'] == 'hardware':
cosigner_is_hardware = True
if self.wizard.is_multisig(self.wizard_data) and derivation_valid and not cosigner_is_hardware:
if self.wizard.has_duplicate_masterkeys(self.wizard_data):
self._logger.debug('Duplicate master keys!')
# TODO: user feedback
@ -645,7 +661,7 @@ class WCCosignerKeystore(WizardComponent):
choices = [
('key', _('Enter cosigner key')),
('seed', _('Enter cosigner seed')),
('hw_device', _('Cosign with hardware device'))
('hardware', _('Cosign with hardware device'))
]
self.c_values = [x[0] for x in choices]
@ -929,3 +945,184 @@ class CosignWidget(QWidget):
qp.setBrush(Qt.green if i < self.m else Qt.gray)
qp.drawPie(self.R, alpha, alpha2)
qp.end()
class WCChooseHWDevice(WizardComponent, Logger):
scanFailed = pyqtSignal([str, str], arguments=['code', 'message'])
scanComplete = pyqtSignal()
def __init__(self, parent, wizard):
WizardComponent.__init__(self, parent, wizard, title=_('Choose Hardware Device'))
Logger.__init__(self)
self.scanFailed.connect(self.on_scan_failed)
self.scanComplete.connect(self.on_scan_complete)
self.plugins = wizard.plugins
self.error_l = WWLabel()
self.error_l.setVisible(False)
self.device_list = QWidget()
self.device_list_layout = QVBoxLayout()
self.device_list.setLayout(self.device_list_layout)
self.clayout = None
self.rescan_button = QPushButton(_('Rescan devices'))
self.rescan_button.clicked.connect(self.on_rescan)
self.layout().addWidget(self.error_l)
self.layout().addWidget(self.device_list)
self.layout().addStretch(1)
self.layout().addWidget(self.rescan_button)
self.c_values = []
def on_ready(self):
self.scan_devices()
def on_rescan(self):
self.scan_devices()
def on_scan_failed(self, code, message):
self.error_l.setText(message)
self.error_l.setVisible(True)
self.device_list.setVisible(False)
self.valid = False
def on_scan_complete(self):
self.error_l.setVisible(False)
self.device_list.setVisible(True)
choices = []
for name, info in self.devices:
state = _("initialized") if info.initialized else _("wiped")
label = info.label or _("An unnamed {}").format(name)
try:
transport_str = info.device.transport_ui_string[:20]
except Exception:
transport_str = 'unknown transport'
descr = f"{label} [{info.model_name or name}, {state}, {transport_str}]"
choices.append(((name, info), descr))
msg = _('Select a device') + ':'
self.c_values = [x[0] for x in choices]
c_titles = [x[1] for x in choices]
# remove old component before adding anew
a = self.device_list.layout().itemAt(0)
self.device_list.layout().removeItem(a)
self.clayout = ChoicesLayout(msg, c_titles)
self.device_list_layout = self.clayout.layout()
self.device_list.layout().addLayout(self.device_list_layout)
self.valid = True
def failed_getting_device_infos(self, debug_msg, name, e):
# nonlocal debug_msg
err_str_oneline = ' // '.join(str(e).splitlines())
self.logger.warning(f'error getting device infos for {name}: {err_str_oneline}')
indented_error_msg = ' '.join([''] + str(e).splitlines(keepends=True))
debug_msg += f' {name}: (error getting device infos)\n{indented_error_msg}\n'
def scan_devices(self):
self.valid = False
self.busy_msg = _('Scanning devices...')
self.busy = True
def scan_task():
# check available plugins
supported_plugins = self.plugins.get_hardware_support()
devices = [] # type: List[Tuple[str, DeviceInfo]]
devmgr = self.plugins.device_manager
debug_msg = ''
# scan devices
try:
# scanned_devices = self.run_task_without_blocking_gui(task=devmgr.scan_devices,
# msg=_("Scanning devices..."))
scanned_devices = devmgr.scan_devices()
except BaseException as e:
self.logger.info('error scanning devices: {}'.format(repr(e)))
debug_msg = ' {}:\n {}'.format(_('Error scanning devices'), e)
else:
for splugin in supported_plugins:
name, plugin = splugin.name, splugin.plugin
# plugin init errored?
if not plugin:
e = splugin.exception
indented_error_msg = ' '.join([''] + str(e).splitlines(keepends=True))
debug_msg += f' {name}: (error during plugin init)\n'
debug_msg += ' {}\n'.format(_('You might have an incompatible library.'))
debug_msg += f'{indented_error_msg}\n'
continue
# see if plugin recognizes 'scanned_devices'
try:
# FIXME: side-effect: this sets client.handler
device_infos = devmgr.list_pairable_device_infos(
handler=None, plugin=plugin, devices=scanned_devices, include_failing_clients=True)
except HardwarePluginLibraryUnavailable as e:
self.failed_getting_device_infos(debug_msg, name, e)
continue
except BaseException as e:
self.logger.exception('')
self.failed_getting_device_infos(debug_msg, name, e)
continue
device_infos_failing = list(filter(lambda di: di.exception is not None, device_infos))
for di in device_infos_failing:
self.failed_getting_device_infos(debug_msg, name, di.exception)
device_infos_working = list(filter(lambda di: di.exception is None, device_infos))
devices += list(map(lambda x: (name, x), device_infos_working))
if not debug_msg:
debug_msg = ' {}'.format(_('No exceptions encountered.'))
if not devices:
msg = (_('No hardware device detected.') + '\n' +
_('To trigger a rescan, press \'Rescan devices\'.') + '\n\n')
if sys.platform == 'win32':
msg += _('If your device is not detected on Windows, go to "Settings", "Devices", "Connected devices", '
'and do "Remove device". Then, plug your device again.') + '\n'
msg += _('While this is less than ideal, it might help if you run Electrum as Administrator.') + '\n'
else:
msg += _('On Linux, you might have to add a new permission to your udev rules.') + '\n'
msg += '\n\n'
msg += _('Debug message') + '\n' + debug_msg
self.scanFailed.emit('no_devices', msg)
self.busy = False
return
# select device
self.devices = devices
self.scanComplete.emit()
self.busy = False
t = threading.Thread(target=scan_task, daemon=True)
t.start()
def apply(self):
if self.clayout:
# TODO: data is not (de)serializable yet, wizard_data cannot be persisted
self.wizard_data['hardware_device'] = self.c_values[self.clayout.selected_index()]
self.logger.debug(repr(self.wizard_data['hardware_device']))
class WCWalletPasswordHardware(WizardComponent):
def __init__(self, parent, wizard):
WizardComponent.__init__(self, parent, wizard, title=_('Password HW'))
self.plugins = wizard.plugins
self.playout = PasswordLayoutForHW(MSG_HW_STORAGE_ENCRYPTION)
self.playout.encrypt_cb.setChecked(True)
self.layout().addLayout(self.playout.layout())
self.layout().addStretch(1)
self._valid = True
def apply(self):
self.wizard_data['encrypt'] = self.playout.encrypt_cb.isChecked()
if self.playout.encrypt_cb.isChecked():
_name, _info = self.wizard_data['hardware_device']
device_id = _info.device.id_
client = self.plugins.device_manager.client_by_id(device_id, scan_now=False)
# client.handler = self.plugin.create_handler(self.wizard)
self.wizard_data['password'] = client.get_password_for_storage_encryption()

21
electrum/gui/qt/wizard/wizard.py

@ -1,3 +1,4 @@
import threading
from abc import abstractmethod
from typing import TYPE_CHECKING
@ -8,7 +9,7 @@ from PyQt5.QtWidgets import (QDialog, QPushButton, QWidget, QLabel, QVBoxLayout,
from electrum.i18n import _
from electrum.logging import get_logger
from electrum.gui.qt.util import Buttons, icon_path
from electrum.gui.qt.util import Buttons, icon_path, MessageBoxMixin
if TYPE_CHECKING:
from electrum.simple_config import SimpleConfig
@ -17,7 +18,7 @@ if TYPE_CHECKING:
from electrum.gui.qt import QElectrumApplication
class QEAbstractWizard(QDialog):
class QEAbstractWizard(QDialog, MessageBoxMixin):
_logger = get_logger(__name__)
# def __init__(self, config: 'SimpleConfig', app: QApplication, plugins: 'Plugins', *, gui_object: 'ElectrumGui'):
@ -25,8 +26,11 @@ class QEAbstractWizard(QDialog):
QDialog.__init__(self, None)
self.app = app
self.config = config
self.plugins = plugins
# self.gui_thread = gui_object.gui_thread
# self.plugins = plugins
# compat
self.gui_thread = threading.current_thread()
self.setMinimumSize(600, 400)
self.title = QLabel()
@ -43,9 +47,9 @@ class QEAbstractWizard(QDialog):
please_wait_layout = QVBoxLayout()
please_wait_layout.addStretch(1)
please_wait_l = QLabel(_("Please wait..."))
please_wait_l.setAlignment(Qt.AlignCenter)
please_wait_layout.addWidget(please_wait_l)
self.please_wait_l = QLabel(_("Please wait..."))
self.please_wait_l.setAlignment(Qt.AlignCenter)
please_wait_layout.addWidget(self.please_wait_l)
please_wait_layout.addStretch(1)
self.please_wait = QWidget()
self.please_wait.setLayout(please_wait_layout)
@ -156,6 +160,7 @@ class QEAbstractWizard(QDialog):
self.next_button.setEnabled(page.valid)
self.main_widget.setVisible(not page.busy and not bool(page.error))
self.please_wait.setVisible(page.busy)
self.please_wait_l.setText(page.busy_msg if page.busy_msg else _("Please wait..."))
self.error_msg.setText(str(page.error))
self.error.setVisible(not page.busy and bool(page.error))
icon = page.params.get('icon', icon_path('electrum.png'))
@ -213,6 +218,7 @@ class WizardComponent(QWidget):
self.setLayout(layout if layout else QVBoxLayout(self))
self.wizard_data = {}
self.title = title if title is not None else 'No title'
self.busy_msg = ''
self.wizard = wizard
self._error = ''
self._valid = False
@ -262,6 +268,7 @@ class WizardComponent(QWidget):
self.updated.emit(self)
# returns (sub)dict of current cosigner (or root if first)
# TODO: maybe just always expose self.cosigner_data in wizardcomponent so we can avoid this call
def _current_cosigner(self, wizard_data):
wdata = wizard_data
if wizard_data['wallet_type'] == 'multisig' and 'multisig_current_cosigner' in wizard_data:

278
electrum/plugins/trezor/qt.py

@ -8,7 +8,7 @@ from PyQt5.QtWidgets import (QVBoxLayout, QLabel, QGridLayout, QPushButton,
QMessageBox, QFileDialog, QSlider, QTabWidget)
from electrum.gui.qt.util import (WindowModalDialog, WWLabel, Buttons, CancelButton,
OkButton, CloseButton, PasswordLineEdit, getOpenFileName)
OkButton, CloseButton, PasswordLineEdit, getOpenFileName, ChoicesLayout)
from electrum.i18n import _
from electrum.plugin import hook
@ -16,7 +16,9 @@ from ..hw_wallet.qt import QtHandlerBase, QtPluginBase
from ..hw_wallet.plugin import only_hook_if_libraries_available
from .trezor import (TrezorPlugin, TIM_NEW, TIM_RECOVER, TrezorInitSettings,
PASSPHRASE_ON_DEVICE, Capability, BackupType, RecoveryDeviceType)
from ...gui.qt.wizard.wallet import WCScriptAndDerivation
from ...gui.qt.wizard.wizard import WizardComponent
from ...logging import Logger
PASSPHRASE_HELP_SHORT =_(
"Passphrases allow you to access new wallets, each "
@ -255,10 +257,25 @@ class QtPlugin(QtPluginBase):
keystore.thread.add(connect, on_success=show_dialog)
def request_trezor_init_settings(self, wizard, method, device_id):
vbox = QVBoxLayout()
next_enabled = True
vbox = InitSettingsLayout(self.device_manager(), method, device_id)
wizard.exec_layout(vbox)
return TrezorInitSettings(
word_count=vbox.bg_numwords.checkedId(),
label=vbox.name.text(),
pin_enabled=vbox.cb_pin.isChecked(),
passphrase_enabled=vbox.cb_phrase.isChecked(),
recovery_type=vbox.bg_rectype.checkedId() if vbox.bg_rectype else None,
backup_type=vbox.bg_backuptype.checkedId(),
no_backup=vbox.cb_no_backup.isChecked() if vbox.cb_no_backup else False,
)
class InitSettingsLayout(QVBoxLayout):
def __init__(self, devmgr, method, device_id) -> QVBoxLayout:
super().__init__()
devmgr = self.device_manager()
client = devmgr.client_by_id(device_id)
if not client:
raise Exception(_("The device was disconnected."))
@ -269,40 +286,40 @@ class QtPlugin(QtPluginBase):
# label
label = QLabel(_("Enter a label to name your device:"))
name = QLineEdit()
self.name = QLineEdit()
hl = QHBoxLayout()
hl.addWidget(label)
hl.addWidget(name)
hl.addWidget(self.name)
hl.addStretch(1)
vbox.addLayout(hl)
self.addLayout(hl)
# Backup type
gb_backuptype = QGroupBox()
hbox_backuptype = QHBoxLayout()
gb_backuptype.setLayout(hbox_backuptype)
vbox.addWidget(gb_backuptype)
self.addWidget(gb_backuptype)
gb_backuptype.setTitle(_('Select backup type:'))
bg_backuptype = QButtonGroup()
self.bg_backuptype = QButtonGroup()
rb_single = QRadioButton(gb_backuptype)
rb_single.setText(_('Single seed (BIP39)'))
bg_backuptype.addButton(rb_single)
bg_backuptype.setId(rb_single, BackupType.Bip39)
self.bg_backuptype.addButton(rb_single)
self.bg_backuptype.setId(rb_single, BackupType.Bip39)
hbox_backuptype.addWidget(rb_single)
rb_single.setChecked(True)
rb_shamir = QRadioButton(gb_backuptype)
rb_shamir.setText(_('Shamir'))
bg_backuptype.addButton(rb_shamir)
bg_backuptype.setId(rb_shamir, BackupType.Slip39_Basic)
self.bg_backuptype.addButton(rb_shamir)
self.bg_backuptype.setId(rb_shamir, BackupType.Slip39_Basic)
hbox_backuptype.addWidget(rb_shamir)
rb_shamir.setEnabled(Capability.Shamir in capabilities)
rb_shamir.setVisible(False) # visible with "expert settings"
rb_shamir_groups = QRadioButton(gb_backuptype)
rb_shamir_groups.setText(_('Super Shamir'))
bg_backuptype.addButton(rb_shamir_groups)
bg_backuptype.setId(rb_shamir_groups, BackupType.Slip39_Advanced)
self.bg_backuptype.addButton(rb_shamir_groups)
self.bg_backuptype.setId(rb_shamir_groups, BackupType.Slip39_Advanced)
hbox_backuptype.addWidget(rb_shamir_groups)
rb_shamir_groups.setEnabled(Capability.ShamirGroups in capabilities)
rb_shamir_groups.setVisible(False) # visible with "expert settings"
@ -313,15 +330,15 @@ class QtPlugin(QtPluginBase):
gb_numwords = QGroupBox()
hbox1 = QHBoxLayout()
gb_numwords.setLayout(hbox1)
vbox.addWidget(gb_numwords)
self.addWidget(gb_numwords)
gb_numwords.setTitle(_("Select seed/share length:"))
bg_numwords = QButtonGroup()
self.bg_numwords = QButtonGroup()
for count in (12, 18, 20, 24, 33):
rb = QRadioButton(gb_numwords)
word_count_buttons[count] = rb
rb.setText(_("{:d} words").format(count))
bg_numwords.addButton(rb)
bg_numwords.setId(rb, count)
self.bg_numwords.addButton(rb)
self.bg_numwords.setId(rb, count)
hbox1.addWidget(rb)
rb.setChecked(True)
@ -348,7 +365,7 @@ class QtPlugin(QtPluginBase):
for c, btn in word_count_buttons.items():
btn.setVisible(c in valid_word_counts)
bg_backuptype.buttonClicked.connect(configure_word_counts)
self.bg_backuptype.buttonClicked.connect(configure_word_counts)
configure_word_counts()
# set up conditional visibility:
@ -359,10 +376,10 @@ class QtPlugin(QtPluginBase):
gb_numwords.setVisible(False)
# PIN
cb_pin = QCheckBox(_('Enable PIN protection'))
cb_pin.setChecked(True)
vbox.addWidget(WWLabel(RECOMMEND_PIN))
vbox.addWidget(cb_pin)
self.cb_pin = QCheckBox(_('Enable PIN protection'))
self.cb_pin.setChecked(True)
self.addWidget(WWLabel(RECOMMEND_PIN))
self.addWidget(self.cb_pin)
# "expert settings" button
expert_vbox = QVBoxLayout()
@ -376,66 +393,55 @@ class QtPlugin(QtPluginBase):
rb_shamir.setVisible(True)
rb_shamir_groups.setVisible(True)
expert_button.clicked.connect(show_expert_settings)
vbox.addWidget(expert_button)
self.addWidget(expert_button)
# passphrase
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)
self.cb_phrase = QCheckBox(_('Enable passphrases'))
self.cb_phrase.setChecked(False)
expert_vbox.addWidget(passphrase_msg)
expert_vbox.addWidget(passphrase_warning)
expert_vbox.addWidget(cb_phrase)
expert_vbox.addWidget(self.cb_phrase)
# ask for recovery type (random word order OR matrix)
bg_rectype = None
self.bg_rectype = None
if method == TIM_RECOVER and model == '1':
gb_rectype = QGroupBox()
hbox_rectype = QHBoxLayout()
gb_rectype.setLayout(hbox_rectype)
expert_vbox.addWidget(gb_rectype)
gb_rectype.setTitle(_("Select recovery type:"))
bg_rectype = QButtonGroup()
self.bg_rectype = QButtonGroup()
rb1 = QRadioButton(gb_rectype)
rb1.setText(_('Scrambled words'))
bg_rectype.addButton(rb1)
bg_rectype.setId(rb1, RecoveryDeviceType.ScrambledWords)
self.bg_rectype.addButton(rb1)
self.bg_rectype.setId(rb1, RecoveryDeviceType.ScrambledWords)
hbox_rectype.addWidget(rb1)
rb1.setChecked(True)
rb2 = QRadioButton(gb_rectype)
rb2.setText(_('Matrix'))
bg_rectype.addButton(rb2)
bg_rectype.setId(rb2, RecoveryDeviceType.Matrix)
self.bg_rectype.addButton(rb2)
self.bg_rectype.setId(rb2, RecoveryDeviceType.Matrix)
hbox_rectype.addWidget(rb2)
# no backup
cb_no_backup = None
self.cb_no_backup = None
if method == TIM_NEW:
cb_no_backup = QCheckBox(f'''{_('Enable seedless mode')}''')
cb_no_backup.setChecked(False)
self.cb_no_backup = QCheckBox(f'''{_('Enable seedless mode')}''')
self.cb_no_backup.setChecked(False)
if (model == '1' and fw_version >= (1, 7, 1)
or model == 'T' and fw_version >= (2, 0, 9)):
cb_no_backup.setToolTip(SEEDLESS_MODE_WARNING)
self.cb_no_backup.setToolTip(SEEDLESS_MODE_WARNING)
else:
cb_no_backup.setEnabled(False)
cb_no_backup.setToolTip(_('Firmware version too old.'))
expert_vbox.addWidget(cb_no_backup)
self.cb_no_backup.setEnabled(False)
self.cb_no_backup.setToolTip(_('Firmware version too old.'))
expert_vbox.addWidget(self.cb_no_backup)
vbox.addWidget(expert_widget)
wizard.exec_layout(vbox, next_enabled=next_enabled)
return TrezorInitSettings(
word_count=bg_numwords.checkedId(),
label=name.text(),
pin_enabled=cb_pin.isChecked(),
passphrase_enabled=cb_phrase.isChecked(),
recovery_type=bg_rectype.checkedId() if bg_rectype else None,
backup_type=bg_backuptype.checkedId(),
no_backup=cb_no_backup.isChecked() if cb_no_backup else False,
)
self.addWidget(expert_widget)
class Plugin(TrezorPlugin, QtPlugin):
@ -450,6 +456,22 @@ class Plugin(TrezorPlugin, QtPlugin):
from trezorlib.qt.pinmatrix import PinMatrixWidget
return PinMatrixWidget
@hook
def init_wallet_wizard(self, wizard: 'QEWalletWizard'):
self.extend_wizard(wizard)
# insert trezor pages in new wallet wizard
def extend_wizard(self, wizard: 'NewWalletWizard'):
super().extend_wizard(wizard)
views = {
'trezor_start': { 'gui': WCScriptAndDerivation },
'trezor_xpub': { 'gui': WCTrezorXPub },
'trezor_not_initialized': { 'gui': WCTrezorInitMethod },
'trezor_choose_new_recover': { 'gui': WCTrezorInitParams },
'trezor_do_init': { 'gui': WCTrezorInit },
}
wizard.navmap_merge(views)
class SettingsDialog(WindowModalDialog):
'''This dialog doesn't require a device be paired with a wallet.
@ -767,3 +789,151 @@ class SettingsDialog(WindowModalDialog):
# Update information
invoke_client(None)
class WCTrezorXPub(WizardComponent, Logger):
def __init__(self, parent, wizard):
WizardComponent.__init__(self, parent, wizard, title=_('Hardware wallet information'))
Logger.__init__(self)
self.plugins = wizard.plugins
self.plugin = self.plugins.get_plugin('trezor')
self._busy = True
self.xpub = None
self.ok_l = WWLabel('Retrieved Hardware Information')
self.ok_l.setAlignment(Qt.AlignCenter)
self.layout().addWidget(self.ok_l)
def on_ready(self):
_name, _info = self.wizard_data['hardware_device']
device_id = _info.device.id_
client = self.plugins.device_manager.client_by_id(device_id, scan_now=False)
client.handler = self.plugin.create_handler(self.wizard)
cosigner = self._current_cosigner(self.wizard_data)
xtype = cosigner['script_type']
derivation = cosigner['derivation_path']
def get_xpub_task(client, derivation, xtype):
try:
self.xpub = client.get_xpub(derivation, xtype)
self.root_fingerprint = client.request_root_fingerprint_from_device()
self.label = client.label()
self.soft_device_id = client.get_soft_device_id()
except Exception as e:
# TODO: handle user interaction exceptions (e.g. invalid pin) more gracefully
self.error = repr(e)
self.logger.error(repr(e))
self.xpub_done()
t = threading.Thread(target=get_xpub_task, args=(client, derivation, xtype), daemon=True)
t.start()
def xpub_done(self):
self.logger.debug(f'Done retrieve xpub: {self.xpub}')
self.busy = False
self.validate()
def validate(self):
if self.xpub and not self.error:
self.valid = True
else:
self.valid = False
def apply(self):
if self.valid:
cosigner_data = self._current_cosigner(self.wizard_data)
cosigner_data['hw_type'] = 'trezor'
cosigner_data['master_key'] = self.xpub
cosigner_data['root_fingerprint'] = self.root_fingerprint
cosigner_data['label'] = self.label
cosigner_data['soft_device_id'] = self.soft_device_id
class WCTrezorInitMethod(WizardComponent, Logger):
def __init__(self, parent, wizard):
WizardComponent.__init__(self, parent, wizard, title=_('HW Setup'))
Logger.__init__(self)
def on_ready(self):
_name, _info = self.wizard_data['hardware_device']
message = _('Choose how you want to initialize your {}.').format(_info.model_name)
choices = [
# Must be short as QT doesn't word-wrap radio button text
(TIM_NEW, _("Let the device generate a completely new seed randomly")),
(TIM_RECOVER, _("Recover from a seed you have previously written down")),
]
self.c_values = [x[0] for x in choices]
c_titles = [x[1] for x in choices]
self.clayout = ChoicesLayout(message, c_titles)
self.layout().addLayout(self.clayout.layout())
self.layout().addStretch(1)
self._valid = True
def apply(self):
self.wizard_data['trezor_init'] = self.c_values[self.clayout.selected_index()]
class WCTrezorInitParams(WizardComponent):
def __init__(self, parent, wizard):
WizardComponent.__init__(self, parent, wizard, title=_('Set-up trezor'))
self.plugins = wizard.plugins
self._busy = True
def on_ready(self):
_name, _info = self.wizard_data['hardware_device']
self.settings_layout = InitSettingsLayout(self.plugins.device_manager, self.wizard_data['trezor_init'], _info.device.id_)
self.layout().addLayout(self.settings_layout)
self.layout().addStretch(1)
self.valid = True
self.busy = False
def apply(self):
vbox = self.settings_layout
trezor_settings = TrezorInitSettings(
word_count=vbox.bg_numwords.checkedId(),
label=vbox.name.text(),
pin_enabled=vbox.cb_pin.isChecked(),
passphrase_enabled=vbox.cb_phrase.isChecked(),
recovery_type=vbox.bg_rectype.checkedId() if vbox.bg_rectype else None,
backup_type=vbox.bg_backuptype.checkedId(),
no_backup=vbox.cb_no_backup.isChecked() if vbox.cb_no_backup else False,
)
self.wizard_data['trezor_settings'] = trezor_settings
class WCTrezorInit(WizardComponent, Logger):
def __init__(self, parent, wizard):
WizardComponent.__init__(self, parent, wizard, title=_('Set-up trezor'))
Logger.__init__(self)
self.plugins = wizard.plugins
self.plugin = self.plugins.get_plugin('trezor')
self._busy = True
def on_ready(self):
settings = self.wizard_data['trezor_settings']
method = self.wizard_data['trezor_init']
_name, _info = self.wizard_data['hardware_device']
device_id = _info.device.id_
client = self.plugins.device_manager.client_by_id(device_id, scan_now=False)
client.handler = self.plugin.create_handler(self.wizard)
def initialize_device_task(settings, method, device_id, wizard, handler):
self.plugin._initialize_device(settings, method, device_id, wizard, handler)
self.init_done()
t = threading.Thread(
target=initialize_device_task,
args=(settings, method, device_id, None, client.handler),
daemon=True)
t.start()
def init_done(self):
self.logger.info('Done initialize device')
self.busy = False
self.layout().addWidget(WWLabel('Done'))
def apply(self):
pass

27
electrum/plugins/trezor/trezor.py

@ -524,3 +524,30 @@ class TrezorPlugin(HW_PluginBase):
for o in tx.outputs()
]
return t
# new wizard
def wizard_entry_for_device(self, device_info: 'DeviceInfo') -> str:
return 'trezor_not_initialized' if not device_info.initialized else 'trezor_start'
# insert trezor pages in new wallet wizard
def extend_wizard(self, wizard: 'NewWalletWizard'):
views = {
'trezor_start': {
'next': 'trezor_xpub',
},
'trezor_xpub': {
'next': lambda d: wizard.wallet_password_view(d) if wizard.last_cosigner(d) else 'multisig_cosigner_keystore',
'last': lambda d: wizard.is_single_password() and wizard.last_cosigner(d)
},
'trezor_not_initialized': {
'next': 'trezor_choose_new_recover',
},
'trezor_choose_new_recover': {
'next': 'trezor_do_init',
},
'trezor_do_init': {
'next': 'trezor_start',
},
}
wizard.navmap_merge(views)

78
electrum/wizard.py

@ -3,7 +3,9 @@ import os
from typing import List, NamedTuple, Any, Dict, Optional
from electrum.keystore import hardware_keystore
from electrum.logging import get_logger
from electrum.plugin import run_hook
from electrum.slip39 import EncryptedSeed
from electrum.storage import WalletStorage, StorageEncryptionVersion
from electrum.wallet_db import WalletDB
@ -74,8 +76,6 @@ class AbstractWizard:
if 'next' not in nav:
# finished
is_finished = True
# self.finished(wizard_data)
# return WizardViewState(None, wizard_data, {})
new_view = WizardViewState(None, wizard_data, {})
else:
view_next = nav['next']
@ -185,7 +185,7 @@ class NewWalletWizard(AbstractWizard):
_logger = get_logger(__name__)
def __init__(self, daemon):
def __init__(self, daemon, plugins):
AbstractWizard.__init__(self)
self.navmap = {
'wallet_name': {
@ -211,13 +211,16 @@ class NewWalletWizard(AbstractWizard):
'last': lambda d: self.is_single_password() and not
(self.needs_derivation_path(d) or self.is_multisig(d))
},
'choose_hardware_device': {
'next': self.on_hardware_device,
},
'script_and_derivation': {
'next': lambda d: 'wallet_password' if not self.is_multisig(d) else 'multisig_cosigner_keystore',
'next': lambda d: self.wallet_password_view(d) if not self.is_multisig(d) else 'multisig_cosigner_keystore',
'accept': self.maybe_master_pubkey,
'last': lambda d: self.is_single_password() and not self.is_multisig(d)
},
'have_master_key': {
'next': lambda d: 'wallet_password' if not self.is_multisig(d) else 'multisig_cosigner_keystore',
'next': lambda d: self.wallet_password_view(d) if not self.is_multisig(d) else 'multisig_cosigner_keystore',
'accept': self.maybe_master_pubkey,
'last': lambda d: self.is_single_password() and not self.is_multisig(d)
},
@ -228,15 +231,18 @@ class NewWalletWizard(AbstractWizard):
'next': self.on_cosigner_keystore_type
},
'multisig_cosigner_key': {
'next': lambda d: 'wallet_password' if self.last_cosigner(d) else 'multisig_cosigner_keystore',
'next': lambda d: self.wallet_password_view(d) if self.last_cosigner(d) else 'multisig_cosigner_keystore',
'last': lambda d: self.is_single_password() and self.last_cosigner(d)
},
'multisig_cosigner_seed': {
'next': self.on_have_cosigner_seed,
'last': lambda d: self.is_single_password() and self.last_cosigner(d) and not self.needs_derivation_path(d)
},
'multisig_cosigner_hardware': {
'next': self.on_hardware_device,
},
'multisig_cosigner_script_and_derivation': {
'next': lambda d: 'wallet_password' if self.last_cosigner(d) else 'multisig_cosigner_keystore',
'next': lambda d: self.wallet_password_view(d) if self.last_cosigner(d) else 'multisig_cosigner_keystore',
'last': lambda d: self.is_single_password() and self.last_cosigner(d)
},
'imported': {
@ -245,9 +251,13 @@ class NewWalletWizard(AbstractWizard):
},
'wallet_password': {
'last': True
},
'wallet_password_hardware': {
'last': True
}
}
self._daemon = daemon
self.plugins = plugins
def start(self, initial_data=None):
if initial_data is None:
@ -292,9 +302,22 @@ class NewWalletWizard(AbstractWizard):
return {
'createseed': 'create_seed',
'haveseed': 'have_seed',
'masterkey': 'have_master_key'
'masterkey': 'have_master_key',
'hardware': 'choose_hardware_device'
}.get(t)
def is_hardware(self, wizard_data):
return wizard_data['keystore_type'] == 'hardware'
def wallet_password_view(self, wizard_data):
return 'wallet_password_hardware' if self.is_hardware(wizard_data) else 'wallet_password'
def on_hardware_device(self, wizard_data):
_type, _info = wizard_data['hardware_device']
run_hook('init_wallet_wizard', self)
plugin = self.plugins.get_plugin(_type)
return plugin.wizard_entry_for_device(_info)
def on_have_or_confirm_seed(self, wizard_data):
if self.needs_derivation_path(wizard_data):
return 'script_and_derivation'
@ -315,7 +338,8 @@ class NewWalletWizard(AbstractWizard):
t = wizard_data['cosigner_keystore_type']
return {
'key': 'multisig_cosigner_key',
'seed': 'multisig_cosigner_seed'
'seed': 'multisig_cosigner_seed',
'hardware': 'multisig_cosigner_hardware'
}.get(t)
def on_have_cosigner_seed(self, wizard_data):
@ -330,6 +354,9 @@ class NewWalletWizard(AbstractWizard):
def last_cosigner(self, wizard_data):
# check if we have the final number of cosigners. Doesn't check if cosigner data itself is complete
# (should be validated by wizardcomponents)
if not self.is_multisig(wizard_data):
return True
if len(wizard_data['multisig_cosigner_data']) < (wizard_data['multisig_participants'] - 1):
return False
@ -337,8 +364,7 @@ class NewWalletWizard(AbstractWizard):
def has_duplicate_masterkeys(self, wizard_data) -> bool:
"""Multisig wallets need distinct master keys. If True, need to prevent wallet-creation."""
xpubs = []
xpubs.append(self.keystore_from_data(wizard_data['wallet_type'], wizard_data).get_master_public_key())
xpubs = [self.keystore_from_data(wizard_data['wallet_type'], wizard_data).get_master_public_key()]
for cosigner in wizard_data['multisig_cosigner_data']:
data = wizard_data['multisig_cosigner_data'][cosigner]
xpubs.append(self.keystore_from_data(wizard_data['wallet_type'], data).get_master_public_key())
@ -492,13 +518,30 @@ class NewWalletWizard(AbstractWizard):
pass
else:
raise Exception(f"unexpected keystore type: {type(keystore)}")
elif data['keystore_type'] == 'hardware': # TODO: prelim impl
k = self.hw_keystore(data)
if isinstance(k, keystore.Xpub): # has xpub
t1 = xpub_type(k.xpub)
if data['wallet_type'] == 'multisig':
if t1 not in ['standard', 'p2wsh', 'p2wsh-p2sh']:
raise Exception('wrong key type %s' % t1)
else:
if t1 not in ['standard', 'p2wpkh', 'p2wpkh-p2sh']:
raise Exception('wrong key type %s' % t1)
# elif isinstance(k, keystore.Old_KeyStore):
# pass
else:
raise Exception(f"unexpected keystore type: {type(keystore)}")
else:
raise Exception('unsupported/unknown keystore_type %s' % data['keystore_type'])
if data['encrypt']:
if k and k.may_have_password():
k.update_password(None, data['password'])
storage.set_password(data['password'], enc_version=StorageEncryptionVersion.USER_PASSWORD)
enc_version = StorageEncryptionVersion.USER_PASSWORD
if data['keystore_type'] == 'hardware':
enc_version = StorageEncryptionVersion.XPUB_PASSWORD
storage.set_password(data['password'], enc_version=enc_version)
db = WalletDB('', storage=storage, manual_upgrades=False)
db.set_keystore_encryption(bool(data['password']) and data['encrypt'])
@ -546,6 +589,17 @@ class NewWalletWizard(AbstractWizard):
db.load_plugins()
db.write()
def hw_keystore(self, data):
return hardware_keystore({
'type': 'hardware',
'hw_type': data['hw_type'],
'derivation': data['derivation_path'],
'root_fingerprint': data['root_fingerprint'],
'xpub': data['master_key'],
'label': data['label'],
'soft_device_id': data['soft_device_id']
})
class ServerConnectWizard(AbstractWizard):

Loading…
Cancel
Save