diff --git a/.containerignore b/.containerignore new file mode 100644 index 000000000..895b58026 --- /dev/null +++ b/.containerignore @@ -0,0 +1,11 @@ +# Exclude local build artifacts and any symlinks outside the context +build* + +# Local documentation files (can be symlinks outside the context) +*.md + +# Common Python / tooling artifacts +__pycache__/ +*.pyc +.venv/ +dist/ diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..895b58026 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +# Exclude local build artifacts and any symlinks outside the context +build* + +# Local documentation files (can be symlinks outside the context) +*.md + +# Common Python / tooling artifacts +__pycache__/ +*.pyc +.venv/ +dist/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 10e2c5153..d4eb98d39 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,6 +95,9 @@ jobs: git config --global --add safe.directory ${{ github.workspace }} contrib/build_bin.sh contrib/build_dist.sh + # Verify setup.py regeneration is stable after generating GUI artifacts. + contrib/generate_setup.sh + git diff --exit-code -- setup.py find dist -type f -exec sha256sum {} \; " @@ -242,4 +245,3 @@ jobs: with: device: keepkey runs-on: ubuntu-latest - diff --git a/.github/workflows/device-test.yml b/.github/workflows/device-test.yml index 5232314e7..5ec9ede76 100644 --- a/.github/workflows/device-test.yml +++ b/.github/workflows/device-test.yml @@ -18,7 +18,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.9', '3.10', '3.11', '3.12'] + python-version: ['3.10', '3.11', '3.12'] device: - ${{ inputs.device }} test: diff --git a/.github/workflows/ledger-app-builder.yml b/.github/workflows/ledger-app-builder.yml index e97f883b6..19cb9cd0a 100644 --- a/.github/workflows/ledger-app-builder.yml +++ b/.github/workflows/ledger-app-builder.yml @@ -11,14 +11,14 @@ jobs: build: name: Build Bitcoin App runs-on: ${{ inputs.runs-on }} - # Pin to 4.23.0 for SDK v25.9.0 compatibility with Speculos v0.25.10 - container: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:4.23.0 + # app-bitcoin v2.5.0 targets SDK v26.5.0, provided by builder 5.3.17. + container: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:5.3.17 steps: - run: | - # Pin to v2.4.1 - last version that worked with HWI CI (PR #795 merged Sept 2025) - git clone --branch 2.4.1 --depth 1 https://github.com/LedgerHQ/app-bitcoin.git + # Pin to v2.5.0 + git clone --branch 2.5.0 --depth 1 https://github.com/LedgerHQ/app-bitcoin.git cd app-bitcoin - make DEBUG=1 BOLOS_SDK=$NANOX_SDK + make DEBUG=1 COIN=bitcoin_testnet BOLOS_SDK=$NANOX_SDK - uses: actions/upload-artifact@v4 with: name: ledger_app diff --git a/.github/workflows/ledger-legacy-app-builder.yml b/.github/workflows/ledger-legacy-app-builder.yml index d9c6179bc..28d7a2d45 100644 --- a/.github/workflows/ledger-legacy-app-builder.yml +++ b/.github/workflows/ledger-legacy-app-builder.yml @@ -11,7 +11,7 @@ jobs: build: name: Build Bitcoin Legacy App runs-on: ${{ inputs.runs-on }} - # Pin to 4.23.0 for SDK v25.9.0 compatibility with Speculos v0.25.10 + # Pin to 4.23.0 for SDK v25.9.0 compatibility with Speculos v0.26.9 container: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:4.23.0 steps: - run: | diff --git a/.python-version b/.python-version index bd28b9c5c..44677e5cc 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.9 +3.10.20 diff --git a/ci/py39.Dockerfile b/ci/py39.Dockerfile deleted file mode 100644 index 9c6f5eeb8..000000000 --- a/ci/py39.Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -# Cache break (modify this line to break cirrus' dockerfile build cache) 1 - -FROM python:3.9 - -ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update -RUN apt-get install -y \ - cython3 \ - git \ - libpcsclite-dev \ - libsdl2-dev \ - libsdl2-image-dev \ - libslirp-dev \ - libudev-dev \ - libusb-1.0-0-dev \ - qemu-user-static \ - swig - -RUN pip install poetry flake8 - -ENV LC_ALL=C.UTF-8 -ENV LANG=C.UTF-8 -ENV LANGUAGE=C.UTF-8 diff --git a/contrib/build-wine.Dockerfile b/contrib/build-wine.Dockerfile index 335b580bb..d781dfdcd 100644 --- a/contrib/build-wine.Dockerfile +++ b/contrib/build-wine.Dockerfile @@ -1,7 +1,5 @@ FROM debian:bookworm-slim -SHELL ["/bin/bash", "-c"] - ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update RUN apt-get install -y \ diff --git a/contrib/build.Dockerfile b/contrib/build.Dockerfile index 4e1025352..d1d23861c 100644 --- a/contrib/build.Dockerfile +++ b/contrib/build.Dockerfile @@ -1,7 +1,5 @@ FROM debian:bookworm-slim -SHELL ["/bin/bash", "-c"] - ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update RUN apt-get install -y \ @@ -36,9 +34,9 @@ ENV PATH="$PYENV_ROOT/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_DATE="Jan 1 2026" ENV BUILD_TIME="00:00:00" -RUN eval "$(pyenv init --path)" && eval "$(pyenv virtualenv-init -)" && cat /opt/reproducible-python.diff | pyenv install -kp 3.9.19 +RUN /bin/bash -c 'eval "$(pyenv init --path)" && eval "$(pyenv virtualenv-init -)" && cat /opt/reproducible-python.diff | pyenv install -kp 3.10.20' ENV LC_ALL=C.UTF-8 ENV LANG=C.UTF-8 diff --git a/contrib/build_bin.sh b/contrib/build_bin.sh index 2db94f439..af8c233dc 100755 --- a/contrib/build_bin.sh +++ b/contrib/build_bin.sh @@ -6,8 +6,11 @@ set -ex ARCH=$(uname -m | tr '[:upper:]' '[:lower:]') +PYTHON_VERSION=3.10.20 + eval "$(pyenv init --path)" eval "$(pyenv virtualenv-init -)" +export PYENV_VERSION="$PYTHON_VERSION" pip install -U pip pip install poetry @@ -21,8 +24,8 @@ else fi # We also need to change the timestamps of all of the base library files -lib_dir=$(pyenv prefix)/lib/python3.9 -TZ=UTC find ${lib_dir} -name '*.py' -type f -execdir touch -t "201901010000.00" '{}' \; +lib_dir=$(pyenv prefix)/lib/python3.10 +TZ=UTC find ${lib_dir} -name '*.py' -type f -execdir touch -t "202601010000.00" '{}' \; # Make the standalone binary export PYTHONHASHSEED=42 @@ -45,7 +48,7 @@ fi target_tarfile="hwi-${VERSION}-${OS}-${ARCH}.tar.gz" -if [[ $gui_support == "--with-gui" ]]; then +if [[ $gui_support == "--with-gui" && $ARCH == "x86_64" ]]; then tar -czf $target_tarfile hwi hwi-qt else tar -czf $target_tarfile hwi diff --git a/contrib/build_dist.sh b/contrib/build_dist.sh index e15b9e5d9..104d37f96 100755 --- a/contrib/build_dist.sh +++ b/contrib/build_dist.sh @@ -4,8 +4,11 @@ set -ex +PYTHON_VERSION=3.10.20 + eval "$(pyenv init --path)" eval "$(pyenv virtualenv-init -)" +export PYENV_VERSION="$PYTHON_VERSION" pip install -U pip pip install poetry @@ -14,11 +17,12 @@ gui_support="${1:---with-gui}"; # Setup poetry and install the dependencies if [[ $gui_support == "--with-gui" ]]; then poetry install -E qt + poetry run contrib/generate-ui.sh else poetry install fi # 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 +# faketime is needed to make sdist deterministic +TZ=UTC faketime -f "2026-01-01 00:00:00" poetry build -f sdist diff --git a/contrib/build_wine.sh b/contrib/build_wine.sh index 64d211f97..30efca207 100755 --- a/contrib/build_wine.sh +++ b/contrib/build_wine.sh @@ -3,7 +3,11 @@ set -ex -PYTHON_VERSION=3.9.13 +# Note: Python MSIs/EXEs are no longer hosted for 3.10.x on python.org. +# The NuGet python package currently only goes up to 3.10.11, so Windows builds use that. +PYTHON_VERSION=3.10.11 +PYTHON_NUGET_URL="https://api.nuget.org/v3-flatcontainer/python/${PYTHON_VERSION}/python.${PYTHON_VERSION}.nupkg" +PYTHON_NUGET_HASH="7c6f99b160a36a7e09492dfcff2b0a3a60bb5229ca44cdcc3ecb32871a6144d0" PYTHON_FOLDER="python3" PYHOME="c:/$PYTHON_FOLDER" @@ -19,20 +23,15 @@ WINDOWS_SDK_VERSION=10.0.17763.0 wine 'wineboot' -# Install Python -# Get the PGP keys -wget -O pubkeys.txt -N -c "https://keybase.io/stevedower/pgp_keys.asc?fingerprint=7ed10b6531d7c8e1bc296021fc624643487034e5" -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 +# Install Python from NuGet package +wget -O python.nupkg -N -c "$PYTHON_NUGET_URL" +echo "$PYTHON_NUGET_HASH python.nupkg" | sha256sum -c +rm -rf python-nupkg +7z x python.nupkg -opython-nupkg >/dev/null +rm -rf ~/.wine/drive_c/python3 +mkdir -p ~/.wine/drive_c/python3 +cp -a python-nupkg/tools/* ~/.wine/drive_c/python3/ +rm -rf python.nupkg python-nupkg # Get and build libusb wget -N -c -O libusb.tar.bz2 "$LIBUSB_URL" @@ -40,7 +39,7 @@ echo "$LIBUSB_HASH libusb.tar.bz2" | sha256sum -c tar -xf libusb.tar.bz2 pushd "libusb-$LIBUSB_VERSION" ./configure --host=x86_64-w64-mingw32 -faketime -f "2019-01-01 00:00:00" make +faketime -f "2026-01-01 00:00:00" make cp libusb/.libs/libusb-1.0.dll ~/.wine/drive_c/python3/ popd rm -r libusb* @@ -62,7 +61,7 @@ $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" '{}' \; +TZ=UTC find ${lib_dir} -name '*.py' -type f -execdir touch -t "202601010000.00" '{}' \; # Install python dependencies POETRY="wine $PYHOME/Scripts/poetry.exe" diff --git a/contrib/generate-ui.sh b/contrib/generate-ui.sh index 9fc982ea9..8b6609633 100755 --- a/contrib/generate-ui.sh +++ b/contrib/generate-ui.sh @@ -3,6 +3,7 @@ set -ex pushd hwilib/ui +rm -f ui_*.py for file in *.ui do gen_file=ui_`echo $file| cut -d. -f1`.py diff --git a/contrib/generate_setup.sh b/contrib/generate_setup.sh index 3e3424d8a..6046ffe9a 100755 --- a/contrib/generate_setup.sh +++ b/contrib/generate_setup.sh @@ -5,6 +5,8 @@ set -ex # Setup poetry and install the dependencies poetry install -E qt +# Generate the Python modules so Poetry consistently detects hwilib.ui as a package. +poetry run contrib/generate-ui.sh # Build the source distribution poetry build -f sdist diff --git a/docs/development/release-process.rst b/docs/development/release-process.rst index fb739ffdc..16cf86081 100644 --- a/docs/development/release-process.rst +++ b/docs/development/release-process.rst @@ -4,7 +4,7 @@ Release Process 1. Bump version number in ``pyproject.toml`` and ``hwilib/__init__.py``, 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 +4. For Windows, use ``contrib/build_wine.sh`` to build the Windows binary using wine. Note that this uses Python 3.10.11 via the NuGet package because python.org no longer hosts the MSIs for newer 3.10.x releases. 5. Make ``SHA256SUMS.txt`` using ``contrib/make_shasums.sh``. 6. Make ``SHA256SUMS.txt.asc`` using ``gpg --clearsign SHA256SUMS.txt`` 7. Upload distribution archives to PyPi @@ -26,7 +26,7 @@ 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" docker run -it --name hwi-wine-builder -v $PWD:/opt/hwi --rm --workdir /opt/hwi hwi-wine-builder /bin/bash -c "contrib/build_wine.sh" - docker run --platform linux/arm64 -it --rm --name hwi-builder-arm64 -v $PWD:/opt/hwi --workdir /opt/hwi hwi-builder-arm64 /bin/bash -c "contrib/build_bin.sh --without-gui && contrib/build_dist.sh --without-gui" + docker run --platform linux/arm64 -it --rm --name hwi-builder-arm64 -v $PWD:/opt/hwi --workdir /opt/hwi hwi-builder-arm64 /bin/bash -c "contrib/build_bin.sh --without-gui && contrib/build_dist.sh --without-gui" Building macOS binary ===================== @@ -35,14 +35,14 @@ Note that the macOS build is non-deterministic. First install `pyenv `_ using whichever method you prefer. -Then a deterministic build of Python 3.9.19 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:: +Then a deterministic build of Python 3.10.20 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.9.19 + cat contrib/reproducible-python.diff | PYTHON_CONFIGURE_OPTS="--enable-framework" BUILD_DATE="Jan 1 2026" BUILD_TIME="00:00:00" pyenv install -kp 3.10.20 -Make sure that python 3.9.19 is active:: +Make sure that python 3.10.20 is active:: $ python --version - Python 3.9.19 + Python 3.10.20 Now install `Poetry `_ with ``pip install poetry`` diff --git a/poetry.lock b/poetry.lock index f7f47dc75..b26f93c74 100644 --- a/poetry.lock +++ b/poetry.lock @@ -507,27 +507,6 @@ files = [ {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, ] -[[package]] -name = "importlib-metadata" -version = "7.0.1" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "python_version == \"3.9\"" -files = [ - {file = "importlib_metadata-7.0.1-py3-none-any.whl", hash = "sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e"}, - {file = "importlib_metadata-7.0.1.tar.gz", hash = "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc"}, -] - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-perf (>=0.9.2)", "pytest-ruff"] - [[package]] name = "jinja2" version = "3.1.3" @@ -882,7 +861,6 @@ files = [ [package.dependencies] altgraph = "*" -importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} packaging = ">=22.0" pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""} @@ -907,7 +885,6 @@ files = [ ] [package.dependencies] -importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} packaging = ">=22.0" setuptools = ">=42.0.0" @@ -933,7 +910,7 @@ description = "Python bindings for the Qt cross-platform application and UI fram optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.11" groups = ["main"] -markers = "python_version == \"3.9\" and extra == \"qt\"" +markers = "python_version == \"3.10\" and extra == \"qt\"" files = [ {file = "PySide2-5.15.2.1-5.15.2-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:b5e1d92f26b0bbaefff67727ccbb2e1b577f2c0164b349b3d6e80febb4c5bde2"}, {file = "PySide2-5.15.2.1-5.15.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:235240b6ec8206d9fdf0232472c6ef3241783d480425e5b54796f06e39ed23da"}, @@ -1017,7 +994,7 @@ description = "Python / C++ bindings helper module" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.11" groups = ["main"] -markers = "python_version == \"3.9\" and extra == \"qt\"" +markers = "python_version == \"3.10\" and extra == \"qt\"" files = [ {file = "shiboken2-5.15.2.1-5.15.2-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:f890f5611ab8f48b88cfecb716da2ac55aef99e2923198cefcf781842888ea65"}, {file = "shiboken2-5.15.2.1-5.15.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:87079c07587859a525b9800d60b1be971338ce9b371d6ead81f15ee5a46d448b"}, @@ -1069,7 +1046,6 @@ babel = ">=2.9" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} docutils = ">=0.18.1,<0.21" imagesize = ">=1.3" -importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} Jinja2 = ">=3.0" packaging = ">=21.0" Pygments = ">=2.13" @@ -1252,7 +1228,7 @@ description = "A lil' TOML parser" optional = false python-versions = ">=3.7" groups = ["dev"] -markers = "python_version < \"3.11\"" +markers = "python_version == \"3.10\"" files = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, @@ -1288,27 +1264,10 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] -[[package]] -name = "zipp" -version = "3.17.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "python_version == \"3.9\"" -files = [ - {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, - {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-ruff"] - [extras] qt = ["pyside2"] [metadata] lock-version = "2.1" -python-versions = "^3.9,<3.13" -content-hash = "cfdbc60bf068f88587fe801325c19dfada1e868dbb0cdd65c5b7a7353dc0ca6a" +python-versions = "^3.10,<3.13" +content-hash = "32c83e332e591f7a95b48ef02297798399287c93bc25be7730a25f84d20b4f2b" diff --git a/pyproject.toml b/pyproject.toml index 3137310c4..618a14b32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,14 +16,14 @@ packages = [ ] [tool.poetry.dependencies] -python = "^3.9,<3.13" +python = "^3.10,<3.13" hidapi = ">=0.14.0" ecdsa = "~0" pyaes = "^1.6" mnemonic = "~0" typing-extensions = "^4.4" libusb1 = ">=1.7,<4" -pyside2 = { version = "^5.14.0", optional = true, python = "<3.10" } +pyside2 = { version = "^5.15.2.1", optional = true, python = "<3.11" } cbor2 = ">=5.4.6,<5.8" pyserial = "^3.5" semver = "^3.0.1" diff --git a/setup.py b/setup.py index d6255f68e..8ea9d7019 100644 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ 'typing-extensions>=4.4,<5.0'] extras_require = \ -{'qt:python_version < "3.10"': ['pyside2>=5.14.0,<6.0.0']} +{'qt:python_version < "3.11"': ['pyside2>=5.15.2.1,<6.0.0.0']} entry_points = \ {'console_scripts': ['hwi = hwilib._cli:main', 'hwi-qt = hwilib._gui:main']} @@ -60,7 +60,7 @@ 'install_requires': install_requires, 'extras_require': extras_require, 'entry_points': entry_points, - 'python_requires': '>=3.9,<3.13', + 'python_requires': '>=3.10,<3.13', } diff --git a/test/data/speculos-automation.json b/test/data/speculos-automation.json index 31f69f72a..41cc06129 100644 --- a/test/data/speculos-automation.json +++ b/test/data/speculos-automation.json @@ -1,6 +1,14 @@ { "version": 1, "rules": [ + { + "regexp": "^(Security risk|High fees warning).*", + "y": 34, + "actions": [ + [ "button", 2, true ], + [ "button", 2, false ] + ] + }, { "text": "Confirm", "x": 43, "y": 37, @@ -11,6 +19,16 @@ [ "button", 2, false ] ] }, + { + "text": "Confirm", + "x": 41, "y": 34, + "actions": [ + [ "button", 1, true ], + [ "button", 2, true ], + [ "button", 1, false ], + [ "button", 2, false ] + ] + }, { "text": "Confirm account ", "actions": [ @@ -28,7 +46,7 @@ ] }, { - "regexp": "^(Address|Review|Amount|Fee|Confirm|The derivation|Derivation path|Reject if|The change path|Change path|Register wallet|Policy map|Key|Path|Public key|Spend from|Wallet name|Wallet policy|Descriptor template|Verify Bitcoin|Output|Warning).*", + "regexp": "^(Address|Review|Amount|External amounts|You spend|You receive|Fee|Confirm|The derivation|Derivation path|Reject if|The change path|Change path|Register wallet|Policy map|Key|Path|Public key|Our key|Their key|Unspendable key|Spend from|Spending policy|Primary spending path|Spending path|Transaction output|Wallet name|Wallet policy|Descriptor template|Verify [Bb]itcoin|Output|Warning).*", "actions": [ [ "button", 2, true ], [ "button", 2, false ] @@ -97,13 +115,6 @@ [ "button", 2, false ], [ "setbool", "seen_msg_hash", false ] ] - }, - { - "regexp": "^(Cancel|Reject).*", - "actions": [ - [ "button", 1, true ], - [ "button", 1, false ] - ] } ] } diff --git a/test/setup_environment.sh b/test/setup_environment.sh index 36788bfdb..c9c2477de 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -69,7 +69,7 @@ TREZOR_VERSION="core/v2.9.6" BITBOX01_VERSION="v7.1.0" BITBOX02_VERSION="firmware/v9.24.0" KEEPKEY_VERSION="v7.10.0" -SPECULOS_VERSION="v0.25.10" # Last version supporting Python 3.9 (v0.25.11+ requires >=3.10) +SPECULOS_VERSION="v0.26.9" # Requires Python >=3.10 (v0.25.11+) JADE_VERSION="1.0.36" # Keep COLDCARD_VERSION in sync with .github/actions/install-sim/action.yml diff --git a/test/test_bitbox02.py b/test/test_bitbox02.py index a9d64b999..400da4afa 100644 --- a/test/test_bitbox02.py +++ b/test/test_bitbox02.py @@ -43,6 +43,7 @@ def __init__(self, simulator): self.include_xpubs = True self.supports_device_multiple_multisig = True self.supports_legacy = False + self.supports_arbitrary_keypool_paths = True def start(self): super().start() diff --git a/test/test_coldcard.py b/test/test_coldcard.py index c30742b3d..4259d1efd 100755 --- a/test/test_coldcard.py +++ b/test/test_coldcard.py @@ -45,6 +45,7 @@ def __init__(self, simulator): self.include_xpubs = False self.supports_device_multiple_multisig = True self.supports_legacy = True + self.supports_arbitrary_keypool_paths = True def start(self): super().start() diff --git a/test/test_device.py b/test/test_device.py index abc6ef1ae..af26c5c78 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -36,6 +36,7 @@ def __init__(self): self.include_xpubs = None self.supports_device_multiple_multisig = None self.supports_legacy = None + self.supports_arbitrary_keypool_paths = True def start(self): assert self.type is not None @@ -174,7 +175,7 @@ def do_command(self, args): args = [f'"{arg}"' for arg in args] input_str = '\n'.join(args) + '\n' proc = subprocess.Popen(['hwi', '--stdin'], stdin=subprocess.PIPE, stdout=subprocess.PIPE) - result = proc.communicate(input_str.encode(), timeout=60) + result = proc.communicate(input_str.encode(), timeout=120) return json.loads(result[0].decode()) else: return process_commands(args) @@ -313,13 +314,14 @@ def test_getkeypool(self): addr_info = self.wrpc.getaddressinfo(self.wrpc.getrawchangeaddress('bech32')) self.assertTrue(addr_info['hdkeypath'].startswith("m/84h/1h/3h/1/")) - keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--path', 'm/0h/0h/4h/*', '0', '20']) - self.assertIsInstance(keypool_desc, list, f"getkeypool returned error: {keypool_desc}") - import_result = self.wrpc.importdescriptors(keypool_desc) - self.assertTrue(import_result[0]['success']) - for _ in range(0, 21): - addr_info = self.wrpc.getaddressinfo(self.wrpc.getnewaddress('', 'bech32')) - self.assertTrue(addr_info['hdkeypath'].startswith("m/0h/0h/4h/")) + if self.emulator.supports_arbitrary_keypool_paths: + keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--path', 'm/0h/0h/4h/*', '0', '20']) + self.assertIsInstance(keypool_desc, list, f"getkeypool returned error: {keypool_desc}") + import_result = self.wrpc.importdescriptors(keypool_desc) + self.assertTrue(import_result[0]['success']) + for _ in range(0, 21): + addr_info = self.wrpc.getaddressinfo(self.wrpc.getnewaddress('', 'bech32')) + self.assertTrue(addr_info['hdkeypath'].startswith("m/0h/0h/4h/")) keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--path', '/0h/0h/4h/*', '0', '20']) self.assertEqual(keypool_desc['error'], 'Path must start with m/') @@ -498,10 +500,12 @@ def _test_signtx(self, input_types, multisig_types, external, op_return: bool): in_amt = 1 number_inputs = 0 # Single-sig - if "segwit" in input_types: + if "segwit" in input_types or "sh_wit" in input_types: self.wpk_rpc.sendtoaddress(sh_wpkh_addr, in_amt) + number_inputs += 1 + if "segwit" in input_types or "wit" in input_types: self.wpk_rpc.sendtoaddress(wpkh_addr, in_amt) - number_inputs += 2 + number_inputs += 1 if "legacy" in input_types: self.wpk_rpc.sendtoaddress(pkh_addr, in_amt) number_inputs += 1 @@ -591,20 +595,12 @@ def test_signtx(self): # https://github.com/bitcoin-core/HWI/pull/795#issuecomment-3112271927 raise unittest.SkipTest("Coldcard sign test temporarily disabled") - if self.emulator.type == "ledger" and not self.emulator.legacy: - # https://github.com/bitcoin-core/HWI/pull/795#issuecomment-3112271927 - raise unittest.SkipTest("Test temporarily disabled for NanoX") - for addrtypes, multisig_types, external, op_return in self.signtx_cases: with self.subTest(addrtypes=addrtypes, multisig_types=multisig_types, external=external, op_return=op_return): self._test_signtx(addrtypes, multisig_types, external, op_return) # Make a huge transaction which might cause some problems with different interfaces def test_big_tx(self): - if self.emulator.type == "ledger" and not self.emulator.legacy: - # https://github.com/bitcoin-core/HWI/pull/795#issuecomment-3112271927 - raise unittest.SkipTest("Test temporarily disabled for NanoX") - # make a huge transaction keypool_desc = self.do_command(self.dev_args + ["getkeypool", "--account", "10", "--addr-type", "sh_wit", "0", "100"]) self.assertIsInstance(keypool_desc, list, f"getkeypool returned error: {keypool_desc}") @@ -636,10 +632,6 @@ def test_big_tx(self): class TestDisplayAddress(DeviceTestCase): def test_display_address_path(self): - if self.emulator.type == "ledger" and not self.emulator.legacy: - # https://github.com/bitcoin-core/HWI/pull/795#issuecomment-3112271927 - raise unittest.SkipTest("Test temporarily disabled for NanoX") - result = self.do_command(self.dev_args + ['displayaddress', "--addr-type", "legacy", '--path', 'm/44h/1h/0h/0/0']) if self.emulator.supports_legacy: self.assertNotIn('error', result) @@ -665,10 +657,6 @@ def test_display_address_bad_path(self): self.assertEqual(result['code'], -7) def test_display_address_descriptor(self): - if self.emulator.type == "ledger" and not self.emulator.legacy: - # https://github.com/bitcoin-core/HWI/pull/795#issuecomment-3112271927 - raise unittest.SkipTest("Test temporarily disabled for NanoX") - account_xpub = self.do_command(self.dev_args + ['getxpub', 'm/84h/1h/0h'])['xpub'] p2sh_segwit_account_xpub = self.do_command(self.dev_args + ['getxpub', 'm/49h/1h/0h'])['xpub'] legacy_account_xpub = self.do_command(self.dev_args + ['getxpub', 'm/44h/1h/0h'])['xpub'] @@ -790,10 +778,6 @@ def _check_sign_msg(self, msg): self.assertTrue(self.rpc.verifymessage(addr, sig, msg)) def test_sign_msg(self): - if self.emulator.type == "ledger" and not self.emulator.legacy: - # https://github.com/bitcoin-core/HWI/pull/795#issuecomment-3112271927 - raise unittest.SkipTest("Test temporarily disabled for NanoX") - self._check_sign_msg("Message signing test") self._check_sign_msg("285") # Specific test case for Ledger shorter S diff --git a/test/test_digitalbitbox.py b/test/test_digitalbitbox.py index 1a98c9562..9fc41ed3f 100755 --- a/test/test_digitalbitbox.py +++ b/test/test_digitalbitbox.py @@ -43,6 +43,7 @@ def __init__(self, simulator): self.include_xpubs = False self.supports_device_multiple_multisig = True self.supports_legacy = True + self.supports_arbitrary_keypool_paths = True def start(self): super().start() diff --git a/test/test_jade.py b/test/test_jade.py index 238c88f37..f5fef76dd 100755 --- a/test/test_jade.py +++ b/test/test_jade.py @@ -53,6 +53,7 @@ def __init__(self, jade_qemu_emulator_path): self.include_xpubs = False self.supports_device_multiple_multisig = True self.supports_legacy = True + self.supports_arbitrary_keypool_paths = True def start(self): super().start() diff --git a/test/test_keepkey.py b/test/test_keepkey.py index 845a0976a..ff9cf84c0 100755 --- a/test/test_keepkey.py +++ b/test/test_keepkey.py @@ -65,6 +65,7 @@ def __init__(self, path): self.include_xpubs = False self.supports_device_multiple_multisig = True self.supports_legacy = True + self.supports_arbitrary_keypool_paths = True def start(self): super().start() diff --git a/test/test_ledger.py b/test/test_ledger.py index 6ab162e74..00a0ceeb2 100755 --- a/test/test_ledger.py +++ b/test/test_ledger.py @@ -47,12 +47,14 @@ def __init__(self, path, legacy=False): self.include_xpubs = True self.supports_device_multiple_multisig = True self.supports_legacy = True + # Bitcoin app 2.5.0 enforces standard Ledger paths for xpub derivation. + self.supports_arbitrary_keypool_paths = legacy def start(self): super().start() automation_path = os.path.abspath("data/speculos-automation.json") app_path = f"./apps/btc-test{'-legacy' if self.legacy else ''}.elf" - os.environ["SPECULOS_APPNAME"] = "Bitcoin Test:1.6.6" if self.legacy else "Bitcoin Test:2.4.1" + os.environ["SPECULOS_APPNAME"] = "Bitcoin Test:1.6.6" if self.legacy else "Bitcoin Test:2.5.0" self.emulator_stderr = open('ledger-emulator.stderr', 'a') # Start the emulator diff --git a/test/test_trezor.py b/test/test_trezor.py index 335fa571f..b61c1f00a 100755 --- a/test/test_trezor.py +++ b/test/test_trezor.py @@ -64,6 +64,7 @@ def __init__(self, path, model): self.include_xpubs = False self.supports_device_multiple_multisig = True self.supports_legacy = True + self.supports_arbitrary_keypool_paths = True def start(self): super().start()