mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-31 17:31:36 +00:00
hardware devices: run all device communication on dedicated thread (#6561)
hidapi/libusb etc are not thread-safe. related: #6554
This commit is contained in:
parent
53a5a21ee8
commit
21c3572600
12 changed files with 195 additions and 97 deletions
|
@ -29,9 +29,10 @@ import time
|
|||
import threading
|
||||
import sys
|
||||
from typing import (NamedTuple, Any, Union, TYPE_CHECKING, Optional, Tuple,
|
||||
Dict, Iterable, List, Sequence)
|
||||
Dict, Iterable, List, Sequence, Callable, TypeVar)
|
||||
import concurrent
|
||||
from concurrent import futures
|
||||
from functools import wraps, partial
|
||||
|
||||
from .i18n import _
|
||||
from .util import (profiler, DaemonThread, UserCancelled, ThreadJob, UserFacingException)
|
||||
|
@ -334,11 +335,37 @@ PLACEHOLDER_HW_CLIENT_LABELS = {None, "", " "}
|
|||
# https://github.com/signal11/hidapi/pull/414#issuecomment-445164238
|
||||
# It is not entirely clear to me, exactly what is safe and what isn't, when
|
||||
# using multiple threads...
|
||||
# For now, we use a dedicated thread to enumerate devices (_hid_executor),
|
||||
# and we synchronize all device opens/closes/enumeration (_hid_lock).
|
||||
# FIXME there are still probably threading issues with how we use hidapi...
|
||||
_hid_executor = None # type: Optional[concurrent.futures.Executor]
|
||||
_hid_lock = threading.Lock()
|
||||
# Hence, we use a single thread for all device communications, including
|
||||
# enumeration. Everything that uses hidapi, libusb, etc, MUST run on
|
||||
# the following thread:
|
||||
_hwd_comms_executor = concurrent.futures.ThreadPoolExecutor(
|
||||
max_workers=1,
|
||||
thread_name_prefix='hwd_comms_thread'
|
||||
)
|
||||
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
def run_in_hwd_thread(func: Callable[[], T]) -> T:
|
||||
if threading.current_thread().name.startswith("hwd_comms_thread"):
|
||||
return func()
|
||||
else:
|
||||
fut = _hwd_comms_executor.submit(func)
|
||||
return fut.result()
|
||||
#except (concurrent.futures.CancelledError, concurrent.futures.TimeoutError) as e:
|
||||
|
||||
|
||||
def runs_in_hwd_thread(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
return run_in_hwd_thread(partial(func, *args, **kwargs))
|
||||
return wrapper
|
||||
|
||||
|
||||
def assert_runs_in_hwd_thread():
|
||||
if not threading.current_thread().name.startswith("hwd_comms_thread"):
|
||||
raise Exception("must only be called from HWD communication thread")
|
||||
|
||||
|
||||
class DeviceMgr(ThreadJob):
|
||||
|
@ -384,24 +411,11 @@ class DeviceMgr(ThreadJob):
|
|||
self._recognised_hardware = {} # type: Dict[Tuple[int, int], HW_PluginBase]
|
||||
# Custom enumerate functions for devices we don't know about.
|
||||
self._enumerate_func = set() # Needs self.lock.
|
||||
# locks: if you need to take multiple ones, acquire them in the order they are defined here!
|
||||
self._scan_lock = threading.RLock()
|
||||
|
||||
self.lock = threading.RLock()
|
||||
self.hid_lock = _hid_lock
|
||||
|
||||
self.config = config
|
||||
|
||||
global _hid_executor
|
||||
if _hid_executor is None:
|
||||
_hid_executor = concurrent.futures.ThreadPoolExecutor(max_workers=1,
|
||||
thread_name_prefix='hid_enumerate_thread')
|
||||
|
||||
def with_scan_lock(func):
|
||||
def func_wrapper(self: 'DeviceMgr', *args, **kwargs):
|
||||
with self._scan_lock:
|
||||
return func(self, *args, **kwargs)
|
||||
return func_wrapper
|
||||
|
||||
def thread_jobs(self):
|
||||
# Thread job to handle device timeouts
|
||||
return [self]
|
||||
|
@ -423,6 +437,7 @@ class DeviceMgr(ThreadJob):
|
|||
with self.lock:
|
||||
self._enumerate_func.add(func)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def create_client(self, device: 'Device', handler: Optional['HardwareHandlerBase'],
|
||||
plugin: 'HW_PluginBase') -> Optional['HardwareClientBase']:
|
||||
# Get from cache first
|
||||
|
@ -452,7 +467,7 @@ class DeviceMgr(ThreadJob):
|
|||
if xpub not in self.xpub_ids:
|
||||
return
|
||||
_id = self.xpub_ids.pop(xpub)
|
||||
self._close_client(_id)
|
||||
self._close_client(_id)
|
||||
|
||||
def unpair_id(self, id_):
|
||||
xpub = self.xpub_by_id(id_)
|
||||
|
@ -462,8 +477,9 @@ class DeviceMgr(ThreadJob):
|
|||
self._close_client(id_)
|
||||
|
||||
def _close_client(self, id_):
|
||||
client = self._client_by_id(id_)
|
||||
self.clients.pop(client, None)
|
||||
with self.lock:
|
||||
client = self._client_by_id(id_)
|
||||
self.clients.pop(client, None)
|
||||
if client:
|
||||
client.close()
|
||||
|
||||
|
@ -486,7 +502,7 @@ class DeviceMgr(ThreadJob):
|
|||
self.scan_devices()
|
||||
return self._client_by_id(id_)
|
||||
|
||||
@with_scan_lock
|
||||
@runs_in_hwd_thread
|
||||
def client_for_keystore(self, plugin: 'HW_PluginBase', handler: Optional['HardwareHandlerBase'],
|
||||
keystore: 'Hardware_KeyStore',
|
||||
force_pair: bool, *,
|
||||
|
@ -655,25 +671,15 @@ class DeviceMgr(ThreadJob):
|
|||
# note: updated label/soft_device_id will be saved after pairing succeeds
|
||||
return info
|
||||
|
||||
@with_scan_lock
|
||||
@runs_in_hwd_thread
|
||||
def _scan_devices_with_hid(self) -> List['Device']:
|
||||
try:
|
||||
import hid
|
||||
except ImportError:
|
||||
return []
|
||||
|
||||
def hid_enumerate():
|
||||
with self.hid_lock:
|
||||
return hid.enumerate(0, 0)
|
||||
|
||||
hid_list_fut = _hid_executor.submit(hid_enumerate)
|
||||
try:
|
||||
hid_list = hid_list_fut.result()
|
||||
except (concurrent.futures.CancelledError, concurrent.futures.TimeoutError) as e:
|
||||
return []
|
||||
|
||||
devices = []
|
||||
for d in hid_list:
|
||||
for d in hid.enumerate(0, 0):
|
||||
product_key = (d['vendor_id'], d['product_id'])
|
||||
if product_key in self._recognised_hardware:
|
||||
plugin = self._recognised_hardware[product_key]
|
||||
|
@ -681,7 +687,7 @@ class DeviceMgr(ThreadJob):
|
|||
devices.append(device)
|
||||
return devices
|
||||
|
||||
@with_scan_lock
|
||||
@runs_in_hwd_thread
|
||||
@profiler
|
||||
def scan_devices(self) -> Sequence['Device']:
|
||||
self.logger.info("scanning devices...")
|
||||
|
@ -693,10 +699,8 @@ class DeviceMgr(ThreadJob):
|
|||
with self.lock:
|
||||
enumerate_funcs = list(self._enumerate_func)
|
||||
for f in enumerate_funcs:
|
||||
# custom enumerate functions might use hidapi, so use hid thread to be safe
|
||||
new_devices_fut = _hid_executor.submit(f)
|
||||
try:
|
||||
new_devices = new_devices_fut.result()
|
||||
new_devices = f()
|
||||
except BaseException as e:
|
||||
self.logger.error('custom device enum failed. func {}, error {}'
|
||||
.format(str(f), repr(e)))
|
||||
|
|
|
@ -13,7 +13,7 @@ from electrum.wallet import Standard_Wallet, Multisig_Wallet, Deterministic_Wall
|
|||
from electrum.util import bh2u, UserFacingException
|
||||
from electrum.base_wizard import ScriptTypeNotSupported, BaseWizard
|
||||
from electrum.logging import get_logger
|
||||
from electrum.plugin import Device, DeviceInfo
|
||||
from electrum.plugin import Device, DeviceInfo, runs_in_hwd_thread
|
||||
from electrum.simple_config import SimpleConfig
|
||||
from electrum.json_db import StoredDict
|
||||
from electrum.storage import get_derivation_used_for_hw_device_encryption
|
||||
|
@ -73,18 +73,19 @@ class BitBox02Client(HardwareClientBase):
|
|||
def is_initialized(self) -> bool:
|
||||
return True
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def close(self):
|
||||
with self.device_manager().hid_lock:
|
||||
try:
|
||||
self.bitbox02_device.close()
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
self.bitbox02_device.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
def has_usable_connection_with_device(self) -> bool:
|
||||
if self.bitbox_hid_info is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def get_soft_device_id(self) -> Optional[str]:
|
||||
if self.handler is None:
|
||||
# Can't do the pairing without the handler. This happens at wallet creation time, when
|
||||
|
@ -94,6 +95,7 @@ class BitBox02Client(HardwareClientBase):
|
|||
self.pairing_dialog()
|
||||
return self.bitbox02_device.root_fingerprint().hex()
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def pairing_dialog(self):
|
||||
def pairing_step(code: str, device_response: Callable[[], bool]) -> bool:
|
||||
msg = "Please compare and confirm the pairing code on your BitBox02:\n" + code
|
||||
|
@ -102,8 +104,7 @@ class BitBox02Client(HardwareClientBase):
|
|||
res = device_response()
|
||||
except:
|
||||
# Close the hid device on exception
|
||||
with self.device_manager().hid_lock:
|
||||
hid_device.close()
|
||||
hid_device.close()
|
||||
raise
|
||||
finally:
|
||||
self.handler.finished()
|
||||
|
@ -167,10 +168,8 @@ class BitBox02Client(HardwareClientBase):
|
|||
return set_noise_privkey(privkey)
|
||||
|
||||
if self.bitbox02_device is None:
|
||||
with self.device_manager().hid_lock:
|
||||
hid_device = hid.device()
|
||||
hid_device.open_path(self.bitbox_hid_info["path"])
|
||||
|
||||
hid_device = hid.device()
|
||||
hid_device.open_path(self.bitbox_hid_info["path"])
|
||||
|
||||
bitbox02_device = bitbox02.BitBox02(
|
||||
transport=u2fhid.U2FHid(hid_device),
|
||||
|
@ -197,6 +196,7 @@ class BitBox02Client(HardwareClientBase):
|
|||
return bitbox02.btc.TBTC
|
||||
return bitbox02.btc.BTC
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def get_password_for_storage_encryption(self) -> str:
|
||||
derivation = get_derivation_used_for_hw_device_encryption()
|
||||
derivation_list = bip32.convert_bip32_path_to_list_of_uint32(derivation)
|
||||
|
@ -204,6 +204,7 @@ class BitBox02Client(HardwareClientBase):
|
|||
node = bip32.BIP32Node.from_xkey(xpub, net = constants.BitcoinMainnet()).subkey_at_public_derivation(())
|
||||
return node.eckey.get_public_key_bytes(compressed=True).hex()
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def get_xpub(self, bip32_path: str, xtype: str, *, display: bool = False) -> str:
|
||||
if self.bitbox02_device is None:
|
||||
self.pairing_dialog()
|
||||
|
@ -244,6 +245,7 @@ class BitBox02Client(HardwareClientBase):
|
|||
display=display,
|
||||
)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def label(self) -> str:
|
||||
if self.handler is None:
|
||||
# Can't do the pairing without the handler. This happens at wallet creation time, when
|
||||
|
@ -258,6 +260,7 @@ class BitBox02Client(HardwareClientBase):
|
|||
self.bitbox02_device.root_fingerprint().hex(),
|
||||
)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def request_root_fingerprint_from_device(self) -> str:
|
||||
if self.bitbox02_device is None:
|
||||
raise Exception(
|
||||
|
@ -271,6 +274,7 @@ class BitBox02Client(HardwareClientBase):
|
|||
return False
|
||||
return True
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def btc_multisig_config(
|
||||
self, coin, bip32_path: List[int], wallet: Multisig_Wallet
|
||||
):
|
||||
|
@ -316,6 +320,7 @@ class BitBox02Client(HardwareClientBase):
|
|||
raise UserFacingException("Failed to register multisig\naccount configuration on BitBox02")
|
||||
return multisig_config
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def show_address(
|
||||
self, bip32_path: str, address_type: str, wallet: Deterministic_Wallet
|
||||
) -> str:
|
||||
|
@ -357,6 +362,7 @@ class BitBox02Client(HardwareClientBase):
|
|||
display=True,
|
||||
)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def sign_transaction(
|
||||
self,
|
||||
keystore: Hardware_KeyStore,
|
||||
|
@ -553,6 +559,7 @@ class BitBox02_KeyStore(Hardware_KeyStore):
|
|||
).format(self.device)
|
||||
)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def sign_transaction(self, tx: PartialTransaction, password: str):
|
||||
if tx.is_complete():
|
||||
return
|
||||
|
@ -572,6 +579,7 @@ class BitBox02_KeyStore(Hardware_KeyStore):
|
|||
self.give_error(e, True)
|
||||
return
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def show_address(
|
||||
self, sequence: Tuple[int, int], txin_type: str, wallet: Deterministic_Wallet
|
||||
):
|
||||
|
@ -616,6 +624,7 @@ class BitBox02Plugin(HW_PluginBase):
|
|||
raise ImportError()
|
||||
|
||||
# handler is a BitBox02_Handler
|
||||
@runs_in_hwd_thread
|
||||
def create_client(self, device: Device, handler: Any) -> BitBox02Client:
|
||||
if not handler:
|
||||
self.handler = handler
|
||||
|
@ -645,6 +654,7 @@ class BitBox02Plugin(HW_PluginBase):
|
|||
assert client.bitbox02_device is not None
|
||||
return client.get_xpub(derivation, xtype)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def show_address(
|
||||
self,
|
||||
wallet: Deterministic_Wallet,
|
||||
|
@ -660,6 +670,7 @@ class BitBox02Plugin(HW_PluginBase):
|
|||
sequence = wallet.get_address_index(address)
|
||||
keystore.show_address(sequence, txin_type, wallet)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def show_xpub(self, keystore: BitBox02_KeyStore):
|
||||
client = keystore.get_client()
|
||||
assert isinstance(client, BitBox02Client)
|
||||
|
|
|
@ -10,7 +10,7 @@ import struct
|
|||
from electrum import bip32
|
||||
from electrum.bip32 import BIP32Node, InvalidMasterKeyVersionBytes
|
||||
from electrum.i18n import _
|
||||
from electrum.plugin import Device, hook
|
||||
from electrum.plugin import Device, hook, runs_in_hwd_thread
|
||||
from electrum.keystore import Hardware_KeyStore, KeyStoreWithMPK
|
||||
from electrum.transaction import PartialTransaction
|
||||
from electrum.wallet import Standard_Wallet, Multisig_Wallet, Abstract_Wallet
|
||||
|
@ -72,9 +72,8 @@ class CKCCClient(HardwareClientBase):
|
|||
self.dev = ElectrumColdcardDevice(dev_path, encrypt=True)
|
||||
else:
|
||||
# open the real HID device
|
||||
with self.device_manager().hid_lock:
|
||||
hd = hid.device(path=dev_path)
|
||||
hd.open_path(dev_path)
|
||||
hd = hid.device(path=dev_path)
|
||||
hd.open_path(dev_path)
|
||||
|
||||
self.dev = ElectrumColdcardDevice(dev=hd, encrypt=True)
|
||||
|
||||
|
@ -85,6 +84,7 @@ class CKCCClient(HardwareClientBase):
|
|||
return '<CKCCClient: xfp=%s label=%r>' % (xfp2str(self.dev.master_fingerprint),
|
||||
self.label())
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def verify_connection(self, expected_xfp: int, expected_xpub=None):
|
||||
ex = (expected_xfp, expected_xpub)
|
||||
|
||||
|
@ -121,14 +121,10 @@ class CKCCClient(HardwareClientBase):
|
|||
# can't do anything w/ devices that aren't setup (this code not normally reachable)
|
||||
return bool(self.dev.master_xpub)
|
||||
|
||||
def timeout(self, cutoff):
|
||||
# nothing to do?
|
||||
pass
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def close(self):
|
||||
# close the HID device (so can be reused)
|
||||
with self.device_manager().hid_lock:
|
||||
self.dev.close()
|
||||
self.dev.close()
|
||||
self.dev = None
|
||||
|
||||
def is_initialized(self):
|
||||
|
@ -160,6 +156,7 @@ class CKCCClient(HardwareClientBase):
|
|||
|
||||
return LabelStr(lab, self.dev.master_fingerprint, self.dev.master_xpub)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def has_usable_connection_with_device(self):
|
||||
# Do end-to-end ping test
|
||||
try:
|
||||
|
@ -168,6 +165,7 @@ class CKCCClient(HardwareClientBase):
|
|||
except:
|
||||
return False
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def get_xpub(self, bip32_path, xtype):
|
||||
assert xtype in ColdcardPlugin.SUPPORTED_XTYPES
|
||||
_logger.info('Derive xtype = %r' % xtype)
|
||||
|
@ -183,6 +181,7 @@ class CKCCClient(HardwareClientBase):
|
|||
xpub = node._replace(xtype=xtype).to_xpub()
|
||||
return xpub
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def ping_check(self):
|
||||
# check connection is working
|
||||
assert self.dev.session_key, 'not encrypted?'
|
||||
|
@ -193,26 +192,32 @@ class CKCCClient(HardwareClientBase):
|
|||
except:
|
||||
raise RuntimeError("Communication trouble with Coldcard")
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def show_address(self, path, addr_fmt):
|
||||
# prompt user w/ address, also returns it immediately.
|
||||
return self.dev.send_recv(CCProtocolPacker.show_address(path, addr_fmt), timeout=None)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def show_p2sh_address(self, *args, **kws):
|
||||
# prompt user w/ p2sh address, also returns it immediately.
|
||||
return self.dev.send_recv(CCProtocolPacker.show_p2sh_address(*args, **kws), timeout=None)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def get_version(self):
|
||||
# gives list of strings
|
||||
return self.dev.send_recv(CCProtocolPacker.version(), timeout=1000).split('\n')
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def sign_message_start(self, path, msg):
|
||||
# this starts the UX experience.
|
||||
self.dev.send_recv(CCProtocolPacker.sign_message(msg, path), timeout=None)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def sign_message_poll(self):
|
||||
# poll device... if user has approved, will get tuple: (addr, sig) else None
|
||||
return self.dev.send_recv(CCProtocolPacker.get_signed_msg(), timeout=None)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def sign_transaction_start(self, raw_psbt: bytes, *, finalize: bool = False):
|
||||
# Multiple steps to sign:
|
||||
# - upload binary
|
||||
|
@ -228,10 +233,12 @@ class CKCCClient(HardwareClientBase):
|
|||
if resp != None:
|
||||
raise ValueError(resp)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def sign_transaction_poll(self):
|
||||
# poll device... if user has approved, will get tuple: (legnth, checksum) else None
|
||||
return self.dev.send_recv(CCProtocolPacker.get_signed_txn(), timeout=None)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def download_file(self, length, checksum, file_number=1):
|
||||
# get a file
|
||||
return self.dev.download_file(length, checksum, file_number=file_number)
|
||||
|
@ -317,7 +324,6 @@ class Coldcard_KeyStore(Hardware_KeyStore):
|
|||
% MSG_SIGNING_MAX_LENGTH)
|
||||
return b''
|
||||
|
||||
client = self.get_client()
|
||||
path = self.get_derivation_prefix() + ("/%d/%d" % sequence)
|
||||
try:
|
||||
cl = self.get_client()
|
||||
|
@ -508,6 +514,7 @@ class ColdcardPlugin(HW_PluginBase):
|
|||
|
||||
return []
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def create_client(self, device, handler):
|
||||
if handler:
|
||||
self.handler = handler
|
||||
|
@ -539,6 +546,7 @@ class ColdcardPlugin(HW_PluginBase):
|
|||
xpub = client.get_xpub(derivation, xtype)
|
||||
return xpub
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def get_client(self, keystore, force_pair=True, *,
|
||||
devices=None, allow_user_interaction=True) -> Optional['CKCCClient']:
|
||||
# Acquire a connection to the hardware device (via USB)
|
||||
|
|
|
@ -30,6 +30,7 @@ from electrum.util import to_string, UserCancelled, UserFacingException, bfh
|
|||
from electrum.base_wizard import ScriptTypeNotSupported, HWD_SETUP_NEW_WALLET
|
||||
from electrum.network import Network
|
||||
from electrum.logging import get_logger
|
||||
from electrum.plugin import runs_in_hwd_thread, run_in_hwd_thread
|
||||
|
||||
from ..hw_wallet import HW_PluginBase, HardwareClientBase
|
||||
|
||||
|
@ -74,21 +75,16 @@ class DigitalBitbox_Client(HardwareClientBase):
|
|||
self.setupRunning = False
|
||||
self.usbReportSize = 64 # firmware > v2.0.0
|
||||
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def close(self):
|
||||
if self.opened:
|
||||
with self.device_manager().hid_lock:
|
||||
try:
|
||||
self.dbb_hid.close()
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
self.dbb_hid.close()
|
||||
except:
|
||||
pass
|
||||
self.opened = False
|
||||
|
||||
|
||||
def timeout(self, cutoff):
|
||||
pass
|
||||
|
||||
|
||||
def is_pairable(self):
|
||||
return True
|
||||
|
||||
|
@ -111,7 +107,6 @@ class DigitalBitbox_Client(HardwareClientBase):
|
|||
if self.check_device_dialog():
|
||||
return self.hid_send_encrypt(('{"xpub": "%s"}' % bip32_path).encode('utf8'))
|
||||
|
||||
|
||||
def get_xpub(self, bip32_path, xtype):
|
||||
assert xtype in self.plugin.SUPPORTED_XTYPES
|
||||
reply = self._get_xpub(bip32_path)
|
||||
|
@ -171,9 +166,9 @@ class DigitalBitbox_Client(HardwareClientBase):
|
|||
self.password = password.encode('utf8')
|
||||
return True
|
||||
|
||||
|
||||
def check_device_dialog(self):
|
||||
match = re.search(r'v([0-9])+\.[0-9]+\.[0-9]+', self.dbb_hid.get_serial_number_string())
|
||||
match = re.search(r'v([0-9])+\.[0-9]+\.[0-9]+',
|
||||
run_in_hwd_thread(self.dbb_hid.get_serial_number_string))
|
||||
if match is None:
|
||||
raise Exception("error detecting firmware version")
|
||||
major_version = int(match.group(1))
|
||||
|
@ -350,7 +345,7 @@ class DigitalBitbox_Client(HardwareClientBase):
|
|||
raise UserFacingException(hid_reply['error']['message'])
|
||||
return True
|
||||
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def hid_send_frame(self, data):
|
||||
HWW_CID = 0xFF000000
|
||||
HWW_CMD = 0x80 + 0x40 + 0x01
|
||||
|
@ -370,7 +365,7 @@ class DigitalBitbox_Client(HardwareClientBase):
|
|||
seq += 1
|
||||
idx += len(write)
|
||||
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def hid_read_frame(self):
|
||||
# INIT response
|
||||
read = bytearray(self.dbb_hid.read(self.usbReportSize))
|
||||
|
@ -386,7 +381,7 @@ class DigitalBitbox_Client(HardwareClientBase):
|
|||
idx += len(read) - 5
|
||||
return data
|
||||
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def hid_send_plain(self, msg):
|
||||
reply = ""
|
||||
try:
|
||||
|
@ -408,7 +403,7 @@ class DigitalBitbox_Client(HardwareClientBase):
|
|||
_logger.info(f'Exception caught {repr(e)}')
|
||||
return reply
|
||||
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def hid_send_encrypt(self, msg):
|
||||
sha256_byte_len = 32
|
||||
reply = ""
|
||||
|
@ -680,11 +675,10 @@ class DigitalBitboxPlugin(HW_PluginBase):
|
|||
|
||||
self.digitalbitbox_config = self.config.get('digitalbitbox', {})
|
||||
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def get_dbb_device(self, device):
|
||||
with self.device_manager().hid_lock:
|
||||
dev = hid.device()
|
||||
dev.open_path(device.path)
|
||||
dev = hid.device()
|
||||
dev.open_path(device.path)
|
||||
return dev
|
||||
|
||||
|
||||
|
|
|
@ -27,7 +27,8 @@
|
|||
from typing import TYPE_CHECKING, Dict, List, Union, Tuple, Sequence, Optional, Type
|
||||
from functools import partial
|
||||
|
||||
from electrum.plugin import BasePlugin, hook, Device, DeviceMgr, DeviceInfo
|
||||
from electrum.plugin import (BasePlugin, hook, Device, DeviceMgr, DeviceInfo,
|
||||
assert_runs_in_hwd_thread, runs_in_hwd_thread)
|
||||
from electrum.i18n import _
|
||||
from electrum.bitcoin import is_address, opcodes
|
||||
from electrum.util import bfh, versiontuple, UserFacingException
|
||||
|
@ -197,6 +198,7 @@ class HardwareClientBase:
|
|||
handler = None # type: Optional['HardwareHandlerBase']
|
||||
|
||||
def __init__(self, *, plugin: 'HW_PluginBase'):
|
||||
assert_runs_in_hwd_thread()
|
||||
self.plugin = plugin
|
||||
|
||||
def device_manager(self) -> 'DeviceMgr':
|
||||
|
@ -242,6 +244,7 @@ class HardwareClientBase:
|
|||
def get_xpub(self, bip32_path: str, xtype) -> str:
|
||||
raise NotImplementedError()
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def request_root_fingerprint_from_device(self) -> str:
|
||||
# digitalbitbox (at least) does not reveal xpubs corresponding to unhardened paths
|
||||
# so ask for a direct child, and read out fingerprint from that:
|
||||
|
@ -249,6 +252,7 @@ class HardwareClientBase:
|
|||
root_fingerprint = BIP32Node.from_xkey(child_of_root_xpub).fingerprint.hex().lower()
|
||||
return root_fingerprint
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def get_password_for_storage_encryption(self) -> str:
|
||||
# note: using a different password based on hw device type is highly undesirable! see #5993
|
||||
derivation = get_derivation_used_for_hw_device_encryption()
|
||||
|
|
|
@ -8,6 +8,7 @@ from electrum.util import UserCancelled
|
|||
from electrum.keystore import bip39_normalize_passphrase
|
||||
from electrum.bip32 import BIP32Node, convert_bip32_path_to_list_of_uint32
|
||||
from electrum.logging import Logger
|
||||
from electrum.plugin import runs_in_hwd_thread
|
||||
from electrum.plugins.hw_wallet.plugin import HardwareClientBase, HardwareHandlerBase
|
||||
|
||||
|
||||
|
@ -129,6 +130,7 @@ class KeepKeyClientBase(HardwareClientBase, GuiMixin, Logger):
|
|||
def is_pairable(self):
|
||||
return not self.features.bootloader_mode
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def has_usable_connection_with_device(self):
|
||||
try:
|
||||
res = self.ping("electrum pinging device")
|
||||
|
@ -143,6 +145,7 @@ class KeepKeyClientBase(HardwareClientBase, GuiMixin, Logger):
|
|||
def prevent_timeouts(self):
|
||||
self.last_operation = float('inf')
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def timeout(self, cutoff):
|
||||
'''Time out the client if the last operation was before cutoff.'''
|
||||
if self.last_operation < cutoff:
|
||||
|
@ -153,6 +156,7 @@ class KeepKeyClientBase(HardwareClientBase, GuiMixin, Logger):
|
|||
def expand_path(n):
|
||||
return convert_bip32_path_to_list_of_uint32(n)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def cancel(self):
|
||||
'''Provided here as in keepkeylib but not trezorlib.'''
|
||||
self.transport.write(self.proto.Cancel())
|
||||
|
@ -160,6 +164,7 @@ class KeepKeyClientBase(HardwareClientBase, GuiMixin, Logger):
|
|||
def i4b(self, x):
|
||||
return pack('>I', x)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def get_xpub(self, bip32_path, xtype):
|
||||
address_n = self.expand_path(bip32_path)
|
||||
creating = False
|
||||
|
@ -171,6 +176,7 @@ class KeepKeyClientBase(HardwareClientBase, GuiMixin, Logger):
|
|||
fingerprint=self.i4b(node.fingerprint),
|
||||
child_number=self.i4b(node.child_num)).to_xpub()
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def toggle_passphrase(self):
|
||||
if self.features.passphrase_protection:
|
||||
self.msg = _("Confirm on your {} device to disable passphrases")
|
||||
|
@ -179,14 +185,17 @@ class KeepKeyClientBase(HardwareClientBase, GuiMixin, Logger):
|
|||
enabled = not self.features.passphrase_protection
|
||||
self.apply_settings(use_passphrase=enabled)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def change_label(self, label):
|
||||
self.msg = _("Confirm the new label on your {} device")
|
||||
self.apply_settings(label=label)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def change_homescreen(self, homescreen):
|
||||
self.msg = _("Confirm on your {} device to change your home screen")
|
||||
self.apply_settings(homescreen=homescreen)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def set_pin(self, remove):
|
||||
if remove:
|
||||
self.msg = _("Confirm on your {} device to disable PIN protection")
|
||||
|
@ -196,6 +205,7 @@ class KeepKeyClientBase(HardwareClientBase, GuiMixin, Logger):
|
|||
self.msg = _("Confirm on your {} device to set a PIN")
|
||||
self.change_pin(remove)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def clear_session(self):
|
||||
'''Clear the session to force pin (and passphrase if enabled)
|
||||
re-entry. Does not leak exceptions.'''
|
||||
|
@ -207,10 +217,12 @@ class KeepKeyClientBase(HardwareClientBase, GuiMixin, Logger):
|
|||
# If the device was removed it has the same effect...
|
||||
self.logger.info(f"clear_session: ignoring error {e}")
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def get_public_node(self, address_n, creating):
|
||||
self.creating_wallet = creating
|
||||
return super(KeepKeyClientBase, self).get_public_node(address_n)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def close(self):
|
||||
'''Called when Our wallet was closed or the device removed.'''
|
||||
self.logger.info("closing client")
|
||||
|
|
|
@ -9,7 +9,7 @@ from electrum import constants
|
|||
from electrum.i18n import _
|
||||
from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput
|
||||
from electrum.keystore import Hardware_KeyStore
|
||||
from electrum.plugin import Device
|
||||
from electrum.plugin import Device, runs_in_hwd_thread
|
||||
from electrum.base_wizard import ScriptTypeNotSupported
|
||||
|
||||
from ..hw_wallet import HW_PluginBase
|
||||
|
@ -37,6 +37,7 @@ class KeepKey_KeyStore(Hardware_KeyStore):
|
|||
def decrypt_message(self, sequence, message, password):
|
||||
raise UserFacingException(_('Encryption and decryption are not implemented by {}').format(self.device))
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def sign_message(self, sequence, message, password):
|
||||
client = self.get_client()
|
||||
address_path = self.get_derivation_prefix() + "/%d/%d"%sequence
|
||||
|
@ -44,6 +45,7 @@ class KeepKey_KeyStore(Hardware_KeyStore):
|
|||
msg_sig = client.sign_message(self.plugin.get_coin_name(), address_n, message)
|
||||
return msg_sig.signature
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def sign_transaction(self, tx, password):
|
||||
if tx.is_complete():
|
||||
return
|
||||
|
@ -95,6 +97,7 @@ class KeepKeyPlugin(HW_PluginBase):
|
|||
except ImportError:
|
||||
self.libraries_available = False
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def enumerate(self):
|
||||
from keepkeylib.transport_webusb import WebUsbTransport
|
||||
results = []
|
||||
|
@ -112,16 +115,19 @@ class KeepKeyPlugin(HW_PluginBase):
|
|||
def _dev_to_str(dev: "usb1.USBDevice") -> str:
|
||||
return ":".join(str(x) for x in ["%03i" % (dev.getBusNumber(),)] + dev.getPortNumberList())
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def hid_transport(self, pair):
|
||||
from keepkeylib.transport_hid import HidTransport
|
||||
return HidTransport(pair)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def webusb_transport(self, device):
|
||||
from keepkeylib.transport_webusb import WebUsbTransport
|
||||
for dev in WebUsbTransport.enumerate():
|
||||
if device.path == self._dev_to_str(dev):
|
||||
return WebUsbTransport(dev)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def _try_hid(self, device):
|
||||
self.logger.info("Trying to connect over USB...")
|
||||
if device.interface_number == 1:
|
||||
|
@ -137,6 +143,7 @@ class KeepKeyPlugin(HW_PluginBase):
|
|||
self.logger.info(f"cannot connect at {device.path} {e}")
|
||||
return None
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def _try_webusb(self, device):
|
||||
self.logger.info("Trying to connect over WebUSB...")
|
||||
try:
|
||||
|
@ -145,6 +152,7 @@ class KeepKeyPlugin(HW_PluginBase):
|
|||
self.logger.info(f"cannot connect at {device.path} {e}")
|
||||
return None
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def create_client(self, device, handler):
|
||||
if device.product_key[1] == 2:
|
||||
transport = self._try_webusb(device)
|
||||
|
@ -179,6 +187,7 @@ class KeepKeyPlugin(HW_PluginBase):
|
|||
|
||||
return client
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def get_client(self, keystore, force_pair=True, *,
|
||||
devices=None, allow_user_interaction=True) -> Optional['KeepKeyClient']:
|
||||
client = super().get_client(keystore, force_pair,
|
||||
|
@ -236,6 +245,7 @@ class KeepKeyPlugin(HW_PluginBase):
|
|||
finally:
|
||||
wizard.loop.exit(exit_code)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def _initialize_device(self, settings, method, device_id, wizard, handler):
|
||||
item, label, pin_protection, passphrase_protection = settings
|
||||
|
||||
|
@ -315,6 +325,7 @@ class KeepKeyPlugin(HW_PluginBase):
|
|||
return self.types.PAYTOMULTISIG
|
||||
raise ValueError('unexpected txin type: {}'.format(electrum_txin_type))
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def sign_transaction(self, keystore, tx: PartialTransaction, prev_tx):
|
||||
self.prev_tx = prev_tx
|
||||
client = self.get_client(keystore)
|
||||
|
@ -325,6 +336,7 @@ class KeepKeyPlugin(HW_PluginBase):
|
|||
signatures = [(bh2u(x) + '01') for x in signatures]
|
||||
tx.update_signatures(signatures)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def show_address(self, wallet, address, keystore=None):
|
||||
if keystore is None:
|
||||
keystore = wallet.get_keystore()
|
||||
|
|
|
@ -16,6 +16,7 @@ from electrum.wallet import Standard_Wallet
|
|||
from electrum.util import bfh, bh2u, versiontuple, UserFacingException
|
||||
from electrum.base_wizard import ScriptTypeNotSupported
|
||||
from electrum.logging import get_logger
|
||||
from electrum.plugin import runs_in_hwd_thread
|
||||
|
||||
from ..hw_wallet import HW_PluginBase, HardwareClientBase
|
||||
from ..hw_wallet.plugin import is_any_tx_output_on_change_branch, validate_op_return_output, LibraryFoundButUnusable
|
||||
|
@ -74,16 +75,14 @@ class Ledger_Client(HardwareClientBase):
|
|||
def is_pairable(self):
|
||||
return True
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def close(self):
|
||||
with self.device_manager().hid_lock:
|
||||
self.dongleObject.dongle.close()
|
||||
|
||||
def timeout(self, cutoff):
|
||||
pass
|
||||
self.dongleObject.dongle.close()
|
||||
|
||||
def is_initialized(self):
|
||||
return True
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def get_soft_device_id(self):
|
||||
if self._soft_device_id is None:
|
||||
# modern ledger can provide xpub without user interaction
|
||||
|
@ -106,6 +105,7 @@ class Ledger_Client(HardwareClientBase):
|
|||
return "Ledger Nano X"
|
||||
return None
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def has_usable_connection_with_device(self):
|
||||
try:
|
||||
self.dongleObject.getFirmwareVersion()
|
||||
|
@ -113,6 +113,7 @@ class Ledger_Client(HardwareClientBase):
|
|||
return False
|
||||
return True
|
||||
|
||||
@runs_in_hwd_thread
|
||||
@test_pin_unlocked
|
||||
def get_xpub(self, bip32_path, xtype):
|
||||
self.checkDevice()
|
||||
|
@ -180,6 +181,7 @@ class Ledger_Client(HardwareClientBase):
|
|||
def supports_segwit_trustedInputs(self):
|
||||
return self.segwitTrustedInputs
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def perform_hw1_preflight(self):
|
||||
try:
|
||||
firmwareInfo = self.dongleObject.getFirmwareVersion()
|
||||
|
@ -224,6 +226,7 @@ class Ledger_Client(HardwareClientBase):
|
|||
"Please make sure that 'Browser support' is disabled on your device.")
|
||||
raise e
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def checkDevice(self):
|
||||
if not self.preflightDone:
|
||||
try:
|
||||
|
@ -290,6 +293,7 @@ class Ledger_KeyStore(Hardware_KeyStore):
|
|||
def decrypt_message(self, pubkey, message, password):
|
||||
raise UserFacingException(_('Encryption and decryption are currently not supported for {}').format(self.device))
|
||||
|
||||
@runs_in_hwd_thread
|
||||
@test_pin_unlocked
|
||||
@set_and_unset_signing
|
||||
def sign_message(self, sequence, message, password):
|
||||
|
@ -336,6 +340,7 @@ class Ledger_KeyStore(Hardware_KeyStore):
|
|||
# And convert it
|
||||
return bytes([27 + 4 + (signature[0] & 0x01)]) + r + s
|
||||
|
||||
@runs_in_hwd_thread
|
||||
@test_pin_unlocked
|
||||
@set_and_unset_signing
|
||||
def sign_transaction(self, tx, password):
|
||||
|
@ -536,6 +541,7 @@ class Ledger_KeyStore(Hardware_KeyStore):
|
|||
finally:
|
||||
self.handler.finished()
|
||||
|
||||
@runs_in_hwd_thread
|
||||
@test_pin_unlocked
|
||||
@set_and_unset_signing
|
||||
def show_address(self, sequence, txin_type):
|
||||
|
@ -607,6 +613,7 @@ class LedgerPlugin(HW_PluginBase):
|
|||
else:
|
||||
raise LibraryFoundButUnusable(library_version=version)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def get_btchip_device(self, device):
|
||||
ledger = False
|
||||
if device.product_key[0] == 0x2581 and device.product_key[1] == 0x3b7c:
|
||||
|
@ -618,12 +625,12 @@ class LedgerPlugin(HW_PluginBase):
|
|||
ledger = True
|
||||
else:
|
||||
return None # non-compatible interface of a Nano S or Blue
|
||||
with self.device_manager().hid_lock:
|
||||
dev = hid.device()
|
||||
dev.open_path(device.path)
|
||||
dev.set_nonblocking(True)
|
||||
dev = hid.device()
|
||||
dev.open_path(device.path)
|
||||
dev.set_nonblocking(True)
|
||||
return HIDDongleHIDAPI(dev, ledger, BTCHIP_DEBUG)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def create_client(self, device, handler):
|
||||
if handler:
|
||||
self.handler = handler
|
||||
|
@ -648,6 +655,7 @@ class LedgerPlugin(HW_PluginBase):
|
|||
xpub = client.get_xpub(derivation, xtype)
|
||||
return xpub
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def get_client(self, keystore, force_pair=True, *,
|
||||
devices=None, allow_user_interaction=True):
|
||||
# All client interaction should not be in the main GUI thread
|
||||
|
@ -661,6 +669,7 @@ class LedgerPlugin(HW_PluginBase):
|
|||
client.checkDevice()
|
||||
return client
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def show_address(self, wallet, address, keystore=None):
|
||||
if keystore is None:
|
||||
keystore = wallet.get_keystore()
|
||||
|
|
|
@ -8,6 +8,7 @@ from electrum.util import UserCancelled
|
|||
from electrum.keystore import bip39_normalize_passphrase
|
||||
from electrum.bip32 import BIP32Node, convert_bip32_path_to_list_of_uint32
|
||||
from electrum.logging import Logger
|
||||
from electrum.plugin import runs_in_hwd_thread
|
||||
from electrum.plugins.hw_wallet.plugin import HardwareClientBase, HardwareHandlerBase
|
||||
|
||||
|
||||
|
@ -131,6 +132,7 @@ class SafeTClientBase(HardwareClientBase, GuiMixin, Logger):
|
|||
def is_pairable(self):
|
||||
return not self.features.bootloader_mode
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def has_usable_connection_with_device(self):
|
||||
try:
|
||||
res = self.ping("electrum pinging device")
|
||||
|
@ -145,6 +147,7 @@ class SafeTClientBase(HardwareClientBase, GuiMixin, Logger):
|
|||
def prevent_timeouts(self):
|
||||
self.last_operation = float('inf')
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def timeout(self, cutoff):
|
||||
'''Time out the client if the last operation was before cutoff.'''
|
||||
if self.last_operation < cutoff:
|
||||
|
@ -155,6 +158,7 @@ class SafeTClientBase(HardwareClientBase, GuiMixin, Logger):
|
|||
def expand_path(n):
|
||||
return convert_bip32_path_to_list_of_uint32(n)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def cancel(self):
|
||||
'''Provided here as in keepkeylib but not safetlib.'''
|
||||
self.transport.write(self.proto.Cancel())
|
||||
|
@ -162,6 +166,7 @@ class SafeTClientBase(HardwareClientBase, GuiMixin, Logger):
|
|||
def i4b(self, x):
|
||||
return pack('>I', x)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def get_xpub(self, bip32_path, xtype):
|
||||
address_n = self.expand_path(bip32_path)
|
||||
creating = False
|
||||
|
@ -173,6 +178,7 @@ class SafeTClientBase(HardwareClientBase, GuiMixin, Logger):
|
|||
fingerprint=self.i4b(node.fingerprint),
|
||||
child_number=self.i4b(node.child_num)).to_xpub()
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def toggle_passphrase(self):
|
||||
if self.features.passphrase_protection:
|
||||
self.msg = _("Confirm on your {} device to disable passphrases")
|
||||
|
@ -181,14 +187,17 @@ class SafeTClientBase(HardwareClientBase, GuiMixin, Logger):
|
|||
enabled = not self.features.passphrase_protection
|
||||
self.apply_settings(use_passphrase=enabled)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def change_label(self, label):
|
||||
self.msg = _("Confirm the new label on your {} device")
|
||||
self.apply_settings(label=label)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def change_homescreen(self, homescreen):
|
||||
self.msg = _("Confirm on your {} device to change your home screen")
|
||||
self.apply_settings(homescreen=homescreen)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def set_pin(self, remove):
|
||||
if remove:
|
||||
self.msg = _("Confirm on your {} device to disable PIN protection")
|
||||
|
@ -198,6 +207,7 @@ class SafeTClientBase(HardwareClientBase, GuiMixin, Logger):
|
|||
self.msg = _("Confirm on your {} device to set a PIN")
|
||||
self.change_pin(remove)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def clear_session(self):
|
||||
'''Clear the session to force pin (and passphrase if enabled)
|
||||
re-entry. Does not leak exceptions.'''
|
||||
|
@ -209,10 +219,12 @@ class SafeTClientBase(HardwareClientBase, GuiMixin, Logger):
|
|||
# If the device was removed it has the same effect...
|
||||
self.logger.info(f"clear_session: ignoring error {e}")
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def get_public_node(self, address_n, creating):
|
||||
self.creating_wallet = creating
|
||||
return super(SafeTClientBase, self).get_public_node(address_n)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def close(self):
|
||||
'''Called when Our wallet was closed or the device removed.'''
|
||||
self.logger.info("closing client")
|
||||
|
|
|
@ -7,7 +7,7 @@ from electrum.util import bfh, bh2u, versiontuple, UserCancelled, UserFacingExce
|
|||
from electrum.bip32 import BIP32Node
|
||||
from electrum import constants
|
||||
from electrum.i18n import _
|
||||
from electrum.plugin import Device
|
||||
from electrum.plugin import Device, runs_in_hwd_thread
|
||||
from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput
|
||||
from electrum.keystore import Hardware_KeyStore
|
||||
from electrum.base_wizard import ScriptTypeNotSupported
|
||||
|
@ -35,6 +35,7 @@ class SafeTKeyStore(Hardware_KeyStore):
|
|||
def decrypt_message(self, sequence, message, password):
|
||||
raise UserFacingException(_('Encryption and decryption are not implemented by {}').format(self.device))
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def sign_message(self, sequence, message, password):
|
||||
client = self.get_client()
|
||||
address_path = self.get_derivation_prefix() + "/%d/%d"%sequence
|
||||
|
@ -42,6 +43,7 @@ class SafeTKeyStore(Hardware_KeyStore):
|
|||
msg_sig = client.sign_message(self.plugin.get_coin_name(), address_n, message)
|
||||
return msg_sig.signature
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def sign_transaction(self, tx, password):
|
||||
if tx.is_complete():
|
||||
return
|
||||
|
@ -96,6 +98,7 @@ class SafeTPlugin(HW_PluginBase):
|
|||
except AttributeError:
|
||||
return 'unknown'
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def enumerate(self):
|
||||
devices = self.transport_handler.enumerate_devices()
|
||||
return [Device(path=d.get_path(),
|
||||
|
@ -106,6 +109,7 @@ class SafeTPlugin(HW_PluginBase):
|
|||
transport_ui_string=d.get_path())
|
||||
for d in devices]
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def create_client(self, device, handler):
|
||||
try:
|
||||
self.logger.info(f"connecting to device at {device.path}")
|
||||
|
@ -141,6 +145,7 @@ class SafeTPlugin(HW_PluginBase):
|
|||
|
||||
return client
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def get_client(self, keystore, force_pair=True, *,
|
||||
devices=None, allow_user_interaction=True) -> Optional['SafeTClient']:
|
||||
client = super().get_client(keystore, force_pair,
|
||||
|
@ -198,6 +203,7 @@ class SafeTPlugin(HW_PluginBase):
|
|||
finally:
|
||||
wizard.loop.exit(exit_code)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def _initialize_device(self, settings, method, device_id, wizard, handler):
|
||||
item, label, pin_protection, passphrase_protection = settings
|
||||
|
||||
|
@ -289,6 +295,7 @@ class SafeTPlugin(HW_PluginBase):
|
|||
return self.types.OutputScriptType.PAYTOMULTISIG
|
||||
raise ValueError('unexpected txin type: {}'.format(electrum_txin_type))
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def sign_transaction(self, keystore, tx: PartialTransaction, prev_tx):
|
||||
self.prev_tx = prev_tx
|
||||
client = self.get_client(keystore)
|
||||
|
@ -299,6 +306,7 @@ class SafeTPlugin(HW_PluginBase):
|
|||
signatures = [(bh2u(x) + '01') for x in signatures]
|
||||
tx.update_signatures(signatures)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def show_address(self, wallet, address, keystore=None):
|
||||
if keystore is None:
|
||||
keystore = wallet.get_keystore()
|
||||
|
|
|
@ -7,6 +7,7 @@ from electrum.util import UserCancelled, UserFacingException
|
|||
from electrum.keystore import bip39_normalize_passphrase
|
||||
from electrum.bip32 import BIP32Node, convert_bip32_path_to_list_of_uint32 as parse_path
|
||||
from electrum.logging import Logger
|
||||
from electrum.plugin import runs_in_hwd_thread
|
||||
from electrum.plugins.hw_wallet.plugin import OutdatedHwFirmwareException, HardwareClientBase
|
||||
|
||||
from trezorlib.client import TrezorClient, PASSPHRASE_ON_DEVICE
|
||||
|
@ -107,6 +108,7 @@ class TrezorClientBase(HardwareClientBase, Logger):
|
|||
def is_pairable(self):
|
||||
return not self.features.bootloader_mode
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def has_usable_connection_with_device(self):
|
||||
if self.in_flow:
|
||||
return True
|
||||
|
@ -123,6 +125,7 @@ class TrezorClientBase(HardwareClientBase, Logger):
|
|||
def prevent_timeouts(self):
|
||||
self.last_operation = float('inf')
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def timeout(self, cutoff):
|
||||
'''Time out the client if the last operation was before cutoff.'''
|
||||
if self.last_operation < cutoff:
|
||||
|
@ -132,6 +135,7 @@ class TrezorClientBase(HardwareClientBase, Logger):
|
|||
def i4b(self, x):
|
||||
return pack('>I', x)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def get_xpub(self, bip32_path, xtype, creating=False):
|
||||
address_n = parse_path(bip32_path)
|
||||
with self.run_flow(creating_wallet=creating):
|
||||
|
@ -143,6 +147,7 @@ class TrezorClientBase(HardwareClientBase, Logger):
|
|||
fingerprint=self.i4b(node.fingerprint),
|
||||
child_number=self.i4b(node.child_num)).to_xpub()
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def toggle_passphrase(self):
|
||||
if self.features.passphrase_protection:
|
||||
msg = _("Confirm on your {} device to disable passphrases")
|
||||
|
@ -152,14 +157,17 @@ class TrezorClientBase(HardwareClientBase, Logger):
|
|||
with self.run_flow(msg):
|
||||
trezorlib.device.apply_settings(self.client, use_passphrase=enabled)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def change_label(self, label):
|
||||
with self.run_flow(_("Confirm the new label on your {} device")):
|
||||
trezorlib.device.apply_settings(self.client, label=label)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def change_homescreen(self, homescreen):
|
||||
with self.run_flow(_("Confirm on your {} device to change your home screen")):
|
||||
trezorlib.device.apply_settings(self.client, homescreen=homescreen)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def set_pin(self, remove):
|
||||
if remove:
|
||||
msg = _("Confirm on your {} device to disable PIN protection")
|
||||
|
@ -170,6 +178,7 @@ class TrezorClientBase(HardwareClientBase, Logger):
|
|||
with self.run_flow(msg):
|
||||
trezorlib.device.change_pin(self.client, remove)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def clear_session(self):
|
||||
'''Clear the session to force pin (and passphrase if enabled)
|
||||
re-entry. Does not leak exceptions.'''
|
||||
|
@ -181,11 +190,13 @@ class TrezorClientBase(HardwareClientBase, Logger):
|
|||
# If the device was removed it has the same effect...
|
||||
self.logger.info(f"clear_session: ignoring error {e}")
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def close(self):
|
||||
'''Called when Our wallet was closed or the device removed.'''
|
||||
self.logger.info("closing client")
|
||||
self.clear_session()
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def is_uptodate(self):
|
||||
if self.client.is_outdated():
|
||||
return False
|
||||
|
@ -203,6 +214,7 @@ class TrezorClientBase(HardwareClientBase, Logger):
|
|||
return "Trezor T"
|
||||
return None
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def show_address(self, address_str, script_type, multisig=None):
|
||||
coin_name = self.plugin.get_coin_name()
|
||||
address_n = parse_path(address_str)
|
||||
|
@ -215,6 +227,7 @@ class TrezorClientBase(HardwareClientBase, Logger):
|
|||
script_type=script_type,
|
||||
multisig=multisig)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def sign_message(self, address_str, message):
|
||||
coin_name = self.plugin.get_coin_name()
|
||||
address_n = parse_path(address_str)
|
||||
|
@ -225,6 +238,7 @@ class TrezorClientBase(HardwareClientBase, Logger):
|
|||
address_n,
|
||||
message)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def recover_device(self, recovery_type, *args, **kwargs):
|
||||
input_callback = self.mnemonic_callback(recovery_type)
|
||||
with self.run_flow():
|
||||
|
@ -237,14 +251,17 @@ class TrezorClientBase(HardwareClientBase, Logger):
|
|||
|
||||
# ========= Unmodified trezorlib methods =========
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def sign_tx(self, *args, **kwargs):
|
||||
with self.run_flow():
|
||||
return trezorlib.btc.sign_tx(self.client, *args, **kwargs)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def reset_device(self, *args, **kwargs):
|
||||
with self.run_flow():
|
||||
return trezorlib.device.reset(self.client, *args, **kwargs)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def wipe_device(self, *args, **kwargs):
|
||||
with self.run_flow():
|
||||
return trezorlib.device.wipe(self.client, *args, **kwargs)
|
||||
|
|
|
@ -6,7 +6,7 @@ from electrum.util import bfh, bh2u, versiontuple, UserCancelled, UserFacingExce
|
|||
from electrum.bip32 import BIP32Node, convert_bip32_path_to_list_of_uint32 as parse_path
|
||||
from electrum import constants
|
||||
from electrum.i18n import _
|
||||
from electrum.plugin import Device
|
||||
from electrum.plugin import Device, runs_in_hwd_thread
|
||||
from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput
|
||||
from electrum.keystore import Hardware_KeyStore
|
||||
from electrum.base_wizard import ScriptTypeNotSupported, HWD_SETUP_NEW_WALLET
|
||||
|
@ -143,6 +143,7 @@ class TrezorPlugin(HW_PluginBase):
|
|||
else:
|
||||
raise LibraryFoundButUnusable(library_version=version)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def is_bridge_available(self) -> bool:
|
||||
# Testing whether the Bridge is available can take several seconds
|
||||
# (when it is not), as it is slow to timeout, hence we cache it.
|
||||
|
@ -157,6 +158,7 @@ class TrezorPlugin(HW_PluginBase):
|
|||
self._is_bridge_available = True
|
||||
return self._is_bridge_available
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def enumerate(self):
|
||||
# If there is a bridge, prefer that.
|
||||
# On Windows, the bridge runs as Admin (and Electrum usually does not),
|
||||
|
@ -174,6 +176,7 @@ class TrezorPlugin(HW_PluginBase):
|
|||
transport_ui_string=d.get_path())
|
||||
for d in devices]
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def create_client(self, device, handler):
|
||||
try:
|
||||
self.logger.info(f"connecting to device at {device.path}")
|
||||
|
@ -190,6 +193,7 @@ class TrezorPlugin(HW_PluginBase):
|
|||
# note that this call can still raise!
|
||||
return TrezorClientBase(transport, handler, self)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def get_client(self, keystore, force_pair=True, *,
|
||||
devices=None, allow_user_interaction=True) -> Optional['TrezorClientBase']:
|
||||
client = super().get_client(keystore, force_pair,
|
||||
|
@ -238,6 +242,7 @@ class TrezorPlugin(HW_PluginBase):
|
|||
finally:
|
||||
wizard.loop.exit(exit_code)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def _initialize_device(self, settings: TrezorInitSettings, method, device_id, wizard, handler):
|
||||
if method == TIM_RECOVER and settings.recovery_type == RecoveryDeviceType.ScrambledWords:
|
||||
handler.show_error(_(
|
||||
|
@ -333,6 +338,7 @@ class TrezorPlugin(HW_PluginBase):
|
|||
return OutputScriptType.PAYTOMULTISIG
|
||||
raise ValueError('unexpected txin type: {}'.format(electrum_txin_type))
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def sign_transaction(self, keystore, tx: PartialTransaction, prev_tx):
|
||||
prev_tx = { bfh(txhash): self.electrum_tx_to_txtype(tx) for txhash, tx in prev_tx.items() }
|
||||
client = self.get_client(keystore)
|
||||
|
@ -343,6 +349,7 @@ class TrezorPlugin(HW_PluginBase):
|
|||
signatures = [(bh2u(x) + '01') for x in signatures]
|
||||
tx.update_signatures(signatures)
|
||||
|
||||
@runs_in_hwd_thread
|
||||
def show_address(self, wallet, address, keystore=None):
|
||||
if keystore is None:
|
||||
keystore = wallet.get_keystore()
|
||||
|
|
Loading…
Add table
Reference in a new issue