Merge branch 'master' into copilot/fix-livereload-ui-delays

This commit is contained in:
k9ert 2026-08-11 22:20:08 +02:00 committed by GitHub
commit 7781726cd7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
91 changed files with 7814 additions and 14280 deletions

View file

@ -1,125 +0,0 @@
container:
# image: python:slim
# image: ubuntu:jammy
# image: python:3.10
image: ghcr.io/cryptoadvance/specter-desktop/cirrus-jammy:20260412
# We assume here that we're having a proper python3 system including virtualenv and pip
prep_stuff_template: &PREP_STUFF_TEMPLATE
bitcoind_installation_cache:
folder: ./tests/bitcoin
fingerprint_script:
- cat pyproject.toml | grep "addopts = " | cut -d'=' -f2 | sed 's/--/+/g' | tr '+' '\n' | grep bitcoin | cut -d' ' -f2
- cat tests/bitcoin_gitrev_pinned 2> /dev/null || true
- cat /etc/os-release | grep VERSION
- cat ./tests/install_noded.sh
- echo "binary" # if the next line is --bitcoin binary, otherwise use echo "compile" - this ensures different caching keys.
populate_script: ./tests/install_noded.sh --debug --bitcoin binary
elementsd_installation_cache:
folder: ./tests/elements
fingerprint_script:
- cat pyproject.toml | grep "addopts = " | cut -d'=' -f2 | sed 's/--/+/g' | tr '+' '\n' | grep elements | cut -d' ' -f2
- cat tests/elements_gitrev_pinned 2> /dev/null || true
- cat /etc/os-release | grep VERSION
- cat ./tests/install_noded.sh
- echo "binary" # if the next line is --elements binary, otherwise use echo "compile" - this ensures different caching keys.
populate_script: ./tests/install_noded.sh --debug --elements binary
verify_script:
- echo " --> Version of python, virtualenv and pip3"
- python3 --version && virtualenv --version && pip3 --version
- echo " --> Executables in tests/elements/src"
- find tests/elements/src -maxdepth 1 -type f -executable -exec ls -ld {} \; || true
- echo " --> Executables in tests/elements/bin"
- find tests/elements/bin -maxdepth 1 -type f -executable -exec ls -ld {} \; || true
- echo " --> Executables in tests/bitcoin/src"
- find tests/bitcoin/src -maxdepth 1 -type f -executable -exec ls -ld {} \; || true
- echo " --> Executables in tests/bitcoin/bin"
- find tests/bitcoin/bin -maxdepth 1 -type f -executable -exec ls -ld {} \; || true
- echo " --> bitcoind version"
- tests/bitcoin/src/bitcoind -version | head -1 || true
- tests/bitcoin/bin/bitcoind -version | head -1 || true
- echo " --> elements version"
- tests/elements/src/elementsd -version | head -1 || true
- tests/elements/bin/elementsd -version | head -1 || true
pip_script:
#folder: /tmp/cirrus-ci-build/.env
#fingerprint_script: echo muh && cat requirements.txt
#populate_script:
- virtualenv --python=python .env
- source ./.env/bin/activate
- pip3 install -r requirements.txt --require-hashes && pip3 install -e ".[test]"
install_script:
- source ./.env/bin/activate
- pip3 install -e .
test_task:
<< : *PREP_STUFF_TEMPLATE
skip: "false"
test_script:
- source ./.env/bin/activate
- echo $PATH
# needed so that setuptools_scm has a t least one tag to guess the version properly
# and the tests/test_util_version.py doesn't fail
- git fetch origin refs/tags/v1.0.0
- pytest --cov=cryptoadvance --junitxml=./testresults.xml
always:
junit_artifacts:
path: "./testresults.xml"
format: junit
cypress_test_task:
use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true'
skip: "false"
persistent_worker:
labels:
os: linux
isolation:
container:
image: ghcr.io/cryptoadvance/specter-desktop/cypress-python-jammy:20260411
cpu: 6
memory: 6G
pre_prep_script:
# The stupid old debian-package is not installing a proper binary but just the python-package
- echo -e '#!/bin/bash\npython3 -m virtualenv "$@"' > /usr/local/bin/virtualenv
- chmod +x /usr/local/bin/virtualenv
- virtualenv --version
<< : *PREP_STUFF_TEMPLATE
npm_cache:
folder: ./node_modules
fingerprint_script: cat package-lock.json
populate_script: npm ci
cypress_script:
- source ./.env/bin/activate
#- pip3 install -e .
- ./utils/test-cypress.sh --debug run
junit_artifacts:
path: "cypresstest-output.xml"
type: text/xml
format: junit
always:
cypress_screenshots_artifacts:
path: "./cypress/screenshots/**"
cypress_videos_artifacts:
path: "./cypress/videos/**"
extension_smoketest_task:
<< : *PREP_STUFF_TEMPLATE
test_script:
- git config --global user.name "CI CD"
- git config --global user.email "cicd@example.com"
- source ./.env/bin/activate
- echo $PATH
- mkdir tmp && cd tmp
- mkdir testextension && cd testextension
- pwd
- python3 -m cryptoadvance.specter ext gen --ext-id cicdtest --org cryptoadvance --no-isolated-client --devicename cicddevice
- pip3 install -e .
- python3 -m cryptoadvance.specter server --config DevelopmentConfig --debug 2> specter.log &
- sleep 15
- cat specter.log | grep "Found CicdtestService" # Discovery failed
- cat specter.log | grep "Extension CicdtestService activated (alpha)"
- apt-get update && apt-get -y install curl
- curl http://127.0.0.1:25441/svc/cicdtest/ | grep "CicdtestService 4thewin."

View file

@ -267,6 +267,15 @@ jobs:
name: specterd-linux
path: ./release-artifacts
- name: Cache Electron binaries
uses: actions/cache@v4
with:
path: |
~/.cache/electron
~/.cache/electron-builder
key: electron-linux-${{ hashFiles('pyinstaller/electron/package-lock.json') }}
restore-keys: electron-linux-
- name: Prepare Electron build
run: |
VERSION=${GITHUB_REF#refs/tags/}
@ -286,7 +295,17 @@ jobs:
cp -R ../../src/cryptoadvance/specter/static/fonts \
../../src/cryptoadvance/specter/static/output.css \
../../src/cryptoadvance/specter/static/typography.css .
npm run dist -- --linux
# Retry: electron-builder downloads from github.com occasionally 502
# (see release run 24636274855).
attempt=0
until npm run dist -- --linux; do
attempt=$((attempt + 1))
if [ $attempt -ge 3 ]; then
echo "electron-builder failed after $attempt attempts"; exit 1
fi
echo "Attempt $attempt failed; retrying in $((attempt * 15))s..."
sleep $((attempt * 15))
done
cd ../..
- name: Package release
@ -311,6 +330,10 @@ jobs:
runs-on: ubuntu-latest
container:
image: electronuserland/builder:wine
env:
# Shared HOME so `npm ci` (electron postinstall) and electron-builder
# write to the same ~/.cache/electron path — kept cacheable across runs.
HOME: /tmp/electron-home
steps:
- uses: actions/checkout@v4
with:
@ -319,6 +342,7 @@ jobs:
- name: Install system dependencies
run: |
apt-get update && apt-get install -y unzip
mkdir -p $HOME
- name: Download specterd Windows artifact
uses: actions/download-artifact@v4
@ -326,6 +350,15 @@ jobs:
name: specterd-windows
path: ./release-artifacts
- name: Cache Electron binaries
uses: actions/cache@v4
with:
path: |
/tmp/electron-home/.cache/electron
/tmp/electron-home/.cache/electron-builder
key: electron-win-${{ hashFiles('pyinstaller/electron/package-lock.json') }}
restore-keys: electron-win-
- name: Prepare Electron build
run: |
VERSION=${GITHUB_REF#refs/tags/}
@ -339,15 +372,22 @@ jobs:
cd ../..
- name: Build Electron app
env:
HOME: /tmp/electron-home
run: |
mkdir -p $HOME
cd pyinstaller/electron
cp -R ../../src/cryptoadvance/specter/static/fonts \
../../src/cryptoadvance/specter/static/output.css \
../../src/cryptoadvance/specter/static/typography.css .
npm run dist -- --win
# Retry: electron-builder downloads from github.com occasionally 502
# (see release run 24636274855).
attempt=0
until npm run dist -- --win; do
attempt=$((attempt + 1))
if [ $attempt -ge 3 ]; then
echo "electron-builder failed after $attempt attempts"; exit 1
fi
echo "Attempt $attempt failed; retrying in $((attempt * 15))s..."
sleep $((attempt * 15))
done
cd ../..
- name: Package release
@ -401,6 +441,15 @@ jobs:
name: specterd-macos-arm64
path: ./release-artifacts
- name: Cache Electron binaries
uses: actions/cache@v4
with:
path: |
~/Library/Caches/electron
~/Library/Caches/electron-builder
key: electron-mac-${{ hashFiles('pyinstaller/electron/package-lock.json') }}
restore-keys: electron-mac-
- name: Import code signing certificate
if: env.HAVE_APPLE_CERT == 'true'
env:
@ -466,7 +515,17 @@ jobs:
cp -R ../../src/cryptoadvance/specter/static/fonts \
../../src/cryptoadvance/specter/static/output.css \
../../src/cryptoadvance/specter/static/typography.css .
npm run dist -- --mac
# Retry: electron-builder downloads from github.com occasionally 502
# (see release run 24636274855).
attempt=0
until npm run dist -- --mac; do
attempt=$((attempt + 1))
if [ $attempt -ge 3 ]; then
echo "electron-builder failed after $attempt attempts"; exit 1
fi
echo "Attempt $attempt failed; retrying in $((attempt * 15))s..."
sleep $((attempt * 15))
done
cd ../..
env:
# electron-builder reads these for notarization

View file

@ -196,6 +196,31 @@ jobs:
pip3 install -r requirements.txt --require-hashes
pip3 install -e ".[test]"
- name: Dev-server smoketest (from the repo-root)
# No job used to start the dev-server from the repo-root the way
# docs/development.md describes it. That's why #2526 (renaming the
# project to cryptoadvance_specter) could break it unnoticed.
run: |
source ./.env/bin/activate
export SPECTER_DATA_FOLDER=$(mktemp -d)
# --debug enables the werkzeug-reloader which forks a child-process, so
# start a new process-group we can kill as a whole further down
setsid python3 -m cryptoadvance.specter server --config DevelopmentConfig --debug > specterd.log 2>&1 &
specterd_pid=$!
started=""
for i in $(seq 1 30); do
if curl -sf http://127.0.0.1:25441/ > /dev/null; then started="yes"; break; fi
sleep 2
done
kill -- -$specterd_pid || true
# the next step needs port 25441, so make sure it's free again
for i in $(seq 1 10); do
curl -sf http://127.0.0.1:25441/ > /dev/null || break
sleep 1
done
cat specterd.log
if [ -z "$started" ]; then echo "The dev-server did not come up!"; exit 1; fi
- name: Extension smoketest
run: |
git config --global user.name "CI CD"

4
.gitignore vendored
View file

@ -59,3 +59,7 @@ _bmad-output/
# Claude Code local workspace
.claude/
# Local agent workspaces
/.agents/
/.history/

View file

@ -1,281 +0,0 @@
# this image contains python, bitcoind and docker
# check docker/python-bitcoind on how it's built
image: registry.gitlab.com/cryptoadvance/specter-desktop/python-bitcoind:v22.0
variables:
# Cache documentation: https://docs.gitlab.com/ee/ci/caching/
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
cache:
# enable per-job and per-branch caching
- key:
files:
- ./requirements.txt
prefix: "$CI_JOB_NAME"
paths:
- .cache/pip
- .env
stages:
- testing
- releasing
- post_releasing
before_script:
- docker info || echo "no docker-command found" # Print out docker version for debugging
- echo CI_PROJECT_NAMESPACE = $CI_PROJECT_NAMESPACE
- echo CI_PROJECT_ROOT_NAMESPACE = $CI_PROJECT_ROOT_NAMESPACE
- python -V # Print out python version for debugging
- apt update
- apt install -y libusb-1.0-0-dev libudev-dev # usb-support in hidapi
# https://github.com/python-babel/babel/issues/990#issuecomment-1760326334
- rm -f /etc/localtime
- ln -s /usr/share/zoneinfo/Etc/UTC /etc/localtime
# This doesn't get cached in gitlab but we don't need it anyway for now:
# - ./tests/install_noded.sh --debug --elements compile
- pip3 install --upgrade virtualenv
- virtualenv --python=python3 .env
- source .env/bin/activate
check:
stage: testing
# We simply check here whether all the tests on github are completed and green
script:
- ./utils/release.sh wait_on_master || exit 1 # that command will have a non-0 exit-value if not everything is green
# jobs with a preceding . like .test are hidden jobs and are not executed. I leave them in here as
# we might want to reactivate them in the case that github explodes or something.
# So effectively, gitlab is currently only used for releasing.
.test:
stage: testing
# We assume here that people who want to get code into the master-branch are
# relying on PRs and people who are working on gitlab-forks are working
# on CI which probably want fast feedback on the releasing-jobs
# and therefore skip the test-job
# tem deactivated as it did not work as expected
#only:
# - $CI_PROJECT_ROOT_NAMESPACE =~ "cryptoadvance"
script:
- pip3 install -r requirements.txt
- pip3 install -e .
- pip3 install -e ".[test]"
- python3 setup.py install # compiles babel stuff as well (might make pip install obsolete)
- py.test --cov-report term --cov cryptoadvance
.test-cypress:
image: registry.gitlab.com/cryptoadvance/specter-desktop/cypress-python-jammy:v9.7.0
stage: testing
script:
# start the server in the background
- pip3 install -e .
- pip3 install -e ".[test]"
- python3 setup.py install # compiles babel stuff as well (might make pip install obsolete)
- npm i
- ./utils/test-cypress.sh --docker --debug run
- docker ps || echo "probably no docker available anyway"
artifacts:
when: always
paths:
- cypress/videos/**/*.mp4
- cypress/screenshots/**/*.png
expire_in: 1 day
release_pip:
stage: releasing
only:
- tags
script:
- pip3 install -e .
- pip3 install -e ".[test]"
- pip3 install .
- pip3 install build==0.10.0 twine
- python3 -m build
- ls -l dist
# twine reads the password from the env-var TWINE_PASSWORD
# Either testing it or doing the real thing depending on which gitlab-project we're running:
- if ! [[ ${CI_PROJECT_ROOT_NAMESPACE} = "cryptoadvance" ]]; then python3 -m twine upload --verbose --user __token__ dist/* --repository-url https://test.pypi.org/legacy/ ; fi
- if [[ ${CI_PROJECT_ROOT_NAMESPACE} = "cryptoadvance" ]]; then python3 -m twine upload --verbose --user __token__ dist/* ; fi
- cd dist
- sha256sum cryptoadvance.specter-*.tar.gz > SHA256SUMS-pip
- ../utils/artifact_signer.sh sign --artifact ./SHA256SUMS-pip
- cd ..
- cat ./dist/SHA256SUMS-pip
#- python ./utils/github.py upload ./dist/SHA256SUMS-pip
#- python ./utils/github.py upload ./dist/SHA256SUMS-pip.asc
- python ./utils/github.py upload ./dist/cryptoadvance.specter-*.tar.gz
artifacts:
when: always
paths:
- dist/*
expire_in: 1 day
release_binary_windows:
stage: releasing
only:
- tags
variables:
GIT_DEPTH: 0 # Disable shallow clone to get all Git history
tags:
- windows
before_script:
- whoami
- python -V
- pip3 --version
- pip install virtualenv
- virtualenv --python=python3 .env
- .\.env\Scripts\activate
- pip3 install -e ".[test]"
script:
# This script won't execute if the script before that fails
# No need to check the version-scheme again
- echo "Releasing for ${CI_PROJECT_ROOT_NAMESPACE}"
- .\pyinstaller\build-win-ci.bat $CI_COMMIT_TAG
- python ./utils/github.py upload ./pyinstaller/release/specterd-$CI_COMMIT_TAG-win64.zip
- cd ./pyinstaller/release
- python ..\..\utils\release_helper.py sha256sums specterd-$CI_COMMIT_TAG-win64.zip > SHA256SUMS-windows
- type SHA256SUMS-windows
- echo $GPG_PASSPHRASE | c:\Program` Files` `(x86`)\GnuPg\bin\gpg --detach-sign --armor --no-tty --batch --yes --passphrase-fd 0 --pinentry-mode loopback SHA256SUMS-windows
artifacts:
when: always
paths:
- pyinstaller/release/*
expire_in: 1 day
cache:
key:
files:
- ./pyinstaller/electron/package-lock.json
prefix: $CI_JOB_NAME
paths:
- ./pyinstaller/electron/node_modules
release_electron_linux_windows:
image: registry.gitlab.com/cryptoadvance/specter-desktop/electron-builder:latest
stage: releasing
only:
- tags
needs:
- release_binary_windows
before_script:
- python3 -V # Print out python version for debugging
- apt update
- apt install -y unzip libusb-1.0-0-dev libudev-dev # usb-support in hidapi
- pip3 install virtualenv
# Only difference to default befor_script: (ToDo fix this)
- python3 -m virtualenv --python=python3 .env
- source .env/bin/activate
# https://github.com/python-babel/babel/issues/990#issuecomment-1760326334
- rm -f /etc/localtime
- ln -s /usr/share/zoneinfo/Etc/UTC /etc/localtime
- pip3 install -e ".[test]" # TZ=UTC because https://github.com/nektos/act/issues/1853
script:
- echo "Releasing for ${CI_PROJECT_ROOT_NAMESPACE}"
- export CI_PROJECT_ROOT_NAMESPACE # needed in the build-script to download the right windows-binary
- ./utils/build-unix.sh --version $CI_COMMIT_TAG make-hash specterd electron-linux electron-win
- ls -l release
- cd release
- sha256sum specterd-${CI_COMMIT_TAG}-x86_64-linux-gnu.zip specter_desktop-${CI_COMMIT_TAG}-x86_64-linux-gnu.tar.gz > ./SHA256SUMS-linux
- cat ./SHA256SUMS-linux
- sha256sum Specter-Setup-${CI_COMMIT_TAG}.exe > ./SHA256SUMS-win
- cat ./SHA256SUMS-win
- cd ..
- ./utils/artifact_signer.sh sign --artifact ./release/SHA256SUMS-win
- ./utils/artifact_signer.sh sign --artifact ./release/SHA256SUMS-linux
- python3 ./utils/github.py upload ./release/Specter-Setup-${CI_COMMIT_TAG}.exe
- python3 ./utils/github.py upload ./release/specterd-${CI_COMMIT_TAG}-x86_64-linux-gnu.zip
- python3 ./utils/github.py upload ./release/specter_desktop-${CI_COMMIT_TAG}-x86_64-linux-gnu.tar.gz
#- python3 ../utils/github.py upload ./release/SHA256SUMS-linux
#- python3 ../utils/github.py upload ./release/SHA256SUMS-linux.asc
#- python3 ../utils/github.py upload ./release/SHA256SUMS-win
#- python3 ../utils/github.py upload ./release/SHA256SUMS-win.asc
cache:
key:
files:
- ./pyinstaller/electron/package-lock.json
prefix: $CI_JOB_NAME
paths:
- ./pyinstaller/electron/node_modules
artifacts:
when: always
paths:
- release/Specter-Setup-${CI_COMMIT_TAG}.exe
- release/specterd-${CI_COMMIT_TAG}-x86_64-linux-gnu.zip
- release/specter_desktop-${CI_COMMIT_TAG}-x86_64-linux-gnu.tar.gz
- release/SHA256SUMS-linux
- release/SHA256SUMS-linux.asc
- release/SHA256SUMS-win
- release/SHA256SUMS-win.asc
expire_in: 1 day
release_signatures:
stage: post_releasing
only:
- tags
before_script:
- python -V # Print out python version for debugging
- pip3 install --upgrade virtualenv
- virtualenv --python=python3 .env
- source .env/bin/activate
- pip3 install -e ".[test]"
- ./utils/artifact_signer.sh init # prepare .gnupg
script:
- python3 -m utils.release_helper download # downloads the job-artifacts from gitlab
- python3 -m utils.release_helper downloadgithub # downloads additional artifacts from github (if not there and is they have SHA256SUMS-something)
- python3 -m utils.release_helper checksigs # checks the signatures of all SHA256SUMM*.asc files
- python3 -m utils.release_helper checkhashes # checks all SHA256SUM* files (might modify files on the fly due to windows line endings)
- python3 -m utils.release_helper create # creates a SHA256SUM
- ./utils/artifact_signer.sh sign --artifact ./signing_dir/SHA256SUMS # Signs the SHA256SUM
- python3 -m utils.release_helper upload_shasums # uploads SHA256SUMS to github
- python3 -m utils.release_helper upload_shasumssig # uploads SHA256SUMS.asc to github
release_docker:
stage: post_releasing
only:
- tags
before_script:
- echo "Triggering Docker Release"
script:
- ./utils/trigger_docker_build.sh
# Tagging the current master-branch of https://github.com/cryptoadvance/specterext-dummy
# with the same CI_COMMIT_TAG
tag_specterext_dummy_repo:
stage: post_releasing
only:
- tags
before_script:
# write access to git@github.com:cryptoadvance/specterext-dummy.git
- source ./utils/prepare_for_git_write.sh "$SSH_SPECTEREXT_DEPLOY_KEY"
script:
- echo "Now tagging ... git@github.com:${CI_PROJECT_ROOT_NAMESPACE}/specterext-dummy.git"
- ./utils/tag_specterext_dummy.sh
update_github:
stage: post_releasing
only:
- tags
needs:
- release_signatures
before_script:
# write access to git@github.com:swan-bitcoin/specter-static.git
- source ./utils/prepare_for_git_write.sh "$SSH_SPECTERSTATIC_DEPLOY_KEY"
script:
- echo "Now updating https://github.com:${CI_PROJECT_ROOT_NAMESPACE}/specter-desktop/releases/tag/${CI_COMMIT_TAG:-v2.0.4-pre8}"
- ./utils/generate_downloadpage.sh --org_name ${CI_PROJECT_ROOT_NAMESPACE:-k9ert} --debug --version ${CI_COMMIT_TAG:-v2.0.4-pre8} generate github # default-value for testing
update_webpage:
stage: post_releasing
only:
- tags
needs:
- release_signatures
before_script:
# write access to git@github.com:swan-bitcoin/specter-static.git
- source ./utils/prepare_for_git_write.sh "$SSH_SPECTERSTATIC_DEPLOY_KEY"
script:
- echo "Now updating https://github.com:${CI_PROJECT_ROOT_NAMESPACE}/specter-static.git"
- ./utils/generate_downloadpage.sh --org_name ${CI_PROJECT_ROOT_NAMESPACE:-k9ert} --debug --version ${CI_COMMIT_TAG:-v2.0.4-pre8} generate webpage # default-value for testing

View file

@ -67,44 +67,32 @@ python3 -m cryptoadvance.specter server --config DevelopmentConfig --debug
## CI/CD
The project uses **GitHub Actions** for linting, testing, Docker images, and **the full release pipeline**, plus **Cirrus CI** for the heavyweight test suite (pytest + Cypress). GitLab CI is retained in `.gitlab-ci.yml` but effectively dead — the release flow was migrated to GitHub Actions.
**GitHub Actions only.** Cirrus CI and GitLab CI were retired in 2026-Q2 — see `docs/ci-migration-evidence.md` for the cutover evidence and `docs/continuous-integration.md` for the active topology.
### Overview
### Workflows
| Provider | Purpose | Config File | Trigger |
|----------|---------|-------------|---------|
| **GitHub Actions** | Lint, smoke-build, Docker images, **releases** (pip + specterd + Electron for Linux/Win/macOS) | `.github/workflows/` | Push, PR, tags |
| **Cirrus CI** | Full test suite (pytest + Cypress + extension smoketest) | `.cirrus.yml` | PR |
| **GitLab CI** | **Dead** — config retained; `check` job only waits on GH master status. Release jobs no longer run. | `.gitlab-ci.yml` | (vestigial) |
| Workflow | File | Trigger | Purpose |
|----------|------|---------|---------|
| Tests | `test.yml` | PR, push | pytest + Cypress + extension smoketest (3 jobs) |
| Release | `release.yml` | Tag push `v*` | pip + specterd + Electron for Linux/Win/macOS + GPG-sign `SHA256SUMS` |
| Black Linter | `zblack.yml` | PR, push | `psf/black@26.3.0` action pinned to Black 22.3.0, python-3.12 |
| TOC Generator | `toc.yml` | Push | Auto-generates TOCs for `README.md`, `docs/faq.md`, `docs/development.md` |
| Docker Push | `docker-push.yml` | Push to any branch | Multi-arch image → `ghcr.io/cryptoadvance/specter-desktop:<branch>` |
| Docker Tag | `docker-tag.yml` | Tag push `v*` | Multi-arch image → `ghcr.io/cryptoadvance/specter-desktop:<tag>` |
| Extension Compat | `extension-compat.yml` | PR touching `requirements.*`/`pyproject.toml` | Installs full lock, imports every bundled extension, runs `pip check` |
| specterd Build Smoke | `test-specterd-build.yml` | PR touching `pyinstaller/`, `requirements*`, `src/**` | Builds specterd on Linux and runs `--help` |
| Electron Smoke | `electron-smoketest.yml` | PR touching `pyinstaller/electron/**` | Smoke test Electron packaging |
### GitHub Actions (7 workflows)
All workflows use public GitHub-hosted runners (`ubuntu-latest` / `ubuntu-22.04` / `windows-latest` / `macos-14`). **No private runners.**
1. **Black Python Linter** (`zblack.yml`) — Runs on every push and PR. Uses `psf/black@26.3.0` action pinned to Black version `22.3.0`, on python-3.12 (pinned to avoid 3.14 incompatibility with Black 22.3.0). Checks `./src`.
2. **TOC Generator** (`toc.yml`) — Auto-generates TOCs for `README.md`, `docs/faq.md`, `docs/development.md` on push.
3. **Docker Push** (`docker-push.yml`) — Builds multi-arch (amd64 + arm64) image on every push. Pushes to `ghcr.io/<owner>/<repo>:<branch>` (upstream: `ghcr.io/cryptoadvance/specter-desktop:<branch>`).
4. **Docker Tag** (`docker-tag.yml`) — Builds multi-arch image on version tags. Pushes to `ghcr.io/<owner>/<repo>:<tag>`.
5. **Extension Compatibility Check** (`extension-compat.yml`) — On changes to `requirements.*` or `pyproject.toml`: installs the full lock file, imports every bundled extension (Swan, LiquidIssuer, DevHelp, Notifications, ExFund, Faucet, Electrum, Spectrum, StackTrack, TimelockRecovery), runs `pip check`, and best-effort runs extension test suites. Catches dep conflicts before they break downstream extensions.
6. **Test specterd build** (`test-specterd-build.yml`) — PR smoke test on changes to `pyinstaller/`, `requirements*`, `src/**`, or packaging files. Builds specterd on Linux and runs `--help` smoke test.
7. **Release** (`release.yml`) — **The release pipeline.** See next section.
### Test workflow — `test.yml`
All GitHub Actions use public runners (`ubuntu-latest` / `ubuntu-24.04` / `windows-latest` / `macos-14`). **No private runners.**
Three jobs on `ubuntu-22.04`:
1. **`test`** — pytest with `--cov=cryptoadvance`, 45-min timeout. Installs system deps inline; no custom image. Caches bitcoind/elementsd via `actions/cache@v4` keyed on `runner.os × runner.arch × hash(pyproject.toml, install_noded.sh, bitcoin_SHA256SUMS, elements_SHA256SUMS)` with `save-always: true`.
2. **`cypress`** — `./utils/test-cypress.sh --debug run` inside `ghcr.io/cryptoadvance/specter-desktop/cypress-python-jammy@sha256:<digest>`, 30-min timeout, `--shm-size=2g`.
3. **`extension-smoketest`** — byte-compatible port of the old Cirrus smoketest, 15-min timeout.
### Cirrus CI (Testing)
Cirrus CI runs the **full test suite** on PRs. Config: `.cirrus.yml`.
**Three tasks:**
1. **`test_task`** — Full pytest suite with bitcoind + elementsd in regtest mode. Uses cached binary downloads. Produces JUnit XML results.
2. **`cypress_test_task`** — Frontend tests with Cypress. Requires 6 CPU, 6GB RAM. Produces screenshots and video artifacts.
3. **`extension_smoketest_task`** — Generates a test extension, starts the server, verifies the extension loads and responds.
**Docker images used:**
- `registry.gitlab.com/cryptoadvance/specter-desktop/cirrus-jammy:20230206` (pytest)
- `registry.gitlab.com/cryptoadvance/specter-desktop/cypress-python-jammy:20230206` (Cypress)
Both images are pre-built and hosted on the GitLab container registry. They include Python, virtualenv, and other dependencies. The `docker/` directory in the repo contains Dockerfiles for building them.
**Caching:** bitcoind and elementsd binaries are cached by Cirrus based on the version pinned in `pyproject.toml`. The `tests/install_noded.sh` script handles downloading or compiling them.
`tests/install_noded.sh` GPG-verifies upstream `SHA256SUMS.asc` and checks tarball SHA256 against the committed trust anchors on every run (cold cache AND cache hit).
### Release pipeline — `.github/workflows/release.yml`
@ -140,12 +128,12 @@ Triggers on tags matching `v[0-9]+.[0-9]+.[0-9]+` (and `-*` suffixes for pre-rel
| Secret | Purpose | Required? |
|----------|---------|---|
| `APPLE_CERTIFICATE_BASE64` | Base64-encoded `.p12` Apple signing certificate | Optional — unsigned build if missing |
| `APPLE_CERTIFICATE_PASSWORD` | Password for the `.p12` | With `APPLE_CERTIFICATE_BASE64` |
| `APPLE_PROVISIONING_PROFILE_BASE64` | Base64-encoded provisioning profile | Optional |
| PyPI trusted publisher | Configured on PyPI side, not a GH secret | Required for `release-pip` in upstream |
Historical GitLab secrets (`GH_BIN_UPLOAD_PW`, `TWINE_PASSWORD`, `GPG_PASSPHRASE`, `SSH_SPECTEREXT_DEPLOY_KEY`, `SSH_SPECTERSTATIC_DEPLOY_KEY`) are **no longer used**. `.gitlab-ci.yml` still references them but the pipeline is dead.
| `GPG_PRIVATE_KEY` + `GPG_PASSPHRASE` | Sign `SHA256SUMS` | Required for signed releases |
| `APPLE_CERTIFICATE_BASE64` + `APPLE_CERTIFICATE_PASSWORD` | macOS signing cert | Optional — unsigned build if missing |
| `APPLE_ID` + `APPLE_APP_SPECIFIC_PASSWORD` + `APPLE_TEAM_ID` | macOS notarization | Required with signing |
| `APPLE_PROVISIONING_PROFILE_BASE64` | Provisioning profile | Optional |
| `AARON_TRIGGER` | Trigger `lncm/docker-specter-desktop` build | Optional — skips Docker trigger if missing |
| PyPI trusted publisher | Configured on PyPI side, not a GH secret | Required for `release-pip` upstream |
### Testing a release on a fork
@ -154,12 +142,6 @@ Historical GitLab secrets (`GH_BIN_UPLOAD_PW`, `TWINE_PASSWORD`, `GPG_PASSPHRASE
3. The `release-pip` PyPI publish step is gated on `github.repository == 'cryptoadvance/specter-desktop'`, so forks build the pip package but don't publish.
4. Unsigned macOS builds work out of the box; signing requires you to add your own Apple secrets.
### Dead config
- `.gitlab-ci.yml` — still in the repo but release jobs no longer run. Safe to remove in a cleanup pass.
- `pyinstaller/build-win-ci.bat` — former GitLab Windows runner entry point; no longer invoked.
- `utils/release.sh` / `utils/release_helper.py` / `utils/github.py` — may contain dead code paths now that GitLab isn't uploading artifacts. Audit before changes.
## Testing
Tests require a `bitcoind` binary (regtest mode). No tests run without it.
@ -217,10 +199,9 @@ Desktop builds use PyInstaller + Electron:
1. **specterd** (daemon binary): `pyinstaller specterd.spec` from `pyinstaller/` dir
2. **Electron app**: wraps specterd, downloads it on first launch with SHA256 + GPG verification
3. Platform scripts: `utils/build-osx.sh`, `utils/build-unix.sh`, `pyinstaller/build-win-ci.bat`
4. pip package: `python3 -m build`
3. pip package: `python3 -m build`
See `docs/build-instructions.md` for step-by-step manual build instructions.
Release builds live in `.github/workflows/release.yml` (triggered by tag push). See `docs/release-guide.md` for the release workflow and `docs/build-instructions.md` for step-by-step manual builds.
## Dependencies
@ -238,7 +219,7 @@ Extensions live in `specterext` namespace packages. Each extension:
- Registers via entry points in setup.cfg
- Can add UI pages, API endpoints, and background services
- Generate a skeleton: `python3 -m cryptoadvance.specter ext gen --ext-id myext --org myorg`
- CI smoke-tests extension generation in `extension_smoketest_task` (Cirrus)
- CI smoke-tests extension generation in the `extension-smoketest` job (`test.yml`)
- See `docs/extensions/` for the extension development guide
## Key Files for Navigation
@ -255,14 +236,14 @@ Extensions live in `specterext` namespace packages. Each extension:
| All managers | `src/cryptoadvance/specter/managers/` |
| Templates | `src/cryptoadvance/specter/templates/` |
| Tests | `tests/` |
| CI config | `.github/workflows/`, `.cirrus.yml`, `.gitlab-ci.yml` |
| CI config | `.github/workflows/` |
| Build scripts | `pyinstaller/`, `utils/`, `electron/` |
| CI Docker images | `docker/` |
## Current State (as of 2026-04)
- **Last release:** v2.1.1 (2025-01-03) — no release on the new GH Actions pipeline yet; first tagged run will exercise it end-to-end.
- **CI migration complete:** release pipeline moved from GitLab to `.github/workflows/release.yml`. `.gitlab-ci.yml` retained but dead.
- **CI migration complete:** Cirrus CI and GitLab CI retired; all workflows now on GitHub Actions. See `docs/ci-migration-evidence.md`.
- **macOS automation:** now covered on Apple Silicon free tier; x86_64 macOS gated on paid runner.
- **Black linter:** reconfigured to pin python-3.12 + `psf/black@26.3.0` action + black version 22.3.0 (worked around 3.14 incompatibility). Verify green state in CI before assuming.
- **Issue/PR backlog:** refreshed counts not captured here — use `gh issue list` / `gh pr list` for current state.

40
cypress.config.js Normal file
View file

@ -0,0 +1,40 @@
const { defineConfig } = require('cypress')
const setupNodeEvents = require('./cypress/plugins')
const specPattern = [
'cypress/integration/spec_empty_specter_home.js',
'cypress/integration/spec_connections.js',
'cypress/integration/spec_devices.js',
'cypress/integration/spec_ghost_machine.js',
'cypress/integration/spec_fees.js',
'cypress/integration/spec_rescan.js',
'cypress/integration/spec_qr_signing.js',
'cypress/integration/spec_labeling.js',
'cypress/integration/spec_balances_amounts.js',
'cypress/integration/spec_wallet_send.js',
'cypress/integration/spec_wallet_utxo.js',
'cypress/integration/spec_plugins.js',
]
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:25444',
specPattern,
supportFile: 'cypress/support/index.js',
testIsolation: false,
setupNodeEvents,
},
env: {
broadcast_timeout: 8000,
},
includeShadowDom: false,
reporter: 'junit',
reporterOptions: {
mochaFile: 'cypresstest-output.xml',
toConsole: true,
},
retries: {
runMode: 1,
openMode: 0,
},
})

View file

@ -29,8 +29,15 @@ describe('Completely empty specter-home', () => {
})
it('Login with password and deactivate password protection again', () => {
cy.get('[data-cy="admin-password"]').type('satoshi')
cy.get('[data-cy="login-btn"]').click()
cy.get('body').then(($body) => {
// Cypress 13 keeps browser context when testIsolation is disabled for this
// stateful suite. If the session cookie survived from the previous test,
// we are already logged in and can go straight to settings.
if ($body.find('[data-cy="admin-password"]').length) {
cy.get('[data-cy="admin-password"]').type('satoshi')
cy.get('[data-cy="login-btn"]').click()
}
})
cy.get('[data-cy="settings-btn"]').click()
cy.contains('Authentication').click()
cy.get('[data-cy="authentication-selection"]').select('None')

View file

@ -152,7 +152,7 @@ describe('Test the fee UI', () => {
cy.intercept('POST', '/wallets/wallet/ghost_wallet/estimate_fee', {fee: 0.00005944})
cy.get('[data-cy="show-estimated-fee-btn"]').click()
cy.contains('You provided no address')
cy.get('#recipient_0').find('#address').invoke('val', "bcrt1qvtdx75y4554ngrq6aff3xdqnvjhmct5wck95qs")
cy.get('#recipient_0').find('#address').invoke('val', "bcrt1qvtdx75y4554ngrq6aff3xdqnvjhmct5wck95qs").trigger('input')
cy.get('[data-cy="sats-checkbox"]').click()
cy.get('#recipient_0').find('[data-cy="amount-input"]').type(50000, { force: true })
cy.get('[data-cy="show-estimated-fee-btn"]').click()

View file

@ -5,10 +5,8 @@ describe('Test the labeling of addresses and transactions', () => {
cy.visit('/')
})
// Keeps the session cookie alive, Cypress by default clears all cookies before each test
beforeEach(() => {
cy.viewport('macbook-13')
Cypress.Cookies.preserveOnce('session')
})
it('Labeling an address on the address overview', () => {

View file

@ -26,9 +26,16 @@ describe('Test plugins', () => {
})
it('Deactivate password protection', () => {
// This flow only works if we don't keep the session alive! So, no Cypress.Cookies.preserveOnce('session') in beforeEach().
cy.get('#password').type("mySecretPassword")
cy.get('#login-btn').click()
cy.get('body').then(($body) => {
// Cypress 13 keeps browser context when testIsolation is disabled for
// this stateful suite. If the session cookie survived from the
// previous test, we are already logged in and can go straight to
// settings; otherwise log in first.
if ($body.find('#password').length) {
cy.get('#password').type("mySecretPassword")
cy.get('#login-btn').click()
}
})
cy.get('[data-cy="settings-btn"]').click()
cy.get('[href="/settings/auth"]').click()
cy.get('select').select("none")

View file

@ -22,7 +22,6 @@ import './commands'
// Set the view port for each test
beforeEach(() => {
cy.viewport('macbook-13')
cy.session('preserveSession', () => {}) // Keeps the session cookie alive, Cypress by default clears all cookies before each test
cy.visit('/')
})

View file

@ -1,7 +0,0 @@
FROM registry.gitlab.com/cryptoadvance/specter-desktop/python:3.8.5-bionic
RUN apt-get update && apt-get install -y --no-install-recommends libusb-1.0-0-dev libudev-dev
RUN apt-get install -y --no-install-recommends libgl1-mesa-dri gvfs gvfs-libs \
libdrm-amdgpu1 libdrm-nouveau2 libdrm-radeon1 libedit2 libelf1 libllvm10 \
libvulkan1 libzstd1 libtdb1 libcanberra-gtk3-0 virtualenv libcanberra-gtk3-module

View file

@ -1,10 +0,0 @@
This Dockerimage is manually created and uploaded:
```
docker build -t registry.gitlab.com/cryptoadvance/specter-desktop/bionic-build:latest .
docker push registry.gitlab.com/cryptoadvance/specter-desktop/bionic-build:latest
```
The reason for this image is explained in [#356](https://github.com/cryptoadvance/specter-desktop/issues/356) and introduced in https://github.com/cryptoadvance/specter-desktop/pull/396/files .
It has been replaced with introducing the electron-build with `registry.gitlab.com/cryptoadvance/specter-desktop/electron-builder:latest` in https://github.com/cryptoadvance/specter-desktop/pull/473/files .

View file

@ -1,13 +0,0 @@
from ubuntu:jammy
# 1. python-stuff and HWI dependencies
# 2. capability to build bitcoind
# 3. cypress dependencies
RUN apt update && DEBIAN_FRONTEND="noninteractive" apt-get install --no-install-recommends -y \
libusb-1.0-0-dev libudev-dev python3 python3-virtualenv python3-dev python3-pip \
build-essential libtool autotools-dev automake autoconf pkg-config bsdmainutils libevent-dev libboost-dev libboost-system-dev libboost-filesystem-dev libboost-test-dev bc \
nodejs npm libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb \
wget libzmq5-dev
# Stuff needed for Elements (compilation)
RUN DEBIAN_FRONTEND="noninteractive" apt-get install --no-install-recommends -y libboost-thread-dev libsqlite3-dev git

View file

@ -1,10 +0,0 @@
An image used to run the build on cirrus (tests only, not cypress-tests).
Create it like this:
```
docker buildx build --platform linux/amd64 -t ghcr.io/cryptoadvance/specter-desktop/cirrus-jammy:20260412 --load .
docker push ghcr.io/cryptoadvance/specter-desktop/cirrus-jammy:20260412
```
Check the `.cirrus.yml` on how this is used and update the $current_date there.

View file

@ -1,39 +0,0 @@
# Purpose
Used for building the electron-app. In short it's the /pyinstaller/build-unix.sh script which is running in this image.
By intention, this is using an older OS-version in order to avoid glibc-issues. For details, see:
* https://github.com/cryptoadvance/specter-desktop/pull/1688#issuecomment-1242796681
* https://github.com/cryptoadvance/specter-desktop/issues/373#issuecomment-695068924
# Usage
If you want to run the image manually, do something like this (copied from [here](https://www.electron.build/multi-platform-build#build-electron-app-using-docker-on-a-local-machine)):
```
docker run --rm -ti \
--env-file <(env | grep -iE 'DEBUG|NODE_|ELECTRON_|YARN_|NPM_|CI|CIRCLE|TRAVIS_TAG|TRAVIS|TRAVIS_REPO_|TRAVIS_BUILD_|TRAVIS_BRANCH|TRAVIS_PULL_REQUEST_|APPVEYOR_|CSC_|GH_|GITHUB_|BT_|AWS_|STRIP|BUILD_') \
--env ELECTRON_CACHE="/root/.cache/electron" \
--env ELECTRON_BUILDER_CACHE="/root/.cache/electron-builder" \
-v ${PWD}:/project \
-v ${PWD##*/}-node-modules:/project/node_modules \
-v ~/.cache/electron:/root/.cache/electron \
-v ~/.cache/electron-builder:/root/.cache/electron-builder \
electronuserland/builder:wine
```
# Building
build the image like:
```
docker build -t registry.gitlab.com/cryptoadvance/specter-desktop/electron-builder:latest .
docker push registry.gitlab.com/cryptoadvance/specter-desktop/electron-builder:latest
```
# Details
This image is putting python3.10 on top of electronuserland/builder:wine. As we want to have stable build-targets the actual `FROM` clause is `electronuserland/builder:14-wine-10.22` as the project is adding the node version (14) and a timestamp (10.22) to the tagname.

View file

@ -1,188 +0,0 @@
FROM electronuserland/builder:14-wine-10.22
# 14-wine-10.22 is a stable tag which has been the same than the "wine" tag but more explicit
# It's based on node 14 and focal and therefore has glibc 2.31.
# It has been created on Oct 22
# Let's put python3.10 on top:
# * electronuserland/builder:14-wine-10.22 is same as wine which depends on "node"
# * which depends on "base" which builds on buildpack-deps:focal curl
# https://github.com/docker-library/buildpack-deps/blob/98a5ab81d47a106c458cdf90733df0ee8beea06c/ubuntu/focal/curl/Dockerfile
# * now the coe below to install python is copied from:
# https://github.com/docker-library/python/blob/master/3.10/bullseye/Dockerfile
# which builds ontop of buildpack-deps:bullseye-scm
# * So to make the python installation fully functional, we have to install some stuff ...
# * and then do the python installation
# Copying some stuff from https://github.com/docker-library/python/blob/master/3.10/bullseye/Dockerfile
RUN set -ex; \
apt-get update; \
apt-get install -y --no-install-recommends \
autoconf \
automake \
bzip2 \
dpkg-dev \
file \
g++ \
gcc \
imagemagick \
libbz2-dev \
libc6-dev \
libcurl4-openssl-dev \
libdb-dev \
libevent-dev \
libffi-dev \
libgdbm-dev \
libglib2.0-dev \
libgmp-dev \
libjpeg-dev \
libkrb5-dev \
liblzma-dev \
libmagickcore-dev \
libmagickwand-dev \
libmaxminddb-dev \
libncurses5-dev \
libncursesw5-dev \
libpng-dev \
libpq-dev \
libreadline-dev \
libsqlite3-dev \
libssl-dev \
libtool \
libwebp-dev \
libxml2-dev \
libxslt-dev \
libyaml-dev \
make \
patch \
unzip \
zip \
jq \
xz-utils \
zlib1g-dev
# This has been copied from:
# https://github.com/docker-library/python/blob/master/3.10/bullseye/Dockerfile
# ensure local python is preferred over distribution python
ENV PATH /usr/local/bin:$PATH
# http://bugs.python.org/issue19846
# > At the moment, setting "LANG=C" on a Linux system *fundamentally breaks Python 3*, and that's not OK.
ENV LANG C.UTF-8
# runtime dependencies
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends \
libbluetooth-dev \
tk-dev \
uuid-dev \
; \
rm -rf /var/lib/apt/lists/*
ENV GPG_KEY A035C8C19219BA821ECEA86B64E628F8D684696D
ENV PYTHON_VERSION 3.10.9
RUN set -eux; \
\
savedAptMark="$(apt-mark showmanual)"; \
apt-get update; \
apt-get install -y --no-install-recommends \
patchelf \
; \
\
wget -O python.tar.xz "https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz"; \
wget -O python.tar.xz.asc "https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz.asc"; \
GNUPGHOME="$(mktemp -d)"; export GNUPGHOME; \
gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys "$GPG_KEY"; \
gpg --batch --verify python.tar.xz.asc python.tar.xz; \
command -v gpgconf > /dev/null && gpgconf --kill all || :; \
rm -rf "$GNUPGHOME" python.tar.xz.asc; \
mkdir -p /usr/src/python; \
tar --extract --directory /usr/src/python --strip-components=1 --file python.tar.xz; \
rm python.tar.xz; \
\
cd /usr/src/python; \
gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)"; \
./configure \
--build="$gnuArch" \
--enable-loadable-sqlite-extensions \
--enable-optimizations \
--enable-option-checking=fatal \
--enable-shared \
--with-lto \
--with-system-expat \
--without-ensurepip \
; \
nproc="$(nproc)"; \
make -j "$nproc" \
; \
make install; \
\
# https://github.com/docker-library/python/issues/784
# prevent accidental usage of a system installed libpython of the same version
bin="$(readlink -vf /usr/local/bin/python3)"; \
patchelf --set-rpath '$ORIGIN/../lib' "$bin"; \
\
# enable GDB to load debugging data: https://github.com/docker-library/python/pull/701
dir="$(dirname "$bin")"; \
mkdir -p "/usr/share/gdb/auto-load/$dir"; \
cp -vL Tools/gdb/libpython.py "/usr/share/gdb/auto-load/$bin-gdb.py"; \
\
cd /; \
rm -rf /usr/src/python; \
\
find /usr/local -depth \
\( \
\( -type d -a \( -name test -o -name tests -o -name idle_test \) \) \
-o \( -type f -a \( -name '*.pyc' -o -name '*.pyo' -o -name 'libpython*.a' \) \) \
\) -exec rm -rf '{}' + \
; \
\
ldconfig; \
\
apt-mark auto '.*' > /dev/null; \
apt-mark manual $savedAptMark; \
apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \
rm -rf /var/lib/apt/lists/*; \
\
python3 --version
# make some useful symlinks that are expected to exist ("/usr/local/bin/python" and friends)
RUN set -eux; \
for src in idle3 pydoc3 python3 python3-config; do \
dst="$(echo "$src" | tr -d 3)"; \
[ -s "/usr/local/bin/$src" ]; \
[ ! -e "/usr/local/bin/$dst" ]; \
ln -svT "$src" "/usr/local/bin/$dst"; \
done
# if this is called "PIP_VERSION", pip explodes with "ValueError: invalid truth value '<VERSION>'"
ENV PYTHON_PIP_VERSION 22.3.1
# https://github.com/docker-library/python/issues/365
ENV PYTHON_SETUPTOOLS_VERSION 65.5.1
# https://github.com/pypa/get-pip
ENV PYTHON_GET_PIP_URL https://github.com/pypa/get-pip/raw/1a96dc5acd0303c4700e02655aefd3bc68c78958/public/get-pip.py
ENV PYTHON_GET_PIP_SHA256 d1d09b0f9e745610657a528689ba3ea44a73bd19c60f4c954271b790c71c2653
RUN set -eux; \
\
wget -O get-pip.py "$PYTHON_GET_PIP_URL"; \
echo "$PYTHON_GET_PIP_SHA256 *get-pip.py" | sha256sum -c -; \
\
export PYTHONDONTWRITEBYTECODE=1; \
\
python get-pip.py \
--disable-pip-version-check \
--no-cache-dir \
--no-compile \
"pip==$PYTHON_PIP_VERSION" \
"setuptools==$PYTHON_SETUPTOOLS_VERSION" \
; \
rm -f get-pip.py; \
\
pip --version

View file

@ -1,28 +0,0 @@
Used for building the electron-app. In short it's the /pyinstaller/build-unix.sh script which is running in this image.
By intention, this is using an older OS-version in order to avoid glibc-issues. For details, see:
* https://github.com/cryptoadvance/specter-desktop/pull/1688#issuecomment-1242796681
* https://github.com/cryptoadvance/specter-desktop/issues/373#issuecomment-695068924
If you want to run the image manually, do something like this (copied from [here](https://www.electron.build/multi-platform-build#build-electron-app-using-docker-on-a-local-machine)):
```
docker run --rm -ti \
--env-file <(env | grep -iE 'DEBUG|NODE_|ELECTRON_|YARN_|NPM_|CI|CIRCLE|TRAVIS_TAG|TRAVIS|TRAVIS_REPO_|TRAVIS_BUILD_|TRAVIS_BRANCH|TRAVIS_PULL_REQUEST_|APPVEYOR_|CSC_|GH_|GITHUB_|BT_|AWS_|STRIP|BUILD_') \
--env ELECTRON_CACHE="/root/.cache/electron" \
--env ELECTRON_BUILDER_CACHE="/root/.cache/electron-builder" \
-v ${PWD}:/project \
-v ${PWD##*/}-node-modules:/project/node_modules \
-v ~/.cache/electron:/root/.cache/electron \
-v ~/.cache/electron-builder:/root/.cache/electron-builder \
electronuserland/builder:wine
```
build the image like:
```
docker build -t registry.gitlab.com/cryptoadvance/specter-desktop/electron-builder:latest .
docker push registry.gitlab.com/cryptoadvance/specter-desktop/electron-builder:latest
```

View file

@ -1,18 +0,0 @@
FROM python:3.8
ARG REPO=https://github.com/cryptoadvance/github-changelog
RUN apt update && apt install -y git
WORKDIR /
RUN git clone $REPO;
WORKDIR /github-changelog
RUN git checkout master
RUN python3 setup.py install
ENV PYTHONUNBUFFERED="1"
ENTRYPOINT ["changelog"]

View file

@ -1,25 +0,0 @@
An image used to create changelogs.
Create it like this:
```
docker build . -t registry.gitlab.com/cryptoadvance/specter-desktop/github-changelog:latest
docker push registry.gitlab.com/cryptoadvance/specter-desktop/github-changelog:latest
```
Use it like this:
```
latest_version=v0.8.1
export GH_TOKEN=YourTokenHere
docker run 4a3dd375832d --github-token $GH_TOKEN --branch master cryptoadvance specter-desktop $latest_version > docs/new_release_notes.md
cp docs/release-notes.md docs/release-notes.md.orig
cat docs/new_release_notes.md docs/release-notes.md.orig > docs/release-notes.md
rm docs/release-notes.md.orig docs/new_release_notes.md
```
# This will print out links to all PRs in order to review better
```
docker run 4a3dd375832d -m --github-token $GH_TOKEN --branch master cryptoadvance specter-desktop $latest_version > docs/new_release_notes.md
```

View file

@ -6,13 +6,9 @@ This document addresses the build-system part. For the continuous-integration-pa
## pip-packages
```
# in the case of a release, the version needs to be adapted:
# sed -i "s/version=\".*/version=\"$CI_COMMIT_TAG\",/" setup.py
python3 setup.py sdist bdist_wheel
cryptoadvance.specter-vx.y.z-get-replaced-by-release-script.tar.gz
python3 -m build
```
This process is the same for all platforms. The result unfortunately is not stable in terms of identically sh256-hashes, though.
Produces an sdist + wheel under `dist/`. The release pipeline sets `SETUPTOOLS_SCM_PRETEND_VERSION` from the git tag; locally, setuptools-scm derives the version from your working tree. The result is not stable in terms of identical sha256-hashes across machines.
## Electron
The electron build is assuming a node-installation. So make sure you have `node` and `npm` available.
@ -20,7 +16,7 @@ The electron build is assuming a node-installation. So make sure you have `node`
The electron-app is built in a way that it's running the `specterd` (specter-demon) internally. It's not bundled with the electron-binary but downloaded with the first start (including sha256- and gpg-verification).
If someone does not want the download, he can manually choose a specterd-binary from the `preferences/Advanced` menu. Nevertheless the Electron-App is tied, at buildtime, to a specific specterd-binary via a sha256-version. This probably doesn't make so much sense if you build outside of a release but we need it anyway.
So let's cover the build of the specterd-binary first. Below is a manual description of the build-process. There are acripts which are doing this but they are partially optimized for the CI-system. Check the `pyinstaller/build-*` scripts for details.
So let's cover the build of the specterd-binary first. Below is a manual description of the build-process. The canonical CI build lives in `.github/workflows/release.yml` (`build-specterd-*` and `build-electron-*` jobs).
First set the virtualenv:
@ -31,11 +27,6 @@ source .buildenv/bin/activate
### specterd Linux and MacOS
Below doesn't seem to work properly, at least on MacOS, better use the build script. For MacOS, that would be:
```bash
./utils/build-osx.sh --version 0.0.0-pre1 specterd
```
```bash
cd pyinstaller
# prerequisites
@ -98,7 +89,7 @@ node ./set-version v1.3.1-custom ../dist/specterd
npm i
# We assume here that no Apple-developer-ID is used to sign the binary.
# Check `build-osx.sh` if you want to sign
# For signed+notarized builds, see .github/workflows/release.yml (build-electron-macos).
echo "`jq '.build.mac.identity=null' package.json`" > package.json
# finally build

View file

@ -0,0 +1,84 @@
# CI Migration Evidence — Cirrus → GitHub Actions
Evidence artifact per `docs/cirrus-replacement-spec.md` §Acceptance. Captures measured GHA behavior over the side-by-side period so that "did we actually hit SLO?" has a grep-able answer after GH Actions logs are GC'd.
## Snapshot
- **Measurement date:** 2026-04-19
- **PR #1 (`test.yml` added):** merged 2026-04-17 as commit `a24df2eb` (PR [#2606](https://github.com/cryptoadvance/specter-desktop/pull/2606))
- **Cirrus sunset deadline:** 2026-06-30 (~10 weeks remaining)
- **Side-by-side window so far:** ~2 days
## Gate status
Spec §Acceptance requires **all four** to hold before merging the cutover PR:
| # | Criterion | Status | Notes |
|---|----------------------------------------------------------|------------|-----------------------------------------------------------------------|
| 1 | 10 consecutive green master runs | **Not met**| 2 master runs observed, both green |
| 2 | ≥ 3 green PR runs incl. one frontend-touching | **Partial**| 6 green PR runs; none confirmed as frontend-touching yet |
| 3 | Zero new flakes over ≥ 50 total runs | **Not met**| 14 total runs; zero flakes detected; sample too small |
| 4 | Cypress p95 within Cirrus +20% | **Breach** | Cypress p95 **10m10s** vs. Cirrus +20% ceiling **7m14s** — see below |
**Merging PR #2 ahead of the nominal gate is a deliberate choice** driven by Cirrus's hard 2026-06-30 shutdown, preservation of revertability (PR #2 is a pure deletion of dead code + docs updates; revert is one click), and the empirical fact that no flakes have surfaced over the available sample. Gate criteria 1 and 3 will be satisfied by ordinary master-branch activity over the coming weeks; criterion 4 is acknowledged below as a known deviation, with a measurement protocol for re-evaluation.
## Measured wall-clock
n = 8 successful runs (2 master + 6 PR) between 2026-04-17 11:13 UTC and 2026-04-17 20:45 UTC.
| Job | n | median | p95 | min | max | Cirrus median | Cirrus +20% ceiling | Result |
|-----------------------|---|---------|---------|---------|---------|---------------|---------------------|--------------------|
| `test` | 8 | 3m53s | 4m14s | 3m43s | 4m14s | 4m47s | 5m44s | **within ceiling** |
| `cypress` | 8 | 9m42s | 10m10s | 9m28s | 10m10s | 6m02s | 7m14s | **BREACH (+47%)** |
| `extension-smoketest` | 8 | 1m57s | 2m09s | 1m44s | 2m09s | 2m10s | 2m36s | **within ceiling** |
Raw data pulled via `gh api repos/cryptoadvance/specter-desktop/actions/runs/<id>/jobs` for runs `24581485817`, `24585472647` (master), and `24580543016`, `24581562475`, `24582301901`, `24582337498`, `24585550944`, `24562219139` (PR).
Cirrus baselines cited from `docs/cirrus-replacement-spec.md` §SLOs (20-sample baseline captured 2026-04-12).
### Cypress breach — acknowledgement
GHA Cypress p95 is **10m10s**, vs. the spec's Cirrus +20% ceiling of **7m14s** (Cirrus p95 6m55s × 1.20). Root cause not yet investigated. Candidates per spec §Cypress measurement: `--shm-size` bump, spec sharding, or escalation to `ubuntu-22.04-large`.
**Decision:** accepted as a known deviation. Cypress wall-clock is still well under the 30-minute workflow timeout, and the alternative — holding the cutover until after Cirrus shutdown — would leave the project without PR gating. The breach is logged here rather than swept under the rug.
**Follow-up:** re-run the measurement protocol (5× on `ubuntu-22.04` free tier) once 10+ master runs accumulate. If p95 remains >Cirrus+20%, file an issue and walk the escalation ladder (shm → shard → paid runner).
## Flake signal
Over 14 total `test.yml` runs (8 success, 5 failure, 1 action_required):
- **Failures on `kn/cirrus-replacement-spec`** (4): iteration during PR #1 development. Each failure was followed by a targeted fix commit. Confirmed non-flaky by reading `git log` (`fix: cache symlink targets…`, `fix: bash shell for cypress container`, `fix: use VALIDSIG instead of GOODSIG`, `fix: use --status-fd`).
- **Failure on `kn/bump-bitcoind-test-v27.2`** (1): bitcoind version bump branch. Likely a real test failure from the version change, not a CI flake.
- **`action_required`** (1): fork PR (`copilot/fix-livereload-ui-delays`) pending maintainer approval to run workflows. Not a flake.
**Flake count: 0** over this window. Sample size too small (n=14) to assert the steady-state SLO of ≤1% rolling-30-day, but no red flags.
## Sample PR runs
| URL | Branch | Conclusion | Frontend-touching? |
|---------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------|------------|--------------------|
| https://github.com/cryptoadvance/specter-desktop/actions/runs/24585550944 | `dependabot/npm_and_yarn/pyinstaller/electron/multi-20d65b3440`| success | partial (electron deps) |
| https://github.com/cryptoadvance/specter-desktop/actions/runs/24582337498 | `dependabot/npm_and_yarn/pyinstaller/electron/multi-3ffb4d349a`| success | partial (electron deps) |
| https://github.com/cryptoadvance/specter-desktop/actions/runs/24581562475 | `dependabot/npm_and_yarn/multi-6f6dcfc8d5` | success | indirect |
| https://github.com/cryptoadvance/specter-desktop/actions/runs/24562219139 | `kn/cirrus-replacement-spec` | success | no |
Strict frontend-touching coverage (changes under `src/cryptoadvance/specter/static/` or `src/cryptoadvance/specter/templates/`) is **not yet confirmed** in the available sample. Criterion 2 will be rechecked at PR #2 merge time.
## Sign-off
| Field | Value |
|----------------------|------------------------------------------------------------|
| Evidence captured at | 2026-04-19 |
| Latest master commit | `62ea0265` (2026-04-17 20:33 UTC) |
| `test.yml` added at | `a24df2eb` (2026-04-17 18:50 UTC) |
| Author | @k9ert |
## Rollback contract
Per spec §Rollback, if post-cutover breakage emerges:
1. Revert the PR #2 merge commit → restores `.cirrus.yml` and `docker/cirrus-jammy/`.
2. Re-add the Cirrus required-check names to master branch protection.
3. Cirrus assumed operational through 2026-06-30.
After 2026-06-30, no Cirrus fallback exists — forward fix only. Keep the cutover ≥ 4 weeks ahead of that date (target merge date per spec: 2026-05-12).

View file

@ -1,111 +1,60 @@
# Introduction
Specter-Desktop is using GitLab, Cirrus and GitHub-Actions for continuous integration purposes but GitHub-actions only for Blackify so far. It might be more effort using more than one CI-approach but it makes us also more resilient.
GitLab and Cirrus have both advantages and disadvantages so ... let's use both!
GitLab:
* is completely open Source for server- and clients
* the gitlab-runner can run docker and is itself running on docker
* but does not support Pull-Requests
* needs to have bitcoind in a prepared docker-container which binds the build to that version
# Continuous Integration
Cirrus-CI:
* supports the PR-model
* quite easy to setup even though it's using docker
Specter-Desktop runs all CI on **GitHub Actions**. Cirrus CI and GitLab CI were retired in 2026-Q2 — see `docs/ci-migration-evidence.md` for the cutover evidence.
## Gitlab
## Workflows
Gitlab is a great CI/CD-platform and in the meantime it's quite easy to use it for GitHub-repositories.
https://docs.gitlab.com/ee/ci/ci_cd_for_external_repos/github_integration.html
The main file which specifies the jobs on GitLab is .gitlab-ci.yml
We're using a `gitlab-docker-runner` which means that all jobs are running in a container.
However at the same time we're using docker to spinup a bitcoind.
| Workflow | File | Trigger |
|--------------------------------|-------------------------------------------------|----------------------------------------------|
| Lint (black) | `.github/workflows/zblack.yml` | PR, push |
| Tests (pytest + Cypress + extension smoketest) | `.github/workflows/test.yml` | PR, push |
| Release | `.github/workflows/release.yml` | Tag push (`v*`) |
| Electron smoketest | `.github/workflows/electron-smoketest.yml` | PR and push to master on `pyinstaller/electron/**` |
| Extension compatibility | `.github/workflows/extension-compat.yml` | PR; push on `requirements.*` / `pyproject.toml`; `workflow_dispatch` |
| Specterd build smoke | `.github/workflows/test-specterd-build.yml` | PR |
| Docker image push | `.github/workflows/docker-push.yml` | Push to any branch |
| Docker image tag | `.github/workflows/docker-tag.yml` | Tag push (`v*`) |
| Docs table of contents | `.github/workflows/toc.yml` | Push |
The image is created manually (see /docker) and used for running the tests AND also for
spinning up bitcoind.
## Test workflow
For that reason we need to share the docker-socket from the host into the container and
create our own GitLab specific runner as described here:
https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#use-docker-socket-binding
`test.yml` has three jobs, all on `ubuntu-22.04`:
Due to that setup there are some specifics which are mainly addressed in tests/conftest
start_bitcoind-function:
* adding -rpcallowip= (from a docker network) to bitcoind
* not use localhost but the docker-network-ip-address when talking to the bitcoind
- **`test`** — pytest with `--cov=cryptoadvance`. Runs in 45 min. Installs system deps inline; no custom image. Caches bitcoind/elementsd binaries via `actions/cache@v4` keyed on `runner.os × runner.arch × hash(pyproject.toml, tests/install_noded.sh, tests/bitcoin_SHA256SUMS, tests/elements_SHA256SUMS)`.
- **`cypress`** — runs `./utils/test-cypress.sh --debug run` inside `ghcr.io/cryptoadvance/specter-desktop/cypress-python-jammy@sha256:<digest>`. 30-minute timeout. `--shm-size=2g` to avoid Cypress OOMs on the default 64 MB `/dev/shm`. Shares the bitcoind/elements cache with `test`.
- **`extension-smoketest`** — byte-compatible port of the former Cirrus smoketest. 15 min. Smoke-tests `ext gen`, server boot, and log-line / curl assertion. Contract must stay stable — downstream extension developers depend on it.
## Travis-CI
All three jobs use `actions/checkout@v4` with `fetch-depth: 0` so `git describe` resolves annotated tags for `tests/test_util_version.py`.
We're no longer using travis-ci due to the abuse-detection-system going wild on us.
## Caching
## Cirrus-CI
`actions/cache@v4` with `save-always: true` on a key that includes `runner.arch` (prevents ARM/x86 cache poisoning). The key hashes the committed `tests/bitcoin_SHA256SUMS` and `tests/elements_SHA256SUMS` trust anchors — bumping a version in `pyproject.toml` rotates the cache via those files.
[Cirrus-CI](https://cirrus-ci.org) is used by Bitcoin-Core and HWI and is a quite good replacement for travis. We're using it only for PRs so far. The [../.cirrus.yml] file defines the build. We have two task, one for pytest and one for the [cypress-tests](./cypress-testing.md).
### Binary verification
## Releasing
`tests/install_noded.sh` GPG-verifies the upstream `SHA256SUMS.asc` against the Bitcoin Core and Elements release signing keys, and checks the tarball SHA256 against the committed trust anchors on every run (cold cache AND cache hit). A tampered cache entry fails closed on restore. See PR #2606 for the threat model.
### What gets released
## Cypress container
We're mostly releasing automatically. Currently the following artifacts are released:
* specterd (daemon) is a binary for kicking off the specter-desktop service on the command-line. We have binaries for windows, Linux and macOS
* We have an Electron-App which we're also releasing for Windows, Linux and MacOS. Unfortunately the macOS build is not yet automated
* We release a pip-package
* Usually some time after the release, the lncm is releasing [docker-images](https://hub.docker.com/r/lncm/specter-desktop). Very much appreciated, even though we can't guarantee for them, obviously.
`ghcr.io/cryptoadvance/specter-desktop/cypress-python-jammy` is pinned by digest (not tag) in `test.yml`. This makes Dockerfile edits visibly require a workflow bump. When editing `docker/cypress-python-jammy/Dockerfile`, rebuild and push to GHCR with a fresh tag, then update the digest pin.
### How we release
As we have a strict build-only-on-private-hardware build-policy, we're using GitLab private runners in order to build our releases. In order to test and develop the releasing automation, people can setup GitLab-projects which are syncing from their GitHub-forks. With such a setup it's possible to create test-releases and therefore test the whole procedure end-to-end.
## Release pipeline
The automation of that kicks in if someone creates a tag which is named like "vX.Y.Z". This is specified in the gitlab-ci.yml. The release-job will only be triggered in cases of tags. One step will also check that the tag follows the convention above.
The package upload will need a token. How to obtain the token is described in the packaging-tutorial. It's injected via GitLab-variables. ToDo: put the token on a trusted build-node.
See [`release-guide.md`](./release-guide.md). Pushing a tag matching `v[0-9]+.[0-9]+.[0-9]+[-*]?` triggers `release.yml`, which builds pip/specterd/Electron artifacts for Linux/Windows/macOS, signs `SHA256SUMS`, and creates a draft GitHub release. Docker images are built by `lncm/docker-specter-desktop` (triggered via `AARON_TRIGGER` secret).
### pyinstaller system-dependent binaries
The [pyinstaller directory](../pyinstaller) contains scripts to create the platform-specific binaries (plus electron) to use specter-desktop as a desktop-software. Some of them are created and uploaded to [GitHub-releases](https://github.com/cryptoadvance/specter-desktop/releases) via more or less special build-agents.
The [windows-build-agent](https://docs.gitlab.com/runner/install/windows.html) needs manual installation
of git, python and docker. Docker is used to build the innosetup-file.
As docker is available in windows only as a "desktop-edition", one need to also
log into the windows-machine to get docker started.
Clearly there is an opportunity to move all of the creation of the windows-binary to wine on docker,
similiar to the way the innosetup is running within docker.
## Flake policy
## CI/CD-dev-env setup
- Cypress: `retries: { runMode: 1, openMode: 0 }`. Specs retry-to-green emit a warning annotation.
- pytest: `--reruns 0` (fail fast). Flakes are debt, not a coping mechanism.
- Spec flagged flaky twice in 14 days gets `@skip(reason="flaky", issue="#NNNN")` with a 2-week SLA.
Here is a brief description on how to create a setup where the release-procedures can be tested:
* We assume you have a fork of cryptoadvance/specter-desktop. We also assume that your GitLab-user-handle is the exact same as on GitHub.
* Create a GitLab-account and then a mirroring project ([here](https://gitlab.com/projects/new#cicd_for_external_repo)) obviously with the exact same name: "specter-desktop"
* Activate the private runners and deactivate the public runners. Contact @k9ert for that.
* Create an account and an [API token](https://test.pypi.org/manage/account/) on there
* Create a token for GitHub in order to release to your GitHub-fork
* Configure both tokens on the GitLab-variables (GH_BIN_UPLOAD_PW and TWINE_PASSWORD)
* create a tag on your GitHub-fork
* watch the test-release unfolding, ready to hack
## Secrets
### GitLab-runner setup (Windows)
| Secret | Used by | Purpose |
|---------------------------|--------------------------|----------------------------------------------|
| `GITHUB_TOKEN` | (auto-provided) | Checkout, artifact upload, ghcr.io push |
| `GPG_PRIVATE_KEY` + `GPG_PASSPHRASE` | `release.yml` | Sign `SHA256SUMS` |
| `APPLE_*` (six) | `release.yml` macOS | Code signing + notarization (optional) |
| `AARON_TRIGGER` | `release.yml` | Trigger `lncm/docker-specter-desktop` build |
For Windows-releasing, we're using a windows GitLab-runner. Here is a short description on how to set one up.
#### Prerequisites
You need at least Windows Home 10 which is up-to-date. The most complex dependency is setting up docker.
Docker-Desktop needs a WSL2 which is a good idea to install on windows anyway. [Here](https://www.omgubuntu.co.uk/how-to-install-wsl2-on-windows-10) is a description on how to do that.
While installing, make sure you know the locations of where that stuff is installed. We'll later need to verify/adjust the PATH.
* Install Python, i took the [3.7.9 webinstaller](https://www.python.org/ftp/python/3.7.9/python-3.7.9-amd64-webinstall.exe)
* Install Git, e.g. [this](https://github.com/git-for-windows/git/releases/download/v2.29.2.windows.2/Git-2.29.2.2-64-bit.exe) (i had 2.28.2)
* Install [Docker-Desktop](https://desktop.docker.com/win/stable/Docker%20Desktop%20Installer.exe)
Now open and check the "Environment-variables" and check that the following lines are in there:
![](./images/continuous-integration_runner_windows_envvars.png)
#### Runner
The runner itself is easy to [setup](https://docs.gitlab.com/runner/install/windows.html). Follow the link or this very brief description:
* `mkdir \Gitlab-Runner`
* download [this binary](https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-amd64.exe) in that folder and rename to gitlab-runner.exe
* Search for "powershell" in windows an open AS ADMINISTRATOR
* `cd \Gitlab-Runner`
* Copy the Registration-token from [here](https://gitlab.com/k9ert/specter-desktop/-/settings/ci_cd) (unfold runners, see specific runners)
* `./gitlab-runner.exe register`and paste the token (the instance-url is the default)
* give a reasonable description. Make sure to tag this runner with "tag". If that's not possible here, you can do it in the page mentioned above
* `.\gitlab-runner.exe install` will install the runner as system-service
* `.\gitlab-runner.exe start` will start it
Done
No GitLab secrets remain.

View file

@ -14,6 +14,7 @@
- [Set up virtualenv](#set-up-virtualenv)
- [If `pip install` fails on `cryptography==3.4.x`](#if-pip-install-fails-on-cryptography34x)
- [How to run the tests](#how-to-run-the-tests)
- [Hardware-attended Jade tests](#hardware-attended-jade-tests)
- [Code-Style](#code-style)
- [Developing on tests](#developing-on-tests)
- [bitcoin-specific stuff](#bitcoin-specific-stuff)
@ -209,6 +210,36 @@ Print the logging output live to the terminal:
pytest --capture=no --log-cli-level=DEBUG
```
### Hardware-attended Jade tests
`tests/test_jade_hardware.py` exercises Specter's HWI integration end-to-end against a physical Blockstream Jade. It is gated by `--run-jade-hardware` and skipped by default, so GitHub Actions ignore it without any workflow change.
Run with `-s` so operator prompts reach the terminal:
```
pytest --run-jade-hardware tests/test_jade_hardware.py -s
```
Three tests, increasing operator effort:
| Test | What it does | Operator action |
|---|---|---|
| `test_jade_enumerate_via_specter` | `HWIBridge.enumerate()` finds the Jade and returns a fingerprint | Connect Jade, unlock |
| `test_jade_extract_xpub_via_specter` | Pulls xpub at `m/84h/0h/0h` (mainnet) | Confirm xpub export on device |
| `test_jade_sign_psbt_via_specter` | Signs a canned testnet PSBT through Specter's sign path | Boot Jade in Temporary Signer mode, scan SeedQR, confirm tx |
The signing test uses the public **BIP-39 abandon vector** (`abandon abandon ... about`) so the PSBT fixture matches anyone's Jade once they load that seed. Setup procedure:
1. Power-cycle the Jade so it shows the boot menu.
2. Choose **Temporary Signer** -> **Scan SeedQR**.
3. Display `tests/fixtures/jade_seedqr_abandon.png` (or `cat tests/fixtures/jade_seedqr_abandon.txt` for the ASCII version) and scan it with the Jade camera.
4. When Jade asks for the network, select **TESTNET**.
5. Press Enter at the test prompt.
6. Confirm the transaction on the Jade screen when it pops up (~99,500 sats to a testnet bech32 output, ~99,500 change auto-validated, 1,000 fee).
Temporary Signer state is held in RAM only and wiped on power-cycle/USB-unplug — your real seed is not affected. Expected master fingerprint for the abandon vector is `73c5da0a`; the test fails fast with a clear hint if the loaded seed is wrong.
The fixture PSBT (`tests/fixtures/jade_hardware.psbt`) was generated with embit against m/84'/1'/0'/0/0 of the abandon vector, including a synthetic `non_witness_utxo` so Jade can verify the input amount per the SegWit fee-spoof mitigation.
Get the log-output of bitcoind side by side with the test-output. For sure you will only see the logs if the test fails.
```
pytest --bitcoind-log-stdout

View file

@ -31,6 +31,7 @@
- [*Can this also work with external nodes like Casa, MyNode, and Raspilitz?*](#can-this-also-work-with-external-nodes-like-casa-mynode-and-raspilitz)
- [*Can I use Tor?*](#can-i-use-tor)
- [I forgot my password, how can I reset it?](#i-forgot-my-password-how-can-i-reset-it)
- [Does the Specter password encrypt my wallet data files?](#does-the-specter-password-encrypt-my-wallet-data-files)
- [BACKING UP FUNDS](#backing-up-funds)
- [*If something happens to the `~/.specter` folder, is it still possible to **restore** access to multisigs created there (assuming there is no backup of the `~/.specter` folder)?*](#if-something-happens-to-the-specter-folder-is-it-still-possible-to-restore-access-to-multisigs-created-there-assuming-there-is-no-backup-of-the-specter-folder)
- [*To recover a multisig that was built on specter (eg: 2 of 3 with ColdCard), is having the seeds of all 3 signing wallets sufficient or do we need to backup more info?*](#to-recover-a-multisig-that-was-built-on-specter-eg-2-of-3-with-coldcard-is-having-the-seeds-of-all-3-signing-wallets-sufficient-or-do-we-need-to-backup-more-info)
@ -241,6 +242,12 @@ Depending on "what's written in `somethingInHere`:
* If it's `usernamepassword`, you won't be able to recover the password but you can deactivate it by setting it to `none` and restart Specter.
* If it's `none` (or you just set it to `none`) you can login without any password. So hurry up with setting it again within specter.
### Does the Specter password encrypt my wallet data files?
No. Specter authentication protects access to the app/session, but it does not encrypt the whole Specter data folder at rest. The data folder contains wallet metadata files such as xpubs, addresses, wallet names, labels, and transaction metadata that may be readable by anyone or any process with filesystem access to that folder.
If this is in your threat model, use OS-level disk encryption (for example FileVault on macOS), make sure backups and sync tools that include the Specter data folder are encrypted and access-controlled, and consider running Specter from a data folder located on an encrypted volume/container via `--specter-data-folder`.
## BACKING UP FUNDS
### *If something happens to the `~/.specter` folder, is it still possible to **restore** access to multisigs created there (assuming there is no backup of the `~/.specter` folder)?*

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 448 KiB

View file

@ -1,160 +1,60 @@
# Release Guide
## Creating release notes
The release pipeline runs on GitHub Actions. Pushing a version tag to `upstream` triggers `.github/workflows/release.yml`, which builds every artifact, creates a draft GitHub release, and signs `SHA256SUMS` with the Specter Signer GPG key.
### Pre-requisites
## Prerequisites
- You need the correct upstream master. You should see
- `upstream` points at `git@github.com:cryptoadvance/specter-desktop.git` (`git remote -v` should show both fetch and push).
- You are on `master` with a clean workspace and `git pull upstream master` applied.
- Release-notes PR has already merged (see [Release notes](#release-notes) below).
## Cut a release
```bash
git remote -v | grep upstream
upstream git@github.com:cryptoadvance/specter-desktop.git (fetch)
upstream git@github.com:cryptoadvance/specter-desktop.git (push)
git tag v1.13.1
git push upstream v1.13.1
```
- You need a GitHub token:
If you don't have one, get one here https://github.com/settings/tokens and make sure to tick the boxes for repo and workflow as below:
That's it. The `Release` workflow on GitHub Actions takes it from here:
![](./images/release-guide/github-token.png)
- **`release-pip`** — builds the sdist/wheel and publishes to PyPI via trusted publishing.
- **`build-specterd-{linux,windows,macos}`** — builds the `specterd` binary on each platform (macOS arm64 on the free `macos-14` runner).
- **`build-electron-{linux,windows,macos}`** — builds the Electron apps using each platform's `specterd` artifact. Windows uses the public `electronuserland/builder:wine` image; macOS signs + notarizes if `APPLE_CERTIFICATE_BASE64` et al. are configured.
- **`create-release`** — collects all artifacts, generates `SHA256SUMS`, signs it with the GPG key from the `GPG_PRIVATE_KEY` secret, generates a release body (with auto-generated "What's Changed" via `gh api .../generate-notes`), and creates a **draft** GitHub release.
- **`trigger-docker`** — POSTs a repository-dispatch to `lncm/docker-specter-desktop` so Aaron's Docker build picks up the new tag (needs `AARON_TRIGGER` secret; skipped otherwise).
Using the new token, run
The release lands as a draft — review and publish it manually on GitHub.
### Required secrets
| Secret | Purpose |
|------------------------------------|--------------------------------------------------------------|
| `GPG_PRIVATE_KEY` | ASCII-armored private key for signing `SHA256SUMS` |
| `GPG_PASSPHRASE` | Passphrase for the above |
| `APPLE_CERTIFICATE_BASE64` | Developer ID cert for macOS signing (optional — unsigned fallback) |
| `APPLE_CERTIFICATE_PASSWORD` | p12 password |
| `APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, `APPLE_TEAM_ID` | Apple notarization credentials |
| `APPLE_PROVISIONING_PROFILE_BASE64`| Optional provisioning profile |
| `AARON_TRIGGER` | PAT for triggering `lncm/docker-specter-desktop` |
PyPI publishing uses trusted publishing (OIDC) — no secret required.
## Release notes
Update `docs/release-notes.md` via a normal PR before tagging. Use the GitHub API or `gh` to pull "What's Changed" between the previous tag and `master`, prepend a heading, and open a PR. The `create-release` workflow job also appends auto-generated notes to the release body.
## GitHub pages download page
`./utils/generate_downloadpage.sh` still generates the `specter-static` website's download page off `utils/templates/`. Clone `specter-static` alongside `specter-desktop` and run:
```bash
export GH_TOKEN=YOURTOKEN
./utils/generate_downloadpage.sh
```
- You need Docker running
- Checkout the master branch and ensure a clean workspace.
The script installs the markdown prerequisite, regenerates the GH-page and download page, asks whether to replace/update the GitHub release page for the latest version, and offers to commit/push the static-site changes.
Now, you can run
## Troubleshooting
```bash
./utils/release.sh --release-notes
```
If something fails mid-pipeline, re-running individual jobs is safe — they `actions/download-artifact` from prior jobs and overwrite existing release assets via `softprops/action-gh-release`. If the draft release already has assets from a stale run, delete the draft and re-run `create-release`.
Or, if you want to directly set the new version:
```bash
./utils/release.sh --new-version v1.13.1 --release-notes
```
## Creating a new tag
Update your master branch after the release notes PR ([example](https://github.com/cryptoadvance/specter-desktop/commit/65ff6959d7fd85cba745e4d454b30031839f857f/)) has been merged and then run:
```bash
git tag v1.13.1 && git push upstream v1.13.1
```
if you have a proper setup of github- AND gitlab forks (and the remote `origin` on git pointing to your github fork), you can push that tag to origin and this is useful to test the procedures. See "CI/CD-dev-env setup" in [continuous-integration](./continuous-integration.md).
```bash
git tag v1.13.1 && git push origin v1.13.1
```
## GitLab - releasing stage
Creating a tag triggers the release process of the GitLab runners.
There exists a mirror of the GitHub repo on GitLab, but only when a tag is created on GitHub will the release part of the runners execute. You can check the status here:
https://gitlab.com/cryptoadvance/specter-desktop/-/pipelines
There are three stages:
![](./images/release-guide/overview-gitlab-pipline.png)
The first relevant stage is "releasing". Here, the Windows, Linux and pip release are created and uploaded to the Specter Desktop GitHub releases page. After this stage, the following artificats should be available:
- cryptoadvance.specter-1.13.1.tar.gz
- Specter-Setup-v1.13.1.exe
- specterd-v1.13.1-win64.zip
- specterd-v1.13.1-x86_64-linux-gnu.zip
- specter_desktop-v1.13.1-x86_64-linux-gnu.tar.gz
The three jobs in more detail:
- release_binary_windows: is creating a binary for specterd and for Windows (Windows runner)
- release_electron_linux_windows: Creates a specterd for Linux, an AppImage for Linux and an executable for Windows (Linux runner).
- release_pip: Is releasing a pypi package on [pypi](https://pypi.org/project/cryptoadvance.specter/) and creates a tarball of the pip package for the GitHub release page (Linux runner).
For details look at `.gitlab-ci.yml`
## MacOS
Ideally, directly after the tag is created, start with the MacOS release. As the binaries of x86/arm64 are not compatible with each other, we need to build on two MacOS architectures.This has to be done manually, for now. There is a script for this. Start with the build on x86:
### MacOS x64 build
```bash
./utils/build-osx.sh --version v2.0.5-pre4 specterd package upload
```
You can also test this procedure without messing the original project via changing the `orgName` to your `orgName` in `pyinstaller/electron/downloadloc.js`.
This will create three artifacts on github:
* specterd-v2.0.5-pre4-osx_x64.zip
* SHA256SUMS-macos_x64
* SHA256SUMS-macos_x64.asc
### MacOS arm64 build
The electron application will get built on the arm architecture. As it needs to store the sha256 hash in the electron-app, the make-hash target
will not only hash the specterd but also download the other specterd and hash it.
```bash
./utils/build-osx.sh --version v2.0.5-pre4 --appleid "Satoshi Nakamoto (appleid)" --mail "satoshi@gmx.com" specterd make-hash electron sign package upload
```
This will create four artifacts on github:
* Specter-v2.0.5-pre4.dmg
* specterd-v2.0.5-pre4-osx_arm64.zip
* SHA256SUMS-macos_arm64
* SHA256SUMS-macos_arm64.asc
## GitLab - post releasing
Back to GitLab, the final stage is "post releasing".
### release_signatures
In this job, the individual SHA256-hashes and signatures are combined into two final files:
- SHA256SUMS
- SHA256SUMS.asc
Everything, apart from the MacOS files, are pulled from the GitLab environment, the MacOS files from GitHub.
Don't forget to delete the four MacOS files (`SHA256SUMS-macos_arm64` and `SHA256SUMS-macos_arm64.asc` and the two corresponding `_x64` files) on the GitHub release page in the end.
This is difficult to automate as sometimes the manual steps has not succeeded while generating the SHASUM-files. As a result, those hashes are not included. So you might want to run this again. And you can, just delete the two generated files - `SHA256SUMS` and `SHA256SUMS.asc` and run the job again.
### release_docker
There are docker images created by the awesome [Chiang Mai LN dev](https://github.com/lncm/docker-specter-desktop). So the task of this job is to trigger their build-system which is done via `utils/trigger_docker_build.sh`. A prerequisite of this is a token in order to authenticate. That token is from Aaron, one of the maintainers of that repo, and can be found in the gitlab variables section of the CI/CD configuration.
### tag_specterext_dummy_repo
Sometimes there are changes on the plugin architecture. In order to create a plugin, it's quite important to know which version of the plugin system should be used. Because of that, we simply assume that the master of the [specterext-dummy](https://github.com/cryptoadvance/specterext-dummy) repo is compatible with the current master which was just tagged with the new version.
So this job will tag that repo with the same tag and the creation of a plugin will take the version into account.
## Trouble shooting
If the MacOS signatures are missing, it can happen that the following Exception will be raised:
```bash
File "/builds/cryptoadvance/specter-desktop/utils/github.py", line 295, in download_artifact
raise Exception(
Exception: Status-cod04 for url ... )
```
In any case, if the macOS binaries arrive on GitHub too late, you have to manually delete the already created `SHA256SUMS` and `SHA256SUMS.asc`, otherwise the upload to GitHub will fail if you rerun the release signatures job on GitLab - for details see ([this PR](https://github.com/cryptoadvance/specter-desktop/pull/689)). The green arrow in the screenshot is where you rerun the release signatures job on GitLab:
![](./images/release-guide/rerun-release-signatures.png)
## GitHub release page and download page
This is handled by the script `./utils/generate_downloadpage.sh`. As a prerequisite, you need to clone the `specter-static` repo which contains the specter website. Clone it on the same level than specter-desktop.
Running that script will:
- install the prerequisites (basically markdown, see pyproject.toml)
- generate the GH-page and the download-page based on the `utils/templates`.
- Asks whether it should replace/update/initialize the Github Release page for the latest version
- copies over the new download-pages and asks whether it should commit/push those
macOS builds are the most likely to fail due to Apple signing/notarization glitches. The workflow falls back to unsigned builds when `APPLE_CERTIFICATE_BASE64` is empty — useful for smoke-testing the pipeline on forks.

7952
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -10,14 +10,22 @@
"dependencies": {
"gulp-livereload": "^4.0.2",
"rimraf": "^3.0.2",
"wait-on": "^5.3.0"
"wait-on": "^9.0.5"
},
"devDependencies": {
"cypress": "^9.7.0",
"cypress": "^13.17.0",
"cypress-wait-until": "^1.7.1",
"gulp": "^4.0.2",
"gulp": "^5.0.1",
"tailwindcss": "^3.2.4"
},
"overrides": {
"@cypress/request": {
"form-data": "^2.5.4"
}
},
"resolutions": {
"**/@cypress/request/form-data": "^2.5.4"
},
"scripts": {
"dev": "gulp",
"tailwind": "npx tailwindcss -i ./src/cryptoadvance/specter/static/style.css -o ./src/cryptoadvance/specter/static/output.css --watch",

View file

@ -1,14 +1,7 @@
# Build scripts
Run `build-<your-os> <version_number>` file to build everything.
For example, `build-osx.sh 1.2.3` will create `SpecterDesktop-1.2.3.dmg` and `specterd-1.2.3-osx.zip` in the `release` folder.
If you're making a real release, you should append `"make hash"` at the end of your command calling the build script.
This will update the file hash and version name the Specter Desktop app expects to download from GitHub.
# Pyinstaller build
Releases are built by `.github/workflows/release.yml` (triggered by a version tag). The notes below are for local / manual builds.
Install requirements:
```bash
@ -52,17 +45,9 @@ If this is the first time you go through this process, you'll need to first set
xcrun altool --store-password-in-keychain-item "AC_PASSWORD" -u "<your-apple-id>" -p "<the-generated-password>"
```
After having these set up, you can use the automated script to sign by passing it 2 extra parameters:
- Your certificate name, which you can see on the Keychain app going to the sidebar -> `My Certificates` and copying the name of the certificate you've created in step 1.
- Your Apple ID.
Release builds sign and notarize via `.github/workflows/release.yml` (`build-electron-macos` job) using the `APPLE_CERTIFICATE_BASE64`, `APPLE_CERTIFICATE_PASSWORD`, `APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, and `APPLE_TEAM_ID` secrets. See `docs/release-guide.md` for the full secret inventory. For manual local signing, use `electron-builder` directly (`npm run dist -- --mac` in `pyinstaller/electron/`, with the identity configured in `package.json`).
With these two, you can run the command like so:
```bash
./build-osx.sh <version_number> "<certificate_name>" "<apple_id>" "make-hash"
```
*Note: "make-hash" is optional and will automatically calculate hash of specterd generated for the macOS app. Should be used only for real release.*
This should take 10 minutes, during which you should receive an email from Apple notifying whatever the notarization was successful.
Notarization takes ~10 minutes, during which Apple emails notification of success/failure.
If for some reason the notarization failed, you'll be able to get the reason by copying the `Request Identifier` (you should be able to find this in the email and in the logs).
Then run the following command:
```bash

View file

@ -1,67 +0,0 @@
#!/usr/bin/env bash
set -e
# pass version number as an argument
echo " --> This build got triggered for version $1"
echo " --> Assumed gitlab-project: ${CI_PROJECT_ROOT_NAMESPACE:+x}"
[ -z "${CI_PROJECT_ROOT_NAMESPACE:+x}" ] && \
echo "Redefining CI_PROJECT_ROOT_NAMESPACE=cryptoadvance " && \
export CI_PROJECT_ROOT_NAMESPACE=cryptoadvance
echo $1 > version.txt
echo " --> Installing (build)-requirements"
pip3 install -r requirements.txt --require-hashes
cd ..
python3 setup.py install
pip3 install -e .
cd pyinstaller
echo " --> Cleaning up"
rm -rf build/ dist/ release/ electron/release/ electron/dist release-linux/ release-win/
echo " --> Building specterd"
pyinstaller specterd.spec
echo " --> Making us ready for building electron-app for linux"
cd electron
npm ci
node ./set-version $1 ../dist/specterd
echo " --> building electron-app"
npm i
npm run dist -- --linux
cd ..
echo " --> Making the release-zip"
mkdir release-linux
cd dist
cp -r ../../udev ./udev
echo "Don't forget to set up udev rules! Check out udev folder for instructions." > README.md
zip -r ../release-linux/specterd-"$1"-"$(uname -m)"-linux-gnu.zip specterd udev README.md
cp ../electron/dist/Specter-* ./
tar -czvf ../release-linux/specter_desktop-"$1"-"$(uname -m)"-linux-gnu.tar.gz Specter-* udev README.md
echo " --> Cleaning up"
cd ..
rm -rf dist
mkdir dist
cd dist
echo " --> Downloading the windows-version of specterd for version $1"
wget --progress=dot -e dotbytes=10M https://github.com/${CI_PROJECT_ROOT_NAMESPACE}/specter-desktop/releases/download/$1/specterd-$1-win64.zip -O ./specterd.zip
unzip specterd.zip
cd ../electron
rm -rf dist/
echo " --> Making us ready for building electron-app for windows"
npm ci
node ./set-version $1 ../dist/specterd.exe
npm run dist -- --win
cd ..
mkdir release-win
cp electron/dist/Specter\ Setup\ *.exe release-win/Specter-Setup-$1.exe

View file

@ -1,42 +0,0 @@
@ECHO OFF
python -V
pip3 install virtualenv
echo " --> cleaning up"
rmdir /s /q .\release\
rmdir /s /q .\dist
rmdir /s /q .buildenv
echo " --> Creating virtualenv"
virtualenv --python=python3 .buildenv
echo " --> Activating virtualenv"
call .\.buildenv\Scripts\activate
echo " --> Installing test-requirement"
pip3 install -e ".[test]"
echo " --> Building pypi package"
pip3 install build==0.10.0
python -m build
echo " --> Installing pypi package"
python .\utils\release_helper.py install_wheel %1%
cd pyinstaller
Rem This file gets further packaged up with the pyinstaller and will help specter to figure out which version it's running on
echo %1% > version.txt
echo " --> installing pyinstaller requirements"
pip3 install -r requirements.txt --require-hashes
rmdir /s /q .\dist\
rmdir /s /q .\build\
rmdir /s /q .\release\
rmdir /s /q .\electron\dist\
echo " --> Creating the pyinstaller binary"
pyinstaller.exe specterd.spec
mkdir release
echo " --> Creating the release-package"
powershell Compress-Archive -Path dist\specterd.exe release\specterd-%1%-win64.zip

View file

@ -1,31 +0,0 @@
@ECHO OFF
echo %1% > version.txt
pip3 install -r requirements.txt --require-hashes
cd ..
Rem Order is relevant here. If you flip the followng lines, the hiddenimports for services won't work anymore
python3 setup.py install
pip3 install -e .
cd pyinstaller
rmdir /s /q .\dist\
rmdir /s /q .\build\
rmdir /s /q .\release\
rmdir /s /q .\electron\dist\
pyinstaller.exe specterd.spec
cd electron
call npm ci
if "%2%"=="make-hash" (
call node ./set-version "%1%" "../dist/specterd.exe"
) else (
node ./set-version "%1%"
)
call npm i
call npm run dist
cd ..
mkdir release
SET EXE_PATH="electron\dist\Specter Setup *.exe"
SET EXE_RELEASE_PATH="release\Specter Setup %1%.exe"
echo f | xcopy /s/y %EXE_PATH% %EXE_RELEASE_PATH%
powershell Compress-Archive -Path dist\specterd.exe release\specterd-%1%-win64.zip

File diff suppressed because it is too large Load diff

View file

@ -20,8 +20,8 @@
"author": "Specter",
"license": "MIT",
"devDependencies": {
"electron": "^39.8.5",
"electron-builder": "^24.13.3"
"electron": "^39.8.10",
"electron-builder": "^26.8.1"
},
"build": {
"productName": "Specter",
@ -60,5 +60,13 @@
"read-last-lines": "^1.8.0",
"request": "^2.88.2",
"winston": "^3.13.0"
},
"overrides": {
"request": {
"form-data": "^2.5.4"
}
},
"resolutions": {
"**/request/form-data": "^2.5.4"
}
}

File diff suppressed because it is too large Load diff

View file

@ -7,6 +7,7 @@
pyinstaller==6.11.1
pyinstaller-hooks-contrib>=2024.0
setuptools>=78.1.1
backports.tarfile==1.2.0
# Platform helpers
altgraph>=0.17

View file

@ -26,7 +26,7 @@ readme = "README.md"
license = {file = "LICENSE"}
requires-python = ">=3.7,<4.0"
requires-python = ">=3.9,<3.13"
dynamic = ["dependencies", "version"]
@ -47,12 +47,13 @@ write_to = "src/cryptoadvance/specter/_version.py"
norecursedirs = "tests/bitcoin* tests/elements* tests/xtestdata_testextensions"
log_format = "[%(levelname)8s] %(message)s %(name)s (%(filename)s:%(lineno)s)"
addopts = "--bitcoind-version v22.0.0 --elementsd-version v0.21.0.2"
addopts = "--bitcoind-version v27.2.0 --elementsd-version v0.21.0.2"
markers = [
"slow: mark test as slow.",
"elm: mark test as elementsd dependent",
"bottleneck: mark a test as so ressource intensive that it can create a bottleneck where the test just fails due to a lack of ressources",
"threading: test needs threading to work"
"threading: test needs threading to work",
"jade_hardware: requires a real Jade attached and an operator; opt-in only via --run-jade-hardware"
]
filterwarnings = [
@ -75,18 +76,17 @@ dependencies = {file = ["requirements.in"]}
[project.optional-dependencies]
test = [
"black==22.3.0",
"black==26.3.1",
"pre-commit==2.13.0",
"pip-tools==6.13",
"pytest==7.1.2",
"PySocks==1.7.1",
"pytest-cov==2.10.1",
"mock==4.0.2",
"python-gitlab==2.10.1",
# requirements for stuff in ./utils
"requests==2.31.0",
"requests==2.33.0",
]
gendownloadpage = [
"markdown==3.3.7"
"markdown==3.8.1"
]

View file

@ -1,30 +1,30 @@
certifi==2024.6.2
certifi==2024.7.4
Click==8.1.7
Flask==2.2.5
Flask-Babel==3.1.0
Flask-Cors==3.0.10
Flask-Cors==6.0.0
Flask-Login==0.6.3
Flask-RESTful==0.3.10
Flask-HTTPAuth==4.8.0
hwi==2.4.0
python-dotenv==0.21.1
requests==2.31.0
hwi==3.1.0
python-dotenv==1.2.2
requests==2.33.0
pysocks==1.7.1
six==1.16.0
stem==1.8.0
embit==0.6.1
psutil==5.9.0
pyopenssl==24.1.0
pyopenssl==26.0.0
flask_wtf==1.2.1
pgpy==0.6.0
cbor2==5.4.6
cbor2==5.9.0
mnemonic==0.21
cryptography==42.0.7
cryptography==46.0.6
Flask-APScheduler==1.12.4
gunicorn==23.0.0
simple-websocket==0.8.1
protobuf==4.23.3
PyJWT==2.4.0
PyJWT==2.13.0
pytimeparse==1.1.8
psycopg2-binary==2.9.5
aioitertools==0.11.0
@ -33,9 +33,9 @@ aioitertools==0.11.0
cryptoadvance-liquidissuer==0.2.4
specterext-exfund==0.1.7
specterext-faucet==0.1.2
cryptoadvance.spectrum==0.7.0
cryptoadvance.spectrum==0.8.0
specterext-stacktrack==0.3.0
specterext-timelockrecovery==0.2.1
specterext-timelockrecovery==0.2.3
# workarounds

View file

@ -20,105 +20,145 @@ babel==2.15.0 \
--hash=sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb \
--hash=sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413
# via flask-babel
cbor2==5.4.6 \
--hash=sha256:0b956f19e93ba3180c336282cd1b6665631f2d3a196a9c19b29a833bf979e7a4 \
--hash=sha256:0bd12c54a48949d11f5ffc2fa27f5df1b4754111f5207453e5fae3512ebb3cab \
--hash=sha256:0d2b926b024d3a1549b819bc82fdc387062bbd977b0299dd5fa5e0ea3267b98b \
--hash=sha256:1618d16e310f7ffed141762b0ff5d8bb6b53ad449406115cc465bf04213cefcf \
--hash=sha256:181ac494091d1f9c5bb373cd85514ce1eb967a8cf3ec298e8dfa8878aa823956 \
--hash=sha256:1835536e76ea16e88c934aac5e369ba9f93d495b01e5fa2d93f0b4986b89146d \
--hash=sha256:1c12c0ab78f5bc290b08a79152a8621822415836a86f8f4b50dadba371736fda \
--hash=sha256:24144822f8d2b0156f4cda9427f071f969c18683ffed39663dc86bc0a75ae4dd \
--hash=sha256:309fffbb7f561d67f02095d4b9657b73c9220558701c997e9bfcfbca2696e927 \
--hash=sha256:3316f09a77af85e7772ecfdd693b0f450678a60b1aee641bac319289757e3fa0 \
--hash=sha256:3545b16f9f0d5f34d4c99052829c3726020a07be34c99c250d0df87418f02954 \
--hash=sha256:39452c799453f5bf33281ffc0752c620b8bfa0b7c13070b87d370257a1311976 \
--hash=sha256:3950be57a1698086cf26d8710b4e5a637b65133c5b1f9eec23967d4089d8cfed \
--hash=sha256:456cdff668a50a52fdb8aa6d0742511e43ed46d6a5b463dba80a5a720fa0d320 \
--hash=sha256:4b9f3924da0e460a93b3674c7e71020dd6c9e9f17400a34e52a88c0af2dcd2aa \
--hash=sha256:4bbbdb2e3ef274865dc3f279aae109b5d94f4654aea3c72c479fb37e4a1e7ed7 \
--hash=sha256:4ce1a2c272ba8523a55ea2f1d66e3464e89fa0e37c9a3d786a919fe64e68dbd7 \
--hash=sha256:56dfa030cd3d67e5b6701d3067923f2f61536a8ffb1b45be14775d1e866b59ae \
--hash=sha256:6709d97695205cd08255363b54afa035306d5302b7b5e38308c8ff5a47e60f2a \
--hash=sha256:6e1b5aee920b6a2f737aa12e2b54de3826b09f885a7ce402db84216343368140 \
--hash=sha256:6f9c702bee2954fffdfa3de95a5af1a6b1c5f155e39490353d5654d83bb05bb9 \
--hash=sha256:78304df140b9e13b93bcbb2aecee64c9aaa9f1cadbd45f043b5e7b93cc2f21a2 \
--hash=sha256:79e048e623846d60d735bb350263e8fdd36cb6195d7f1a2b57eacd573d9c0b33 \
--hash=sha256:7bbd3470eb685325398023e335be896b74f61b014896604ed45049a7b7b6d8ac \
--hash=sha256:80ac8ba450c7a41c5afe5f7e503d3092442ed75393e1de162b0bf0d97edf7c7f \
--hash=sha256:9394ca49ecdf0957924e45d09a4026482d184a465a047f60c4044eb464c43de9 \
--hash=sha256:94f844d0e232aca061a86dd6ff191e47ba0389ddd34acb784ad9a41594dc99a4 \
--hash=sha256:96087fa5336ebfc94465c0768cd5de0fcf9af3840d2cf0ce32f5767855f1a293 \
--hash=sha256:b893500db0fe033e570c3adc956af6eefc57e280026bd2d86fd53da9f1e594d7 \
--hash=sha256:c285a2cb2c04004bfead93df89d92a0cef1874ad337d0cb5ea53c2c31e97bfdb \
--hash=sha256:d2984a488f350aee1d54fa9cb8c6a3c1f1f5b268abbc91161e47185de4d829f3 \
--hash=sha256:d54bd840b4fe34f097b8665fc0692c7dd175349e53976be6c5de4433b970daa4 \
--hash=sha256:db9eb582fce972f0fa429d8159b7891ff8deccb7affc4995090afc61ce0d328a \
--hash=sha256:e5094562dfe3e5583202b93ef7ca5082c2ba5571accb2c4412d27b7d0ba8a563 \
--hash=sha256:e73ca40dd3c7210ff776acff9869ddc9ff67bae7c425b58e5715dcf55275163f \
--hash=sha256:ff95b33e5482313a74648ca3620c9328e9f30ecfa034df040b828e476597d352
cbor2==5.9.0 \
--hash=sha256:0322296b9d52f55880e300ba8ba09ecf644303b99b51138bbb1c0fb644fa7c3e \
--hash=sha256:0485d3372fc832c5e16d4eb45fa1a20fc53e806e6c29a1d2b0d3e176cedd52b9 \
--hash=sha256:08388ea54195738602b4c4999966bcaef6f0b17d293c9658658409d9fff96f57 \
--hash=sha256:1d02b65f070fd726bdc310d927228975bb655d155bf059b6eb7cacefb3dca86f \
--hash=sha256:1da96ce5d852fe3d342c1eb2c202a52d1c97edfddc9230f1be7e02674662bf26 \
--hash=sha256:1f223dffb1bcdd2764665f04c1152943d9daa4bc124a576cd8dee1cad4264313 \
--hash=sha256:23606d31ba1368bd1b6602e3020ee88fe9523ca80e8630faf6b2fc904fd84560 \
--hash=sha256:2372d357d403e7912f104ff085950ffc82a5854d6d717f1ca1ce16a40a0ef5a7 \
--hash=sha256:25bec7beb2089465382b1be72e78667fe9090598800826559c3e3008cf0db743 \
--hash=sha256:27695cbd70c90b8de5c4a284642c2836449b14e2c2e07e3ffe0744cb7669a01b \
--hash=sha256:2a54fbb32cb828c214f7f333a707e4aec61182e7efdc06ea5d9596d3ecee624a \
--hash=sha256:3095dc49e75572841a9534cbfdabc2a17487ea4ee33341436abc4a7ac7245a3a \
--hash=sha256:34a6cb15e6ab6a8eae94ad2041731cd3ef786af43a8df99f847969af5b902ee7 \
--hash=sha256:380e534482b843e43442b87d8777a7bf9bed20cb7526f89b780c3400f617304b \
--hash=sha256:420d2490c7836c81151b4bd591c35cffc55391e33e7e333c50fda391bcea7d31 \
--hash=sha256:422817286c1d0ce947fb2f7eca9212b39bddd7231e8b452e2d2cc52f15332dba \
--hash=sha256:4753a6d1bc71054d9179557bc65740860f185095ccb401d46637fff028a5b3ec \
--hash=sha256:4aa07b392cc3d76fb31c08a46a226b58c320d1c172ff3073e864409ced7bc50f \
--hash=sha256:4cd43d8fc374b31643b2830910f28177a606a7bc84975a62675dd3f2e320fc7b \
--hash=sha256:5326336f633cc89dfe543c78829c16c3a6449c2c03277d1ddba99086c3323363 \
--hash=sha256:53cfa49e0df9c639beb871d480de098eedc81eb63ff29f2dc922720d7577b676 \
--hash=sha256:55bea0dd9a7d354e35f4e5fe58ceab393e76962713749dc3a0a64a0e5d19545e \
--hash=sha256:5e702b02d42a5ace45425b595ffe70fe35aebaf9a3cdfdc2c758b6189c744422 \
--hash=sha256:65f8eac3268c608533f326f0fd9010ab1b2a8a917b05edaf3853116336821669 \
--hash=sha256:7221483fad0c63afa4244624d552abf89d7dfdbc5f5edfc56fc1ff2b4b818975 \
--hash=sha256:7d1ddc4541e7367ac58c2470cc0df847f7137167fe4f5729e2d3cc0b993d7da4 \
--hash=sha256:837754ece9052b3f607047e1741e5f852a538aa2b0ee3db11c82a8fa11804aa4 \
--hash=sha256:85c7a46279ac8f226e1059275221e6b3d0e370d2bb6bd0500f9780781615bcea \
--hash=sha256:86baf870d4c0bfc6f79de3801f3860a84ab76d9c8b0abb7f081f2c14c38d79d3 \
--hash=sha256:971d425b3a23b75953d8853d5f9911bdeefa09d759ee3b5e6b07b5ff3cbd9073 \
--hash=sha256:9a4907e0c3035bb8836116854ed8e56d8aef23909d601fa59706320897ec2551 \
--hash=sha256:a9d6e4e0f988b0e766509a8071975a8ee99f930e14a524620bf38083106158d2 \
--hash=sha256:ac684fe195c39821fca70d18afbf748f728aefbfbf88456018d299e559b8cae0 \
--hash=sha256:ae6c706ac1d85a0b3cb3395308fd0c4d55e3202b4760773675957e93cdff45fc \
--hash=sha256:cc5efec69055c3c470997935d95762be7e4bfd1248d88fb1a33bb7e0f45712e9 \
--hash=sha256:d1a21c006760f95acd9509cc5a7d15d6fc82e58f721f94fa9039b4e77189a6e5 \
--hash=sha256:d8524a8c142c3cc228e635f8a97499a6c0b18ca91382e8276565658035cdcb6d \
--hash=sha256:dcf0f695873e5c94bd072d6af8698e72b8fb7f7a18f37e0bced1041b7111a6cf \
--hash=sha256:f29e5c3abcc91c1aeefecde0e057bf33f1655588d3065c6560c30ceb3be6f333 \
--hash=sha256:f797532d13469f2193e5c16e827d8df7a8c33674b19be755790b54ab231e6a73 \
--hash=sha256:f7c9751a9611601ab326d8f5837f01379195bbf06175fb4effeb552140e7c9e8 \
--hash=sha256:fb7afe77f8d269e42d7c4b515c6fd14f1ccc0625379fb6829b269f493d16eddd \
--hash=sha256:fbb06f34aa645b4deca66643bba3d400d20c15312d1fe88d429be60c1ab50f27 \
--hash=sha256:fbdcf4d74acbeb7672e6413e81cd2c1ced1a4a8cf949484ac54e9af5265c3c72
# via
# -r requirements.in
# hwi
certifi==2024.6.2 \
--hash=sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516 \
--hash=sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56
certifi==2024.7.4 \
--hash=sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b \
--hash=sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90
# via
# -r requirements.in
# requests
cffi==1.16.0 \
--hash=sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc \
--hash=sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a \
--hash=sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417 \
--hash=sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab \
--hash=sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520 \
--hash=sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36 \
--hash=sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743 \
--hash=sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8 \
--hash=sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed \
--hash=sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684 \
--hash=sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56 \
--hash=sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324 \
--hash=sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d \
--hash=sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235 \
--hash=sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e \
--hash=sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088 \
--hash=sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000 \
--hash=sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7 \
--hash=sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e \
--hash=sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673 \
--hash=sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c \
--hash=sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe \
--hash=sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2 \
--hash=sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098 \
--hash=sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8 \
--hash=sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a \
--hash=sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0 \
--hash=sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b \
--hash=sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896 \
--hash=sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e \
--hash=sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9 \
--hash=sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2 \
--hash=sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b \
--hash=sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6 \
--hash=sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404 \
--hash=sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f \
--hash=sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0 \
--hash=sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4 \
--hash=sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc \
--hash=sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936 \
--hash=sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba \
--hash=sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872 \
--hash=sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb \
--hash=sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614 \
--hash=sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1 \
--hash=sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d \
--hash=sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969 \
--hash=sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b \
--hash=sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4 \
--hash=sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627 \
--hash=sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956 \
--hash=sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357
cffi==2.0.0 \
--hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \
--hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \
--hash=sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f \
--hash=sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9 \
--hash=sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44 \
--hash=sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2 \
--hash=sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c \
--hash=sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75 \
--hash=sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65 \
--hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \
--hash=sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a \
--hash=sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e \
--hash=sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25 \
--hash=sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a \
--hash=sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe \
--hash=sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b \
--hash=sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91 \
--hash=sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592 \
--hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \
--hash=sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c \
--hash=sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1 \
--hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \
--hash=sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba \
--hash=sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb \
--hash=sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165 \
--hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \
--hash=sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca \
--hash=sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c \
--hash=sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6 \
--hash=sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c \
--hash=sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0 \
--hash=sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743 \
--hash=sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63 \
--hash=sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5 \
--hash=sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5 \
--hash=sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4 \
--hash=sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d \
--hash=sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b \
--hash=sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93 \
--hash=sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205 \
--hash=sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27 \
--hash=sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512 \
--hash=sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d \
--hash=sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c \
--hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \
--hash=sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26 \
--hash=sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322 \
--hash=sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb \
--hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \
--hash=sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8 \
--hash=sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4 \
--hash=sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414 \
--hash=sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9 \
--hash=sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664 \
--hash=sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9 \
--hash=sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775 \
--hash=sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739 \
--hash=sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc \
--hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \
--hash=sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe \
--hash=sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9 \
--hash=sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92 \
--hash=sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5 \
--hash=sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13 \
--hash=sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d \
--hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 \
--hash=sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f \
--hash=sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495 \
--hash=sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b \
--hash=sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6 \
--hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c \
--hash=sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef \
--hash=sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5 \
--hash=sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18 \
--hash=sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad \
--hash=sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3 \
--hash=sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7 \
--hash=sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5 \
--hash=sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534 \
--hash=sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49 \
--hash=sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2 \
--hash=sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5 \
--hash=sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453 \
--hash=sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf
# via cryptography
charset-normalizer==2.0.12 \
--hash=sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597 \
@ -134,51 +174,68 @@ cryptoadvance-liquidissuer==0.2.4 \
--hash=sha256:5a2c531801854c5a4a46daf184877e22f731cdb42d2cfb840785bda7371ba6fb \
--hash=sha256:9e468f3e35ecc566b3f74a2263677cf26632548abb194521dba15ad37acd1e9b
# via -r requirements.in
cryptoadvance-spectrum==0.7.0 \
--hash=sha256:40b31d38ad40c85438bc44d9243edb9ddbaf331c4468a48ebc0f40a109b43a62 \
--hash=sha256:6cd8858fa07668536345ecab08b4143e28055d7524c9b27b8c885c5b98730879
cryptoadvance-spectrum==0.8.0 \
--hash=sha256:cc91eef05f6417898e735b793b91135ddd989fe223ff51aeaa4bc278eb15c3ed \
--hash=sha256:d3a53b21ca13d8e6a014ff2c0b0e68f8d4eef2c1efbe6383ae018c15409c67c1
# via -r requirements.in
cryptography==42.0.7 \
--hash=sha256:02c0eee2d7133bdbbc5e24441258d5d2244beb31da5ed19fbb80315f4bbbff55 \
--hash=sha256:0d563795db98b4cd57742a78a288cdbdc9daedac29f2239793071fe114f13785 \
--hash=sha256:16268d46086bb8ad5bf0a2b5544d8a9ed87a0e33f5e77dd3c3301e63d941a83b \
--hash=sha256:1a58839984d9cb34c855197043eaae2c187d930ca6d644612843b4fe8513c886 \
--hash=sha256:2954fccea107026512b15afb4aa664a5640cd0af630e2ee3962f2602693f0c82 \
--hash=sha256:2e47577f9b18723fa294b0ea9a17d5e53a227867a0a4904a1a076d1646d45ca1 \
--hash=sha256:31adb7d06fe4383226c3e963471f6837742889b3c4caa55aac20ad951bc8ffda \
--hash=sha256:3577d029bc3f4827dd5bf8bf7710cac13527b470bbf1820a3f394adb38ed7d5f \
--hash=sha256:36017400817987670037fbb0324d71489b6ead6231c9604f8fc1f7d008087c68 \
--hash=sha256:362e7197754c231797ec45ee081f3088a27a47c6c01eff2ac83f60f85a50fe60 \
--hash=sha256:3de9a45d3b2b7d8088c3fbf1ed4395dfeff79d07842217b38df14ef09ce1d8d7 \
--hash=sha256:4f698edacf9c9e0371112792558d2f705b5645076cc0aaae02f816a0171770fd \
--hash=sha256:5482e789294854c28237bba77c4c83be698be740e31a3ae5e879ee5444166582 \
--hash=sha256:5e44507bf8d14b36b8389b226665d597bc0f18ea035d75b4e53c7b1ea84583cc \
--hash=sha256:779245e13b9a6638df14641d029add5dc17edbef6ec915688f3acb9e720a5858 \
--hash=sha256:789caea816c6704f63f6241a519bfa347f72fbd67ba28d04636b7c6b7da94b0b \
--hash=sha256:7f8b25fa616d8b846aef64b15c606bb0828dbc35faf90566eb139aa9cff67af2 \
--hash=sha256:8cb8ce7c3347fcf9446f201dc30e2d5a3c898d009126010cbd1f443f28b52678 \
--hash=sha256:93a3209f6bb2b33e725ed08ee0991b92976dfdcf4e8b38646540674fc7508e13 \
--hash=sha256:a3a5ac8b56fe37f3125e5b72b61dcde43283e5370827f5233893d461b7360cd4 \
--hash=sha256:a47787a5e3649008a1102d3df55424e86606c9bae6fb77ac59afe06d234605f8 \
--hash=sha256:a79165431551042cc9d1d90e6145d5d0d3ab0f2d66326c201d9b0e7f5bf43604 \
--hash=sha256:a987f840718078212fdf4504d0fd4c6effe34a7e4740378e59d47696e8dfb477 \
--hash=sha256:a9bc127cdc4ecf87a5ea22a2556cab6c7eda2923f84e4f3cc588e8470ce4e42e \
--hash=sha256:bd13b5e9b543532453de08bcdc3cc7cebec6f9883e886fd20a92f26940fd3e7a \
--hash=sha256:c65f96dad14f8528a447414125e1fc8feb2ad5a272b8f68477abbcc1ea7d94b9 \
--hash=sha256:d8e3098721b84392ee45af2dd554c947c32cc52f862b6a3ae982dbb90f577f14 \
--hash=sha256:e6b79d0adb01aae87e8a44c2b64bc3f3fe59515280e00fb6d57a7267a2583cda \
--hash=sha256:e6b8f1881dac458c34778d0a424ae5769de30544fc678eac51c1c8bb2183e9da \
--hash=sha256:e9b2a6309f14c0497f348d08a065d52f3020656f675819fc405fb63bbcd26562 \
--hash=sha256:ecbfbc00bf55888edda9868a4cf927205de8499e7fabe6c050322298382953f2 \
--hash=sha256:efd0bf5205240182e0f13bcaea41be4fdf5c22c5129fc7ced4a0282ac86998c9
cryptography==46.0.6 \
--hash=sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70 \
--hash=sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d \
--hash=sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a \
--hash=sha256:12f0fa16cc247b13c43d56d7b35287ff1569b5b1f4c5e87e92cc4fcc00cd10c0 \
--hash=sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97 \
--hash=sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30 \
--hash=sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759 \
--hash=sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c \
--hash=sha256:2ea0f37e9a9cf0df2952893ad145fd9627d326a59daec9b0802480fa3bcd2ead \
--hash=sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275 \
--hash=sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58 \
--hash=sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f \
--hash=sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361 \
--hash=sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507 \
--hash=sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa \
--hash=sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b \
--hash=sha256:50575a76e2951fe7dbd1f56d181f8c5ceeeb075e9ff88e7ad997d2f42af06e7b \
--hash=sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8 \
--hash=sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8 \
--hash=sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72 \
--hash=sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175 \
--hash=sha256:6728c49e3b2c180ef26f8e9f0a883a2c585638db64cf265b49c9ba10652d430e \
--hash=sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124 \
--hash=sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a \
--hash=sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c \
--hash=sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f \
--hash=sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d \
--hash=sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4 \
--hash=sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c \
--hash=sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290 \
--hash=sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca \
--hash=sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d \
--hash=sha256:90e5f0a7b3be5f40c3a0a0eafb32c681d8d2c181fc2a1bdabe9b3f611d9f6b1a \
--hash=sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed \
--hash=sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a \
--hash=sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb \
--hash=sha256:a3e84d5ec9ba01f8fd03802b2147ba77f0c8f2617b2aff254cedd551844209c8 \
--hash=sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707 \
--hash=sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410 \
--hash=sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736 \
--hash=sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2 \
--hash=sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4 \
--hash=sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013 \
--hash=sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19 \
--hash=sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b \
--hash=sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738 \
--hash=sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463 \
--hash=sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77 \
--hash=sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4
# via
# -r requirements.in
# noiseprotocol
# pgpy
# pyopenssl
ecdsa==0.19.0 \
--hash=sha256:2cea9b88407fdac7bbeca0833b189e4c9c53f2ef1e1eaa29f6224dbc809b707a \
--hash=sha256:60eaad1199659900dd0af521ed462b793bbdf867432b3948e87416ae4caf6bf8
ecdsa==0.19.2 \
--hash=sha256:62635b0ac1ca2e027f82122b5b81cb706edc38cd91c63dda28e4f3455a2bf930 \
--hash=sha256:840f5dc5e375c68f36c1a7a5b9caad28f95daa65185c9253c0c08dd952bb7399
# via hwi
embit==0.6.1 \
--hash=sha256:16a84c6668dc9ffc907594457a46f7142cee379646bc009a5a9b77b0d2cb4e12
@ -206,9 +263,9 @@ flask-babel==3.1.0 \
--hash=sha256:be015772c5d7f046f3b99c508dcf618636eb93d21b713b356db79f3e79f69f39 \
--hash=sha256:deb3ee272d5adf97f5974ed09ab501243d63e7fb4a047501a00de4bd4aca4830
# via -r requirements.in
flask-cors==3.0.10 \
--hash=sha256:74efc975af1194fc7891ff5cd85b0f7478be4f7f59fe158102e91abb72bb4438 \
--hash=sha256:b60839393f3b84a0f3746f6cdca56c1ad7426aa738b70d6c61375857823181de
flask-cors==6.0.0 \
--hash=sha256:4592c1570246bf7beee96b74bc0adbbfcb1b0318f6ba05c412e8909eceec3393 \
--hash=sha256:6332073356452343a8ccddbfec7befdc3fdd040141fe776ec9b94c262f058657
# via -r requirements.in
flask-httpauth==4.8.0 \
--hash=sha256:66568a05bc73942c65f1e2201ae746295816dc009edd84b482c44c758d75097a \
@ -295,14 +352,16 @@ greenlet==2.0.2 \
--hash=sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19 \
--hash=sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1 \
--hash=sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526
# via -r requirements.in
# via
# -r requirements.in
# sqlalchemy
gunicorn==23.0.0 \
--hash=sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d \
--hash=sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec
# via -r requirements.in
h11==0.14.0 \
--hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \
--hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761
h11==0.16.0 \
--hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
--hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
# via wsproto
hidapi==0.14.0 \
--hash=sha256:01929fbbe206ebcb0bad9b8e925e16de0aa8f872bf80a263f599e519866d9900 \
@ -375,13 +434,13 @@ hidapi==0.14.0 \
--hash=sha256:fb4e94e45f6dddb20d59501187721e5d3b02e6cc8a59d261dd5cac739008582a \
--hash=sha256:fc9ec2321bf3b0b4953910aa87c0c8ab5f93b1f113a9d3d4f18845ce54708d13
# via hwi
hwi==2.4.0 \
--hash=sha256:3eaa7593f1ab360569eacdd9507dab75532bb58e8cd991d8ad72f5c4fcb67997 \
--hash=sha256:7cb7ef2a4db4bc434815374d9bad43c6425491f77828314a2d2898d3e86d3f04
hwi==3.1.0 \
--hash=sha256:21ba92bb06e2f805e2806c686f2c50d02db6826a363b01e44052415755504d6f \
--hash=sha256:42e875cbb616a91638fb90679cad93edb5075bf375e92fc1709be9b2a3dfd59c
# via -r requirements.in
idna==3.7 \
--hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \
--hash=sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0
idna==3.15 \
--hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8 \
--hash=sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc
# via requests
itsdangerous==2.2.0 \
--hash=sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef \
@ -389,9 +448,9 @@ itsdangerous==2.2.0 \
# via
# flask
# flask-wtf
jinja2==3.1.4 \
--hash=sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369 \
--hash=sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d
jinja2==3.1.6 \
--hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \
--hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67
# via
# flask
# flask-babel
@ -514,6 +573,10 @@ numpy==1.26.4 \
--hash=sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3 \
--hash=sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f
# via pandas
packaging==26.0 \
--hash=sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4 \
--hash=sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529
# via gunicorn
pandas==2.2.2 \
--hash=sha256:001910ad31abc7bf06f49dcc903755d2f7f3a9186c0c040b827e522e9cef0863 \
--hash=sha256:0ca6377b8fca51815f382bd0b697a0814c8bda55115678cbc94c30aacbb6eff2 \
@ -569,10 +632,6 @@ protobuf==4.23.3 \
# via
# -r requirements.in
# hwi
packaging==26.0 \
--hash=sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529 \
--hash=sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4
# via gunicorn
psutil==5.9.0 \
--hash=sha256:072664401ae6e7c1bfb878c65d7282d4b4391f1bc9a56d5e03b5a490403271b5 \
--hash=sha256:1070a9b287846a21a5d572d6dddd369517510b68710fca56b0e9e02fd24bed9a \
@ -685,21 +744,21 @@ psycopg2-binary==2.9.5 \
pyaes==1.6.1 \
--hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f
# via hwi
pyasn1==0.6.0 \
--hash=sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c \
--hash=sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473
pyasn1==0.6.4 \
--hash=sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81 \
--hash=sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b
# via pgpy
pycparser==2.22 \
--hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \
--hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc
# via cffi
pyjwt==2.4.0 \
--hash=sha256:72d1d253f32dbd4f5c88eaf1fdc62f3a19f676ccbadb9dbc5d07e951b2b26daf \
--hash=sha256:d42908208c699b3b973cbeb01a969ba6a96c821eefb1c5bfe4c390c01d67abba
pyjwt==2.13.0 \
--hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \
--hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728
# via -r requirements.in
pyopenssl==24.1.0 \
--hash=sha256:17ed5be5936449c5418d1cd269a1a9e9081bc54c17aed272b45856a3d3dc86ad \
--hash=sha256:cabed4bfaa5df9f1a16c0ef64a0cb65318b5cd077a7eda7d6970131ca2f41a6f
pyopenssl==26.0.0 \
--hash=sha256:df94d28498848b98cc1c0ffb8ef1e71e40210d3b0a8064c9d29571ed2904bf81 \
--hash=sha256:f293934e52936f2e3413b89c6ce36df66a0b34ae1ea3a053b8c5020ff2f513fc
# via -r requirements.in
pyserial==3.5 \
--hash=sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb \
@ -718,9 +777,9 @@ python-dateutil==2.9.0.post0 \
# via
# flask-apscheduler
# pandas
python-dotenv==0.21.1 \
--hash=sha256:1c93de8f636cde3ce377292818d0e440b6e45a82f215c3744979151fa8151c49 \
--hash=sha256:41e12e0318bebc859fcc4d97d4db8d20ad21721a6aa5047dd59f090391cb549a
python-dotenv==1.2.2 \
--hash=sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a \
--hash=sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3
# via -r requirements.in
pytimeparse==1.1.8 \
--hash=sha256:04b7be6cc8bd9f5647a6325444926c3ac34ee6bc7e69da4367ba282f076036bd \
@ -734,9 +793,9 @@ pytz==2024.1 \
# flask-babel
# flask-restful
# pandas
requests==2.31.0 \
--hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 \
--hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f
requests==2.33.0 \
--hash=sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b \
--hash=sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652
# via
# -r requirements.in
# cryptoadvance-spectrum
@ -755,7 +814,6 @@ six==1.16.0 \
# -r requirements.in
# apscheduler
# ecdsa
# flask-cors
# flask-restful
# python-dateutil
specterext-exfund==0.1.7 \
@ -768,9 +826,9 @@ specterext-stacktrack==0.3.0 \
--hash=sha256:14f96f1f552f57ba017b8bc642f07343edbb1abafe09e03bbaae179d78d7ce23 \
--hash=sha256:9e2946185730aab377951e83a27d8791a34e0f031e44f15991212b6b85722ca0
# via -r requirements.in
specterext-timelockrecovery==0.2.1 \
--hash=sha256:2fc2a3eec045377b54926ff41dab9c5f5a6f46074150aec1ff67cb1456fef911 \
--hash=sha256:35e181971b5b69ca6706496169c4581f2fa20706c88ba24760e56e153ec51939
specterext-timelockrecovery==0.2.3 \
--hash=sha256:5df3deb6245a22d48f75bbb62fd87dc141a9f60407011ce9fbb0a7bcc40fe4cc \
--hash=sha256:a0b22c4e010061055da4ea6ea4e08bbc38d1f7c2ec58327970caa0ce987cba95
# via -r requirements.in
sqlalchemy==1.4.52 \
--hash=sha256:1296f2cdd6db09b98ceb3c93025f0da4835303b8ac46c15c2136e27ee4d18d94 \
@ -829,10 +887,14 @@ tenacity==8.3.0 \
--hash=sha256:3649f6443dbc0d9b01b9d8020a9c4ec7a1ff5f6f3c6c8a036ef371f573fe9185 \
--hash=sha256:953d4e6ad24357bceffbc9707bc74349aca9d245f68eb65419cf0c249a1949a2
# via plotly
typing-extensions==4.11.0 \
--hash=sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0 \
--hash=sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a
# via hwi
typing-extensions==4.15.0 \
--hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \
--hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548
# via
# cryptography
# hwi
# pyjwt
# pyopenssl
tzdata==2024.1 \
--hash=sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd \
--hash=sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252
@ -841,15 +903,16 @@ tzlocal==5.2 \
--hash=sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8 \
--hash=sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e
# via apscheduler
urllib3==1.26.18 \
--hash=sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07 \
--hash=sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0
urllib3==2.7.0 \
--hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \
--hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897
# via requests
werkzeug==3.0.3 \
--hash=sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18 \
--hash=sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8
# via
# flask
# flask-cors
# flask-login
wsproto==1.2.0 \
--hash=sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065 \
@ -861,5 +924,6 @@ wtforms==3.1.2 \
# via flask-wtf
# WARNING: The following packages were not pinned, but pip requires them to be
# pinned when the requirements file includes hashes. Consider using the --allow-unsafe flag.
# pinned when the requirements file includes hashes and the requirement is not
# satisfied by a package already installed. Consider using the --allow-unsafe flag.
# setuptools

View file

@ -40,11 +40,21 @@ def verify_token(jwt_token):
return abort(401)
try:
payload = jwt.decode(jwt_token, app.config["SECRET_KEY"], algorithms=["HS256"])
username = payload["username"]
the_user = app.specter.user_manager.get_user_by_username(username)
if not the_user:
username = payload.get("username")
jwt_token_id = payload.get("jwt_token_id")
if (
not isinstance(username, str)
or not username
or not isinstance(jwt_token_id, str)
or not jwt_token_id
):
return abort(401)
g.user = app.specter.user_manager.get_user_by_username(username)
the_user = app.specter.user_manager.get_user_by_username(username)
if not the_user or not the_user.verify_jwt_token_id_and_jwt_token(
jwt_token_id, jwt_token
):
return abort(401)
g.user = the_user
logger.info({"payload": payload})
logger.info(f"Rest-Request for user {username} PASSED JWT-test")
return username

View file

@ -179,7 +179,14 @@ def server(
print(
" * Running in HWI Bridge mode.\n"
" * You can configure access to the API "
"at: %s://%s:%d/hwi/settings" % ("http", host, app.config["PORT"])
"at: %s://%s:%d%s%s/hwi/settings"
% (
"http",
host,
app.config["PORT"],
app.config["APP_URL_PREFIX"],
app.config["SPECTER_URL_PREFIX"],
)
)
# debug is false by default

View file

@ -65,13 +65,17 @@ class BitcoinCore(Device):
def taproot_available(self, rpc):
try:
# currently only master branch supports tr() descriptors
# TODO: replace to 220000
core_version = rpc.getnetworkinfo().get("version", 0)
info = rpc.getblockchaininfo()
taproot_active = (core_version >= 219900) and (
info.get("softforks", {}).get("taproot", {}).get("active", False)
)
if core_version >= 240000:
# Core 24 moved softfork status from getblockchaininfo.softforks
# to the getdeploymentinfo RPC's deployments field.
deployments = rpc.getdeploymentinfo().get("deployments", {})
taproot_active = deployments.get("taproot", {}).get("active", False)
else:
softforks = rpc.getblockchaininfo().get("softforks", {})
taproot_active = (core_version >= 219900) and softforks.get(
"taproot", {}
).get("active", False)
taproot_support = self.use_descriptors(rpc) and taproot_active
self.taproot_support = taproot_support
return taproot_support

View file

@ -1,7 +1,16 @@
import json, os, random, requests
from flask import Blueprint, Flask, jsonify, url_for, redirect, render_template, request
from flask import (
Blueprint,
Flask,
jsonify,
redirect,
render_template,
request,
url_for,
)
from .server_endpoints import flash
from flask import current_app as app
from flask_login import current_user, login_required
from flask_cors import CORS
from .hwi_rpc import HWIBridge
from .helpers import deep_update, hwi_get_config, save_hwi_bridge_config
@ -10,13 +19,14 @@ import logging
logger = logging.getLogger(__name__)
hwi_server = Blueprint("hwi_server", __name__)
hwi_server_settings = Blueprint("hwi_server_settings", __name__)
CORS(hwi_server)
rand = random.randint(0, int(1e32)) # to force style refresh
@hwi_server.route("/", methods=["GET"])
def index():
return redirect(url_for("hwi_server.hwi_bridge_settings"))
return redirect(url_for("hwi_server_settings.hwi_bridge_settings"))
@hwi_server.route("/api/", methods=["POST"])
@ -100,8 +110,15 @@ def api():
return jsonify(app.specter.hwi.jsonrpc(data))
@hwi_server.route("/settings/", methods=["GET", "POST"])
@hwi_server_settings.route("/settings/", methods=["GET", "POST"])
@login_required
def hwi_bridge_settings():
if app.config.get("LOGIN_DISABLED") and (
not current_user.is_authenticated or not current_user.is_admin
):
app.login("admin")
if not current_user.is_admin:
return "Forbidden", 403
config = hwi_get_config(app.specter)
if request.method == "POST":
action = request.form["action"]

View file

@ -464,7 +464,7 @@ class ExtensionManager:
site_package = Path(virtuelenv_path, *(Path(site_package).parts[-3:-1]))
virtualenv_search_path = site_package
# ... and as the classes are in the .buildenv (see build-unix.sh) let's add ..
# ... and as the classes are in the .buildenv let's add ..
arr = [Path(virtualenv_search_path, path) for path in arr]
# Non internal-repo extensions sitting in org/specterext/... need to be added, too

View file

@ -23,7 +23,7 @@ from werkzeug.wrappers import Response
from cryptoadvance.specter.hwi_rpc import HWIBridge
from .htmlsafebabel import HTMLSafeBabel
from .hwi_server import hwi_server
from .hwi_server import hwi_server, hwi_server_settings
from .services.callbacks import after_serverpy_init_app, specter_added_to_flask_app
from .specter import Specter
from .util.specter_migrator import SpecterMigrator
@ -229,8 +229,16 @@ def init_app(app: SpecterFlask, hwibridge=False, specter=None):
app.logger.info("Login enabled")
app.config["LOGIN_DISABLED"] = False
app.logger.info("Initializing Controller ...")
hwi_settings_prefix = f"{app.config['SPECTER_URL_PREFIX']}/hwi"
app.register_blueprint(hwi_server_settings, url_prefix=hwi_settings_prefix)
app.register_blueprint(hwi_server, url_prefix="/hwi")
csrf.exempt(hwi_server)
csrf.exempt(app.view_functions["hwi_server.api"])
if hwi_settings_prefix != "/hwi":
app.add_url_rule(
"/hwi/settings/",
"hwi_server.hwi_bridge_settings",
lambda: redirect(url_for("hwi_server_settings.hwi_bridge_settings")),
)
if not hwibridge:
with app.app_context():
from cryptoadvance.specter.server_endpoints import controller
@ -258,10 +266,17 @@ def init_app(app: SpecterFlask, hwibridge=False, specter=None):
importlib.reload(controller)
importlib.reload(serviceController)
else:
with app.app_context():
from cryptoadvance.specter.server_endpoints.auth import auth_endpoint
app.register_blueprint(
auth_endpoint,
url_prefix=f"{app.config['SPECTER_URL_PREFIX']}/auth",
)
@app.route("/", methods=["GET"])
def index():
return redirect(url_for("hwi_server.hwi_bridge_settings"))
return redirect(url_for("hwi_server_settings.hwi_bridge_settings"))
if app.config["SPECTER_API_ACTIVE"]:
app.logger.info("Initializing REST ...")

View file

@ -152,7 +152,7 @@ class ExtGen:
shutil.copy(sourcepath, targetpath)
print(f" --> Created {targetpath} (copied)")
else:
r = requests.get(self.env.loader.url_for_template(sourcepath))
r = requests.get(self.env.loader.url_for_template(sourcepath), timeout=30)
open(targetpath, "wb").write(r.content)
print(f" --> Created {targetpath} (via Github)")
@ -233,7 +233,7 @@ class GithubUrlLoader(BaseLoader):
def get_source(self, environment, template):
url = self.url_for_template(template)
for attempt in range(3):
r = requests.get(url)
r = requests.get(url, timeout=30)
if r.status_code == 200:
return r.text, url, None
if r.status_code == 429 and attempt < 2:

View file

@ -1100,6 +1100,10 @@ input[type="number"]::-webkit-outer-spin-button,
z-index: 50;
}
.z-20 {
z-index: 20;
}
.z-30 {
z-index: 30;
}

View file

@ -30,6 +30,16 @@
</div>
{% endif %}
<div id="coldcard-warning" class="flex p-4 mt-3 mb-4 bg-orange-100 text-orange-700 rounded-lg {% if device_class.device_type != 'coldcard' %}hidden{% endif %}" role="alert">
<img class="flex-shrink-0 w-6 h-6 mr-3" src="{{ url_for('static', filename='img/warning_sign.svg') }}" aria-hidden="true">
<span class="sr-only">{{ _("Security warning") }}</span>
<div>
<span class="font-medium">{{ _("Security warning: ColdCard seed entropy advisory") }}</span><br>
{{ _("Coinkite disclosed a firmware flaw that reduced generated-seed entropy on ColdCard Mk2/Mk3 (firmware 4.0.1-4.1.9) and Mk4/Mk5/Q before their fixed firmware. If your seed was generated on-device with fewer than 50 independent dice rolls, update firmware, generate a new seed and move your funds.") }}
<a class="text-orange-700 underline" href="https://blog.coinkite.com/coldcard-mk3-seed-generation-warning/" target="_blank" rel="noopener noreferrer">{{ _("Read Coinkite's advisory") }}</a>
</div>
</div>
<div id="coldcard-instructions" class="{%if device_class.device_type != 'coldcard' %}hidden{% endif %}">
<p>{{ _("Connect your ColdCard to the computer via USB and unlock it or upload a wallet export file from micro SD card.") }}</p>

View file

@ -49,7 +49,7 @@
<p class="mt-3 mb-0 selection-button cursor-pointer" data-style="text-decoration: underline; cursor: pointer;" onclick="showPageOverlay('usb_import_account_number')">{{ _("Specify account number (advanced)") }}</p>
<div class="flex space-x-3 mt-3">
<div class="flex space-x-3 mt-3 relative z-20">
<qr-scanner id="xpub-scan">
<a slot="button" href="#" class="button mb-3">
<img src="{{ url_for('static', filename='img/qr-code.svg') }}" data-style="width: 26px; margin-right: 2px;" class="svg-white"> {{ _("Scan QR Code") }}

View file

@ -22,6 +22,14 @@
</div>
{% endif %}
<div class="flex p-4 mt-3 mb-4 bg-dark-600 rounded-lg" role="alert">
<svg class="flex-shrink-0 inline w-6 h-6 mr-3" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><!--Generated by IJSVG (https://github.com/iconjar/IJSVG)--><path d="M32.0022,55.9844l-1.04907e-06,-2.84217e-14c-13.2548,-5.79387e-07 -24,-10.7452 -24,-24c5.79387e-07,-13.2548 10.7452,-24 24,-24c13.2548,5.79387e-07 24,10.7452 24,24l2.13163e-14,-1.04907e-06c0,13.2548 -10.7452,24 -24,24Zm1.5,-32l3.82016e-08,-2.71086e-09c-1.79133,0.127116 -3.35165,-1.21031 -3.5,-3l6.50646e-08,-7.8524e-07c0.148296,-1.78972 1.70866,-3.12717 3.5,-3l-5.85322e-09,4.15529e-10c1.79134,-0.127171 3.3517,1.21028 3.5,3l-5.57104e-08,6.72365e-07c-0.148295,1.78976 -1.70872,3.12723 -3.5001,3Zm1.4206,6.7635l-2.7853,10.5093l-8.37179e-08,3.15667e-07c-0.270677,1.02062 0.262417,2.08281 1.2425,2.4757l-5.91015e-08,-2.36708e-08c0.379505,0.151996 0.628277,0.519688 0.6282,0.9285v0.323l8.52651e-14,4.05e-07c0,0.551933 -0.447167,0.999503 -0.9991,1h-2.9891l-4.04477e-08,1.96948e-10c-1.65938,0.00807986 -3.01111,-1.33056 -3.01919,-2.98993c-0.00127082,-0.260991 0.0314695,-0.521035 0.0973943,-0.773566l2.7852,-10.5094l2.79671e-08,-1.05468e-07c0.270652,-1.02067 -0.262537,-2.08289 -1.2427,-2.4757l-2.20542e-08,-8.83296e-09c-0.379505,-0.151996 -0.628276,-0.519688 -0.6282,-0.9285v-0.3229l-1.77636e-14,-9.40042e-08c-8.33514e-08,-0.552011 0.447289,-0.999614 0.9993,-1h2.9887l1.06497e-07,-5.28143e-10c1.65943,-0.00822943 3.01133,1.33033 3.01956,2.98976c0.00129458,0.261047 -0.0314356,0.52115 -0.0973639,0.773738Z" fill="currentColor" fill-rule="evenodd"></path></svg>
<span class="sr-only">Info</span>
<div>
{{ _("Specter authentication protects access to the app, but it does not encrypt the whole Specter data folder at rest. Wallet metadata such as addresses, labels, and xpubs may be readable from files on disk. Use OS disk encryption or an encrypted volume for the Specter data folder if other users, malware, backup tools, or forensic access are in your threat model.") }}
</div>
</div>
<div id="hasencryptedservicedata" class="flex p-4 mt-3 mb-4 bg-dark-600 rounded-lg" role="alert">
<svg class="flex-shrink-0 inline w-6 h-6 mr-3" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><!--Generated by IJSVG (https://github.com/iconjar/IJSVG)--><path d="M32.0022,55.9844l-1.04907e-06,-2.84217e-14c-13.2548,-5.79387e-07 -24,-10.7452 -24,-24c5.79387e-07,-13.2548 10.7452,-24 24,-24c13.2548,5.79387e-07 24,10.7452 24,24l2.13163e-14,-1.04907e-06c0,13.2548 -10.7452,24 -24,24Zm1.5,-32l3.82016e-08,-2.71086e-09c-1.79133,0.127116 -3.35165,-1.21031 -3.5,-3l6.50646e-08,-7.8524e-07c0.148296,-1.78972 1.70866,-3.12717 3.5,-3l-5.85322e-09,4.15529e-10c1.79134,-0.127171 3.3517,1.21028 3.5,3l-5.57104e-08,6.72365e-07c-0.148295,1.78976 -1.70872,3.12723 -3.5001,3Zm1.4206,6.7635l-2.7853,10.5093l-8.37179e-08,3.15667e-07c-0.270677,1.02062 0.262417,2.08281 1.2425,2.4757l-5.91015e-08,-2.36708e-08c0.379505,0.151996 0.628277,0.519688 0.6282,0.9285v0.323l8.52651e-14,4.05e-07c0,0.551933 -0.447167,0.999503 -0.9991,1h-2.9891l-4.04477e-08,1.96948e-10c-1.65938,0.00807986 -3.01111,-1.33056 -3.01919,-2.98993c-0.00127082,-0.260991 0.0314695,-0.521035 0.0973943,-0.773566l2.7852,-10.5094l2.79671e-08,-1.05468e-07c0.270652,-1.02067 -0.262537,-2.08289 -1.2427,-2.4757l-2.20542e-08,-8.83296e-09c-0.379505,-0.151996 -0.628276,-0.519688 -0.6282,-0.9285v-0.3229l-1.77636e-14,-9.40042e-08c-8.33514e-08,-0.552011 0.447289,-0.999614 0.9993,-1h2.9887l1.06497e-07,-5.28143e-10c1.65943,-0.00822943 3.01133,1.33033 3.01956,2.98976c0.00129458,0.261047 -0.0314356,0.52115 -0.0973639,0.773738Z" fill="currentColor" fill-rule="evenodd"></path></svg>
<span class="sr-only">Info</span>

View file

@ -463,10 +463,10 @@ class User(UserMixin):
def verify_jwt_token_id_and_jwt_token(self, jwt_token_id, jwt_token):
# Verifying the JWT token ID and JWT token
if jwt_token_id in self.jwt_tokens:
if self.jwt_tokens[jwt_token_id]["jwt_token"] == jwt_token:
return True
return False
if not isinstance(self.jwt_tokens, dict) or not isinstance(jwt_token_id, str):
return False
token_info = self.jwt_tokens.get(jwt_token_id)
return isinstance(token_info, dict) and token_info.get("jwt_token") == jwt_token
def get_jwt_token(self, jwt_token_id):
# Getting a JWT token from the hashmap by ID

View file

@ -5,11 +5,11 @@ import os
from pathlib import Path
import pkgutil
from pkgutil import iter_modules
import re
import sys
from typing import List
from .common import camelcase2snake_case
from ..specter_error import SpecterError, SpecterInternalException
from .shell import grep
from .reflection_fs import detect_extension_style_in_cwd, search_dirs_in_path
@ -130,6 +130,29 @@ def get_classlist_of_type_clazz_from_modulelist(clazz, modulelist, skip_missing=
return class_list
def is_specter_desktop_project(cwd=".") -> bool:
"""Whether cwd is the specter-desktop project itself rather than an
extension-project. Detected via the project-name in the pyproject.toml.
Hmm, a bit hackish but we don't want to depend on toml-parsing libs.
"""
try:
with open(Path(cwd, "pyproject.toml")) as pyproject_file:
for line in pyproject_file:
line = line.strip().replace(" ", "").replace("'", "").replace('"', "")
if not line.startswith("name="):
continue
# PEP 503: ".", "-" and "_" are equivalent in project-names, so
# "cryptoadvance.specter" and "cryptoadvance_specter" are the same
name = re.sub(r"[-_.]+", "-", line[len("name=") :]).lower()
if name == "cryptoadvance-specter":
return True
except FileNotFoundError:
# Expected for adhoc-style extension-projects: those have no
# pyproject.toml at all and are therefore not specter-desktop
pass
return False
def get_subclasses_for_clazz_in_cwd(clazz, cwd=".") -> List[type]:
"""Returns all subclasses of class clazz located in the CWD if the cwd
is not a specter-desktop dev-env-kind-of-dir or contains any .py-file
@ -140,37 +163,26 @@ def get_subclasses_for_clazz_in_cwd(clazz, cwd=".") -> List[type]:
return []
# if not testing but in a folder which looks like specter-desktop/src --> No dynamic extensions
if "PYTEST_CURRENT_TEST" not in os.environ:
# Hmm, a bit hackish but if the pyproject.toml specifies cryptoadvance.specter as a name and
# we don't need to depend on toml-parsing libs, that should be ok.
try:
found, line = grep("./pyproject.toml", 'name = "cryptoadvance.specter"')
if found:
return []
if line:
line = line.replace(" ", "").replace("'", "").replace('"', "")
if line == "name=cryptoadvance.specter":
return []
except FileNotFoundError:
pass
if "PYTEST_CURRENT_TEST" not in os.environ and is_specter_desktop_project(cwd):
return []
# Depending on the style we either add "." or "./src" to the searchpath
# Depending on the style we either add cwd or cwd/src to the searchpath
extension_style = detect_extension_style_in_cwd()
extension_style = detect_extension_style_in_cwd(cwd)
# raise Exception(extension_style)
if extension_style == "adhoc":
package_dirs.append(Path("."))
package_dirs.append(Path(cwd))
elif extension_style == "publish-ready":
package_dirs.extend(search_dirs_in_path(Path("./src")))
package_dirs.extend(search_dirs_in_path(Path(cwd, "src")))
elif extension_style == "specter-desktop":
if "PYTEST_CURRENT_TEST" in os.environ:
# I admit, ugly hack
logger.info("We're in testing mode. Adding CWD to searchpath")
package_dirs.append(Path("./src"))
package_dirs.append(Path(cwd, "src"))
else:
raise Exception(
f"""
We checked before that we're not in the specter-desktop home
We checked before that we're not in the specter-desktop home
directory but now the extension-style is 'specter-desktop' ?!
This should not happen!
"""

View file

@ -354,6 +354,40 @@ class Wallet(AbstractWallet):
res = wallet_rpc.importdescriptors(args)
else:
res = wallet_rpc.importmulti(args, {"rescan": False})
if not all([r["success"] for r in res]):
# Core refuses to shrink an existing keypool range on
# (re-)import: "new range must include current range".
# This can happen when re-creating a wallet under a name
# that Core already has a wider range recorded for (e.g.
# after a keypoolrefill). Parse the range Core is telling
# us about and retry once with a range wide enough to
# include it (see #2604).
widened_range = None
for r in res:
if r["success"]:
continue
match = re.search(
r"current range = \[(\d+),\s*(\d+)\]",
r.get("error", {}).get("message", ""),
)
if not match:
continue
current_start, current_end = int(match.group(1)), int(
match.group(2)
)
start = min(0, current_start)
end = max(cls.GAP_LIMIT, current_end)
if widened_range is None:
widened_range = [start, end]
else:
widened_range = [
min(widened_range[0], start),
max(widened_range[1], end),
]
if widened_range is not None:
for arg in args:
arg["range"] = widened_range
res = wallet_rpc.importmulti(args, {"rescan": False})
if not all([r["success"] for r in res]):
all_issues = " and ".join(

View file

@ -137,12 +137,25 @@ class SwanClient:
).decode()
auth_header["Authorization"] = f"Basic {auth_hash}"
response = requests.post(
f"{self.api_url}/oidc/token",
data=payload,
headers=auth_header,
)
resp = json.loads(response.text)
try:
response = requests.post(
f"{self.api_url}/oidc/token",
data=payload,
headers=auth_header,
timeout=30,
)
except requests.exceptions.RequestException as e:
logger.exception(e)
raise SwanApiException(
f"Could not reach the Swan API ({self.api_url}/oidc/token): {e}"
) from e
try:
resp = json.loads(response.text)
except ValueError as e:
logger.error(f"{response.status_code}: {response.text}")
raise SwanApiException(
f"Swan API returned no valid json ({response.status_code}): {response.text}"
) from e
"""
{
"access_token": "***************",
@ -184,27 +197,43 @@ class SwanClient:
"User-Agent": "Specter Desktop",
"Authorization": f"Bearer {access_token}",
}
request_context = f"endpoint: {self.api_url}{endpoint} | method: {method} | payload: {json.dumps(json_payload, indent=4)}"
try:
if method == "GET":
response = requests.get(self.api_url + endpoint, headers=auth_header)
response = requests.get(
self.api_url + endpoint, headers=auth_header, timeout=30
)
elif method in ["POST", "PATCH", "PUT", "DELETE"]:
response = requests.request(
method=method,
url=self.api_url + endpoint,
headers=auth_header,
json=json_payload,
timeout=30,
)
if response.status_code != 200:
raise SwanApiException(f"{response.status_code}: {response.text}")
return response.json()
except Exception as e:
# TODO: tighten up expected Exceptions
else:
raise SwanApiException(f"Unsupported method: {method}")
except requests.exceptions.RequestException as e:
# Timeouts, connection errors, ... : no response to report about
logger.exception(e)
logger.error(
f"endpoint: {self.api_url}{endpoint} | method: {method} | payload: {json.dumps(json_payload, indent=4)}"
)
logger.error(request_context)
raise SwanApiException(f"Could not reach the Swan API: {e}") from e
if response.status_code != 200:
logger.error(request_context)
logger.error(f"{response.status_code}: {response.text}")
raise e
raise SwanApiException(f"{response.status_code}: {response.text}")
try:
return response.json()
except ValueError as e:
logger.exception(e)
logger.error(request_context)
logger.error(f"{response.status_code}: {response.text}")
raise SwanApiException(
f"Swan API returned no valid json ({response.status_code}): {response.text}"
) from e
def get_autowithdrawal_addresses(self, swan_wallet_id: str) -> dict:
"""

View file

@ -1,8 +1,8 @@
# Trusted reference: verbatim plaintext of https://bitcoincore.org/bin/bitcoin-core-22.0/SHA256SUMS
# Trusted reference: verbatim plaintext of https://bitcoincore.org/bin/bitcoin-core-27.2/SHA256SUMS
# Provenance:
# Source URL: https://bitcoincore.org/bin/bitcoin-core-22.0/SHA256SUMS
# Signature URL: https://bitcoincore.org/bin/bitcoin-core-22.0/SHA256SUMS.asc
# Verified on: 2026-04-15
# Source URL: https://bitcoincore.org/bin/bitcoin-core-27.2/SHA256SUMS
# Signature URL: https://bitcoincore.org/bin/bitcoin-core-27.2/SHA256SUMS.asc
# Verified on: 2026-04-19
# Verified by: Good signatures from:
# - fanquake (primary fingerprint E777299FC265DD04793070EB944D35F9AC3DB76A,
# signing subkey CFB16E21C950F67FA95E558F2EEB9F5CC09526C1)
@ -10,26 +10,30 @@
# Builder keys sourced from: https://github.com/bitcoin-core/guix.sigs/tree/main/builder-keys
# Do not edit the hash lines below — they are the on-disk trust anchor compared
# against downloaded tarballs by tests/install_noded.sh::verify_binary.
9547fa03574f8bde296f707c7d9f7d89827c75c5a28f84402578a4fa92a787ec bitcoin-22.0-aarch64-linux-gnu-debug.tar.gz
ac718fed08570a81b3587587872ad85a25173afa5f9fbbd0c03ba4d1714cfa3e bitcoin-22.0-aarch64-linux-gnu.tar.gz
80071e0ecd24edfec8a1972b495b9822c79a5d33c7123bff51688638aac97cab bitcoin-22.0-arm-linux-gnueabihf-debug.tar.gz
b8713c6c5f03f5258b54e9f436e2ed6d85449aa24c2c9972f91963d413e86311 bitcoin-22.0-arm-linux-gnueabihf.tar.gz
8f70852feb39078e02182563517d17bdfc4a12904cf1bdabbae95594d9a1e473 bitcoin-22.0-codesignatures-22.0.tar.gz
d0e9d089b57048b1555efa7cd5a63a7ed042482045f6f33402b1df425bf9613b bitcoin-22.0.tar.gz
bfc04a3c4e8b613bfd9359e54da6cc60f027860e9723f9a6bfd6f13873eb811f bitcoin-22.0-powerpc64-linux-gnu-debug.tar.gz
2cca5f99007d060aca9d8c7cbd035dfe2f040dd8200b210ce32cdf858479f70d bitcoin-22.0-powerpc64-linux-gnu.tar.gz
5f0bf1491bc8825ca1506f7cf586030f06bb17a563ccde92e8c75720022704e6 bitcoin-22.0-powerpc64le-linux-gnu-debug.tar.gz
91b1e012975c5a363b5b5fcc81b5b7495e86ff703ec8262d4b9afcfec633c30d bitcoin-22.0-powerpc64le-linux-gnu.tar.gz
59b16e63aa935f50fd2813efe7f137187fcf0fff84e3205a9c6cb462a8bb160c bitcoin-22.0-riscv64-linux-gnu-debug.tar.gz
9cc3a62c469fe57e11485fdd32c916f10ce7a2899299855a2e479256ff49ff3c bitcoin-22.0-riscv64-linux-gnu.tar.gz
3b3e2680f7d9304c13bfebaf6445ada40d72324b4b3e0a07de9db807389a6c5b bitcoin-22.0-osx-signed.dmg
52449aa894a6ce5653315e1260d0ce87c1d9f490afe3c92b44285710804b11ae bitcoin-22.0-osx-unsigned.dmg
f51156774c24c0ac5cc30237fa08aa17ed04a180dfd72c3e7d20fdc3f45806dc bitcoin-22.0-osx-unsigned.tar.gz
2744d199c3343b2d94faffdfb2c94d75a630ba27301a70e47b0ad30a7e0155e9 bitcoin-22.0-osx64.tar.gz
3a4f05657c048d3e9505bdb9c4fb3658e5e3d4233b0b93c1853e080620589765 bitcoin-22.0-x86_64-linux-gnu-debug.tar.gz
59ebd25dd82a51638b7a6bb914586201e67db67b919b2a1ff08925a7936d1b16 bitcoin-22.0-x86_64-linux-gnu.tar.gz
9169989d649937c0f9ebccd3ab088501328aa319fe9e91fc7ea8e8cf0fcccede bitcoin-22.0-win64-setup.exe
f890473d6d910d478f8ff08f9356d0305d19b46cf06e4fc3b5a49b0b684fd2a7 bitcoin-22.0-win-unsigned.tar.gz
0a97ebc8ae44913e3ef9c5b1ddd2af3a4ffb0ba25b6ab1ee8173e40e60499402 bitcoin-22.0-win64-debug.zip
ecc579d006230d6ffc5a5b7b53ce8c76477d37c1c7bad69694e9c2d69f00331d bitcoin-22.0-win64-setup-unsigned.exe
9485e4b52ed6cebfe474ab4d7d0c1be6d0bb879ba7246a8239326b2230a77eb1 bitcoin-22.0-win64.zip
eb0c8518ad2e96d01208f589ecd827b5a951be6dfcac418d4b807842a6ebdf65 bitcoin-27.2-aarch64-linux-gnu-debug.tar.gz
154c9b9e6e17136edc8f20fda5d252fb339e727e4a85ef49e7d8facb9085f2d3 bitcoin-27.2-aarch64-linux-gnu.tar.gz
4a05d46dcb74d337f62b78b884df080534677897fd9e1a0ae41253547789cef8 bitcoin-27.2-arm-linux-gnueabihf-debug.tar.gz
fb00da13525b52a4aad091c6521f94a79879e5ca0956c2302289301f71e6303c bitcoin-27.2-arm-linux-gnueabihf.tar.gz
3a4795d591c0e506e88ce227b17048d347be37611ba98837a71a16c3a5f908c6 bitcoin-27.2-arm64-apple-darwin.zip
4e91003a95108f85e85f92c3af23aa6108175a1d0a0c60cb515c2d3daf8a20f4 bitcoin-27.2-arm64-apple-darwin-unsigned.tar.gz
48800ea29baca2e27f1544fc0c4d4cd096a908291c1e681bd9daa632645bce06 bitcoin-27.2-arm64-apple-darwin-unsigned.zip
8f2247f4786f3559d37189b58452c91623efc5fa6886c975fa9386f9ff3f1001 bitcoin-27.2-arm64-apple-darwin.tar.gz
33b59d57b1a3f11ca9c7993891a359fdc19e62284be3235d3951053cbbe81a60 bitcoin-27.2-codesignatures-27.2.tar.gz
5a8b0094b3c6bc7f63c7fd6e0ba2e733a3022f09c0064c96ccd6e331bd7b9f6a bitcoin-27.2.tar.gz
6a273a634a04123c48c4a4be9ddbeeba27d2acdbefecd1025cfca1f30b394759 bitcoin-27.2-powerpc64-linux-gnu-debug.tar.gz
566ed37e33ce529c75d9d0c72ff3894d0d5a311bd5f21be2de7495937348e454 bitcoin-27.2-powerpc64-linux-gnu.tar.gz
6f9824a833a0c9a2c11cabf302cf0b0f573471cf8a7f0f7ad8723db4094515d5 bitcoin-27.2-powerpc64le-linux-gnu-debug.tar.gz
efb66cc2648b3968f7186c990640589bd6fd72d93879b0cc1ff2adef9cd377b9 bitcoin-27.2-powerpc64le-linux-gnu.tar.gz
fdc90b4692a5aaf04f6626b71fe40368ec0bcf000d066b1efdab99318000a3a8 bitcoin-27.2-riscv64-linux-gnu-debug.tar.gz
0c272ec7b7bb6bdee410fb177ceaab0df0373de5aed2228ab9fb4e6128ccc793 bitcoin-27.2-riscv64-linux-gnu.tar.gz
d74feac2e7bbb7df84c8f67ca6216ef4f83eb7e9c2213a901896d7814bfafb90 bitcoin-27.2-x86_64-apple-darwin.zip
cbac742ce80dac1b57d455680b3accb8f75c20e782169185ef519b3e0534d498 bitcoin-27.2-x86_64-apple-darwin-unsigned.tar.gz
8630d96eba03ecb57066442947cc08d58367666f96d825c636015ff458f46c16 bitcoin-27.2-x86_64-apple-darwin-unsigned.zip
6ebc56ca1397615d5a6df2b5cf6727b768e3dcac320c2d5c2f321dcaabc7efa2 bitcoin-27.2-x86_64-apple-darwin.tar.gz
8eb4e52ab1b3a27640a2c1a00c526815ab8a740443f963f5e6dfc5fdb37bcc2c bitcoin-27.2-x86_64-linux-gnu-debug.tar.gz
acc223af46c178064c132b235392476f66d486453ddbd6bca6f1f8411547da78 bitcoin-27.2-x86_64-linux-gnu.tar.gz
30e9a0e4b3e1e32aceb4773615c0a9ed54443b037ef541ea3be07508377cf128 bitcoin-27.2-win64-setup.exe
723a70ca4c0f7d0cef028d6cf502f595e3684c9f30c6c3c6b88d05c26b638c35 bitcoin-27.2-win64-debug.zip
5f779dbf911e75c4ca4a57b04f972a52006ed49de3519c640fef99a596b22e0e bitcoin-27.2-win64-setup-unsigned.exe
e92e8531f50915a74790689e6143d8d712b471233c38231f60bfa40ffb820bca bitcoin-27.2-win64-unsigned.tar.gz
82e18f768aa5962b3c002d7f5d6ec9338896804f48406af4b5054c927575dbdf bitcoin-27.2-win64.zip

View file

@ -89,9 +89,24 @@ def pytest_addoption(parser):
default="master",
help="Version of elementsd (something which works with git checkout ...)",
)
parser.addoption(
"--run-jade-hardware",
action="store_true",
default=False,
help="Run tests marked jade_hardware (real Jade attached + operator).",
)
listen()
def pytest_collection_modifyitems(config, items):
if config.getoption("--run-jade-hardware"):
return
skip = pytest.mark.skip(reason="opt-in via --run-jade-hardware")
for item in items:
if item.get_closest_marker("jade_hardware") is not None:
item.add_marker(skip)
def pytest_generate_tests(metafunc):
# ToDo: use custom compiled version of bitcoind
# E.g. test again bitcoind version [currentRelease] + master-branch

1
tests/fixtures/jade_hardware.psbt vendored Normal file
View file

@ -0,0 +1 @@
cHNidP8BAHECAAAAAYz9aGwGnHSzsZXBuDlKg9hNl6KeLvo8czy6WHaPTGwGAAAAAAD/////AqyEAQAAAAAAFgAUb6AWUAo8anN+uyYOLdyni6kjRVishAEAAAAAABYAFC80qhzwClOwVaKRoDp9RfCmmItSAAAAAAABAFMCAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wFR/////wFADQMAAAAAABYAFNDEo+8J6Ze26Z45flGP4+QaEYyhAAAAAAEBH0ANAwAAAAAAFgAU0MSj7wnpl7bpnjl+UY/j5BoRjKEiBgLnqyU3tdSelwMJquBunknzbOHJ/rvUTsjg0cygtPnDGRhzxdoKVAAAgAEAAIAAAACAAAAAAAAAAAAAACICA11J7M1U0AmeQ2did8em1GJdYR2oil30m/lReneRp3elGHPF2gpUAACAAQAAgAAAAIABAAAAAAAAAAA=

BIN
tests/fixtures/jade_seedqr_abandon.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 B

15
tests/fixtures/jade_seedqr_abandon.txt vendored Normal file
View file

@ -0,0 +1,15 @@
█████████████████████████████
██ ▄▄▄▄▄ ██▄▀▀ ▀▄▀▄█ ▄▄▄▄▄ ██
██ █   █ █▄  ▀▄▀▄▀▄█ █   █ ██
██ █▄▄▄█ ██▀▄██▀▄▀▄█ █▄▄▄█ ██
██▄▄▄▄▄▄▄█ ▀ █ ▀▄▀▄█▄▄▄▄▄▄▄██
██  ▄▀ ▄▄▄▄█  █▀▄▀▄ ▄▀█▀▄▀███
██ ▀██▄▀▄▀▀▄█▀██▄▀▄▀▄▀▄▀▄▀▄██
████▀▄▄▄▄▀▀██▀ ▀█▀▄▀▄▀▄▀▄▀▄██
██▄▀█ ██▄▀██ ▄ ▀▄▀▄▀▄▀▄▀▄▀▄██
██▄██▄▄▄▄▄▀▀▀ ██▄▀ ▄▄▄ ▀▄▀▄██
██ ▄▄▄▄▄ █ ▄ ▀█▄█▀ █▄█ ▀▄▀▄██
██ █   █ █▄▄ ▀ ▀█▀▄ ▄ ▄▀▄ ███
██ █▄▄▄█ █ ▀█▄ ▀ ▀▄▀▄▀▄▀▄█▄██
██▄▄▄▄▄▄▄█▄█▄▄██▄█▄█▄█▄█▄█▄██
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀

View file

@ -317,14 +317,18 @@ function gpg_verify_sums {
fi
local imported=0
for fpr in "${keys[@]}"; do
# Try keys.openpgp.org first, then keyserver.ubuntu.com.
if curl -fsSL "https://keys.openpgp.org/vks/v1/by-fingerprint/${fpr}" 2>/dev/null \
| gpg --import 2>/dev/null; then
imported=$((imported + 1))
continue
fi
if curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x${fpr}&options=mr" 2>/dev/null \
| gpg --import 2>/dev/null; then
# Import from both sources. Some keyservers may return partial key
# material for old signatures (e.g. missing signing subkeys).
# Importing from both increases robustness while keeping the same
# pinned trust anchors for verification.
curl -fsSL "https://keys.openpgp.org/vks/v1/by-fingerprint/${fpr}" 2>/dev/null \
| gpg --import 2>/dev/null || true
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x${fpr}&options=mr" 2>/dev/null \
| gpg --import 2>/dev/null || true
# Count this key as imported if it exists in the keyring after the
# multi-source import attempts.
if gpg --list-keys --with-colons "$fpr" 2>/dev/null | grep -q '^pub:'; then
imported=$((imported + 1))
fi
done

View file

@ -181,7 +181,11 @@ def test_addressinfo(caplog, client, funded_ghost_machine_wallet):
assert res.data.decode().startswith(
'{"error":"Request error for method getaddressinfo'
)
assert res.data.decode().endswith('Invalid address format"}\n')
# Core 22 says "Invalid address format"; Core 24+ says "Invalid checksum".
assert (
'Invalid address format"}\n' in res.data.decode()
or 'Invalid checksum"}\n' in res.data.decode()
)
# send post request with address, not belonging to wallet
# this recreates an edge case, see https://github.com/cryptoadvance/specter-desktop/issues/2000

180
tests/test_hwi_server.py Normal file
View file

@ -0,0 +1,180 @@
import re
import sys
from uuid import uuid4
import pytest
from cryptoadvance.specter.config import TestConfig
from cryptoadvance.specter.helpers import hwi_get_config
from cryptoadvance.specter.server import create_app, init_app
from cryptoadvance.specter.specter import Specter
def make_scoped_app(tmp_path, hwibridge, auth_method="none"):
config_name = f"ScopedTestConfig_{uuid4().hex}"
config_class = type(
config_name,
(TestConfig,),
{
"__module__": __name__,
"SPECTER_DATA_FOLDER": str(tmp_path),
"SPECTER_URL_PREFIX": "/spc",
"SESSION_COOKIE_PATH": "/spc",
"SESSION_PROTECTION": None,
"SKIP_HWI_INITIALISATION_AT_STARTUP": True,
"SPECTER_API_ACTIVE": False,
},
)
setattr(sys.modules[__name__], config_name, config_class)
specter = Specter(data_folder=str(tmp_path), checker_threads=False)
if auth_method != "none":
specter.update_auth(auth_method, 10, 1)
app = create_app(config_class)
app.config["TESTING"] = True
with app.app_context():
init_app(app, hwibridge=hwibridge, specter=specter)
return app
def csrf_token(response):
match = re.search(b'name="csrf_token" value="([^"]+)', response.data)
assert match is not None
return match.group(1).decode()
@pytest.mark.parametrize("hwibridge", [False, True])
def test_hwi_settings_use_session_scoped_route(tmp_path, hwibridge):
app = make_scoped_app(tmp_path, hwibridge)
client = app.test_client()
legacy_response = client.get("/hwi/settings/")
assert legacy_response.status_code == 302
assert legacy_response.headers["Location"] == "/spc/hwi/settings/"
settings_response = client.get("/spc/hwi/settings/")
assert settings_response.status_code == 200
assert "Path=/spc" in settings_response.headers["Set-Cookie"]
missing_csrf_response = client.post(
"/spc/hwi/settings/",
data={"action": "update", "whitelisted_domains": "http://attacker/"},
)
assert missing_csrf_response.status_code in (302, 400)
assert hwi_get_config(app.specter)["whitelisted_domains"] != "http://attacker/"
invalid_csrf_response = client.post(
"/spc/hwi/settings/",
data={
"action": "update",
"csrf_token": "invalid",
"whitelisted_domains": "http://attacker/",
},
)
assert invalid_csrf_response.status_code in (302, 400)
assert hwi_get_config(app.specter)["whitelisted_domains"] != "http://attacker/"
update_response = client.post(
"/spc/hwi/settings/",
data={
"action": "update",
"csrf_token": csrf_token(settings_response),
"whitelisted_domains": "http://example.com/",
},
)
assert update_response.status_code == 200
assert (
hwi_get_config(app.specter)["whitelisted_domains"].strip()
== "http://example.com/"
)
@pytest.mark.parametrize("hwibridge", [False, True])
def test_hwi_settings_require_authenticated_admin(tmp_path, hwibridge):
app = make_scoped_app(tmp_path, hwibridge=hwibridge, auth_method="usernamepassword")
client = app.test_client()
anonymous_response = client.get("/spc/hwi/settings/")
assert anonymous_response.status_code == 302
assert anonymous_response.headers["Location"].startswith(
"/spc/auth/login?next=%2Fspc%2Fhwi%2Fsettings%2F"
)
assert client.get("/spc/auth/login").status_code == 200
login_response = client.post(
"/spc/auth/login",
data={
"username": "admin",
"password": "admin",
"next": "/spc/hwi/settings/",
},
)
assert login_response.status_code == 302
assert login_response.headers["Location"] == "/spc/hwi/settings/"
assert client.get(login_response.headers["Location"]).status_code == 200
app.specter.user_manager.create_user(
user_id="nonadmin",
username="nonadmin",
plaintext_password="nonadmin",
config={},
)
nonadmin_client = app.test_client()
nonadmin_login_response = nonadmin_client.post(
"/spc/auth/login",
data={
"username": "nonadmin",
"password": "nonadmin",
"next": "/spc/hwi/settings/",
},
)
assert nonadmin_login_response.status_code == 302
assert nonadmin_client.get("/spc/hwi/settings/").status_code == 403
def test_hwi_settings_force_admin_when_login_disabled(tmp_path):
app = make_scoped_app(tmp_path, hwibridge=True, auth_method="usernamepassword")
app.specter.user_manager.create_user(
user_id="nonadmin",
username="nonadmin",
plaintext_password="nonadmin",
config={},
)
client = app.test_client()
login_response = client.post(
"/spc/auth/login",
data={
"username": "nonadmin",
"password": "nonadmin",
"next": "/spc/hwi/settings/",
},
)
assert login_response.status_code == 302
app.specter.update_auth("none", 10, 1)
app.config["LOGIN_DISABLED"] = True
assert client.get("/spc/hwi/settings/").status_code == 200
with client.session_transaction(path="/spc/hwi/settings/") as session:
assert session["_user_id"] == "admin"
@pytest.mark.parametrize("hwibridge", [False, True])
def test_hwi_api_remains_csrf_exempt(tmp_path, hwibridge):
app = make_scoped_app(tmp_path, hwibridge=hwibridge)
app.specter.hwi.exposed_rpc["enumerate"] = lambda **kwargs: []
client = app.test_client()
client.environ_base["HTTP_ORIGIN"] = "http://127.0.0.1:25441/"
response = client.post(
"/hwi/api/",
json={
"jsonrpc": "2.0",
"method": "enumerate",
"id": 1,
"params": {},
"forwarded_request": True,
},
)
assert response.status_code == 200
assert response.get_json() == {"id": 1, "jsonrpc": "2.0", "result": []}

140
tests/test_jade_hardware.py Normal file
View file

@ -0,0 +1,140 @@
"""Opt-in tests requiring a physical Blockstream Jade attached + operator.
Skipped by default. Enable with::
pytest --run-jade-hardware tests/test_jade_hardware.py -s
The ``-s`` is required so operator prompts reach your terminal.
Operator setup
--------------
For ``test_jade_enumerate_via_specter`` and
``test_jade_extract_xpub_via_specter``: any Jade with any seed (your
real one is fine those tests only read public material).
For ``test_jade_sign_psbt_via_specter``: the Jade must be in
**Temporary Signer mode** with the public **BIP-39 abandon vector** seed
(``abandon abandon ... about``) loaded. The simplest path:
1. Power-cycle the Jade so it's at the boot menu.
2. Choose ``Temporary Signer`` -> ``Scan SeedQR``.
3. Scan ``tests/fixtures/jade_seedqr_abandon.png`` (or display
``tests/fixtures/jade_seedqr_abandon.txt`` and scan from screen).
4. Confirm the **testnet** network on Jade.
5. The PSBT at ``tests/fixtures/jade_hardware.psbt`` is fabricated by
Coldcard's psbt_faker against this exact seed.
Temporary Signer state lives in RAM only; it's wiped on power-cycle. Your
real seed is not affected.
"""
from pathlib import Path
import pytest
from cryptoadvance.specter.hwi_rpc import HWIBridge
FIXTURE_DIR = Path(__file__).parent / "fixtures"
PSBT_FIXTURE = FIXTURE_DIR / "jade_hardware.psbt"
SEEDQR_PNG = FIXTURE_DIR / "jade_seedqr_abandon.png"
SEEDQR_TXT = FIXTURE_DIR / "jade_seedqr_abandon.txt"
ABANDON_FINGERPRINT = "73c5da0a"
def _enumerate_jade(bridge: HWIBridge, chain: str = "main"):
# HWIBridge.enumerate defaults chain="" which Chain.argparse passes
# through unchanged; Jade's enumerate then fails with
# "Unhandled network: ". Pass an explicit chain.
devs = bridge.enumerate(chain=chain)
return [d for d in devs if d.get("type") == "jade"]
def _prompt(msg: str) -> None:
print(f"\n>>> {msg}")
try:
input(">>> Press Enter when ready... ")
except EOFError:
pass
@pytest.mark.jade_hardware
def test_jade_enumerate_via_specter():
"""Jade is detected by Specter's HWIBridge and reports a fingerprint."""
_prompt("Connect and unlock the Jade.")
bridge = HWIBridge(skip_hwi_initialisation=True)
jades = _enumerate_jade(bridge)
assert jades, "no Jade detected — connect, unlock, and rerun"
jade = jades[0]
assert jade.get("fingerprint"), f"Jade enumerated without fingerprint: {jade}"
assert jade.get("path"), f"Jade enumerated without path: {jade}"
@pytest.mark.jade_hardware
def test_jade_extract_xpub_via_specter():
"""Specter can pull an xpub at a known derivation from Jade."""
_prompt("Unlock the Jade. You may be asked to confirm the xpub export.")
bridge = HWIBridge(skip_hwi_initialisation=True)
jades = _enumerate_jade(bridge)
assert jades, "no Jade detected"
fingerprint = jades[0].get("fingerprint")
assert fingerprint, f"Jade enumerated without fingerprint: {jades[0]}"
# chain must be passed explicitly: HWIBridge.extract_xpub default is
# chain="" which Specter's JadeClient.__init__ rejects via _network()
# before extract_xpub's post-init override can apply.
xpub_line = bridge.extract_xpub(
derivation="m/84h/0h/0h",
device_type="jade",
fingerprint=fingerprint,
chain="main",
)
assert xpub_line, "extract_xpub returned empty"
assert xpub_line.startswith("["), f"unexpected format: {xpub_line!r}"
assert "]" in xpub_line, f"unexpected format: {xpub_line!r}"
body = xpub_line.split("]", 1)[1].strip()
assert body.startswith(("xpub", "zpub", "ypub")), f"unexpected xpub: {body[:8]}"
@pytest.mark.jade_hardware
def test_jade_sign_psbt_via_specter():
"""End-to-end: Specter signs the canned abandon-vector PSBT through Jade.
Requires Jade in Temporary Signer mode with the abandon-vector seed
(see module docstring). The fixture PSBT was generated by Coldcard's
psbt_faker against m/84'/1'/0' on testnet; xfp is 73c5da0a.
"""
assert PSBT_FIXTURE.exists(), f"missing fixture: {PSBT_FIXTURE}"
seedqr_hint = (
f"\n PNG: {SEEDQR_PNG}\n"
f" ASCII: cat {SEEDQR_TXT}"
)
_prompt(
"Put Jade in Temporary Signer mode -> Scan SeedQR -> select TESTNET."
f"\n SeedQR for the BIP-39 abandon-vector lives at:{seedqr_hint}\n"
" Then confirm the transaction on device when prompted."
)
psbt_b64 = PSBT_FIXTURE.read_text().strip()
bridge = HWIBridge(skip_hwi_initialisation=True)
jades = _enumerate_jade(bridge, chain="test")
assert jades, "no Jade detected"
fingerprint = jades[0].get("fingerprint")
assert fingerprint, f"Jade enumerated without fingerprint: {jades[0]}"
assert fingerprint.lower() == ABANDON_FINGERPRINT, (
f"connected Jade fingerprint is {fingerprint}; "
f"expected {ABANDON_FINGERPRINT} (abandon-vector). "
"Are you in Temporary Signer mode with the right SeedQR?"
)
signed = bridge.sign_tx(
psbt=psbt_b64,
device_type="jade",
fingerprint=fingerprint,
chain="test",
)
assert signed, "sign_tx returned empty"
assert signed != psbt_b64, "PSBT was returned unsigned"

View file

@ -82,6 +82,99 @@ def test_token_endpoints(client, empty_data_folder, caplog):
assert data["jwt_token_life"] == 360
jwt_token_id = data["jwt_token_id"]
jwt_token = data["jwt_token"]
# API-created tokens remain registered after the user store is reloaded.
client.application.specter.user_manager.update()
# An active, registered token authenticates successfully. The missing wallet
# is rejected by authorization after authentication has completed.
token_headers = {"Authorization": "Bearer " + jwt_token}
response = client.get(
"/api/v1alpha/wallets/missing/psbt",
follow_redirects=True,
headers=token_headers,
)
assert response.status_code == 403
# A signed token must still be registered in the user's active token store.
unregistered_token = User.generate_jwt_token(
"someuser", User.generate_token_id(), "unregistered", 360
)
response = client.get(
"/api/v1alpha/wallets/missing/psbt",
follow_redirects=True,
headers={"Authorization": "Bearer " + unregistered_token},
)
assert response.status_code == 401
# Missing and non-string token identifiers fail closed.
expiry = datetime.datetime.utcnow() + datetime.timedelta(seconds=360)
invalid_payloads = [
{"username": "someuser", "exp": expiry},
{"username": "someuser", "jwt_token_id": ["invalid"], "exp": expiry},
]
for invalid_payload in invalid_payloads:
invalid_token = jwt.encode(
invalid_payload,
client.application.config["SECRET_KEY"],
algorithm="HS256",
)
response = client.get(
"/api/v1alpha/wallets/missing/psbt",
follow_redirects=True,
headers={"Authorization": "Bearer " + invalid_token},
)
assert response.status_code == 401
# A different signed token cannot borrow an active token's identifier.
mismatched_token = User.generate_jwt_token(
"someuser", jwt_token_id, "mismatched", 360
)
response = client.get(
"/api/v1alpha/wallets/missing/psbt",
follow_redirects=True,
headers={"Authorization": "Bearer " + mismatched_token},
)
assert response.status_code == 401
# Malformed persisted records fail closed instead of raising an error.
user_details = client.application.specter.user_manager.get_user_by_username(
"someuser"
)
stored_token_info = user_details.jwt_tokens[jwt_token_id]
user_details.jwt_tokens[jwt_token_id] = {}
user_details.save_info()
client.application.specter.user_manager.update()
response = client.get(
"/api/v1alpha/wallets/missing/psbt",
follow_redirects=True,
headers=token_headers,
)
assert response.status_code == 401
user_details = client.application.specter.user_manager.get_user_by_username(
"someuser"
)
user_details.jwt_tokens[jwt_token_id] = stored_token_info
user_details.save_info()
# A malformed persisted token container also fails closed after reload.
stored_tokens = user_details.jwt_tokens
user_details.jwt_tokens = []
user_details.save_info()
client.application.specter.user_manager.update()
response = client.get(
"/api/v1alpha/wallets/missing/psbt",
follow_redirects=True,
headers=token_headers,
)
assert response.status_code == 401
user_details = client.application.specter.user_manager.get_user_by_username(
"someuser"
)
user_details.jwt_tokens = stored_tokens
user_details.save_info()
client.application.specter.user_manager.update()
# testing GET request
response = client.get("/api/v1alpha/token", follow_redirects=True, headers=headers)
@ -133,6 +226,14 @@ def test_token_endpoints(client, empty_data_folder, caplog):
data = json.loads(response.data)
assert data["message"] == "Token deleted"
# Deletion immediately revokes the bearer token.
response = client.get(
"/api/v1alpha/wallets/missing/psbt",
follow_redirects=True,
headers=token_headers,
)
assert response.status_code == 401
# retry accessing a deleted token
response = client.get(
"/api/v1alpha/token/" + jwt_token_id, follow_redirects=True, headers=headers

View file

@ -17,6 +17,17 @@ from cryptoadvance.specter.user import User
logger = logging.getLogger(__name__)
def create_registered_jwt_token(specter, username):
user = specter.user_manager.get_user_by_username(username)
jwt_token_id = User.generate_token_id()
jwt_token_life = 3600
jwt_token = User.generate_jwt_token(
username, jwt_token_id, "test token", jwt_token_life
)
user.add_jwt_token(jwt_token_id, jwt_token, "test token", jwt_token_life)
return jwt_token
def almost_equal(a: Number, b: Number, precision: float = 0.01) -> bool:
"""
Checks if a and b are not very different.
@ -58,9 +69,7 @@ def test_rr_psbt_get(client, specter_regtest_configured, bitcoin_regtest, caplog
# Admin but not authorized (admin is NOT allowed to read everything)
headers = {
"Authorization": "Bearer "
+ User.generate_jwt_token(
"admin", "tokenid", "tokendescription", random.randrange(100, 200)
)
+ create_registered_jwt_token(specter_regtest_configured, "admin")
}
result = client.get(
"/api/v1alpha/wallets/a_simple_wallet/psbt",
@ -76,9 +85,7 @@ def test_rr_psbt_get(client, specter_regtest_configured, bitcoin_regtest, caplog
# Proper authorized (the wallet is owned by someuser)
headers = {
"Authorization": "Bearer "
+ User.generate_jwt_token(
"someuser", "tokenid", "tokendescription", random.randrange(100, 200)
)
+ create_registered_jwt_token(specter_regtest_configured, "someuser")
}
result = client.get(
"/api/v1alpha/wallets/a_simple_wallet/psbt",
@ -97,9 +104,7 @@ def test_rr_psbt_post(specter_regtest_configured, bitcoin_regtest, client, caplo
headers = {
"Authorization": "Bearer "
+ User.generate_jwt_token(
"someuser", "tokenid", "tokendescription", random.randrange(100, 200)
),
+ create_registered_jwt_token(specter_regtest_configured, "someuser"),
"Content-type": "application/json",
}

View file

@ -6,7 +6,10 @@ from unittest.mock import MagicMock
import pytest
import mock
from mock import Mock, patch
import requests
from cryptoadvance.specterext.swan.client import (
SwanApiException,
SwanApiRefreshTokenException,
SwanClient,
)
@ -106,6 +109,65 @@ def test_expired_access_token():
sc._get_access_token()
def construct_client_with_valid_token():
"""A client which won't need to fetch an access_token first"""
return SwanClient(
"a_hostname", "forever_valid_access_token", 5000000000, "a_refresh_token"
)
def test_authenticated_request_get_timeout(app_no_node):
"""A timeout must surface as SwanApiException, not as an UnboundLocalError"""
sc = construct_client_with_valid_token()
with app_no_node.app_context():
with mock.patch(
"requests.get", side_effect=requests.exceptions.Timeout("simulated timeout")
):
with pytest.raises(SwanApiException) as exc_info:
sc.authenticated_request("/some/endpoint")
assert "simulated timeout" in str(exc_info.value)
assert isinstance(exc_info.value.__cause__, requests.exceptions.Timeout)
def test_authenticated_request_post_timeout(app_no_node):
"""Same for the methods going through requests.request"""
sc = construct_client_with_valid_token()
with app_no_node.app_context():
with mock.patch(
"requests.request",
side_effect=requests.exceptions.ConnectTimeout("simulated timeout"),
):
with pytest.raises(SwanApiException) as exc_info:
sc.authenticated_request(
"/some/endpoint", method="POST", json_payload={"muuh": "meeh"}
)
assert isinstance(exc_info.value.__cause__, requests.exceptions.ConnectTimeout)
def test_authenticated_request_error_status_code(app_no_node):
sc = construct_client_with_valid_token()
fake_response = Mock()
fake_response.status_code = 500
fake_response.text = "Internal Server Error"
with app_no_node.app_context():
with mock.patch("requests.get", return_value=fake_response):
with pytest.raises(SwanApiException, match="500: Internal Server Error"):
sc.authenticated_request("/some/endpoint")
def test_get_access_token_timeout(app_no_node):
"""The token-endpoint is used before authenticated_request can even start"""
sc = SwanClient("a_hostname", "an_expired_access_token", 1000, "a_refresh_token")
with app_no_node.app_context():
with mock.patch(
"requests.post",
side_effect=requests.exceptions.Timeout("simulated timeout"),
):
with pytest.raises(SwanApiException) as exc_info:
sc.authenticated_request("/some/endpoint")
assert isinstance(exc_info.value.__cause__, requests.exceptions.Timeout)
@patch("requests.delete")
@patch("requests.request")
@patch("requests.patch")

View file

@ -11,6 +11,7 @@ from cryptoadvance.specter.util.reflection import (
get_subclasses_for_clazz,
get_subclasses_for_clazz_in_cwd,
get_classlist_of_type_clazz_from_modulelist,
is_specter_desktop_project,
_get_module_from_class,
get_package_dir_for_subclasses_of,
search_dirs_in_path,
@ -119,13 +120,53 @@ def test_get_classlist_raises_on_missing_module_by_default():
get_classlist_of_type_clazz_from_modulelist(Service, modulelist)
repo_root = Path(__file__).parent.parent
xtestdata = repo_root / "tests" / "xtestdata_testextensions"
def test_is_specter_desktop_project():
"""The specter-desktop project detects itself via the name in its own
pyproject.toml. If that name changes (PEP 503 allows "." "-" and "_" to be
used interchangeably), the dev-server dies on startup, see #2526."""
assert is_specter_desktop_project(repo_root)
assert not is_specter_desktop_project(xtestdata / "ext_root_fully_qualified_1")
assert not is_specter_desktop_project(xtestdata)
def test_is_specter_desktop_project_pep503_names(tmp_path):
for name in [
"cryptoadvance.specter",
"cryptoadvance_specter",
"Cryptoadvance-Specter",
]:
(tmp_path / "pyproject.toml").write_text(
f'[project]\nname = "{name}"\nversion = "1.2.3"\n'
)
assert is_specter_desktop_project(tmp_path), f"{name} should be detected"
(tmp_path / "pyproject.toml").write_text(
'[project]\nname = "boatacccorp.tretboot"\n'
)
assert not is_specter_desktop_project(tmp_path)
def test_get_subclasses_for_clazz_in_cwd_in_specter_desktop_project(monkeypatch):
"""No dynamic extension-discovery in the specter-desktop project itself.
Regression test: this used to raise "This should not happen!" when the
project got renamed to cryptoadvance_specter, breaking
`python3 -m cryptoadvance.specter server --config DevelopmentConfig`"""
# the production code takes a shortcut for tests, so pretend we're not testing
monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
assert get_subclasses_for_clazz_in_cwd(Service, cwd=repo_root) == []
def test_get_subclasses_for_clazz_in_cwd(caplog):
caplog.set_level(logging.DEBUG)
classlist: List[type] = get_subclasses_for_clazz_in_cwd(
Service, cwd="./tests/xtestdata_testextensions"
)
# damn, this is difficult to test
# assert len(classlist) == 3
classlist: List[type] = get_subclasses_for_clazz_in_cwd(Service, cwd=xtestdata)
# That folder is a container of extension-projects, not an extension-project
# itself, so there is nothing importable in there
assert classlist == []
assert "Detected Extension-style: adhoc" in caplog.text
def test_get_subclasses_for_class(caplog):

View file

@ -23,7 +23,18 @@ def test_last_lines(caplog):
assert lines[-2].startswith("OUT OF OR IN CONNECTION WITH THE SOFTWARE ")
def test_grep():
def test_grep(tmp_path):
"""grep returns a (found, line)-tuple. Asserting on the tuple itself is
always truthy, so always assert on the first element!"""
from cryptoadvance.specter.util.shell import grep
assert grep("./pyproject.toml", 'name = "cryptoadvance.specter"')
some_file = tmp_path / "some_file.txt"
some_file.write_text('name = "cryptoadvance_specter"\nversion = "1.2.3"\n')
found, line = grep(str(some_file), 'name = "cryptoadvance_specter"')
assert found
assert line.strip() == 'name = "cryptoadvance_specter"'
found, line = grep(str(some_file), "does not exist")
assert not found
assert line is None

View file

@ -0,0 +1,177 @@
"""
Regression tests for https://github.com/cryptoadvance/specter-desktop/issues/2604
Bitcoin Core's legacy `importmulti` refuses to shrink the keypool range of an
already-imported descriptor ("new range must include current range"). This can
surface when re-creating a wallet under a name that Core already has a wider
range recorded for. Wallet.create should detect that specific rejection and
retry once with a range wide enough to include Core's current range, instead
of failing outright.
These tests mock the RPC layer entirely, so they don't need a running
bitcoind/regtest node.
"""
from unittest.mock import MagicMock
from cryptoadvance.specter.key import Key
from cryptoadvance.specter.wallet import Wallet
def _make_rpc(importmulti_responses):
"""Builds a fake `rpc`/`wallet_rpc` pair.
importmulti_responses: a list of return values, one per call to
`importmulti`. Each element is itself the list-of-dicts the real RPC
would return.
"""
wallet_rpc = MagicMock()
wallet_rpc.importmulti.side_effect = importmulti_responses
rpc = MagicMock()
rpc.getnetworkinfo.return_value = {"version": 200000} # pre-descriptor-wallets
rpc.wallet.return_value = wallet_rpc
return rpc, wallet_rpc
class _BareWallet(Wallet):
"""A stand-in for Wallet that skips the real __init__ (which touches
rpc, address lists, tx history, etc.) so these tests only exercise the
`create()` classmethod's import/retry logic."""
def __init__(self, *args, **kwargs):
self.init_args = args
self.init_kwargs = kwargs
def _single_sig_key():
# A known-good testnet tpub (m/84'/1'/0') used elsewhere in the test
# suite's ghost-machine fixtures.
tpub = "tpubDC4DsqH5rqHqipMNqUbDFtQT3AkKkUrvLsN6miySvortU3s1LGaNVAb7wX2No2VsuxQV82T8s3HJLv3kdx1CPjsJ3onC1Zo5mWCQzRVaWVX"
return Key.parse_xpub(f"[81f802e3/84h/1h/0h]{tpub}")
def test_create_retries_with_widened_range_on_shrink_refusal():
"""Core rejects the default [0, GAP_LIMIT] range because it already has
a wider range on file; Wallet.create should retry with a widened range
and succeed."""
key = _single_sig_key()
shrink_refusal = [
{
"success": False,
"error": {"message": "new range must include current range = [0,1008]"},
},
{
"success": False,
"error": {"message": "new range must include current range = [0,1000]"},
},
]
success = [{"success": True}, {"success": True}]
rpc, wallet_rpc = _make_rpc([shrink_refusal, success])
wallet = _BareWallet.create(
rpc=rpc,
rpc_path="specter",
working_folder="/tmp",
device_manager=MagicMock(),
wallet_manager=MagicMock(),
name="mywallet",
alias="mywallet",
sigs_required=1,
key_type="wpkh",
keys=[key],
devices=[MagicMock()],
core_version=200000,
)
assert wallet is not None
# One retry: the first call (default range) failed, the second (widened
# range) succeeded.
assert wallet_rpc.importmulti.call_count == 2
second_call_args = wallet_rpc.importmulti.call_args_list[1][0][0]
# Widened range must cover the widest range Core reported (1008)
assert all(arg["range"] == [0, 1008] for arg in second_call_args)
def test_create_raises_if_retry_also_fails():
"""If the widened-range retry still fails, Wallet.create should still
raise a SpecterError (no silent swallow of a real problem)."""
from cryptoadvance.specter.specter_error import SpecterError
key = _single_sig_key()
shrink_refusal = [
{
"success": False,
"error": {"message": "new range must include current range = [0,1008]"},
},
]
still_failing = [
{
"success": False,
"error": {"message": "some other unrelated error"},
},
]
rpc, wallet_rpc = _make_rpc([shrink_refusal, still_failing])
try:
_BareWallet.create(
rpc=rpc,
rpc_path="specter",
working_folder="/tmp",
device_manager=MagicMock(),
wallet_manager=MagicMock(),
name="mywallet",
alias="mywallet",
sigs_required=1,
key_type="wpkh",
keys=[key],
devices=[MagicMock()],
core_version=200000,
)
assert False, "expected SpecterError"
except SpecterError:
pass
assert wallet_rpc.importmulti.call_count == 2
def test_create_does_not_retry_on_unrelated_failure():
"""A failure that isn't the range-shrink message should raise immediately,
without a pointless retry."""
from cryptoadvance.specter.specter_error import SpecterError
key = _single_sig_key()
unrelated_failure = [
{
"success": False,
"error": {"message": "some other unrelated error"},
},
]
rpc, wallet_rpc = _make_rpc([unrelated_failure])
try:
_BareWallet.create(
rpc=rpc,
rpc_path="specter",
working_folder="/tmp",
device_manager=MagicMock(),
wallet_manager=MagicMock(),
name="mywallet",
alias="mywallet",
sigs_required=1,
key_type="wpkh",
keys=[key],
devices=[MagicMock()],
core_version=200000,
)
assert False, "expected SpecterError"
except SpecterError:
pass
assert wallet_rpc.importmulti.call_count == 1

View file

@ -347,7 +347,10 @@ def test_WalletAwareTxItem(bitcoin_regtest, parent_mock, empty_data_folder):
print("\n\nOutgoing-Transaction (0.2 btc)")
print("=========================================")
txid_outgoing_addr = "n4MN27Lk7Yh3pwfjCiAbRXtRVjs4Uk67fG"
# bech32 destination so Core keeps change as wpkh (Core 24+ matches change
# type to destination type; legacy destinations produce legacy change that
# the wpkh-only watch descriptor in this test does not recognise).
txid_outgoing_addr = "bcrt1qvtdx75y4554ngrq6aff3xdqnvjhmct5wck95qs"
print(f"address = {txid_outgoing_addr}")
txid_outgoing = wrpc.sendtoaddress(txid_outgoing_addr, 0.2)
print(f"balance: {wrpc.getbalances()['mine']['trusted']}")

View file

@ -1,12 +1,8 @@
# HW.1 / Nano
# HW.1, Nano
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2581", ATTRS{idProduct}=="1b7c|2b7c|3b7c|4b7c", TAG+="uaccess", TAG+="udev-acl"
# Blue
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0000|0000|0001|0002|0003|0004|0005|0006|0007|0008|0009|000a|000b|000c|000d|000e|000f|0010|0011|0012|0013|0014|0015|0016|0017|0018|0019|001a|001b|001c|001d|001e|001f", TAG+="uaccess", TAG+="udev-acl"
# Nano S
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0001|1000|1001|1002|1003|1004|1005|1006|1007|1008|1009|100a|100b|100c|100d|100e|100f|1010|1011|1012|1013|1014|1015|1016|1017|1018|1019|101a|101b|101c|101d|101e|101f", TAG+="uaccess", TAG+="udev-acl"
# Aramis
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0002|2000|2001|2002|2003|2004|2005|2006|2007|2008|2009|200a|200b|200c|200d|200e|200f|2010|2011|2012|2013|2014|2015|2016|2017|2018|2019|201a|201b|201c|201d|201e|201f", TAG+="uaccess", TAG+="udev-acl"
# HW2
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0003|3000|3001|3002|3003|3004|3005|3006|3007|3008|3009|300a|300b|300c|300d|300e|300f|3010|3011|3012|3013|3014|3015|3016|3017|3018|3019|301a|301b|301c|301d|301e|301f", TAG+="uaccess", TAG+="udev-acl"
# Nano X
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0004|4000|4001|4002|4003|4004|4005|4006|4007|4008|4009|400a|400b|400c|400d|400e|400f|4010|4011|4012|4013|4014|4015|4016|4017|4018|4019|401a|401b|401c|401d|401e|401f", TAG+="uaccess", TAG+="udev-acl"
# Blue, NanoS, Aramis, HW.2, Nano X, NanoSP, Stax, Ledger Test,
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", TAG+="uaccess", TAG+="udev-acl"
# Same, but with hidraw-based library (instead of libusb)
KERNEL=="hidraw*", ATTRS{idVendor}=="2c97", MODE="0666"

View file

@ -1,81 +0,0 @@
#!/bin/bash
function sub_help {
echo "This script is to sign artifacts or to prepare the gpg-system to be able to verify artifacts."
echo "Do one of these:"
echo "$ ./utils/artifact_signer.sh init"
echo "This makes sense only on a gitlab-runner. It'll unpack a gpg-directory to be ready to sign and verify"
echo "$ ./utils/artifact_signer.sh sign --artifact ./release-win/SHA256SUMS-win"
echo "Signs a specific artifact. Will do the init on the fly. So no need to call it extra."
}
while [[ $# -gt 0 ]]
do
key="$1"
command="main"
case $key in
--help)
sub_help
exit
shift
;;
--artifact)
artifact=$2
shift
shift
;;
sign)
action=sign
shift
;;
init)
action=init
shift
;;
--debug)
set -x
shift # past argument
;;
*) # unknown option
POSITIONAL="$1" # save it in an array for later
shift # past argument
;;
esac
done
# We want a detached signature in cleartext. Extension: .asc (as in bitcoin)
output_file=${artifact}.asc
# lazy init: We're initializing Each thime script is called with these two things.
# So that action "init" is just to have a bit more semantics for the one calling this script
function init {
if [[ -f /credentials/gnupg.tar.gz ]]; then
echo "Init: extracting gnupg.tar.gz"
tar -xzf /credentials/gnupg.tar.gz -C /root
chown -R root:root ~/.gnupg
else
echo "Init: Could not find any /credentials/gnupg.tar.gz"
fi
if [[ -f /credentials/private.key ]]; then
echo "Init: Importing single private key"
gpg --import --no-tty --batch --yes /credentials/private.key
else
echo "Init: Could not find any /credentials/private.key"
fi
}
if [ "$action" = "init" ]; then
init
fi
if [ "$action" = "sign" ]; then
init
if [[ -z $artifact ]]; then
echo "no --artifact given "
exit 1
fi
echo "signing ..."
echo $GPG_PASSPHRASE | gpg --detach-sign --armor --no-tty --batch --yes --passphrase-fd 0 --pinentry-mode loopback $artifact
fi

View file

@ -1,135 +0,0 @@
#!/usr/bin/env bash
# All functions in here are responsible to change directory
# from the project root to wherever they want
# They need to change back to project-root when they finish
function create_virtualenv_for_pyinstaller {
echo " --> Creating new virtualsenv"
if [ -d .buildenv ]; then
echo " But first Delete it ..."
rm -rf .buildenv
fi
virtualenv --python=python3.10 .buildenv
source .buildenv/bin/activate
pip3 install -e ".[test]"
}
function build_pypi_pckgs_and_install {
echo " --> Build pip3-package"
rm -rf dist
if ! git diff --quiet setup.py; then
echo "ERROR: setup.py is dirty, can't reasonably build"
exit 1
fi
if [[ "$OSTYPE" == "darwin"* ]]; then
SML_ADD="\"\""
fi
pip3 install build==0.10.0
python3 -m build
pip3 install ./dist/cryptoadvance.specter-*.whl
}
function configure {
echo " --> Configure some variables"
if [ -z "$app_name" ]; then
# activate virtualenv. This is e.g. not needed in CI
app_name=specter
specterd_filename=specterd
specterimg_filename=Specter
pkg_filename=specter_desktop
else
specterd_filename=${app_name}d # usually "specterd"
specterimg_filename=${app_name^} # usually "Specter"
pkg_filename=${app_name}
fi
export ARCH=$(node -e "console.log(process.arch)")
export dist_mac_folder_name=mac-universal
export CI_COMMIT_TAG=$version
export CI_PROJECT_ROOT_NAMESPACE=$(node -e "const downloadloc = require('./pyinstaller/electron/downloadloc');console.log(downloadloc.orgName())")
echo specterd_filename=${specterd_filename}
echo specterimg_filename=${specterimg_filename}
echo pkg_filename=${pkg_filename}
echo ARCH=$ARCH
echo dist_mac_folder_name=$dist_mac_folder_name
echo CI_COMMIT_TAG=$CI_COMMIT_TAG
echo CI_PROJECT_ROOT_NAMESPACE=$CI_PROJECT_ROOT_NAMESPACE
}
function install_build_requirements {
echo " --> Installing pyinstaller build-requirements"
cd pyinstaller
pip3 install -r requirements.txt --require-hashes > /dev/null
cd ..
}
function cleanup {
echo " --> Cleaning up"
cd pyinstaller
rm -rf build/ dist/ release/ electron/release/ electron/dist
rm *.dmg || true
cd ..
}
function building_app {
echo " --> Building ${specterd_filename}"
cd pyinstaller
specterd_filename=${specterd_filename} pyinstaller specterd.spec > /dev/null
cd ..
}
function prepare_npm {
cd pyinstaller/electron
echo " --> Making us ready for building electron-app"
npm ci
cd ../..
}
function make_hash_if_necessary {
cd pyinstaller/electron
echo " --> calculate the hash of the binary for download"
if [[ "$1" = "win" ]]; then
specterd_plt_filename=../dist/${specterd_filename}.exe
else
specterd_plt_filename=../dist/${specterd_filename}
fi
if [[ "$make_hash" == 'True' ]]
then
node ./set-version $version ${specterd_plt_filename}
else
node ./set-version $version
fi
echo " Hash in version -data.json $(cat ./version-data.json | jq -r '.sha256')"
echo " Hash of file $(sha256sum ${specterd_plt_filename} )"
cd ../..
}
function building_electron_app {
# https://www.electron.build/
# Prerequisites:
# * A developer Certificate (in the System keychain)
# * private and public key in the login-keychain
# * The cert needs to be referenced in pyinstaller/electron/package.json -> build.mac.identity
platform="-- --${1}" # either linux or win (maxOS is empty)
cd pyinstaller/electron
echo " --> building electron-app"
echo " --> Copying over resources"
cp -R ../../src/cryptoadvance/specter/static/fonts ../../src/cryptoadvance/specter/static/output.css ../../src/cryptoadvance/specter/static/typography.css .
npm i
npm run dist ${platform}
cd ../..
}
function make_release_zip {
echo " --> Making the release-zip"
}

View file

@ -1,404 +0,0 @@
#!/usr/bin/env bash
set -e
# We start in the directory where this script is located
cd "$( dirname "${BASH_SOURCE[0]}" )/."
source build-common.sh
cd ..
# Now in project-root
# Overriding this function
function create_virtualenv_for_pyinstaller {
# This currently assumes to be run with: Python 3.10.11
# Important: pyinstaller needs a Python binary with shared library files
# With pyenv, for example, you get this like so: env PYTHON_CONFIGURE_OPTS="--enable-shared" pyenv install 3.10.4
# Use pyenv if set as environment variable
if [ $USE_PYENV_FOR_SPECTER_BUILD = true ]; then
echo "Trying to use pyenv ..."
if ! command -v pyenv >/dev/null 2>&1; then
echo "Error: pyenv is not available. Please make sure pyenv is installed and configured properly." >&2
exit 1
fi
### This is usually in .zshrc, putting it in .bashrc didn't work ###
export PYENV_ROOT="$HOME/.pyenv"
command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
### this needs the pyenv-virtualenv plugin. If you don't have it:
### git clone https://github.com/pyenv/pyenv-virtualenv.git $(pyenv root)/plugins/pyenv-virtualenv
eval "$(pyenv virtualenv-init -)"
### ------------------------------------------------------------ ###
PYTHON_VERSION=3.10.11
export PYENV_VERSION=$PYTHON_VERSION
echo "Setting PYENV_VERSION to 3.10.11, using pyenv-virtualenv to create the buildenv..."
echo " --> Deleting .buildenv"
pyenv uninstall -f .buildenv
rm -rf "$HOME/.pyenv/versions/$PYTHON_VERSION/envs/.buildenv"
pyenv virtualenv 3.10.11 .buildenv
pyenv activate .buildenv
else
echo "pyenv is not available. Using system Python version."
if [ -d .buildenv ]; then
echo " --> Deleting .buildenv"
rm -rf .buildenv
fi
virtualenv .buildenv
source .buildenv/bin/activate
fi
pip3 install -e ".[test]"
}
# Overriding this function to deal with the x86 special case
function make_hash_if_necessary {
cd pyinstaller/electron
echo " --> calculate the hash of the binary for download"
specterd_plt_filename=../dist/${specterd_filename}
# early exit
if [[ "$make_hash" != 'True' ]]; then
node ./set-version $version
return 0
fi
# We need to set-versions for two specterd, one arm and one intel.
# arm64 one
node ./set-version $version ${specterd_plt_filename}
# Download and check the intel one
# this needs some env-vars to be set
rm -rf signing_dir/*
PYTHONPATH=../.. python3 -m utils.release_helper downloadgithub
ret_code=$?
if [ $ret_code -ne 0 ]; then
echo "Downloading and verifying x64 specterd failed with exit code $ret_code"
exit $ret_code
fi
if [[ ! -f ./signing_dir/specterd-${version}-osx_x64.zip ]]; then
echo "Downloading and verifying x64 specterd failed as the file does not seem to be there"
exit 1
fi
rm -f /tmp/specterd
unzip ./signing_dir/specterd-${version}-osx_x64.zip -d /tmp
node ./set-version $version /tmp/specterd x64
echo " Hashes in version-data.json $(cat ./version-data.json | jq -r '.sha256')"
echo " Hash of file $(sha256sum ${specterd_plt_filename} )"
echo " Hash of x64 file $(sha256sum /tmp/specterd )"
cd ../..
}
function macos_code_sign {
# prerequisites for this:
# in short:
# * make sure you have a proper app-specific password on https://appleid.apple.com/account/manage
# * collect some information via scrun altool --list-providers -u "<yourAppleID>"
# * create profile via xcrun notarytool store-credentials --apple-id "<YourAppleID>" --password "app-specific-pw" --team-id "seeFromAbove"
# * Call the profile: SpecterProfile
# For details see:
# * https://www.youtube.com/watch?v=2xJcMzoi0EI
# * https://blog.dgunia.de/2022/09/01/switching-from-altool-to-notarytool/
# * https://scriptingosx.com/2021/07/notarize-a-command-line-tool-with-notarytool/
# This creates a ZIP archive from the app package (using the ditto command).
# This ZIP archive is then used to upload the app to the Apple notarization service via xcrun notarytool (formerly xcrun altool)
# After the app has been uploaded to the Apple servers and notarized, the ZIP archive is not used again.
# The function uses the xcrun stapler command to attach the notarization result to the app, and then exits.
# docs:
# https://help.apple.com/itc/apploader/#/apdATD1E53-D1E1A1303-D1E53A1126
# https://keith.github.io/xcode-man-pages/altool.1.html
cd pyinstaller/electron
echo ' --> Attempting to code sign...'
specterimg_filename_fqfn=dist/${dist_mac_folder_name}/${specterimg_filename}.app
echo " executing: ditto -c -k --keepParent "${specterimg_filename_fqfn}" dist/${specterimg_filename}.zip"
ditto -c -k --keepParent "${specterimg_filename_fqfn}" dist/${specterimg_filename}.zip
# upload
echo ' uploading for notarisation ... '
output_json=$(xcrun notarytool submit dist/${specterimg_filename}.zip --apple-id "kneunert@gmail.com" --keychain-profile "SpecterProfile" --output-format json --wait )
# parsing the requestuuid which we'll need to track progress
requestuuid=$(echo $output_json | jq -r '.id')
status=$(echo $output_json | jq -r '.status')
echo "Request ID: $requestuuid"
if [ "$status" = "Invalid" ]; then
mkdir -p signing_logs
echo "issues with notarisation"
xcrun notarytool log ${requestuuid} --keychain-profile SpecterProfile | tee ./signing_logs/${app_name}_${timestamp}_${requestuuid}.log
exit 1
fi
# The stapler somehow "staples" the result of the notarisation in to your app
# see e.g. https://stackoverflow.com/questions/58817903/how-to-download-notarized-files-from-apple
echo " --> Staple the file dist/${dist_mac_folder_name}/${specterimg_filename}.app"
xcrun stapler staple "dist/${dist_mac_folder_name}/${specterimg_filename}.app"
echo ' Successfully Stapled the file'
cd ../..
}
function sub_help {
cat << EOF
### Quick overview
: <<'END_COMMENT'
What do you need to sign the Specter app with Apple's notary service?
- An Apple Developer account
- You must create a signing certificate in your developer account, which will be used to sign your app.
- This certificate must be stored in your keychain on your Mac.
- When you create a signing certificate in your developer account, you will be asked to specify a password for the certificate.
- You can store this password in the keychain, too, so that it - and thus the certificate - can be accessed automatically during the signing process. Like so:
xcrun altool --store-password-in-keychain-item AC_PASSWORD -u '<your apple id>' -p apassword
- As seen above, you need the the xcrun command line tool: This tool is also used to upload your app to the notary service and check the status of the notarization process.
In summary, to sign a macOS app with Apple's notary service, you need an Apple Developer account, a signing certificate, a password for your keychain, the app package to be signed, and the xcrun command line tool.
END_COMMENT
### Prerequisites
# brew install gmp # to prevent module 'embit.util' has no attribute 'ctypes_secp256k1'
# brew install jq
# npm install --global create-dmg
### Trouble shooting
# If you have the common issue "errSecInternalComponent" while signing the code:
# https://medium.com/@ceyhunkeklik/how-to-fix-ios-application-code-signing-error-4818bd331327
# create-dmg issue? Note that there are 2 create-dmg scripts out there. We use:
# https://github.com/sindresorhus/create-dmg
The different "tasks" are now somehow separated from one another.
We have:
* make-hash is rather a flag for the electron-build to incorporate the hash of the specterd
* specterd will trigger the pyinstaller build of the specterd
* electron will build the electron-app
* sign will upload the electron-app to the Apple notary service and get it back notarized
* upload will upload all the binary artifacts to the github-release-page. This includes the creation of the hash-files
and the gnupg signing
### Trouble shooting (Legacy)
# Currently, only MacOS Catalina is supported to build the dmg-file
# Therefore we expect xcode 12.1 (according to google)
# After installation of xcode: sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
# otherwise you get xcrun: error: unable to find utility "altool", not a developer tool or in PATH
# catalina might have a a too old version of bash. You need at least 4.0 or so
# 3.2 is too low definitely
# brew install bash
# Example-call:
./utils/build-osx.sh --debug --version v1.10.0-pre23 --appleid "Kim Neunert (FWV59JHV83)" --mail "kim@specter.solutions" make-hash specterd electron sign upload
EOF
}
appleid=""
while [[ $# -gt 0 ]]
do
arg="$1"
case $arg in
"" | "-h" | "--help")
sub_help
exit 0
shift
;;
--debug)
set -x
DEBUG=true
shift
;;
--version)
version=$2
shift
shift
;;
--appleid)
appleid=$2
shift
shift
;;
--mail)
mail=$2
shift
shift
;;
specterd)
build_specterd=True
shift
;;
make-hash)
make_hash=True
shift
;;
electron)
build_electron=True
shift
;;
sign)
build_sign=True
shift
;;
package)
build_package=True
shift
;;
upload)
upload=True
shift
;;
help)
sub_help
shift
;;
*)
shift
sub_${arg} $@ && ret=0 || ret=$?
if [ "$ret" = 127 ]; then
echo "Error: '$arg' is not a known subcommand." >&2
echo " Run '$progname --help' for a list of known subcommands." >&2
exit 1
else
exit $ret_value
fi
;;
esac
done
echo " --> This build got triggered for version $version"
echo $version > pyinstaller/version.txt
configure
if [[ "$build_specterd" = "True" ]]; then
create_virtualenv_for_pyinstaller
build_pypi_pckgs_and_install
install_build_requirements
cleanup
building_app
fi
if [[ "$make_hash" = "True" ]]; then
# Making the hash only makes sense on a arm arch
if [[ "$ARCH" != "arm64" ]]; then
echo "ERROR: make-hash target should be only called on an arm64 machine on a mac"
exit 1
fi
make_hash_if_necessary
fi
if [[ "$build_electron" = "True" ]]; then
# Making the hash only makes sense on a arm arch
if [[ "$ARCH" != "arm64" ]]; then
echo "ERROR: electron target should be only called on an arm64 machine on a mac"
exit 1
fi
prepare_npm
npm i
if [[ "${appleid}" == '' ]]
then
echo "`jq '.build.mac.identity=null' package.json`" > package.json
else
echo "`jq '.build.mac.identity="'"${appleid}"'"' package.json`" > package.json
fi
building_electron_app
fi
if [[ "$build_sign" = "True" ]]; then
# if [ "$(uname -m)" = "arm64" ]; then
# dist_mac_folder_name=${dist_mac_folder_name}-arm64
# fi
if [[ "$appleid" != '' ]]; then
macos_code_sign
else
echo "WARNING: Forgot to add the appleid ?!"
exit 1
fi
fi
if [[ "$build_package" = "True" ]]; then
echo " --> Preparing the release"
mkdir -p release
rm -rf release/*
# The specterd-zipfile from specterd
if [[ -f pyinstaller/dist/${specterd_filename} ]]; then
echo " --> Making the release-zip for specterd"
pushd pyinstaller/dist # to not preserve folder structure
zip ../../release/${specterd_filename}-${version}-osx_${ARCH}.zip ${specterd_filename}
popd
fi
# The dmg image file from App
if [[ -d pyinstaller/electron/dist/${dist_mac_folder_name}/${specterimg_filename}.app ]]; then
rm -f pyinstaller/electron/dist/*.dmg
echo " --> Creating dmg"
create-dmg pyinstaller/electron/dist/${dist_mac_folder_name}/${specterimg_filename}.app --identity="Developer ID Application: ${appleid}" pyinstaller/electron/dist
# create-dmg doesn't create the prepending "v" to the version
node_comp_version=$(python3 -c "print('$version'[1:])")
mv "pyinstaller/electron/dist/${specterimg_filename} ${node_comp_version}.dmg" dist/${specterimg_filename}-${version}.dmg
echo " --> Copying img file dist/${specterimg_filename}-${version}.dmg"
cp dist/${specterimg_filename}-${version}.dmg release/${specterimg_filename}-${version}.dmg
else
echo "WARNING: Skipping packaging for electron App"
echo "No pyinstaller/electron/dist/${dist_mac_folder_name}/${specterimg_filename}.app has been found."
fi
file=./release/${specterd_filename}-${version}-osx_${ARCH}.zip
if [[ -f $file ]]; then
echo -n " FYI : "
sha256sum $file
fi
file=./release/${specterimg_filename}-${version}.dmg
if [[ -f $file ]]; then
echo -n " FIY : "
sha256sum $file
fi
fi
if [ "$app_name" != "specter" ]; then
# "early" exit
if [[ "$upload" = "True" ]]; then
echo "no upload for app_name $app_name"
exit 1
fi
exit
fi
if [[ "$upload" = "True" ]]; then
echo " --> gpg-signing the hashes and uploading"
. ../../specter_gh_upload.sh # A simple file looks like: export GH_BIN_UPLOAD_PW=...(GH token)
export CI_COMMIT_TAG=$version
if [[ -z "$CI_PROJECT_ROOT_NAMESPACE" ]]; then
echo "WARNING: Why is CI_PROJECT_ROOT_NAMESPACE not set? Setting to cryptoadvance"
export CI_PROJECT_ROOT_NAMESPACE=cryptoadvance
fi
echo " This build: version: $version gh-project: $CI_PROJECT_ROOT_NAMESPACE"
specterd_zip_fqfn=./release/specterd-${version}-osx_${ARCH}.zip
echo " Checking for file $specterd_zip_fqfn"
if [[ -f $specterd_zip_fqfn ]]; then
python3 ./utils/github.py upload $specterd_zip_fqfn
else
echo " WARNING: not uploading as it does not exist: $specterd_zip_fqfn"
fi
specter_dmg_fqfn=./release/Specter-${version}.dmg
echo " Checking for file $specter_dmg_fqfn"
if [[ -f $specter_dmg_fqfn ]]; then
python3 ./utils/github.py upload $specter_dmg_fqfn
else
echo " WARNING: not uploading as it does not exist: $specter_dmg_fqfn"
fi
cd release
# Maybe we have some SHA256SUMS files from other runs lying around. We don't want to shasum them
rm -f SHA256SUMS*
sha256sum * > SHA256SUMS-macos_${ARCH}
python3 ../utils/github.py upload SHA256SUMS-macos_${ARCH}
# The GPG comman below has a timeout. If that's reached, the script will interrupt. So let's make some noise
say "Hello?! Your overlord is speaking! You're now allowed to sign the binary!"
echo "Just in case you missed the timeout, those three last commands are missing:"
echo "cd release"
echo "gpg --detach-sign --armor SHA256SUMS-macos_${ARCH}"
echo "python3 ../utils/github.py upload SHA256SUMS-macos_${ARCH}.asc"
gpg --detach-sign --armor SHA256SUMS-macos_${ARCH}
python3 ../utils/github.py upload SHA256SUMS-macos_${ARCH}.asc
fi

View file

@ -1,169 +0,0 @@
#!/usr/bin/env bash
set -e
# We start in the directory where this script is located
cd "$( dirname "${BASH_SOURCE[0]}" )/."
source build-common.sh
cd ..
# Now in project-root
function sub_help {
cat << EOF
Building various components of specter-desktop
Usage: $build-unix [options] <subcommand>
Options:
--debug
will set -x
--version v1.2.3-pre4
If you don't set the version, CI_COMMIT_TAG will determine the version
Subcommands:
make-hash
will make the hash for the electron-app. This hash will get checked after download
specterd
will build the pyinstaller's specterd binary (linux only)
electron-linux
will build the linux binary of the electron-app
electron-win
will build the win binary of the electron-app. The specterd.exe will get downloaded from
github.com/\$CI_PROJECT_ROOT_NAMESPACE/specter-desktop...
This need a wine-environment. See the docker-image electron-builder
Example-call:
./build-unix.sh --debug --version v1.7.0-pre1 make-hash specterd electron-linux
EOF
}
function create_release_zip_linux {
echo " --> Making the release-zip"
# consists of specterd and Specter-version.AppImage
mkdir -p release
# first the specterd
cd pyinstaller/dist
cp -r ../../udev ./udev
echo "Don't forget to set up udev rules! Check out udev folder for instructions." > README.md
zip -r ../../release/${specterd_filename}-"$version"-"$(uname -m)"-linux-gnu.zip ${specterd_filename} udev README.md
echo $app_name
# now the AppImage
cd ../electron/dist
cp -r ../../../udev ./udev
echo "Don't forget to set up udev rules! Check out udev folder for instructions." > README.md
tar -czvf ../../../release/${pkg_filename}-"$version"-"$(uname -m)"-linux-gnu.tar.gz ${app_name^}-* udev README.md
cd ../../..
}
function prepare_building_electron_app_win {
cd pyinstaller/dist
echo " --> Downloading the windows-version of specterd for version $version"
wget --progress=dot -e dotbytes=10M https://github.com/${CI_PROJECT_ROOT_NAMESPACE}/specter-desktop/releases/download/${version}/specterd-${version}-win64.zip -O ./specterd.zip
unzip specterd.zip
cd ../electron
rm -rf dist/
cd ../..
}
version=$CI_COMMIT_TAG
echo " --> Assume gitlab-project: ${CI_PROJECT_ROOT_NAMESPACE}"
[ -z "${CI_PROJECT_ROOT_NAMESPACE:+x}" ] && \
echo " Redefining CI_PROJECT_ROOT_NAMESPACE=cryptoadvance " && \
export CI_PROJECT_ROOT_NAMESPACE=cryptoadvance
while [[ $# -gt 0 ]]
do
arg="$1"
case $arg in
"" | "-h" | "--help")
sub_help
exit 0
shift
;;
--debug)
set -x
DEBUG=true
shift
;;
--version)
version=$2
shift
shift
if [ -n "$CI_COMMIT_TAG" ]; then
if [ "$version" != "$CI_COMMIT_TAG" ]; then
echo "ERROR: Cannot set version to something different than CI_COMMIT_TAG env-var if that var is set. "
exit 1
fi
fi
;;
specterd)
build_specterd=True
shift
;;
make-hash)
make_hash=True
shift
;;
electron-linux)
build_electron_linux=True
shift
;;
electron-win)
build_electron_win=True
shift
;;
help)
sub_help
shift
;;
*)
shift
sub_${arg} $@ && ret=0 || ret=$?
if [ "$ret" = 127 ]; then
echo "Error: '$arg' is not a known subcommand." >&2
echo " Run '$progname --help' for a list of known subcommands." >&2
exit 1
else
exit $ret_value
fi
;;
esac
done
if [[ "$version" = "" ]]; then
echo "ERROR: version could not be determined (--version or CI_COMMIT_TAG)"
exit 1
fi
echo " --> This build got triggered for version $version"
# This file gets further packaged up with the pyinstaller and will help specter to figure out which version it's running on
echo $version > pyinstaller/version.txt
configure
if [[ "$build_specterd" = "True" ]]; then
create_virtualenv_for_pyinstaller
build_pypi_pckgs_and_install
install_build_requirements
cleanup
building_app
fi
if [[ "$build_electron_linux" = "True" ]]; then
prepare_npm
make_hash_if_necessary
building_electron_app linux
create_release_zip_linux
fi
if [ "$build_electron_win" = "True" ]; then
prepare_building_electron_app_win
make_hash_if_necessary win
building_electron_app win
cp pyinstaller/electron/dist/Specter\ Setup\ *.exe release/Specter-Setup-$version.exe
fi

View file

@ -20,7 +20,7 @@ logger = logging.getLogger(__name__)
@click.option("--delimiter", default=",", help="the delimiter")
@click.argument("spec_file")
def execute(debug, run, delimiter, spec_file):
with open("cypress.json") as json_file:
with open("cypress-tests.json") as json_file:
data = json.load(json_file)
spec_create_list = []

View file

@ -1,16 +0,0 @@
#!/bin/bash
cat > ~/.python-gitlab.cfg << EOF
[global]
default = specterdesktop
ssl_verify = true
timeout = 5
[specterdesktop]
url = https://gitlab.com
#private_token = ${CI_JOB_TOKEN}
job_token =${CI_JOB_TOKEN}
api_version = 4
EOF

View file

@ -1,532 +0,0 @@
""" We assume that this script is running on a gitlab-runner and therefore has some variables set.
Specifically:
CI_PROJECT_ROOT_NAMESPACE=k9ert
CI_COMMIT_TAG=v0.9.6-pre2
"""
import logging
import os
import sys
from pathlib import Path
import requests
import argparse
import collections
import getpass
import json
import logging
import os
import re
import subprocess
import sys
from typing import (
cast,
Any,
Callable,
List,
Optional,
) # noqa: F401 # pylint: disable=unused-import
try:
# Allow an import of this module without `requests` and `yacl` being installed for meta data queries
# (e.g. version information)
import requests
from yacl import setup_colored_stderr_logging
except ImportError:
pass
logging.basicConfig(format="%(levelname)s:%(message)s", level=logging.INFO)
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
github_api_root_url = f"https://api.github.com"
github_username = "gitlab_upload_release_binaries"
def main():
if sys.argv[1] != "upload":
# Maybe something more fancy in the future:
logger.error("Command {sys.argv[1]} not found! Only 'upload' right now")
exit(2)
artifact = sys.argv[2]
if not Path(artifact).exists():
logger.error(f"local artifact {artifact} does not exist.")
exit(2)
if not "CI_PROJECT_ROOT_NAMESPACE" in os.environ:
logger.error("CI_PROJECT_ROOT_NAMESPACE not found")
exit(2)
else:
project = f"{os.environ['CI_PROJECT_ROOT_NAMESPACE']}/specter-desktop"
if not "CI_COMMIT_TAG" in os.environ:
logger.error("CI_COMMIT_TAG not found")
exit(2)
else:
tag = os.environ["CI_COMMIT_TAG"]
if len(sys.argv) != 3:
logger.error("argument artifact not found.")
if artifact_exists(project, tag, Path(artifact).name):
logger.info("Github artifact existing. Skipping upload.")
exit(0)
else:
logger.info(f"Github artifact {artifact} does not exist. Let's upload!")
if not "GH_BIN_UPLOAD_PW" in os.environ:
logger.error("GH_BIN_UPLOAD_PW not found.")
else:
password = os.environ["GH_BIN_UPLOAD_PW"]
publish_release_from_tag(
project,
tag,
[artifact],
github_username,
password,
)
def artifact_exists(project, tag, artifact):
artifact_url = f"https://github.com/{project}/releases/download/{tag}/{artifact}"
logger.debug(f"checking for artifact url {artifact_url}")
r = requests.head(artifact_url)
if r.status_code != 302:
return False
else:
return True
__copyright__ = "Copyright © 2019 Forschungszentrum Jülich GmbH. All rights reserved."
__license__ = "MIT"
__version_info__ = (0, 1, 5)
__version__ = ".".join(map(str, __version_info__))
DEFAULT_GITHUB_ROOT = "github.com"
class MissingDependencyError(Exception):
pass
class FileCommandError(Exception):
pass
class InvalidFileCommandOutputError(Exception):
pass
class NoTagsAvailableError(Exception):
pass
class HTTPError(Exception):
pass
class JSONError(Exception):
pass
class InvalidUploadUrlError(Exception):
pass
class InvalidServerNameError(Exception):
pass
class MissingProjectError(Exception):
pass
class MissingTagError(Exception):
pass
class CredentialsReadError(Exception):
pass
class AttributeDict(dict): # type: ignore
def __getattr__(self, attr: str) -> Any:
return self[attr]
def __setattr__(self, attr: str, value: Any) -> None:
self[attr] = value
Release = collections.namedtuple("Release", ["id", "asset_upload_url"])
Asset = collections.namedtuple("Asset", ["id", "name"])
def setup_stderr_logging() -> None:
logging.basicConfig(level=logging.INFO)
setup_colored_stderr_logging(format_string="[%(levelname)s] %(message)s")
def get_mimetype(filepath: str) -> str:
if not os.path.isfile(filepath):
raise FileNotFoundError(
'The file "{}" does not exist or is not a regular file.'.format(filepath)
)
if not os.access(filepath, os.R_OK):
raise PermissionError('The file "{}" is not readable.'.format(filepath))
if os.name == "nt":
try:
import mimetypes
mime_type = mimetypes.types_map[f".{filepath.split('.')[-1]}"]
except ModuleNotFoundError:
raise Exception(
"mimetypes module not found. Do something like pip install mimetypes"
)
else:
try:
file_command_output = subprocess.check_output(
["file", "--mime", filepath], universal_newlines=True
) # type: str
mime_type = file_command_output.split()[1][:-1]
except subprocess.CalledProcessError as e:
raise FileCommandError(
"The `file` command returned with exit code {:d}".format(e.returncode)
)
except IndexError:
raise InvalidFileCommandOutputError(
'The file command output "{}" could not be parsed.'.format(
file_command_output
)
)
return mime_type
def strip_asset_upload_url(asset_upload_url_with_get_params: str) -> str:
match_obj = re.match(r"([^{]+)(?:\{.*\})?", asset_upload_url_with_get_params)
if not match_obj:
raise InvalidUploadUrlError(
'The upload url "{}" is not in the expected format.'.format(
asset_upload_url_with_get_params
)
)
asset_upload_url = match_obj.group(1) # type: str
return asset_upload_url
class GithubConnection:
def __init__(self, project):
self.github_api_root_url = github_api_root_url
self.project = project
self.username = github_username
self.password = os.environ["GH_BIN_UPLOAD_PW"]
def fetch_existing_release(self, tag) -> Optional[Release]:
try:
release_query_url = "{}/repos/{}/releases/tags/{}".format(
self.github_api_root_url, self.project, tag
)
response = requests.get(
release_query_url,
auth=(self.username, self.password),
headers={"Accept": "application/json"},
)
response.raise_for_status()
logger.info(
'Fetched the existing release "%s" in the GitHub repository "%s"',
tag,
self.project,
)
response_json = response.json()
asset_upload_url_with_get_params = response_json["upload_url"]
asset_upload_url = strip_asset_upload_url(asset_upload_url_with_get_params)
release = Release(response_json["id"], asset_upload_url)
return release
except requests.HTTPError as e:
if e.response.status_code == 404:
return None
raise HTTPError(
'Could not fetch the release "{}" due to a severe HTTP error.'.format(
tag
)
)
def list_assets(self, release: Release) -> List[Asset]:
try:
asset_list_url = "{}/repos/{}/releases/{}/assets".format(
self.github_api_root_url, self.project, release.id
)
response = requests.get(asset_list_url, auth=(self.username, self.password))
response.raise_for_status()
assets = [
Asset(asset_dict["id"], asset_dict["name"])
for asset_dict in response.json()
]
return assets
except requests.HTTPError:
raise HTTPError(
'Could not get a list of assets for project "{}".'.format(self.project)
)
except json.decoder.JSONDecodeError:
raise JSONError("Got an invalid json string.")
except KeyError as e:
raise JSONError(
'Got an unexpected json object missing the key "{}".'.format(e.args[0])
)
def download_artifact(self, tag, artifact, target_dir="."):
artifact_url = (
f"https://github.com/{self.project}/releases/download/{tag}/{artifact}"
)
response = requests.get(artifact_url)
# If the HTTP GET request can be served
if response.status_code == 200:
# Write the file contents in the response to a file specified by local_file_path
with open(os.path.join(target_dir, artifact), "wb") as local_file:
for chunk in response.iter_content(chunk_size=128):
local_file.write(chunk)
else:
raise Exception(
f"Status-code {response.status_code} for url {artifact_url}"
)
def publish_release_from_tag(
project: str,
tag: Optional[str],
asset_filepaths: List[str],
username: str,
password: str,
dry_run: bool = False,
) -> None:
if "requests" not in sys.modules:
raise MissingDependencyError(
'The "requests" package is missing. Please install and run again.'
)
def fetch_latest_tag() -> str:
try:
tags_url = "{}/repos/{}/tags".format(github_api_root_url, project)
response = requests.get(
tags_url,
auth=(username, password),
headers={"Accept": "application/json"},
)
response.raise_for_status()
tags = response.json()
if not tags:
raise NoTagsAvailableError(
'The given repository "{}" has no tags yet.'.format(project)
)
latest_tag = tags[0]["name"] # type: str
logger.info(
'Fetched the latest tag "%s" from the GitHub repository "%s"',
latest_tag,
project,
)
return latest_tag
except requests.HTTPError:
raise HTTPError(
'Could not query the latest tag of the repository "{}" due to a http error.'.format(
project
)
)
except (json.decoder.JSONDecodeError, IndexError):
raise JSONError("Got an invalid json string.")
except KeyError as e:
raise JSONError(
'Got an unexpected json object missing the key "{}".'.format(e.args[0])
)
def publish_release(tag: str) -> Release:
def fetch_existing_release() -> Optional[Release]:
try:
release_query_url = "{}/repos/{}/releases/tags/{}".format(
github_api_root_url, project, tag
)
response = requests.get(
release_query_url,
auth=(username, password),
headers={"Accept": "application/json"},
)
response.raise_for_status()
logger.info(
'Fetched the existing release "%s" in the GitHub repository "%s"',
tag,
project,
)
response_json = response.json()
asset_upload_url_with_get_params = response_json["upload_url"]
asset_upload_url = strip_asset_upload_url(
asset_upload_url_with_get_params
)
release = Release(response_json["id"], asset_upload_url)
return release
except requests.HTTPError as e:
if e.response.status_code == 404:
return None
raise HTTPError(
'Could not fetch the release "{}" due to a severe HTTP error.'.format(
tag
)
)
def create_release() -> Release:
try:
release_creation_url = "{}/repos/{}/releases".format(
github_api_root_url, project
)
response = requests.post(
release_creation_url,
auth=(username, password),
json={
"tag_name": tag,
"name": tag,
"body": "",
"draft": False,
"prerelease": False,
},
)
response.raise_for_status()
logger.info(
'Created the release "%s" in the GitHub repository "%s"',
tag,
project,
)
response_json = response.json()
asset_upload_url_with_get_params = response_json["upload_url"]
asset_upload_url = strip_asset_upload_url(
asset_upload_url_with_get_params
)
release = Release(response_json["id"], asset_upload_url)
return release
except requests.HTTPError:
raise HTTPError('Could not create the release "{}".'.format(tag))
except json.decoder.JSONDecodeError:
raise JSONError("Got an invalid json string.")
except KeyError as e:
raise JSONError(
'Got an unexpected json object missing the key "{}".'.format(
e.args[0]
)
)
release = fetch_existing_release()
if release is None:
release = create_release()
return release
def list_assets(release: Release) -> List[Asset]:
try:
asset_list_url = "{}/repos/{}/releases/{}/assets".format(
github_api_root_url, project, release.id
)
response = requests.get(asset_list_url, auth=(username, password))
response.raise_for_status()
assets = [
Asset(asset_dict["id"], asset_dict["name"])
for asset_dict in response.json()
]
return assets
except requests.HTTPError:
raise HTTPError(
'Could not get a list of assets for project "{}".'.format(project)
)
except json.decoder.JSONDecodeError:
raise JSONError("Got an invalid json string.")
except KeyError as e:
raise JSONError(
'Got an unexpected json object missing the key "{}".'.format(e.args[0])
)
def delete_asset(asset: Asset) -> None:
try:
asset_delete_url = "{}/repos/{}/releases/assets/{}".format(
github_api_root_url, project, asset.id
)
response = requests.delete(asset_delete_url, auth=(username, password))
response.raise_for_status()
logger.info(
'Deleted the asset "%s" attached to release "%s" of the GitHub repository "%s"',
asset.name,
tag,
project,
)
except requests.HTTPError:
raise HTTPError(
'Could not get a list of assets for project "{}".'.format(project)
)
except json.decoder.JSONDecodeError:
raise JSONError("Got an invalid json string.")
except KeyError as e:
raise JSONError(
'Got an unexpected json object missing the key "{}".'.format(e.args[0])
)
def upload_asset(release: Release, asset_filepath: str) -> None:
asset_filename = os.path.basename(asset_filepath)
try:
asset_mimetype = get_mimetype(asset_filepath)
with open(asset_filepath, "rb") as f:
response = requests.post(
"{}?name={}".format(release.asset_upload_url, asset_filename),
auth=(username, password),
data=f,
headers={"Content-Type": asset_mimetype},
)
response.raise_for_status()
logger.info(
'Uploaded the asset "%s" attached to release "%s" of the GitHub repository "%s"',
asset_filename,
tag,
project,
)
except requests.HTTPError:
raise HTTPError('Could not upload the asset "{}".'.format(asset_filename))
if tag is None:
logger.info(
'No tag given, fetching the latest tag from the GitHub repository "%s"',
project,
)
tag = fetch_latest_tag()
if dry_run:
logger.info(
'Would create the release "%s" in the GitHub repository "%s"', tag, project
)
assets = [] # type: List[Asset]
else:
release = publish_release(tag)
assets = list_assets(release)
for asset_filepath in asset_filepaths:
asset_matches = [
asset for asset in assets if asset.name == os.path.basename(asset_filepath)
]
if dry_run:
for asset_match in asset_matches:
logger.info(
'Would delete the asset "%s" attached to release "%s" of the GitHub repository "%s"',
asset_match.name,
tag,
project,
)
logger.info(
'Would upload the asset "%s" attached to release "%s" of the GitHub repository "%s"',
os.path.basename(asset_filepath),
tag,
project,
)
else:
for asset_match in asset_matches:
delete_asset(asset_match)
upload_asset(release, asset_filepath)
if __name__ == "__main__":
main()

View file

@ -1,49 +0,0 @@
#!/bin/bash
# This script prepares the shell so that it can do git-pushes
# It's using the first param as the secret key and the env-var
# KNOWN_HOSTS.
## Install ssh-agent if not already installed, it is required by Docker.
## (change apt-get to yum if you use an RPM-based image)
##
which ssh-agent || ( apk update && apk add --no-cache bash git openssh )
docker info
##
## Run ssh-agent (inside the build environment)
##
eval $(ssh-agent -s)
##
## Add the SSH key stored in SSH_PRIVATE_KEY variable to the agent store
## We're using tr to fix line endings which makes ed25519 keys work
## without extra base64 encoding.
## https://gitlab.com/gitlab-examples/ssh-private-key/issues/1#note_48526556
##
echo "$1" | tr -d '\r' | ssh-add - > /dev/null
##
## Create the SSH directory and give it the right permissions
##
mkdir -p ~/.ssh
chmod 700 ~/.ssh
# Check if Git user email is not set
if [ -z "$(git config --global --get user.email)" ]; then
git config --global user.email "specter@secretvalues"
fi
# Check if Git user name is not set
if [ -z "$(git config --global --get user.name)" ]; then
git config --global user.name "specter"
fi
# Check if KNOWN_HOSTS is set and not empty
if [ -n "$KNOWN_HOSTS" ]; then
# Add KNOWN_HOSTS to known_hosts file
echo "$KNOWN_HOSTS" > ~/.ssh/known_hosts
# Ensure the file permissions are correct
chmod 644 ~/.ssh/known_hosts
fi

View file

@ -1,232 +0,0 @@
#!/bin/bash
# Replacing MacOS utilities with GNU core utilities to make script more robust
# See: https://apple.stackexchange.com/questions/69223/how-to-replace-mac-os-x-utilities-with-gnu-core-utilities
if [[ "$OSTYPE" == "darwin"* ]]; then
brew ls --versions coreutils > /dev/null;
exitCode=$?
if [[ $exitCode == 0 ]]; then
echo "Coreutils are installed via Homebrew, prepending PATH to use GNU core utilities over MacOS utilities."
export PATH="/usr/local/opt/coreutils/libexec/gnubin:$PATH"
else
echo "GNU core utilities not installed. Run brew install coreutils"
fi
fi
ask_yn() {
while true; do
read -p "Is this correct [y/n]" yn
case $yn in
[Yy]* ) return 0 ;;
[Nn]* ) return 1;;
* ) echo "Please answer yes or no.";;
esac
done
}
while [[ $# -gt 0 ]]
do
key="$1"
command="main"
case $key in
wait_on_master)
command=wait_on_master
shift
;;
--help)
help
shift
exit 0
;;
--release-notes)
RELEASE_NOTES="yes"
shift # past value
;;
--dev)
DEV="yes"
shift
;;
--new-version)
new_version=$2
shift
shift
;;
--tag)
TAG="yes"
shift
;;
--debug)
set -x
shift # past argument
;;
*) # unknown option
POSITIONAL="$1" # save it in an array for later
shift # past argument
;;
esac
done
function help() {
# echo HERE_DOC
# ...
echo "not yet implemented"
}
function main() {
# Sed is used as there can be whitespaces
if ! [ "$(git remote -v | grep upstream | grep 'git@github.com:cryptoadvance/specter-desktop.git' | wc -l | sed -e 's/[[:space:]]*//')" = "2" ]; then
echo " --> You don't have the correct upstream-remote. You need this to release. Please do this:"
echo "git remote add upstream git@github.com:cryptoadvance/specter-desktop.git "
exit 2
fi
if ! [ "$(git remote -v | grep origin | grep 'git@github.com:' | wc -l | sed -e 's/[[:space:]]*//')" = "2" ]; then
echo " --> You don't have a reasonable origin-remote. You need this to release (especially with --dev). Please add one!"
exit 2
fi
current_branch=$(git rev-parse --abbrev-ref HEAD)
if [ "$current_branch" != "master" ]; then
echo "You're currently not on the master-branch, exiting"
exit 2
fi
echo " --> Fetching all tags ..."
git fetch upstream --tags
echo " --> git pull upstream master"
git pull upstream master
if [[ -z "$new_version" ]]; then
echo "What should be the new version? Type in please (e.g. v0.9.3 ):"
read new_version
fi
if ! [[ $new_version =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)(-([0-9A-Za-z-]+))?$ ]]; then
echo "version $new_version Does not match the pattern!"
exit 1;
fi
if [[ -n "$RELEASE_NOTES" ]]; then
if [ -z $GH_TOKEN ]; then
echo "Your github-token is missing. Please export them like:"
echo "export GH_TOKEN="
exit 2
fi
latest_version=$(git tag -l "v*" | grep -v 'pre' | grep -v 'dev' | sort -V | tail -1)
echo " --> The latest version is $latest_version. "
if ! ask_yn ; then
echo "Ok, then you type in the latest_version:"
read latest_version
if ! [[ $new_version =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)(-([0-9A-Za-z-]+))?$ ]]; then
echo "Does not match the pattern!"
exit 1;
fi
fi
echo "Here are the release-notes:"
echo "--------------------------------------------------"
echo "# Release Notes" > docs/new_release_notes.md
echo "" >> docs/new_release_notes.md
echo "## ${new_version} $(date +'%B %d, %Y')" >> docs/new_release_notes.md
docker run registry.gitlab.com/cryptoadvance/specter-desktop/github-changelog:latest --github-token $GH_TOKEN --branch master cryptoadvance specter-desktop $latest_version | sort >> docs/new_release_notes.md
echo "" >> docs/new_release_notes.md
cat docs/new_release_notes.md
echo "--------------------------------------------------"
cp docs/release-notes.md docs/release-notes.md.orig
sed -i -e '1,2d' docs/release-notes.md.orig # Assuming the release-Notes start with # Release Notes\n
cat docs/new_release_notes.md docs/release-notes.md.orig > docs/release-notes.md
rm docs/release-notes.md.orig docs/new_release_notes.md
echo "Please check your new File and modify as you find approriate!"
echo "We're waiting here ..."
echo " --> Should we create a PR-branch now? "
if ! ask_yn ; then
echo "break"
#git checkout docs/release-notes.md
exit 2
fi
echo " --> Creating branch ${new_version}_release_notes "
git checkout -b ${new_version}_release_notes
git add docs/release-notes.md
git commit -m "adding release_notes for $new_version"
git push --set-upstream origin ${new_version}_release_notes
echo "Now go ahead and make your PR:"
echo "https://github.com/cryptoadvance/specter-desktop/pulls"
exit 0
fi
if [[ -n "$TAG" ]]; then
echo " --> Should i now create the tag and push the version $new_version ?"
if [ -z $DEV ]; then
echo " --> This will push to your origin-remote!"
else
echo " --> THIS WILL PUSH TO THE UPSTREAM-REMOTE!"
if ! ask_yn ; then
echo "break"
exit 2
fi
fi
git tag $new_version
if [ -z $DEV ]; then
git push origin $new_version
else
git push upstream $new_version
fi
fi
}
function wait_on_master() {
echo "# check status of masterbranch ..."
i=0
# First, wait on the check-runs to be completed:
for i in {1..5} ; do
current_state=$(curl -s https://api.github.com/repos/cryptoadvance/specter-desktop/commits/master/check-runs)
different_states=$(echo $current_state | jq -r '.check_runs[] | select(.status == "completed") | .status' | uniq | wc -l)
status=$(echo $current_state | jq -r '.check_runs[] | select(.status == "completed") | .status' | uniq)
if [[ "$different_states" == 1 ]] && [[ "$status" == "completed" ]] ; then
break
fi
echo "# Builds still running. Will check again in 5 seconds."
sleep 5
done
# Now check all the runs and make sure there are all green:
current_state=$(curl -s https://api.github.com/repos/cryptoadvance/specter-desktop/commits/master/check-runs)
different_conclusions=$(echo $current_state | jq -r '.check_runs[] | select(.conclusion == "success") | .conclusion' | uniq | wc -l)
conclusion=$(echo $current_state | jq -r '.check_runs[] | select(.conclusion == "success") | .conclusion' | uniq)
# We only have one conclusion over all runs:
if [ $different_conclusions -gt 1 ] ; then
echo "# different_conclusions: $different_conclusions"
echo "# Seems that master is not green. Exiting 1"
exit 1
fi
# ... and that conclusion is "success"
if [[ "$conclusion" == "success" ]]; then
echo "# Great, conclusion is success! Exiting 0"
exit 0
fi
echo "# ERROR: I'm confused. This should not happened, exiting 99"
echo "# conclusion = $conclusion"
#echo $current_state
exit 99
}
$command

View file

@ -1,571 +0,0 @@
import hashlib
import logging
import os
import shutil
import subprocess
import sys
import zipfile
from pathlib import Path
from glob import glob
logging.basicConfig(format="%(levelname)s:%(message)s", level=logging.INFO)
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
import os
import subprocess
import hashlib
import logging
import gitlab
logger = logging.getLogger(__name__)
class Sha256sumFile:
"""
A class that provides functionality to manage SHA256 checksums for files within a
specified directory.
Attributes:
name (str): The name of the file that contains the SHA256 checksums for other files.
target_dir (str): The path to the directory where the checksum file and other related
files are stored or will be downloaded to. Defaults to `./signing_dir`.
hashed_files (dict): A dictionary storing file names as keys and their corresponding
SHA256 hashes as values.
The `hashed_files` dictionary data structure is used to map each file name (a string)
to its SHA256 hash (also a string). The SHA256 hash is computed for each corresponding
file present in the `target_dir`, allowing for verification of file integrity by comparing
computed hashes against stored hashes.
"""
def __init__(self, name, target_dir="./signing_dir"):
"""
Initializes a Sha256sumFile instance with the provided checksum file name and target directory.
Parameters:
name (str): The name of the checksum file.
target_dir (str): The directory path where the checksum file and other files are present or downloaded.
"""
self.name = name
self.target_dir = target_dir
self.hashed_files = {}
def is_in_target_dir(self):
"""
Checks if the checksum file is present in the target directory.
Returns:
bool: True if the checksum file is present; False otherwise.
"""
return os.path.isfile(os.path.join(self.target_dir, self.name))
def download_from_tag(self, tag, gc):
"""
Downloads the checksum file and its signature from a specific tag using a given client (gc).
Parameters:
tag (str): The tag associated with the artifacts to be downloaded.
gc (object): The client object which provides the `download_artifact` method for downloading.
"""
gc.download_artifact(tag, self.name, target_dir=self.target_dir)
gc.download_artifact(tag, self.name + ".asc", target_dir=self.target_dir)
self.read()
def download_hashed_files(self, tag, gc):
"""
Downloads all files listed in the hashed_files dictionary from a specific tag using a given client (gc).
Parameters:
tag (str): The tag associated with the artifacts to be downloaded.
gc (object): The client object which provides the `download_artifact` method for downloading.
"""
for file in self.hashed_files.keys():
logger.info(f"Downloading {file} from {tag}")
gc.download_artifact(tag, file, target_dir=self.target_dir)
def read(self):
"""
Reads the checksum file and populates the hashed_files dictionary with file names and their corresponding hashes.
"""
with open(os.path.join(self.target_dir, self.name), "r") as file:
line = file.readline()
while line:
line = line.split(maxsplit=2)
self.hashed_files[line[1]] = line[0]
line = file.readline()
def print(self):
"""
Prints each file's hash and name from the hashed_files dictionary to the standard output.
"""
for hashed_file, hash in self.hashed_files.items():
print(f"{hash} {hashed_file}")
def write(self):
"""
Writes the hashed_files dictionary entries to the checksum file in the target directory.
"""
with open(os.path.join(self.target_dir, self.name), "w") as file:
for hashed_file, hash in self.hashed_files.items():
file.write(f"{hash} {hashed_file}\n")
def add_file(self, file):
"""
Computes the SHA256 hash for a given file and adds the file and its hash to the hashed_files dictionary.
Parameters:
file (str): The filename for which the SHA256 hash should be computed and added.
"""
self.hashed_files[file] = Sha256sumFile.sha256_checksum(file, self.target_dir)
def check_hashes(self):
"""
Verifies the integrity of the files by checking their SHA256 hashes against the entries in the checksum file.
Raises:
Exception: If the verification of any file fails, an exception is raised with
the subprocess output that caused the failure.
"""
try:
subprocess.run(
["sha256sum", "-c", self.name], cwd=self.target_dir, check=True
)
except subprocess.CalledProcessError as e:
raise Exception(
f"Could not validate hashes for file {self.name}: {e.output}"
)
def check_sig(self):
"""
Verifies the signature of the checksum file using gpg.
Raises:
Exception: If the verification of the file signature fails, an exception is raised.
"""
returncode = subprocess.call(
["gpg", "--verify", self.name + ".asc"], cwd=self.target_dir
)
if returncode != 0:
raise Exception(f"Could not validate signature of file {self.name}")
@classmethod
def sha256_checksum(cls, filename, folder, block_size=65536):
"""
Computes the SHA256 hash of a given file.
Parameters:
filename (str): The name of the file for which to compute the SHA256 hash.
folder (str): The path to the directory containing the file.
block_size (int): The block size used for reading the file. Defaults to 65536.
Returns:
str: The SHA256 hash of the file.
"""
sha256 = hashlib.sha256()
with open(os.path.join(folder, filename), "rb") as f:
for block in iter(lambda: f.read(block_size), b""):
sha256.update(block)
return sha256.hexdigest()
class ReleaseHelper:
"""
A class that manages software build artifacts for a CI/CD pipeline.
This class is designed to perform operations such as downloading artifacts from CI
pipelines, verifying SHA256 checksums, verifying GPG signatures, and uploading
artifacts to GitHub releases.
The class relies on a number of environment variables being present:
CI_COMMIT_TAG: The git tag to work with (format: export CI_COMMIT_TAG=<tag_name>).
CI_PIPELINE_ID: The pipeline ID for which artifacts are managed
(format: export CI_PIPELINE_ID=<pipeline_id>).
CI_PROJECT_ROOT_NAMESPACE: The root namespace of the CI project
(required for uploading to GitHub).
GH_BIN_UPLOAD_PW: gh_token or token for GitHub to authenticate uploads.
Attributes:
target_dir (str): The directory path where artifacts are to be managed.
tag (str): The git tag associated with the artifacts being managed.
pipeline_id (str): The CI pipeline ID for artifact management operations.
pipeline (Pipeline): A pipeline object fetched from the CI server.
github_project (str): The GitHub repository in which the release should be created or updated.
gh_token (str): The password or token used to authenticate with GitHub.
Methods:
download_and_unpack_all_artifacts(): Downloads and unpacks artifacts from a CI pipeline.
download_and_unpack_new_artifacts_from_github():
Downloads and unpacks new artifacts from GitHub.
create_sha256sum_file(): Creates a SHA256SUMS file with checksums of all artifacts.
check_all_hashes(): Verifies checksums for all artifacts.
check_all_sigs(): Verifies GPG signatures for all artifacts.
calculate_publish_params(): Calculates and validates necessary parameters for publishing.
upload_sha256sum_file(): Uploads the SHA256SUMS file to a GitHub release.
upload_sha256sumsig_file(): Uploads the SHA256SUMS.asc signature file to a GitHub release.
Note: The actual implementation of the methods and the use of additional classes
like `github.GithubConnection` or `Sha256sumFile` are assumed to exist and are
not defined in this documentation.
"""
def __init__(self):
self.target_dir = "signing_dir"
Path(self.target_dir).mkdir(parents=True, exist_ok=True)
@property
def gl(self):
"""https://python-gitlab.readthedocs.io/en/stable/api-usage.html"""
if hasattr(self, "_gl"):
return self._gl
if os.environ.get("GITLAB_PRIVATE_TOKEN"):
logger.info("Using GITLAB_PRIVATE_TOKEN")
self._gl = gitlab.Gitlab(
"http://gitlab.com",
private_token=os.environ.get("GITLAB_PRIVATE_TOKEN"),
)
elif os.environ.get("CI_JOB_TOKEN"):
logger.info("Using CI_JOB_TOKEN")
self._gl = gitlab.Gitlab(
"http://gitlab.com", job_token=os.environ["CI_JOB_TOKEN"]
)
else:
raise Exception(
"Can't authenticate against Gitlab ( export GITLAB_PRIVATE_TOKEN )"
)
return self._gl
@property
def gitlab_project(self):
if hasattr(self, "_gitlab_project"):
return self._gitlab_project
try:
from gitlab.v4.objects import Project
self._gitlab_project: Project = self.gl.projects.get(self.ci_project_id)
except gitlab.exceptions.GitlabAuthenticationError as e:
logger.fatal(e)
logger.error("Your token might be expired or wrong. Get a new one here:")
logger.error(" https://gitlab.com/-/profile/personal_access_tokens")
exit(2)
if (
self._gitlab_project.attributes["namespace"]["path"]
!= self.ci_project_root_namespace
):
logger.fatal(
f"project_root_namespace ({ self.ci_project_root_namespace }) does not match namespace of Project ({self._gitlab_project.attributes['namespace']['path']}) "
)
logger.error("You might want to: unset CI_PROJECT_ID")
exit(2)
return self._gitlab_project
@property
def ci_project_id(self):
if hasattr(self, "_ci_project_id"):
return self._ci_project_id
if os.environ.get("CI_PROJECT_ID"):
self._ci_project_id = os.environ.get("CI_PROJECT_ID")
logger.info(f"Using ci_project_id: {self._ci_project_id} ")
else:
logger.error("No Project given. choose one:")
for project in self.gl.projects.list(search="specter-desktop"):
logger.info(
f" export CI_PROJECT_ID={project.id} # {project.name_with_namespace}"
)
if project.name_with_namespace.startswith("cryptoadvance"):
self._ci_project_id = project.id
logger.warning(
f"{self._ci_project_id} has been chosen as self._ci_project_id"
)
return self._ci_project_id
@property
def github_project(self):
if hasattr(self, "_github_project"):
return self._github_project
self._github_project = f"{self.ci_project_root_namespace}/specter-desktop"
logger.info(f"Using github_project: {self._github_project}")
return self._github_project
@property
def gh_token(self):
if hasattr(self, "_gh_token"):
return self._gh_token
if os.environ.get("GH_BIN_UPLOAD_PW"):
self._gh_token = os.environ.get("GH_BIN_UPLOAD_PW")
logger.info(f"Using gh_token: REDACTED")
else:
raise Exception(
"no Github token given ( export GH_BIN_UPLOAD_PW=v0.0.0.0-pre13 )"
)
return self._gh_token
@property
def ci_commit_tag(self):
if hasattr(self, "_ci_commit_tag"):
return self._ci_commit_tag
if os.environ.get("CI_COMMIT_TAG"):
self._ci_commit_tag = os.environ.get("CI_COMMIT_TAG")
else:
raise Exception("no tag given ( export CI_COMMIT_TAG=v0.0.0.0-pre13 )")
logger.info(f"Using tag: {self._ci_commit_tag}")
return self._ci_commit_tag
@property
def ci_project_root_namespace(self):
if hasattr(self, "_ci_project_root_namespace"):
return self._ci_project_root_namespace
if os.environ.get("CI_PROJECT_ROOT_NAMESPACE"):
self._ci_project_root_namespace = os.environ.get(
"CI_PROJECT_ROOT_NAMESPACE"
)
logger.info(
f"Using project_root_namespace: {self._ci_project_root_namespace} ( export CI_PROJECT_ROOT_NAMESPACE={self._ci_project_root_namespace} )"
)
else:
self._ci_project_root_namespace = "cryptoadvance"
logger.warning(
f"Using project_root_namespace: {self._ci_project_root_namespace} ( export CI_PROJECT_ROOT_NAMESPACE={self._ci_project_root_namespace} )"
)
return self._ci_project_root_namespace
@property
def ci_pipeline_id(self):
if hasattr(self, "_ci_pipeline_id"):
return self._ci_pipeline_id
if os.environ.get("CI_PIPELINE_ID"):
self._ci_pipeline_id = os.environ.get("CI_PIPELINE_ID")
else:
logger.info(
"no CI_PIPELINE_ID given, trying to find an appropriate one ..."
)
pipelines = self.gitlab_project.pipelines.list()
for pipeline in pipelines:
if pipeline.ref == self.ci_commit_tag:
self._ci_pipeline_id = pipeline.id
self._ci_pipeline = pipeline
logger.info(f"Found matching pipeline: {pipeline}")
if not hasattr(self, "_ci_pipeline"):
logger.error(
f"Could not find tag {self.ci_commit_tag} in the pipeline-refs:"
)
for pipeline in self.gitlab_project.pipelines.list():
logger.error(pipeline.ref)
raise Exception(
"no CI_PIPELINE_ID given ( export CI_PIPELINE_ID= ) or maybe you're on the wrong project ( export CI_PROJECT_ROOT_NAMESPACE= )"
)
logger.info(f"Using pipeline_id: {self.ci_pipeline.id}")
return self._ci_pipeline_id
@property
def ci_pipeline(self):
if hasattr(self, "_ci_pipeline"):
return self._ci_pipeline
self._ci_pipeline = self.gitlab_project.pipelines.get(self.ci_pipeline_id)
return self._ci_pipeline
def download_and_unpack_all_artifacts(self):
if os.path.isdir(self.target_dir):
logger.info(f"First purging {self.target_dir}")
shutil.rmtree(self.target_dir)
for job in self.ci_pipeline.jobs.list():
if job.name in [
"release_electron_linux_windows",
"release_binary_windows",
"release_pip",
]:
zipfn = f"/tmp/_artifacts_{job.name}.zip"
job_obj = self.gitlab_project.jobs.get(job.id, lazy=True)
if not os.path.isfile(zipfn):
logger.info(f"Downloading artifacts for {job.name}")
with open(zipfn, "wb") as f:
job_obj.artifacts(streamed=True, action=f.write)
else:
logger.info(f"Skipping Download artifacts for {job.name}")
logger.info(f"Unzipping {zipfn} in target-folder")
with zipfile.ZipFile(zipfn, "r") as zip:
for zip_info in zip.infolist():
if zip_info.filename[-1] == "/":
continue
zip_info.filename = os.path.basename(zip_info.filename)
logger.info(f" Extracting {zip_info.filename}")
zip.extract(zip_info, self.target_dir)
def download_and_unpack_new_artifacts_from_github(self):
gc = github.GithubConnection(self.github_project)
release = gc.fetch_existing_release(self.ci_commit_tag)
assets = gc.list_assets(release)
for asset in assets:
if not asset.name.startswith("SHA256"):
continue
if asset.name == "SHA256SUMS" or asset.name == "SHA256SUMS.asc":
continue
if asset.name.endswith(".asc"):
continue
logger.info("iterating file " + asset.name)
shasumfile = Sha256sumFile(asset.name)
if not shasumfile.is_in_target_dir():
shasumfile.download_from_tag(self.ci_commit_tag, gc)
shasumfile.download_hashed_files(self.ci_commit_tag, gc)
shasumfile.check_hashes()
shasumfile.check_sig()
logger.info("All files have valid signatures")
def create_sha256sum_file(self):
with open(f"{self.target_dir}/SHA256SUMS", "w") as shafile:
for file in os.listdir(self.target_dir):
if file.startswith("SHA256SUMS-") and not file.endswith(".asc"):
logger.debug(f"Processing {file}")
sha_src = Sha256sumFile(file)
sha_src.read()
for hashed_file in sha_src.hashed_files.keys():
print(f"{sha_src.hashed_files[hashed_file]} {hashed_file}\n")
shafile.write(
f"{sha_src.hashed_files[hashed_file]} {hashed_file}\n"
)
returncode = subprocess.call(
["sha256sum", "-c", "SHA256SUMS"], cwd=self.target_dir
)
if returncode != 0:
raise Exception(
f"One of the hashes is not matching: {subprocess.run(['sha256sum', '-c', 'SHA256SUMS'], cwd=self.target_dir)}"
)
def check_all_hashes(self):
for file in os.listdir(self.target_dir):
if file.startswith("SHA256SUM") and not file.endswith(".asc"):
logger.info(f"Checking hashes in {file}")
if file.endswith("windows"):
logger.info(f"Converting dos2unix for {file}")
dos2unix(os.path.join("signing_dir", file))
returncode = subprocess.call(
["sha256sum", "-c", file], cwd=self.target_dir
)
if returncode != 0:
raise Exception(f"Could not validate hashes for file {file}")
logger.info("All files SHA256SUM* (not .asc) has valid hashes")
def check_all_sigs(self):
for file in os.listdir(self.target_dir):
if file.endswith(".asc"):
logger.info(f"Checking signature for {file}")
returncode = subprocess.call(
["gpg", "--verify", file], cwd=self.target_dir
)
if returncode != 0:
raise Exception(
f"Could not validate signature of file {file}: {subprocess.run(['gpg', '--verify', file], cwd=self.target_dir)}"
)
logger.info("All files *.asc has valid signatures")
def upload_sha256sum_file(self):
artifact = os.path.join("signing_dir", "SHA256SUMS")
if github.artifact_exists(
self.github_project, self.ci_commit_tag, Path(artifact).name
):
logger.info(f"Github artifact {artifact} existing. Skipping upload.")
exit(0)
else:
logger.info(f"Github artifact {artifact} does not exist. Let's upload!")
github.publish_release_from_tag(
self.github_project,
self.ci_commit_tag,
[artifact],
"gitlab_upload_release_binaries",
self.gh_token,
)
def upload_sha256sumsig_file(self):
artifact = os.path.join("signing_dir", "SHA256SUMS.asc")
if github.artifact_exists(
self.github_project, self.ci_commit_tag, Path(artifact).name
):
logger.info(f"Github artifact {artifact} existing. Skipping upload.")
exit(0)
else:
logger.info(f"Github artifact {artifact} does not exist. Let's upload!")
github.publish_release_from_tag(
self.github_project,
self.ci_commit_tag,
[artifact],
"gitlab_upload_release_binaries",
self.gh_token,
)
def dos2unix(filename):
content = ""
outsize = 0
with open(filename, "rb") as infile:
content = infile.read()
with open(filename, "wb") as output:
for line in content.splitlines():
outsize += len(line) + 1
output.write(line + b"\n")
def sha256sum(filenames):
sha_file = Sha256sumFile("SHA256SUMS", target_dir=".")
for filename in filenames:
logger.info(f"Adding {filename}")
sha_file.add_file(filename)
sha_file.print()
if __name__ == "__main__":
if "sha256sums" in sys.argv:
# Used by build-win.ci.bat
sha256sum(sys.argv[2:])
exit(0)
if "install_wheel" in sys.argv:
# List all .whl files in the 'dist' directory
wheel_files = glob(
str(Path("dist", "cryptoadvance.specter-*-py3-none-any.whl"))
)
print("found those wheel files: " + str(wheel_files))
# Loop through the wheel files and install them
for wheel_file in wheel_files:
cmd = f"pip3 install {wheel_file}"
res = os.system(cmd)
print(f"Result of command: {cmd}")
print(res)
# If the installation fails, exit with the error code
if res != 0:
exit(res)
# Exit with a success code if all installations were successful
exit(0)
rh = ReleaseHelper()
try:
from utils import github
except Exception as e:
logger.fatal(e)
logger.error("You might have called this script wrong. Execute it like:")
logger.error("python3 -m utils.release_helper ...")
if "download" in sys.argv:
rh.download_and_unpack_all_artifacts()
if "downloadgithub" in sys.argv:
rh.download_and_unpack_new_artifacts_from_github()
if "checkhashes" in sys.argv:
rh.check_all_hashes()
if "checksigs" in sys.argv:
rh.check_all_sigs()
if "create" in sys.argv:
rh.create_sha256sum_file()
if "upload_shasums" in sys.argv:
rh.upload_sha256sum_file()
if "upload_shasumssig" in sys.argv:
rh.upload_sha256sumsig_file()

View file

@ -1,13 +0,0 @@
#!/bin/bash
set -e
echo "This script will checkout github.com:${CI_PROJECT_ROOT_NAMESPACE}/specterext-dummy.git"
echo "and tag it with ${CI_COMMIT_TAG}"
git clone git@github.com:${CI_PROJECT_ROOT_NAMESPACE}/specterext-dummy.git
cd specterext-dummy
git checkout master
git tag ${CI_COMMIT_TAG}
#git push origin ${CI_COMMIT_TAG}

View file

@ -240,7 +240,7 @@ function restore_snapshot {
# Checking whether spec-files exists
if ! [ -f ./cypress/integration/${spec_file} ]; then
echo "Spec-file $spec_file does not exist, these are the options:"
cat cypress.json | jq -r ".testFiles[]"
cat cypress-tests.json | jq -r ".testFiles[]"
exit 1
fi
snapshot_file=./cypress/fixtures/${spec_file}_btcdir.tar.gz
@ -341,7 +341,11 @@ function sub_run {
start_bitcoind --reset
start_elementsd --reset
start_specter --reset
npx cypress run
# Cypress 10+ normalizes specs discovered from config (often alphabetically),
# but this suite is intentionally stateful and depends on the order in
# cypress-tests.json. Pass an explicit --spec list so a full run keeps the
# legacy order from cypress.json/testFiles.
npx cypress run --spec "$(./utils/calc_cypress_test_spec.py --run spec_empty_specter_home.js)"
fi
}
@ -358,7 +362,7 @@ function sub_snapshot {
# We'll create a snapshot BEFORE this spec-file has been tested:
if [ ! -f ./cypress/integration/$spec_file ]; then
echo "ERROR: Use one of these arguments:"
cat cypress.json | jq -r ".testFiles[]"
cat cypress-tests.json | jq -r ".testFiles[]"
exit 2
fi
start_bitcoind --reset

View file

@ -1,11 +0,0 @@
#!/bin/bash
payload="{\"ref\":\"master\", \"inputs\": {\"tag\": \"${CI_COMMIT_TAG}\"}}"
echo $payload
# This Token is controlled by https://github.com/AaronDewes
curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token ${AARON_TOKEN}" \
https://api.github.com/repos/lncm/docker-specter-desktop/actions/workflows/dispatch.yml/dispatches -d @<(cat <<EOF
$payload
EOF
)

2620
yarn.lock

File diff suppressed because it is too large Load diff