electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 2f1d6b237954b306cfc456a8f56ddcdc3a318ade
parent 237747620752b4697988622e68307ab82ba46bd6
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:
Mlib/plugins.py | 155++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mplugins/trezor/client.py | 45+++++++++++++++------------------------------
Mplugins/trezor/plugin.py | 65+++++++++++++++++++++++++++++++++++------------------------------
Mplugins/trezor/qt_generic.py | 147+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
4 files changed, 221 insertions(+), 191 deletions(-)

diff --git a/lib/plugins.py b/lib/plugins.py @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/plugins/trezor/client.py b/plugins/trezor/client.py @@ -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): @@ -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) @@ -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): @@ -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)) @@ -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 diff --git a/plugins/trezor/plugin.py b/plugins/trezor/plugin.py @@ -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) @@ -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): @@ -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''' @@ -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 @@ -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: @@ -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: @@ -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 ' @@ -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(): diff --git a/plugins/trezor/qt_generic.py b/plugins/trezor/qt_generic.py @@ -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:]]) @@ -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) @@ -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(): @@ -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(): @@ -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 @@ -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() @@ -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), @@ -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 " @@ -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) @@ -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() @@ -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() @@ -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) @@ -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()