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