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.plugins = {}
|
||||||
self.gui_name = gui_name
|
self.gui_name = gui_name
|
||||||
self.descriptions = []
|
self.descriptions = []
|
||||||
|
self.device_manager = DeviceMgr()
|
||||||
|
|
||||||
for loader, name, ispkg in pkgutil.iter_modules([self.pkgpath]):
|
for loader, name, ispkg in pkgutil.iter_modules([self.pkgpath]):
|
||||||
m = loader.find_module(name).load_module(name)
|
m = loader.find_module(name).load_module(name)
|
||||||
d = m.__dict__
|
d = m.__dict__
|
||||||
|
@ -212,3 +214,199 @@ class BasePlugin(PrintError):
|
||||||
|
|
||||||
def settings_dialog(self):
|
def settings_dialog(self):
|
||||||
pass
|
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)
|
client_class = trezor_client_class(ProtocolMixin, BaseClient, proto)
|
||||||
import keepkeylib.ckd_public as ckd_public
|
import keepkeylib.ckd_public as ckd_public
|
||||||
from keepkeylib.client import types
|
from keepkeylib.client import types
|
||||||
from keepkeylib.transport_hid import HidTransport
|
from keepkeylib.transport_hid import HidTransport, DEVICE_IDS
|
||||||
libraries_available = True
|
libraries_available = True
|
||||||
except:
|
except ImportError:
|
||||||
libraries_available = False
|
libraries_available = False
|
||||||
|
|
|
@ -77,7 +77,7 @@ def trezor_client_class(protocol_mixin, base_client, proto):
|
||||||
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[0])
|
return "%s/%s/%s" % (self.label(), self.device_id(), self.path)
|
||||||
|
|
||||||
def label(self):
|
def label(self):
|
||||||
'''The name given by the user to the device.'''
|
'''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.'''
|
'''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):
|
def handler(self):
|
||||||
assert self.wallet and self.wallet.handler
|
assert self.wallet and self.wallet.handler
|
||||||
return 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)
|
path.append(abs(int(x)) | prime)
|
||||||
return path
|
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):
|
def address_from_derivation(self, derivation):
|
||||||
return self.get_address('Bitcoin', self.expand_path(derivation))
|
return self.get_address('Bitcoin', self.expand_path(derivation))
|
||||||
|
|
||||||
|
@ -128,6 +140,24 @@ def trezor_client_class(protocol_mixin, base_client, proto):
|
||||||
finally:
|
finally:
|
||||||
self.msg_code_override = None
|
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):
|
def firmware_version(self):
|
||||||
f = self.features
|
f = self.features
|
||||||
return (f.major_version, f.minor_version, f.patch_version)
|
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)
|
Transaction, x_to_xpub)
|
||||||
from electrum.wallet import BIP32_HD_Wallet, BIP44_Wallet
|
from electrum.wallet import BIP32_HD_Wallet, BIP44_Wallet
|
||||||
from electrum.util import ThreadJob
|
from electrum.util import ThreadJob
|
||||||
|
from electrum.plugins import DeviceMgr
|
||||||
|
|
||||||
class DeviceDisconnectedError(Exception):
|
class DeviceDisconnectedError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class OutdatedFirmwareError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
class TrezorCompatibleWallet(BIP44_Wallet):
|
class TrezorCompatibleWallet(BIP44_Wallet):
|
||||||
# Extend BIP44 Wallet as required by hardware implementation.
|
# Extend BIP44 Wallet as required by hardware implementation.
|
||||||
# Derived classes must set:
|
# Derived classes must set:
|
||||||
# - device
|
# - device
|
||||||
|
# - DEVICE_IDS
|
||||||
# - wallet_type
|
# - wallet_type
|
||||||
|
|
||||||
restore_wallet_class = BIP44_Wallet
|
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,
|
'''The wallet is watching-only if its trezor device is not connected,
|
||||||
or if it is connected but uninitialized.'''
|
or if it is connected but uninitialized.'''
|
||||||
assert not self.has_seed()
|
assert not self.has_seed()
|
||||||
client = self.plugin.lookup_client(self)
|
client = self.get_client(DeviceMgr.CACHED)
|
||||||
return not (client and client.is_initialized())
|
return not (client and client.is_initialized())
|
||||||
|
|
||||||
def can_change_password(self):
|
def can_change_password(self):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def client(self):
|
def get_client(self, lookup=DeviceMgr.PAIRED):
|
||||||
return self.plugin.client(self)
|
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):
|
def derive_xkeys(self, root, derivation, password):
|
||||||
if self.master_public_keys.get(root):
|
if self.master_public_keys.get(root):
|
||||||
|
@ -96,7 +107,7 @@ class TrezorCompatibleWallet(BIP44_Wallet):
|
||||||
return xpub, None
|
return xpub, None
|
||||||
|
|
||||||
def get_public_key(self, bip32_path):
|
def get_public_key(self, bip32_path):
|
||||||
client = self.client()
|
client = self.get_client()
|
||||||
address_n = client.expand_path(bip32_path)
|
address_n = client.expand_path(bip32_path)
|
||||||
node = client.get_public_node(address_n).node
|
node = client.get_public_node(address_n).node
|
||||||
xpub = ("0488B21E".decode('hex') + chr(node.depth)
|
xpub = ("0488B21E".decode('hex') + chr(node.depth)
|
||||||
|
@ -111,7 +122,7 @@ class TrezorCompatibleWallet(BIP44_Wallet):
|
||||||
raise RuntimeError(_('Decrypt method is not implemented'))
|
raise RuntimeError(_('Decrypt method is not implemented'))
|
||||||
|
|
||||||
def sign_message(self, address, message, password):
|
def sign_message(self, address, message, password):
|
||||||
client = self.client()
|
client = self.get_client()
|
||||||
address_path = self.address_id(address)
|
address_path = self.address_id(address)
|
||||||
address_n = client.expand_path(address_path)
|
address_n = client.expand_path(address_path)
|
||||||
msg_sig = client.sign_message('Bitcoin', address_n, message)
|
msg_sig = client.sign_message('Bitcoin', address_n, message)
|
||||||
|
@ -152,96 +163,89 @@ class TrezorCompatiblePlugin(BasePlugin, ThreadJob):
|
||||||
# libraries_available, libraries_URL, minimum_firmware,
|
# libraries_available, libraries_URL, minimum_firmware,
|
||||||
# wallet_class, ckd_public, types, HidTransport
|
# 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):
|
def __init__(self, parent, config, name):
|
||||||
BasePlugin.__init__(self, parent, config, name)
|
BasePlugin.__init__(self, parent, config, name)
|
||||||
self.device = self.wallet_class.device
|
self.device = self.wallet_class.device
|
||||||
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
|
||||||
# A set of client instances to USB paths
|
self.device_manager().register_devices(self, self.DEVICE_IDS)
|
||||||
self.clients = set()
|
|
||||||
# The device wallets we have seen to inform on reconnection
|
def is_enabled(self):
|
||||||
self.paired_wallets = set()
|
return self.libraries_available
|
||||||
self.last_scan = 0
|
|
||||||
|
def device_manager(self):
|
||||||
|
return self.parent.device_manager
|
||||||
|
|
||||||
def thread_jobs(self):
|
def thread_jobs(self):
|
||||||
# Scan connected devices every second. The test for libraries
|
# Thread job to handle device timeouts
|
||||||
# available is necessary to recover wallets on machines without
|
|
||||||
# libraries
|
|
||||||
return [self] if self.libraries_available else []
|
return [self] if self.libraries_available else []
|
||||||
|
|
||||||
def run(self):
|
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()
|
now = time.time()
|
||||||
if now > self.last_scan + 1:
|
for wallet in self.device_manager().paired_wallets():
|
||||||
self.last_scan = now
|
if (isinstance(wallet, self.wallet_class)
|
||||||
self.scan_devices()
|
and hasattr(wallet, 'last_operation')
|
||||||
|
and now > wallet.last_operation + wallet.session_timeout):
|
||||||
for wallet in self.paired_wallets:
|
client = self.get_client(wallet, DeviceMgr.CACHED)
|
||||||
if now > wallet.last_operation + wallet.session_timeout:
|
|
||||||
client = self.lookup_client(wallet)
|
|
||||||
if client:
|
if client:
|
||||||
wallet.last_operation = self.prevent_timeout
|
wallet.last_operation = self.prevent_timeout
|
||||||
self.clear_session(client)
|
client.clear_session()
|
||||||
wallet.timeout()
|
wallet.timeout()
|
||||||
|
|
||||||
def scan_devices(self):
|
def create_client(self, path, product_key):
|
||||||
'''Scan devices. Runs in the context of the Plugins thread.'''
|
pair = ((None, path) if self.HidTransport._detect_debuglink(path)
|
||||||
paths = self.HidTransport.enumerate()
|
else (path, None))
|
||||||
connected = set([c for c in self.clients if c.path in paths])
|
|
||||||
disconnected = self.clients - connected
|
|
||||||
|
|
||||||
self.clients = connected
|
|
||||||
|
|
||||||
# Inform clients and wallets they were disconnected
|
|
||||||
for client in disconnected:
|
|
||||||
self.print_error("device disconnected:", client)
|
|
||||||
if client.wallet:
|
|
||||||
client.wallet.disconnected()
|
|
||||||
|
|
||||||
for path in paths:
|
|
||||||
# Look for new paths
|
|
||||||
if any(c.path == path for c in connected):
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
transport = self.HidTransport(path)
|
transport = self.HidTransport(pair)
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
# We were probably just disconnected; never mind
|
# We were probably just disconnected; never mind
|
||||||
self.print_error("cannot connect at", path, str(e))
|
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:
|
try:
|
||||||
client = self.client_class(transport, path, self)
|
client.ping('t')
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
self.print_error("cannot create client for", path, str(e))
|
self.print_error("ping failed", str(e))
|
||||||
else:
|
# Remove it from the manager's cache
|
||||||
self.clients.add(client)
|
self.device_manager().close_client(client)
|
||||||
self.print_error("new device:", client)
|
client = None
|
||||||
|
|
||||||
# Inform reconnected wallets
|
if lookup == DeviceMgr.PAIRED:
|
||||||
for wallet in self.paired_wallets:
|
assert wallet.handler
|
||||||
if wallet.device_id == client.features.device_id:
|
if not client:
|
||||||
client.wallet = wallet
|
msg = (_('Could not connect to your %s. Verify the '
|
||||||
wallet.connected()
|
'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):
|
if (check_firmware and not
|
||||||
# Clearing the session forces pin re-entry
|
client.atleast_version(*self.minimum_firmware)):
|
||||||
self.print_error("clear session:", client)
|
msg = (_('Outdated %s firmware for device labelled %s. Please '
|
||||||
client.clear_session()
|
'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):
|
def initialize_device(self, wallet, wizard):
|
||||||
# Prevent timeouts during initialization
|
# Prevent timeouts during initialization
|
||||||
|
@ -254,105 +258,25 @@ class TrezorCompatiblePlugin(BasePlugin, ThreadJob):
|
||||||
strength = 64 * (strength + 2) # 128, 192 or 256
|
strength = 64 * (strength + 2) # 128, 192 or 256
|
||||||
language = ''
|
language = ''
|
||||||
|
|
||||||
client = self.client(wallet)
|
client = self.get_client(wallet)
|
||||||
client.reset_device(True, strength, passphrase_protection,
|
client.reset_device(True, strength, passphrase_protection,
|
||||||
pin_protection, label, language)
|
pin_protection, label, language)
|
||||||
|
|
||||||
|
|
||||||
def select_device(self, wallet, wizard):
|
def select_device(self, wallet, wizard):
|
||||||
'''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.'''
|
||||||
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)")]
|
suffixes = [_("An unnamed device (wiped)"), _(" (initialized)")]
|
||||||
labels = [client.label() + suffixes[client.is_initialized()]
|
labels = [client.label() + suffixes[client.is_initialized()]
|
||||||
for client in clients]
|
for client in clients]
|
||||||
msg = _("Please select which %s device to use:") % self.device
|
msg = _("Please select which %s device to use:") % self.device
|
||||||
client = clients[wizard.query_choice(msg, labels)]
|
client = clients[wizard.query_choice(msg, labels)]
|
||||||
self.pair_wallet(wallet, client)
|
self.device_manager().pair_wallet(wallet, client)
|
||||||
if not client.is_initialized():
|
if not client.is_initialized():
|
||||||
self.initialize_device(wallet, wizard)
|
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):
|
def on_restore_wallet(self, wallet, wizard):
|
||||||
assert isinstance(wallet, self.wallet_class)
|
assert isinstance(wallet, self.wallet_class)
|
||||||
|
|
||||||
|
@ -371,22 +295,10 @@ class TrezorCompatiblePlugin(BasePlugin, ThreadJob):
|
||||||
wallet.create_main_account(password)
|
wallet.create_main_account(password)
|
||||||
return wallet
|
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):
|
def sign_transaction(self, wallet, tx, prev_tx, xpub_path):
|
||||||
self.prev_tx = prev_tx
|
self.prev_tx = prev_tx
|
||||||
self.xpub_path = xpub_path
|
self.xpub_path = xpub_path
|
||||||
client = self.client(wallet)
|
client = self.get_client(wallet)
|
||||||
inputs = self.tx_inputs(tx, True)
|
inputs = self.tx_inputs(tx, True)
|
||||||
outputs = self.tx_outputs(wallet, tx)
|
outputs = self.tx_outputs(wallet, tx)
|
||||||
signed_tx = client.sign_tx('Bitcoin', inputs, outputs)[1]
|
signed_tx = client.sign_tx('Bitcoin', inputs, outputs)[1]
|
||||||
|
@ -394,7 +306,7 @@ class TrezorCompatiblePlugin(BasePlugin, ThreadJob):
|
||||||
tx.update_signatures(raw)
|
tx.update_signatures(raw)
|
||||||
|
|
||||||
def show_address(self, wallet, address):
|
def show_address(self, wallet, address):
|
||||||
client = self.client(wallet)
|
client = self.get_client(wallet)
|
||||||
if not client.atleast_version(1, 3):
|
if not client.atleast_version(1, 3):
|
||||||
wallet.handler.show_error(_("Your device firmware is too old"))
|
wallet.handler.show_error(_("Your device firmware is too old"))
|
||||||
return
|
return
|
||||||
|
|
|
@ -10,7 +10,7 @@ from electrum_gui.qt.util import *
|
||||||
from plugin import TrezorCompatiblePlugin
|
from plugin import TrezorCompatiblePlugin
|
||||||
|
|
||||||
from electrum.i18n import _
|
from electrum.i18n import _
|
||||||
from electrum.plugins import hook
|
from electrum.plugins import hook, DeviceMgr
|
||||||
from electrum.util import PrintError
|
from electrum.util import PrintError
|
||||||
from electrum.wallet import BIP44_Wallet
|
from electrum.wallet import BIP44_Wallet
|
||||||
|
|
||||||
|
@ -132,7 +132,7 @@ def qt_plugin_class(base_plugin_class):
|
||||||
window.statusBar().addPermanentWidget(window.tzb)
|
window.statusBar().addPermanentWidget(window.tzb)
|
||||||
wallet.handler = self.create_handler(window)
|
wallet.handler = self.create_handler(window)
|
||||||
# Trigger a pairing
|
# Trigger a pairing
|
||||||
self.client(wallet)
|
self.get_client(wallet)
|
||||||
|
|
||||||
def on_create_wallet(self, wallet, wizard):
|
def on_create_wallet(self, wallet, wizard):
|
||||||
assert type(wallet) == self.wallet_class
|
assert type(wallet) == self.wallet_class
|
||||||
|
@ -148,8 +148,8 @@ def qt_plugin_class(base_plugin_class):
|
||||||
|
|
||||||
def settings_dialog(self, window):
|
def settings_dialog(self, window):
|
||||||
|
|
||||||
def client():
|
def get_client(lookup=DeviceMgr.PAIRED):
|
||||||
return self.client(wallet)
|
return self.get_client(wallet, lookup)
|
||||||
|
|
||||||
def add_rows_to_layout(layout, rows):
|
def add_rows_to_layout(layout, rows):
|
||||||
for row_num, items in enumerate(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)
|
layout.addWidget(widget, row_num, col_num)
|
||||||
|
|
||||||
def refresh():
|
def refresh():
|
||||||
features = client().features
|
features = get_client(DeviceMgr.PAIRED).features
|
||||||
bl_hash = features.bootloader_hash.encode('hex').upper()
|
bl_hash = features.bootloader_hash.encode('hex').upper()
|
||||||
bl_hash = "%s...%s" % (bl_hash[:10], bl_hash[-10:])
|
bl_hash = "%s...%s" % (bl_hash[:10], bl_hash[-10:])
|
||||||
version = "%d.%d.%d" % (features.major_version,
|
version = "%d.%d.%d" % (features.major_version,
|
||||||
|
@ -184,11 +184,11 @@ def qt_plugin_class(base_plugin_class):
|
||||||
response = QInputDialog().getText(dialog, title, msg)
|
response = QInputDialog().getText(dialog, title, msg)
|
||||||
if not response[1]:
|
if not response[1]:
|
||||||
return
|
return
|
||||||
client().change_label(str(response[0]))
|
get_client().change_label(str(response[0]))
|
||||||
refresh()
|
refresh()
|
||||||
|
|
||||||
def set_pin():
|
def set_pin():
|
||||||
client().set_pin(remove=False)
|
get_client().set_pin(remove=False)
|
||||||
refresh()
|
refresh()
|
||||||
|
|
||||||
def clear_pin():
|
def clear_pin():
|
||||||
|
@ -198,10 +198,11 @@ def qt_plugin_class(base_plugin_class):
|
||||||
"Are you certain you want to remove your PIN?") % device
|
"Are you certain you want to remove your PIN?") % device
|
||||||
if not dialog.question(msg, title=title):
|
if not dialog.question(msg, title=title):
|
||||||
return
|
return
|
||||||
client().set_pin(remove=True)
|
get_client().set_pin(remove=True)
|
||||||
refresh()
|
refresh()
|
||||||
|
|
||||||
def wipe_device():
|
def wipe_device():
|
||||||
|
# FIXME: cannot yet wipe a device that is only plugged in
|
||||||
title = _("Confirm Device Wipe")
|
title = _("Confirm Device Wipe")
|
||||||
msg = _("Are you sure you want to wipe the device? "
|
msg = _("Are you sure you want to wipe the device? "
|
||||||
"You should make sure you have a copy of your recovery "
|
"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,
|
if not dialog.question(msg, title=title,
|
||||||
icon=QMessageBox.Critical):
|
icon=QMessageBox.Critical):
|
||||||
return
|
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()
|
refresh()
|
||||||
|
|
||||||
def slider_moved():
|
def slider_moved():
|
||||||
|
|
|
@ -17,7 +17,7 @@ class TrezorPlugin(TrezorCompatiblePlugin):
|
||||||
client_class = trezor_client_class(ProtocolMixin, BaseClient, proto)
|
client_class = trezor_client_class(ProtocolMixin, BaseClient, proto)
|
||||||
import trezorlib.ckd_public as ckd_public
|
import trezorlib.ckd_public as ckd_public
|
||||||
from trezorlib.client import types
|
from trezorlib.client import types
|
||||||
from trezorlib.transport_hid import HidTransport
|
from trezorlib.transport_hid import HidTransport, DEVICE_IDS
|
||||||
libraries_available = True
|
libraries_available = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
libraries_available = False
|
libraries_available = False
|
||||||
|
|
Loading…
Reference in New Issue