diff --git a/.python-version b/.python-version index c49282585..424e1794d 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.5.6 +3.6.8 diff --git a/.travis.yml b/.travis.yml index aa45112b4..94a8d57c5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: python os: linux dist: xenial python: - - '3.5' + - '3.6.8' cache: pip: true ccache: true @@ -40,7 +40,7 @@ addons: - cython3 - ccache install: - - pip install pipenv pysdl2 python-bitcoinrpc protobuf + - pip install pipenv pysdl2 python-bitcoinrpc protobuf poetry # 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" - unzip "protoc-3.4.0-linux-x86_64.zip" -d protoc @@ -48,10 +48,33 @@ install: # Build emulators/simulators and bitcoind - cd test; ./setup_environment.sh; cd .. - pip uninstall -y trezor # Hack to get rid of master branch version of trezor that is installed for trezor-mcu build - - python setup.py install + - poetry install jobs: include: - name: With process_commands interface - script: cd test; ./run_tests.py --interface=library + script: cd test; poetry run ./run_tests.py --interface=library - name: With command line interface - script: cd test; ./run_tests.py --interface=cli + script: cd test; poetry run ./run_tests.py --interface=cli + - name: With linux binary distribution command line interface + services: docker + before_script: + - docker build -t hwi-builder -f contrib/build.Dockerfile . + script: + - docker run -it --name hwi-builder -v $PWD:/opt/hwi --rm --workdir /opt/hwi hwi-builder /bin/bash -c "contrib/build_bin.sh && contrib/build_dist.sh" + - sudo chown -R `whoami`:`whoami` dist/ + - cd test; poetry run ./run_tests.py --interface=bindist + - cd ..; sha256sum dist/* + - name: macOS binary distribution (no tests) + os: osx + osx_image: xcode7.3 + language: generic + addons: + artifacts: + working_dir: dist + install: + - brew update && brew upgrade pyenv + - brew install libusb + - cat contrib/reproducible-python.diff | PYTHON_CONFIGURE_OPTS="--enable-framework" BUILD_DATE="Jan 1 2019" BUILD_TIME="00:00:00" pyenv install -kp 3.6.8 + script: + - contrib/build_bin.sh + - shasum -a 256 dist/* diff --git a/contrib/README.md b/contrib/README.md new file mode 100644 index 000000000..f484e2d95 --- /dev/null +++ b/contrib/README.md @@ -0,0 +1,21 @@ +# Assorted tools + +## `build_bin.sh` + +Creates a virtualenv with the locked dependencies using Poetry. Then uses pyinstaller to create a standalone binary for the OS type currently running. + +## `build_dist.sh` + +Creates a virtualenv with the locked dependencies using Poetry. Then uses Poetry to produce deterministic builds of the wheel and sdist for upload to PyPi + +`faketime` needs to be installed + +## `build_wine.sh` + +Sets up Wine with Python and everything needed to build Windows binaries. Creates a virtualenv with the locked dependencies using Poetry. Then uses pyinstaller to create a standalone Windows binary. + +`wine` needs to be installed + +## `generate_setup.sh` + +Builds the source distribution and extracts the setup.py from it. diff --git a/contrib/build.Dockerfile b/contrib/build.Dockerfile new file mode 100644 index 000000000..d588ed1b0 --- /dev/null +++ b/contrib/build.Dockerfile @@ -0,0 +1,48 @@ +FROM debian:stretch-slim + +SHELL ["/bin/bash", "-c"] + +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update +RUN apt-get install -y \ + apt-transport-https \ + git \ + make \ + build-essential \ + libssl-dev \ + zlib1g-dev \ + libbz2-dev \ + libreadline-dev \ + libsqlite3-dev \ + wget \ + curl \ + llvm \ + libncurses5-dev \ + xz-utils \ + libxml2-dev \ + libxmlsec1-dev \ + libffi-dev \ + liblzma-dev \ + libusb-1.0-0-dev \ + libudev-dev \ + faketime + +RUN curl https://pyenv.run | bash +ENV PATH="/root/.pyenv/bin:$PATH" +COPY contrib/reproducible-python.diff /opt/reproducible-python.diff +ENV PYTHON_CONFIGURE_OPTS="--enable-shared" +ENV BUILD_DATE="Jan 1 2019" +ENV BUILD_TIME="00:00:00" +RUN eval "$(pyenv init -)" && eval "$(pyenv virtualenv-init -)" && cat /opt/reproducible-python.diff | pyenv install -kp 3.6.8 + +RUN dpkg --add-architecture i386 +RUN wget -nc https://dl.winehq.org/wine-builds/winehq.key +RUN apt-key add winehq.key +RUN echo "deb https://dl.winehq.org/wine-builds/debian/ stretch main" >> /etc/apt/sources.list +RUN apt-get update +RUN apt-get install --install-recommends -y \ + wine-stable-amd64 \ + wine-stable-i386 \ + wine-stable \ + winehq-stable \ + p7zip-full diff --git a/contrib/build_bin.sh b/contrib/build_bin.sh new file mode 100755 index 000000000..151f5b9dc --- /dev/null +++ b/contrib/build_bin.sh @@ -0,0 +1,26 @@ +#! /bin/bash +# Script for building standalone binary releases deterministically + +eval "$(pyenv init -)" +eval "$(pyenv virtualenv-init -)" +pip install -U pip +pip install poetry + +# Setup poetry and install the dependencies +poetry install + +# We now need to remove debugging symbols and build id from the hidapi SO file +so_dir=`dirname $(dirname $(poetry run which python))`/lib/python3.6/site-packages +find ${so_dir} -name '*.so' -type f -execdir strip '{}' \; +if [[ $OSTYPE != *"darwin"* ]]; then + find ${so_dir} -name '*.so' -type f -execdir strip -R .note.gnu.build-id '{}' \; +fi + +# We also need to change the timestamps of all of the base library files +lib_dir=`pyenv root`/versions/3.6.8/lib/python3.6 +TZ=UTC find ${lib_dir} -name '*.py' -type f -execdir touch -t "201901010000.00" '{}' \; + +# Make the standalone binary +export PYTHONHASHSEED=42 +poetry run pyinstaller hwi.spec +unset PYTHONHASHSEED diff --git a/contrib/build_dist.sh b/contrib/build_dist.sh new file mode 100755 index 000000000..b6b7d6b75 --- /dev/null +++ b/contrib/build_dist.sh @@ -0,0 +1,15 @@ +#! /bin/bash +# Script for building pypi distribution archives deterministically + +eval "$(pyenv init -)" +eval "$(pyenv virtualenv-init -)" +pip install -U pip +pip install poetry + +# Setup poetry and install the dependencies +poetry install + +# Make the distribution archives for pypi +poetry build -f wheel +# faketime is needed to make sdist detereministic +TZ=UTC faketime -f "2019-01-01 00:00:00" poetry build -f sdist diff --git a/contrib/build_wine.sh b/contrib/build_wine.sh new file mode 100755 index 000000000..3a92d87c2 --- /dev/null +++ b/contrib/build_wine.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# Script which sets up Wine and builds the Windows standalone binary + +set -e + +PYTHON_VERSION=3.6.8 + +PYTHON_FOLDER="python3" +PYHOME="c:/$PYTHON_FOLDER" +PYTHON="wine $PYHOME/python.exe -OO -B" + +LIBUSB_URL=https://github.com/libusb/libusb/releases/download/v1.0.22/libusb-1.0.22.7z +LIBUSB_HASH="671f1a420757b4480e7fadc8313d6fb3cbb75ca00934c417c1efa6e77fb8779b" + +wine 'wineboot' + +# Install Python +# Get the PGP keys +wget -N -c "https://www.python.org/static/files/pubkeys.txt" +gpg --import pubkeys.txt +rm pubkeys.txt + +# Install python components +for msifile in core dev exe lib pip tools; do + wget -N -c "https://www.python.org/ftp/python/$PYTHON_VERSION/amd64/${msifile}.msi" + wget -N -c "https://www.python.org/ftp/python/$PYTHON_VERSION/amd64/${msifile}.msi.asc" + gpg --verify "${msifile}.msi.asc" "${msifile}.msi" + wine msiexec /i "${msifile}.msi" /qb TARGETDIR=$PYHOME + rm $msifile.msi* +done + +# Get libusb +wget -N -c -O libusb.7z "$LIBUSB_URL" +echo "$LIBUSB_HASH libusb.7z" | sha256sum -c +7za x -olibusb libusb.7z -aoa +cp libusb/MS64/dll/libusb-1.0.dll ~/.wine/drive_c/python3/ +rm -r libusb* + +# Update pip +$PYTHON -m pip install -U pip + +# Install Poetry and things needed for pyinstaller +$PYTHON -m pip install poetry + +# We also need to change the timestamps of all of the base library files +lib_dir=~/.wine/drive_c/python3/Lib +TZ=UTC find ${lib_dir} -name '*.py' -type f -execdir touch -t "201901010000.00" '{}' \; + +# Do the build +POETRY="wine $PYHOME/Scripts/poetry.exe" +$POETRY install -E windist +export PYTHONHASHSEED=42 +$POETRY run pyinstaller hwi.spec +unset PYTHONHASHSEED diff --git a/contrib/generate_setup.sh b/contrib/generate_setup.sh new file mode 100755 index 000000000..8786487ba --- /dev/null +++ b/contrib/generate_setup.sh @@ -0,0 +1,32 @@ +#! /bin/bash +# Generates the setup.py file + +set -e + +# Setup poetry and install the dependencies +poetry install + +# Build the source distribution +poetry build -f sdist + +# Extract setup.py from the distribution +unset -v tarball +for file in dist/* +do + if [[ $file -nt $tarball && $file == *".tar.gz" ]] + then + tarball=$file + fi +done +unset -v toextract +for file in `tar -tf $tarball` +do + if [[ $file == *"setup.py" ]] + then + toextract=$file + fi +done +tar -xf $tarball $toextract +mv $toextract . +dir=`echo $toextract | cut -f1 -d"/"` +rm -r $dir diff --git a/contrib/pyinstaller-hooks/hook-hwilib.devices.py b/contrib/pyinstaller-hooks/hook-hwilib.devices.py new file mode 100644 index 000000000..260d2623a --- /dev/null +++ b/contrib/pyinstaller-hooks/hook-hwilib.devices.py @@ -0,0 +1,4 @@ +from hwilib.devices import __all__ +hiddenimports = [] +for d in __all__: + hiddenimports.append('hwilib.devices.' + d) diff --git a/contrib/reproducible-python.diff b/contrib/reproducible-python.diff new file mode 100644 index 000000000..13721e0a4 --- /dev/null +++ b/contrib/reproducible-python.diff @@ -0,0 +1,13 @@ +# DP: Build getbuildinfo.o with DATE/TIME values when defined + +--- Makefile.pre.in ++++ Makefile.pre.in +@@ -741,6 +741,8 @@ Modules/getbuildinfo.o: $(PARSER_OBJS) \ + -DGITVERSION="\"`LC_ALL=C $(GITVERSION)`\"" \ + -DGITTAG="\"`LC_ALL=C $(GITTAG)`\"" \ + -DGITBRANCH="\"`LC_ALL=C $(GITBRANCH)`\"" \ ++ $(if $(BUILD_DATE),-DDATE='"$(BUILD_DATE)"') \ ++ $(if $(BUILD_TIME),-DTIME='"$(BUILD_TIME)"') \ + -o $@ $(srcdir)/Modules/getbuildinfo.c + + Modules/getpath.o: $(srcdir)/Modules/getpath.c Makefile diff --git a/docs/release-process.md b/docs/release-process.md new file mode 100644 index 000000000..62af0e6a7 --- /dev/null +++ b/docs/release-process.md @@ -0,0 +1,51 @@ +# Release Process + +1. Bump version number in `pyproject.toml`, generate the setup.py file, and git tag release +2. Build distribution archives for PyPi with `contrib/build_dist.sh` +3. For MacOS and Linux, use `contrib/build_bin.sh`. This needs to be run on a MacOS machine for the MacOS binary and on a Linux machine for the linux one. +4. For Windows, use `contrib/build_wine.sh` to build the Windows binary using wine +5. Upload distribution archives to PyPi +6. Upload distribution archives and standalone binaries to Github + +## Deterministic builds with Docker + +Create the docker image: + +``` +docker build --no-cache -t hwi-builder -f contrib/build.Dockerfile . +``` + +Build everything + +``` +docker run -it --name hwi-builder -v $PWD:/opt/hwi --rm --workdir /opt/hwi hwi-builder /bin/bash -c "contrib/build_bin.sh && contrib/build_dist.sh && contrib/build_wine.sh" +``` + +## Building macOS binary + +Note that the macOS build is non-deterministic. + +First install [pyenv](https://github.com/pyenv/pyenv) using whichever method you prefer. + +Then a deterministic build of Python 3.6.8 needs to be installed. This can be done with the patch in `contrib/reproducible-python.diff`. First `cd` into HWI's source tree. Then use: + +``` +cat contrib/reproducible-python.diff | PYTHON_CONFIGURE_OPTS="--enable-framework" BUILD_DATE="Jan 1 2019" BUILD_TIME="00:00:00" pyenv install -kp 3.6.8 +``` + +Make sure that python 3.6.8 is active + +``` +$ python --version +Python 3.6.8 +``` + +Now install [Poetry](https://github.com/sdispater/poetry) with `pip install poetry` + +Additional dependencies can be installed with: + +``` +brew install libusb +``` + +Build the binaries by using `contrib/build_bin.sh`. diff --git a/hwi.spec b/hwi.spec new file mode 100644 index 000000000..049b96f35 --- /dev/null +++ b/hwi.spec @@ -0,0 +1,42 @@ +# -*- mode: python -*- +import platform +import subprocess + +block_cipher = None + +binaries = [] +if platform.system() == 'Windows': + binaries = [("c:/python3/libusb-1.0.dll", ".")] +elif platform.system() == 'Linux': + binaries = [("/lib/x86_64-linux-gnu/libusb-1.0.so.0", ".")] +elif platform.system() == 'Darwin': + find_brew_libusb_proc = subprocess.Popen(['brew', '--prefix', 'libusb'], stdout=subprocess.PIPE) + libusb_path = find_brew_libusb_proc.communicate()[0] + binaries = [(libusb_path.rstrip().decode() + "/lib/libusb-1.0.dylib", ".")] + +a = Analysis(['hwi.py'], + binaries=binaries, + datas=[], + hiddenimports=[], + hookspath=['contrib/pyinstaller-hooks/'], + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False) +pyz = PYZ(a.pure, a.zipped_data, + cipher=block_cipher) +exe = EXE(pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='hwi', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + runtime_tmpdir=None, + console=True ) diff --git a/hwilib/commands.py b/hwilib/commands.py index 002c3f17b..f55ecb9e2 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -2,14 +2,13 @@ # Hardware wallet interaction script -import glob import importlib from .serializations import PSBT, Base64ToHex, HexToBase64, hash160 from .base58 import get_xpub_fingerprint_as_id, get_xpub_fingerprint_hex, xpub_to_pub_hex -from os.path import dirname, basename, isfile from .errors import NoPasswordError, UnavailableActionError, DeviceAlreadyInitError, DeviceAlreadyUnlockedError, UnknownDeviceError, BAD_ARGUMENT, NOT_IMPLEMENTED from .descriptor import Descriptor +from .devices import __all__ as all_devs # Get the client for the device def get_client(device_type, device_path, password=''): @@ -32,11 +31,7 @@ def get_client(device_type, device_path, password=''): def enumerate(password=''): result = [] - # Gets the module names of all the files in devices/ - files = glob.glob(dirname(__file__)+"/devices/*.py") - modules = [ basename(f)[:-3] for f in files if isfile(f) and not f.endswith('__init__.py')] - - for module in modules: + for module in all_devs: try: imported_dev = importlib.import_module('.devices.' + module, __package__) result.extend(imported_dev.enumerate(password)) diff --git a/hwilib/devices/__init__.py b/hwilib/devices/__init__.py index e69de29bb..a4f93f114 100644 --- a/hwilib/devices/__init__.py +++ b/hwilib/devices/__init__.py @@ -0,0 +1,7 @@ +__all__ = [ + 'trezor', + 'ledger', + 'keepkey', + 'digitalbitbox', + 'coldcard' +] diff --git a/hwilib/devices/ckcc/client.py b/hwilib/devices/ckcc/client.py index f550e5179..3159cdbda 100644 --- a/hwilib/devices/ckcc/client.py +++ b/hwilib/devices/ckcc/client.py @@ -8,7 +8,7 @@ # # - ec_mult, ec_setup, aes_setup, mitm_verify # -import hid, sys, os +import hid, sys, os, platform from binascii import b2a_hex, a2b_hex from hashlib import sha256 from .protocol import CCProtocolPacker, CCProtocolUnpacker, CCProtoError, MAX_MSG_LEN, MAX_BLK_LEN @@ -27,6 +27,8 @@ def __init__(self, sn=None, dev=None, encrypt=True): self.is_simulator = False if not dev and sn and '/' in sn: + if platform.system() == 'Windows': + raise RuntimeError("Cannot connect to simulator. Is it running?") dev = UnixSimulatorPipe(sn) found = 'simulator' self.is_simulator = True diff --git a/hwilib/devices/trezorlib/transport/webusb.py b/hwilib/devices/trezorlib/transport/webusb.py index a4c2840ee..61d14e4a2 100644 --- a/hwilib/devices/trezorlib/transport/webusb.py +++ b/hwilib/devices/trezorlib/transport/webusb.py @@ -126,7 +126,7 @@ def enumerate(cls) -> Iterable["WebUsbTransport"]: # non-functional. dev.getProduct() devices.append(WebUsbTransport(dev)) - except usb1.USBErrorNotSupported: + except: pass return devices diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 000000000..a13c8b74c --- /dev/null +++ b/poetry.lock @@ -0,0 +1,164 @@ +[[package]] +category = "dev" +description = "Python graph (network) package" +name = "altgraph" +optional = false +python-versions = "*" +version = "0.16.1" + +[[package]] +category = "main" +description = "ECDSA cryptographic signature library (pure python)" +name = "ecdsa" +optional = false +python-versions = "*" +version = "0.13" + +[[package]] +category = "dev" +description = "Clean single-source support for Python 3 and 2" +name = "future" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "0.17.1" + +[[package]] +category = "main" +description = "A Cython interface to the hidapi from https://github.com/signal11/hidapi" +name = "hidapi" +optional = false +python-versions = "*" +version = "0.7.99.post21" + +[package.dependencies] +setuptools = ">=19.0" + +[[package]] +category = "main" +description = "Pure-python wrapper for libusb-1.0" +name = "libusb1" +optional = false +python-versions = "*" +version = "1.7" + +[[package]] +category = "dev" +description = "Mach-O header analysis and editing" +name = "macholib" +optional = false +python-versions = "*" +version = "1.11" + +[package.dependencies] +altgraph = ">=0.15" + +[[package]] +category = "main" +description = "Implementation of Bitcoin BIP-0039" +name = "mnemonic" +optional = false +python-versions = "*" +version = "0.18" + +[package.dependencies] +pbkdf2 = "*" + +[[package]] +category = "main" +description = "PKCS#5 v2.0 PBKDF2 Module" +name = "pbkdf2" +optional = false +python-versions = "*" +version = "1.3" + +[[package]] +category = "dev" +description = "Python PE parsing module" +name = "pefile" +optional = false +python-versions = "*" +version = "2018.8.8" + +[package.dependencies] +future = "*" + +[[package]] +category = "main" +description = "Pure-Python Implementation of the AES block-cipher and common modes of operation" +name = "pyaes" +optional = false +python-versions = "*" +version = "1.6.1" + +[[package]] +category = "dev" +description = "PyInstaller bundles a Python application and all its dependencies into a single package." +name = "pyinstaller" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "3.4" + +[package.dependencies] +altgraph = "*" +macholib = ">=1.8" +pefile = ">=2017.8.1" +setuptools = "*" + +[[package]] +category = "dev" +description = "Enhanced version of python-jsonrpc for use with Bitcoin" +name = "python-bitcoinrpc" +optional = false +python-versions = "*" +version = "1.0" + +[[package]] +category = "main" +description = "" +name = "pywin32-ctypes" +optional = true +python-versions = "*" +version = "0.2.0" + +[[package]] +category = "main" +description = "Type Hints for Python" +name = "typing" +optional = false +python-versions = "*" +version = "3.6.6" + +[[package]] +category = "main" +description = "Backported and Experimental Type Hints for Python 3.5+" +name = "typing-extensions" +optional = false +python-versions = "*" +version = "3.7.2" + +[package.dependencies] +typing = ">=3.6.2" + +[extras] +windist = ["pywin32-ctypes"] + +[metadata] +content-hash = "f668b6352b31d2aa7cf5b0cf19b77d04b92821df3383c9105f75699bbe42aa2e" +python-versions = ">=3.5.6" + +[metadata.hashes] +altgraph = ["d6814989f242b2b43025cba7161fc1b8fb487a62cd49c49245d6fd01c18ac997", "ddf5320017147ba7b810198e0b6619bd7b5563aa034da388cea8546b877f9b0c"] +ecdsa = ["40d002cf360d0e035cf2cb985e1308d41aaa087cbfc135b2dc2d844296ea546c", "64cf1ee26d1cde3c73c6d7d107f835fed7c6a2904aef9eac223d57ad800c43fa"] +future = ["67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8"] +hidapi = ["1ac170f4d601c340f2cd52fd06e85c5e77bad7ceac811a7bb54b529f7dc28c24", "6424ad75da0021ce8c1bcd78056a04adada303eff3c561f8d132b85d0a914cb3", "8d3be666f464347022e2b47caf9132287885d9eacc7895314fc8fefcb4e42946", "92878bad7324dee619b7832fbfc60b5360d378aa7c5addbfef0a410d8fd342c7", "b4b1f6aff0192e9be153fe07c1b7576cb7a1ff52e78e3f76d867be95301a8e87", "bf03f06f586ce7d8aeb697a94b7dba12dc9271aae92d7a8d4486360ff711a660", "c76de162937326fcd57aa399f94939ce726242323e65c15c67e183da1f6c26f7", "d4ad1e46aef98783a9e6274d523b8b1e766acfc3d72828cd44a337564d984cfa", "d4b5787a04613503357606bb10e59c3e2c1114fa00ee328b838dd257f41cbd7b", "e0be1aa6566979266a8fc845ab0e18613f4918cf2c977fe67050f5dc7e2a9a97", "edfb16b16a298717cf05b8c8a9ad1828b6ff3de5e93048ceccd74e6ae4ff0922"] +libusb1 = ["9d4f66d2ed699986b06bc3082cd262101cb26af7a76a34bd15b7eb56cba37e0f"] +macholib = ["ac02d29898cf66f27510d8f39e9112ae00590adb4a48ec57b25028d6962b1ae1", "c4180ffc6f909bf8db6cd81cff4b6f601d575568f4d5dee148c830e9851eb9db"] +mnemonic = ["02a7306a792370f4a0c106c2cf1ce5a0c84b9dbd7e71c6792fdb9ad88a727f1d"] +pbkdf2 = ["ac6397369f128212c43064a2b4878038dab78dab41875364554aaf2a684e6979"] +pefile = ["4c5b7e2de0c8cb6c504592167acf83115cbbde01fe4a507c16a1422850e86cd6"] +pyaes = ["02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f"] +pyinstaller = ["a5a6e04a66abfcf8761e89a2ebad937919c6be33a7b8963e1a961b55cb35986b"] +python-bitcoinrpc = ["a6a6f35672635163bc491c25fe29520bdd063dedbeda3b37bf5be97aa038c6e7"] +pywin32-ctypes = ["24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942", "9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"] +typing = ["4027c5f6127a6267a435201981ba156de91ad0d1d98e9ddc2aa173453453492d", "57dcf675a99b74d64dacf6fba08fb17cf7e3d5fdff53d4a30ea2a5e7e52543d4", "a4c8473ce11a65999c8f59cb093e70686b6c84c98df58c1dae9b3b196089858a"] +typing-extensions = ["07b2c978670896022a43c4b915df8958bec4a6b84add7f2c87b2b728bda3ba64", "f3f0e67e1d42de47b5c67c32c9b26641642e9170fe7e292991793705cd5fef7c", "fb2cd053238d33a8ec939190f30cfd736c00653a85a2919415cecf7dc3d9da71"] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..0a1a24ae5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,35 @@ +[tool.poetry] +name = "hwi" +version = "0.0.5" +description = "A library for working with Bitcoin hardware wallets" +authors = ["Andrew Chow "] +license = "MIT" +readme = "README.md" +repository = "https://github.com/bitcoin-core/HWI" +homepage = "https://github.com/bitcoin-core/HWI" +exclude = ["docs/", "test/"] +include = ["hwilib/**/*.py"] + +[tool.poetry.dependencies] +python = ">=3.5.6" +hidapi = "^0.7.99" +ecdsa = "^0.13.0" +pyaes = "^1.6" +pywin32-ctypes = {version = "^0.2.0", optional = true} +mnemonic = "^0.18.0" +typing-extensions = "^3.7" +libusb1 = "^1.7" + +[tool.poetry.dev-dependencies] +pyinstaller = "^3.4" +python-bitcoinrpc = "^1.0" + +[tool.poetry.extras] +windist = ["pywin32-ctypes"] + +[tool.poetry.scripts] +hwi = 'hwilib.cli:main' + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/setup.py b/setup.py index ef012ce2d..ca0614f58 100644 --- a/setup.py +++ b/setup.py @@ -1,38 +1,36 @@ -import setuptools +# -*- coding: utf-8 -*- +from distutils.core import setup -with open("README.md", "r") as fh: - long_description = fh.read() +modules = \ +['hwi'] +install_requires = \ +['ecdsa>=0.13.0,<0.14.0', + 'hidapi>=0.7.99,<0.8.0', + 'libusb1>=1.7,<2.0', + 'mnemonic>=0.18.0,<0.19.0', + 'pyaes>=1.6,<2.0', + 'typing-extensions>=3.7,<4.0'] -setuptools.setup( - name="hwi", - version="0.0.5", - author="Andrew Chow", - author_email="andrew@achow101.com", - description="A library for working with Bitcoin hardware wallets", - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/bitcoin-core/hwi", - packages=setuptools.find_packages(exclude=['docs', 'test']), - install_requires=[ - 'hidapi', # HID API needed in general - 'pyaes', - 'ecdsa', # Needed for Ledger but their library does not install it - 'typing_extensions>=3.7', - 'mnemonic>=0.18.0', - 'libusb1' - ], - python_requires='>=3', - classifiers=[ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - ], - extras_require={ - 'tests': ['python-bitcoinrpc'] - }, - entry_points={ - 'console_scripts': [ - 'hwi = hwilib.cli:main' - ] - } -) +extras_require = \ +{'windist': ['pywin32-ctypes>=0.2.0,<0.3.0']} + +entry_points = \ +{'console_scripts': ['hwi = hwilib.cli:main']} + +setup_kwargs = { + 'name': 'hwi', + 'version': '0.0.5', + 'description': 'A library for working with Bitcoin hardware wallets', + 'long_description': '# Bitcoin Hardware Wallet Interaction scripts\n\n[![Build Status](https://travis-ci.org/bitcoin-core/HWI.svg?branch=master)](https://travis-ci.org/bitcoin-core/HWI)\n\nThis project contains several scripts for interacting with Bitcoin hardware wallets.\n\n## Prerequisites\n\nPython 3 is required. The libraries and udev rules for each device must also be installed.\n\nInstall all of the libraries using `pip` (in virtualenv or system):\n\n```\npip3 install hidapi # HID API needed in general\npip3 install trezor[hidapi] # Trezor One\npip3 install btchip-python # Ledger Nano S\npip3 install ecdsa # Needed for btchip-python but is not installed by it\npip3 install keepkey # KeepKey\npip3 install ckcc-protocol[cli] # Coldcard\npip3 install pyaes # For digitalbitbox\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 -d \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', + 'author': 'Andrew Chow', + 'author_email': 'andrew@achow101.com', + 'url': 'https://github.com/bitcoin-core/HWI', + 'py_modules': modules, + 'install_requires': install_requires, + 'extras_require': extras_require, + 'entry_points': entry_points, + 'python_requires': '>=3.5.6', +} + + +setup(**setup_kwargs) diff --git a/test/run_tests.py b/test/run_tests.py index a7afea153..18c43a370 100755 --- a/test/run_tests.py +++ b/test/run_tests.py @@ -32,7 +32,7 @@ dbb_group.add_argument('--bitbox', help='Path to Digital bitbox simulator.', default='work/mcu/build/bin/simulator') parser.add_argument('--bitcoind', help='Path to bitcoind.', default='work/bitcoin/src/bitcoind') -parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli'], default='library') +parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli', 'bindist'], default='library') args = parser.parse_args() # Run tests diff --git a/test/test_coldcard.py b/test/test_coldcard.py index d11176573..85d7364ed 100755 --- a/test/test_coldcard.py +++ b/test/test_coldcard.py @@ -82,7 +82,7 @@ def test_pin(self): parser = argparse.ArgumentParser(description='Test Coldcard implementation') parser.add_argument('simulator', help='Path to the Coldcard simulator') parser.add_argument('bitcoind', help='Path to bitcoind binary') - parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli'], default='library') + parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli', 'bindist'], default='library') args = parser.parse_args() # Start bitcoind diff --git a/test/test_device.py b/test/test_device.py index 3f8b9bc67..03623ab63 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -83,6 +83,10 @@ def do_command(self, args): proc = subprocess.Popen(['hwi ' + ' '.join(args)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True) result = proc.communicate() return json.loads(result[0].decode()) + elif self.interface == 'bindist': + proc = subprocess.Popen(['../dist/hwi ' + ' '.join(args)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True) + result = proc.communicate() + return json.loads(result[0].decode()) else: return process_commands(args) diff --git a/test/test_digitalbitbox.py b/test/test_digitalbitbox.py index 5a65aa1f2..39e6b9710 100755 --- a/test/test_digitalbitbox.py +++ b/test/test_digitalbitbox.py @@ -137,7 +137,7 @@ def test_backup(self): parser = argparse.ArgumentParser(description='Test Digital Bitbox implementation') parser.add_argument('simulator', help='Path to simulator binary') parser.add_argument('bitcoind', help='Path to bitcoind binary') - parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli'], default='library') + parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli', 'bindist'], default='library') args = parser.parse_args() # Start bitcoind diff --git a/test/test_keepkey.py b/test/test_keepkey.py index 2525228a1..4b116aa47 100755 --- a/test/test_keepkey.py +++ b/test/test_keepkey.py @@ -86,6 +86,10 @@ def do_command(self, args): proc = subprocess.Popen(['hwi ' + ' '.join(args)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True) result = proc.communicate() return json.loads(result[0].decode()) + elif self.interface == 'bindist': + proc = subprocess.Popen(['../dist/hwi ' + ' '.join(args)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True) + result = proc.communicate() + return json.loads(result[0].decode()) else: return process_commands(args) @@ -233,7 +237,7 @@ def keepkey_test_suite(emulator, rpc, userpass, interface): parser = argparse.ArgumentParser(description='Test Keepkey implementation') parser.add_argument('emulator', help='Path to the Keepkey emulator') parser.add_argument('bitcoind', help='Path to bitcoind binary') - parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli'], default='library') + parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli', 'bindist'], default='library') args = parser.parse_args() # Start bitcoind diff --git a/test/test_ledger.py b/test/test_ledger.py index e6233ff46..9c85f2ba8 100755 --- a/test/test_ledger.py +++ b/test/test_ledger.py @@ -84,7 +84,7 @@ def test_backup(self): if __name__ == '__main__': parser = argparse.ArgumentParser(description='Test Ledger implementation') parser.add_argument('bitcoind', help='Path to bitcoind binary') - parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli'], default='library') + parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli', 'bindist'], default='library') args = parser.parse_args() # Start bitcoind diff --git a/test/test_trezor.py b/test/test_trezor.py index d4e271ac0..d2abfe919 100755 --- a/test/test_trezor.py +++ b/test/test_trezor.py @@ -86,6 +86,10 @@ def do_command(self, args): proc = subprocess.Popen(['hwi ' + ' '.join(args)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True) result = proc.communicate() return json.loads(result[0].decode()) + elif self.interface == 'bindist': + proc = subprocess.Popen(['../dist/hwi ' + ' '.join(args)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True) + result = proc.communicate() + return json.loads(result[0].decode()) else: return process_commands(args) @@ -233,7 +237,7 @@ def trezor_test_suite(emulator, rpc, userpass, interface): parser = argparse.ArgumentParser(description='Test Trezor implementation') parser.add_argument('emulator', help='Path to the Trezor emulator') parser.add_argument('bitcoind', help='Path to bitcoind binary') - parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli'], default='library') + parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli', 'bindist'], default='library') args = parser.parse_args() # Start bitcoind