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 ####-*.patch
gui/icons_rc.py
lib/icons_rc.py
*.pyc *.pyc
*.swp *.swp
build/ build/

View File

@ -132,13 +132,6 @@ class InstallWizard(WindowModalDialog, MessageBoxMixin, WizardBase):
the password or None for no password.""" the password or None for no password."""
return self.pw_dialog(msg or MSG_ENTER_PASSWORD, PasswordDialog.PW_NEW) 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): def choose_server(self, network):
# Show network dialog if config does not exist # Show network dialog if config does not exist
if self.config.get('server') is None: if self.config.get('server') is None:
@ -323,7 +316,7 @@ class InstallWizard(WindowModalDialog, MessageBoxMixin, WizardBase):
self.config.set_key('auto_connect', True, True) self.config.set_key('auto_connect', True, True)
network.auto_connect = True network.auto_connect = True
def choice(self, msg, choices): def query_choice(self, msg, choices):
vbox = QVBoxLayout() vbox = QVBoxLayout()
self.set_layout(vbox) self.set_layout(vbox)
gb2 = QGroupBox(msg) gb2 = QGroupBox(msg)
@ -335,7 +328,7 @@ class InstallWizard(WindowModalDialog, MessageBoxMixin, WizardBase):
group2 = QButtonGroup() group2 = QButtonGroup()
for i,c in enumerate(choices): for i,c in enumerate(choices):
button = QRadioButton(gb2) button = QRadioButton(gb2)
button.setText(c[1]) button.setText(c)
vbox2.addWidget(button) vbox2.addWidget(button)
group2.addButton(button) group2.addButton(button)
group2.setId(button, i) group2.setId(button, i)
@ -347,8 +340,7 @@ class InstallWizard(WindowModalDialog, MessageBoxMixin, WizardBase):
vbox.addLayout(Buttons(CancelButton(self), next_button)) vbox.addLayout(Buttons(CancelButton(self), next_button))
if not self.exec_(): if not self.exec_():
raise UserCancelled raise UserCancelled
wallet_type = choices[group2.checkedId()][0] return group2.checkedId()
return wallet_type
def query_multisig(self, action): def query_multisig(self, action):
vbox = QVBoxLayout() 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.connect(self, QtCore.SIGNAL('payment_request_error'), self.payment_request_error)
self.history_list.setFocus(True) self.history_list.setFocus(True)
self.connect(self, QtCore.SIGNAL('watching_only_changed'),
self.watching_only_changed)
# network callbacks # network callbacks
if self.network: if self.network:
self.connect(self, QtCore.SIGNAL('network'), self.on_network_qt) self.connect(self, QtCore.SIGNAL('network'), self.on_network_qt)
@ -280,7 +283,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.warn_if_watching_only() self.warn_if_watching_only()
def watching_only_changed(self): def watching_only_changed(self):
self.saved_wwo = self.wallet.is_watching_only()
title = 'Electrum %s - %s' % (self.wallet.electrum_version, title = 'Electrum %s - %s' % (self.wallet.electrum_version,
self.wallet.basename()) self.wallet.basename())
if self.wallet.is_watching_only(): if self.wallet.is_watching_only():
@ -495,6 +497,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions) self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
def timer_actions(self): def timer_actions(self):
# Note this runs in the GUI thread
if self.need_update.is_set(): if self.need_update.is_set():
self.need_update.clear() self.need_update.clear()
self.update_wallet() self.update_wallet()
@ -504,8 +507,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
if self.require_fee_update: if self.require_fee_update:
self.do_update_fee() self.do_update_fee()
self.require_fee_update = False self.require_fee_update = False
if self.saved_wwo != self.wallet.is_watching_only():
self.watching_only_changed()
run_hook('timer_actions') run_hook('timer_actions')
def format_amount(self, x, is_diff=False, whitespaces=False): def format_amount(self, x, is_diff=False, whitespaces=False):

View File

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

View File

@ -205,6 +205,9 @@ class Abstract_Wallet(PrintError):
def diagnostic_name(self): def diagnostic_name(self):
return self.basename() return self.basename()
def __str__(self):
return self.basename()
def set_use_encryption(self, use_encryption): def set_use_encryption(self, use_encryption):
self.use_encryption = use_encryption self.use_encryption = use_encryption
self.storage.put('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): def can_create_accounts(self):
return not self.is_watching_only() return not self.is_watching_only()
@classmethod
def prefix(self): def prefix(self):
return "/".join(self.root_derivation.split("/")[1:]) return "/".join(self.root_derivation.split("/")[1:])
@classmethod
def account_derivation(self, account_id): def account_derivation(self, account_id):
return self.prefix() + "/" + account_id + "'" return self.prefix() + "/" + account_id + "'"
def address_id(self, address): @classmethod
acc_id, (change, address_index) = self.get_address_index(address) def address_derivation(self, account_id, change, address_index):
account_derivation = self.account_derivation(acc_id) account_derivation = self.account_derivation(account_id)
return "%s/%d/%d" % (account_derivation, change, address_index) 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 # See BIP39
import pbkdf2, hashlib, hmac import pbkdf2, hashlib, hmac
PBKDF2_ROUNDS = 2048 PBKDF2_ROUNDS = 2048

View File

@ -76,11 +76,9 @@ class WizardBase(PrintError):
string like "2of3". Action is 'create' or 'restore'.""" string like "2of3". Action is 'create' or 'restore'."""
raise NotImplementedError raise NotImplementedError
def query_hardware(self, choices, action): def query_choice(self, msg, choices):
"""Asks the user what kind of hardware wallet they want from the given """Asks the user which of several choices they would like.
choices. choices is a list of (wallet_type, translated Return the index of the choice."""
description) tuples. Action is 'create' or 'restore'. Return
the wallet type chosen."""
raise NotImplementedError raise NotImplementedError
def show_and_verify_seed(self, seed): def show_and_verify_seed(self, seed):
@ -205,8 +203,13 @@ class WizardBase(PrintError):
if kind == 'multisig': if kind == 'multisig':
wallet_type = self.query_multisig(action) wallet_type = self.query_multisig(action)
elif kind == 'hardware': elif kind == 'hardware':
choices = self.plugins.hardware_wallets(action) wallet_types, choices = self.plugins.hardware_wallets(action)
wallet_type = self.query_hardware(choices, 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': elif kind == 'twofactor':
wallet_type = '2fa' wallet_type = '2fa'
else: 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" icon_file = ":icons/keepkey.png"
def pin_matrix_widget_class(): @classmethod
def pin_matrix_widget_class(self):
from keepkeylib.qt.pinmatrix import PinMatrixWidget from keepkeylib.qt.pinmatrix import PinMatrixWidget
return PinMatrixWidget return PinMatrixWidget

View File

@ -27,7 +27,7 @@ class GuiMixin(object):
else: else:
cancel_callback = None 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() return self.proto.ButtonAck()
def callback_PinMatrixRequest(self, msg): def callback_PinMatrixRequest(self, msg):
@ -40,14 +40,14 @@ class GuiMixin(object):
"Note the numbers have been shuffled!")) "Note the numbers have been shuffled!"))
else: else:
msg = _("Please enter %s PIN") msg = _("Please enter %s PIN")
pin = self.handler.get_pin(msg % self.device) pin = self.handler().get_pin(msg % self.device)
if not pin: if not pin:
return self.proto.Cancel() return self.proto.Cancel()
return self.proto.PinMatrixAck(pin=pin) return self.proto.PinMatrixAck(pin=pin)
def callback_PassphraseRequest(self, req): def callback_PassphraseRequest(self, req):
msg = _("Please enter your %s passphrase") 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: if passphrase is None:
return self.proto.Cancel() return self.proto.Cancel()
return self.proto.PassphraseAck(passphrase=passphrase) 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): class TrezorClient(protocol_mixin, GuiMixin, base_client, PrintError):
def __init__(self, transport, plugin): def __init__(self, transport, path, plugin):
base_client.__init__(self, transport) base_client.__init__(self, transport)
protocol_mixin.__init__(self, transport) protocol_mixin.__init__(self, transport)
self.proto = proto self.proto = proto
self.device = plugin.device self.device = plugin.device
self.handler = None self.path = path
self.wallet = None
self.plugin = plugin self.plugin = plugin
self.tx_api = plugin self.tx_api = plugin
self.bad = False
self.msg_code_override = None 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 # Copied from trezorlib/client.py as there it is not static, sigh
@staticmethod @staticmethod
@ -94,34 +105,8 @@ def trezor_client_class(protocol_mixin, base_client, proto):
path.append(abs(int(x)) | prime) path.append(abs(int(x)) | prime)
return path return path
def check_proper_device(self, wallet): def address_from_derivation(self, derivation):
try: return self.get_address('Bitcoin', self.expand_path(derivation))
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 change_label(self, label): def change_label(self, label):
self.msg_code_override = '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): def atleast_version(self, major, minor=0, patch=0):
return cmp(self.firmware_version(), (major, minor, patch)) 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 re
import time
from binascii import unhexlify from binascii import unhexlify
from struct import pack from struct import pack
from unicodedata import normalize from unicodedata import normalize
@ -12,6 +14,9 @@ from electrum.transaction import (deserialize, is_extended_pubkey,
Transaction, x_to_xpub) Transaction, x_to_xpub)
from electrum.wallet import BIP32_HD_Wallet, BIP44_Wallet from electrum.wallet import BIP32_HD_Wallet, BIP44_Wallet
class DeviceDisconnectedError(Exception):
pass
class TrezorCompatibleWallet(BIP44_Wallet): class TrezorCompatibleWallet(BIP44_Wallet):
# Extend BIP44 Wallet as required by hardware implementation. # Extend BIP44 Wallet as required by hardware implementation.
# Derived classes must set: # Derived classes must set:
@ -22,11 +27,21 @@ class TrezorCompatibleWallet(BIP44_Wallet):
def __init__(self, storage): def __init__(self, storage):
BIP44_Wallet.__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): def disconnected(self):
self.print_error(message) self.print_error("disconnected")
raise Exception(message) self.handler.watching_only_changed()
def connected(self):
self.print_error("connected")
self.handler.watching_only_changed()
def get_action(self): def get_action(self):
pass pass
@ -35,29 +50,29 @@ class TrezorCompatibleWallet(BIP44_Wallet):
return False return False
def is_watching_only(self): 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() assert not self.has_seed()
return not self.proper_device return self.plugin.lookup_client(self) is None
def can_change_password(self): def can_change_password(self):
return False return False
def get_client(self): def client(self):
return self.plugin.get_client(self) return self.plugin.client(self)
def check_proper_device(self):
return self.get_client().check_proper_device(self)
def derive_xkeys(self, root, derivation, password): def derive_xkeys(self, root, derivation, password):
if self.master_public_keys.get(root): if self.master_public_keys.get(root):
return BIP44_wallet.derive_xkeys(self, root, derivation, password) 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() + "/") derivation = derivation.replace(self.root_name, self.prefix() + "/")
xpub = self.get_public_key(derivation) xpub = self.get_public_key(derivation)
return xpub, None return xpub, None
def get_public_key(self, bip32_path): def get_public_key(self, bip32_path):
client = self.get_client() client = self.client()
address_n = client.expand_path(bip32_path) address_n = client.expand_path(bip32_path)
node = client.get_public_node(address_n).node node = client.get_public_node(address_n).node
xpub = ("0488B21E".decode('hex') + chr(node.depth) xpub = ("0488B21E".decode('hex') + chr(node.depth)
@ -72,25 +87,15 @@ class TrezorCompatibleWallet(BIP44_Wallet):
raise RuntimeError(_('Decrypt method is not implemented')) raise RuntimeError(_('Decrypt method is not implemented'))
def sign_message(self, address, message, password): def sign_message(self, address, message, password):
client = self.get_client() client = self.client()
self.check_proper_device() address_path = self.address_id(address)
try: address_n = client.expand_path(address_path)
address_path = self.address_id(address) msg_sig = client.sign_message('Bitcoin', address_n, message)
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()
return msg_sig.signature return msg_sig.signature
def sign_transaction(self, tx, password): def sign_transaction(self, tx, password):
if tx.is_complete() or self.is_watching_only(): if tx.is_complete() or self.is_watching_only():
return return
self.check_proper_device()
# previous transactions used as inputs # previous transactions used as inputs
prev_tx = {} prev_tx = {}
# path of the xpubs that are involved # path of the xpubs that are involved
@ -123,51 +128,172 @@ class TrezorCompatiblePlugin(BasePlugin):
# libraries_available, libraries_URL, minimum_firmware, # libraries_available, libraries_URL, minimum_firmware,
# wallet_class, ckd_public, types, HidTransport # 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): def __init__(self, parent, config, name):
BasePlugin.__init__(self, parent, config, name) BasePlugin.__init__(self, parent, config, name)
self.device = self.wallet_class.device self.device = self.wallet_class.device
self.client = None
self.wallet_class.plugin = self 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): @hook
self.print_error(message) def timer_actions(self):
raise Exception(message) 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): def is_enabled(self):
return self.libraries_available 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 @staticmethod
def normalize_passphrase(self, passphrase): def normalize_passphrase(self, passphrase):
return normalize('NFKD', unicode(passphrase or '')) return normalize('NFKD', unicode(passphrase or ''))
@ -192,41 +318,33 @@ class TrezorCompatiblePlugin(BasePlugin):
@hook @hook
def close_wallet(self, wallet): def close_wallet(self, wallet):
if self.client: # Don't retain references to a closed wallet
self.print_error("clear session") self.paired_wallets.discard(wallet)
self.client.clear_session() client = self.lookup_client(wallet)
self.client.transport.close() if client:
self.client = None self.clear_session(client)
# Release the device
self.clients.discard(client)
client.transport.close()
def sign_transaction(self, wallet, tx, prev_tx, xpub_path): def sign_transaction(self, wallet, tx, prev_tx, xpub_path):
self.prev_tx = prev_tx self.prev_tx = prev_tx
self.xpub_path = xpub_path self.xpub_path = xpub_path
client = self.get_client() client = self.client(wallet)
inputs = self.tx_inputs(tx, True) inputs = self.tx_inputs(tx, True)
outputs = self.tx_outputs(wallet, tx) outputs = self.tx_outputs(wallet, tx)
try: signed_tx = client.sign_tx('Bitcoin', inputs, outputs)[1]
signed_tx = client.sign_tx('Bitcoin', inputs, outputs)[1]
except Exception as e:
self.give_error(e)
finally:
self.get_handler(wallet).stop()
raw = signed_tx.encode('hex') raw = signed_tx.encode('hex')
tx.update_signatures(raw) tx.update_signatures(raw)
def show_address(self, wallet, address): def show_address(self, wallet, address):
client = self.get_client() client = self.client(wallet)
wallet.check_proper_device() if not client.atleast_version(1, 3):
try: wallet.handler.show_error(_("Your device firmware is too old"))
address_path = wallet.address_id(address) return
address_n = self.client_class.expand_path(address_path) address_path = wallet.address_id(address)
except Exception as e: address_n = client.expand_path(address_path)
self.give_error(e) client.get_address('Bitcoin', address_n, True)
try:
client.get_address('Bitcoin', address_n, True)
except Exception as e:
self.give_error(e)
finally:
self.get_handler(wallet).stop()
def tx_inputs(self, tx, for_sig=False): def tx_inputs(self, tx, for_sig=False):
inputs = [] 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" icon_file = ":icons/trezor.png"
@staticmethod @classmethod
def pin_matrix_widget_class(): def pin_matrix_widget_class(self):
from trezorlib.qt.pinmatrix import PinMatrixWidget from trezorlib.qt.pinmatrix import PinMatrixWidget
return PinMatrixWidget return PinMatrixWidget

View File

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