Compare commits
35 Commits
pr-devfind
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b722055c4 | ||
|
|
84315dd5de | ||
|
|
ef816628e1 | ||
|
|
6952edd879 | ||
|
|
9484803b71 | ||
|
|
d8cf9f120b | ||
|
|
4d21f29127 | ||
|
|
54e14ba301 | ||
|
|
1c33312bb1 | ||
|
|
bca85354f9 | ||
|
|
d2696cfadf | ||
|
|
a4a3aff89b | ||
|
|
cada7f5ce0 | ||
|
|
64153a8a65 | ||
|
|
1fb5eb42c7 | ||
|
|
54bd6687cc | ||
|
|
0974f6944c | ||
|
|
cea784ef60 | ||
|
|
70e31cad0c | ||
|
|
129e91d9f0 | ||
|
|
c14896c6eb | ||
|
|
6275418c90 | ||
|
|
67359835eb | ||
|
|
57a06836b9 | ||
|
|
c3f2521855 | ||
|
|
d0c561e516 | ||
|
|
85eaf70f36 | ||
|
|
4dd56fda3c | ||
|
|
ae9bca3ae3 | ||
|
|
7201300e92 | ||
|
|
892b552bb3 | ||
|
|
0c831e4f75 | ||
|
|
90f5e32cc3 | ||
|
|
483b589eeb | ||
|
|
d00ae56eef |
@ -40,7 +40,7 @@ addons:
|
|||||||
- cython3
|
- cython3
|
||||||
- ccache
|
- ccache
|
||||||
install:
|
install:
|
||||||
- pip install pipenv pysdl2 python-bitcoinrpc protobuf poetry
|
- pip install pipenv pysdl2 python-bitcoinrpc protobuf poetry==0.12.12
|
||||||
# From trezor-mcu to get the correct protobuf version
|
# From trezor-mcu to get the correct protobuf version
|
||||||
- curl -LO "https://github.com/google/protobuf/releases/download/v3.4.0/protoc-3.4.0-linux-x86_64.zip"
|
- curl -LO "https://github.com/google/protobuf/releases/download/v3.4.0/protoc-3.4.0-linux-x86_64.zip"
|
||||||
- unzip "protoc-3.4.0-linux-x86_64.zip" -d protoc
|
- unzip "protoc-3.4.0-linux-x86_64.zip" -d protoc
|
||||||
@ -56,7 +56,7 @@ jobs:
|
|||||||
- name: With command line interface
|
- name: With command line interface
|
||||||
script: cd test; poetry run ./run_tests.py --interface=cli
|
script: cd test; poetry run ./run_tests.py --interface=cli
|
||||||
- name: With stdin interface
|
- name: With stdin interface
|
||||||
script: cd test; ./run_tests.py --interface=stdin
|
script: cd test; poetry run ./run_tests.py --interface=stdin
|
||||||
- name: With linux binary distribution command line interface
|
- name: With linux binary distribution command line interface
|
||||||
services: docker
|
services: docker
|
||||||
before_script:
|
before_script:
|
||||||
|
|||||||
32
README.md
32
README.md
@ -69,23 +69,23 @@ Please also see [docs](docs/) for additional information about each device.
|
|||||||
| Support Planned | Yes | Yes | Yes | Yes | Yes | Yes |
|
| Support Planned | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||||
| Implemented | Yes | Yes | Yes | Yes | Yes | Yes |
|
| Implemented | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||||
| xpub retrieval | Yes | Yes | Yes | Yes | Yes | Yes |
|
| xpub retrieval | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||||
| Message Signing | Yes | Yes | ? | Yes | Yes | Yes |
|
| Message Signing | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||||
| Device Setup | N/A | Yes | ? | Yes | Yes | N/A |
|
| Device Setup | N/A | Yes | Yes | Yes | Yes | N/A |
|
||||||
| Device Wipe | N/A | Yes | ? | Yes | Yes | N/A |
|
| Device Wipe | N/A | Yes | Yes | Yes | Yes | N/A |
|
||||||
| Device Recovery | N/A | Yes | ? | N/A | Yes | N/A |
|
| Device Recovery | N/A | Yes | Yes | N/A | Yes | N/A |
|
||||||
| Device Backup | N/A | N/A | ? | Yes | N/A | Yes |
|
| Device Backup | N/A | N/A | N/A | Yes | N/A | Yes |
|
||||||
| P2PKH Inputs | Yes | Yes | ? | Yes | Yes | Yes |
|
| P2PKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||||
| P2SH-P2WPKH Inputs | Yes | Yes | ? | Yes | Yes | Yes |
|
| P2SH-P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||||
| P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes |
|
| P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||||
| P2SH Multisig Inputs | Yes | Yes | ? | Yes | Yes | N/A |
|
| P2SH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | N/A |
|
||||||
| P2SH-P2WSH Multisig Inputs | Yes | No | ? | Yes | No | N/A |
|
| P2SH-P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | No | N/A |
|
||||||
| P2WSH Multisig Inputs | Yes | No | ? | Yes | Yes | N/A |
|
| P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | N/A |
|
||||||
| Bare Multisig Inputs | Yes | N/A | ? | Yes | N/A | N/A |
|
| Bare Multisig Inputs | Yes | N/A | N/A | Yes | N/A | N/A |
|
||||||
| Aribtrary scriptPubKey Inputs | Yes | N/A | ? | Yes | N/A | N/A |
|
| Aribtrary scriptPubKey Inputs | Yes | N/A | N/A | Yes | N/A | N/A |
|
||||||
| Aribtrary redeemScript Inputs | Yes | N/A | ? | Yes | N/A | N/A |
|
| Aribtrary redeemScript Inputs | Yes | N/A | N/A | Yes | N/A | N/A |
|
||||||
| Arbitrary witnessScript Inputs | Yes | N/A | ? | Yes | N/A | N/A |
|
| Arbitrary witnessScript Inputs | Yes | N/A | N/A | Yes | N/A | N/A |
|
||||||
| Non-wallet inputs | Yes | Yes | ? | Yes | Yes | Yes |
|
| Non-wallet inputs | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||||
| Mixed Segwit and Non-Segwit Inputs | N/A | Yes | ? | Yes | Yes | Yes |
|
| Mixed Segwit and Non-Segwit Inputs | N/A | Yes | N/A | Yes | Yes | Yes |
|
||||||
| Display on device screen | Yes | Yes | Yes | N/A | Yes | Yes |
|
| Display on device screen | Yes | Yes | Yes | N/A | Yes | Yes |
|
||||||
|
|
||||||
## Using with Bitcoin Core
|
## Using with Bitcoin Core
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
eval "$(pyenv init -)"
|
eval "$(pyenv init -)"
|
||||||
eval "$(pyenv virtualenv-init -)"
|
eval "$(pyenv virtualenv-init -)"
|
||||||
pip install -U pip
|
pip install -U pip
|
||||||
pip install poetry
|
pip install poetry==0.12.12
|
||||||
|
|
||||||
# Setup poetry and install the dependencies
|
# Setup poetry and install the dependencies
|
||||||
poetry install
|
poetry install
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
eval "$(pyenv init -)"
|
eval "$(pyenv init -)"
|
||||||
eval "$(pyenv virtualenv-init -)"
|
eval "$(pyenv virtualenv-init -)"
|
||||||
pip install -U pip
|
pip install -U pip
|
||||||
pip install poetry
|
pip install poetry==0.12.12
|
||||||
|
|
||||||
# Setup poetry and install the dependencies
|
# Setup poetry and install the dependencies
|
||||||
poetry install
|
poetry install
|
||||||
|
|||||||
@ -53,7 +53,7 @@ popd
|
|||||||
$PYTHON -m pip install -U pip
|
$PYTHON -m pip install -U pip
|
||||||
|
|
||||||
# Install Poetry and things needed for pyinstaller
|
# Install Poetry and things needed for pyinstaller
|
||||||
$PYTHON -m pip install poetry
|
$PYTHON -m pip install poetry==0.12.12
|
||||||
|
|
||||||
# We also need to change the timestamps of all of the base library files
|
# We also need to change the timestamps of all of the base library files
|
||||||
lib_dir=~/.wine/drive_c/python3/Lib
|
lib_dir=~/.wine/drive_c/python3/Lib
|
||||||
|
|||||||
@ -286,7 +286,7 @@ e51392c82e13bbfe714c73361aff14ac1a1637abf37587a562844ae5a4265adf
|
|||||||
When the keypools run out, they can be refilled by using the `getkeypool` commands as done in the beginning, but with different starting and ending indexes. For example, to refill my keypools, I would use the following `getkeypool` commands:
|
When the keypools run out, they can be refilled by using the `getkeypool` commands as done in the beginning, but with different starting and ending indexes. For example, to refill my keypools, I would use the following `getkeypool` commands:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ ./hwi.py -f 8038ecd9 getkeypool --wpkh --keypool --internal 1000 2000
|
$ ./hwi.py -f 8038ecd9 getkeypool --wpkh --keypool 1000 2000
|
||||||
$ ./hwi.py -f 8038ecd9 getkeypool --wpkh --keypool --internal 1000 2000
|
$ ./hwi.py -f 8038ecd9 getkeypool --wpkh --keypool --internal 1000 2000
|
||||||
```
|
```
|
||||||
The output can be imported with `importmulti` as shown in the Setup steps.
|
The output can be imported with `importmulti` as shown in the Setup steps.
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
# Ledger Nano S
|
# Ledger Nano S
|
||||||
|
|
||||||
The Ledger Nano S is supported by HWI.
|
The Ledger Nano S is supported by HWI.
|
||||||
|
Note that the Bitcoin App must be installed and running on the device.
|
||||||
|
|
||||||
Currently implemented commands:
|
Currently implemented commands:
|
||||||
|
|
||||||
|
|||||||
4
hwi.spec
4
hwi.spec
@ -25,6 +25,10 @@ a = Analysis(['hwi.py'],
|
|||||||
win_private_assemblies=False,
|
win_private_assemblies=False,
|
||||||
cipher=block_cipher,
|
cipher=block_cipher,
|
||||||
noarchive=False)
|
noarchive=False)
|
||||||
|
|
||||||
|
if platform.system() == 'Linux':
|
||||||
|
a.datas += Tree('udev', prefix='udev')
|
||||||
|
|
||||||
pyz = PYZ(a.pure, a.zipped_data,
|
pyz = PYZ(a.pure, a.zipped_data,
|
||||||
cipher=block_cipher)
|
cipher=block_cipher)
|
||||||
exe = EXE(pyz,
|
exe = EXE(pyz,
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from .commands import backup_device, displayaddress, enumerate, find_device, \
|
from .commands import backup_device, displayaddress, enumerate, find_device, \
|
||||||
get_client, getmasterxpub, getxpub, getkeypool, prompt_pin, restore_device, send_pin, setup_device, \
|
get_client, getmasterxpub, getxpub, getkeypool, prompt_pin, restore_device, send_pin, setup_device, \
|
||||||
signmessage, signtx, wipe_device
|
signmessage, signtx, wipe_device, install_udev_rules
|
||||||
from .errors import (
|
from .errors import (
|
||||||
HWWError,
|
HWWError,
|
||||||
NO_DEVICE_PATH,
|
NO_DEVICE_PATH,
|
||||||
@ -63,6 +63,9 @@ def prompt_pin_handler(args, client):
|
|||||||
def send_pin_handler(args, client):
|
def send_pin_handler(args, client):
|
||||||
return send_pin(client, pin=args.pin)
|
return send_pin(client, pin=args.pin)
|
||||||
|
|
||||||
|
def install_udev_rules_handler(args):
|
||||||
|
return install_udev_rules(args.source, args.location)
|
||||||
|
|
||||||
def process_commands(cli_args):
|
def process_commands(cli_args):
|
||||||
parser = argparse.ArgumentParser(description='Hardware Wallet Interface, version {}.\nAccess and send commands to a hardware wallet device. Responses are in JSON format.'.format(__version__), formatter_class=argparse.RawDescriptionHelpFormatter)
|
parser = argparse.ArgumentParser(description='Hardware Wallet Interface, version {}.\nAccess and send commands to a hardware wallet device. Responses are in JSON format.'.format(__version__), formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
parser.add_argument('--device-path', '-d', help='Specify the device path of the device to connect to')
|
parser.add_argument('--device-path', '-d', help='Specify the device path of the device to connect to')
|
||||||
@ -142,6 +145,13 @@ def process_commands(cli_args):
|
|||||||
sendpin_parser.add_argument('pin', help='The numeric positions of the PIN')
|
sendpin_parser.add_argument('pin', help='The numeric positions of the PIN')
|
||||||
sendpin_parser.set_defaults(func=send_pin_handler)
|
sendpin_parser.set_defaults(func=send_pin_handler)
|
||||||
|
|
||||||
|
if sys.platform.startswith("linux"):
|
||||||
|
udevrules_parser = subparsers.add_parser('installudevrules', help='Install and load the udev rule files for the hardware wallet devices')
|
||||||
|
udevrules_parser.add_argument('--source', help=argparse.SUPPRESS, default='udev')
|
||||||
|
udevrules_parser.add_argument('--location', help='The path where the udev rules files will be copied', default='/etc/udev/rules.d/')
|
||||||
|
udevrules_parser.set_defaults(func=install_udev_rules_handler)
|
||||||
|
|
||||||
|
|
||||||
if any(arg == '--stdin' for arg in cli_args):
|
if any(arg == '--stdin' for arg in cli_args):
|
||||||
blank_count = 0
|
blank_count = 0
|
||||||
while True:
|
while True:
|
||||||
@ -177,6 +187,17 @@ def process_commands(cli_args):
|
|||||||
if command == 'enumerate':
|
if command == 'enumerate':
|
||||||
return args.func(args)
|
return args.func(args)
|
||||||
|
|
||||||
|
# Install the devices udev rules for Linux
|
||||||
|
if command == 'installudevrules':
|
||||||
|
try:
|
||||||
|
result = args.func(args)
|
||||||
|
except Exception as e:
|
||||||
|
if args.debug:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
result = {'error': str(e), 'code': UNKNOWN_ERROR}
|
||||||
|
return result
|
||||||
|
|
||||||
# Auto detect if we are using fingerprint or type to identify device
|
# Auto detect if we are using fingerprint or type to identify device
|
||||||
if args.fingerprint or (args.device_type and not args.device_path):
|
if args.fingerprint or (args.device_type and not args.device_path):
|
||||||
client = find_device(args.device_path, args.password, args.device_type, args.fingerprint)
|
client = find_device(args.device_path, args.password, args.device_type, args.fingerprint)
|
||||||
|
|||||||
@ -9,6 +9,8 @@ from .base58 import get_xpub_fingerprint_as_id, get_xpub_fingerprint_hex, xpub_t
|
|||||||
from .errors import NoPasswordError, UnavailableActionError, DeviceAlreadyInitError, DeviceAlreadyUnlockedError, UnknownDeviceError, BAD_ARGUMENT, NOT_IMPLEMENTED
|
from .errors import NoPasswordError, UnavailableActionError, DeviceAlreadyInitError, DeviceAlreadyUnlockedError, UnknownDeviceError, BAD_ARGUMENT, NOT_IMPLEMENTED
|
||||||
from .descriptor import Descriptor
|
from .descriptor import Descriptor
|
||||||
from .devices import __all__ as all_devs
|
from .devices import __all__ as all_devs
|
||||||
|
from .udevinstaller import UDevInstaller
|
||||||
|
|
||||||
|
|
||||||
# Get the client for the device
|
# Get the client for the device
|
||||||
def get_client(device_type, device_path, password=''):
|
def get_client(device_type, device_path, password=''):
|
||||||
@ -155,6 +157,10 @@ def displayaddress(client, path=None, desc=None, sh_wpkh=False, wpkh=False):
|
|||||||
return {'error':'Both `--wpkh` and `--sh_wpkh` can not be selected at the same time.','code':BAD_ARGUMENT}
|
return {'error':'Both `--wpkh` and `--sh_wpkh` can not be selected at the same time.','code':BAD_ARGUMENT}
|
||||||
return client.display_address(path, sh_wpkh, wpkh)
|
return client.display_address(path, sh_wpkh, wpkh)
|
||||||
elif desc is not None:
|
elif desc is not None:
|
||||||
|
if client.fingerprint is None:
|
||||||
|
master_xpub = client.get_pubkey_at_path('m/0h')['xpub']
|
||||||
|
client.fingerprint = get_xpub_fingerprint_hex(master_xpub)
|
||||||
|
|
||||||
if sh_wpkh == True or wpkh == True:
|
if sh_wpkh == True or wpkh == True:
|
||||||
return {'error':' `--wpkh` and `--sh_wpkh` can not be combined with --desc','code':BAD_ARGUMENT}
|
return {'error':' `--wpkh` and `--sh_wpkh` can not be combined with --desc','code':BAD_ARGUMENT}
|
||||||
descriptor = Descriptor.parse(desc, client.is_testnet)
|
descriptor = Descriptor.parse(desc, client.is_testnet)
|
||||||
@ -186,3 +192,6 @@ def prompt_pin(client):
|
|||||||
|
|
||||||
def send_pin(client, pin):
|
def send_pin(client, pin):
|
||||||
return client.send_pin(pin)
|
return client.send_pin(pin)
|
||||||
|
|
||||||
|
def install_udev_rules(source, location):
|
||||||
|
return UDevInstaller.install(source, location);
|
||||||
@ -1,7 +1,7 @@
|
|||||||
# Coldcard interaction script
|
# Coldcard interaction script
|
||||||
|
|
||||||
from ..hwwclient import HardwareWalletClient
|
from ..hwwclient import HardwareWalletClient
|
||||||
from ..errors import ActionCanceledError, BadArgumentError, DeviceBusyError, UnavailableActionError, DeviceFailureError
|
from ..errors import ActionCanceledError, BadArgumentError, DeviceBusyError, DeviceFailureError, HWWError, UnavailableActionError, UNKNOWN_ERROR
|
||||||
from .ckcc.client import ColdcardDevice, COINKITE_VID, CKCC_PID
|
from .ckcc.client import ColdcardDevice, COINKITE_VID, CKCC_PID
|
||||||
from .ckcc.protocol import CCProtocolPacker, CCBusyError, CCProtoError, CCUserRefused
|
from .ckcc.protocol import CCProtocolPacker, CCBusyError, CCProtoError, CCUserRefused
|
||||||
from .ckcc.constants import MAX_BLK_LEN, AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH
|
from .ckcc.constants import MAX_BLK_LEN, AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH
|
||||||
@ -14,6 +14,8 @@ import hid
|
|||||||
import io
|
import io
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import struct
|
||||||
|
from binascii import hexlify
|
||||||
|
|
||||||
CC_SIMULATOR_SOCK = '/tmp/ckcc-simulator.sock'
|
CC_SIMULATOR_SOCK = '/tmp/ckcc-simulator.sock'
|
||||||
# Using the simulator: https://github.com/Coldcard/firmware/blob/master/unix/README.md
|
# Using the simulator: https://github.com/Coldcard/firmware/blob/master/unix/README.md
|
||||||
@ -56,6 +58,10 @@ class ColdcardClient(HardwareWalletClient):
|
|||||||
else:
|
else:
|
||||||
return {'xpub':xpub}
|
return {'xpub':xpub}
|
||||||
|
|
||||||
|
def _get_fingerprint_hex(self):
|
||||||
|
# quick method to get fingerprint of wallet
|
||||||
|
return hexlify(struct.pack('<I', self.device.master_fingerprint)).decode()
|
||||||
|
|
||||||
# Must return a hex string with the signed transaction
|
# Must return a hex string with the signed transaction
|
||||||
# The tx must be in the combined unsigned transaction format
|
# The tx must be in the combined unsigned transaction format
|
||||||
@coldcard_exception
|
@coldcard_exception
|
||||||
@ -219,29 +225,35 @@ def enumerate(password=''):
|
|||||||
path = d['path'].decode()
|
path = d['path'].decode()
|
||||||
d_data['type'] = 'coldcard'
|
d_data['type'] = 'coldcard'
|
||||||
d_data['path'] = path
|
d_data['path'] = path
|
||||||
|
d_data['needs_passphrase'] = False
|
||||||
|
|
||||||
client = None
|
client = None
|
||||||
try:
|
try:
|
||||||
client = ColdcardClient(path)
|
client = ColdcardClient(path)
|
||||||
master_xpub = client.get_pubkey_at_path('m/0h')['xpub']
|
d_data['fingerprint'] = client._get_fingerprint_hex()
|
||||||
d_data['fingerprint'] = get_xpub_fingerprint_hex(master_xpub)
|
except HWWError as e:
|
||||||
|
d_data['error'] = "Could not open client or get fingerprint information: " + e.get_msg()
|
||||||
|
d_data['code'] = e.get_code()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
d_data['error'] = "Could not open client or get fingerprint information: " + str(e)
|
d_data['error'] = "Could not open client or get fingerprint information: " + str(e)
|
||||||
|
d_data['code'] = UNKNOWN_ERROR
|
||||||
|
|
||||||
if client:
|
if client:
|
||||||
client.close()
|
client.close()
|
||||||
|
|
||||||
results.append(d_data)
|
results.append(d_data)
|
||||||
|
|
||||||
# Check if the simulator is there
|
# Check if the simulator is there
|
||||||
client = None
|
client = None
|
||||||
try:
|
try:
|
||||||
client = ColdcardClient(CC_SIMULATOR_SOCK)
|
client = ColdcardClient(CC_SIMULATOR_SOCK)
|
||||||
master_xpub = client.get_pubkey_at_path('m/0h')['xpub']
|
|
||||||
|
|
||||||
d_data = {}
|
d_data = {}
|
||||||
d_data['fingerprint'] = get_xpub_fingerprint_hex(master_xpub)
|
d_data['fingerprint'] = client._get_fingerprint_hex()
|
||||||
d_data['type'] = 'coldcard'
|
d_data['type'] = 'coldcard'
|
||||||
d_data['path'] = CC_SIMULATOR_SOCK
|
d_data['path'] = CC_SIMULATOR_SOCK
|
||||||
|
d_data['needs_pin_sent'] = False
|
||||||
|
d_data['needs_passphrase_sent'] = False
|
||||||
results.append(d_data)
|
results.append(d_data)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
if str(e) == 'Cannot connect to simulator. Is it running?':
|
if str(e) == 'Cannot connect to simulator. Is it running?':
|
||||||
@ -250,4 +262,5 @@ def enumerate(password=''):
|
|||||||
raise e
|
raise e
|
||||||
if client:
|
if client:
|
||||||
client.close()
|
client.close()
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import sys
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
from ..hwwclient import HardwareWalletClient
|
from ..hwwclient import HardwareWalletClient
|
||||||
from ..errors import ActionCanceledError, BadArgumentError, DeviceFailureError, DeviceAlreadyInitError, DeviceNotReadyError, NoPasswordError, UnavailableActionError, DeviceFailureError
|
from ..errors import ActionCanceledError, BadArgumentError, DeviceFailureError, DeviceAlreadyInitError, DeviceNotReadyError, HWWError, NoPasswordError, UnavailableActionError, UNKNOWN_ERROR
|
||||||
from ..serializations import CTransaction, PSBT, hash256, hash160, ser_sig_der, ser_sig_compact, ser_compact_size
|
from ..serializations import CTransaction, PSBT, hash256, hash160, ser_sig_der, ser_sig_compact, ser_compact_size
|
||||||
from ..base58 import get_xpub_fingerprint, decode, to_address, xpub_main_2_test, get_xpub_fingerprint_hex
|
from ..base58 import get_xpub_fingerprint, decode, to_address, xpub_main_2_test, get_xpub_fingerprint_hex
|
||||||
|
|
||||||
@ -603,8 +603,14 @@ def enumerate(password=''):
|
|||||||
else:
|
else:
|
||||||
master_xpub = client.get_pubkey_at_path('m/0h')['xpub']
|
master_xpub = client.get_pubkey_at_path('m/0h')['xpub']
|
||||||
d_data['fingerprint'] = get_xpub_fingerprint_hex(master_xpub)
|
d_data['fingerprint'] = get_xpub_fingerprint_hex(master_xpub)
|
||||||
|
d_data['needs_pin_sent'] = False
|
||||||
|
d_data['needs_passphrase_sent'] = True
|
||||||
|
except HWWError as e:
|
||||||
|
d_data['error'] = "Could not open client or get fingerprint information: " + e.get_msg()
|
||||||
|
d_data['code'] = e.get_code()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
d_data['error'] = "Could not open client or get fingerprint information: " + str(e)
|
d_data['error'] = "Could not open client or get fingerprint information: " + str(e)
|
||||||
|
d_data['code'] = UNKNOWN_ERROR
|
||||||
|
|
||||||
if client:
|
if client:
|
||||||
client.close()
|
client.close()
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
# KeepKey interaction script
|
# KeepKey interaction script
|
||||||
|
|
||||||
|
from ..errors import HWWError, UNKNOWN_ERROR
|
||||||
from .trezorlib.transport import enumerate_devices
|
from .trezorlib.transport import enumerate_devices
|
||||||
from .trezor import TrezorClient
|
from .trezor import TrezorClient
|
||||||
from ..base58 import get_xpub_fingerprint_hex
|
from ..base58 import get_xpub_fingerprint_hex
|
||||||
@ -25,13 +26,24 @@ def enumerate(password=''):
|
|||||||
client.client.init_device()
|
client.client.init_device()
|
||||||
if not 'keepkey' in client.client.features.vendor:
|
if not 'keepkey' in client.client.features.vendor:
|
||||||
continue
|
continue
|
||||||
|
d_data['needs_pin_sent'] = client.client.features.pin_protection and not client.client.features.pin_cached
|
||||||
|
d_data['needs_passphrase_sent'] = client.client.features.passphrase_protection and not client.client.features.passphrase_cached
|
||||||
|
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:
|
if client.client.features.initialized:
|
||||||
master_xpub = client.get_pubkey_at_path('m/0h')['xpub']
|
master_xpub = client.get_pubkey_at_path('m/0h')['xpub']
|
||||||
d_data['fingerprint'] = get_xpub_fingerprint_hex(master_xpub)
|
d_data['fingerprint'] = get_xpub_fingerprint_hex(master_xpub)
|
||||||
|
d_data['needs_passphrase_sent'] = False # Passphrase is always needed for the above to have worked, so it's already sent
|
||||||
else:
|
else:
|
||||||
d_data['error'] = 'Not initialized'
|
d_data['error'] = 'Not initialized'
|
||||||
|
except HWWError as e:
|
||||||
|
d_data['error'] = "Could not open client or get fingerprint information: " + e.get_msg()
|
||||||
|
d_data['code'] = e.get_code()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
d_data['error'] = "Could not open client or get fingerprint information: " + str(e)
|
d_data['error'] = "Could not open client or get fingerprint information: " + str(e)
|
||||||
|
d_data['code'] = UNKNOWN_ERROR
|
||||||
|
|
||||||
if client:
|
if client:
|
||||||
client.close()
|
client.close()
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
# Ledger interaction script
|
# Ledger interaction script
|
||||||
|
|
||||||
from ..hwwclient import HardwareWalletClient
|
from ..hwwclient import HardwareWalletClient
|
||||||
from ..errors import ActionCanceledError, BadArgumentError, DeviceConnectionError, DeviceFailureError, UnavailableActionError
|
from ..errors import ActionCanceledError, BadArgumentError, DeviceConnectionError, DeviceFailureError, HWWError, UnavailableActionError, UNKNOWN_ERROR
|
||||||
from .btchip.btchip import *
|
from .btchip.btchip import *
|
||||||
from .btchip.btchipUtils import *
|
from .btchip.btchipUtils import *
|
||||||
import base64
|
import base64
|
||||||
@ -350,8 +350,11 @@ def enumerate(password=''):
|
|||||||
client = LedgerClient(path, password)
|
client = LedgerClient(path, password)
|
||||||
master_xpub = client.get_pubkey_at_path('m/0h')['xpub']
|
master_xpub = client.get_pubkey_at_path('m/0h')['xpub']
|
||||||
d_data['fingerprint'] = get_xpub_fingerprint_hex(master_xpub)
|
d_data['fingerprint'] = get_xpub_fingerprint_hex(master_xpub)
|
||||||
|
d_data['needs_pin_sent'] = False
|
||||||
|
d_data['needs_passphrase_sent'] = False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
d_data['error'] = "Could not open client or get fingerprint information: " + str(e)
|
d_data['error'] = "Could not open client or get fingerprint information: " + str(e)
|
||||||
|
d_data['code'] = UNKNOWN_ERROR
|
||||||
|
|
||||||
if client:
|
if client:
|
||||||
client.close()
|
client.close()
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
# Trezor interaction script
|
# Trezor interaction script
|
||||||
|
|
||||||
from ..hwwclient import HardwareWalletClient
|
from ..hwwclient import HardwareWalletClient
|
||||||
from ..errors import ActionCanceledError, BadArgumentError, DeviceAlreadyInitError, DeviceAlreadyUnlockedError, DeviceConnectionError, UnavailableActionError, DeviceNotReadyError
|
from ..errors import ActionCanceledError, BadArgumentError, DeviceAlreadyInitError, DeviceAlreadyUnlockedError, DeviceConnectionError, DeviceNotReadyError, HWWError, UnavailableActionError, UNKNOWN_ERROR
|
||||||
from .trezorlib.client import TrezorClient as Trezor
|
from .trezorlib.client import TrezorClient as Trezor
|
||||||
from .trezorlib.debuglink import TrezorClientDebugLink, DebugUI
|
from .trezorlib.debuglink import TrezorClientDebugLink, DebugUI
|
||||||
from .trezorlib.exceptions import Cancelled
|
from .trezorlib.exceptions import Cancelled
|
||||||
@ -100,6 +100,7 @@ class TrezorClient(HardwareWalletClient):
|
|||||||
transport = get_transport(path)
|
transport = get_transport(path)
|
||||||
self.client = TrezorClientDebugLink(transport=transport)
|
self.client = TrezorClientDebugLink(transport=transport)
|
||||||
self.simulator = True
|
self.simulator = True
|
||||||
|
self.client.set_passphrase(password)
|
||||||
else:
|
else:
|
||||||
self.client = Trezor(transport=get_transport(path), ui=PassphraseUI(password))
|
self.client = Trezor(transport=get_transport(path), ui=PassphraseUI(password))
|
||||||
|
|
||||||
@ -421,13 +422,24 @@ def enumerate(password=''):
|
|||||||
client.client.init_device()
|
client.client.init_device()
|
||||||
if not 'trezor' in client.client.features.vendor:
|
if not 'trezor' in client.client.features.vendor:
|
||||||
continue
|
continue
|
||||||
|
d_data['needs_pin_sent'] = client.client.features.pin_protection and not client.client.features.pin_cached
|
||||||
|
d_data['needs_passphrase_sent'] = client.client.features.passphrase_protection and not client.client.features.passphrase_cached
|
||||||
|
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:
|
if client.client.features.initialized:
|
||||||
master_xpub = client.get_pubkey_at_path('m/0h')['xpub']
|
master_xpub = client.get_pubkey_at_path('m/0h')['xpub']
|
||||||
d_data['fingerprint'] = get_xpub_fingerprint_hex(master_xpub)
|
d_data['fingerprint'] = get_xpub_fingerprint_hex(master_xpub)
|
||||||
|
d_data['needs_passphrase_sent'] = False # Passphrase is always needed for the above to have worked, so it's already sent
|
||||||
else:
|
else:
|
||||||
d_data['error'] = 'Not initialized'
|
d_data['error'] = 'Not initialized'
|
||||||
|
except HWWError as e:
|
||||||
|
d_data['error'] = "Could not open client or get fingerprint information: " + e.get_msg()
|
||||||
|
d_data['code'] = e.get_code()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
d_data['error'] = "Could not open client or get fingerprint information: " + str(e)
|
d_data['error'] = "Could not open client or get fingerprint information: " + str(e)
|
||||||
|
d_data['code'] = UNKNOWN_ERROR
|
||||||
|
|
||||||
if client:
|
if client:
|
||||||
client.close()
|
client.close()
|
||||||
|
|||||||
@ -178,7 +178,10 @@ class TrezorClient:
|
|||||||
|
|
||||||
@tools.session
|
@tools.session
|
||||||
def init_device(self):
|
def init_device(self):
|
||||||
resp = self.call_raw(messages.Initialize(state=self.state))
|
resp = self.call_raw(messages.GetFeatures())
|
||||||
|
# If GetFeatures fails, try initializing and clearing inconsistent state on the device
|
||||||
|
if isinstance(resp, messages.Failure):
|
||||||
|
resp = self.call_raw(messages.Initialize())
|
||||||
if not isinstance(resp, messages.Features):
|
if not isinstance(resp, messages.Features):
|
||||||
raise exceptions.TrezorException("Unexpected initial response")
|
raise exceptions.TrezorException("Unexpected initial response")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -16,6 +16,7 @@ DEVICE_NOT_READY = -12
|
|||||||
UNKNOWN_ERROR = -13
|
UNKNOWN_ERROR = -13
|
||||||
ACTION_CANCELED = -14
|
ACTION_CANCELED = -14
|
||||||
DEVICE_BUSY = -15
|
DEVICE_BUSY = -15
|
||||||
|
NEED_TO_BE_ROOT = -16
|
||||||
|
|
||||||
# Exceptions
|
# Exceptions
|
||||||
class HWWError(Exception):
|
class HWWError(Exception):
|
||||||
|
|||||||
60
hwilib/udevinstaller.py
Normal file
60
hwilib/udevinstaller.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import sys
|
||||||
|
from subprocess import check_call, CalledProcessError, DEVNULL
|
||||||
|
from .errors import NEED_TO_BE_ROOT
|
||||||
|
from shutil import copy
|
||||||
|
from os import path, environ, listdir, getlogin, geteuid
|
||||||
|
|
||||||
|
class UDevInstaller(object):
|
||||||
|
@staticmethod
|
||||||
|
def install(source, location):
|
||||||
|
try:
|
||||||
|
udev_installer = UDevInstaller()
|
||||||
|
udev_installer.copy_udev_rule_files(source, location)
|
||||||
|
udev_installer.trigger()
|
||||||
|
udev_installer.reload_rules()
|
||||||
|
udev_installer.add_user_plugdev_group()
|
||||||
|
except CalledProcessError as e:
|
||||||
|
if geteuid() != 0:
|
||||||
|
return {'error':'Need to be root.','code':NEED_TO_BE_ROOT}
|
||||||
|
raise
|
||||||
|
return {"success": True}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._udevadm = '/sbin/udevadm'
|
||||||
|
self._groupadd = '/usr/sbin/groupadd'
|
||||||
|
self._usermod = '/usr/sbin/usermod'
|
||||||
|
|
||||||
|
def _execute(self, command, *args):
|
||||||
|
command = [command] + list(args)
|
||||||
|
check_call(command, stderr=DEVNULL, stdout=DEVNULL)
|
||||||
|
|
||||||
|
def trigger(self):
|
||||||
|
self._execute(self._udevadm, 'trigger')
|
||||||
|
|
||||||
|
def reload_rules(self):
|
||||||
|
self._execute(self._udevadm, 'control', '--reload-rules')
|
||||||
|
|
||||||
|
def add_user_plugdev_group(self):
|
||||||
|
self._create_group('plugdev')
|
||||||
|
self._add_user_to_group(getlogin(), 'plugdev')
|
||||||
|
|
||||||
|
def _create_group(self, name):
|
||||||
|
try:
|
||||||
|
self._execute(self._groupadd, name)
|
||||||
|
except CalledProcessError as e:
|
||||||
|
if e.returncode != 9: # group already exists
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _add_user_to_group(self, user, group):
|
||||||
|
self._execute(self._usermod, '-aG', group, user)
|
||||||
|
|
||||||
|
def copy_udev_rule_files(self, source, location):
|
||||||
|
src_dir_path = source
|
||||||
|
for rules_file_name in listdir(src_dir_path):
|
||||||
|
rules_file_path = _resource_path(path.join(src_dir_path, rules_file_name))
|
||||||
|
copy(rules_file_path, location)
|
||||||
|
|
||||||
|
def _resource_path(relative_path):
|
||||||
|
if hasattr(sys, '_MEIPASS'):
|
||||||
|
return path.join(sys._MEIPASS, relative_path)
|
||||||
|
return path.join(relative_path)
|
||||||
@ -9,6 +9,10 @@ repository = "https://github.com/bitcoin-core/HWI"
|
|||||||
homepage = "https://github.com/bitcoin-core/HWI"
|
homepage = "https://github.com/bitcoin-core/HWI"
|
||||||
exclude = ["docs/", "test/"]
|
exclude = ["docs/", "test/"]
|
||||||
include = ["hwilib/**/*.py"]
|
include = ["hwilib/**/*.py"]
|
||||||
|
packages = [
|
||||||
|
{ include = "hwi.py" },
|
||||||
|
{ include = "hwilib" },
|
||||||
|
]
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = ">=3.5.6"
|
python = ">=3.5.6"
|
||||||
|
|||||||
16
setup.py
16
setup.py
@ -1,6 +1,18 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from distutils.core import setup
|
from distutils.core import setup
|
||||||
|
|
||||||
|
packages = \
|
||||||
|
['hwilib',
|
||||||
|
'hwilib.devices',
|
||||||
|
'hwilib.devices.btchip',
|
||||||
|
'hwilib.devices.ckcc',
|
||||||
|
'hwilib.devices.trezorlib',
|
||||||
|
'hwilib.devices.trezorlib.messages',
|
||||||
|
'hwilib.devices.trezorlib.transport']
|
||||||
|
|
||||||
|
package_data = \
|
||||||
|
{'': ['*']}
|
||||||
|
|
||||||
modules = \
|
modules = \
|
||||||
['hwi']
|
['hwi']
|
||||||
install_requires = \
|
install_requires = \
|
||||||
@ -21,10 +33,12 @@ setup_kwargs = {
|
|||||||
'name': 'hwi',
|
'name': 'hwi',
|
||||||
'version': '1.0.0',
|
'version': '1.0.0',
|
||||||
'description': 'A library for working with Bitcoin hardware wallets',
|
'description': 'A library for working with Bitcoin hardware wallets',
|
||||||
'long_description': "# Bitcoin Hardware Wallet Interface\n\n[](https://travis-ci.org/bitcoin-core/HWI)\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\n## Prerequisites\n\nPython 3 is required. The libraries and udev rules 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\n```\n\nFor macOS:\n```\nbrew install libusb\n```\n\nThis project uses the [Poetry](https://github.com/sdispater/poetry) dependency manager.\nOnce HWI's source has been downloaded with git clone, it and its dependencies can be installed via poetry by execting the following in the root source directory:\n\n```\npoetry install\n```\n\nPip can also be used to install all of the dependencies (in virtualenv or system):\n\n```\npip3 install hidapi # HID API needed in general\npip3 install ecdsa\npip3 install pyaes\npip3 install typing_extensions\npip3 install mnemonic\npip3 install libusb1\n```\n## Install\n\n```\ngit clone https://github.com/bitcoin-core/HWI.git\ncd HWI\n```\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 is known, issue commands to it like so:\n\n```\n./hwi.py -t <type> -d <path> <command> <command args>\n```\n\n## Device Support\n\nThe below table lists what devices and features are supported for each device.\n\nPlease also see [docs](docs/) for additional information about each device.\n\n| Feature \\ Device | Ledger Nano S | Trezor One | Digital BitBox | KeepKey | Coldcard |\n|:---:|:---:|:---:|:---:|:---:|:---:|\n| Support Planned | Yes | Yes | Yes | Yes | Yes |\n| Implemented | Yes | Yes | Yes | Yes | Yes |\n| xpub retrieval | Yes | Yes | Yes | Yes | Yes |\n| Message Signing | Yes | Yes | Yes | Yes | Yes |\n| Device Setup | N/A | Yes | Yes | Yes | N/A |\n| Device Wipe | N/A | Yes | Yes | Yes | N/A |\n| Device Recovery | N/A | Yes | N/A | Yes | N/A |\n| Device Backup | N/A | N/A | Yes | N/A | Yes |\n| P2PKH Inputs | Yes | Yes | Yes | Yes | Yes |\n| P2SH-P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes |\n| P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes |\n| P2SH Multisig Inputs | Yes | Yes | Yes | Yes | N/A |\n| P2SH-P2WSH Multisig Inputs | Yes | No | Yes | No | N/A |\n| P2WSH Multisig Inputs | Yes | No | Yes | Yes | N/A |\n| Bare Multisig Inputs | Yes | N/A | Yes | N/A | N/A |\n| Aribtrary scriptPubKey Inputs | Yes | N/A | Yes | N/A | N/A |\n| Aribtrary redeemScript Inputs | Yes | N/A | Yes | N/A | N/A |\n| Arbitrary witnessScript Inputs | Yes | N/A | Yes | N/A | N/A |\n| Non-wallet inputs | Yes | Yes | Yes | Yes | Yes |\n| Mixed Segwit and Non-Segwit Inputs | N/A | Yes | Yes | Yes | Yes |\n| Display on device screen | Yes | Yes | N/A | Yes | Yes |\n\n## Using with Bitcoin Core\n\nSee [Using Bitcoin Core with Hardware Wallets](docs/bitcoin-core-usage.md).\n\n## License\n\nThis project is available under the MIT License, Copyright Andrew Chow.\n",
|
'long_description': "# Bitcoin Hardware Wallet Interface\n\n[](https://travis-ci.org/bitcoin-core/HWI)\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\n## Prerequisites\n\nPython 3 is required. The libraries and udev rules 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\n```\n\nFor macOS:\n```\nbrew install libusb\n```\n\nThis project uses the [Poetry](https://github.com/sdispater/poetry) dependency manager.\nOnce HWI's source has been downloaded with git clone, it and its dependencies can be installed via poetry by execting the following in the root source directory:\n\n```\npoetry install\n```\n\nPip can also be used to install all of the dependencies (in virtualenv or system):\n\n```\npip3 install hidapi # HID API needed in general\npip3 install ecdsa\npip3 install pyaes\npip3 install typing_extensions\npip3 install mnemonic\npip3 install libusb1\n```\n## Install\n\n```\ngit clone https://github.com/bitcoin-core/HWI.git\ncd HWI\n```\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 is known, issue commands to it like so:\n\n```\n./hwi.py -t <type> -d <path> <command> <command args>\n```\n\n## Device Support\n\nThe below table lists what devices and features are supported for each device.\n\nPlease also see [docs](docs/) for additional information about each device.\n\n| Feature \\ Device | Ledger Nano S | Trezor One | Trezor Model T | Digital BitBox | KeepKey | Coldcard |\n|:---:|:---:|:---:|:---:|:---:|:---:|:---:|\n| Support Planned | Yes | Yes | Yes | Yes | Yes | Yes |\n| Implemented | Yes | Yes | Yes | Yes | Yes | Yes |\n| xpub retrieval | Yes | Yes | Yes | Yes | Yes | Yes |\n| Message Signing | Yes | Yes | ? | Yes | Yes | Yes |\n| Device Setup | N/A | Yes | ? | Yes | Yes | N/A |\n| Device Wipe | N/A | Yes | ? | Yes | Yes | N/A |\n| Device Recovery | N/A | Yes | ? | N/A | Yes | N/A |\n| Device Backup | N/A | N/A | ? | Yes | N/A | Yes |\n| P2PKH Inputs | Yes | Yes | ? | Yes | Yes | Yes |\n| P2SH-P2WPKH Inputs | Yes | Yes | ? | Yes | Yes | Yes |\n| P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH Multisig Inputs | Yes | Yes | ? | Yes | Yes | N/A |\n| P2SH-P2WSH Multisig Inputs | Yes | No | ? | Yes | No | N/A |\n| P2WSH Multisig Inputs | Yes | No | ? | Yes | Yes | N/A |\n| Bare Multisig Inputs | Yes | N/A | ? | Yes | N/A | N/A |\n| Aribtrary scriptPubKey Inputs | Yes | N/A | ? | Yes | N/A | N/A |\n| Aribtrary redeemScript Inputs | Yes | N/A | ? | Yes | N/A | N/A |\n| Arbitrary witnessScript Inputs | Yes | N/A | ? | Yes | N/A | N/A |\n| Non-wallet inputs | Yes | Yes | ? | Yes | Yes | Yes |\n| Mixed Segwit and Non-Segwit Inputs | N/A | Yes | ? | Yes | Yes | Yes |\n| Display on device screen | Yes | Yes | Yes | N/A | Yes | Yes |\n\n## Using with Bitcoin Core\n\nSee [Using Bitcoin Core with Hardware Wallets](docs/bitcoin-core-usage.md).\n\n## License\n\nThis project is available under the MIT License, Copyright Andrew Chow.\n",
|
||||||
'author': 'Andrew Chow',
|
'author': 'Andrew Chow',
|
||||||
'author_email': 'andrew@achow101.com',
|
'author_email': 'andrew@achow101.com',
|
||||||
'url': 'https://github.com/bitcoin-core/HWI',
|
'url': 'https://github.com/bitcoin-core/HWI',
|
||||||
|
'packages': packages,
|
||||||
|
'package_data': package_data,
|
||||||
'py_modules': modules,
|
'py_modules': modules,
|
||||||
'install_requires': install_requires,
|
'install_requires': install_requires,
|
||||||
'extras_require': extras_require,
|
'extras_require': extras_require,
|
||||||
|
|||||||
@ -14,11 +14,15 @@ from test_trezor import trezor_test_suite
|
|||||||
from test_ledger import ledger_test_suite
|
from test_ledger import ledger_test_suite
|
||||||
from test_digitalbitbox import digitalbitbox_test_suite
|
from test_digitalbitbox import digitalbitbox_test_suite
|
||||||
from test_keepkey import keepkey_test_suite
|
from test_keepkey import keepkey_test_suite
|
||||||
|
from test_udevrules import TestUdevRulesInstaller
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description='Setup the testing environment and run automated tests')
|
parser = argparse.ArgumentParser(description='Setup the testing environment and run automated tests')
|
||||||
trezor_group = parser.add_mutually_exclusive_group()
|
trezor_group = parser.add_mutually_exclusive_group()
|
||||||
trezor_group.add_argument('--no_trezor', help='Do not run Trezor test with emulator', action='store_true')
|
trezor_group.add_argument('--no_trezor', help='Do not run Trezor test with emulator', action='store_true')
|
||||||
trezor_group.add_argument('--trezor', help='Path to Trezor emulator.', default='work/trezor-mcu/firmware/trezor.elf')
|
trezor_group.add_argument('--trezor', help='Path to Trezor emulator.', default='work/trezor-firmware/legacy/firmware/trezor.elf')
|
||||||
|
trezor_t_group = parser.add_mutually_exclusive_group()
|
||||||
|
trezor_t_group.add_argument('--no_trezor_t', help='Do not run Trezor T test with emulator', action='store_true')
|
||||||
|
trezor_t_group.add_argument('--trezor_t', help='Path to Trezor T emulator.', default='work/trezor-firmware/core/emu.sh')
|
||||||
coldcard_group = parser.add_mutually_exclusive_group()
|
coldcard_group = parser.add_mutually_exclusive_group()
|
||||||
coldcard_group.add_argument('--no_coldcard', help='Do not run Coldcard test with simulator', action='store_true')
|
coldcard_group.add_argument('--no_coldcard', help='Do not run Coldcard test with simulator', action='store_true')
|
||||||
coldcard_group.add_argument('--coldcard', help='Path to Coldcard simulator.', default='work/firmware/unix/headless.py')
|
coldcard_group.add_argument('--coldcard', help='Path to Coldcard simulator.', default='work/firmware/unix/headless.py')
|
||||||
@ -40,20 +44,25 @@ suite = unittest.TestSuite()
|
|||||||
suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestDescriptor))
|
suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestDescriptor))
|
||||||
suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestSegwitAddress))
|
suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestSegwitAddress))
|
||||||
suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestPSBT))
|
suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestPSBT))
|
||||||
|
if sys.platform.startswith("linux"):
|
||||||
|
suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestUdevRulesInstaller))
|
||||||
|
|
||||||
if not args.no_trezor or not args.no_coldcard or args.ledger or not args.no_bitbox or not args.no_keepkey:
|
|
||||||
|
if not args.no_trezor or not args.no_coldcard or args.ledger or not args.no_bitbox or not args.no_keepkey or not args.no_trezor_t:
|
||||||
# Start bitcoind
|
# Start bitcoind
|
||||||
rpc, userpass = start_bitcoind(args.bitcoind)
|
rpc, userpass = start_bitcoind(args.bitcoind)
|
||||||
|
|
||||||
if not args.no_trezor:
|
|
||||||
suite.addTest(trezor_test_suite(args.trezor, rpc, userpass, args.interface))
|
|
||||||
if not args.no_coldcard:
|
|
||||||
suite.addTest(coldcard_test_suite(args.coldcard, rpc, userpass, args.interface))
|
|
||||||
if args.ledger:
|
|
||||||
suite.addTest(ledger_test_suite(rpc, userpass, args.interface))
|
|
||||||
if not args.no_bitbox:
|
if not args.no_bitbox:
|
||||||
suite.addTest(digitalbitbox_test_suite(rpc, userpass, args.bitbox, args.interface))
|
suite.addTest(digitalbitbox_test_suite(rpc, userpass, args.bitbox, args.interface))
|
||||||
|
if not args.no_coldcard:
|
||||||
|
suite.addTest(coldcard_test_suite(args.coldcard, rpc, userpass, args.interface))
|
||||||
|
if not args.no_trezor:
|
||||||
|
suite.addTest(trezor_test_suite(args.trezor, rpc, userpass, args.interface))
|
||||||
|
if not args.no_trezor_t:
|
||||||
|
suite.addTest(trezor_test_suite(args.trezor_t, rpc, userpass, args.interface, True))
|
||||||
if not args.no_keepkey:
|
if not args.no_keepkey:
|
||||||
suite.addTest(keepkey_test_suite(args.keepkey, rpc, userpass, args.interface))
|
suite.addTest(keepkey_test_suite(args.keepkey, rpc, userpass, args.interface))
|
||||||
|
if args.ledger:
|
||||||
|
suite.addTest(ledger_test_suite(rpc, userpass, args.interface))
|
||||||
result = unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite)
|
result = unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite)
|
||||||
sys.exit(not result.wasSuccessful())
|
sys.exit(not result.wasSuccessful())
|
||||||
|
|||||||
@ -9,12 +9,12 @@ cd work
|
|||||||
|
|
||||||
# Clone trezor-mcu if it doesn't exist, or update it if it does
|
# Clone trezor-mcu if it doesn't exist, or update it if it does
|
||||||
trezor_setup_needed=false
|
trezor_setup_needed=false
|
||||||
if [ ! -d "trezor-mcu" ]; then
|
if [ ! -d "trezor-firmware" ]; then
|
||||||
git clone --recursive https://github.com/trezor/trezor-mcu.git
|
git clone --recursive https://github.com/trezor/trezor-firmware.git
|
||||||
cd trezor-mcu
|
cd trezor-firmware
|
||||||
trezor_setup_needed=true
|
trezor_setup_needed=true
|
||||||
else
|
else
|
||||||
cd trezor-mcu
|
cd trezor-firmware
|
||||||
git fetch
|
git fetch
|
||||||
|
|
||||||
# Determine if we need to pull. From https://stackoverflow.com/a/3278427
|
# Determine if we need to pull. From https://stackoverflow.com/a/3278427
|
||||||
@ -31,16 +31,30 @@ else
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Build emulator. This is pretty fast, so rebuilding every time is ok
|
# Build trezor one emulator. This is pretty fast, so rebuilding every time is ok
|
||||||
# But there should be some caching that makes this faster
|
# But there should be some caching that makes this faster
|
||||||
|
cd legacy
|
||||||
export EMULATOR=1 TREZOR_TRANSPORT_V1=1 DEBUG_LINK=1 HEADLESS=1
|
export EMULATOR=1 TREZOR_TRANSPORT_V1=1 DEBUG_LINK=1 HEADLESS=1
|
||||||
if [ "$trezor_setup_needed" == true ] ; then
|
if [ "$trezor_setup_needed" == true ] ; then
|
||||||
script/setup
|
script/setup
|
||||||
pipenv install
|
pipenv install
|
||||||
fi
|
fi
|
||||||
pipenv run script/cibuild
|
pipenv run script/cibuild
|
||||||
|
# Delete any emulator.img file
|
||||||
|
find . -name "emulator.img" -exec rm {} \;
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
|
# Build trezor t emulator. This is pretty fast, so rebuilding every time is ok
|
||||||
|
# But there should be some caching that makes this faster
|
||||||
|
cd core
|
||||||
|
if [ "$trezor_setup_needed" == true ] ; then
|
||||||
|
make vendor
|
||||||
|
fi
|
||||||
|
make build_unix
|
||||||
|
# Delete any emulator.img file
|
||||||
|
rm /var/tmp/trezor.flash
|
||||||
|
cd ../..
|
||||||
|
|
||||||
# Clone coldcard firmware if it doesn't exist, or update it if it does
|
# Clone coldcard firmware if it doesn't exist, or update it if it does
|
||||||
coldcard_setup_needed=false
|
coldcard_setup_needed=false
|
||||||
if [ ! -d "firmware" ]; then
|
if [ ! -d "firmware" ]; then
|
||||||
@ -144,6 +158,8 @@ cd ../../../
|
|||||||
export PATH=$PATH:`pwd`/nanopb/generator
|
export PATH=$PATH:`pwd`/nanopb/generator
|
||||||
pipenv run cmake -C cmake/caches/emulator.cmake . -DNANOPB_DIR=nanopb/ -DKK_HAVE_STRLCAT=OFF -DKK_HAVE_STRLCPY=OFF
|
pipenv run cmake -C cmake/caches/emulator.cmake . -DNANOPB_DIR=nanopb/ -DKK_HAVE_STRLCAT=OFF -DKK_HAVE_STRLCPY=OFF
|
||||||
pipenv run make -j$(nproc) kkemu
|
pipenv run make -j$(nproc) kkemu
|
||||||
|
# Delete any emulator.img file
|
||||||
|
find . -name "emulator.img" -exec rm {} \;
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
# Clone bitcoind if it doesn't exist, or update it if it does
|
# Clone bitcoind if it doesn't exist, or update it if it does
|
||||||
|
|||||||
@ -18,7 +18,12 @@ def coldcard_test_suite(simulator, rpc, userpass, interface):
|
|||||||
# Wait for simulator to be up
|
# Wait for simulator to be up
|
||||||
while True:
|
while True:
|
||||||
enum_res = process_commands(['enumerate'])
|
enum_res = process_commands(['enumerate'])
|
||||||
if len(enum_res) > 0 and 'error' not in enum_res[0]:
|
found = False
|
||||||
|
for dev in enum_res:
|
||||||
|
if dev['type'] == 'coldcard' and 'error' not in dev:
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
if found:
|
||||||
break
|
break
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
# Cleanup
|
# Cleanup
|
||||||
@ -70,12 +75,12 @@ def coldcard_test_suite(simulator, rpc, userpass, interface):
|
|||||||
|
|
||||||
# Generic device tests
|
# Generic device tests
|
||||||
suite = unittest.TestSuite()
|
suite = unittest.TestSuite()
|
||||||
suite.addTest(DeviceTestCase.parameterize(TestColdcardManCommands, rpc, userpass, 'coldcard', '/tmp/ckcc-simulator.sock', '0f056943', '', interface=interface))
|
suite.addTest(DeviceTestCase.parameterize(TestColdcardManCommands, rpc, userpass, 'coldcard', 'coldcard', '/tmp/ckcc-simulator.sock', '0f056943', '', interface=interface))
|
||||||
suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, 'coldcard', '/tmp/ckcc-simulator.sock', '0f056943', 'tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd', interface=interface))
|
suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, 'coldcard', 'coldcard', '/tmp/ckcc-simulator.sock', '0f056943', 'tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd', interface=interface))
|
||||||
suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, rpc, userpass, 'coldcard', '/tmp/ckcc-simulator.sock', '0f056943', 'tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd', interface=interface))
|
suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, rpc, userpass, 'coldcard', 'coldcard', '/tmp/ckcc-simulator.sock', '0f056943', 'tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd', interface=interface))
|
||||||
suite.addTest(DeviceTestCase.parameterize(TestDisplayAddress, rpc, userpass, 'coldcard', '/tmp/ckcc-simulator.sock', '0f056943', 'tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd', interface=interface))
|
suite.addTest(DeviceTestCase.parameterize(TestDisplayAddress, rpc, userpass, 'coldcard', 'coldcard', '/tmp/ckcc-simulator.sock', '0f056943', 'tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd', interface=interface))
|
||||||
suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, 'coldcard', '/tmp/ckcc-simulator.sock', '0f056943', 'tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd', interface=interface))
|
suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, 'coldcard', 'coldcard', '/tmp/ckcc-simulator.sock', '0f056943', 'tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd', interface=interface))
|
||||||
suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, 'coldcard', '/tmp/ckcc-simulator.sock', '0f056943', 'tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd', interface=interface))
|
suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, 'coldcard', 'coldcard', '/tmp/ckcc-simulator.sock', '0f056943', 'tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd', interface=interface))
|
||||||
return suite
|
return suite
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
import atexit
|
import atexit
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import shlex
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
@ -51,11 +52,12 @@ def start_bitcoind(bitcoind_path):
|
|||||||
return (rpc, userpass)
|
return (rpc, userpass)
|
||||||
|
|
||||||
class DeviceTestCase(unittest.TestCase):
|
class DeviceTestCase(unittest.TestCase):
|
||||||
def __init__(self, rpc, rpc_userpass, type, path, fingerprint, master_xpub, password = '', emulator=None, interface='library', methodName='runTest'):
|
def __init__(self, rpc, rpc_userpass, type, full_type, path, fingerprint, master_xpub, password = '', emulator=None, interface='library', methodName='runTest'):
|
||||||
super(DeviceTestCase, self).__init__(methodName)
|
super(DeviceTestCase, self).__init__(methodName)
|
||||||
self.rpc = rpc
|
self.rpc = rpc
|
||||||
self.rpc_userpass = rpc_userpass
|
self.rpc_userpass = rpc_userpass
|
||||||
self.type = type
|
self.type = type
|
||||||
|
self.full_type = full_type
|
||||||
self.path = path
|
self.path = path
|
||||||
self.fingerprint = fingerprint
|
self.fingerprint = fingerprint
|
||||||
self.master_xpub = master_xpub
|
self.master_xpub = master_xpub
|
||||||
@ -70,21 +72,24 @@ class DeviceTestCase(unittest.TestCase):
|
|||||||
self.interface = interface
|
self.interface = interface
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parameterize(testclass, rpc, rpc_userpass, type, path, fingerprint, master_xpub, password = '', interface='library', emulator=None):
|
def parameterize(testclass, rpc, rpc_userpass, type, full_type, path, fingerprint, master_xpub, password = '', interface='library', emulator=None):
|
||||||
testloader = unittest.TestLoader()
|
testloader = unittest.TestLoader()
|
||||||
testnames = testloader.getTestCaseNames(testclass)
|
testnames = testloader.getTestCaseNames(testclass)
|
||||||
suite = unittest.TestSuite()
|
suite = unittest.TestSuite()
|
||||||
for name in testnames:
|
for name in testnames:
|
||||||
suite.addTest(testclass(rpc, rpc_userpass, type, path, fingerprint, master_xpub, password, emulator, interface, name))
|
suite.addTest(testclass(rpc, rpc_userpass, type, full_type, path, fingerprint, master_xpub, password, emulator, interface, name))
|
||||||
return suite
|
return suite
|
||||||
|
|
||||||
def do_command(self, args):
|
def do_command(self, args):
|
||||||
|
cli_args = []
|
||||||
|
for arg in args:
|
||||||
|
cli_args.append(shlex.quote(arg))
|
||||||
if self.interface == 'cli':
|
if self.interface == 'cli':
|
||||||
proc = subprocess.Popen(['hwi ' + ' '.join(args)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True)
|
proc = subprocess.Popen(['hwi ' + ' '.join(cli_args)], stdout=subprocess.PIPE, shell=True)
|
||||||
result = proc.communicate()
|
result = proc.communicate()
|
||||||
return json.loads(result[0].decode())
|
return json.loads(result[0].decode())
|
||||||
elif self.interface == 'bindist':
|
elif self.interface == 'bindist':
|
||||||
proc = subprocess.Popen(['../dist/hwi ' + ' '.join(args)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True)
|
proc = subprocess.Popen(['../dist/hwi ' + ' '.join(cli_args)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True)
|
||||||
result = proc.communicate()
|
result = proc.communicate()
|
||||||
return json.loads(result[0].decode())
|
return json.loads(result[0].decode())
|
||||||
elif self.interface == 'stdin':
|
elif self.interface == 'stdin':
|
||||||
@ -101,10 +106,10 @@ class DeviceTestCase(unittest.TestCase):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '{}: {}'.format(self.type, super().__str__())
|
return '{}: {}'.format(self.full_type, super().__str__())
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '{}: {}'.format(self.type, super().__repr__())
|
return '{}: {}'.format(self.full_type, super().__repr__())
|
||||||
|
|
||||||
class TestDeviceConnect(DeviceTestCase):
|
class TestDeviceConnect(DeviceTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -154,9 +159,9 @@ class TestDeviceConnect(DeviceTestCase):
|
|||||||
class TestGetKeypool(DeviceTestCase):
|
class TestGetKeypool(DeviceTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.rpc = AuthServiceProxy('http://{}@127.0.0.1:18443'.format(self.rpc_userpass))
|
self.rpc = AuthServiceProxy('http://{}@127.0.0.1:18443'.format(self.rpc_userpass))
|
||||||
if '{}_test'.format(self.type) not in self.rpc.listwallets():
|
if '{}_test'.format(self.full_type) not in self.rpc.listwallets():
|
||||||
self.rpc.createwallet('{}_test'.format(self.type), True)
|
self.rpc.createwallet('{}_test'.format(self.full_type), True)
|
||||||
self.wrpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/{}_test'.format(self.rpc_userpass, self.type))
|
self.wrpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/{}_test'.format(self.rpc_userpass, self.full_type))
|
||||||
self.wpk_rpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/'.format(self.rpc_userpass))
|
self.wpk_rpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/'.format(self.rpc_userpass))
|
||||||
if '--testnet' not in self.dev_args:
|
if '--testnet' not in self.dev_args:
|
||||||
self.dev_args.append('--testnet')
|
self.dev_args.append('--testnet')
|
||||||
@ -261,9 +266,9 @@ class TestGetKeypool(DeviceTestCase):
|
|||||||
class TestSignTx(DeviceTestCase):
|
class TestSignTx(DeviceTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.rpc = AuthServiceProxy('http://{}@127.0.0.1:18443'.format(self.rpc_userpass))
|
self.rpc = AuthServiceProxy('http://{}@127.0.0.1:18443'.format(self.rpc_userpass))
|
||||||
if '{}_test'.format(self.type) not in self.rpc.listwallets():
|
if '{}_test'.format(self.full_type) not in self.rpc.listwallets():
|
||||||
self.rpc.createwallet('{}_test'.format(self.type), True)
|
self.rpc.createwallet('{}_test'.format(self.full_type), True)
|
||||||
self.wrpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/{}_test'.format(self.rpc_userpass, self.type))
|
self.wrpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/{}_test'.format(self.rpc_userpass, self.full_type))
|
||||||
self.wpk_rpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/'.format(self.rpc_userpass))
|
self.wpk_rpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/'.format(self.rpc_userpass))
|
||||||
if '--testnet' not in self.dev_args:
|
if '--testnet' not in self.dev_args:
|
||||||
self.dev_args.append('--testnet')
|
self.dev_args.append('--testnet')
|
||||||
@ -405,13 +410,13 @@ class TestSignTx(DeviceTestCase):
|
|||||||
|
|
||||||
# Test wrapper to avoid mixed-inputs signing for Ledger
|
# Test wrapper to avoid mixed-inputs signing for Ledger
|
||||||
def test_signtx(self):
|
def test_signtx(self):
|
||||||
supports_mixed = {'coldcard', 'trezor', 'digitalbitbox', 'keepkey'}
|
supports_mixed = {'coldcard', 'trezor_1', 'digitalbitbox', 'keepkey'}
|
||||||
supports_multisig = {'ledger', 'trezor', 'digitalbitbox', 'keepkey'}
|
supports_multisig = {'ledger', 'trezor_1', 'digitalbitbox', 'keepkey'}
|
||||||
if self.type not in supports_mixed:
|
if self.full_type not in supports_mixed:
|
||||||
self._test_signtx("legacy", self.type in supports_multisig)
|
self._test_signtx("legacy", self.full_type in supports_multisig)
|
||||||
self._test_signtx("segwit", self.type in supports_multisig)
|
self._test_signtx("segwit", self.full_type in supports_multisig)
|
||||||
else:
|
else:
|
||||||
self._test_signtx("all", self.type in supports_multisig)
|
self._test_signtx("all", self.full_type in supports_multisig)
|
||||||
|
|
||||||
# Make a huge transaction which might cause some problems with different interfaces
|
# Make a huge transaction which might cause some problems with different interfaces
|
||||||
def test_big_tx(self):
|
def test_big_tx(self):
|
||||||
@ -456,45 +461,68 @@ class TestDisplayAddress(DeviceTestCase):
|
|||||||
self.assertEqual(result['code'], -7)
|
self.assertEqual(result['code'], -7)
|
||||||
|
|
||||||
def test_display_address_path(self):
|
def test_display_address_path(self):
|
||||||
self.do_command(self.dev_args + ['displayaddress', '--path', 'm/44h/1h/0h/0/0'])
|
result = self.do_command(self.dev_args + ['displayaddress', '--path', 'm/44h/1h/0h/0/0'])
|
||||||
self.do_command(self.dev_args + ['displayaddress', '--sh_wpkh', '--path', 'm/49h/1h/0h/0/0'])
|
self.assertNotIn('error', result)
|
||||||
self.do_command(self.dev_args + ['displayaddress', '--wpkh', '--path', 'm/84h/1h/0h/0/0'])
|
self.assertNotIn('code', result)
|
||||||
|
self.assertIn('address', result)
|
||||||
|
|
||||||
|
result = self.do_command(self.dev_args + ['displayaddress', '--sh_wpkh', '--path', 'm/49h/1h/0h/0/0'])
|
||||||
|
self.assertNotIn('error', result)
|
||||||
|
self.assertNotIn('code', result)
|
||||||
|
self.assertIn('address', result)
|
||||||
|
|
||||||
|
result = self.do_command(self.dev_args + ['displayaddress', '--wpkh', '--path', 'm/84h/1h/0h/0/0'])
|
||||||
|
self.assertNotIn('error', result)
|
||||||
|
self.assertNotIn('code', result)
|
||||||
|
self.assertIn('address', result)
|
||||||
|
|
||||||
def test_display_address_bad_path(self):
|
def test_display_address_bad_path(self):
|
||||||
result = self.do_command(self.dev_args + ['displayaddress', '--path', 'f'])
|
result = self.do_command(self.dev_args + ['displayaddress', '--path', 'f'])
|
||||||
self.assertEquals(result['code'], -7)
|
self.assertEquals(result['code'], -7)
|
||||||
|
|
||||||
def test_display_address_descriptor(self):
|
def test_display_address_descriptor(self):
|
||||||
account_xpub = process_commands(self.dev_args + ['getxpub', 'm/84h/1h/0h'])['xpub']
|
account_xpub = self.do_command(self.dev_args + ['getxpub', 'm/84h/1h/0h'])['xpub']
|
||||||
p2sh_segwit_account_xpub = process_commands(self.dev_args + ['getxpub', 'm/49h/1h/0h'])['xpub']
|
p2sh_segwit_account_xpub = self.do_command(self.dev_args + ['getxpub', 'm/49h/1h/0h'])['xpub']
|
||||||
legacy_account_xpub = process_commands(self.dev_args + ['getxpub', 'm/44h/1h/0h'])['xpub']
|
legacy_account_xpub = self.do_command(self.dev_args + ['getxpub', 'm/44h/1h/0h'])['xpub']
|
||||||
|
|
||||||
# Native SegWit address using xpub:
|
# Native SegWit address using xpub:
|
||||||
process_commands(self.dev_args + ['displayaddress', '--desc', 'wpkh([' + self.fingerprint + '/84h/1h/0h)]' + account_xpub + '/0/0)'])
|
result = self.do_command(self.dev_args + ['displayaddress', '--desc', 'wpkh([' + self.fingerprint + '/84h/1h/0h]' + account_xpub + '/0/0)'])
|
||||||
|
self.assertNotIn('error', result)
|
||||||
|
self.assertNotIn('code', result)
|
||||||
|
self.assertIn('address', result)
|
||||||
|
|
||||||
# Native SegWit address using hex encoded pubkey:
|
# Native SegWit address using hex encoded pubkey:
|
||||||
process_commands(self.dev_args + ['displayaddress', '--desc', 'wpkh([' + self.fingerprint + '/84h/1h/0h)]' + xpub_to_pub_hex(account_xpub) + '/0/0)'])
|
result = self.do_command(self.dev_args + ['displayaddress', '--desc', 'wpkh([' + self.fingerprint + '/84h/1h/0h]' + xpub_to_pub_hex(account_xpub) + '/0/0)'])
|
||||||
|
self.assertNotIn('error', result)
|
||||||
|
self.assertNotIn('code', result)
|
||||||
|
self.assertIn('address', result)
|
||||||
|
|
||||||
# P2SH wrapped SegWit address using xpub:
|
# P2SH wrapped SegWit address using xpub:
|
||||||
process_commands(self.dev_args + ['displayaddress', '--desc', 'sh(wpkh([' + self.fingerprint + '/49h/1h/0h)]' + p2sh_segwit_account_xpub + '/0/0))'])
|
result = self.do_command(self.dev_args + ['displayaddress', '--desc', 'sh(wpkh([' + self.fingerprint + '/49h/1h/0h]' + p2sh_segwit_account_xpub + '/0/0))'])
|
||||||
|
self.assertNotIn('error', result)
|
||||||
|
self.assertNotIn('code', result)
|
||||||
|
self.assertIn('address', result)
|
||||||
|
|
||||||
# Legacy address
|
# Legacy address
|
||||||
process_commands(self.dev_args + ['displayaddress', '--desc', 'pkh([' + self.fingerprint + '/44h/1h/0h)]' + legacy_account_xpub + '/0/0)'])
|
result = self.do_command(self.dev_args + ['displayaddress', '--desc', 'pkh([' + self.fingerprint + '/44h/1h/0h]' + legacy_account_xpub + '/0/0)'])
|
||||||
|
self.assertNotIn('error', result)
|
||||||
|
self.assertNotIn('code', result)
|
||||||
|
self.assertIn('address', result)
|
||||||
|
|
||||||
# Should check xpub
|
# Should check xpub
|
||||||
result = process_commands(self.dev_args + ['displayaddress', '--desc', 'wpkh([' + self.fingerprint + '/84h/1h/0h)]' + "not_and_xpub" + '/0/0)'])
|
result = self.do_command(self.dev_args + ['displayaddress', '--desc', 'wpkh([' + self.fingerprint + '/84h/1h/0h]' + "not_and_xpub" + '/0/0)'])
|
||||||
self.assertIn('error', result)
|
self.assertIn('error', result)
|
||||||
self.assertIn('code', result)
|
self.assertIn('code', result)
|
||||||
self.assertEqual(result['code'], -7)
|
self.assertEqual(result['code'], -7)
|
||||||
|
|
||||||
# Should check hex pub
|
# Should check hex pub
|
||||||
result = process_commands(self.dev_args + ['displayaddress', '--desc', 'wpkh([' + self.fingerprint + '/84h/1h/0h)]' + "not_and_xpub" + '/0/0)'])
|
result = self.do_command(self.dev_args + ['displayaddress', '--desc', 'wpkh([' + self.fingerprint + '/84h/1h/0h]' + "not_and_xpub" + '/0/0)'])
|
||||||
self.assertIn('error', result)
|
self.assertIn('error', result)
|
||||||
self.assertIn('code', result)
|
self.assertIn('code', result)
|
||||||
self.assertEqual(result['code'], -7)
|
self.assertEqual(result['code'], -7)
|
||||||
|
|
||||||
# Should check fingerprint
|
# Should check fingerprint
|
||||||
process_commands(self.dev_args + ['displayaddress', '--desc', 'wpkh([00000000/84h/1h/0h)]' + account_xpub + '/0/0)'])
|
self.do_command(self.dev_args + ['displayaddress', '--desc', 'wpkh([00000000/84h/1h/0h]' + account_xpub + '/0/0)'])
|
||||||
self.assertIn('error', result)
|
self.assertIn('error', result)
|
||||||
self.assertIn('code', result)
|
self.assertIn('code', result)
|
||||||
self.assertEqual(result['code'], -7)
|
self.assertEqual(result['code'], -7)
|
||||||
|
|||||||
@ -37,6 +37,7 @@ def digitalbitbox_test_suite(rpc, userpass, simulator, interface):
|
|||||||
|
|
||||||
# params
|
# params
|
||||||
type = 'digitalbitbox'
|
type = 'digitalbitbox'
|
||||||
|
full_type = 'digitalbitbox'
|
||||||
path = 'udp:127.0.0.1:35345'
|
path = 'udp:127.0.0.1:35345'
|
||||||
fingerprint = 'a31b978a'
|
fingerprint = 'a31b978a'
|
||||||
master_xpub = 'xpub6BsWJiRvbzQJg3J6tgUKmHWYbHJSj41EjAAje6LuDwnYLqLiNSWK4N7rCXwiUmNJTBrKL8AEH3LBzhJdgdxoy4T9aMPLCWAa6eWKGCFjQhq'
|
master_xpub = 'xpub6BsWJiRvbzQJg3J6tgUKmHWYbHJSj41EjAAje6LuDwnYLqLiNSWK4N7rCXwiUmNJTBrKL8AEH3LBzhJdgdxoy4T9aMPLCWAa6eWKGCFjQhq'
|
||||||
@ -93,7 +94,7 @@ def digitalbitbox_test_suite(rpc, userpass, simulator, interface):
|
|||||||
self.assertTrue(result['success'])
|
self.assertTrue(result['success'])
|
||||||
|
|
||||||
# Reset back to original
|
# Reset back to original
|
||||||
result = process_commands(self.dev_args + ['wipe'])
|
result = self.do_command(self.dev_args + ['wipe'])
|
||||||
self.assertTrue(result['success'])
|
self.assertTrue(result['success'])
|
||||||
send_plain(b'{"password":"0000"}', dev)
|
send_plain(b'{"password":"0000"}', dev)
|
||||||
send_encrypt(json.dumps({"seed":{"source":"backup","filename":"test_backup.pdf","key":"key"}}), '0000', dev)
|
send_encrypt(json.dumps({"seed":{"source":"backup","filename":"test_backup.pdf","key":"key"}}), '0000', dev)
|
||||||
@ -126,11 +127,11 @@ def digitalbitbox_test_suite(rpc, userpass, simulator, interface):
|
|||||||
|
|
||||||
# Generic Device tests
|
# Generic Device tests
|
||||||
suite = unittest.TestSuite()
|
suite = unittest.TestSuite()
|
||||||
suite.addTest(DeviceTestCase.parameterize(TestDBBManCommands, rpc, userpass, type, path, fingerprint, master_xpub, '0000', interface=interface))
|
suite.addTest(DeviceTestCase.parameterize(TestDBBManCommands, rpc, userpass, type, full_type, path, fingerprint, master_xpub, '0000', interface=interface))
|
||||||
suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, type, path, fingerprint, master_xpub, '0000', interface=interface))
|
suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, type, full_type, path, fingerprint, master_xpub, '0000', interface=interface))
|
||||||
suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, rpc, userpass, type, path, fingerprint, master_xpub, '0000', interface=interface))
|
suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, rpc, userpass, type, full_type, path, fingerprint, master_xpub, '0000', interface=interface))
|
||||||
suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, type, path, fingerprint, master_xpub, '0000', interface=interface))
|
suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, type, full_type, path, fingerprint, master_xpub, '0000', interface=interface))
|
||||||
suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, type, path, fingerprint, master_xpub, '0000', interface=interface))
|
suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, type, full_type, path, fingerprint, master_xpub, '0000', interface=interface))
|
||||||
return suite
|
return suite
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import argparse
|
|||||||
import atexit
|
import atexit
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import shlex
|
||||||
import socket
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
@ -82,12 +83,15 @@ class KeepkeyTestCase(unittest.TestCase):
|
|||||||
return suite
|
return suite
|
||||||
|
|
||||||
def do_command(self, args):
|
def do_command(self, args):
|
||||||
|
cli_args = []
|
||||||
|
for arg in args:
|
||||||
|
cli_args.append(shlex.quote(arg))
|
||||||
if self.interface == 'cli':
|
if self.interface == 'cli':
|
||||||
proc = subprocess.Popen(['hwi ' + ' '.join(args)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True)
|
proc = subprocess.Popen(['hwi ' + ' '.join(cli_args)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True)
|
||||||
result = proc.communicate()
|
result = proc.communicate()
|
||||||
return json.loads(result[0].decode())
|
return json.loads(result[0].decode())
|
||||||
elif self.interface == 'bindist':
|
elif self.interface == 'bindist':
|
||||||
proc = subprocess.Popen(['../dist/hwi ' + ' '.join(args)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True)
|
proc = subprocess.Popen(['../dist/hwi ' + ' '.join(cli_args)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True)
|
||||||
result = proc.communicate()
|
result = proc.communicate()
|
||||||
return json.loads(result[0].decode())
|
return json.loads(result[0].decode())
|
||||||
elif self.interface == 'stdin':
|
elif self.interface == 'stdin':
|
||||||
@ -176,11 +180,19 @@ class TestKeepkeyManCommands(KeepkeyTestCase):
|
|||||||
result = self.do_command(self.dev_args + ['sendpin', '1234'])
|
result = self.do_command(self.dev_args + ['sendpin', '1234'])
|
||||||
self.assertEqual(result['error'], 'This device does not need a PIN')
|
self.assertEqual(result['error'], 'This device does not need a PIN')
|
||||||
self.assertEqual(result['code'], -11)
|
self.assertEqual(result['code'], -11)
|
||||||
|
result = self.do_command(self.dev_args + ['enumerate'])
|
||||||
|
for dev in result:
|
||||||
|
if dev['type'] == 'trezor' and dev['path'] == 'udp:127.0.0.1:21324':
|
||||||
|
self.assertFalse(dev['needs_pin_sent'])
|
||||||
|
|
||||||
# Set a PIN
|
# Set a PIN
|
||||||
device.wipe(self.client)
|
device.wipe(self.client)
|
||||||
load_device_by_mnemonic(client=self.client, mnemonic='alcohol woman abuse must during monitor noble actual mixed trade anger aisle', pin='1234', passphrase_protection=False, label='test')
|
load_device_by_mnemonic(client=self.client, mnemonic='alcohol woman abuse must during monitor noble actual mixed trade anger aisle', pin='1234', passphrase_protection=False, label='test')
|
||||||
self.client.call(messages.ClearSession())
|
self.client.call(messages.ClearSession())
|
||||||
|
result = self.do_command(self.dev_args + ['enumerate'])
|
||||||
|
for dev in result:
|
||||||
|
if dev['type'] == 'trezor' and dev['path'] == 'udp:127.0.0.1:21324':
|
||||||
|
self.assertTrue(dev['needs_pin_sent'])
|
||||||
result = self.do_command(self.dev_args + ['promptpin'])
|
result = self.do_command(self.dev_args + ['promptpin'])
|
||||||
self.assertTrue(result['success'])
|
self.assertTrue(result['success'])
|
||||||
|
|
||||||
@ -208,6 +220,11 @@ class TestKeepkeyManCommands(KeepkeyTestCase):
|
|||||||
result = self.do_command(self.dev_args + ['sendpin', pin])
|
result = self.do_command(self.dev_args + ['sendpin', pin])
|
||||||
self.assertTrue(result['success'])
|
self.assertTrue(result['success'])
|
||||||
|
|
||||||
|
result = self.do_command(self.dev_args + ['enumerate'])
|
||||||
|
for dev in result:
|
||||||
|
if dev['type'] == 'trezor' and dev['path'] == 'udp:127.0.0.1:21324':
|
||||||
|
self.assertFalse(dev['needs_pin_sent'])
|
||||||
|
|
||||||
# Sending PIN after unlock
|
# Sending PIN after unlock
|
||||||
result = self.do_command(self.dev_args + ['promptpin'])
|
result = self.do_command(self.dev_args + ['promptpin'])
|
||||||
self.assertEqual(result['error'], 'The PIN has already been sent to this device')
|
self.assertEqual(result['error'], 'The PIN has already been sent to this device')
|
||||||
@ -216,12 +233,58 @@ class TestKeepkeyManCommands(KeepkeyTestCase):
|
|||||||
self.assertEqual(result['error'], 'The PIN has already been sent to this device')
|
self.assertEqual(result['error'], 'The PIN has already been sent to this device')
|
||||||
self.assertEqual(result['code'], -11)
|
self.assertEqual(result['code'], -11)
|
||||||
|
|
||||||
|
def test_passphrase(self):
|
||||||
|
# There's no passphrase
|
||||||
|
result = self.do_command(self.dev_args + ['enumerate'])
|
||||||
|
for dev in result:
|
||||||
|
if dev['type'] == 'keepkey' and dev['path'] == 'udp:127.0.0.1:21324':
|
||||||
|
self.assertFalse(dev['needs_passphrase_sent'])
|
||||||
|
self.assertEquals(dev['fingerprint'], '95d8f670')
|
||||||
|
# Setting a passphrase won't change the fingerprint
|
||||||
|
result = self.do_command(self.dev_args + ['-p', 'pass', 'enumerate'])
|
||||||
|
for dev in result:
|
||||||
|
if dev['type'] == 'keepkey' and dev['path'] == 'udp:127.0.0.1:21324':
|
||||||
|
self.assertFalse(dev['needs_passphrase_sent'])
|
||||||
|
self.assertEquals(dev['fingerprint'], '95d8f670')
|
||||||
|
|
||||||
|
# Set a passphrase
|
||||||
|
device.wipe(self.client)
|
||||||
|
self.client.set_passphrase('pass')
|
||||||
|
load_device_by_mnemonic(client=self.client, mnemonic='alcohol woman abuse must during monitor noble actual mixed trade anger aisle', pin='', passphrase_protection=True, label='test')
|
||||||
|
self.client.call(messages.ClearSession())
|
||||||
|
|
||||||
|
# A passphrase will need to be sent
|
||||||
|
result = self.do_command(self.dev_args + ['enumerate'])
|
||||||
|
for dev in result:
|
||||||
|
if dev['type'] == 'keepkey' and dev['path'] == 'udp:127.0.0.1:21324':
|
||||||
|
self.assertTrue(dev['needs_passphrase_sent'])
|
||||||
|
result = self.do_command(self.dev_args + ['-p', 'pass', 'enumerate'])
|
||||||
|
for dev in result:
|
||||||
|
if dev['type'] == 'keepkey' and dev['path'] == 'udp:127.0.0.1:21324':
|
||||||
|
self.assertFalse(dev['needs_passphrase_sent'])
|
||||||
|
fpr = dev['fingerprint']
|
||||||
|
# A different passphrase would not change the fingerprint
|
||||||
|
result = self.do_command(self.dev_args + ['-p', 'pass2', 'enumerate'])
|
||||||
|
for dev in result:
|
||||||
|
if dev['type'] == 'keepkey' and dev['path'] == 'udp:127.0.0.1:21324':
|
||||||
|
self.assertFalse(dev['needs_passphrase_sent'])
|
||||||
|
self.assertEqual(dev['fingerprint'], fpr)
|
||||||
|
|
||||||
|
# Clearing the session and starting a new one with a new passphrase should change the passphrase
|
||||||
|
self.client.call(messages.ClearSession())
|
||||||
|
result = self.do_command(self.dev_args + ['-p', 'pass3', 'enumerate'])
|
||||||
|
for dev in result:
|
||||||
|
if dev['type'] == 'keepkey' and dev['path'] == 'udp:127.0.0.1:21324':
|
||||||
|
self.assertFalse(dev['needs_passphrase_sent'])
|
||||||
|
self.assertNotEqual(dev['fingerprint'], fpr)
|
||||||
|
|
||||||
def keepkey_test_suite(emulator, rpc, userpass, interface):
|
def keepkey_test_suite(emulator, rpc, userpass, interface):
|
||||||
# Redirect stderr to /dev/null as it's super spammy
|
# Redirect stderr to /dev/null as it's super spammy
|
||||||
sys.stderr = open(os.devnull, 'w')
|
sys.stderr = open(os.devnull, 'w')
|
||||||
|
|
||||||
# Device info for tests
|
# Device info for tests
|
||||||
type = 'keepkey'
|
type = 'keepkey'
|
||||||
|
full_type = 'keepkey'
|
||||||
path = 'udp:127.0.0.1:21324'
|
path = 'udp:127.0.0.1:21324'
|
||||||
fingerprint = '95d8f670'
|
fingerprint = '95d8f670'
|
||||||
master_xpub = 'xpub6D1weXBcFAo8CqBbpP4TbH5sxQH8ZkqC5pDEvJ95rNNBZC9zrKmZP2fXMuve7ZRBe18pWQQsGg68jkq24mZchHwYENd8cCiSb71u3KD4AFH'
|
master_xpub = 'xpub6D1weXBcFAo8CqBbpP4TbH5sxQH8ZkqC5pDEvJ95rNNBZC9zrKmZP2fXMuve7ZRBe18pWQQsGg68jkq24mZchHwYENd8cCiSb71u3KD4AFH'
|
||||||
@ -229,11 +292,11 @@ def keepkey_test_suite(emulator, rpc, userpass, interface):
|
|||||||
|
|
||||||
# Generic Device tests
|
# Generic Device tests
|
||||||
suite = unittest.TestSuite()
|
suite = unittest.TestSuite()
|
||||||
suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
|
suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, type, full_type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
|
||||||
suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, rpc, userpass, type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
|
suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, rpc, userpass, type, full_type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
|
||||||
suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
|
suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, type, full_type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
|
||||||
suite.addTest(DeviceTestCase.parameterize(TestDisplayAddress, rpc, userpass, type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
|
suite.addTest(DeviceTestCase.parameterize(TestDisplayAddress, rpc, userpass, type, full_type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
|
||||||
suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
|
suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, type, full_type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
|
||||||
suite.addTest(KeepkeyTestCase.parameterize(TestKeepkeyGetxpub, emulator=dev_emulator, interface=interface))
|
suite.addTest(KeepkeyTestCase.parameterize(TestKeepkeyGetxpub, emulator=dev_emulator, interface=interface))
|
||||||
suite.addTest(KeepkeyTestCase.parameterize(TestKeepkeyManCommands, emulator=dev_emulator, interface=interface))
|
suite.addTest(KeepkeyTestCase.parameterize(TestKeepkeyManCommands, emulator=dev_emulator, interface=interface))
|
||||||
return suite
|
return suite
|
||||||
|
|||||||
@ -73,12 +73,12 @@ def ledger_test_suite(rpc, userpass, interface):
|
|||||||
|
|
||||||
# Generic Device tests
|
# Generic Device tests
|
||||||
suite = unittest.TestSuite()
|
suite = unittest.TestSuite()
|
||||||
suite.addTest(DeviceTestCase.parameterize(TestLedgerDisabledCommands, rpc, userpass, 'ledger', path, fingerprint, master_xpub, interface=interface))
|
suite.addTest(DeviceTestCase.parameterize(TestLedgerDisabledCommands, rpc, userpass, 'ledger', 'ledger', path, fingerprint, master_xpub, interface=interface))
|
||||||
suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, 'ledger', path, fingerprint, master_xpub, interface=interface))
|
suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, 'ledger', 'ledger', path, fingerprint, master_xpub, interface=interface))
|
||||||
suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, rpc, userpass, 'ledger', path, fingerprint, master_xpub, interface=interface))
|
suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, rpc, userpass, 'ledger', 'ledger', path, fingerprint, master_xpub, interface=interface))
|
||||||
suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, 'ledger', path, fingerprint, master_xpub, interface=interface))
|
suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, 'ledger', 'ledger', path, fingerprint, master_xpub, interface=interface))
|
||||||
suite.addTest(DeviceTestCase.parameterize(TestDisplayAddress, rpc, userpass, 'ledger', path, fingerprint, master_xpub, interface=interface))
|
suite.addTest(DeviceTestCase.parameterize(TestDisplayAddress, rpc, userpass, 'ledger', 'ledger', path, fingerprint, master_xpub, interface=interface))
|
||||||
suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, 'ledger', path, fingerprint, master_xpub, interface=interface))
|
suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, 'ledger', 'ledger', path, fingerprint, master_xpub, interface=interface))
|
||||||
return suite
|
return suite
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import argparse
|
|||||||
import atexit
|
import atexit
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import shlex
|
||||||
|
import signal
|
||||||
import socket
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
@ -35,7 +37,7 @@ class TrezorEmulator(DeviceEmulator):
|
|||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
# Start the Trezor emulator
|
# Start the Trezor emulator
|
||||||
self.emulator_proc = subprocess.Popen(['./' + os.path.basename(self.emulator_path)], cwd=os.path.dirname(self.emulator_path))
|
self.emulator_proc = subprocess.Popen(['./' + os.path.basename(self.emulator_path)], cwd=os.path.dirname(self.emulator_path), stdout=subprocess.DEVNULL, env={'SDL_VIDEODRIVER': 'dummy', 'PYOPT': '0'}, shell=True, preexec_fn=os.setsid)
|
||||||
# Wait for emulator to be up
|
# Wait for emulator to be up
|
||||||
# From https://github.com/trezor/trezor-mcu/blob/master/script/wait_for_emulator.py
|
# From https://github.com/trezor/trezor-mcu/blob/master/script/wait_for_emulator.py
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
@ -63,8 +65,8 @@ class TrezorEmulator(DeviceEmulator):
|
|||||||
return client
|
return client
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.emulator_proc.kill()
|
os.killpg(os.getpgid(self.emulator_proc.pid), signal.SIGINT)
|
||||||
self.emulator_proc.wait()
|
os.waitpid(self.emulator_proc.pid, 0)
|
||||||
|
|
||||||
class TrezorTestCase(unittest.TestCase):
|
class TrezorTestCase(unittest.TestCase):
|
||||||
def __init__(self, emulator, interface='library', methodName='runTest'):
|
def __init__(self, emulator, interface='library', methodName='runTest'):
|
||||||
@ -82,12 +84,15 @@ class TrezorTestCase(unittest.TestCase):
|
|||||||
return suite
|
return suite
|
||||||
|
|
||||||
def do_command(self, args):
|
def do_command(self, args):
|
||||||
|
cli_args = []
|
||||||
|
for arg in args:
|
||||||
|
cli_args.append(shlex.quote(arg))
|
||||||
if self.interface == 'cli':
|
if self.interface == 'cli':
|
||||||
proc = subprocess.Popen(['hwi ' + ' '.join(args)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True)
|
proc = subprocess.Popen(['hwi ' + ' '.join(cli_args)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True)
|
||||||
result = proc.communicate()
|
result = proc.communicate()
|
||||||
return json.loads(result[0].decode())
|
return json.loads(result[0].decode())
|
||||||
elif self.interface == 'bindist':
|
elif self.interface == 'bindist':
|
||||||
proc = subprocess.Popen(['../dist/hwi ' + ' '.join(args)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True)
|
proc = subprocess.Popen(['../dist/hwi ' + ' '.join(cli_args)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True)
|
||||||
result = proc.communicate()
|
result = proc.communicate()
|
||||||
return json.loads(result[0].decode())
|
return json.loads(result[0].decode())
|
||||||
elif self.interface == 'stdin':
|
elif self.interface == 'stdin':
|
||||||
@ -99,10 +104,10 @@ class TrezorTestCase(unittest.TestCase):
|
|||||||
return process_commands(args)
|
return process_commands(args)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return 'trezor: {}'.format(super().__str__())
|
return 'trezor 1: {}'.format(super().__str__())
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'trezor: {}'.format(super().__repr__())
|
return 'trezor 1: {}'.format(super().__repr__())
|
||||||
|
|
||||||
# Trezor specific getxpub test because this requires device specific thing to set xprvs
|
# Trezor specific getxpub test because this requires device specific thing to set xprvs
|
||||||
class TestTrezorGetxpub(TrezorTestCase):
|
class TestTrezorGetxpub(TrezorTestCase):
|
||||||
@ -176,11 +181,19 @@ class TestTrezorManCommands(TrezorTestCase):
|
|||||||
result = self.do_command(self.dev_args + ['sendpin', '1234'])
|
result = self.do_command(self.dev_args + ['sendpin', '1234'])
|
||||||
self.assertEqual(result['error'], 'This device does not need a PIN')
|
self.assertEqual(result['error'], 'This device does not need a PIN')
|
||||||
self.assertEqual(result['code'], -11)
|
self.assertEqual(result['code'], -11)
|
||||||
|
result = self.do_command(self.dev_args + ['enumerate'])
|
||||||
|
for dev in result:
|
||||||
|
if dev['type'] == 'trezor' and dev['path'] == 'udp:127.0.0.1:21324':
|
||||||
|
self.assertFalse(dev['needs_pin_sent'])
|
||||||
|
|
||||||
# Set a PIN
|
# Set a PIN
|
||||||
device.wipe(self.client)
|
device.wipe(self.client)
|
||||||
load_device_by_mnemonic(client=self.client, mnemonic='alcohol woman abuse must during monitor noble actual mixed trade anger aisle', pin='1234', passphrase_protection=False, label='test')
|
load_device_by_mnemonic(client=self.client, mnemonic='alcohol woman abuse must during monitor noble actual mixed trade anger aisle', pin='1234', passphrase_protection=False, label='test')
|
||||||
self.client.call(messages.ClearSession())
|
self.client.call(messages.ClearSession())
|
||||||
|
result = self.do_command(self.dev_args + ['enumerate'])
|
||||||
|
for dev in result:
|
||||||
|
if dev['type'] == 'trezor' and dev['path'] == 'udp:127.0.0.1:21324':
|
||||||
|
self.assertTrue(dev['needs_pin_sent'])
|
||||||
result = self.do_command(self.dev_args + ['promptpin'])
|
result = self.do_command(self.dev_args + ['promptpin'])
|
||||||
self.assertTrue(result['success'])
|
self.assertTrue(result['success'])
|
||||||
|
|
||||||
@ -208,6 +221,11 @@ class TestTrezorManCommands(TrezorTestCase):
|
|||||||
result = self.do_command(self.dev_args + ['sendpin', pin])
|
result = self.do_command(self.dev_args + ['sendpin', pin])
|
||||||
self.assertTrue(result['success'])
|
self.assertTrue(result['success'])
|
||||||
|
|
||||||
|
result = self.do_command(self.dev_args + ['enumerate'])
|
||||||
|
for dev in result:
|
||||||
|
if dev['type'] == 'trezor' and dev['path'] == 'udp:127.0.0.1:21324':
|
||||||
|
self.assertFalse(dev['needs_pin_sent'])
|
||||||
|
|
||||||
# Sending PIN after unlock
|
# Sending PIN after unlock
|
||||||
result = self.do_command(self.dev_args + ['promptpin'])
|
result = self.do_command(self.dev_args + ['promptpin'])
|
||||||
self.assertEqual(result['error'], 'The PIN has already been sent to this device')
|
self.assertEqual(result['error'], 'The PIN has already been sent to this device')
|
||||||
@ -216,7 +234,51 @@ class TestTrezorManCommands(TrezorTestCase):
|
|||||||
self.assertEqual(result['error'], 'The PIN has already been sent to this device')
|
self.assertEqual(result['error'], 'The PIN has already been sent to this device')
|
||||||
self.assertEqual(result['code'], -11)
|
self.assertEqual(result['code'], -11)
|
||||||
|
|
||||||
def trezor_test_suite(emulator, rpc, userpass, interface):
|
def test_passphrase(self):
|
||||||
|
# There's no passphrase
|
||||||
|
result = self.do_command(self.dev_args + ['enumerate'])
|
||||||
|
for dev in result:
|
||||||
|
if dev['type'] == 'trezor' and dev['path'] == 'udp:127.0.0.1:21324':
|
||||||
|
self.assertFalse(dev['needs_passphrase_sent'])
|
||||||
|
self.assertEquals(dev['fingerprint'], '95d8f670')
|
||||||
|
# Setting a passphrase won't change the fingerprint
|
||||||
|
result = self.do_command(self.dev_args + ['-p', 'pass', 'enumerate'])
|
||||||
|
for dev in result:
|
||||||
|
if dev['type'] == 'trezor' and dev['path'] == 'udp:127.0.0.1:21324':
|
||||||
|
self.assertFalse(dev['needs_passphrase_sent'])
|
||||||
|
self.assertEquals(dev['fingerprint'], '95d8f670')
|
||||||
|
|
||||||
|
# Set a passphrase
|
||||||
|
device.wipe(self.client)
|
||||||
|
load_device_by_mnemonic(client=self.client, mnemonic='alcohol woman abuse must during monitor noble actual mixed trade anger aisle', pin='', passphrase_protection=True, label='test')
|
||||||
|
self.client.call(messages.ClearSession())
|
||||||
|
|
||||||
|
# A passphrase will need to be sent
|
||||||
|
result = self.do_command(self.dev_args + ['enumerate'])
|
||||||
|
for dev in result:
|
||||||
|
if dev['type'] == 'trezor' and dev['path'] == 'udp:127.0.0.1:21324':
|
||||||
|
self.assertTrue(dev['needs_passphrase_sent'])
|
||||||
|
result = self.do_command(self.dev_args + ['-p', 'pass', 'enumerate'])
|
||||||
|
for dev in result:
|
||||||
|
if dev['type'] == 'trezor' and dev['path'] == 'udp:127.0.0.1:21324':
|
||||||
|
self.assertFalse(dev['needs_passphrase_sent'])
|
||||||
|
fpr = dev['fingerprint']
|
||||||
|
# A different passphrase would not change the fingerprint
|
||||||
|
result = self.do_command(self.dev_args + ['-p', 'pass2', 'enumerate'])
|
||||||
|
for dev in result:
|
||||||
|
if dev['type'] == 'trezor' and dev['path'] == 'udp:127.0.0.1:21324':
|
||||||
|
self.assertFalse(dev['needs_passphrase_sent'])
|
||||||
|
self.assertEqual(dev['fingerprint'], fpr)
|
||||||
|
|
||||||
|
# Clearing the session and starting a new one with a new passphrase should change the passphrase
|
||||||
|
self.client.call(messages.Initialize())
|
||||||
|
result = self.do_command(self.dev_args + ['-p', 'pass3', 'enumerate'])
|
||||||
|
for dev in result:
|
||||||
|
if dev['type'] == 'trezor' and dev['path'] == 'udp:127.0.0.1:21324':
|
||||||
|
self.assertFalse(dev['needs_passphrase_sent'])
|
||||||
|
self.assertNotEqual(dev['fingerprint'], fpr)
|
||||||
|
|
||||||
|
def trezor_test_suite(emulator, rpc, userpass, interface, model_t=False):
|
||||||
# Redirect stderr to /dev/null as it's super spammy
|
# Redirect stderr to /dev/null as it's super spammy
|
||||||
sys.stderr = open(os.devnull, 'w')
|
sys.stderr = open(os.devnull, 'w')
|
||||||
|
|
||||||
@ -227,15 +289,21 @@ def trezor_test_suite(emulator, rpc, userpass, interface):
|
|||||||
master_xpub = 'xpub6D1weXBcFAo8CqBbpP4TbH5sxQH8ZkqC5pDEvJ95rNNBZC9zrKmZP2fXMuve7ZRBe18pWQQsGg68jkq24mZchHwYENd8cCiSb71u3KD4AFH'
|
master_xpub = 'xpub6D1weXBcFAo8CqBbpP4TbH5sxQH8ZkqC5pDEvJ95rNNBZC9zrKmZP2fXMuve7ZRBe18pWQQsGg68jkq24mZchHwYENd8cCiSb71u3KD4AFH'
|
||||||
dev_emulator = TrezorEmulator(emulator)
|
dev_emulator = TrezorEmulator(emulator)
|
||||||
|
|
||||||
|
if model_t:
|
||||||
|
full_type = 'trezor_t'
|
||||||
|
else:
|
||||||
|
full_type = 'trezor_1'
|
||||||
|
|
||||||
# Generic Device tests
|
# Generic Device tests
|
||||||
suite = unittest.TestSuite()
|
suite = unittest.TestSuite()
|
||||||
suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
|
suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, type, full_type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
|
||||||
suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, rpc, userpass, type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
|
suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, rpc, userpass, type, full_type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
|
||||||
suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
|
suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, type, full_type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
|
||||||
suite.addTest(DeviceTestCase.parameterize(TestDisplayAddress, rpc, userpass, type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
|
suite.addTest(DeviceTestCase.parameterize(TestDisplayAddress, rpc, userpass, type, full_type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
|
||||||
suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
|
suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, type, full_type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
|
||||||
suite.addTest(TrezorTestCase.parameterize(TestTrezorGetxpub, emulator=dev_emulator, interface=interface))
|
if not model_t:
|
||||||
suite.addTest(TrezorTestCase.parameterize(TestTrezorManCommands, emulator=dev_emulator, interface=interface))
|
suite.addTest(TrezorTestCase.parameterize(TestTrezorGetxpub, emulator=dev_emulator, interface=interface))
|
||||||
|
suite.addTest(TrezorTestCase.parameterize(TestTrezorManCommands, emulator=dev_emulator, interface=interface))
|
||||||
return suite
|
return suite
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
@ -243,10 +311,11 @@ if __name__ == '__main__':
|
|||||||
parser.add_argument('emulator', help='Path to the Trezor emulator')
|
parser.add_argument('emulator', help='Path to the Trezor emulator')
|
||||||
parser.add_argument('bitcoind', help='Path to bitcoind binary')
|
parser.add_argument('bitcoind', help='Path to bitcoind binary')
|
||||||
parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli', 'bindist'], default='library')
|
parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli', 'bindist'], default='library')
|
||||||
|
parser.add_argument('--model_t', help='The emulator is for the Trezor T', action='store_true')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# Start bitcoind
|
# Start bitcoind
|
||||||
rpc, userpass = start_bitcoind(args.bitcoind)
|
rpc, userpass = start_bitcoind(args.bitcoind)
|
||||||
|
|
||||||
suite = trezor_test_suite(args.emulator, rpc, userpass, args.interface)
|
suite = trezor_test_suite(args.emulator, rpc, userpass, args.interface, args.model_t)
|
||||||
unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite)
|
unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite)
|
||||||
|
|||||||
39
test/test_udevrules.py
Executable file
39
test/test_udevrules.py
Executable file
@ -0,0 +1,39 @@
|
|||||||
|
#! /usr/bin/env python3
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import json
|
||||||
|
import filecmp
|
||||||
|
from os import makedirs, remove, removedirs, walk, path
|
||||||
|
from hwilib.cli import process_commands
|
||||||
|
|
||||||
|
class TestUdevRulesInstaller(unittest.TestCase):
|
||||||
|
INSTALLATION_FOLDER = 'rules.d'
|
||||||
|
SOURCE_FOLDER = '../udev'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
# Create directory where copy the udev rules to.
|
||||||
|
makedirs(cls.INSTALLATION_FOLDER, exist_ok=True)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(self):
|
||||||
|
for root, dirs, files in walk(self.INSTALLATION_FOLDER, topdown=False):
|
||||||
|
for name in files:
|
||||||
|
remove(path.join(root, name))
|
||||||
|
removedirs(self.INSTALLATION_FOLDER)
|
||||||
|
|
||||||
|
def test_rules_file_are_copied(self):
|
||||||
|
result = process_commands( ['installudevrules', '--source', self.SOURCE_FOLDER, '--location', self.INSTALLATION_FOLDER])
|
||||||
|
self.assertIn('error', result)
|
||||||
|
self.assertIn('code', result)
|
||||||
|
self.assertEqual(result['error'], 'Need to be root.')
|
||||||
|
self.assertEqual(result['code'], -16)
|
||||||
|
# Assert files wre copied
|
||||||
|
for root, dirs, files in walk(self.INSTALLATION_FOLDER, topdown=False):
|
||||||
|
for file_name in files:
|
||||||
|
src = path.join(self.SOURCE_FOLDER, file_name)
|
||||||
|
tgt = path.join(self.INSTALLATION_FOLDER, file_name)
|
||||||
|
self.assertTrue(filecmp.cmp(src, tgt))
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Loading…
Reference in New Issue
Block a user