commit 21bf5a8a84316b2f71540c95e3a66205e2c3d12a
parent 187b4dc9c1d1b03a13ab5bcea9516a970aed5ff9
Author: Neil Booth <kyuupichan@gmail.com>
Date: Sat, 2 Jan 2016 09:43:56 +0900
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
Diffstat:
11 files changed, 343 insertions(+), 223 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -1,6 +1,4 @@
####-*.patch
-gui/icons_rc.py
-lib/icons_rc.py
*.pyc
*.swp
build/
diff --git a/gui/qt/installwizard.py b/gui/qt/installwizard.py
@@ -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()
diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py
@@ -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):
diff --git a/lib/plugins.py b/lib/plugins.py
@@ -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):
diff --git a/lib/wallet.py b/lib/wallet.py
@@ -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 + "'"
+ @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 address_id(self, address):
acc_id, (change, address_index) = self.get_address_index(address)
- account_derivation = self.account_derivation(acc_id)
- return "%s/%d/%d" % (account_derivation, change, address_index)
+ return self.address_derivation(acc_id, change, address_index)
- def mnemonic_to_seed(self, mnemonic, passphrase):
+ @staticmethod
+ def mnemonic_to_seed(mnemonic, passphrase):
# See BIP39
import pbkdf2, hashlib, hmac
PBKDF2_ROUNDS = 2048
diff --git a/lib/wizard.py b/lib/wizard.py
@@ -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:
diff --git a/plugins/keepkey/qt.py b/plugins/keepkey/qt.py
@@ -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
diff --git a/plugins/trezor/client.py b/plugins/trezor/client.py
@@ -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):
+
+ 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 base_client.call_raw(self, msg)
- except:
- self.print_error("Marking %s client bad" % self.device)
- self.bad = True
- raise
+ 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 TrezorClient
+ return cls
diff --git a/plugins/trezor/plugin.py b/plugins/trezor/plugin.py
@@ -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
-
- def give_error(self, message):
- self.print_error(message)
- raise Exception(message)
+ # 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 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,50 +128,171 @@ 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)
-
- def is_enabled(self):
- return self.libraries_available
+ @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)
- 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)
+ def is_enabled(self):
+ return self.libraries_available
@staticmethod
def normalize_passphrase(self, passphrase):
@@ -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 = []
diff --git a/plugins/trezor/qt.py b/plugins/trezor/qt.py
@@ -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
diff --git a/plugins/trezor/qt_generic.py b/plugins/trezor/qt_generic.py
@@ -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 exec_dialog(self, dialog):
+ self.window_stack.append(dialog)
+ try:
+ dialog.exec_()
+ finally:
+ assert dialog == self.window_stack.pop()
- def push_window(self, window):
- self.windows.append(window)
+def qt_plugin_class(base_plugin_class):
-class QtPlugin(TrezorPlugin):
+ 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