Browse Source

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
master
Neil Booth 10 years ago
parent
commit
21bf5a8a84
  1. 2
      .gitignore
  2. 14
      gui/qt/installwizard.py
  3. 7
      gui/qt/main_window.py
  4. 9
      lib/plugins.py
  5. 16
      lib/wallet.py
  6. 17
      lib/wizard.py
  7. 8
      plugins/keepkey/qt.py
  8. 85
      plugins/trezor/client.py
  9. 268
      plugins/trezor/plugin.py
  10. 9
      plugins/trezor/qt.py
  11. 99
      plugins/trezor/qt_generic.py

2
.gitignore vendored

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

14
gui/qt/installwizard.py

@ -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()

7
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.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):

9
lib/plugins.py

@ -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):

16
lib/wallet.py

@ -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 + "'"
@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): def address_id(self, address):
acc_id, (change, address_index) = self.get_address_index(address) acc_id, (change, address_index) = self.get_address_index(address)
account_derivation = self.account_derivation(acc_id) return self.address_derivation(acc_id, change, address_index)
return "%s/%d/%d" % (account_derivation, change, address_index)
def mnemonic_to_seed(self, mnemonic, passphrase): @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

17
lib/wizard.py

@ -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:

8
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" 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

85
plugins/trezor/client.py

@ -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):
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: try:
return base_client.call_raw(self, msg) return func(self, *args, **kwargs)
except: except BaseException as e:
self.print_error("Marking %s client bad" % self.device) handler.show_error(str(e))
self.bad = True raise e
raise 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

268
plugins/trezor/plugin.py

@ -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
def give_error(self, message): self.device_id = None
self.print_error(message) # Errors and other user interaction is done through the wallet's
raise Exception(message) # 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): 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()
try:
address_path = self.address_id(address) address_path = self.address_id(address)
address_n = client.expand_path(address_path) 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) 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,50 +128,171 @@ 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
def is_enabled(self): try:
return self.libraries_available 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
def create_client(self): self.print_error("connected to device at", path[0])
if not self.libraries_available:
self.give_error(_('please install the %s libraries from %s')
% (self.device, self.libraries_URL))
devices = self.HidTransport.enumerate() try:
if not devices: client = self.client_class(transport, path, self)
self.give_error(_('Could not connect to your %s. Verify the ' except BaseException as e:
'cable is connected and that no other app is ' self.print_error("cannot create client for", path, str(e))
'using it.\nContinuing in watching-only mode.' else:
% self.device)) 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
transport = self.HidTransport(devices[0])
client = self.client_class(transport, self)
if not client.atleast_version(*self.minimum_firmware): if not client.atleast_version(*self.minimum_firmware):
self.give_error(_('Outdated %s firmware. Please update the ' wallet.handler.show_error(
'firmware from %s') _('Outdated %s firmware for device labelled %s. Please '
% (self.device, self.firmware_URL)) '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 client
def get_handler(self, wallet): return None
return self.get_client(wallet).handler
def lookup_client(self, wallet):
for client in self.clients:
if client.features.device_id == wallet.device_id:
return client
return None
def get_client(self, wallet=None): def client(self, wallet):
if not self.client or self.client.bad: '''Returns a wrapped client which handles cleanup in case of
self.client = self.create_client() 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 self.client return client
def atleast_version(self, major, minor=0, patch=0): def is_enabled(self):
return self.get_client().atleast_version(major, minor, patch) return self.libraries_available
@staticmethod @staticmethod
def normalize_passphrase(self, passphrase): def normalize_passphrase(self, passphrase):
@ -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"))
return
address_path = wallet.address_id(address) address_path = wallet.address_id(address)
address_n = self.client_class.expand_path(address_path) address_n = client.expand_path(address_path)
except Exception as e:
self.give_error(e)
try:
client.get_address('Bitcoin', address_n, True) 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 = []

9
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" 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

99
plugins/trezor/qt_generic.py

@ -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:
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: # 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)
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)) partial(self.settings_dialog, window))
window.statusBar().addPermanentWidget(self.button) window.statusBar().addPermanentWidget(window.tzb)
except Exception as e: wallet.handler = self.create_handler(window)
window.show_error(str(e)) # Trigger a pairing
self.client(wallet)
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()

Loading…
Cancel
Save