Skip to content

Commit

Permalink
Add --emulators option and skip enumerating emulators by default
Browse files Browse the repository at this point in the history
  • Loading branch information
achow101 committed Mar 28, 2024
1 parent 8d4fa19 commit 86abdf5
Show file tree
Hide file tree
Showing 12 changed files with 65 additions and 47 deletions.
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
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 @@ -171,11 +171,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
26 changes: 18 additions & 8 deletions test/test_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ def __init__(self, bitcoind, emulator=None, interface='library', methodName='run
self.rpc = bitcoind.rpc
self.emulator = emulator

self.dev_args = ['-t', self.emulator.type, '-d', self.emulator.path, '--chain', 'test']
self.dev_args = ['-t', self.emulator.type, '-d', self.emulator.path, '--chain', 'test', "--emulators"]
if self.emulator.password is not None:
self.dev_args.extend(['-p', self.emulator.password])

Expand Down Expand Up @@ -215,32 +215,42 @@ def test_enumerate(self):
found = True
self.assertTrue(found)

def test_no_emus(self):
res = self.do_command(self.get_password_args() + ["enumerate"])
self.assertEqual(len(res), 0)
res = self.do_command(self.get_password_args() + ["-f", self.emulator.fingerprint, "--chain", "test", "getmasterxpub", "--addr-type", "legacy"])
self.assertEqual(res['error'], 'Could not find device with specified fingerprint or type')
self.assertEqual(res['code'], -3)
res = self.do_command(self.get_password_args() + ["-t", self.detect_type, "--chain", "test", "getmasterxpub", "--addr-type", "legacy"])
self.assertEqual(res['error'], 'Could not find device with specified fingerprint or type')
self.assertEqual(res['code'], -3)

def test_no_type(self):
gmxp_res = self.do_command(["--chain", "test", 'getmasterxpub', "--addr-type", "legacy"])
gmxp_res = self.do_command(["--chain", "test", "--emulators", "getmasterxpub", "--addr-type", "legacy"])
self.assertIn('error', gmxp_res)
self.assertEqual(gmxp_res['error'], 'You must specify a device type or fingerprint for all commands except enumerate')
self.assertIn('code', gmxp_res)
self.assertEqual(gmxp_res['code'], -1)

def test_path_type(self):
gmxp_res = self.do_command(self.get_password_args() + ['-t', self.detect_type, '-d', self.emulator.path, "--chain", "test", 'getmasterxpub', "--addr-type", "legacy"])
gmxp_res = self.do_command(self.get_password_args() + ["-t", self.detect_type, "-d", self.emulator.path, "--chain", "test", "--emulators", "getmasterxpub", "--addr-type", "legacy"])
self.assertEqual(gmxp_res['xpub'], self.emulator.master_xpub)

def test_fingerprint_autodetect(self):
gmxp_res = self.do_command(self.get_password_args() + ['-f', self.emulator.fingerprint, "--chain", "test", 'getmasterxpub', "--addr-type", "legacy"])
gmxp_res = self.do_command(self.get_password_args() + ["-f", self.emulator.fingerprint, "--chain", "test", "--emulators", "getmasterxpub", "--addr-type", "legacy"])
self.assertEqual(gmxp_res['xpub'], self.emulator.master_xpub)

# Nonexistent fingerprint
gmxp_res = self.do_command(self.get_password_args() + ['-f', '0000ffff', "--chain", "test", 'getmasterxpub', "--addr-type", "legacy"])
self.assertEqual(gmxp_res['error'], 'Could not find device with specified fingerprint')
gmxp_res = self.do_command(self.get_password_args() + ["-f", "0000ffff", "--chain", "test", "--emulators", "getmasterxpub", "--addr-type", "legacy"])
self.assertEqual(gmxp_res['error'], 'Could not find device with specified fingerprint or type')
self.assertEqual(gmxp_res['code'], -3)

def test_type_only_autodetect(self):
gmxp_res = self.do_command(self.get_password_args() + ['-t', self.detect_type, "--chain", "test", 'getmasterxpub', "--addr-type", "legacy"])
gmxp_res = self.do_command(self.get_password_args() + ["-t", self.detect_type, "--chain", "test", "--emulators", "getmasterxpub", "--addr-type", "legacy"])
self.assertEqual(gmxp_res['xpub'], self.emulator.master_xpub)

# Unknown device type
gmxp_res = self.do_command(['-t', 'fakedev', '-d', 'fakepath', "--chain", "test", 'getmasterxpub', "--addr-type", "legacy"])
gmxp_res = self.do_command(["-t", "fakedev", "-d", "fakepath", "--chain", "test", "--emulators", "getmasterxpub", "--addr-type", "legacy"])
self.assertEqual(gmxp_res['error'], 'Unknown device type specified')
self.assertEqual(gmxp_res['code'], -4)

Expand Down
6 changes: 3 additions & 3 deletions test/test_keepkey.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,16 +180,16 @@ def test_getxpub(self):
load_device_by_mnemonic(client=self.client, mnemonic=vec['mnemonic'], pin='', passphrase_protection=False, label='test', language='english')

# Test getmasterxpub
gmxp_res = self.do_command(['-t', 'keepkey', '-d', 'udp:127.0.0.1:11044', 'getmasterxpub', "--addr-type", "legacy"])
gmxp_res = self.do_command(["-t", "keepkey", "-d", "udp:127.0.0.1:11044", "--emulators", "getmasterxpub", "--addr-type", "legacy"])
self.assertEqual(gmxp_res['xpub'], vec['master_xpub'])

# Test the path derivs
for path_vec in vec['vectors']:
gxp_res = self.do_command(['-t', 'keepkey', '-d', 'udp:127.0.0.1:11044', 'getxpub', path_vec['path']])
gxp_res = self.do_command(["-t", "keepkey", "-d", "udp:127.0.0.1:11044", "--emulators", "getxpub", path_vec["path"]])
self.assertEqual(gxp_res['xpub'], path_vec['xpub'])

def test_expert_getxpub(self):
result = self.do_command(['-t', 'keepkey', '-d', 'udp:127.0.0.1:11044', '--expert', 'getxpub', 'm/44h/0h/0h/3'])
result = self.do_command(["-t", "keepkey", "-d", "udp:127.0.0.1:11044", "--expert", "--emulators", "getxpub", "m/44h/0h/0h/3"])
self.assertEqual(result['xpub'], 'xpub6FMafWAi3n3ET2rU5yQr16UhRD1Zx4dELmcEw3NaYeBaNnipcr2zjzYp1sNdwR3aTN37hxAqRWQ13AWUZr6L9jc617mU6EvgYXyBjXrEhgr')
self.assertFalse(result['testnet'])
self.assertFalse(result['private'])
Expand Down
6 changes: 3 additions & 3 deletions test/test_trezor.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,16 +175,16 @@ def test_getxpub(self):
load_device_by_mnemonic(client=self.client, mnemonic=vec['mnemonic'], pin='', passphrase_protection=False, label='test', language='english')

# Test getmasterxpub
gmxp_res = self.do_command(['-t', 'trezor', '-d', 'udp:127.0.0.1:21324', 'getmasterxpub', "--addr-type", "legacy"])
gmxp_res = self.do_command(["-t", "trezor", "-d", "udp:127.0.0.1:21324", "--emulators", "getmasterxpub", "--addr-type", "legacy"])
self.assertEqual(gmxp_res['xpub'], vec['master_xpub'])

# Test the path derivs
for path_vec in vec['vectors']:
gxp_res = self.do_command(['-t', 'trezor', '-d', 'udp:127.0.0.1:21324', 'getxpub', path_vec['path']])
gxp_res = self.do_command(["-t", "trezor", "-d", "udp:127.0.0.1:21324", "--emulators", "getxpub", path_vec["path"]])
self.assertEqual(gxp_res['xpub'], path_vec['xpub'])

def test_expert_getxpub(self):
result = self.do_command(['-t', 'trezor', '-d', 'udp:127.0.0.1:21324', '--expert', 'getxpub', 'm/44h/0h/0h/3'])
result = self.do_command(["-t", "trezor", "-d", "udp:127.0.0.1:21324", "--expert", "--emulators", "getxpub", "m/44h/0h/0h/3"])
self.assertEqual(result['xpub'], 'xpub6FMafWAi3n3ET2rU5yQr16UhRD1Zx4dELmcEw3NaYeBaNnipcr2zjzYp1sNdwR3aTN37hxAqRWQ13AWUZr6L9jc617mU6EvgYXyBjXrEhgr')
self.assertFalse(result['testnet'])
self.assertFalse(result['private'])
Expand Down

0 comments on commit 86abdf5

Please sign in to comment.