From 4d2abc75f518ff2d2901b002772a3d97ec53e79a Mon Sep 17 00:00:00 2001 From: k9ert Date: Tue, 5 Jul 2022 13:41:27 +0200 Subject: [PATCH] Chore: migrating to hwi 2.1.0 (#1693) * migrating to hwi 2.1.0 * removing keepkey and trezor (Multisig is now in hwilib) * some helper to understand hwi * fixing the transport-issue * package docstring * understand trezor and hwi * uncomment again * mark test to be skipped * fix inproper use of hwilib --- requirements.in | 3 +- requirements.txt | 24 +- .../specter/devices/hwi/__init__.py | 4 + .../specter/devices/hwi/keepkey.py | 219 ----- .../specter/devices/hwi/trezor.py | 922 ------------------ src/cryptoadvance/specter/devices/keepkey.py | 2 +- src/cryptoadvance/specter/devices/trezor.py | 2 +- src/cryptoadvance/specter/hwi_rpc.py | 42 +- tests/test_hwi_rpc.py | 88 ++ 9 files changed, 136 insertions(+), 1170 deletions(-) delete mode 100644 src/cryptoadvance/specter/devices/hwi/keepkey.py delete mode 100644 src/cryptoadvance/specter/devices/hwi/trezor.py create mode 100644 tests/test_hwi_rpc.py diff --git a/requirements.in b/requirements.in index 39fd76293e..010d2d94e0 100644 --- a/requirements.in +++ b/requirements.in @@ -8,9 +8,8 @@ Flask-Cors==3.0.10 Flask-Login==0.5.0 Flask-RESTful==0.3.9 Flask-HTTPAuth==4.4.0 -hwi==2.0.2 +hwi==2.1.0 importlib_metadata==4.8.1 -pyserial==3.4 python-dotenv==0.13.0 requests==2.26.0 pysocks==1.7.1 diff --git a/requirements.txt b/requirements.txt index 07ab1c1928..be0d8bd634 100644 --- a/requirements.txt +++ b/requirements.txt @@ -40,13 +40,15 @@ base58==2.1.0 \ --hash=sha256:171a547b4a3c61e1ae3807224a6f7aec75e364c4395e7562649d7335768001a2 \ --hash=sha256:8225891d501b68c843ffe30b86371f844a21c6ba00da76f52f9b998ba771fb48 # via bitbox02 -bitbox02==5.3.0 \ - --hash=sha256:797e6904d431f6d2ef711f169e7ce8fffc125cc8c5b3efb8187fd451f45635e1 \ - --hash=sha256:fe0e8aeb9b32fd7d76bb3e9838895973a74dfd532a8fb8ac174a1a60214aee26 +bitbox02==6.0.0 \ + --hash=sha256:a738f9977317c104298c0b59a0fc93c98d48a1b03d192dcd6396963d3ed58127 \ + --hash=sha256:c136ad7a1ddd272705368ccb6906c0cd71746afaf6a1f05d8e5aaa70e04baffd # via hwi cbor==1.0.0 \ --hash=sha256:13225a262ddf5615cbd9fd55a76a0d53069d18b07d2e9f19c39e6acb8609bbb6 - # via -r requirements.in + # via + # -r requirements.in + # hwi certifi==2021.10.8 \ --hash=sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872 \ --hash=sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569 @@ -217,9 +219,9 @@ hidapi==0.10.1 \ # via # bitbox02 # hwi -hwi==2.0.2 \ - --hash=sha256:70b83e9905d3ae7057cae516e7b26547a934cdaf1feee5ef26fe65f0ddeda058 \ - --hash=sha256:762d5f46c31bc291dc0453530958af7f1a70854a1428b0376139c9c4aabf8c73 +hwi==2.1.0 \ + --hash=sha256:3aec14eaecbe7f1b1c8d622c6e35d0309a12696bd35a2c8ceee21ee1754a7bc5 \ + --hash=sha256:d09d818c91e2ae5213bc3fcf2999668f2bb9098e6bfaa6a3723400356db8fb66 # via -r requirements.in idna==2.10 \ --hash=sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6 \ @@ -357,10 +359,10 @@ pyopenssl==20.0.1 \ --hash=sha256:4c231c759543ba02560fcd2480c48dcec4dae34c9da7d3747c508227e0624b51 \ --hash=sha256:818ae18e06922c066f777a33f1fca45786d85edfe71cd043de6379337a7f274b # via -r requirements.in -pyserial==3.4 \ - --hash=sha256:6e2d401fdee0eab996cf734e67773a0143b932772ca8b42451440cfed942c627 \ - --hash=sha256:e0770fadba80c31013896c7e6ef703f72e7834965954a78e71a3049488d4d7d8 - # via -r requirements.in +pyserial==3.5 \ + --hash=sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb \ + --hash=sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0 + # via hwi pysocks==1.7.1 \ --hash=sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299 \ --hash=sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5 \ diff --git a/src/cryptoadvance/specter/devices/hwi/__init__.py b/src/cryptoadvance/specter/devices/hwi/__init__.py index e69de29bb2..704270ef36 100644 --- a/src/cryptoadvance/specter/devices/hwi/__init__.py +++ b/src/cryptoadvance/specter/devices/hwi/__init__.py @@ -0,0 +1,4 @@ +""" The classes in here are complementary stuff to hwilib: +* The jade-stuff got additional Liquid functionality +* Specter-DIY could potentially be in hwilib, though, i guess +""" diff --git a/src/cryptoadvance/specter/devices/hwi/keepkey.py b/src/cryptoadvance/specter/devices/hwi/keepkey.py deleted file mode 100644 index ac84c389ef..0000000000 --- a/src/cryptoadvance/specter/devices/hwi/keepkey.py +++ /dev/null @@ -1,219 +0,0 @@ -""" -Keepkey -******* -""" - -from hwilib.errors import ( - DEVICE_NOT_INITIALIZED, - DeviceNotReadyError, - common_err_msgs, - handle_errors, -) -from hwilib.devices.trezorlib import protobuf as p -from hwilib.devices.trezorlib.transport import ( - hid, - udp, - webusb, -) -from .trezor import TrezorClient, HID_IDS, WEBUSB_IDS -from hwilib.devices.trezorlib.messages import ( - DebugLinkState, - Features, - HDNodeType, - ResetDevice, -) - -from typing import ( - Any, - Dict, - List, - Optional, -) - -py_enumerate = enumerate # Need to use the enumerate built-in but there's another function already named that - -KEEPKEY_HID_IDS = {(0x2B24, 0x0001)} -KEEPKEY_WEBUSB_IDS = {(0x2B24, 0x0002)} - -HID_IDS.update(KEEPKEY_HID_IDS) -WEBUSB_IDS.update(KEEPKEY_WEBUSB_IDS) - - -class KeepkeyFeatures(Features): # type: ignore - def __init__( - self, - *, - firmware_variant: Optional[str] = None, - firmware_hash: Optional[bytes] = None, - **kwargs: Any, - ) -> None: - super().__init__(**kwargs) - self.firmware_variant = firmware_variant - self.firmware_hash = firmware_hash - - @classmethod - def get_fields(cls) -> Dict[int, p.FieldInfo]: - return { - 1: ("vendor", p.UnicodeType, None), - 2: ("major_version", p.UVarintType, None), - 3: ("minor_version", p.UVarintType, None), - 4: ("patch_version", p.UVarintType, None), - 5: ("bootloader_mode", p.BoolType, None), - 6: ("device_id", p.UnicodeType, None), - 7: ("pin_protection", p.BoolType, None), - 8: ("passphrase_protection", p.BoolType, None), - 9: ("language", p.UnicodeType, None), - 10: ("label", p.UnicodeType, None), - 12: ("initialized", p.BoolType, None), - 13: ("revision", p.BytesType, None), - 14: ("bootloader_hash", p.BytesType, None), - 15: ("imported", p.BoolType, None), - 16: ("unlocked", p.BoolType, None), - 21: ("model", p.UnicodeType, None), - 22: ("firmware_variant", p.UnicodeType, None), - 23: ("firmware_hash", p.BytesType, None), - 24: ("no_backup", p.BoolType, None), - 25: ("wipe_code_protection", p.BoolType, None), - } - - -class KeepkeyResetDevice(ResetDevice): # type: ignore - def __init__( - self, - *, - auto_lock_delay_ms: Optional[int] = None, - **kwargs: Any, - ) -> None: - super().__init__(**kwargs) - self.auto_lock_delay_ms = auto_lock_delay_ms - - @classmethod - def get_fields(cls) -> Dict[int, p.FieldInfo]: - return { - 1: ("display_random", p.BoolType, None), - 2: ("strength", p.UVarintType, 256), # default=256 - 3: ("passphrase_protection", p.BoolType, None), - 4: ("pin_protection", p.BoolType, None), - 5: ("language", p.UnicodeType, "en-US"), # default=en-US - 6: ("label", p.UnicodeType, None), - 7: ("no_backup", p.BoolType, None), - 8: ("auto_lock_delay_ms", p.UVarintType, None), - 9: ("u2f_counter", p.UVarintType, None), - } - - -class KeepkeyDebugLinkState(DebugLinkState): # type: ignore - def __init__( - self, - *, - recovery_cipher: Optional[str] = None, - recovery_auto_completed_word: Optional[str] = None, - firmware_hash: Optional[bytes] = None, - storage_hash: Optional[bytes] = None, - **kwargs: Any, - ) -> None: - super().__init__(**kwargs) - self.recovery_cipher = recovery_cipher - self.recovery_auto_completed_word = recovery_auto_completed_word - self.firmware_hash = firmware_hash - self.storage_hash = storage_hash - - @classmethod - def get_fields(cls) -> Dict[int, p.FieldType]: - return { - 1: ("layout", p.BytesType, None), - 2: ("pin", p.UnicodeType, None), - 3: ("matrix", p.UnicodeType, None), - 4: ("mnemonic_secret", p.BytesType, None), - 5: ("node", HDNodeType, None), - 6: ("passphrase_protection", p.BoolType, None), - 7: ("reset_word", p.UnicodeType, None), - 8: ("reset_entropy", p.BytesType, None), - 9: ("recovery_fake_word", p.UnicodeType, None), - 10: ("recovery_word_pos", p.UVarintType, None), - 11: ("recovery_cipher", p.UnicodeType, None), - 12: ("recovery_auto_completed_word", p.UnicodeType, None), - 13: ("firmware_hash", p.BytesType, None), - 14: ("storage_hash", p.BytesType, None), - } - - -class KeepkeyClient(TrezorClient): - def __init__(self, path: str, password: str = "", expert: bool = False) -> None: - """ - The `KeepkeyClient` is a `HardwareWalletClient` for interacting with the Keepkey. - As Keepkeys are clones of the Trezor 1, please refer to `TrezorClient` for documentation. - """ - super(KeepkeyClient, self).__init__(path, password, expert) - self.type = "Keepkey" - self.client.vendors = "keepkey.com" - self.client.minimum_versions = {"K1-14AM": (0, 0, 0)} - self.client.map_type_to_class_override[ - KeepkeyFeatures.MESSAGE_WIRE_TYPE - ] = KeepkeyFeatures - self.client.map_type_to_class_override[ - KeepkeyResetDevice.MESSAGE_WIRE_TYPE - ] = KeepkeyResetDevice - if self.simulator: - self.client.debug.map_type_to_class_override[ - KeepkeyDebugLinkState.MESSAGE_WIRE_TYPE - ] = KeepkeyDebugLinkState - - -def enumerate(password: str = "") -> List[Dict[str, Any]]: - results = [] - devs = hid.HidTransport.enumerate(usb_ids=KEEPKEY_HID_IDS) - devs.extend(webusb.WebUsbTransport.enumerate(usb_ids=KEEPKEY_WEBUSB_IDS)) - devs.extend(udp.UdpTransport.enumerate()) - for dev in devs: - d_data: Dict[str, Any] = {} - - d_data["type"] = "keepkey" - d_data["model"] = "keepkey" - d_data["path"] = dev.get_path() - - client = None - - with handle_errors(common_err_msgs["enumerate"], d_data): - client = KeepkeyClient(d_data["path"], password) - try: - client.client.refresh_features() - except TypeError: - continue - if "keepkey" not in client.client.features.vendor: - continue - - if d_data["path"] == "udp:127.0.0.1:21324": - d_data["model"] += "_simulator" - - d_data["needs_pin_sent"] = ( - client.client.features.pin_protection - and not client.client.features.unlocked - ) - d_data[ - "needs_passphrase_sent" - ] = ( - client.client.features.passphrase_protection - ) # always need the passphrase sent for Keepkey if it has passphrase protection enabled - if d_data["needs_pin_sent"]: - raise DeviceNotReadyError( - "Keepkey is locked. Unlock by using 'promptpin' and then 'sendpin'." - ) - if d_data["needs_passphrase_sent"] and not password: - raise DeviceNotReadyError( - "Passphrase needs to be specified before the fingerprint information can be retrieved" - ) - if client.client.features.initialized: - d_data["fingerprint"] = client.get_master_fingerprint().hex() - d_data[ - "needs_passphrase_sent" - ] = False # Passphrase is always needed for the above to have worked, so it's already sent - else: - d_data["error"] = "Not initialized" - d_data["code"] = DEVICE_NOT_INITIALIZED - - if client: - client.close() - - results.append(d_data) - return results diff --git a/src/cryptoadvance/specter/devices/hwi/trezor.py b/src/cryptoadvance/specter/devices/hwi/trezor.py deleted file mode 100644 index 09351c57f5..0000000000 --- a/src/cryptoadvance/specter/devices/hwi/trezor.py +++ /dev/null @@ -1,922 +0,0 @@ -""" -Trezor Devices -************** -""" - -from functools import wraps -from typing import ( - Any, - Callable, - Dict, - List, - NoReturn, - Optional, - Sequence, - Tuple, - Union, -) -from hwilib.descriptor import MultisigDescriptor -from hwilib.hwwclient import HardwareWalletClient -from hwilib.errors import ( - ActionCanceledError, - BadArgumentError, - DeviceAlreadyInitError, - DeviceAlreadyUnlockedError, - DeviceConnectionError, - DEVICE_NOT_INITIALIZED, - DeviceNotReadyError, - UnavailableActionError, - common_err_msgs, - handle_errors, -) -from hwilib.devices.trezorlib.client import TrezorClient as Trezor, PASSPHRASE_ON_DEVICE -from hwilib.devices.trezorlib.debuglink import TrezorClientDebugLink -from hwilib.devices.trezorlib.exceptions import Cancelled, TrezorFailure -from hwilib.devices.trezorlib.transport import ( - DEV_TREZOR1, - TREZORS, - hid, - udp, - webusb, -) -from hwilib.devices.trezorlib import ( - btc, - device, -) -from hwilib.devices.trezorlib import messages -from hwilib._base58 import ( - get_xpub_fingerprint, - to_address, -) -from hwilib import _base58 as base58 - -from hwilib.key import ( - ExtendedKey, - parse_path, -) -from hwilib._script import ( - is_p2pkh, - is_p2sh, - is_p2wsh, - is_witness, -) -from hwilib.psbt import ( - PSBT, - PartiallySignedInput, - PartiallySignedOutput, - KeyOriginInfo, -) -from hwilib.tx import ( - CTxOut, -) -from hwilib._serialize import ( - ser_uint256, -) -from hwilib.common import ( - AddressType, - Chain, - hash256, -) -from hwilib import _bech32 as bech32 -from mnemonic import Mnemonic -from usb1 import USBErrorNoDevice -from types import MethodType - -import base64 -import getpass -import logging -import sys -import struct - -py_enumerate = enumerate # Need to use the enumerate built-in but there's another function already named that - -PIN_MATRIX_DESCRIPTION = """ -Use the numeric keypad to describe number positions. The layout is: - 7 8 9 - 4 5 6 - 1 2 3 -""".strip() - -Device = Union[hid.HidTransport, webusb.WebUsbTransport, udp.UdpTransport] - - -# Only handles up to 15 of 15 -def parse_multisig( - script: bytes, - tx_xpubs: Dict[bytes, KeyOriginInfo], - psbt_scope: Union[PartiallySignedInput, PartiallySignedOutput], -) -> Tuple[bool, Optional[messages.MultisigRedeemScriptType]]: - # at least OP_M pub OP_N OP_CHECKMULTISIG - if len(script) < 37: - return (False, None) - # Get m - m = script[0] - 80 - if m < 1 or m > 15: - return (False, None) - - # Get pubkeys and build HDNodePathType - pubkeys = [] - offset = 1 - while True: - pubkey_len = script[offset] - if pubkey_len != 33: - break - offset += 1 - key = script[offset : offset + 33] - offset += 33 - - hd_node = messages.HDNodeType( - depth=0, - fingerprint=0, - child_num=0, - chain_code=b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", - public_key=key, - ) - pubkeys.append(messages.HDNodePathType(node=hd_node, address_n=[])) - - # Check things at the end - n = script[offset] - 80 - if n != len(pubkeys): - return (False, None) - offset += 1 - op_cms = script[offset] - if op_cms != 174: - return (False, None) - - # check if we know corresponding xpubs from global scope - for pub in pubkeys: - if pub.node.public_key in psbt_scope.hd_keypaths: - derivation = psbt_scope.hd_keypaths[pub.node.public_key] - for xpub in tx_xpubs: - hd = ExtendedKey.deserialize(base58.encode(xpub + hash256(xpub)[:4])) - origin = tx_xpubs[xpub] - # check fingerprint and derivation - if (origin.fingerprint == derivation.fingerprint) and ( - origin.path == derivation.path[: len(origin.path)] - ): - # all good - populate node and break - pub.address_n = list(derivation.path[len(origin.path) :]) - pub.node = messages.HDNodeType( - depth=hd.depth, - fingerprint=int.from_bytes(hd.parent_fingerprint, "big"), - child_num=hd.child_num, - chain_code=hd.chaincode, - public_key=hd.pubkey, - ) - break - # Build MultisigRedeemScriptType and return it - multisig = messages.MultisigRedeemScriptType( - m=m, signatures=[b""] * n, pubkeys=pubkeys - ) - return (True, multisig) - - -def trezor_exception(f: Callable[..., Any]) -> Any: - @wraps(f) - def func(*args: Any, **kwargs: Any) -> Any: - try: - return f(*args, **kwargs) - except ValueError as e: - raise BadArgumentError(str(e)) - except Cancelled: - raise ActionCanceledError("{} canceled".format(f.__name__)) - except USBErrorNoDevice: - raise DeviceConnectionError("Device disconnected") - - return func - - -def interactive_get_pin(self: object, code: Optional[int] = None) -> str: - if code == messages.PinMatrixRequestType.Currrent: - desc = "current PIN" - elif code == messages.PinMatrixRequestType.NewFirst: - desc = "new PIN" - elif code == messages.PinMatrixRequestType.NewSecond: - desc = "new PIN again" - else: - desc = "PIN" - - print(PIN_MATRIX_DESCRIPTION, file=sys.stderr) - - while True: - pin = getpass.getpass(f"Please entire {desc}:\n") - if not pin.isdigit(): - print("Non-numerical PIN provided, please try again", file=sys.stderr) - else: - return pin - - -def mnemonic_words( - expand: bool = False, language: str = "english" -) -> Callable[[Any], str]: - wordlist: Sequence[str] = [] - if expand: - wordlist = Mnemonic(language).wordlist - - def expand_word(word: str) -> str: - if not expand: - return word - if word in wordlist: - return word - matches = [w for w in wordlist if w.startswith(word)] - if len(matches) == 1: - return matches[0] - print("Choose one of: " + ", ".join(matches), file=sys.stderr) - raise KeyError(word) - - def get_word(type: messages.WordRequestType) -> str: - assert type == messages.WordRequestType.Plain - while True: - try: - word = input("Enter one word of mnemonic:\n") - return expand_word(word) - except KeyError: - pass - except Exception: - raise Cancelled from None - - return get_word - - -class PassphraseUI: - def __init__(self, passphrase: str) -> None: - self.passphrase = passphrase - self.pinmatrix_shown = False - self.prompt_shown = False - self.always_prompt = False - self.return_passphrase = True - - def button_request(self, code: Optional[int]) -> None: - if not self.prompt_shown: - print("Please confirm action on your Trezor device", file=sys.stderr) - if not self.always_prompt: - self.prompt_shown = True - - def get_pin(self, code: Optional[int] = None) -> NoReturn: - raise NotImplementedError("get_pin is not needed") - - def disallow_passphrase(self) -> None: - self.return_passphrase = False - - def get_passphrase(self, available_on_device: bool) -> object: - if available_on_device: - return PASSPHRASE_ON_DEVICE - if self.return_passphrase: - return self.passphrase - raise ValueError("Passphrase from Host is not allowed for Trezor T") - - -HID_IDS = {DEV_TREZOR1} -WEBUSB_IDS = TREZORS.copy() - - -def get_path_transport(path: str) -> Device: - devs = hid.HidTransport.enumerate(usb_ids=HID_IDS) - devs.extend(webusb.WebUsbTransport.enumerate(usb_ids=WEBUSB_IDS)) - devs.extend(udp.UdpTransport.enumerate()) - for dev in devs: - if path == dev.get_path(): - return dev - raise BadArgumentError(f"Could not find device by path: {path}") - - -# This class extends the HardwareWalletClient for Trezor specific things -class TrezorClient(HardwareWalletClient): - def __init__(self, path: str, password: str = "", expert: bool = False) -> None: - super(TrezorClient, self).__init__(path, password, expert) - self.simulator = False - transport = get_path_transport(path) - if path.startswith("udp"): - logging.debug("Simulator found, using DebugLink") - self.client = TrezorClientDebugLink(transport=transport) - self.simulator = True - self.client.use_passphrase(password) - else: - self.client = Trezor(transport=transport, ui=PassphraseUI(password)) - - # if it wasn't able to find a client, throw an error - if not self.client: - raise IOError("no Device") - - self.password = password - self.type = "Trezor" - - def _prepare_device(self) -> None: - self.coin_name = "Bitcoin" if self.chain == Chain.MAIN else "Testnet" - resp = self.client.refresh_features() - # If this is a Trezor One or Keepkey, do Initialize - if resp.model == "1" or resp.model == "K1-14AM": - self.client.init_device() - # For the T, we need to check if a passphrase needs to be entered - elif resp.model == "T": - try: - self.client.ensure_unlocked() - except TrezorFailure: - self.client.init_device() - - def _check_unlocked(self) -> None: - self._prepare_device() - if self.client.features.model == "T" and isinstance( - self.client.ui, PassphraseUI - ): - self.client.ui.disallow_passphrase() - if self.client.features.pin_protection and not self.client.features.unlocked: - raise DeviceNotReadyError( - "{} is locked. Unlock by using 'promptpin' and then 'sendpin'.".format( - self.type - ) - ) - - @trezor_exception - def get_pubkey_at_path(self, path: str) -> ExtendedKey: - self._check_unlocked() - try: - expanded_path = parse_path(path) - except ValueError as e: - raise BadArgumentError(str(e)) - output = btc.get_public_node( - self.client, expanded_path, coin_name=self.coin_name - ) - xpub = ExtendedKey.deserialize(output.xpub) - if self.chain != Chain.MAIN: - xpub.version = ExtendedKey.TESTNET_PUBLIC - return xpub - - @trezor_exception - def sign_tx(self, tx: PSBT) -> PSBT: - """ - Sign a transaction with the Trezor. There are some limitations to what transactions can be signed. - - - Multisig inputs are limited to at most n-of-15 multisigs. This is a firmware limitation. - - Transactions with arbitrary input scripts (scriptPubKey, redeemScript, or witnessScript) and arbitrary output scripts cannot be signed. This is a firmware limitation. - - Send-to-self transactions will result in no prompt for outputs as all outputs will be detected as change. - """ - self._check_unlocked() - - # Get this devices master key fingerprint - master_key = btc.get_public_node(self.client, [0x80000000], coin_name="Bitcoin") - master_fp = get_xpub_fingerprint(master_key.xpub) - - # Do multiple passes for multisig - passes = 1 - p = 0 - - while p < passes: - # Prepare inputs - inputs = [] - to_ignore = ( - [] - ) # Note down which inputs whose signatures we're going to ignore - for input_num, (psbt_in, txin) in py_enumerate( - list(zip(tx.inputs, tx.tx.vin)) - ): - txinputtype = messages.TxInputType( - prev_hash=ser_uint256(txin.prevout.hash)[::-1], - prev_index=txin.prevout.n, - sequence=txin.nSequence, - ) - - # Detrermine spend type - scriptcode = b"" - utxo = None - if psbt_in.witness_utxo: - utxo = psbt_in.witness_utxo - if psbt_in.non_witness_utxo: - if txin.prevout.hash != psbt_in.non_witness_utxo.sha256: - raise BadArgumentError( - "Input {} has a non_witness_utxo with the wrong hash".format( - input_num - ) - ) - utxo = psbt_in.non_witness_utxo.vout[txin.prevout.n] - if utxo is None: - continue - scriptcode = utxo.scriptPubKey - - # Check if P2SH - p2sh = False - if is_p2sh(scriptcode): - # Look up redeemscript - if len(psbt_in.redeem_script) == 0: - continue - scriptcode = psbt_in.redeem_script - p2sh = True - - # Check segwit - is_wit, _, _ = is_witness(scriptcode) - - if is_wit: - if p2sh: - txinputtype.script_type = ( - messages.InputScriptType.SPENDP2SHWITNESS - ) - else: - txinputtype.script_type = messages.InputScriptType.SPENDWITNESS - else: - txinputtype.script_type = messages.InputScriptType.SPENDADDRESS - txinputtype.amount = utxo.nValue - - # Check if P2WSH - p2wsh = False - if is_p2wsh(scriptcode): - # Look up witnessscript - if len(psbt_in.witness_script) == 0: - continue - scriptcode = psbt_in.witness_script - p2wsh = True - - def ignore_input() -> None: - txinputtype.address_n = [ - 0x80000000 | 84, - 0x80000000 | (0 if self.chain == Chain.MAIN else 1), - 0x80000000, - 0, - 0, - ] - txinputtype.multisig = None - txinputtype.script_type = messages.InputScriptType.SPENDWITNESS - inputs.append(txinputtype) - to_ignore.append(input_num) - - # Check for multisig - is_ms, multisig = parse_multisig(scriptcode, tx.xpub, psbt_in) - if is_ms: - # Add to txinputtype - txinputtype.multisig = multisig - if not is_wit: - if utxo.is_p2sh: - txinputtype.script_type = ( - messages.InputScriptType.SPENDMULTISIG - ) - else: - # Cannot sign bare multisig, ignore it - ignore_input() - continue - elif not is_ms and not is_wit and not is_p2pkh(scriptcode): - # Cannot sign unknown spk, ignore it - ignore_input() - continue - elif not is_ms and is_wit and p2wsh: - # Cannot sign unknown witness script, ignore it - ignore_input() - continue - - # Find key to sign with - found = False # Whether we have found a key to sign with - found_in_sigs = ( - False # Whether we have found one of our keys in the signatures - ) - our_keys = 0 - for key in psbt_in.hd_keypaths.keys(): - keypath = psbt_in.hd_keypaths[key] - if keypath.fingerprint == master_fp: - if ( - key in psbt_in.partial_sigs - ): # This key already has a signature - found_in_sigs = True - continue - if ( - not found - ): # This key does not have a signature and we don't have a key to sign with yet - txinputtype.address_n = keypath.path - found = True - our_keys += 1 - - # Determine if we need to do more passes to sign everything - if our_keys > passes: - passes = our_keys - - if ( - not found and not found_in_sigs - ): # None of our keys were in hd_keypaths or in partial_sigs - # This input is not one of ours - ignore_input() - continue - elif ( - not found and found_in_sigs - ): # All of our keys are in partial_sigs, ignore whatever signature is produced for this input - ignore_input() - continue - - # append to inputs - inputs.append(txinputtype) - - # address version byte - if self.chain != Chain.MAIN: - p2pkh_version = b"\x6f" - p2sh_version = b"\xc4" - bech32_hrp = "tb" - else: - p2pkh_version = b"\x00" - p2sh_version = b"\x05" - bech32_hrp = "bc" - - # prepare outputs - outputs = [] - for i, out in py_enumerate(tx.tx.vout): - txoutput = messages.TxOutputType(amount=out.nValue) - txoutput.script_type = messages.OutputScriptType.PAYTOADDRESS - if out.is_p2pkh(): - txoutput.address = to_address(out.scriptPubKey[3:23], p2pkh_version) - elif out.is_p2sh(): - txoutput.address = to_address(out.scriptPubKey[2:22], p2sh_version) - elif out.is_opreturn(): - txoutput.script_type = messages.OutputScriptType.PAYTOOPRETURN - txoutput.op_return_data = out.scriptPubKey[2:] - else: - wit, ver, prog = out.is_witness() - if wit: - txoutput.address = bech32.encode(bech32_hrp, ver, prog) - else: - raise BadArgumentError("Output is not an address") - - # Add the derivation path for change - psbt_out = tx.outputs[i] - for _, keypath in psbt_out.hd_keypaths.items(): - if keypath.fingerprint != master_fp: - continue - wit, ver, prog = out.is_witness() - if out.is_p2pkh(): - txoutput.address_n = keypath.path - txoutput.address = None - elif wit: - txoutput.script_type = messages.OutputScriptType.PAYTOWITNESS - txoutput.address_n = keypath.path - txoutput.address = None - elif out.is_p2sh() and psbt_out.redeem_script: - wit, ver, prog = CTxOut(0, psbt_out.redeem_script).is_witness() - if wit and len(prog) in [20, 32]: - txoutput.script_type = ( - messages.OutputScriptType.PAYTOP2SHWITNESS - ) - txoutput.address_n = keypath.path - txoutput.address = None - - # add multisig info - is_ms, multisig = parse_multisig( - psbt_out.witness_script or psbt_out.redeem_script, tx.xpub, psbt_out - ) - if is_ms: - txoutput.multisig = multisig - - # append to outputs - outputs.append(txoutput) - - # Prepare prev txs - prevtxs = {} - for psbt_in in tx.inputs: - if psbt_in.non_witness_utxo: - prev = psbt_in.non_witness_utxo - - t = messages.TransactionType() - t.version = prev.nVersion - t.lock_time = prev.nLockTime - - for vin in prev.vin: - i = messages.TxInputType( - prev_hash=ser_uint256(vin.prevout.hash)[::-1], - prev_index=vin.prevout.n, - script_sig=vin.scriptSig, - sequence=vin.nSequence, - ) - t.inputs.append(i) - - for vout in prev.vout: - o = messages.TxOutputBinType( - amount=vout.nValue, - script_pubkey=vout.scriptPubKey, - ) - t.bin_outputs.append(o) - logging.debug(psbt_in.non_witness_utxo.hash) - assert psbt_in.non_witness_utxo.sha256 is not None - prevtxs[ser_uint256(psbt_in.non_witness_utxo.sha256)[::-1]] = t - - # Sign the transaction - signed_tx = btc.sign_tx( - client=self.client, - coin_name=self.coin_name, - inputs=inputs, - outputs=outputs, - prev_txes=prevtxs, - version=tx.tx.nVersion, - lock_time=tx.tx.nLockTime, - ) - - # Each input has one signature - for input_num, (psbt_in, sig) in py_enumerate( - list(zip(tx.inputs, signed_tx[0])) - ): - if input_num in to_ignore: - continue - for pubkey in psbt_in.hd_keypaths.keys(): - fp = psbt_in.hd_keypaths[pubkey].fingerprint - if fp == master_fp and pubkey not in psbt_in.partial_sigs: - psbt_in.partial_sigs[pubkey] = sig + b"\x01" - break - - p += 1 - - return tx - - @trezor_exception - def sign_message(self, message: Union[str, bytes], keypath: str) -> str: - self._check_unlocked() - path = parse_path(keypath) - result = btc.sign_message(self.client, self.coin_name, path, message) - return base64.b64encode(result.signature).decode("utf-8") - - @trezor_exception - def display_singlesig_address( - self, - keypath: str, - addr_type: AddressType, - ) -> str: - self._check_unlocked() - - # Script type - if addr_type == AddressType.SH_WIT: - script_type = messages.InputScriptType.SPENDP2SHWITNESS - elif addr_type == AddressType.WIT: - script_type = messages.InputScriptType.SPENDWITNESS - elif addr_type == AddressType.LEGACY: - script_type = messages.InputScriptType.SPENDADDRESS - else: - raise BadArgumentError("Unknown address type") - - expanded_path = parse_path(keypath) - - try: - address = btc.get_address( - self.client, - self.coin_name, - expanded_path, - show_display=True, - script_type=script_type, - multisig=None, - ) - assert isinstance(address, str) - return address - except Exception: - pass - - raise BadArgumentError("No path supplied matched device keys") - - @trezor_exception - def display_multisig_address( - self, - addr_type: AddressType, - multisig: MultisigDescriptor, - ) -> str: - self._check_unlocked() - - der_pks = list( - zip([p.get_pubkey_bytes(0) for p in multisig.pubkeys], multisig.pubkeys) - ) - if multisig.is_sorted: - der_pks = sorted(der_pks) - - pubkey_objs = [] - for pk, p in der_pks: - if p.extkey is not None: - xpub = p.extkey - hd_node = messages.HDNodeType( - depth=xpub.depth, - fingerprint=int.from_bytes(xpub.parent_fingerprint, "big"), - child_num=xpub.child_num, - chain_code=xpub.chaincode, - public_key=xpub.pubkey, - ) - pubkey_objs.append( - messages.HDNodePathType( - node=hd_node, - address_n=parse_path( - "m" + p.deriv_path if p.deriv_path is not None else "" - ), - ) - ) - else: - hd_node = messages.HDNodeType( - depth=0, - fingerprint=0, - child_num=0, - chain_code=b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", - public_key=pk, - ) - pubkey_objs.append(messages.HDNodePathType(node=hd_node, address_n=[])) - - trezor_ms = messages.MultisigRedeemScriptType( - m=multisig.thresh, signatures=[b""] * len(pubkey_objs), pubkeys=pubkey_objs - ) - - # Script type - if addr_type == AddressType.SH_WIT: - script_type = messages.InputScriptType.SPENDP2SHWITNESS - elif addr_type == AddressType.WIT: - script_type = messages.InputScriptType.SPENDWITNESS - elif addr_type == AddressType.LEGACY: - script_type = messages.InputScriptType.SPENDMULTISIG - else: - raise BadArgumentError("Unknown address type") - - for p in multisig.pubkeys: - keypath = p.origin.get_derivation_path() if p.origin is not None else "m/" - keypath += p.deriv_path if p.deriv_path is not None else "" - path = parse_path(keypath) - try: - address = btc.get_address( - self.client, - self.coin_name, - path, - show_display=True, - script_type=script_type, - multisig=trezor_ms, - ) - assert isinstance(address, str) - return address - except Exception: - pass - - raise BadArgumentError("No path supplied matched device keys") - - @trezor_exception - def setup_device(self, label: str = "", passphrase: str = "") -> bool: - self._prepare_device() - if not self.simulator: - # Use interactive_get_pin - self.client.ui.get_pin = MethodType(interactive_get_pin, self.client.ui) - - if self.client.features.initialized: - raise DeviceAlreadyInitError( - "Device is already initialized. Use wipe first and try again" - ) - device.reset(self.client, passphrase_protection=bool(self.password)) - return True - - @trezor_exception - def wipe_device(self) -> bool: - self._check_unlocked() - device.wipe(self.client) - return True - - @trezor_exception - def restore_device(self, label: str = "", word_count: int = 24) -> bool: - self._prepare_device() - if not self.simulator: - # Use interactive_get_pin - self.client.ui.get_pin = MethodType(interactive_get_pin, self.client.ui) - - device.recover( - self.client, - word_count=word_count, - label=label, - input_callback=mnemonic_words(), - passphrase_protection=bool(self.password), - ) - return True - - def backup_device(self, label: str = "", passphrase: str = "") -> bool: - """ - Trezor devices do not support backing up via software. - - :raises UnavailableActionError: Always, this function is unavailable - """ - raise UnavailableActionError( - "The {} does not support creating a backup via software".format(self.type) - ) - - @trezor_exception - def close(self) -> None: - self.client.close() - - @trezor_exception - def prompt_pin(self) -> bool: - self.coin_name = "Bitcoin" if self.chain == Chain.MAIN else "Testnet" - self.client.open() - self._prepare_device() - if not self.client.features.pin_protection: - raise DeviceAlreadyUnlockedError("This device does not need a PIN") - if self.client.features.unlocked: - raise DeviceAlreadyUnlockedError( - "The PIN has already been sent to this device" - ) - print( - "Use 'sendpin' to provide the number positions for the PIN as displayed on your device's screen", - file=sys.stderr, - ) - print(PIN_MATRIX_DESCRIPTION, file=sys.stderr) - self.client.call_raw( - messages.GetPublicKey( - address_n=[0x8000002C, 0x80000001, 0x80000000], - ecdsa_curve_name=None, - show_display=False, - coin_name=self.coin_name, - script_type=messages.InputScriptType.SPENDADDRESS, - ) - ) - return True - - @trezor_exception - def send_pin(self, pin: str) -> bool: - self.client.open() - if not pin.isdigit(): - raise BadArgumentError("Non-numeric PIN provided") - resp = self.client.call_raw(messages.PinMatrixAck(pin=pin)) - if isinstance(resp, messages.Failure): - self.client.features = self.client.call_raw(messages.GetFeatures()) - if isinstance(self.client.features, messages.Features): - if not self.client.features.pin_protection: - raise DeviceAlreadyUnlockedError("This device does not need a PIN") - if self.client.features.unlocked: - raise DeviceAlreadyUnlockedError( - "The PIN has already been sent to this device" - ) - return False - elif isinstance(resp, messages.PassphraseRequest): - pass_resp = self.client.call_raw( - messages.PassphraseAck( - passphrase=self.client.ui.get_passphrase(available_on_device=False), - on_device=False, - ) - ) - if isinstance(pass_resp, messages.Deprecated_PassphraseStateRequest): - self.client.call_raw(messages.Deprecated_PassphraseStateAck()) - return True - - @trezor_exception - def toggle_passphrase(self) -> bool: - self._check_unlocked() - try: - device.apply_settings( - self.client, - use_passphrase=not self.client.features.passphrase_protection, - ) - except Exception: - if self.type == "Keepkey": - print("Confirm the action by entering your PIN", file=sys.stderr) - print( - "Use 'sendpin' to provide the number positions for the PIN as displayed on your device's screen", - file=sys.stderr, - ) - print(PIN_MATRIX_DESCRIPTION, file=sys.stderr) - return True - - -def enumerate(password: str = "") -> List[Dict[str, Any]]: - results = [] - devs = hid.HidTransport.enumerate() - devs.extend(webusb.WebUsbTransport.enumerate()) - devs.extend(udp.UdpTransport.enumerate()) - for dev in devs: - d_data: Dict[str, Any] = {} - - d_data["type"] = "trezor" - d_data["path"] = dev.get_path() - - client = None - with handle_errors(common_err_msgs["enumerate"], d_data): - client = TrezorClient(d_data["path"], password) - try: - client._prepare_device() - except TypeError: - continue - if "trezor" not in client.client.features.vendor: - continue - - d_data["model"] = "trezor_" + client.client.features.model.lower() - if d_data["path"] == "udp:127.0.0.1:21324": - d_data["model"] += "_simulator" - - d_data["needs_pin_sent"] = ( - client.client.features.pin_protection - and not client.client.features.unlocked - ) - if client.client.features.model == "1": - d_data[ - "needs_passphrase_sent" - ] = ( - client.client.features.passphrase_protection - ) # always need the passphrase sent for Trezor One if it has passphrase protection enabled - else: - d_data["needs_passphrase_sent"] = False - if d_data["needs_pin_sent"]: - raise DeviceNotReadyError( - "Trezor is locked. Unlock by using 'promptpin' and then 'sendpin'." - ) - if d_data["needs_passphrase_sent"] and not password: - raise DeviceNotReadyError( - "Passphrase needs to be specified before the fingerprint information can be retrieved" - ) - if client.client.features.initialized: - d_data["fingerprint"] = client.get_master_fingerprint().hex() - d_data[ - "needs_passphrase_sent" - ] = False # Passphrase is always needed for the above to have worked, so it's already sent - else: - d_data["error"] = "Not initialized" - d_data["code"] = DEVICE_NOT_INITIALIZED - - if client: - client.close() - - results.append(d_data) - return results diff --git a/src/cryptoadvance/specter/devices/keepkey.py b/src/cryptoadvance/specter/devices/keepkey.py index 3152528591..af5335df79 100644 --- a/src/cryptoadvance/specter/devices/keepkey.py +++ b/src/cryptoadvance/specter/devices/keepkey.py @@ -1,6 +1,6 @@ from . import DeviceTypes from .hwi_device import HWIDevice -from .hwi.keepkey import KeepkeyClient +from hwilib.devices.keepkey import KeepkeyClient class Keepkey(HWIDevice): diff --git a/src/cryptoadvance/specter/devices/trezor.py b/src/cryptoadvance/specter/devices/trezor.py index 7f440b9b52..dd8071dfd0 100644 --- a/src/cryptoadvance/specter/devices/trezor.py +++ b/src/cryptoadvance/specter/devices/trezor.py @@ -1,6 +1,6 @@ from . import DeviceTypes from .hwi_device import HWIDevice -from .hwi.trezor import TrezorClient +from hwilib.devices.trezor import TrezorClient class Trezor(HWIDevice): diff --git a/src/cryptoadvance/specter/hwi_rpc.py b/src/cryptoadvance/specter/hwi_rpc.py index c0cde4e9d1..aac286a0a6 100644 --- a/src/cryptoadvance/specter/hwi_rpc.py +++ b/src/cryptoadvance/specter/hwi_rpc.py @@ -22,6 +22,7 @@ is_liquid, ) from hwilib.devices.bitbox02 import Bitbox02Client +from hwilib.devices.trezorlib.transport import get_transport from .devices.hwi.specter_diy import SpecterClient from .devices.hwi.jade import JadeClient @@ -196,6 +197,7 @@ def send_pin(self, pin="", device_type=None, path=None, passphrase="", chain="") with self._get_client( device_type=device_type, path=path, passphrase=passphrase, chain=chain ) as client: + logger.debug(f"client is : {client}") return hwi_commands.send_pin(client, pin) else: raise Exception( @@ -461,8 +463,10 @@ def _extract_xpubs_from_client(self, client, account=0): ).to_string() zpub = convert_xpub_prefix(xpub, b"\x04\xb2\x47\x46") xpubs += "[{}/84'/0'/{}']{}\n".format(master_fpr, account, zpub) - except Exception: - logger.warning("Failed to import native Segwit singlesig mainnet key.") + except Exception as e: + logger.warning( + f"Failed to import native Segwit singlesig mainnet key: {e}" + ) try: # Multisig nested Segwit @@ -471,8 +475,10 @@ def _extract_xpubs_from_client(self, client, account=0): ).to_string() Ypub = convert_xpub_prefix(xpub, b"\x02\x95\xb4\x3f") xpubs += "[{}/48'/0'/{}'/1']{}\n".format(master_fpr, account, Ypub) - except Exception: - logger.warning("Failed to import Nested Segwit multisig mainnet key.") + except Exception as e: + logger.warning( + f"Failed to import Nested Segwit multisig mainnet key: {e}" + ) try: # Multisig native Segwit @@ -481,8 +487,10 @@ def _extract_xpubs_from_client(self, client, account=0): ).to_string() Zpub = convert_xpub_prefix(xpub, b"\x02\xaa\x7e\xd3") xpubs += "[{}/48'/0'/{}'/2']{}\n".format(master_fpr, account, Zpub) - except Exception: - logger.warning("Failed to import native Segwit multisig mainnet key.") + except Exception as e: + logger.warning( + f"Failed to import native Segwit multisig mainnet key {e}" + ) # And testnet client.chain = Chain.TEST @@ -494,8 +502,10 @@ def _extract_xpubs_from_client(self, client, account=0): ).to_string() upub = convert_xpub_prefix(xpub, b"\x04\x4a\x52\x62") xpubs += "[{}/49'/1'/{}']{}\n".format(master_fpr, account, upub) - except Exception: - logger.warning("Failed to import Nested Segwit singlesig testnet key.") + except Exception as e: + logger.warning( + f"Failed to import Nested Segwit singlesig testnet key: {e}" + ) try: # Testnet native Segwit @@ -504,8 +514,10 @@ def _extract_xpubs_from_client(self, client, account=0): ).to_string() vpub = convert_xpub_prefix(xpub, b"\x04\x5f\x1c\xf6") xpubs += "[{}/84'/1'/{}']{}\n".format(master_fpr, account, vpub) - except Exception: - logger.warning("Failed to import native Segwit singlesig testnet key.") + except Exception as e: + logger.warning( + f"Failed to import native Segwit singlesig testnet key: {e}" + ) try: # Testnet multisig nested Segwit @@ -514,9 +526,9 @@ def _extract_xpubs_from_client(self, client, account=0): ).to_string() Upub = convert_xpub_prefix(xpub, b"\x02\x42\x89\xef") xpubs += "[{}/48'/1'/{}'/1']{}\n".format(master_fpr, account, Upub) - except Exception: + except Exception as e: logger.warning( - "Failed to import Nested Segwit multisigsig testnet key." + f"Failed to import Nested Segwit multisigsig testnet key: {e}" ) try: @@ -526,8 +538,10 @@ def _extract_xpubs_from_client(self, client, account=0): ).to_string() Vpub = convert_xpub_prefix(xpub, b"\x02\x57\x54\x83") xpubs += "[{}/48'/1'/{}'/2']{}\n".format(master_fpr, account, Vpub) - except Exception: - logger.warning("Failed to import native Segwit multisig testnet key.") + except Exception as e: + logger.warning( + f"Failed to import native Segwit multisig testnet key: {e}" + ) # Do proper cleanup otherwise have to reconnect device to access again client.close() diff --git a/tests/test_hwi_rpc.py b/tests/test_hwi_rpc.py new file mode 100644 index 0000000000..3869f6d42d --- /dev/null +++ b/tests/test_hwi_rpc.py @@ -0,0 +1,88 @@ +""" This is just a manual test to understand how HWI works. All tests are marked skipped as hardware plugged in is necessary. + Don't take this as best practise. This is just something to test difference in behaviour for migration from HWI 2.0.2 to 2.1.0 + + To get it to run: + * comment the test: @pytest.mark.skip() + * Plugin yout trezor + * Run the test like: pytest tests/test_hwi_rpc.py::test_enumerate_trezor -vv -s + * Type in your Pin + * Success! +""" + +import logging +import io +import pytest +from cryptoadvance.specter.hwi_rpc import HWIBridge +from cryptoadvance.specter.key import Key +from cryptoadvance.specter.util.descriptor import Descriptor + + +@pytest.mark.skip() +def test_trezor(caplog, monkeypatch): + """In order to get this test working, you have to run it with "-s": + pytest tests/test_hwi_rpc.py::test_enumerate_trezor -vv -s + """ + caplog.set_level(logging.DEBUG) + + hwi = HWIBridge() + # bla = hwi.detect_device() + + res = hwi.enumerate(passphrase="")[0] + print(res) + # seems to be normal + assert res["type"] == "trezor" + assert res["model"] == "trezor_1" + assert res["path"].startswith("webusb:003:1:1:") + + if res["needs_pin_sent"]: + assert res["error"].startswith( + "Could not open client or get fingerprint information: Trezor is locked" + ) + res = hwi.prompt_pin(device_type="trezor", passphrase="") + assert res["success"] == True + # monkeypatch.setattr('sys.stdin', io.StringIO('my input')) + pin = input("Enter pin: ") + + res = hwi.send_pin(pin, device_type="trezor", passphrase="") + assert res["success"] == True + + else: + assert res["error"].startswith( + "Could not open client or get fingerprint information: Passphrase needs to be specified before the fingerprint information can be retrieved" + ) + assert len(res["fingerprint"]) == 8 + results = hwi.extract_xpubs(chain="test", device_type="trezor").split("\n") + assert len(results) == 9 + assert results[0].startswith("[") + print(results[0]) + # You can construct keys from the results: + key: Key = Key.parse_xpub(results[0]) + assert len(key.fingerprint) == 8 + assert key.derivation == "m/49h/0h/0h" + assert key.xpub.startswith("xpub") + + +@pytest.mark.skip() +def test_jade(caplog): + caplog.set_level(logging.DEBUG) + + hwi = HWIBridge() + # bla = hwi.detect_device() + + res = hwi.enumerate(passphrase="")[0] + + # seems to be normal + assert res["type"] == "jade" + assert res["model"] == "jade" + assert res["path"] == "/dev/ttyUSB0" + assert res["error"].startswith( + "Could not open client or get fingerprint information: __init__() got an unexpected keyword argument 'timeout'" + ) + assert res["code"] == -13 + assert res["fingerprint"] == "4c6de3ce" + + results = hwi.extract_xpubs( + chain="main", device_type="jade", path="/dev/ttyUSB0" + ).split("\n") + assert len(results) == 9 + assert results[0].startswith("[")