URI: 
       tBetter support for USB devices - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 21bf5a8a84316b2f71540c95e3a66205e2c3d12a
   DIR parent 187b4dc9c1d1b03a13ab5bcea9516a970aed5ff9
  HTML 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:
         M .gitignore                          |       2 --
         M gui/qt/installwizard.py             |      14 +++-----------
         M gui/qt/main_window.py               |       7 ++++---
         M lib/plugins.py                      |       9 +++++----
         M lib/wallet.py                       |      16 +++++++++++++---
         M lib/wizard.py                       |      17 ++++++++++-------
         M plugins/keepkey/qt.py               |       8 +++++---
         M plugins/trezor/client.py            |      85 +++++++++++++++----------------
         M plugins/trezor/plugin.py            |     294 ++++++++++++++++++++++---------
         M plugins/trezor/qt.py                |       9 +++++----
         M plugins/trezor/qt_generic.py        |     105 +++++++++++++++----------------
       
       11 files changed, 343 insertions(+), 223 deletions(-)
       ---
   DIR diff --git a/.gitignore b/.gitignore
       t@@ -1,6 +1,4 @@
        ####-*.patch
       -gui/icons_rc.py
       -lib/icons_rc.py
        *.pyc
        *.swp
        build/
   DIR diff --git a/gui/qt/installwizard.py b/gui/qt/installwizard.py
       t@@ -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:
       t@@ -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)
       t@@ -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)
       t@@ -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()
   DIR diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py
       t@@ -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)
       t@@ -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():
       t@@ -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()
       t@@ -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):
   DIR diff --git a/lib/plugins.py b/lib/plugins.py
       t@@ -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
        
       t@@ -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):
   DIR diff --git a/lib/wallet.py b/lib/wallet.py
       t@@ -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)
       t@@ -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
   DIR diff --git a/lib/wizard.py b/lib/wizard.py
       t@@ -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):
       t@@ -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:
   DIR diff --git a/plugins/keepkey/qt.py b/plugins/keepkey/qt.py
       t@@ -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
   DIR diff --git a/plugins/trezor/client.py b/plugins/trezor/client.py
       t@@ -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):
       t@@ -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)
       t@@ -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
       t@@ -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'
       t@@ -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
   DIR diff --git a/plugins/trezor/plugin.py b/plugins/trezor/plugin.py
       t@@ -1,4 +1,6 @@
        import re
       +import time
       +
        from binascii import unhexlify
        from struct import pack
        from unicodedata import normalize
       t@@ -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:
       t@@ -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
       t@@ -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)
       t@@ -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
       t@@ -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):
       t@@ -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 = []
   DIR diff --git a/plugins/trezor/qt.py b/plugins/trezor/qt.py
       t@@ -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
   DIR diff --git a/plugins/trezor/qt_generic.py b/plugins/trezor/qt_generic.py
       t@@ -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 *
       t@@ -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)
       t@@ -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:
       t@@ -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:
       t@@ -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
       t@@ -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")
       t@@ -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():
       t@@ -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()
       t@@ -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"))
       t@@ -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