Skip to content

Commit

Permalink
Merge #729: Add --emulators option to explicitly choose to detect emu…
Browse files Browse the repository at this point in the history
…lators in enumeration and device auto detection

ea7310f Bump version to 3.0.0 (Ava Chow)
27c1b42 Add --emulators option and skip enumerating emulators by default (Ava Chow)

Pull request description:

  This changes the default behavior to ignore emulators unless specified with `--emulators`. Since this is a backwards incompatible change, the major version number is bumped to 3.

  Closes #653

Top commit has no ACKs.

Tree-SHA512: 373dbf21e813471c58262cd8e27f1c33f63d42db783ff0f567d223b5e35589e116f3c1029c13befddb741555da41940f97bafd443fc135ad0de0b7e7086749f7
  • Loading branch information
achow101 committed Apr 1, 2024
2 parents cfaf26c + ea7310f commit b5786ad
Show file tree
Hide file tree
Showing 18 changed files with 98 additions and 78 deletions.
2 changes: 1 addition & 1 deletion hwilib/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "2.4.0"
__version__ = "3.0.0-dev"
7 changes: 4 additions & 3 deletions hwilib/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def displayaddress_handler(args: argparse.Namespace, client: HardwareWalletClien
return displayaddress(client, desc=args.desc, path=args.path, addr_type=args.addr_type)

def enumerate_handler(args: argparse.Namespace) -> List[Dict[str, Any]]:
return enumerate(password=args.password, expert=args.expert, chain=args.chain)
return enumerate(password=args.password, expert=args.expert, chain=args.chain, allow_emulators=args.allow_emulators)

def getmasterxpub_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, str]:
return getmasterxpub(client, addrtype=args.addr_type, account=args.account)
Expand Down Expand Up @@ -145,6 +145,7 @@ def get_parser() -> HWIArgumentParser:
parser.add_argument('--stdin', help='Enter commands and arguments via stdin', action='store_true')
parser.add_argument('--interactive', '-i', help='Use some commands interactively. Currently required for all device configuration commands', action='store_true')
parser.add_argument('--expert', help='Do advanced things and get more detailed information returned from some commands. Use at your own risk.', action='store_true')
parser.add_argument("--emulators", help="Enable enumeration and detection of device emulators", action="store_true", dest="allow_emulators")

subparsers = parser.add_subparsers(description='Commands', dest='command')
# work-around to make subparser required
Expand Down Expand Up @@ -277,9 +278,9 @@ def process_commands(cli_args: List[str]) -> Any:

# Auto detect if we are using fingerprint or type to identify device
if args.fingerprint or (args.device_type and not args.device_path):
client = find_device(args.password, args.device_type, args.fingerprint, args.expert, args.chain)
client = find_device(args.password, args.device_type, args.fingerprint, args.expert, args.chain, args.allow_emulators)
if not client:
return {'error': 'Could not find device with specified fingerprint', 'code': DEVICE_CONN_ERROR}
return {'error': 'Could not find device with specified fingerprint or type', 'code': DEVICE_CONN_ERROR}
elif args.device_type and args.device_path:
with handle_errors(result=result, code=DEVICE_CONN_ERROR):
client = get_client(device_type, device_path, password, args.expert, args.chain)
Expand Down
8 changes: 5 additions & 3 deletions hwilib/_gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ def attestation_check(self, result: bool) -> None:
pass

class HWIQt(QMainWindow):
def __init__(self, passphrase=None, chain=Chain.MAIN):
def __init__(self, passphrase=None, chain=Chain.MAIN, allow_emulators: bool = False):
super(HWIQt, self).__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
Expand All @@ -343,6 +343,7 @@ def __init__(self, passphrase=None, chain=Chain.MAIN):
'path': None,
'account_used': True
}
self.allow_emulators = allow_emulators

self.ui.enumerate_refresh_button.clicked.connect(self.refresh_clicked)
self.ui.setpass_button.clicked.connect(self.show_setpassphrasedialog)
Expand Down Expand Up @@ -372,7 +373,7 @@ def refresh_clicked(self):
self.client.close()
self.client = None

self.devices = commands.enumerate(self.passphrase)
self.devices = commands.enumerate(password=self.passphrase, expert=False, chain=self.chain, allow_emulators=self.allow_emulators)
self.ui.enumerate_combobox.currentIndexChanged.disconnect()
self.ui.enumerate_combobox.clear()
self.ui.enumerate_combobox.addItem('')
Expand Down Expand Up @@ -524,6 +525,7 @@ def process_gui_commands(cli_args):
parser.add_argument('--chain', help='Select chain to work with', type=Chain.argparse, choices=list(Chain), default=Chain.MAIN)
parser.add_argument('--debug', help='Print debug statements', action='store_true')
parser.add_argument('--version', action='version', version='%(prog)s {}'.format(__version__))
parser.add_argument("--emulators", help="Enable enumeration and detection of device emulators", action="store_true", dest="allow_emulators")

# Parse arguments again for anything entered over stdin
args = parser.parse_args(cli_args)
Expand All @@ -536,7 +538,7 @@ def process_gui_commands(cli_args):
# Qt setup
app = QApplication()

window = HWIQt(args.password, args.chain)
window = HWIQt(args.password, args.chain, args.allow_emulators)

window.refresh_clicked()

Expand Down
7 changes: 4 additions & 3 deletions hwilib/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def get_client(device_type: str, device_path: str, password: Optional[str] = Non
return client

# Get a list of all available hardware wallets
def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN) -> List[Dict[str, Any]]:
def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN, allow_emulators: bool = False) -> List[Dict[str, Any]]:
"""
Enumerate all of the devices that HWI can potentially access.
Expand All @@ -114,7 +114,7 @@ def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain
for module in all_devs:
try:
imported_dev = importlib.import_module('.devices.' + module, __package__)
result.extend(imported_dev.enumerate(password, expert, chain))
result.extend(imported_dev.enumerate(password, expert, chain, allow_emulators))
except ImportError as e:
# Warn for ImportErrors, but largely ignore them to allow users not install
# all device dependencies if only one or some devices are wanted.
Expand All @@ -129,6 +129,7 @@ def find_device(
fingerprint: Optional[str] = None,
expert: bool = False,
chain: Chain = Chain.MAIN,
allow_emulators: bool = False,
) -> Optional[HardwareWalletClient]:
"""
Find a device from the device type or fingerprint and get a client to access it.
Expand All @@ -145,7 +146,7 @@ def find_device(
:return: A client to interact with the found device
"""

devices = enumerate(password)
devices = enumerate(password, expert, chain, allow_emulators)
for d in devices:
if device_type is not None and d['type'] != device_type and d['model'] != device_type:
continue
Expand Down
2 changes: 1 addition & 1 deletion hwilib/devices/bitbox02.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ def _xpubs_equal_ignoring_version(xpub1: bytes, xpub2: bytes) -> bool:
return xpub1[4:] == xpub2[4:]


def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN) -> List[Dict[str, Any]]:
def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN, allow_emulators: bool = False) -> List[Dict[str, Any]]:
"""
Enumerate all BitBox02 devices. Bootloaders excluded.
"""
Expand Down
5 changes: 3 additions & 2 deletions hwilib/devices/coldcard.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,10 +399,11 @@ def can_sign_taproot(self) -> bool:
return False


def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN) -> List[Dict[str, Any]]:
def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN, allow_emulators: bool = True) -> List[Dict[str, Any]]:
results = []
devices = hid.enumerate(COINKITE_VID, CKCC_PID)
devices.append({'path': CC_SIMULATOR_SOCK.encode()})
if allow_emulators:
devices.append({'path': CC_SIMULATOR_SOCK.encode()})
for d in devices:
d_data: Dict[str, Any] = {}

Expand Down
17 changes: 9 additions & 8 deletions hwilib/devices/digitalbitbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -679,17 +679,18 @@ def can_sign_taproot(self) -> bool:
return False


def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN) -> List[Dict[str, Any]]:
def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN, allow_emulators: bool = False) -> List[Dict[str, Any]]:
results = []
devices = hid.enumerate(DBB_VENDOR_ID, DBB_DEVICE_ID)
# Try connecting to simulator
try:
dev = BitboxSimulator('127.0.0.1', 35345)
dev.send_recv(b'{"device" : "info"}')
devices.append({'path': b'udp:127.0.0.1:35345', 'interface_number': 0})
dev.close()
except Exception:
pass
if allow_emulators:
try:
dev = BitboxSimulator('127.0.0.1', 35345)
dev.send_recv(b'{"device" : "info"}')
devices.append({'path': b'udp:127.0.0.1:35345', 'interface_number': 0})
dev.close()
except Exception:
pass
for d in devices:
if ('interface_number' in d and d['interface_number'] == 0
or ('usage_page' in d and d['usage_page'] == 0xffff)):
Expand Down
21 changes: 11 additions & 10 deletions hwilib/devices/jade.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ def can_sign_taproot(self) -> bool:
return False


def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN) -> List[Dict[str, Any]]:
def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN, allow_emulators: bool = False) -> List[Dict[str, Any]]:
results = []

def _get_device_entry(device_model: str, device_path: str) -> Dict[str, Any]:
Expand Down Expand Up @@ -537,16 +537,17 @@ def _get_device_entry(device_model: str, device_path: str) -> Dict[str, Any]:
results.append(_get_device_entry('jade', devinfo.device))

# If we can connect to the simulator, add it too
try:
with JadeAPI.create_serial(SIMULATOR_PATH, timeout=1) as jade:
verinfo = jade.get_version_info()
if allow_emulators:
try:
with JadeAPI.create_serial(SIMULATOR_PATH, timeout=1) as jade:
verinfo = jade.get_version_info()

if verinfo is not None:
results.append(_get_device_entry('jade_simulator', SIMULATOR_PATH))
if verinfo is not None:
results.append(_get_device_entry('jade_simulator', SIMULATOR_PATH))

except Exception as e:
# If we get any sort of error do not add the simulator
logging.debug(f'Failed to connect to Jade simulator at {SIMULATOR_PATH}')
logging.debug(e)
except Exception as e:
# If we get any sort of error do not add the simulator
logging.debug(f'Failed to connect to Jade simulator at {SIMULATOR_PATH}')
logging.debug(e)

return results
5 changes: 3 additions & 2 deletions hwilib/devices/keepkey.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,11 +172,12 @@ def can_sign_taproot(self) -> bool:
return False


def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN) -> List[Dict[str, Any]]:
def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN, allow_emulators: bool = False) -> 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(KEEPKEY_SIMULATOR_PATH))
if allow_emulators:
devs.extend(udp.UdpTransport.enumerate(KEEPKEY_SIMULATOR_PATH))
for dev in devs:
d_data: Dict[str, Any] = {}

Expand Down
5 changes: 3 additions & 2 deletions hwilib/devices/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -546,11 +546,12 @@ def can_sign_taproot(self) -> bool:
return isinstance(self.client, NewClient)


def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN) -> List[Dict[str, Any]]:
def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN, allow_emulators: bool = False) -> List[Dict[str, Any]]:
results = []
devices = []
devices.extend(hid.enumerate(LEDGER_VENDOR_ID, 0))
devices.append({'path': SIMULATOR_PATH.encode(), 'interface_number': 0, 'product_id': 0x1000})
if allow_emulators:
devices.append({'path': SIMULATOR_PATH.encode(), 'interface_number': 0, 'product_id': 0x1000})

for d in devices:
if ('interface_number' in d and d['interface_number'] == 0
Expand Down
5 changes: 3 additions & 2 deletions hwilib/devices/trezor.py
Original file line number Diff line number Diff line change
Expand Up @@ -851,11 +851,12 @@ def can_sign_taproot(self) -> bool:
return True


def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN) -> List[Dict[str, Any]]:
def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN, allow_emulators: bool = False) -> List[Dict[str, Any]]:
results = []
devs = hid.HidTransport.enumerate()
devs.extend(webusb.WebUsbTransport.enumerate())
devs.extend(udp.UdpTransport.enumerate())
if allow_emulators:
devs.extend(udp.UdpTransport.enumerate())
for dev in devs:
d_data: Dict[str, Any] = {}

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "hwi"
version = "2.4.0"
version = "3.0.0-dev"
description = "A library for working with Bitcoin hardware wallets"
authors = ["Ava Chow <me@achow101.com>"]
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@

setup_kwargs = {
'name': 'hwi',
'version': '2.4.0',
'version': '3.0.0.dev0',
'description': 'A library for working with Bitcoin hardware wallets',
'long_description': "# Bitcoin Hardware Wallet Interface\n\n[![Build Status](https://api.cirrus-ci.com/github/bitcoin-core/HWI.svg)](https://cirrus-ci.com/github/bitcoin-core/HWI)\n[![Documentation Status](https://readthedocs.org/projects/hwi/badge/?version=latest)](https://hwi.readthedocs.io/en/latest/?badge=latest)\n\nThe Bitcoin Hardware Wallet Interface is a Python library and command line tool for interacting with hardware wallets.\nIt provides a standard way for software to work with hardware wallets without needing to implement device specific drivers.\nPython software can use the provided library (`hwilib`). Software in other languages can execute the `hwi` tool.\n\nCaveat emptor: Inclusion of a specific hardware wallet vendor does not imply any endorsement of quality or security.\n\n## Prerequisites\n\nPython 3 is required. The libraries and [udev rules](hwilib/udev/README.md) for each device must also be installed. Some libraries will need to be installed\n\nFor Ubuntu/Debian:\n```\nsudo apt install libusb-1.0-0-dev libudev-dev python3-dev\n```\n\nFor Centos:\n```\nsudo yum -y install python3-devel libusbx-devel systemd-devel\n```\n\nFor macOS:\n```\nbrew install libusb\n```\n\n## Install\n\n```\ngit clone https://github.com/bitcoin-core/HWI.git\ncd HWI\npoetry install # or 'pip3 install .' or 'python3 setup.py install'\n```\n\nThis project uses the [Poetry](https://github.com/sdispater/poetry) dependency manager. HWI and its dependencies can be installed via poetry by executing the following in the root source directory:\n\n```\npoetry install\n```\n\nPip can also be used to automatically install HWI and its dependencies using the `setup.py` file (which is usually in sync with `pyproject.toml`):\n\n```\npip3 install .\n```\n\nThe `setup.py` file can be used to install HWI and its dependencies so long as `setuptools` is also installed:\n\n```\npip3 install -U setuptools\npython3 setup.py install\n```\n\n## Dependencies\n\nSee `pyproject.toml` for all dependencies. Dependencies under `[tool.poetry.dependencies]` are user dependencies, and `[tool.poetry.dev-dependencies]` for development based dependencies. These dependencies will be installed with any of the three above installation methods.\n\n## Usage\n\nTo use, first enumerate all devices and find the one that you want to use with\n\n```\n./hwi.py enumerate\n```\n\nOnce the device type and device path are known, issue commands to it like so:\n\n```\n./hwi.py -t <type> -d <path> <command> <command args>\n```\n\nAll output will be in JSON form and sent to `stdout`.\nAdditional information or prompts will be sent to `stderr` and will not necessarily be in JSON.\nThis additional information is for debugging purposes.\n\nTo see a complete list of available commands and global parameters, run\n`./hwi.py --help`. To see options specific to a particular command,\npass the `--help` parameter after the command name; for example:\n\n```\n./hwi.py getdescriptors --help\n```\n\n## Documentation\n\nDocumentation for HWI can be found on [readthedocs.io](https://hwi.readthedocs.io/).\n\n### Device Support\n\nFor documentation on devices supported and how they are supported, please check the [device support page](https://hwi.readthedocs.io/en/latest/devices/index.html#support-matrix)\n\n### Using with Bitcoin Core\n\nSee [Using Bitcoin Core with Hardware Wallets](https://hwi.readthedocs.io/en/latest/examples/bitcoin-core-usage.html).\n\n## License\n\nThis project is available under the MIT License, Copyright Andrew Chow.\n",
'author': 'Ava Chow',
Expand Down
2 changes: 1 addition & 1 deletion test/test_coldcard.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def start(self):
# Wait for simulator to be up
while True:
try:
enum_res = process_commands(["enumerate"])
enum_res = process_commands(["--emulators", "enumerate"])
found = False
for dev in enum_res:
if dev["type"] == "coldcard" and "error" not in dev:
Expand Down
Loading

0 comments on commit b5786ad

Please sign in to comment.