Better support for USB devices

Benefits of this rewrite include:

- support of disconnecting / reconnecting a device without having
  to close the wallet, even in a different USB socket
- support of multiple keepkey / trezor devices, both during wallet
  creation and general use
- wallet is watching-only dynamically according to whether the
  associated device is currently plugged in or not
This commit is contained in:
Neil Booth 2016-01-02 09:43:56 +09:00
parent 187b4dc9c1
commit 21bf5a8a84
11 changed files with 345 additions and 225 deletions

2
.gitignore vendored
View File

@ -1,6 +1,4 @@
####-*.patch
gui/icons_rc.py
lib/icons_rc.py
*.pyc
*.swp
build/

View File

@ -132,13 +132,6 @@ class InstallWizard(WindowModalDialog, MessageBoxMixin, WizardBase):
the password or None for no password."""
return self.pw_dialog(msg or MSG_ENTER_PASSWORD, PasswordDialog.PW_NEW)
def query_hardware(self, choices, action):
if action == 'create':
msg = _('Select the hardware wallet to create')
else:
msg = _('Select the hardware wallet to restore')
return self.choice(msg, choices)
def choose_server(self, network):
# Show network dialog if config does not exist
if self.config.get('server') is None:
@ -323,7 +316,7 @@ class InstallWizard(WindowModalDialog, MessageBoxMixin, WizardBase):
self.config.set_key('auto_connect', True, True)
network.auto_connect = True
def choice(self, msg, choices):
def query_choice(self, msg, choices):
vbox = QVBoxLayout()
self.set_layout(vbox)
gb2 = QGroupBox(msg)
@ -335,7 +328,7 @@ class InstallWizard(WindowModalDialog, MessageBoxMixin, WizardBase):
group2 = QButtonGroup()
for i,c in enumerate(choices):
button = QRadioButton(gb2)
button.setText(c[1])
button.setText(c)
vbox2.addWidget(button)
group2.addButton(button)
group2.setId(button, i)
@ -347,8 +340,7 @@ class InstallWizard(WindowModalDialog, MessageBoxMixin, WizardBase):
vbox.addLayout(Buttons(CancelButton(self), next_button))
if not self.exec_():
raise UserCancelled
wallet_type = choices[group2.checkedId()][0]
return wallet_type
return group2.checkedId()
def query_multisig(self, action):
vbox = QVBoxLayout()

View File

@ -152,6 +152,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.connect(self, QtCore.SIGNAL('payment_request_error'), self.payment_request_error)
self.history_list.setFocus(True)
self.connect(self, QtCore.SIGNAL('watching_only_changed'),
self.watching_only_changed)
# network callbacks
if self.network:
self.connect(self, QtCore.SIGNAL('network'), self.on_network_qt)
@ -280,7 +283,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.warn_if_watching_only()
def watching_only_changed(self):
self.saved_wwo = self.wallet.is_watching_only()
title = 'Electrum %s - %s' % (self.wallet.electrum_version,
self.wallet.basename())
if self.wallet.is_watching_only():
@ -495,6 +497,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
def timer_actions(self):
# Note this runs in the GUI thread
if self.need_update.is_set():
self.need_update.clear()
self.update_wallet()
@ -504,8 +507,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
if self.require_fee_update:
self.do_update_fee()
self.require_fee_update = False
if self.saved_wwo != self.wallet.is_watching_only():
self.watching_only_changed()
run_hook('timer_actions')
def format_amount(self, x, is_diff=False, whitespaces=False):

View File

@ -73,7 +73,7 @@ class Plugins(DaemonThread):
self.print_error("loaded", name)
return plugin
except Exception:
print_msg(_("Error: cannot initialize plugin"), name)
self.print_error("cannot initialize plugin", name)
traceback.print_exc(file=sys.stdout)
return None
@ -106,16 +106,17 @@ class Plugins(DaemonThread):
return not requires or w.wallet_type in requires
def hardware_wallets(self, action):
result = []
wallet_types, descs = [], []
for name, (gui_good, details) in self.hw_wallets.items():
if gui_good:
try:
p = self.wallet_plugin_loader(name)
if action == 'restore' or p.is_enabled():
result.append((details[1], details[2]))
wallet_types.append(details[1])
descs.append(details[2])
except:
self.print_error("cannot load plugin for:", name)
return result
return wallet_types, descs
def register_plugin_wallet(self, name, gui_good, details):
def dynamic_constructor(storage):

View File

@ -205,6 +205,9 @@ class Abstract_Wallet(PrintError):
def diagnostic_name(self):
return self.basename()
def __str__(self):
return self.basename()
def set_use_encryption(self, use_encryption):
self.use_encryption = use_encryption
self.storage.put('use_encryption', use_encryption)
@ -1718,18 +1721,25 @@ class BIP44_Wallet(BIP32_HD_Wallet):
def can_create_accounts(self):
return not self.is_watching_only()
@classmethod
def prefix(self):
return "/".join(self.root_derivation.split("/")[1:])
@classmethod
def account_derivation(self, account_id):
return self.prefix() + "/" + account_id + "'"
def address_id(self, address):
acc_id, (change, address_index) = self.get_address_index(address)
account_derivation = self.account_derivation(acc_id)
@classmethod
def address_derivation(self, account_id, change, address_index):
account_derivation = self.account_derivation(account_id)
return "%s/%d/%d" % (account_derivation, change, address_index)
def mnemonic_to_seed(self, mnemonic, passphrase):
def address_id(self, address):
acc_id, (change, address_index) = self.get_address_index(address)
return self.address_derivation(acc_id, change, address_index)
@staticmethod
def mnemonic_to_seed(mnemonic, passphrase):
# See BIP39
import pbkdf2, hashlib, hmac
PBKDF2_ROUNDS = 2048

View File

@ -76,11 +76,9 @@ class WizardBase(PrintError):
string like "2of3". Action is 'create' or 'restore'."""
raise NotImplementedError
def query_hardware(self, choices, action):
"""Asks the user what kind of hardware wallet they want from the given
choices. choices is a list of (wallet_type, translated
description) tuples. Action is 'create' or 'restore'. Return
the wallet type chosen."""
def query_choice(self, msg, choices):
"""Asks the user which of several choices they would like.
Return the index of the choice."""
raise NotImplementedError
def show_and_verify_seed(self, seed):
@ -205,8 +203,13 @@ class WizardBase(PrintError):
if kind == 'multisig':
wallet_type = self.query_multisig(action)
elif kind == 'hardware':
choices = self.plugins.hardware_wallets(action)
wallet_type = self.query_hardware(choices, action)
wallet_types, choices = self.plugins.hardware_wallets(action)
if action == 'create':
msg = _('Select the hardware wallet to create')
else:
msg = _('Select the hardware wallet to restore')
choice = self.query_choice(msg, choices)
wallet_type = wallet_types[choice]
elif kind == 'twofactor':
wallet_type = '2fa'
else:

View File

@ -1,9 +1,11 @@
from plugins.trezor.qt_generic import QtPlugin
from plugins.trezor.qt_generic import qt_plugin_class
from keepkey import KeepKeyPlugin
class Plugin(QtPlugin):
class Plugin(qt_plugin_class(KeepKeyPlugin)):
icon_file = ":icons/keepkey.png"
def pin_matrix_widget_class():
@classmethod
def pin_matrix_widget_class(self):
from keepkeylib.qt.pinmatrix import PinMatrixWidget
return PinMatrixWidget

View File

@ -27,7 +27,7 @@ class GuiMixin(object):
else:
cancel_callback = None
self.handler.show_message(message % self.device, cancel_callback)
self.handler().show_message(message % self.device, cancel_callback)
return self.proto.ButtonAck()
def callback_PinMatrixRequest(self, msg):
@ -40,14 +40,14 @@ class GuiMixin(object):
"Note the numbers have been shuffled!"))
else:
msg = _("Please enter %s PIN")
pin = self.handler.get_pin(msg % self.device)
pin = self.handler().get_pin(msg % self.device)
if not pin:
return self.proto.Cancel()
return self.proto.PinMatrixAck(pin=pin)
def callback_PassphraseRequest(self, req):
msg = _("Please enter your %s passphrase")
passphrase = self.handler.get_passphrase(msg % self.device)
passphrase = self.handler().get_passphrase(msg % self.device)
if passphrase is None:
return self.proto.Cancel()
return self.proto.PassphraseAck(passphrase=passphrase)
@ -65,18 +65,29 @@ def trezor_client_class(protocol_mixin, base_client, proto):
class TrezorClient(protocol_mixin, GuiMixin, base_client, PrintError):
def __init__(self, transport, plugin):
def __init__(self, transport, path, plugin):
base_client.__init__(self, transport)
protocol_mixin.__init__(self, transport)
self.proto = proto
self.device = plugin.device
self.handler = None
self.path = path
self.wallet = None
self.plugin = plugin
self.tx_api = plugin
self.bad = False
self.msg_code_override = None
self.proper_device = False
self.checked_device = False
def __str__(self):
return "%s/%s/%s" % (self.label(), self.device_id(), self.path[0])
def label(self):
return self.features.label
def device_id(self):
return self.features.device_id
def handler(self):
assert self.wallet and self.wallet.handler
return self.wallet.handler
# Copied from trezorlib/client.py as there it is not static, sigh
@staticmethod
@ -94,34 +105,8 @@ def trezor_client_class(protocol_mixin, base_client, proto):
path.append(abs(int(x)) | prime)
return path
def check_proper_device(self, wallet):
try:
self.ping('t')
except BaseException as e:
self.plugin.give_error(
__("%s device not detected. Continuing in watching-only "
"mode.") % self.device + "\n\n" + str(e))
if not self.is_proper_device(wallet):
self.plugin.give_error(_('Wrong device or password'))
def is_proper_device(self, wallet):
if not self.checked_device:
addresses = wallet.addresses(False)
if not addresses: # Wallet being created?
return True
address = addresses[0]
address_id = wallet.address_id(address)
path = self.expand_path(address_id)
self.checked_device = True
try:
device_address = self.get_address('Bitcoin', path)
self.proper_device = (device_address == address)
except:
self.proper_device = False
wallet.proper_device = self.proper_device
return self.proper_device
def address_from_derivation(self, derivation):
return self.get_address('Bitcoin', self.expand_path(derivation))
def change_label(self, label):
self.msg_code_override = 'label'
@ -144,12 +129,26 @@ def trezor_client_class(protocol_mixin, base_client, proto):
def atleast_version(self, major, minor=0, patch=0):
return cmp(self.firmware_version(), (major, minor, patch))
def call_raw(self, msg):
try:
return base_client.call_raw(self, msg)
except:
self.print_error("Marking %s client bad" % self.device)
self.bad = True
raise
return TrezorClient
def wrapper(func):
'''Wrap base class methods to show exceptions and clear
any dialog box it opened.'''
def wrapped(self, *args, **kwargs):
handler = self.handler()
try:
return func(self, *args, **kwargs)
except BaseException as e:
handler.show_error(str(e))
raise e
finally:
handler.finished()
return wrapped
cls = TrezorClient
for method in ['apply_settings', 'change_pin', 'get_address',
'get_public_node', 'sign_message', 'sign_tx']:
setattr(cls, method, wrapper(getattr(cls, method)))
return cls

View File

@ -1,4 +1,6 @@
import re
import time
from binascii import unhexlify
from struct import pack
from unicodedata import normalize
@ -12,6 +14,9 @@ from electrum.transaction import (deserialize, is_extended_pubkey,
Transaction, x_to_xpub)
from electrum.wallet import BIP32_HD_Wallet, BIP44_Wallet
class DeviceDisconnectedError(Exception):
pass
class TrezorCompatibleWallet(BIP44_Wallet):
# Extend BIP44 Wallet as required by hardware implementation.
# Derived classes must set:
@ -22,11 +27,21 @@ class TrezorCompatibleWallet(BIP44_Wallet):
def __init__(self, storage):
BIP44_Wallet.__init__(self, storage)
self.proper_device = False
# This is set when paired with a device, and used to re-pair
# a device that is disconnected and re-connected
self.device_id = None
# Errors and other user interaction is done through the wallet's
# handler. The handler is per-window and preserved across
# device reconnects
self.handler = None
def give_error(self, message):
self.print_error(message)
raise Exception(message)
def disconnected(self):
self.print_error("disconnected")
self.handler.watching_only_changed()
def connected(self):
self.print_error("connected")
self.handler.watching_only_changed()
def get_action(self):
pass
@ -35,29 +50,29 @@ class TrezorCompatibleWallet(BIP44_Wallet):
return False
def is_watching_only(self):
'''The wallet is watching-only if its trezor device is not
connected. This result is dynamic and changes over time.'''
assert not self.has_seed()
return not self.proper_device
return self.plugin.lookup_client(self) is None
def can_change_password(self):
return False
def get_client(self):
return self.plugin.get_client(self)
def check_proper_device(self):
return self.get_client().check_proper_device(self)
def client(self):
return self.plugin.client(self)
def derive_xkeys(self, root, derivation, password):
if self.master_public_keys.get(root):
return BIP44_wallet.derive_xkeys(self, root, derivation, password)
# Happens when creating a wallet
# When creating a wallet we need to ask the device for the
# master public key
derivation = derivation.replace(self.root_name, self.prefix() + "/")
xpub = self.get_public_key(derivation)
return xpub, None
def get_public_key(self, bip32_path):
client = self.get_client()
client = self.client()
address_n = client.expand_path(bip32_path)
node = client.get_public_node(address_n).node
xpub = ("0488B21E".decode('hex') + chr(node.depth)
@ -72,25 +87,15 @@ class TrezorCompatibleWallet(BIP44_Wallet):
raise RuntimeError(_('Decrypt method is not implemented'))
def sign_message(self, address, message, password):
client = self.get_client()
self.check_proper_device()
try:
address_path = self.address_id(address)
address_n = client.expand_path(address_path)
except Exception as e:
self.give_error(e)
try:
msg_sig = client.sign_message('Bitcoin', address_n, message)
except Exception as e:
self.give_error(e)
finally:
self.plugin.get_handler(self).stop()
client = self.client()
address_path = self.address_id(address)
address_n = client.expand_path(address_path)
msg_sig = client.sign_message('Bitcoin', address_n, message)
return msg_sig.signature
def sign_transaction(self, tx, password):
if tx.is_complete() or self.is_watching_only():
return
self.check_proper_device()
# previous transactions used as inputs
prev_tx = {}
# path of the xpubs that are involved
@ -123,51 +128,172 @@ class TrezorCompatiblePlugin(BasePlugin):
# libraries_available, libraries_URL, minimum_firmware,
# wallet_class, ckd_public, types, HidTransport
# This plugin automatically keeps track of attached devices, and
# connects to anything attached creating a new Client instance.
# When disconnected, the client is informed via a callback.
# As a device can be disconnected and/or reconnected in a different
# USB port (giving it a new path), the wallet must be dynamic in
# asking for its client.
# If a wallet is successfully paired with a given device, the plugin
# stores its serial number in the wallet so it can be automatically
# re-paired if the same device is connected elsewhere.
# Approaching things this way permits several devices to be connected
# simultaneously and handled smoothly.
def __init__(self, parent, config, name):
BasePlugin.__init__(self, parent, config, name)
self.device = self.wallet_class.device
self.client = None
self.wallet_class.plugin = self
# A set of client instances to USB paths
self.clients = set()
# The device wallets we have seen to inform on reconnection
self.paired_wallets = set()
# Do an initial scan
self.last_scan = 0
self.timer_actions()
def give_error(self, message):
self.print_error(message)
raise Exception(message)
@hook
def timer_actions(self):
if self.libraries_available:
# Scan connected devices every second
now = time.time()
if now > self.last_scan + 1:
self.last_scan = now
self.scan_devices()
def scan_devices(self):
paths = self.HidTransport.enumerate()
connected = set([c for c in self.clients if c.path in paths])
disconnected = self.clients - connected
# Inform clients and wallets they were disconnected
for client in disconnected:
self.print_error("device disconnected:", client)
if client.wallet:
client.wallet.disconnected()
for path in paths:
# Look for new paths
if any(c.path == path for c in connected):
continue
try:
transport = self.HidTransport(path)
except BaseException as e:
# We were probably just disconnected; never mind
self.print_error("cannot connect at", path, str(e))
continue
self.print_error("connected to device at", path[0])
try:
client = self.client_class(transport, path, self)
except BaseException as e:
self.print_error("cannot create client for", path, str(e))
else:
connected.add(client)
self.print_error("new device:", client)
# Inform reconnected wallets
for wallet in self.paired_wallets:
if wallet.device_id == client.features.device_id:
client.wallet = wallet
wallet.connected()
self.clients = connected
def clear_session(self, client):
# Clearing the session forces pin re-entry
self.print_error("clear session:", client)
client.clear_session()
def select_device(self, wallet, wizard):
'''Called when creating a new wallet. Select the device
to use.'''
clients = list(self.clients)
if not len(clients):
return
if len(clients) > 1:
labels = [client.label() for client in clients]
msg = _("Please select which %s device to use:") % self.device
client = clients[wizard.query_choice(msg, labels)]
else:
client = clients[0]
self.pair_wallet(wallet, client)
def pair_wallet(self, wallet, client):
self.print_error("pairing wallet %s to device %s" % (wallet, client))
self.paired_wallets.add(wallet)
wallet.device_id = client.features.device_id
client.wallet = wallet
wallet.connected()
def try_to_pair_wallet(self, wallet):
'''Call this when loading an existing wallet to find if the
associated device is connected.'''
account = '0'
if not account in wallet.accounts:
self.print_error("try pair_wallet: wallet has no accounts")
return None
first_address = wallet.accounts[account].first_address()[0]
derivation = wallet.address_derivation(account, 0, 0)
for client in self.clients:
if client.wallet:
continue
if not client.atleast_version(*self.minimum_firmware):
wallet.handler.show_error(
_('Outdated %s firmware for device labelled %s. Please '
'download the updated firmware from %s') %
(self.device, client.label(), self.firmware_URL))
continue
# This gives us a handler
client.wallet = wallet
device_address = None
try:
device_address = client.address_from_derivation(derivation)
finally:
client.wallet = None
if first_address == device_address:
self.pair_wallet(wallet, client)
return client
return None
def lookup_client(self, wallet):
for client in self.clients:
if client.features.device_id == wallet.device_id:
return client
return None
def client(self, wallet):
'''Returns a wrapped client which handles cleanup in case of
thrown exceptions, etc.'''
assert isinstance(wallet, self.wallet_class)
assert wallet.handler != None
if wallet.device_id is None:
client = self.try_to_pair_wallet(wallet)
else:
client = self.lookup_client(wallet)
if not client:
msg = (_('Could not connect to your %s. Verify the '
'cable is connected and that no other app is '
'using it.\nContinuing in watching-only mode '
'until the device is re-connected.') % self.device)
if not self.clients:
wallet.handler.show_error(msg)
raise DeviceDisconnectedError(msg)
return client
def is_enabled(self):
return self.libraries_available
def create_client(self):
if not self.libraries_available:
self.give_error(_('please install the %s libraries from %s')
% (self.device, self.libraries_URL))
devices = self.HidTransport.enumerate()
if not devices:
self.give_error(_('Could not connect to your %s. Verify the '
'cable is connected and that no other app is '
'using it.\nContinuing in watching-only mode.'
% self.device))
transport = self.HidTransport(devices[0])
client = self.client_class(transport, self)
if not client.atleast_version(*self.minimum_firmware):
self.give_error(_('Outdated %s firmware. Please update the '
'firmware from %s')
% (self.device, self.firmware_URL))
return client
def get_handler(self, wallet):
return self.get_client(wallet).handler
def get_client(self, wallet=None):
if not self.client or self.client.bad:
self.client = self.create_client()
return self.client
def atleast_version(self, major, minor=0, patch=0):
return self.get_client().atleast_version(major, minor, patch)
@staticmethod
def normalize_passphrase(self, passphrase):
return normalize('NFKD', unicode(passphrase or ''))
@ -192,41 +318,33 @@ class TrezorCompatiblePlugin(BasePlugin):
@hook
def close_wallet(self, wallet):
if self.client:
self.print_error("clear session")
self.client.clear_session()
self.client.transport.close()
self.client = None
# Don't retain references to a closed wallet
self.paired_wallets.discard(wallet)
client = self.lookup_client(wallet)
if client:
self.clear_session(client)
# Release the device
self.clients.discard(client)
client.transport.close()
def sign_transaction(self, wallet, tx, prev_tx, xpub_path):
self.prev_tx = prev_tx
self.xpub_path = xpub_path
client = self.get_client()
client = self.client(wallet)
inputs = self.tx_inputs(tx, True)
outputs = self.tx_outputs(wallet, tx)
try:
signed_tx = client.sign_tx('Bitcoin', inputs, outputs)[1]
except Exception as e:
self.give_error(e)
finally:
self.get_handler(wallet).stop()
signed_tx = client.sign_tx('Bitcoin', inputs, outputs)[1]
raw = signed_tx.encode('hex')
tx.update_signatures(raw)
def show_address(self, wallet, address):
client = self.get_client()
wallet.check_proper_device()
try:
address_path = wallet.address_id(address)
address_n = self.client_class.expand_path(address_path)
except Exception as e:
self.give_error(e)
try:
client.get_address('Bitcoin', address_n, True)
except Exception as e:
self.give_error(e)
finally:
self.get_handler(wallet).stop()
client = self.client(wallet)
if not client.atleast_version(1, 3):
wallet.handler.show_error(_("Your device firmware is too old"))
return
address_path = wallet.address_id(address)
address_n = client.expand_path(address_path)
client.get_address('Bitcoin', address_n, True)
def tx_inputs(self, tx, for_sig=False):
inputs = []

View File

@ -1,10 +1,11 @@
from plugins.trezor.qt_generic import QtPlugin
from plugins.trezor.qt_generic import qt_plugin_class
from trezor import TrezorPlugin
class Plugin(QtPlugin):
class Plugin(qt_plugin_class(TrezorPlugin)):
icon_file = ":icons/trezor.png"
@staticmethod
def pin_matrix_widget_class():
@classmethod
def pin_matrix_widget_class(self):
from trezorlib.qt.pinmatrix import PinMatrixWidget
return PinMatrixWidget

View File

@ -3,7 +3,6 @@ import threading
from PyQt4.Qt import QGridLayout, QInputDialog, QPushButton
from PyQt4.Qt import QVBoxLayout, QLabel, SIGNAL
from trezor import TrezorPlugin
from electrum_gui.qt.main_window import StatusBarButton
from electrum_gui.qt.password_dialog import PasswordDialog
from electrum_gui.qt.util import *
@ -19,23 +18,30 @@ class QtHandler(PrintError):
Trezor protocol; derived classes can customize it.'''
def __init__(self, win, pin_matrix_widget_class, device):
win.connect(win, SIGNAL('message_done'), self.dialog_stop)
win.connect(win, SIGNAL('clear_dialog'), self.clear_dialog)
win.connect(win, SIGNAL('error_dialog'), self.error_dialog)
win.connect(win, SIGNAL('message_dialog'), self.message_dialog)
win.connect(win, SIGNAL('pin_dialog'), self.pin_dialog)
win.connect(win, SIGNAL('passphrase_dialog'), self.passphrase_dialog)
self.window_stack = [win]
self.win = win
self.windows = [win]
self.pin_matrix_widget_class = pin_matrix_widget_class
self.device = device
self.done = threading.Event()
self.dialog = None
self.done = threading.Event()
def stop(self):
self.win.emit(SIGNAL('message_done'))
def watching_only_changed(self):
self.win.emit(SIGNAL('watching_only_changed'))
def show_message(self, msg, cancel_callback=None):
self.win.emit(SIGNAL('message_dialog'), msg, cancel_callback)
def show_error(self, msg):
self.win.emit(SIGNAL('error_dialog'), msg)
def finished(self):
self.win.emit(SIGNAL('clear_dialog'))
def get_pin(self, msg):
self.done.clear()
self.win.emit(SIGNAL('pin_dialog'), msg)
@ -50,22 +56,19 @@ class QtHandler(PrintError):
def pin_dialog(self, msg):
# Needed e.g. when renaming label and haven't entered PIN
self.dialog_stop()
d = WindowModalDialog(self.windows[-1], _("Enter PIN"))
dialog = WindowModalDialog(self.window_stack[-1], _("Enter PIN"))
matrix = self.pin_matrix_widget_class()
vbox = QVBoxLayout()
vbox.addWidget(QLabel(msg))
vbox.addWidget(matrix)
vbox.addLayout(Buttons(CancelButton(d), OkButton(d)))
d.setLayout(vbox)
if not d.exec_():
self.response = None # FIXME: this is lost?
vbox.addLayout(Buttons(CancelButton(dialog), OkButton(dialog)))
dialog.setLayout(vbox)
dialog.exec_()
self.response = str(matrix.get_value())
self.done.set()
def passphrase_dialog(self, msg):
self.dialog_stop()
d = PasswordDialog(self.windows[-1], None, msg,
d = PasswordDialog(self.window_stack[-1], None, msg,
PasswordDialog.PW_PASSHPRASE)
confirmed, p, passphrase = d.run()
if confirmed:
@ -75,9 +78,9 @@ class QtHandler(PrintError):
def message_dialog(self, msg, cancel_callback):
# Called more than once during signing, to confirm output and fee
self.dialog_stop()
self.clear_dialog()
title = _('Please check your %s device') % self.device
dialog = self.dialog = WindowModalDialog(self.windows[-1], title)
self.dialog = dialog = WindowModalDialog(self.window_stack[-1], title)
l = QLabel(msg)
vbox = QVBoxLayout(dialog)
if cancel_callback:
@ -86,19 +89,25 @@ class QtHandler(PrintError):
vbox.addWidget(l)
dialog.show()
def dialog_stop(self):
def error_dialog(self, msg):
self.win.show_error(msg, parent=self.window_stack[-1])
def clear_dialog(self):
if self.dialog:
self.dialog.hide()
self.dialog.accept()
self.dialog = None
def pop_window(self):
self.windows.pop()
def push_window(self, window):
self.windows.append(window)
def exec_dialog(self, dialog):
self.window_stack.append(dialog)
try:
dialog.exec_()
finally:
assert dialog == self.window_stack.pop()
class QtPlugin(TrezorPlugin):
def qt_plugin_class(base_plugin_class):
class QtPlugin(base_plugin_class):
# Derived classes must provide the following class-static variables:
# icon_file
# pin_matrix_widget_class
@ -110,33 +119,28 @@ class QtPlugin(TrezorPlugin):
def load_wallet(self, wallet, window):
if type(wallet) != self.wallet_class:
return
try:
client = self.get_client(wallet)
client.handler = self.create_handler(window)
client.check_proper_device(wallet)
self.button = StatusBarButton(QIcon(self.icon_file), self.device,
partial(self.settings_dialog, window))
window.statusBar().addPermanentWidget(self.button)
except Exception as e:
window.show_error(str(e))
window.tzb = StatusBarButton(QIcon(self.icon_file), self.device,
partial(self.settings_dialog, window))
window.statusBar().addPermanentWidget(window.tzb)
wallet.handler = self.create_handler(window)
# Trigger a pairing
self.client(wallet)
def on_create_wallet(self, wallet, wizard):
client = self.get_client(wallet)
client.handler = self.create_handler(wizard)
assert type(wallet) == self.wallet_class
wallet.handler = self.create_handler(wizard)
self.select_device(wallet, wizard)
wallet.create_main_account(None)
@hook
def receive_menu(self, menu, addrs, wallet):
if type(wallet) != self.wallet_class:
return
if (not wallet.is_watching_only() and
self.atleast_version(1, 3) and len(addrs) == 1):
if type(wallet) == self.wallet_class and len(addrs) == 1:
menu.addAction(_("Show on %s") % self.device,
lambda: self.show_address(wallet, addrs[0]))
def settings_dialog(self, window):
handler = self.get_client(window.wallet).handler
handler = window.wallet.handler
client = self.client(window.wallet)
def rename():
title = _("Set Device Label")
@ -145,10 +149,7 @@ class QtPlugin(TrezorPlugin):
if not response[1]:
return
new_label = str(response[0])
try:
client.change_label(new_label)
finally:
handler.stop()
client.change_label(new_label)
device_label.setText(new_label)
def update_pin_info():
@ -159,13 +160,9 @@ class QtPlugin(TrezorPlugin):
clear_pin_button.setVisible(features.pin_protection)
def set_pin(remove):
try:
client.set_pin(remove=remove)
finally:
handler.stop()
client.set_pin(remove=remove)
update_pin_info()
client = self.get_client()
features = client.features
noyes = [_("No"), _("Yes")]
bl_hash = features.bootloader_hash.encode('hex').upper()
@ -200,7 +197,7 @@ class QtPlugin(TrezorPlugin):
widget = item if isinstance(item, QWidget) else QLabel(item)
layout.addWidget(widget, row_num, col_num)
dialog = WindowModalDialog(None, _("%s Settings") % self.device)
dialog = WindowModalDialog(window, _("%s Settings") % self.device)
vbox = QVBoxLayout()
tabs = QTabWidget()
tabs.addTab(info_tab, _("Information"))
@ -210,8 +207,6 @@ class QtPlugin(TrezorPlugin):
vbox.addLayout(Buttons(CloseButton(dialog)))
dialog.setLayout(vbox)
handler.push_window(dialog)
try:
dialog.exec_()
finally:
handler.pop_window()
handler.exec_dialog(dialog)
return QtPlugin