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.
This commit is contained in:
parent
2377476207
commit
2f1d6b2379
151
lib/plugins.py
151
lib/plugins.py
|
@ -218,102 +218,117 @@ class BasePlugin(PrintError):
|
||||||
|
|
||||||
class DeviceMgr(PrintError):
|
class DeviceMgr(PrintError):
|
||||||
'''Manages hardware clients. A client communicates over a hardware
|
'''Manages hardware clients. A client communicates over a hardware
|
||||||
channel with the device. A client is a pair: a device ID (serial
|
channel with the device.
|
||||||
number) and hardware port. If either change then a different
|
|
||||||
client is instantiated.
|
|
||||||
|
|
||||||
In addition to tracking device IDs, the device manager tracks
|
In addition to tracking device HID IDs, the device manager tracks
|
||||||
hardware wallets and manages wallet pairing. A device ID may be
|
hardware wallets and manages wallet pairing. A HID ID may be
|
||||||
paired with a wallet when it is confirmed that the hardware device
|
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
|
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
|
Because of hotplugging, a wallet must request its client
|
||||||
dynamically each time it is required, rather than caching it
|
dynamically each time it is required, rather than caching it
|
||||||
itself.
|
itself.
|
||||||
|
|
||||||
The device manager is shared across plugins, so just one place
|
The device manager is shared across plugins, so just one place
|
||||||
does hardware scans when needed. By tracking device serial
|
does hardware scans when needed. By tracking HID IDs, if a device
|
||||||
numbers the number of necessary hardware scans is reduced, e.g. if
|
is plugged into a different port the wallet is automatically
|
||||||
a device is plugged into a different port the wallet is
|
re-paired.
|
||||||
automatically re-paired.
|
|
||||||
|
|
||||||
Wallets are informed on connect / disconnect events. It must
|
Wallets are informed on connect / disconnect events. It must
|
||||||
implement connected(), disconnected() callbacks. Being connected
|
implement connected(), disconnected() callbacks. Being connected
|
||||||
implies a pairing. Callbacks can happen in any thread context,
|
implies a pairing. Callbacks can happen in any thread context,
|
||||||
and we do them without holding the lock.
|
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
|
This plugin is thread-safe. Currently only devices supported by
|
||||||
# only. PRESENT will do a scan if there is no client in the cache.
|
hidapi are implemented.
|
||||||
# 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):
|
def __init__(self):
|
||||||
super(DeviceMgr, self).__init__()
|
super(DeviceMgr, self).__init__()
|
||||||
# Keyed by wallet. The value is the device_id if the wallet
|
# Keyed by wallet. The value is the hid_id if the wallet has
|
||||||
# has been paired, and None otherwise.
|
# been paired, and None otherwise.
|
||||||
self.wallets = {}
|
self.wallets = {}
|
||||||
# A list of clients. We create a client for every device present
|
# A list of clients. We create a client for every device present
|
||||||
# that is of a registered hardware type
|
# that is of a registered hardware type
|
||||||
self.clients = []
|
self.clients = []
|
||||||
# What we recognise. Keyed by (vendor_id, product_id) pairs,
|
# What we recognise. Keyed by (vendor_id, product_id) pairs,
|
||||||
# the value is a handler for those devices. The handler must
|
# the value is a callback to create a client for those devices
|
||||||
# implement
|
|
||||||
self.recognised_hardware = {}
|
self.recognised_hardware = {}
|
||||||
# For synchronization
|
# For synchronization
|
||||||
self.lock = threading.RLock()
|
self.lock = threading.RLock()
|
||||||
|
|
||||||
def register_devices(self, handler, device_pairs):
|
def register_devices(self, device_pairs, create_client):
|
||||||
for pair in device_pairs:
|
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):
|
def close_client(self, client):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
if client in self.clients:
|
if client in self.clients:
|
||||||
self.clients.remove(client)
|
self.clients.remove(client)
|
||||||
client.close()
|
if client:
|
||||||
|
client.close()
|
||||||
|
|
||||||
def close_wallet(self, wallet):
|
def close_wallet(self, wallet):
|
||||||
# Remove the wallet from our list; close any client
|
# Remove the wallet from our list; close any client
|
||||||
with self.lock:
|
with self.lock:
|
||||||
device_id = self.wallets.pop(wallet, None)
|
hid_id = self.wallets.pop(wallet, None)
|
||||||
self.close_client(self.client_by_device_id(device_id))
|
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:
|
with self.lock:
|
||||||
return [client for client in self.clients
|
return [client for client in self.clients
|
||||||
if isinstance(client, classinfo)]
|
if isinstance(client, classinfo)
|
||||||
|
and not self.wallet_by_hid_id(client.hid_id())]
|
||||||
|
|
||||||
def client_by_device_id(self, device_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:
|
with self.lock:
|
||||||
for client in self.clients:
|
for client in self.clients:
|
||||||
if client.device_id() == device_id:
|
if client.hid_id() == hid_id:
|
||||||
return client
|
return client
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def wallet_by_device_id(self, device_id):
|
def wallet_hid_id(self, wallet):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
for wallet, wallet_device_id in self.wallets.items():
|
return self.wallets.get(wallet)
|
||||||
if wallet_device_id == device_id:
|
|
||||||
|
def wallet_by_hid_id(self, hid_id):
|
||||||
|
with self.lock:
|
||||||
|
for wallet, wallet_hid_id in self.wallets.items():
|
||||||
|
if wallet_hid_id == hid_id:
|
||||||
return wallet
|
return wallet
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def paired_wallets(self):
|
def paired_wallets(self):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
return [wallet for (wallet, device_id) in self.wallets.items()
|
return [wallet for (wallet, hid_id) in self.wallets.items()
|
||||||
if device_id is not None]
|
if hid_id is not None]
|
||||||
|
|
||||||
def pair_wallet(self, wallet, client):
|
def pair_wallet(self, wallet, client):
|
||||||
assert client in self.clients
|
assert client in self.clients
|
||||||
self.print_error("paired:", wallet, client)
|
self.print_error("paired:", wallet, client)
|
||||||
self.wallets[wallet] = client.device_id()
|
self.wallets[wallet] = client.hid_id()
|
||||||
client.pair_wallet(wallet)
|
|
||||||
wallet.connected()
|
wallet.connected()
|
||||||
|
|
||||||
def scan_devices(self):
|
def scan_devices(self, handler):
|
||||||
# All currently supported hardware libraries use hid, so we
|
# All currently supported hardware libraries use hid, so we
|
||||||
# assume it here. This can be easily abstracted if necessary.
|
# assume it here. This can be easily abstracted if necessary.
|
||||||
# Note this import must be local so those without hardware
|
# Note this import must be local so those without hardware
|
||||||
|
@ -326,29 +341,26 @@ class DeviceMgr(PrintError):
|
||||||
devices = {}
|
devices = {}
|
||||||
for d in hid.enumerate(0, 0):
|
for d in hid.enumerate(0, 0):
|
||||||
product_key = (d['vendor_id'], d['product_id'])
|
product_key = (d['vendor_id'], d['product_id'])
|
||||||
device_id = d['serial_number']
|
create_client = self.recognised_hardware.get(product_key)
|
||||||
path = d['path']
|
if create_client:
|
||||||
|
devices[d['serial_number']] = (create_client, d['path'])
|
||||||
handler = self.recognised_hardware.get(product_key)
|
|
||||||
if handler:
|
|
||||||
devices[device_id] = (handler, path, product_key)
|
|
||||||
|
|
||||||
# Now find out what was disconnected
|
# Now find out what was disconnected
|
||||||
with self.lock:
|
with self.lock:
|
||||||
disconnected = [client for client in self.clients
|
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
|
# Close disconnected clients after informing their wallets
|
||||||
for client in disconnected:
|
for client in disconnected:
|
||||||
wallet = self.wallet_by_device_id(client.device_id())
|
wallet = self.wallet_by_hid_id(client.hid_id())
|
||||||
if wallet:
|
if wallet:
|
||||||
wallet.disconnected()
|
wallet.disconnected()
|
||||||
self.close_client(client)
|
self.close_client(client)
|
||||||
|
|
||||||
# Now see if any new devices are present.
|
# 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:
|
try:
|
||||||
client = handler.create_client(path, product_key)
|
client = create_client(path, handler, hid_id)
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
self.print_error("could not create client", str(e))
|
self.print_error("could not create client", str(e))
|
||||||
client = None
|
client = None
|
||||||
|
@ -357,21 +369,26 @@ class DeviceMgr(PrintError):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
self.clients.append(client)
|
self.clients.append(client)
|
||||||
# Inform re-paired wallet
|
# Inform re-paired wallet
|
||||||
wallet = self.wallet_by_device_id(device_id)
|
wallet = self.wallet_by_hid_id(hid_id)
|
||||||
if wallet:
|
if wallet:
|
||||||
self.pair_wallet(wallet, client)
|
self.pair_wallet(wallet, client)
|
||||||
|
|
||||||
def get_client(self, wallet, lookup=PAIRED):
|
def get_client(self, wallet, force_pair=True):
|
||||||
'''Returns a client for the wallet, or None if one could not be
|
'''Returns a client for the wallet, or None if one could not be found.
|
||||||
found.'''
|
If force_pair is False then if an already paired client cannot
|
||||||
with self.lock:
|
be found None is returned rather than requiring user
|
||||||
device_id = self.wallets.get(wallet)
|
interaction.'''
|
||||||
client = self.client_by_device_id(device_id)
|
# We must scan devices to get an up-to-date idea of which
|
||||||
if client:
|
# devices are present. Operating on a client when its device
|
||||||
return client
|
# has been removed can cause the process to hang.
|
||||||
|
# Unfortunately there is no plugged / unplugged notification
|
||||||
|
# system.
|
||||||
|
self.scan_devices(wallet.handler)
|
||||||
|
|
||||||
if lookup == DeviceMgr.CACHED:
|
# Previously paired wallets only need look for matching HID IDs
|
||||||
return None
|
hid_id = self.wallet_hid_id(wallet)
|
||||||
|
if hid_id:
|
||||||
|
return self.client_by_hid_id(hid_id)
|
||||||
|
|
||||||
first_address, derivation = wallet.first_address()
|
first_address, derivation = wallet.first_address()
|
||||||
# Wallets don't have a first address in the install wizard
|
# 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)
|
self.print_error("no first address for ", wallet)
|
||||||
return None
|
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:
|
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
|
# The wallet has not been previously paired, so get the
|
||||||
# first address of all unpaired clients and compare.
|
# first address of all unpaired clients and compare.
|
||||||
for client in self.clients:
|
for client in self.clients:
|
||||||
# If already paired skip it
|
# If already paired skip it
|
||||||
if self.wallet_by_device_id(client.device_id()):
|
if self.wallet_by_hid_id(client.hid_id()):
|
||||||
continue
|
continue
|
||||||
# This will trigger a PIN/passphrase entry request
|
# 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)
|
self.pair_wallet(wallet, client)
|
||||||
return client
|
return client
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ class GuiMixin(object):
|
||||||
else:
|
else:
|
||||||
cancel_callback = None
|
cancel_callback = None
|
||||||
|
|
||||||
self.handler().show_message(message % self.device, cancel_callback)
|
self.handler.show_message(message % self.device, cancel_callback)
|
||||||
return self.proto.ButtonAck()
|
return self.proto.ButtonAck()
|
||||||
|
|
||||||
def callback_PinMatrixRequest(self, msg):
|
def callback_PinMatrixRequest(self, msg):
|
||||||
|
@ -42,21 +42,21 @@ class GuiMixin(object):
|
||||||
"Note the numbers have been shuffled!"))
|
"Note the numbers have been shuffled!"))
|
||||||
else:
|
else:
|
||||||
msg = _("Please enter %s PIN")
|
msg = _("Please enter %s PIN")
|
||||||
pin = self.handler().get_pin(msg % self.device)
|
pin = self.handler.get_pin(msg % self.device)
|
||||||
if not pin:
|
if not pin:
|
||||||
return self.proto.Cancel()
|
return self.proto.Cancel()
|
||||||
return self.proto.PinMatrixAck(pin=pin)
|
return self.proto.PinMatrixAck(pin=pin)
|
||||||
|
|
||||||
def callback_PassphraseRequest(self, req):
|
def callback_PassphraseRequest(self, req):
|
||||||
msg = _("Please enter your %s passphrase")
|
msg = _("Please enter your %s passphrase")
|
||||||
passphrase = self.handler().get_passphrase(msg % self.device)
|
passphrase = self.handler.get_passphrase(msg % self.device)
|
||||||
if passphrase is None:
|
if passphrase is None:
|
||||||
return self.proto.Cancel()
|
return self.proto.Cancel()
|
||||||
return self.proto.PassphraseAck(passphrase=passphrase)
|
return self.proto.PassphraseAck(passphrase=passphrase)
|
||||||
|
|
||||||
def callback_WordRequest(self, msg):
|
def callback_WordRequest(self, msg):
|
||||||
msg = _("Enter seed word as explained on your %s") % self.device
|
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:
|
if word is None:
|
||||||
return self.proto.Cancel()
|
return self.proto.Cancel()
|
||||||
return self.proto.WordAck(word=word)
|
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):
|
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)
|
base_client.__init__(self, transport)
|
||||||
protocol_mixin.__init__(self, transport)
|
protocol_mixin.__init__(self, transport)
|
||||||
self.proto = proto
|
self.proto = proto
|
||||||
self.device = plugin.device
|
self.device = plugin.device
|
||||||
self.path = path
|
self.handler = handler
|
||||||
self.wallet = None
|
self.hid_id_ = hid_id
|
||||||
self.plugin = plugin
|
|
||||||
self.tx_api = plugin
|
self.tx_api = plugin
|
||||||
self.msg_code_override = None
|
self.msg_code_override = None
|
||||||
|
|
||||||
def __str__(self):
|
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):
|
def label(self):
|
||||||
'''The name given by the user to the device.'''
|
'''The name given by the user to the device.'''
|
||||||
return self.features.label
|
return self.features.label
|
||||||
|
|
||||||
def device_id(self):
|
def hid_id(self):
|
||||||
'''The device serial number.'''
|
'''The HID ID of the device.'''
|
||||||
return self.features.device_id
|
return self.hid_id_
|
||||||
|
|
||||||
def is_initialized(self):
|
def is_initialized(self):
|
||||||
'''True if initialized, False if wiped.'''
|
'''True if initialized, False if wiped.'''
|
||||||
return self.features.initialized
|
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
|
# Copied from trezorlib/client.py as there it is not static, sigh
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def expand_path(n):
|
def expand_path(n):
|
||||||
|
@ -116,14 +108,8 @@ def trezor_client_class(protocol_mixin, base_client, proto):
|
||||||
path.append(abs(int(x)) | prime)
|
path.append(abs(int(x)) | prime)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
def first_address(self, wallet, derivation):
|
def first_address(self, derivation):
|
||||||
assert not self.wallet
|
return self.address_from_derivation(derivation)
|
||||||
# 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):
|
def address_from_derivation(self, derivation):
|
||||||
return self.get_address('Bitcoin', self.expand_path(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.'''
|
any dialog box it opened.'''
|
||||||
|
|
||||||
def wrapped(self, *args, **kwargs):
|
def wrapped(self, *args, **kwargs):
|
||||||
handler = self.handler()
|
|
||||||
try:
|
try:
|
||||||
return func(self, *args, **kwargs)
|
return func(self, *args, **kwargs)
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
handler.show_error(str(e))
|
self.handler.show_error(str(e))
|
||||||
raise e
|
raise e
|
||||||
finally:
|
finally:
|
||||||
handler.finished()
|
self.handler.finished()
|
||||||
|
|
||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
|
|
|
@ -35,15 +35,13 @@ class TrezorCompatibleWallet(BIP44_Wallet):
|
||||||
|
|
||||||
def __init__(self, storage):
|
def __init__(self, storage):
|
||||||
BIP44_Wallet.__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
|
# After timeout seconds we clear the device session
|
||||||
self.session_timeout = storage.get('session_timeout', 180)
|
self.session_timeout = storage.get('session_timeout', 180)
|
||||||
# Errors and other user interaction is done through the wallet's
|
# Errors and other user interaction is done through the wallet's
|
||||||
# handler. The handler is per-window and preserved across
|
# handler. The handler is per-window and preserved across
|
||||||
# device reconnects
|
# device reconnects
|
||||||
self.handler = None
|
self.handler = None
|
||||||
|
self.force_watching_only = True
|
||||||
|
|
||||||
def set_session_timeout(self, seconds):
|
def set_session_timeout(self, seconds):
|
||||||
self.print_error("setting session timeout to %d seconds" % 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
|
'''A device paired with the wallet was diconnected. Note this is
|
||||||
called in the context of the Plugins thread.'''
|
called in the context of the Plugins thread.'''
|
||||||
self.print_error("disconnected")
|
self.print_error("disconnected")
|
||||||
|
self.force_watching_only = True
|
||||||
self.handler.watching_only_changed()
|
self.handler.watching_only_changed()
|
||||||
|
|
||||||
def connected(self):
|
def connected(self):
|
||||||
'''A device paired with the wallet was (re-)connected. Note this
|
'''A device paired with the wallet was (re-)connected. Note this
|
||||||
is called in the context of the Plugins thread.'''
|
is called in the context of the Plugins thread.'''
|
||||||
self.print_error("connected")
|
self.print_error("connected")
|
||||||
|
self.force_watching_only = False
|
||||||
self.handler.watching_only_changed()
|
self.handler.watching_only_changed()
|
||||||
|
|
||||||
def timeout(self):
|
def timeout(self):
|
||||||
|
@ -77,17 +77,15 @@ class TrezorCompatibleWallet(BIP44_Wallet):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def is_watching_only(self):
|
def is_watching_only(self):
|
||||||
'''The wallet is watching-only if its trezor device is not connected,
|
'''The wallet is watching-only if its trezor device is unpaired.'''
|
||||||
or if it is connected but uninitialized.'''
|
|
||||||
assert not self.has_seed()
|
assert not self.has_seed()
|
||||||
client = self.get_client(DeviceMgr.CACHED)
|
return self.force_watching_only
|
||||||
return not (client and client.is_initialized())
|
|
||||||
|
|
||||||
def can_change_password(self):
|
def can_change_password(self):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_client(self, lookup=DeviceMgr.PAIRED):
|
def get_client(self, force_pair=True):
|
||||||
return self.plugin.get_client(self, lookup)
|
return self.plugin.get_client(self, force_pair)
|
||||||
|
|
||||||
def first_address(self):
|
def first_address(self):
|
||||||
'''Used to check a hardware wallet matches a software wallet'''
|
'''Used to check a hardware wallet matches a software wallet'''
|
||||||
|
@ -170,7 +168,8 @@ class TrezorCompatiblePlugin(BasePlugin, ThreadJob):
|
||||||
self.wallet_class.plugin = self
|
self.wallet_class.plugin = self
|
||||||
self.prevent_timeout = time.time() + 3600 * 24 * 365
|
self.prevent_timeout = time.time() + 3600 * 24 * 365
|
||||||
if self.libraries_available:
|
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):
|
def is_enabled(self):
|
||||||
return self.libraries_available
|
return self.libraries_available
|
||||||
|
@ -188,15 +187,15 @@ class TrezorCompatiblePlugin(BasePlugin, ThreadJob):
|
||||||
now = time.time()
|
now = time.time()
|
||||||
for wallet in self.device_manager().paired_wallets():
|
for wallet in self.device_manager().paired_wallets():
|
||||||
if (isinstance(wallet, self.wallet_class)
|
if (isinstance(wallet, self.wallet_class)
|
||||||
and hasattr(wallet, 'last_operation')
|
and hasattr(wallet, 'last_operation')
|
||||||
and now > wallet.last_operation + wallet.session_timeout):
|
and now > wallet.last_operation + wallet.session_timeout):
|
||||||
client = self.get_client(wallet, DeviceMgr.CACHED)
|
client = self.get_client(wallet, force_pair=False)
|
||||||
if client:
|
if client:
|
||||||
wallet.last_operation = self.prevent_timeout
|
|
||||||
client.clear_session()
|
client.clear_session()
|
||||||
|
wallet.last_operation = self.prevent_timeout
|
||||||
wallet.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)
|
pair = ((None, path) if self.HidTransport._detect_debuglink(path)
|
||||||
else (path, None))
|
else (path, None))
|
||||||
try:
|
try:
|
||||||
|
@ -206,14 +205,14 @@ class TrezorCompatiblePlugin(BasePlugin, ThreadJob):
|
||||||
self.print_error("cannot connect at", path, str(e))
|
self.print_error("cannot connect at", path, str(e))
|
||||||
return None
|
return None
|
||||||
self.print_error("connected to device at", path)
|
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):
|
def get_client(self, wallet, force_pair=True, check_firmware=True):
|
||||||
'''check_firmware is ignored unless doing a PAIRED lookup.'''
|
'''check_firmware is ignored unless force_pair is True.'''
|
||||||
client = self.device_manager().get_client(wallet, lookup)
|
client = self.device_manager().get_client(wallet, force_pair)
|
||||||
|
|
||||||
# Try a ping if doing at least a PRESENT lookup
|
# Try a ping for device sanity
|
||||||
if client and lookup != DeviceMgr.CACHED:
|
if client:
|
||||||
self.print_error("set last_operation")
|
self.print_error("set last_operation")
|
||||||
wallet.last_operation = time.time()
|
wallet.last_operation = time.time()
|
||||||
try:
|
try:
|
||||||
|
@ -224,7 +223,7 @@ class TrezorCompatiblePlugin(BasePlugin, ThreadJob):
|
||||||
self.device_manager().close_client(client)
|
self.device_manager().close_client(client)
|
||||||
client = None
|
client = None
|
||||||
|
|
||||||
if lookup == DeviceMgr.PAIRED:
|
if force_pair:
|
||||||
assert wallet.handler
|
assert wallet.handler
|
||||||
if not client:
|
if not client:
|
||||||
msg = (_('Could not connect to your %s. Verify the '
|
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,
|
client.load_device_by_xprv(item, pin, passphrase_protection,
|
||||||
label, language)
|
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):
|
def select_device(self, wallet):
|
||||||
'''Called when creating a new wallet. Select the device to use. If
|
'''Called when creating a new wallet. Select the device to use. If
|
||||||
the device is uninitialized, go through the intialization
|
the device is uninitialized, go through the intialization
|
||||||
process.'''
|
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
|
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)]
|
client = clients[wallet.handler.query_choice(msg, labels)]
|
||||||
self.device_manager().pair_wallet(wallet, client)
|
self.device_manager().pair_wallet(wallet, client)
|
||||||
if not client.is_initialized():
|
if not client.is_initialized():
|
||||||
|
|
|
@ -261,29 +261,69 @@ def qt_plugin_class(base_plugin_class):
|
||||||
lambda: self.show_address(wallet, addrs[0]))
|
lambda: self.show_address(wallet, addrs[0]))
|
||||||
|
|
||||||
def settings_dialog(self, window):
|
def settings_dialog(self, window):
|
||||||
dialog = SettingsDialog(window, self)
|
hid_id = self.choose_device(window)
|
||||||
window.wallet.handler.exec_dialog(dialog)
|
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
|
return QtPlugin
|
||||||
|
|
||||||
|
|
||||||
class SettingsDialog(WindowModalDialog):
|
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):
|
def __init__(self, window, plugin, hid_id):
|
||||||
self.plugin = plugin
|
|
||||||
self.window = window # The main electrum window
|
|
||||||
title = _("%s Settings") % plugin.device
|
title = _("%s Settings") % plugin.device
|
||||||
super(SettingsDialog, self).__init__(window, title)
|
super(SettingsDialog, self).__init__(window, title)
|
||||||
self.setMaximumWidth(540)
|
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)
|
hs_rows, hs_cols = (64, 128)
|
||||||
|
|
||||||
def get_client(lookup=DeviceMgr.PAIRED):
|
def get_client():
|
||||||
return self.plugin.get_client(wallet, lookup)
|
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():
|
def update():
|
||||||
features = get_client(DeviceMgr.PAIRED).features
|
# self.features for outer scopes
|
||||||
self.features = features
|
client = get_client()
|
||||||
# The above was for outer scopes. Now the real logic.
|
features = self.features = client.features
|
||||||
set_label_enabled()
|
set_label_enabled()
|
||||||
bl_hash = features.bootloader_hash.encode('hex')
|
bl_hash = features.bootloader_hash.encode('hex')
|
||||||
bl_hash = "\n".join([bl_hash[:32], bl_hash[32:]])
|
bl_hash = "\n".join([bl_hash[:32], bl_hash[32:]])
|
||||||
|
@ -301,6 +341,7 @@ class SettingsDialog(WindowModalDialog):
|
||||||
bl_hash_label.setText(bl_hash)
|
bl_hash_label.setText(bl_hash)
|
||||||
label_edit.setText(features.label)
|
label_edit.setText(features.label)
|
||||||
device_id_label.setText(features.device_id)
|
device_id_label.setText(features.device_id)
|
||||||
|
serial_number_label.setText(client.hid_id())
|
||||||
initialized_label.setText(noyes[features.initialized])
|
initialized_label.setText(noyes[features.initialized])
|
||||||
version_label.setText(version)
|
version_label.setText(version)
|
||||||
coins_label.setText(coins)
|
coins_label.setText(coins)
|
||||||
|
@ -309,7 +350,6 @@ class SettingsDialog(WindowModalDialog):
|
||||||
pin_button.setText(setchange[features.pin_protection])
|
pin_button.setText(setchange[features.pin_protection])
|
||||||
pin_msg.setVisible(not features.pin_protection)
|
pin_msg.setVisible(not features.pin_protection)
|
||||||
passphrase_button.setText(endis[features.passphrase_protection])
|
passphrase_button.setText(endis[features.passphrase_protection])
|
||||||
|
|
||||||
language_label.setText(features.language)
|
language_label.setText(features.language)
|
||||||
|
|
||||||
def set_label_enabled():
|
def set_label_enabled():
|
||||||
|
@ -331,7 +371,7 @@ class SettingsDialog(WindowModalDialog):
|
||||||
if not self.question(msg, title=title):
|
if not self.question(msg, title=title):
|
||||||
return
|
return
|
||||||
get_client().toggle_passphrase()
|
get_client().toggle_passphrase()
|
||||||
self.device_manager().close_wallet(wallet) # Unpair
|
devmgr.unpair(hid_id)
|
||||||
update()
|
update()
|
||||||
|
|
||||||
def change_homescreen():
|
def change_homescreen():
|
||||||
|
@ -362,27 +402,25 @@ class SettingsDialog(WindowModalDialog):
|
||||||
set_pin(remove=True)
|
set_pin(remove=True)
|
||||||
|
|
||||||
def wipe_device():
|
def wipe_device():
|
||||||
# FIXME: cannot yet wipe a device that is only plugged in
|
if wallet and sum(wallet.get_balance()):
|
||||||
if sum(wallet.get_balance()):
|
|
||||||
title = _("Confirm Device Wipe")
|
title = _("Confirm Device Wipe")
|
||||||
msg = _("Are you SURE you want to wipe the device?\n"
|
msg = _("Are you SURE you want to wipe the device?\n"
|
||||||
"Your wallet still has bitcoins in it!")
|
"Your wallet still has bitcoins in it!")
|
||||||
if not self.question(msg, title=title,
|
if not self.question(msg, title=title,
|
||||||
icon=QMessageBox.Critical):
|
icon=QMessageBox.Critical):
|
||||||
return
|
return
|
||||||
# Note: we use PRESENT so that a user who has forgotten
|
get_client().wipe_device()
|
||||||
# their PIN is not prevented from wiping their device
|
devmgr.unpair(hid_id)
|
||||||
get_client(DeviceMgr.PRESENT).wipe_device()
|
|
||||||
self.device_manager().close_wallet(wallet)
|
|
||||||
update()
|
update()
|
||||||
|
|
||||||
def slider_moved():
|
def slider_moved():
|
||||||
mins = timeout_slider.sliderPosition()
|
mins = timeout_slider.sliderPosition()
|
||||||
timeout_minutes.setText(_("%2d minutes") % mins)
|
timeout_minutes.setText(_("%2d minutes") % mins)
|
||||||
|
|
||||||
wallet = window.wallet
|
def slider_released():
|
||||||
handler = wallet.handler
|
seconds = timeout_slider.sliderPosition() * 60
|
||||||
device = plugin.device
|
wallet.set_session_timeout(seconds)
|
||||||
|
|
||||||
dialog_vbox = QVBoxLayout(self)
|
dialog_vbox = QVBoxLayout(self)
|
||||||
|
|
||||||
# Information tab
|
# Information tab
|
||||||
|
@ -394,6 +432,7 @@ class SettingsDialog(WindowModalDialog):
|
||||||
pin_set_label = QLabel()
|
pin_set_label = QLabel()
|
||||||
version_label = QLabel()
|
version_label = QLabel()
|
||||||
device_id_label = QLabel()
|
device_id_label = QLabel()
|
||||||
|
serial_number_label = QLabel()
|
||||||
bl_hash_label = QLabel()
|
bl_hash_label = QLabel()
|
||||||
bl_hash_label.setWordWrap(True)
|
bl_hash_label.setWordWrap(True)
|
||||||
coins_label = QLabel()
|
coins_label = QLabel()
|
||||||
|
@ -404,7 +443,8 @@ class SettingsDialog(WindowModalDialog):
|
||||||
(_("Device Label"), device_label),
|
(_("Device Label"), device_label),
|
||||||
(_("PIN set"), pin_set_label),
|
(_("PIN set"), pin_set_label),
|
||||||
(_("Firmware Version"), version_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),
|
(_("Bootloader Hash"), bl_hash_label),
|
||||||
(_("Supported Coins"), coins_label),
|
(_("Supported Coins"), coins_label),
|
||||||
(_("Language"), language_label),
|
(_("Language"), language_label),
|
||||||
|
@ -419,7 +459,6 @@ class SettingsDialog(WindowModalDialog):
|
||||||
settings_tab = QWidget()
|
settings_tab = QWidget()
|
||||||
settings_layout = QVBoxLayout(settings_tab)
|
settings_layout = QVBoxLayout(settings_tab)
|
||||||
settings_glayout = QGridLayout()
|
settings_glayout = QGridLayout()
|
||||||
#settings_glayout.setColumnStretch(3, 1)
|
|
||||||
|
|
||||||
# Settings tab - Label
|
# Settings tab - Label
|
||||||
label_msg = QLabel(_("Name this %s. If you have mutiple devices "
|
label_msg = QLabel(_("Name this %s. If you have mutiple devices "
|
||||||
|
@ -429,7 +468,7 @@ class SettingsDialog(WindowModalDialog):
|
||||||
label_label = QLabel(_("Device Label"))
|
label_label = QLabel(_("Device Label"))
|
||||||
label_edit = QLineEdit()
|
label_edit = QLineEdit()
|
||||||
label_edit.setMinimumWidth(150)
|
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 = QPushButton(_("Apply"))
|
||||||
label_apply.clicked.connect(rename)
|
label_apply.clicked.connect(rename)
|
||||||
label_edit.textChanged.connect(set_label_enabled)
|
label_edit.textChanged.connect(set_label_enabled)
|
||||||
|
@ -451,7 +490,6 @@ class SettingsDialog(WindowModalDialog):
|
||||||
pin_msg.setWordWrap(True)
|
pin_msg.setWordWrap(True)
|
||||||
pin_msg.setStyleSheet("color: red")
|
pin_msg.setStyleSheet("color: red")
|
||||||
settings_glayout.addWidget(pin_msg, 3, 1, 1, -1)
|
settings_glayout.addWidget(pin_msg, 3, 1, 1, -1)
|
||||||
settings_layout.addLayout(settings_glayout)
|
|
||||||
|
|
||||||
# Settings tab - Homescreen
|
# Settings tab - Homescreen
|
||||||
homescreen_layout = QHBoxLayout()
|
homescreen_layout = QHBoxLayout()
|
||||||
|
@ -471,25 +509,31 @@ class SettingsDialog(WindowModalDialog):
|
||||||
settings_glayout.addWidget(homescreen_msg, 5, 1, 1, -1)
|
settings_glayout.addWidget(homescreen_msg, 5, 1, 1, -1)
|
||||||
|
|
||||||
# Settings tab - Session Timeout
|
# Settings tab - Session Timeout
|
||||||
timeout_label = QLabel(_("Session Timeout"))
|
if wallet:
|
||||||
timeout_minutes = QLabel()
|
timeout_label = QLabel(_("Session Timeout"))
|
||||||
timeout_slider = self.slider = QSlider(Qt.Horizontal)
|
timeout_minutes = QLabel()
|
||||||
timeout_slider.setRange(1, 60)
|
timeout_slider = QSlider(Qt.Horizontal)
|
||||||
timeout_slider.setSingleStep(1)
|
timeout_slider.setRange(1, 60)
|
||||||
timeout_slider.setSliderPosition(wallet.session_timeout // 60)
|
timeout_slider.setSingleStep(1)
|
||||||
timeout_slider.setTickInterval(5)
|
timeout_slider.setTickInterval(5)
|
||||||
timeout_slider.setTickPosition(QSlider.TicksBelow)
|
timeout_slider.setTickPosition(QSlider.TicksBelow)
|
||||||
timeout_slider.setTracking(True)
|
timeout_slider.setTracking(True)
|
||||||
timeout_slider.valueChanged.connect(slider_moved)
|
timeout_msg = QLabel(
|
||||||
timeout_msg = QLabel(_("Clear the session after the specified period "
|
_("Clear the session after the specified period "
|
||||||
"of inactivity. Once a session has timed out, "
|
"of inactivity. Once a session has timed out, "
|
||||||
"your PIN and passphrase (if enabled) must be "
|
"your PIN and passphrase (if enabled) must be "
|
||||||
"re-entered to use the device."))
|
"re-entered to use the device."))
|
||||||
timeout_msg.setWordWrap(True)
|
timeout_msg.setWordWrap(True)
|
||||||
settings_glayout.addWidget(timeout_label, 6, 0)
|
timeout_slider.setSliderPosition(wallet.session_timeout // 60)
|
||||||
settings_glayout.addWidget(timeout_slider, 6, 1, 1, 3)
|
slider_moved()
|
||||||
settings_glayout.addWidget(timeout_minutes, 6, 4)
|
timeout_slider.valueChanged.connect(slider_moved)
|
||||||
settings_glayout.addWidget(timeout_msg, 7, 1, 1, -1)
|
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
|
||||||
advanced_tab = QWidget()
|
advanced_tab = QWidget()
|
||||||
|
@ -499,9 +543,9 @@ class SettingsDialog(WindowModalDialog):
|
||||||
# Advanced tab - clear PIN
|
# Advanced tab - clear PIN
|
||||||
clear_pin_button = QPushButton(_("Disable PIN"))
|
clear_pin_button = QPushButton(_("Disable PIN"))
|
||||||
clear_pin_button.clicked.connect(clear_pin)
|
clear_pin_button.clicked.connect(clear_pin)
|
||||||
clear_pin_warning = QLabel(_("If you disable your PIN, anyone with "
|
clear_pin_warning = QLabel(
|
||||||
"physical access to your %s device can "
|
_("If you disable your PIN, anyone with physical access to your "
|
||||||
"spend your bitcoins.") % plugin.device)
|
"%s device can spend your bitcoins.") % plugin.device)
|
||||||
clear_pin_warning.setWordWrap(True)
|
clear_pin_warning.setWordWrap(True)
|
||||||
clear_pin_warning.setStyleSheet("color: red")
|
clear_pin_warning.setStyleSheet("color: red")
|
||||||
advanced_glayout.addWidget(clear_pin_button, 0, 2)
|
advanced_glayout.addWidget(clear_pin_button, 0, 2)
|
||||||
|
@ -552,14 +596,7 @@ class SettingsDialog(WindowModalDialog):
|
||||||
tabs.addTab(settings_tab, _("Settings"))
|
tabs.addTab(settings_tab, _("Settings"))
|
||||||
tabs.addTab(advanced_tab, _("Advanced"))
|
tabs.addTab(advanced_tab, _("Advanced"))
|
||||||
|
|
||||||
# Update information and then connect change slots
|
# Update information
|
||||||
update()
|
update()
|
||||||
slider_moved()
|
|
||||||
|
|
||||||
dialog_vbox.addWidget(tabs)
|
dialog_vbox.addWidget(tabs)
|
||||||
dialog_vbox.addLayout(Buttons(CloseButton(self)))
|
dialog_vbox.addLayout(Buttons(CloseButton(self)))
|
||||||
|
|
||||||
def closeEvent(self, event):
|
|
||||||
seconds = self.slider.sliderPosition() * 60
|
|
||||||
self.window.wallet.set_session_timeout(seconds)
|
|
||||||
event.accept()
|
|
||||||
|
|
Loading…
Reference in New Issue