URI: 
       tHave Trezor dialog work even if wallet unpaired - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 2f1d6b237954b306cfc456a8f56ddcdc3a318ade
   DIR parent 237747620752b4697988622e68307ab82ba46bd6
  HTML Author: Neil Booth <kyuupichan@gmail.com>
       Date:   Sat,  9 Jan 2016 14:18:06 +0900
       
       Have Trezor dialog work even if wallet unpaired
       
       Required cleanup of handler logic.  Now every client
       is constructed with a handler, so there is never a
       question of not having one.
       
       Diffstat:
         M lib/plugins.py                      |     155 ++++++++++++++++---------------
         M plugins/trezor/client.py            |      45 +++++++++++---------------------
         M plugins/trezor/plugin.py            |      65 +++++++++++++++++--------------
         M plugins/trezor/qt_generic.py        |     147 +++++++++++++++++++------------
       
       4 files changed, 221 insertions(+), 191 deletions(-)
       ---
   DIR diff --git a/lib/plugins.py b/lib/plugins.py
       t@@ -218,102 +218,117 @@ class BasePlugin(PrintError):
        
        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.
       +    channel with the device.
        
       -    In addition to tracking device IDs, the device manager tracks
       -    hardware wallets and manages wallet pairing.  A device ID may be
       +    In addition to tracking device HID IDs, the device manager tracks
       +    hardware wallets and manages wallet pairing.  A HID 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.
       +    HID 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.
       +    does hardware scans when needed.  By tracking HID IDs, if a device
       +    is plugged into a different port the wallet is automatically
       +    re-paired.
        
            Wallets are informed on connect / disconnect events.  It must
            implement connected(), disconnected() callbacks.  Being connected
            implies a pairing.  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.'''
       +    Confusingly, the HID ID (serial number) reported by the HID system
       +    doesn't match the device ID reported by the device itself.  We use
       +    the HID IDs.
        
       -    # 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)
       +    This plugin is thread-safe.  Currently only devices supported by
       +    hidapi are implemented.
       +
       +    '''
        
            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.
       +        # Keyed by wallet.  The value is the hid_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
       +        # the value is a callback to create a client for those devices
                self.recognised_hardware = {}
                # For synchronization
                self.lock = threading.RLock()
        
       -    def register_devices(self, handler, device_pairs):
       +    def register_devices(self, device_pairs, create_client):
                for pair in device_pairs:
       -            self.recognised_hardware[pair] = handler
       +            self.recognised_hardware[pair] = create_client
       +
       +    def unpair(self, hid_id):
       +        with self.lock:
       +            wallet = self.wallet_by_hid_id(hid_id)
       +            if wallet:
       +                self.wallets[wallet] = None
        
            def close_client(self, client):
                with self.lock:
                    if client in self.clients:
                        self.clients.remove(client)
       -                client.close()
       +        if 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))
       +            hid_id = self.wallets.pop(wallet, None)
       +            self.close_client(self.client_by_hid_id(hid_id))
        
       -    def clients_of_type(self, classinfo):
       +    def unpaired_clients(self, handler, classinfo):
       +        '''Returns all unpaired clients of the given type.'''
       +        self.scan_devices(handler)
                with self.lock:
                    return [client for client in self.clients
       -                    if isinstance(client, classinfo)]
       -
       -    def client_by_device_id(self, device_id):
       +                    if isinstance(client, classinfo)
       +                    and not self.wallet_by_hid_id(client.hid_id())]
       +
       +    def client_by_hid_id(self, hid_id, handler=None):
       +        '''Like get_client() but when we don't care about wallet pairing.  If
       +        a device is wiped or in bootloader mode pairing is impossible;
       +        in such cases we communicate by device ID and not wallet.'''
       +        if handler:
       +            self.scan_devices(handler)
                with self.lock:
                    for client in self.clients:
       -                if client.device_id() == device_id:
       +                if client.hid_id() == hid_id:
                            return client
                    return None
        
       -    def wallet_by_device_id(self, device_id):
       +    def wallet_hid_id(self, wallet):
       +        with self.lock:
       +            return self.wallets.get(wallet)
       +
       +    def wallet_by_hid_id(self, hid_id):
                with self.lock:
       -            for wallet, wallet_device_id in self.wallets.items():
       -                if wallet_device_id == device_id:
       +            for wallet, wallet_hid_id in self.wallets.items():
       +                if wallet_hid_id == hid_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]
       +            return [wallet for (wallet, hid_id) in self.wallets.items()
       +                    if hid_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)
       +        self.wallets[wallet] = client.hid_id()
                wallet.connected()
        
       -    def scan_devices(self):
       +    def scan_devices(self, handler):
                # 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
       t@@ -326,29 +341,26 @@ class DeviceMgr(PrintError):
                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)
       +            create_client = self.recognised_hardware.get(product_key)
       +            if create_client:
       +                devices[d['serial_number']] = (create_client, d['path'])
        
                # Now find out what was disconnected
                with self.lock:
                    disconnected = [client for client in self.clients
       -                            if not client.device_id() in devices]
       +                            if not client.hid_id() in devices]
        
                # Close disconnected clients after informing their wallets
                for client in disconnected:
       -            wallet = self.wallet_by_device_id(client.device_id())
       +            wallet = self.wallet_by_hid_id(client.hid_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():
       +        for hid_id, (create_client, path) in devices.items():
                    try:
       -                client = handler.create_client(path, product_key)
       +                client = create_client(path, handler, hid_id)
                    except BaseException as e:
                        self.print_error("could not create client", str(e))
                        client = None
       t@@ -357,21 +369,26 @@ class DeviceMgr(PrintError):
                        with self.lock:
                            self.clients.append(client)
                        # Inform re-paired wallet
       -                wallet = self.wallet_by_device_id(device_id)
       +                wallet = self.wallet_by_hid_id(hid_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
       +    def get_client(self, wallet, force_pair=True):
       +        '''Returns a client for the wallet, or None if one could not be found.
       +        If force_pair is False then if an already paired client cannot
       +        be found None is returned rather than requiring user
       +        interaction.'''
       +        # We must scan devices to get an up-to-date idea of which
       +        # devices are present.  Operating on a client when its device
       +        # has been removed can cause the process to hang.
       +        # Unfortunately there is no plugged / unplugged notification
       +        # system.
       +        self.scan_devices(wallet.handler)
       +
       +        # Previously paired wallets only need look for matching HID IDs
       +        hid_id = self.wallet_hid_id(wallet)
       +        if hid_id:
       +            return self.client_by_hid_id(hid_id)
        
                first_address, derivation = wallet.first_address()
                # Wallets don't have a first address in the install wizard
       t@@ -380,29 +397,15 @@ class DeviceMgr(PrintError):
                    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()):
       +                if self.wallet_by_hid_id(client.hid_id()):
                            continue
                        # This will trigger a PIN/passphrase entry request
       -                if client.first_address(wallet, derivation) == first_address:
       +                if client.first_address(derivation) == first_address:
                            self.pair_wallet(wallet, client)
                            return client
        
   DIR diff --git a/plugins/trezor/client.py b/plugins/trezor/client.py
       t@@ -29,7 +29,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@@ -42,21 +42,21 @@ 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)
        
            def callback_WordRequest(self, msg):
                msg = _("Enter seed word as explained on your %s") % self.device
       -        word = self.handler().get_word(msg)
       +        word = self.handler.get_word(msg)
                if word is None:
                    return self.proto.Cancel()
                return self.proto.WordAck(word=word)
       t@@ -67,39 +67,31 @@ def trezor_client_class(protocol_mixin, base_client, proto):
        
            class TrezorClient(protocol_mixin, GuiMixin, base_client, PrintError):
        
       -        def __init__(self, transport, path, plugin):
       +        def __init__(self, transport, handler, plugin, hid_id):
                    base_client.__init__(self, transport)
                    protocol_mixin.__init__(self, transport)
                    self.proto = proto
                    self.device = plugin.device
       -            self.path = path
       -            self.wallet = None
       -            self.plugin = plugin
       +            self.handler = handler
       +            self.hid_id_ = hid_id
                    self.tx_api = plugin
                    self.msg_code_override = None
        
                def __str__(self):
       -            return "%s/%s/%s" % (self.label(), self.device_id(), self.path)
       +            return "%s/%s" % (self.label(), self.hid_id())
        
                def label(self):
                    '''The name given by the user to the device.'''
                    return self.features.label
        
       -        def device_id(self):
       -            '''The device serial number.'''
       -            return self.features.device_id
       +        def hid_id(self):
       +            '''The HID ID of the device.'''
       +            return self.hid_id_
        
                def is_initialized(self):
                    '''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
       -
                # Copied from trezorlib/client.py as there it is not static, sigh
                @staticmethod
                def expand_path(n):
       t@@ -116,14 +108,8 @@ 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 first_address(self, derivation):
       +            return self.address_from_derivation(derivation)
        
                def address_from_derivation(self, derivation):
                    return self.get_address('Bitcoin', self.expand_path(derivation))
       t@@ -188,14 +174,13 @@ def trezor_client_class(protocol_mixin, base_client, proto):
                any dialog box it opened.'''
        
                def wrapped(self, *args, **kwargs):
       -            handler = self.handler()
                    try:
                        return func(self, *args, **kwargs)
                    except BaseException as e:
       -                handler.show_error(str(e))
       +                self.handler.show_error(str(e))
                        raise e
                    finally:
       -                handler.finished()
       +                self.handler.finished()
        
                return wrapped
        
   DIR diff --git a/plugins/trezor/plugin.py b/plugins/trezor/plugin.py
       t@@ -35,15 +35,13 @@ class TrezorCompatibleWallet(BIP44_Wallet):
        
            def __init__(self, storage):
                BIP44_Wallet.__init__(self, storage)
       -        # 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
                # After timeout seconds we clear the device session
                self.session_timeout = storage.get('session_timeout', 180)
                # 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
       +        self.force_watching_only = True
        
            def set_session_timeout(self, seconds):
                self.print_error("setting session timeout to %d seconds" % seconds)
       t@@ -54,12 +52,14 @@ class TrezorCompatibleWallet(BIP44_Wallet):
                '''A device paired with the wallet was diconnected.  Note this is
                called in the context of the Plugins thread.'''
                self.print_error("disconnected")
       +        self.force_watching_only = True
                self.handler.watching_only_changed()
        
            def connected(self):
                '''A device paired with the wallet was (re-)connected.  Note this
                is called in the context of the Plugins thread.'''
                self.print_error("connected")
       +        self.force_watching_only = False
                self.handler.watching_only_changed()
        
            def timeout(self):
       t@@ -77,17 +77,15 @@ class TrezorCompatibleWallet(BIP44_Wallet):
                return False
        
            def is_watching_only(self):
       -        '''The wallet is watching-only if its trezor device is not connected,
       -        or if it is connected but uninitialized.'''
       +        '''The wallet is watching-only if its trezor device is unpaired.'''
                assert not self.has_seed()
       -        client = self.get_client(DeviceMgr.CACHED)
       -        return not (client and client.is_initialized())
       +        return self.force_watching_only
        
            def can_change_password(self):
                return False
        
       -    def get_client(self, lookup=DeviceMgr.PAIRED):
       -        return self.plugin.get_client(self, lookup)
       +    def get_client(self, force_pair=True):
       +        return self.plugin.get_client(self, force_pair)
        
            def first_address(self):
                '''Used to check a hardware wallet matches a software wallet'''
       t@@ -170,7 +168,8 @@ class TrezorCompatiblePlugin(BasePlugin, ThreadJob):
                self.wallet_class.plugin = self
                self.prevent_timeout = time.time() + 3600 * 24 * 365
                if self.libraries_available:
       -            self.device_manager().register_devices(self, self.DEVICE_IDS)
       +            self.device_manager().register_devices(
       +                self.DEVICE_IDS, self.create_client)
        
            def is_enabled(self):
                return self.libraries_available
       t@@ -188,15 +187,15 @@ class TrezorCompatiblePlugin(BasePlugin, ThreadJob):
                now = time.time()
                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)
       +                    and hasattr(wallet, 'last_operation')
       +                    and now > wallet.last_operation + wallet.session_timeout):
       +                client = self.get_client(wallet, force_pair=False)
                        if client:
       -                    wallet.last_operation = self.prevent_timeout
                            client.clear_session()
       +                    wallet.last_operation = self.prevent_timeout
                            wallet.timeout()
        
       -    def create_client(self, path, product_key):
       +    def create_client(self, path, handler, hid_id):
                pair = ((None, path) if self.HidTransport._detect_debuglink(path)
                        else (path, None))
                try:
       t@@ -206,14 +205,14 @@ class TrezorCompatiblePlugin(BasePlugin, ThreadJob):
                    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)
       +        return self.client_class(transport, handler, self, hid_id)
        
       -    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)
       +    def get_client(self, wallet, force_pair=True, check_firmware=True):
       +        '''check_firmware is ignored unless force_pair is True.'''
       +        client = self.device_manager().get_client(wallet, force_pair)
        
       -        # Try a ping if doing at least a PRESENT lookup
       -        if client and lookup != DeviceMgr.CACHED:
       +        # Try a ping for device sanity
       +        if client:
                    self.print_error("set last_operation")
                    wallet.last_operation = time.time()
                    try:
       t@@ -224,7 +223,7 @@ class TrezorCompatiblePlugin(BasePlugin, ThreadJob):
                        self.device_manager().close_client(client)
                        client = None
        
       -        if lookup == DeviceMgr.PAIRED:
       +        if force_pair:
                    assert wallet.handler
                    if not client:
                        msg = (_('Could not connect to your %s.  Verify the '
       t@@ -295,19 +294,25 @@ class TrezorCompatiblePlugin(BasePlugin, ThreadJob):
                    client.load_device_by_xprv(item, pin, passphrase_protection,
                                               label, language)
        
       +    def unpaired_clients(self, handler):
       +        '''Returns all connected, unpaired devices as a list of clients and a
       +        list of descriptions.'''
       +        devmgr = self.device_manager()
       +        clients = devmgr.unpaired_clients(handler, self.client_class)
       +        states = [_("wiped"), _("initialized")]
       +        def client_desc(client):
       +            label = client.label() or _("An unnamed device")
       +            state = states[client.is_initialized()]
       +            return ("%s: serial number %s (%s)"
       +                    % (label, client.hid_id(), state))
       +        return clients, list(map(client_desc, clients))
       +
            def select_device(self, wallet):
                '''Called when creating a new wallet.  Select the device to use.  If
                the device is uninitialized, go through the intialization
                process.'''
       -        self.device_manager().scan_devices()
       -        clients = self.device_manager().clients_of_type(self.client_class)
       -        suffixes = [_(" (wiped)"), _(" (initialized)")]
       -        def client_desc(client):
       -            label = client.label() or _("An unnamed device")
       -            return label + suffixes[client.is_initialized()]
       -        labels = list(map(client_desc, clients))
       -
                msg = _("Please select which %s device to use:") % self.device
       +        clients, labels = self.unpaired_clients(wallet.handler)
                client = clients[wallet.handler.query_choice(msg, labels)]
                self.device_manager().pair_wallet(wallet, client)
                if not client.is_initialized():
   DIR diff --git a/plugins/trezor/qt_generic.py b/plugins/trezor/qt_generic.py
       t@@ -261,29 +261,69 @@ def qt_plugin_class(base_plugin_class):
                                   lambda: self.show_address(wallet, addrs[0]))
        
            def settings_dialog(self, window):
       -        dialog = SettingsDialog(window, self)
       -        window.wallet.handler.exec_dialog(dialog)
       +        hid_id = self.choose_device(window)
       +        if hid_id:
       +            dialog = SettingsDialog(window, self, hid_id)
       +            window.wallet.handler.exec_dialog(dialog)
       +
       +    def choose_device(self, window):
       +        '''This dialog box should be usable even if the user has
       +        forgotten their PIN or it is in bootloader mode.'''
       +        handler = window.wallet.handler
       +        hid_id = self.device_manager().wallet_hid_id(window.wallet)
       +        if not hid_id:
       +            clients, labels = self.unpaired_clients(handler)
       +            if clients:
       +                msg = _("Select a %s device:") % self.device
       +                choice = self.query_choice(window, msg, labels)
       +                if choice is not None:
       +                    hid_id = clients[choice].hid_id()
       +            else:
       +                handler.show_error(_("No devices found"))
       +        return hid_id
       +
       +    def query_choice(self, window, msg, choices):
       +        dialog = WindowModalDialog(window)
       +        clayout = ChoicesLayout(msg, choices)
       +        layout = clayout.layout()
       +        layout.addStretch(1)
       +        layout.addLayout(Buttons(CancelButton(dialog), OkButton(dialog)))
       +        dialog.setLayout(layout)
       +        if not dialog.exec_():
       +            return None
       +        return clayout.selected_index()
       +
        
          return QtPlugin
        
        
        class SettingsDialog(WindowModalDialog):
       +    '''This dialog doesn't require a device be paired with a wallet.
       +    We want users to be able to wipe a device even if they've forgotten
       +    their PIN.'''
        
       -    def __init__(self, window, plugin):
       -        self.plugin = plugin
       -        self.window = window  # The main electrum window
       +    def __init__(self, window, plugin, hid_id):
                title = _("%s Settings") % plugin.device
                super(SettingsDialog, self).__init__(window, title)
                self.setMaximumWidth(540)
       +
       +        devmgr = plugin.device_manager()
       +        handler = window.wallet.handler
       +        # wallet can be None, needn't be window.wallet
       +        wallet = devmgr.wallet_by_hid_id(hid_id)
                hs_rows, hs_cols = (64, 128)
        
       -        def get_client(lookup=DeviceMgr.PAIRED):
       -            return self.plugin.get_client(wallet, lookup)
       +        def get_client():
       +            client = devmgr.client_by_hid_id(hid_id, handler)
       +            if not client:
       +                self.show_error("Device not connected!")
       +                raise RuntimeError("Device not connected")
       +            return client
        
                def update():
       -            features = get_client(DeviceMgr.PAIRED).features
       -            self.features = features
       -            # The above was for outer scopes.  Now the real logic.
       +            # self.features for outer scopes
       +            client = get_client()
       +            features = self.features = client.features
                    set_label_enabled()
                    bl_hash = features.bootloader_hash.encode('hex')
                    bl_hash = "\n".join([bl_hash[:32], bl_hash[32:]])
       t@@ -301,6 +341,7 @@ class SettingsDialog(WindowModalDialog):
                    bl_hash_label.setText(bl_hash)
                    label_edit.setText(features.label)
                    device_id_label.setText(features.device_id)
       +            serial_number_label.setText(client.hid_id())
                    initialized_label.setText(noyes[features.initialized])
                    version_label.setText(version)
                    coins_label.setText(coins)
       t@@ -309,7 +350,6 @@ class SettingsDialog(WindowModalDialog):
                    pin_button.setText(setchange[features.pin_protection])
                    pin_msg.setVisible(not features.pin_protection)
                    passphrase_button.setText(endis[features.passphrase_protection])
       -
                    language_label.setText(features.language)
        
                def set_label_enabled():
       t@@ -331,7 +371,7 @@ class SettingsDialog(WindowModalDialog):
                    if not self.question(msg, title=title):
                        return
                    get_client().toggle_passphrase()
       -            self.device_manager().close_wallet(wallet)  # Unpair
       +            devmgr.unpair(hid_id)
                    update()
        
                def change_homescreen():
       t@@ -362,27 +402,25 @@ class SettingsDialog(WindowModalDialog):
                    set_pin(remove=True)
        
                def wipe_device():
       -            # FIXME: cannot yet wipe a device that is only plugged in
       -            if sum(wallet.get_balance()):
       +            if wallet and sum(wallet.get_balance()):
                        title = _("Confirm Device Wipe")
                        msg = _("Are you SURE you want to wipe the device?\n"
                                "Your wallet still has bitcoins in it!")
                        if not self.question(msg, title=title,
                                             icon=QMessageBox.Critical):
                            return
       -            # 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()
       -            self.device_manager().close_wallet(wallet)
       +            get_client().wipe_device()
       +            devmgr.unpair(hid_id)
                    update()
        
                def slider_moved():
                    mins = timeout_slider.sliderPosition()
                    timeout_minutes.setText(_("%2d minutes") % mins)
        
       -        wallet = window.wallet
       -        handler = wallet.handler
       -        device = plugin.device
       +        def slider_released():
       +            seconds = timeout_slider.sliderPosition() * 60
       +            wallet.set_session_timeout(seconds)
       +
                dialog_vbox = QVBoxLayout(self)
        
                # Information tab
       t@@ -394,6 +432,7 @@ class SettingsDialog(WindowModalDialog):
                pin_set_label = QLabel()
                version_label = QLabel()
                device_id_label = QLabel()
       +        serial_number_label = QLabel()
                bl_hash_label = QLabel()
                bl_hash_label.setWordWrap(True)
                coins_label = QLabel()
       t@@ -404,7 +443,8 @@ class SettingsDialog(WindowModalDialog):
                    (_("Device Label"), device_label),
                    (_("PIN set"), pin_set_label),
                    (_("Firmware Version"), version_label),
       -            (_("Serial Number"), device_id_label),
       +            (_("Device ID"), device_id_label),
       +            (_("Serial Number"), serial_number_label),
                    (_("Bootloader Hash"), bl_hash_label),
                    (_("Supported Coins"), coins_label),
                    (_("Language"), language_label),
       t@@ -419,7 +459,6 @@ class SettingsDialog(WindowModalDialog):
                settings_tab = QWidget()
                settings_layout = QVBoxLayout(settings_tab)
                settings_glayout = QGridLayout()
       -        #settings_glayout.setColumnStretch(3, 1)
        
                # Settings tab - Label
                label_msg = QLabel(_("Name this %s.  If you have mutiple devices "
       t@@ -429,7 +468,7 @@ class SettingsDialog(WindowModalDialog):
                label_label = QLabel(_("Device Label"))
                label_edit = QLineEdit()
                label_edit.setMinimumWidth(150)
       -        label_edit.setMaxLength(self.plugin.MAX_LABEL_LEN)
       +        label_edit.setMaxLength(plugin.MAX_LABEL_LEN)
                label_apply = QPushButton(_("Apply"))
                label_apply.clicked.connect(rename)
                label_edit.textChanged.connect(set_label_enabled)
       t@@ -451,7 +490,6 @@ class SettingsDialog(WindowModalDialog):
                pin_msg.setWordWrap(True)
                pin_msg.setStyleSheet("color: red")
                settings_glayout.addWidget(pin_msg, 3, 1, 1, -1)
       -        settings_layout.addLayout(settings_glayout)
        
                # Settings tab - Homescreen
                homescreen_layout = QHBoxLayout()
       t@@ -471,25 +509,31 @@ class SettingsDialog(WindowModalDialog):
                settings_glayout.addWidget(homescreen_msg, 5, 1, 1, -1)
        
                # Settings tab - Session Timeout
       -        timeout_label = QLabel(_("Session Timeout"))
       -        timeout_minutes = QLabel()
       -        timeout_slider = self.slider = QSlider(Qt.Horizontal)
       -        timeout_slider.setRange(1, 60)
       -        timeout_slider.setSingleStep(1)
       -        timeout_slider.setSliderPosition(wallet.session_timeout // 60)
       -        timeout_slider.setTickInterval(5)
       -        timeout_slider.setTickPosition(QSlider.TicksBelow)
       -        timeout_slider.setTracking(True)
       -        timeout_slider.valueChanged.connect(slider_moved)
       -        timeout_msg = QLabel(_("Clear the session after the specified period "
       -                               "of inactivity.  Once a session has timed out, "
       -                               "your PIN and passphrase (if enabled) must be "
       -                               "re-entered to use the device."))
       -        timeout_msg.setWordWrap(True)
       -        settings_glayout.addWidget(timeout_label, 6, 0)
       -        settings_glayout.addWidget(timeout_slider, 6, 1, 1, 3)
       -        settings_glayout.addWidget(timeout_minutes, 6, 4)
       -        settings_glayout.addWidget(timeout_msg, 7, 1, 1, -1)
       +        if wallet:
       +            timeout_label = QLabel(_("Session Timeout"))
       +            timeout_minutes = QLabel()
       +            timeout_slider = QSlider(Qt.Horizontal)
       +            timeout_slider.setRange(1, 60)
       +            timeout_slider.setSingleStep(1)
       +            timeout_slider.setTickInterval(5)
       +            timeout_slider.setTickPosition(QSlider.TicksBelow)
       +            timeout_slider.setTracking(True)
       +            timeout_msg = QLabel(
       +                _("Clear the session after the specified period "
       +                  "of inactivity.  Once a session has timed out, "
       +                  "your PIN and passphrase (if enabled) must be "
       +                  "re-entered to use the device."))
       +            timeout_msg.setWordWrap(True)
       +            timeout_slider.setSliderPosition(wallet.session_timeout // 60)
       +            slider_moved()
       +            timeout_slider.valueChanged.connect(slider_moved)
       +            timeout_slider.sliderReleased.connect(slider_released)
       +            settings_glayout.addWidget(timeout_label, 6, 0)
       +            settings_glayout.addWidget(timeout_slider, 6, 1, 1, 3)
       +            settings_glayout.addWidget(timeout_minutes, 6, 4)
       +            settings_glayout.addWidget(timeout_msg, 7, 1, 1, -1)
       +        settings_layout.addLayout(settings_glayout)
       +        settings_layout.addStretch(1)
        
                # Advanced tab
                advanced_tab = QWidget()
       t@@ -499,9 +543,9 @@ class SettingsDialog(WindowModalDialog):
                # Advanced tab - clear PIN
                clear_pin_button = QPushButton(_("Disable PIN"))
                clear_pin_button.clicked.connect(clear_pin)
       -        clear_pin_warning = QLabel(_("If you disable your PIN, anyone with "
       -                                     "physical access to your %s device can "
       -                                     "spend your bitcoins.") % plugin.device)
       +        clear_pin_warning = QLabel(
       +            _("If you disable your PIN, anyone with physical access to your "
       +              "%s device can spend your bitcoins.") % plugin.device)
                clear_pin_warning.setWordWrap(True)
                clear_pin_warning.setStyleSheet("color: red")
                advanced_glayout.addWidget(clear_pin_button, 0, 2)
       t@@ -552,14 +596,7 @@ class SettingsDialog(WindowModalDialog):
                tabs.addTab(settings_tab, _("Settings"))
                tabs.addTab(advanced_tab, _("Advanced"))
        
       -        # Update information and then connect change slots
       +        # Update information
                update()
       -        slider_moved()
       -
                dialog_vbox.addWidget(tabs)
                dialog_vbox.addLayout(Buttons(CloseButton(self)))
       -
       -    def closeEvent(self, event):
       -        seconds = self.slider.sliderPosition() * 60
       -        self.window.wallet.set_session_timeout(seconds)
       -        event.accept()