tUse a shared device manager - electrum - Electrum Bitcoin wallet
 (HTM) git clone https://git.parazyd.org/electrum
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) Submodules
       ---
 (DIR) commit 3d9f321cae28f5806e5d75d9032d3c89a7459dd2
 (DIR) parent 5b8e096d5709e051024c91a1b637ff2bc9b2cb74
 (HTM) Author: Neil Booth <kyuupichan@gmail.com>
       Date:   Tue,  5 Jan 2016 06:47:14 +0900
       
       Use a shared device manager
       
       Use a shared device manager across USB devices (not yet taken
       advantage of by ledger).  This reduces USB scans and abstracts
       device management cleanly.
       
       We no longer scan at regular intervals in a background thread.
       
       Diffstat:
         M lib/plugins.py                      |     198 +++++++++++++++++++++++++++++++
         M plugins/keepkey/keepkey.py          |       4 ++--
         M plugins/trezor/client.py            |      32 ++++++++++++++++++++++++++++++-
         M plugins/trezor/plugin.py            |     264 +++++++++++---------------------
         M plugins/trezor/qt_generic.py        |      23 ++++++++++++++---------
         M plugins/trezor/trezor.py            |       2 +-
       
       6 files changed, 334 insertions(+), 189 deletions(-)
       ---
 (DIR) diff --git a/lib/plugins.py b/lib/plugins.py
       t@@ -44,6 +44,8 @@ class Plugins(DaemonThread):
                self.plugins = {}
                self.gui_name = gui_name
                self.descriptions = []
       +        self.device_manager = DeviceMgr()
       +
                for loader, name, ispkg in pkgutil.iter_modules([self.pkgpath]):
                    m = loader.find_module(name).load_module(name)
                    d = m.__dict__
       t@@ -212,3 +214,199 @@ class BasePlugin(PrintError):
        
            def settings_dialog(self):
                pass
       +
       +
       +class DeviceMgr(PrintError):
       +    '''Manages hardware clients.  A client communicates over a hardware
       +    channel with the device.  A client is a pair: a device ID (serial
       +    number) and hardware port.  If either change then a different
       +    client is instantiated.
       +
       +    In addition to tracking device IDs, the device manager tracks
       +    hardware wallets and manages wallet pairing.  A device ID may be
       +    paired with a wallet when it is confirmed that the hardware device
       +    matches the wallet, i.e. they have the same master public key.  A
       +    device ID can be unpaired if e.g. it is wiped.
       +
       +    Because of hotplugging, a wallet must request its client
       +    dynamically each time it is required, rather than caching it
       +    itself.
       +
       +    The device manager is shared across plugins, so just one place
       +    does hardware scans when needed.  By tracking device serial
       +    numbers the number of necessary hardware scans is reduced, e.g. if
       +    a device is plugged into a different port the wallet is
       +    automatically re-paired.
       +
       +    Wallets are informed on connect / disconnect / unpairing events.
       +    It must implement connected(), disconnected() and unpaired()
       +    callbacks.  Being connected implies a pairing.  Being disconnected
       +    doesn't.  Callbacks can happen in any thread context, and we do
       +    them without holding the lock.
       +
       +    This plugin is thread-safe.  Currently only USB is implemented.
       +    '''
       +
       +    # Client lookup types.  CACHED will look up in our client cache
       +    # only.  PRESENT will do a scan if there is no client in the cache.
       +    # PAIRED will try and pair the wallet, which will involve requesting
       +    # a PIN and passphrase if they are enabled
       +    (CACHED, PRESENT, PAIRED) = range(3)
       +
       +    def __init__(self):
       +        super(DeviceMgr, self).__init__()
       +        # Keyed by wallet.  The value is the device_id if the wallet
       +        # has been paired, and None otherwise.
       +        self.wallets = {}
       +        # A list of clients.  We create a client for every device present
       +        # that is of a registered hardware type
       +        self.clients = []
       +        # What we recognise.  Keyed by (vendor_id, product_id) pairs,
       +        # the value is a handler for those devices.  The handler must
       +        # implement
       +        self.recognised_hardware = {}
       +        # For synchronization
       +        self.lock = threading.RLock()
       +
       +    def register_devices(self, handler, device_pairs):
       +        for pair in device_pairs:
       +            self.recognised_hardware[pair] = handler
       +
       +    def close_client(self, client):
       +        with self.lock:
       +            if client in self.clients:
       +                self.clients.remove(client)
       +                client.close()
       +
       +    def close_wallet(self, wallet):
       +        # Remove the wallet from our list; close any client
       +        with self.lock:
       +            device_id = self.wallets.pop(wallet, None)
       +            self.close_client(self.client_by_device_id(device_id))
       +
       +    def clients_of_type(self, classinfo):
       +        with self.lock:
       +            return [client for client in self.clients
       +                    if isinstance(client, classinfo)]
       +
       +    def client_by_device_id(self, device_id):
       +        with self.lock:
       +            for client in self.clients:
       +                if client.device_id() == device_id:
       +                    return client
       +            return None
       +
       +    def wallet_by_device_id(self, device_id):
       +        with self.lock:
       +            for wallet, wallet_device_id in self.wallets.items():
       +                if wallet_device_id == device_id:
       +                    return wallet
       +            return None
       +
       +    def paired_wallets(self):
       +        with self.lock:
       +            return [wallet for (wallet, device_id) in self.wallets.items()
       +                    if device_id is not None]
       +
       +    def pair_wallet(self, wallet, client):
       +        assert client in self.clients
       +        self.print_error("paired:", wallet, client)
       +        self.wallets[wallet] = client.device_id()
       +        client.pair_wallet(wallet)
       +        wallet.connected()
       +
       +    def scan_devices(self):
       +        # All currently supported hardware libraries use hid, so we
       +        # assume it here.  This can be easily abstracted if necessary.
       +        # Note this import must be local so those without hardware
       +        # wallet libraries are not affected.
       +        import hid
       +
       +        self.print_error("scanning devices...")
       +
       +        # First see what's connected that we know about
       +        devices = {}
       +        for d in hid.enumerate(0, 0):
       +            product_key = (d['vendor_id'], d['product_id'])
       +            device_id = d['serial_number']
       +            path = d['path']
       +
       +            handler = self.recognised_hardware.get(product_key)
       +            if handler:
       +                devices[device_id] = (handler, path, product_key)
       +
       +        # Now find out what was disconnected
       +        with self.lock:
       +            disconnected = [client for client in self.clients
       +                            if not client.device_id() in devices]
       +
       +        # Close disconnected clients after informing their wallets
       +        for client in disconnected:
       +            wallet = self.wallet_by_device_id(client.device_id())
       +            if wallet:
       +                wallet.disconnected()
       +            self.close_client(client)
       +
       +        # Now see if any new devices are present.
       +        for device_id, (handler, path, product_key) in devices.items():
       +            try:
       +                client = handler.create_client(path, product_key)
       +            except BaseException as e:
       +                self.print_error("could not create client", str(e))
       +                client = None
       +            if client:
       +                self.print_error("client created for", path)
       +                with self.lock:
       +                    self.clients.append(client)
       +                # Inform re-paired wallet
       +                wallet = self.wallet_by_device_id(device_id)
       +                if wallet:
       +                    self.pair_wallet(wallet, client)
       +
       +    def get_client(self, wallet, lookup=PAIRED):
       +        '''Returns a client for the wallet, or None if one could not be
       +        found.'''
       +        with self.lock:
       +            device_id = self.wallets.get(wallet)
       +            client = self.client_by_device_id(device_id)
       +            if client:
       +                return client
       +
       +        if lookup == DeviceMgr.CACHED:
       +            return None
       +
       +        first_address, derivation = wallet.first_address()
       +        # Wallets don't have a first address in the install wizard
       +        # until account creation
       +        if not first_address:
       +            self.print_error("no first address for ", wallet)
       +            return None
       +
       +        # We didn't find it, so scan for new devices.  We scan as
       +        # little as possible: some people report a USB scan is slow on
       +        # Linux when a Trezor is plugged in
       +        self.scan_devices()
       +
       +        with self.lock:
       +            # Maybe the scan found it?  If the wallet has a device_id
       +            # from a prior pairing, we can determine success now.
       +            if device_id:
       +                return self.client_by_device_id(device_id)
       +
       +            # Stop here if no wake and we couldn't find it.
       +            if lookup == DeviceMgr.PRESENT:
       +                return None
       +
       +            # The wallet has not been previously paired, so get the
       +            # first address of all unpaired clients and compare.
       +            for client in self.clients:
       +                # If already paired skip it
       +                if self.wallet_by_device_id(client.device_id()):
       +                    continue
       +                # This will trigger a PIN/passphrase entry request
       +                if client.first_address(wallet, derivation) == first_address:
       +                    self.pair_wallet(wallet, client)
       +                    return client
       +
       +            # Not found
       +            return None
 (DIR) diff --git a/plugins/keepkey/keepkey.py b/plugins/keepkey/keepkey.py
       t@@ -17,7 +17,7 @@ class KeepKeyPlugin(TrezorCompatiblePlugin):
                client_class = trezor_client_class(ProtocolMixin, BaseClient, proto)
                import keepkeylib.ckd_public as ckd_public
                from keepkeylib.client import types
       -        from keepkeylib.transport_hid import HidTransport
       +        from keepkeylib.transport_hid import HidTransport, DEVICE_IDS
                libraries_available = True
       -    except:
       +    except ImportError:
                libraries_available = False
 (DIR) diff --git a/plugins/trezor/client.py b/plugins/trezor/client.py
       t@@ -77,7 +77,7 @@ def trezor_client_class(protocol_mixin, base_client, proto):
                    self.msg_code_override = None
        
                def __str__(self):
       -            return "%s/%s/%s" % (self.label(), self.device_id(), self.path[0])
       +            return "%s/%s/%s" % (self.label(), self.device_id(), self.path)
        
                def label(self):
                    '''The name given by the user to the device.'''
       t@@ -91,6 +91,9 @@ def trezor_client_class(protocol_mixin, base_client, proto):
                    '''True if initialized, False if wiped.'''
                    return self.features.initialized
        
       +        def pair_wallet(self, wallet):
       +            self.wallet = wallet
       +
                def handler(self):
                    assert self.wallet and self.wallet.handler
                    return self.wallet.handler
       t@@ -111,6 +114,15 @@ def trezor_client_class(protocol_mixin, base_client, proto):
                        path.append(abs(int(x)) | prime)
                    return path
        
       +        def first_address(self, wallet, derivation):
       +            assert not self.wallet
       +            # Assign the wallet so we have a handler
       +            self.wallet = wallet
       +            try:
       +                return self.address_from_derivation(derivation)
       +            finally:
       +                self.wallet = None
       +
                def address_from_derivation(self, derivation):
                    return self.get_address('Bitcoin', self.expand_path(derivation))
        
       t@@ -128,6 +140,24 @@ def trezor_client_class(protocol_mixin, base_client, proto):
                    finally:
                        self.msg_code_override = None
        
       +        def clear_session(self):
       +            '''Clear the session to force pin (and passphrase if enabled)
       +            re-entry.  Does not leak exceptions.'''
       +            self.print_error("clear session:", self)
       +            try:
       +                super(TrezorClient, self).clear_session()
       +            except BaseException as e:
       +                # If the device was removed it has the same effect...
       +                self.print_error("clear_session: ignoring error", str(e))
       +                pass
       +
       +        def close(self):
       +            '''Called when Our wallet was closed or the device removed.'''
       +            self.print_error("disconnected")
       +            self.clear_session()
       +            # Release the device
       +            self.transport.close()
       +
                def firmware_version(self):
                    f = self.features
                    return (f.major_version, f.minor_version, f.patch_version)
 (DIR) diff --git a/plugins/trezor/plugin.py b/plugins/trezor/plugin.py
       t@@ -13,14 +13,19 @@ from electrum.transaction import (deserialize, is_extended_pubkey,
                                          Transaction, x_to_xpub)
        from electrum.wallet import BIP32_HD_Wallet, BIP44_Wallet
        from electrum.util import ThreadJob
       +from electrum.plugins import DeviceMgr
        
        class DeviceDisconnectedError(Exception):
            pass
        
       +class OutdatedFirmwareError(Exception):
       +    pass
       +
        class TrezorCompatibleWallet(BIP44_Wallet):
            # Extend BIP44 Wallet as required by hardware implementation.
            # Derived classes must set:
            #   - device
       +    #   - DEVICE_IDS
            #   - wallet_type
        
            restore_wallet_class = BIP44_Wallet
       t@@ -76,14 +81,20 @@ class TrezorCompatibleWallet(BIP44_Wallet):
                '''The wallet is watching-only if its trezor device is not connected,
                or if it is connected but uninitialized.'''
                assert not self.has_seed()
       -        client = self.plugin.lookup_client(self)
       +        client = self.get_client(DeviceMgr.CACHED)
                return not (client and client.is_initialized())
        
            def can_change_password(self):
                return False
        
       -    def client(self):
       -        return self.plugin.client(self)
       +    def get_client(self, lookup=DeviceMgr.PAIRED):
       +        return self.plugin.get_client(self, lookup)
       +
       +    def first_address(self):
       +        '''Used to check a hardware wallet matches a software wallet'''
       +        account = self.accounts.get('0')
       +        derivation = self.address_derivation('0', 0, 0)
       +        return (account.first_address()[0] if account else None, derivation)
        
            def derive_xkeys(self, root, derivation, password):
                if self.master_public_keys.get(root):
       t@@ -96,7 +107,7 @@ class TrezorCompatibleWallet(BIP44_Wallet):
                return xpub, None
        
            def get_public_key(self, bip32_path):
       -        client = self.client()
       +        client = self.get_client()
                address_n = client.expand_path(bip32_path)
                node = client.get_public_node(address_n).node
                xpub = ("0488B21E".decode('hex') + chr(node.depth)
       t@@ -111,7 +122,7 @@ class TrezorCompatibleWallet(BIP44_Wallet):
                raise RuntimeError(_('Decrypt method is not implemented'))
        
            def sign_message(self, address, message, password):
       -        client = self.client()
       +        client = self.get_client()
                address_path = self.address_id(address)
                address_n = client.expand_path(address_path)
                msg_sig = client.sign_message('Bitcoin', address_n, message)
       t@@ -152,96 +163,89 @@ class TrezorCompatiblePlugin(BasePlugin, ThreadJob):
            #     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.wallet_class.plugin = self
                self.prevent_timeout = time.time() + 3600 * 24 * 365
       -        # 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()
       -        self.last_scan = 0
       +        self.device_manager().register_devices(self, self.DEVICE_IDS)
       +
       +    def is_enabled(self):
       +        return self.libraries_available
       +
       +    def device_manager(self):
       +        return self.parent.device_manager
        
            def thread_jobs(self):
       -        # Scan connected devices every second.  The test for libraries
       -        # available is necessary to recover wallets on machines without
       -        # libraries
       +        # Thread job to handle device timeouts
                return [self] if self.libraries_available else []
        
            def run(self):
       -        '''Runs in the context of the Plugins thread.'''
       +        '''Handle device timeouts.  Runs in the context of the Plugins
       +        thread.'''
                now = time.time()
       -        if now > self.last_scan + 1:
       -            self.last_scan = now
       -            self.scan_devices()
       -
       -            for wallet in self.paired_wallets:
       -                if now > wallet.last_operation + wallet.session_timeout:
       -                    client = self.lookup_client(wallet)
       -                    if client:
       -                        wallet.last_operation = self.prevent_timeout
       -                        self.clear_session(client)
       -                        wallet.timeout()
       -
       -    def scan_devices(self):
       -        '''Scan devices.  Runs in the context of the Plugins thread.'''
       -        paths = self.HidTransport.enumerate()
       -        connected = set([c for c in self.clients if c.path in paths])
       -        disconnected = self.clients - connected
       -
       -        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
       +        for wallet in self.device_manager().paired_wallets():
       +            if (isinstance(wallet, self.wallet_class)
       +                and hasattr(wallet, 'last_operation')
       +                and now > wallet.last_operation + wallet.session_timeout):
       +                client = self.get_client(wallet, DeviceMgr.CACHED)
       +                if client:
       +                    wallet.last_operation = self.prevent_timeout
       +                    client.clear_session()
       +                    wallet.timeout()
       +
       +    def create_client(self, path, product_key):
       +        pair = ((None, path) if self.HidTransport._detect_debuglink(path)
       +                else (path, None))
       +        try:
       +            transport = self.HidTransport(pair)
       +        except BaseException as e:
       +            # We were probably just disconnected; never mind
       +            self.print_error("cannot connect at", path, str(e))
       +            return None
       +        self.print_error("connected to device at", path)
       +        return self.client_class(transport, path, self)
        
       -            self.print_error("connected to device at", path[0])
       +    def get_client(self, wallet, lookup=DeviceMgr.PAIRED, check_firmware=True):
       +        '''check_firmware is ignored unless doing a PAIRED lookup.'''
       +        client = self.device_manager().get_client(wallet, lookup)
        
       +        # Try a ping if doing at least a PRESENT lookup
       +        if client and lookup != DeviceMgr.CACHED:
       +            self.print_error("set last_operation")
       +            wallet.last_operation = time.time()
                    try:
       -                client = self.client_class(transport, path, self)
       +                client.ping('t')
                    except BaseException as e:
       -                self.print_error("cannot create client for", path, str(e))
       -            else:
       -                self.clients.add(client)
       -                self.print_error("new device:", client)
       +                self.print_error("ping failed", str(e))
       +                # Remove it from the manager's cache
       +                self.device_manager().close_client(client)
       +                client = None
       +
       +        if lookup == DeviceMgr.PAIRED:
       +            assert wallet.handler
       +            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)
       +                wallet.handler.show_error(msg)
       +                raise DeviceDisconnectedError(msg)
       +
       +            if (check_firmware and not
       +                client.atleast_version(*self.minimum_firmware)):
       +                msg = (_('Outdated %s firmware for device labelled %s. Please '
       +                         'download the updated firmware from %s') %
       +                       (self.device, client.label(), self.firmware_URL))
       +                wallet.handler.show_error(msg)
       +                raise OutdatedFirmwareError(msg)
        
       -            # Inform reconnected wallets
       -            for wallet in self.paired_wallets:
       -                if wallet.device_id == client.features.device_id:
       -                    client.wallet = wallet
       -                    wallet.connected()
       +        return client
        
       -    def clear_session(self, client):
       -        # Clearing the session forces pin re-entry
       -        self.print_error("clear session:", client)
       -        client.clear_session()
       +    @hook
       +    def close_wallet(self, wallet):
       +        if isinstance(wallet, self.wallet_class):
       +            self.device_manager().close_wallet(wallet)
        
            def initialize_device(self, wallet, wizard):
                # Prevent timeouts during initialization
       t@@ -254,105 +258,25 @@ class TrezorCompatiblePlugin(BasePlugin, ThreadJob):
                strength = 64 * (strength + 2)    # 128, 192 or 256
                language = ''
        
       -        client = self.client(wallet)
       +        client = self.get_client(wallet)
                client.reset_device(True, strength, passphrase_protection,
                                    pin_protection, label, language)
        
       -
            def select_device(self, wallet, wizard):
                '''Called when creating a new wallet.  Select the device to use.  If
                the device is uninitialized, go through the intialization
                process.'''
       -        clients = list(self.clients)
       +        self.device_manager().scan_devices()
       +        clients = self.device_manager().clients_of_type(self.client_class)
                suffixes = [_("An unnamed device (wiped)"), _(" (initialized)")]
                labels = [client.label() + suffixes[client.is_initialized()]
                          for client in clients]
                msg = _("Please select which %s device to use:") % self.device
                client = clients[wizard.query_choice(msg, labels)]
       -        self.pair_wallet(wallet, client)
       +        self.device_manager().pair_wallet(wallet, client)
                if not client.is_initialized():
                    self.initialize_device(wallet, wizard)
        
       -    def operated_on(self, wallet):
       -        self.print_error("set last_operation")
       -        wallet.last_operation = time.time()
       -
       -    def pair_wallet(self, wallet, client):
       -        self.print_error("pairing wallet %s to device %s" % (wallet, client))
       -        self.operated_on(wallet)
       -        self.paired_wallets.add(wallet)
       -        wallet.device_id = client.features.device_id
       -        wallet.last_operation = time.time()
       -        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
       -
       -        self.operated_on(wallet)
       -        if wallet.device_id is None:
       -            client = self.try_to_pair_wallet(wallet)
       -        else:
       -            client = self.lookup_client(wallet)
       -
       -        if not client:
       -            msg = (_('Could not connect to your %s.  Verify the '
       -                     'cable is connected and that no other app is '
       -                     'using it.\nContinuing in watching-only mode '
       -                     'until the device is re-connected.') % self.device)
       -            if not self.clients:
       -                wallet.handler.show_error(msg)
       -            raise DeviceDisconnectedError(msg)
       -
       -        return client
       -
       -    def is_enabled(self):
       -        return self.libraries_available
       -
            def on_restore_wallet(self, wallet, wizard):
                assert isinstance(wallet, self.wallet_class)
        
       t@@ -371,22 +295,10 @@ class TrezorCompatiblePlugin(BasePlugin, ThreadJob):
                wallet.create_main_account(password)
                return wallet
        
       -    @hook
       -    def close_wallet(self, wallet):
       -        if isinstance(wallet, self.wallet_class):
       -            # 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.client(wallet)
       +        client = self.get_client(wallet)
                inputs = self.tx_inputs(tx, True)
                outputs = self.tx_outputs(wallet, tx)
                signed_tx = client.sign_tx('Bitcoin', inputs, outputs)[1]
       t@@ -394,7 +306,7 @@ class TrezorCompatiblePlugin(BasePlugin, ThreadJob):
                tx.update_signatures(raw)
        
            def show_address(self, wallet, address):
       -        client = self.client(wallet)
       +        client = self.get_client(wallet)
                if not client.atleast_version(1, 3):
                    wallet.handler.show_error(_("Your device firmware is too old"))
                    return
 (DIR) diff --git a/plugins/trezor/qt_generic.py b/plugins/trezor/qt_generic.py
       t@@ -10,7 +10,7 @@ from electrum_gui.qt.util import *
        from plugin import TrezorCompatiblePlugin
        
        from electrum.i18n import _
       -from electrum.plugins import hook
       +from electrum.plugins import hook, DeviceMgr
        from electrum.util import PrintError
        from electrum.wallet import BIP44_Wallet
        
       t@@ -132,7 +132,7 @@ def qt_plugin_class(base_plugin_class):
                window.statusBar().addPermanentWidget(window.tzb)
                wallet.handler = self.create_handler(window)
                # Trigger a pairing
       -        self.client(wallet)
       +        self.get_client(wallet)
        
            def on_create_wallet(self, wallet, wizard):
                assert type(wallet) == self.wallet_class
       t@@ -148,8 +148,8 @@ def qt_plugin_class(base_plugin_class):
        
            def settings_dialog(self, window):
        
       -        def client():
       -            return self.client(wallet)
       +        def get_client(lookup=DeviceMgr.PAIRED):
       +            return self.get_client(wallet, lookup)
        
                def add_rows_to_layout(layout, rows):
                    for row_num, items in enumerate(rows):
       t@@ -158,7 +158,7 @@ def qt_plugin_class(base_plugin_class):
                            layout.addWidget(widget, row_num, col_num)
        
                def refresh():
       -            features = client().features
       +            features = get_client(DeviceMgr.PAIRED).features
                    bl_hash = features.bootloader_hash.encode('hex').upper()
                    bl_hash = "%s...%s" % (bl_hash[:10], bl_hash[-10:])
                    version = "%d.%d.%d" % (features.major_version,
       t@@ -184,11 +184,11 @@ def qt_plugin_class(base_plugin_class):
                    response = QInputDialog().getText(dialog, title, msg)
                    if not response[1]:
                        return
       -            client().change_label(str(response[0]))
       +            get_client().change_label(str(response[0]))
                    refresh()
        
                def set_pin():
       -            client().set_pin(remove=False)
       +            get_client().set_pin(remove=False)
                    refresh()
        
                def clear_pin():
       t@@ -198,10 +198,11 @@ def qt_plugin_class(base_plugin_class):
                            "Are you certain you want to remove your PIN?") % device
                    if not dialog.question(msg, title=title):
                        return
       -            client().set_pin(remove=True)
       +            get_client().set_pin(remove=True)
                    refresh()
        
                def wipe_device():
       +            # FIXME: cannot yet wipe a device that is only plugged in
                    title = _("Confirm Device Wipe")
                    msg = _("Are you sure you want to wipe the device?  "
                            "You should make sure you have a copy of your recovery "
       t@@ -215,7 +216,11 @@ def qt_plugin_class(base_plugin_class):
                        if not dialog.question(msg, title=title,
                                               icon=QMessageBox.Critical):
                            return
       -            client().wipe_device()
       +            # Note: we use PRESENT so that a user who has forgotten
       +            # their PIN is not prevented from wiping their device
       +            get_client(DeviceMgr.PRESENT).wipe_device()
       +            wallet.wiped()
       +            self.device_manager().close_wallet(wallet)
                    refresh()
        
                def slider_moved():
 (DIR) diff --git a/plugins/trezor/trezor.py b/plugins/trezor/trezor.py
       t@@ -17,7 +17,7 @@ class TrezorPlugin(TrezorCompatiblePlugin):
                client_class = trezor_client_class(ProtocolMixin, BaseClient, proto)
                import trezorlib.ckd_public as ckd_public
                from trezorlib.client import types
       -        from trezorlib.transport_hid import HidTransport
       +        from trezorlib.transport_hid import HidTransport, DEVICE_IDS
                libraries_available = True
            except ImportError:
                libraries_available = False