mirror of
https://github.com/cryptoadvance/specter-desktop.git
synced 2026-08-13 12:33:29 +02:00
Compare commits
36 commits
v2.1.8-pre
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bcbae1a32 | ||
|
|
180801223d | ||
|
|
07eb1520dc | ||
|
|
f814ac47c5 | ||
|
|
69a43ef1ea | ||
|
|
6755046e4b | ||
|
|
8843e2f064 | ||
|
|
bf0780d6bc | ||
|
|
7b4d90f917 | ||
|
|
cf491ee5b9 | ||
|
|
9f890a90f8 | ||
|
|
1a3ecf07fb | ||
|
|
426ed39aac | ||
|
|
693338d3af | ||
|
|
3d554fbc2e | ||
|
|
48fea77b8a | ||
|
|
0b027f49fd | ||
|
|
fe232048d4 | ||
|
|
54b3616b40 | ||
|
|
44c5fdeaad | ||
|
|
febcf619d9 | ||
|
|
314a8579a0 | ||
|
|
a1a82082dd | ||
|
|
64139ebdb7 | ||
|
|
9e43afbdd0 | ||
|
|
5a4f24295f | ||
|
|
a793e2dce8 | ||
|
|
6b7013066d | ||
|
|
c183c772a7 | ||
|
|
fdd1cd8f3c | ||
|
|
62ea02657b | ||
|
|
a24df2eb50 | ||
|
|
3e277689ef | ||
|
|
50d81434a8 | ||
|
|
496f3f5d79 | ||
|
|
9780d82d70 |
106 changed files with 9188 additions and 14361 deletions
120
.cirrus.yml
120
.cirrus.yml
|
|
@ -1,120 +0,0 @@
|
|||
container:
|
||||
# image: python:slim
|
||||
# image: ubuntu:jammy
|
||||
# image: python:3.10
|
||||
image: registry.gitlab.com/cryptoadvance/specter-desktop/cirrus-jammy:20230206
|
||||
|
||||
# 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:
|
||||
skip: "false"
|
||||
container:
|
||||
image: registry.gitlab.com/cryptoadvance/specter-desktop/cypress-python-jammy:20230206
|
||||
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."
|
||||
|
||||
91
.github/workflows/electron-smoketest.yml
vendored
Normal file
91
.github/workflows/electron-smoketest.yml
vendored
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
name: Electron smoketest
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'pyinstaller/electron/**'
|
||||
- '.github/workflows/electron-smoketest.yml'
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'pyinstaller/electron/**'
|
||||
- '.github/workflows/electron-smoketest.yml'
|
||||
|
||||
jobs:
|
||||
smoketest:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: pyinstaller/electron/package-lock.json
|
||||
|
||||
- name: Install xvfb and electron runtime libs
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
xvfb \
|
||||
libgtk-3-0 \
|
||||
libnss3 \
|
||||
libasound2t64 \
|
||||
libgbm1 \
|
||||
libxss1 \
|
||||
libxtst6
|
||||
|
||||
- name: Install electron deps
|
||||
working-directory: pyinstaller/electron
|
||||
run: npm ci
|
||||
|
||||
- name: Syntax check all electron JS
|
||||
working-directory: pyinstaller/electron
|
||||
run: |
|
||||
for f in main.js src/*.js; do
|
||||
echo "checking $f"
|
||||
node --check "$f"
|
||||
done
|
||||
|
||||
- name: Seed dev data folder
|
||||
run: |
|
||||
mkdir -p "$RUNNER_TEMP/specter_dev"
|
||||
echo '{"version":"","sha256":{"x64":"","arm64":""}}' \
|
||||
> "$RUNNER_TEMP/specter_dev/version-data.json"
|
||||
|
||||
- name: Boot electron, assert splash reaches no-specterd branch
|
||||
working-directory: pyinstaller/electron
|
||||
env:
|
||||
NODE_ENV: development
|
||||
SPECTER_DATA_FOLDER: ${{ runner.temp }}/specter_dev
|
||||
run: |
|
||||
set -u
|
||||
xvfb-run --auto-servernum --server-args='-screen 0 1280x960x24' \
|
||||
./node_modules/.bin/electron . --no-sandbox > electron.log 2>&1 &
|
||||
PID=$!
|
||||
trap 'kill $PID 2>/dev/null || true; wait $PID 2>/dev/null || true' EXIT
|
||||
|
||||
# Expected log line emitted by uiHelpers.js:updatingLoaderMsg when
|
||||
# main.js reaches the "specterd missing + no version" branch.
|
||||
# If main.js changes this message, update both sides together.
|
||||
EXPECTED="Specterd was not found and no version is configured"
|
||||
|
||||
for i in $(seq 1 30); do
|
||||
if grep -qF "$EXPECTED" electron.log; then
|
||||
echo "smoketest passed (splash reached no-specterd branch)"
|
||||
exit 0
|
||||
fi
|
||||
if ! kill -0 $PID 2>/dev/null; then
|
||||
echo "electron exited before reaching expected state"
|
||||
echo "--- electron.log ---"
|
||||
cat electron.log
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "expected log line not seen within 30s"
|
||||
echo "--- electron.log ---"
|
||||
cat electron.log
|
||||
exit 1
|
||||
71
.github/workflows/release.yml
vendored
71
.github/workflows/release.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
240
.github/workflows/test.yml
vendored
Normal file
240
.github/workflows/test.yml
vendored
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
name: Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
cache: pip
|
||||
cache-dependency-path: requirements.txt
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libusb-1.0-0-dev libudev-dev python3-virtualenv
|
||||
|
||||
- name: Cache bitcoind and elementsd
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
./tests/bitcoin
|
||||
./tests/bitcoin-*
|
||||
./tests/elements
|
||||
./tests/elements-*
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-noded-${{ hashFiles('pyproject.toml', 'tests/elements_gitrev_pinned', 'tests/install_noded.sh', 'tests/bitcoin_SHA256SUMS', 'tests/elements_SHA256SUMS') }}-binary
|
||||
save-always: true
|
||||
|
||||
- name: Install bitcoind
|
||||
run: ./tests/install_noded.sh --debug --bitcoin binary
|
||||
|
||||
- name: Install elementsd
|
||||
run: ./tests/install_noded.sh --debug --elements binary
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
pip install -r requirements.txt --require-hashes
|
||||
pip install -e ".[test]"
|
||||
|
||||
- name: Fetch version tag
|
||||
run: git fetch origin refs/tags/v1.0.0
|
||||
|
||||
- name: Run pytest
|
||||
run: >
|
||||
pytest
|
||||
--cov=cryptoadvance
|
||||
--cov-report=term
|
||||
--junitxml=./testresults.xml
|
||||
-p no:cacheprovider
|
||||
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: testresults
|
||||
path: ./testresults.xml
|
||||
|
||||
cypress:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 30
|
||||
container:
|
||||
image: ghcr.io/cryptoadvance/specter-desktop/cypress-python-jammy@sha256:149f19f9534a3932972151d9f0084b386f8beef1b897ff739ad64a962af891dc
|
||||
options: --shm-size=2g
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Fix virtualenv shim
|
||||
shell: bash
|
||||
run: |
|
||||
echo -e '#!/bin/bash\npython3 -m virtualenv "$@"' > /usr/local/bin/virtualenv
|
||||
chmod +x /usr/local/bin/virtualenv
|
||||
virtualenv --version
|
||||
|
||||
- name: Cache bitcoind and elementsd
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
./tests/bitcoin
|
||||
./tests/bitcoin-*
|
||||
./tests/elements
|
||||
./tests/elements-*
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-noded-${{ hashFiles('pyproject.toml', 'tests/elements_gitrev_pinned', 'tests/install_noded.sh', 'tests/bitcoin_SHA256SUMS', 'tests/elements_SHA256SUMS') }}-binary
|
||||
save-always: true
|
||||
|
||||
- name: Install bitcoind
|
||||
run: ./tests/install_noded.sh --debug --bitcoin binary
|
||||
|
||||
- name: Install elementsd
|
||||
run: ./tests/install_noded.sh --debug --elements binary
|
||||
|
||||
- name: Install Python dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
# Container uid differs from checkout owner — trust the repo dir so
|
||||
# setuptools-scm/vcs_versioning can run `git describe` during pip -e.
|
||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
virtualenv --python=python .env
|
||||
source ./.env/bin/activate
|
||||
pip3 install -r requirements.txt --require-hashes
|
||||
pip3 install -e ".[test]"
|
||||
|
||||
- name: Cache npm modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ./node_modules
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- name: Install npm deps
|
||||
run: npm ci
|
||||
|
||||
- name: Run Cypress
|
||||
shell: bash
|
||||
run: |
|
||||
source ./.env/bin/activate
|
||||
./utils/test-cypress.sh --debug run
|
||||
|
||||
- name: Upload Cypress test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cypress-results
|
||||
path: cypresstest-output.xml
|
||||
|
||||
- name: Upload Cypress screenshots
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cypress-screenshots
|
||||
path: cypress/screenshots/**
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: Upload Cypress videos
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cypress-videos
|
||||
path: cypress/videos/**
|
||||
if-no-files-found: ignore
|
||||
|
||||
extension-smoketest:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
cache: pip
|
||||
cache-dependency-path: requirements.txt
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libusb-1.0-0-dev libudev-dev python3-virtualenv
|
||||
|
||||
- name: Cache bitcoind and elementsd
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
./tests/bitcoin
|
||||
./tests/bitcoin-*
|
||||
./tests/elements
|
||||
./tests/elements-*
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-noded-${{ hashFiles('pyproject.toml', 'tests/elements_gitrev_pinned', 'tests/install_noded.sh', 'tests/bitcoin_SHA256SUMS', 'tests/elements_SHA256SUMS') }}-binary
|
||||
save-always: true
|
||||
|
||||
- name: Install bitcoind
|
||||
run: ./tests/install_noded.sh --debug --bitcoin binary
|
||||
|
||||
- name: Install elementsd
|
||||
run: ./tests/install_noded.sh --debug --elements binary
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
virtualenv --python=python .env
|
||||
source ./.env/bin/activate
|
||||
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"
|
||||
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)"
|
||||
sudo apt-get update && sudo apt-get -y install curl
|
||||
curl http://127.0.0.1:25441/svc/cicdtest/ | grep "CicdtestService 4thewin."
|
||||
2
.github/workflows/zblack.yml
vendored
2
.github/workflows/zblack.yml
vendored
|
|
@ -11,7 +11,7 @@ jobs:
|
|||
with:
|
||||
python-version: '3.12' # Pin Python to avoid 3.14 incompatibility with Black 22.3.0
|
||||
# This version should match what you can find in .pre-commit-config.yaml
|
||||
- uses: psf/black@22.3.0 # https://black.readthedocs.io/en/stable/integrations/github_actions.html
|
||||
- uses: psf/black@26.3.0 # https://black.readthedocs.io/en/stable/integrations/github_actions.html
|
||||
with:
|
||||
options: "--check --verbose"
|
||||
src: "./src"
|
||||
|
|
|
|||
13
.gitignore
vendored
13
.gitignore
vendored
|
|
@ -38,7 +38,9 @@ node_modules
|
|||
btcd-conn.json
|
||||
elmd-conn.json
|
||||
tests/bitcoin*
|
||||
!tests/bitcoin_SHA256SUMS
|
||||
tests/elements*
|
||||
!tests/elements_SHA256SUMS
|
||||
token.sh
|
||||
src/cryptoadvance/specter/translations/**/messages.mo
|
||||
src/cryptoadvance/specter/_version.py
|
||||
|
|
@ -50,3 +52,14 @@ cypresstest-output.xml
|
|||
cypress/downloads
|
||||
pyinstaller/electron/output.css
|
||||
pyinstaller/electron/typography.css
|
||||
|
||||
# BMad skill installation and generated artifacts — local only
|
||||
_bmad/
|
||||
_bmad-output/
|
||||
|
||||
# Claude Code local workspace
|
||||
.claude/
|
||||
|
||||
# Local agent workspaces
|
||||
/.agents/
|
||||
/.history/
|
||||
|
|
|
|||
281
.gitlab-ci.yml
281
.gitlab-ci.yml
|
|
@ -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
|
||||
|
||||
27
.worktreeinclude
Normal file
27
.worktreeinclude
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Copied into new worktrees created by `claude --worktree` / EnterWorktree.
|
||||
# Format: .gitignore syntax. Only gitignored files matching these patterns
|
||||
# are copied (never tracked files).
|
||||
|
||||
# Generated version file — avoids re-running setuptools_scm in each worktree.
|
||||
src/cryptoadvance/specter/_version.py
|
||||
|
||||
# Compiled translation catalogs — regenerating requires pybabel and is slow.
|
||||
src/cryptoadvance/specter/translations/**/messages.mo
|
||||
|
||||
# BMad skill installation — copy so new worktrees have skills available without
|
||||
# reinstalling. Gitignored (see .gitignore) so the main repo stays clean.
|
||||
_bmad/
|
||||
|
||||
# Claude Code project skills + local settings — copy so new worktrees inherit them.
|
||||
# NOTE: .claude/worktrees/ is deliberately NOT included (that's where worktrees
|
||||
# themselves live; copying it would recurse).
|
||||
.claude/skills/
|
||||
.claude/settings.local.json
|
||||
|
||||
# NOT included on purpose (too big and/or not portable; recreate per worktree):
|
||||
# .env/ — Python venv (402M, absolute paths in scripts)
|
||||
# .buildenv/ — second Python venv (329M)
|
||||
# pyinstaller/electron/node_modules/ — npm deps (483M, slow but portable;
|
||||
# add if you run electron builds in worktrees)
|
||||
# tests/bitcoin*, tests/elements* — cached regtest node binaries; CI repopulates
|
||||
# _bmad-output/ — local BMad artifacts; regenerate per worktree
|
||||
178
AGENTS.md
178
AGENTS.md
|
|
@ -8,7 +8,7 @@ Specter Desktop is a GUI for Bitcoin Core & Electrum optimized for airgapped har
|
|||
|
||||
**License:** MIT
|
||||
**Stack:** Python 3.9-3.10, Flask, Jinja2 templates, plain JavaScript (no frameworks), JSON file persistence, PyInstaller for desktop builds
|
||||
**Status:** Maintenance mode. Last release v2.1.1 (2025-01-03). ~259 open issues, CI partially broken.
|
||||
**Status:** Reviving. Last release v2.1.1 (2025-01-03). Release pipeline recently migrated to GitHub Actions. See "Current State" section at the bottom.
|
||||
|
||||
## Architecture (Quick Reference)
|
||||
|
||||
|
|
@ -67,126 +67,80 @@ python3 -m cryptoadvance.specter server --config DevelopmentConfig --debug
|
|||
|
||||
## CI/CD
|
||||
|
||||
The project uses **three CI providers** for different purposes, plus private GitLab runners for releases.
|
||||
**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** | Linting, Docker images, TOC generation | `.github/workflows/` | Push, PR |
|
||||
| **Cirrus CI** | Full test suite (pytest + Cypress) | `.cirrus.yml` | PR |
|
||||
| **GitLab CI** | Releasing (binaries, pip, Electron, signatures) | `.gitlab-ci.yml` | Tags only |
|
||||
| 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 (4 workflows)
|
||||
All workflows use public GitHub-hosted runners (`ubuntu-latest` / `ubuntu-22.04` / `windows-latest` / `macos-14`). **No private runners.**
|
||||
|
||||
1. **Black Python Linter** (`.github/workflows/zblack.yml`) — Runs on every push and PR. Checks `./src` with Black 22.3.0. **Currently failing on `master`.**
|
||||
2. **TOC Generator** (`.github/workflows/toc.yml`) — Auto-generates table of contents for README.md, docs/faq.md, docs/development.md on push.
|
||||
3. **Docker Push** (`.github/workflows/docker-push.yml`) — Builds multi-arch Docker image (amd64 + arm64) on every push to any branch. Pushes to `ghcr.io/<owner>/<repo>:<branch>` (upstream: `ghcr.io/cryptoadvance/specter-desktop:<branch>`).
|
||||
4. **Docker Tag** (`.github/workflows/docker-tag.yml`) — Builds multi-arch Docker image on version tags (`v*.*.*`). Pushes to `ghcr.io/<owner>/<repo>:<tag>` (upstream: `ghcr.io/cryptoadvance/specter-desktop:<tag>`).
|
||||
### Test workflow — `test.yml`
|
||||
|
||||
GitHub Actions use standard public runners (`ubuntu-latest` / `ubuntu-24.04`). No private runners needed.
|
||||
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)
|
||||
`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).
|
||||
|
||||
Cirrus CI runs the **full test suite** on PRs. Config: `.cirrus.yml`.
|
||||
### Release pipeline — `.github/workflows/release.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.
|
||||
Triggers on tags matching `v[0-9]+.[0-9]+.[0-9]+` (and `-*` suffixes for pre-releases). Runs entirely on **GitHub-hosted runners** — no private hardware required.
|
||||
|
||||
**Docker images used:**
|
||||
- `registry.gitlab.com/cryptoadvance/specter-desktop/cirrus-jammy:20230206` (pytest)
|
||||
- `registry.gitlab.com/cryptoadvance/specter-desktop/cypress-python-jammy:20230206` (Cypress)
|
||||
**Python version:** pinned to `3.10` via the `PYTHON_VERSION` env var at the top of `release.yml`.
|
||||
|
||||
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.
|
||||
**Jobs (10 total):**
|
||||
|
||||
**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.
|
||||
|
||||
### GitLab CI (Releasing)
|
||||
|
||||
GitLab CI handles the **entire release pipeline**. Config: `.gitlab-ci.yml`. It mirrors the GitHub repo and triggers on tags.
|
||||
|
||||
**Important: GitLab uses private runners, not public shared runners.**
|
||||
|
||||
**Base image:** `registry.gitlab.com/cryptoadvance/specter-desktop/python-bitcoind:v22.0` — a custom image with Python and bitcoind pre-installed.
|
||||
|
||||
**Stages:**
|
||||
|
||||
1. **`testing`** — `check` job: Verifies all GitHub Actions tests are green before proceeding (calls `utils/release.sh wait_on_master`). The actual test jobs (`.test`, `.test-cypress`) are hidden (disabled) since testing moved to Cirrus CI.
|
||||
|
||||
2. **`releasing`** — Builds and uploads release artifacts:
|
||||
- **`release_pip`** — Builds pip package, uploads to PyPI (or test.pypi.org for forks), signs SHA256SUMS
|
||||
- **`release_binary_windows`** — Builds specterd Windows binary using a **Windows GitLab runner** (tag: `windows`). Uses PyInstaller via `pyinstaller/build-win-ci.bat`
|
||||
- **`release_electron_linux_windows`** — Builds Electron desktop apps for Linux and Windows. Uses the Windows specterd from the previous job. Uploads `.exe`, `.tar.gz`, and `.zip` to GitHub Releases
|
||||
|
||||
3. **`post_releasing`** — Final steps:
|
||||
- **`release_signatures`** — Downloads all artifacts, verifies individual SHA256SUMS signatures, creates a combined `SHA256SUMS` file, signs it, uploads to GitHub Releases
|
||||
- **`release_docker`** — Triggers Docker image build
|
||||
- **`tag_specterext_dummy_repo`** — Tags the specterext-dummy repo with the same version
|
||||
- **`update_github`** / **`update_webpage`** — Updates the GitHub release page and the static download page
|
||||
1. **`release-pip`** — Builds the pip package and publishes to PyPI via **trusted publishing** (no `TWINE_PASSWORD` secret needed when configured on PyPI; falls back to token auth otherwise). Only publishes when `github.repository == 'cryptoadvance/specter-desktop'`. Version derived from tag with `-pre` → `rc` PEP 440 mapping.
|
||||
2. **`build-specterd-linux`** — PyInstaller build on `ubuntu-latest`. Produces `specterd-<version>-x86_64-linux-gnu.zip`.
|
||||
3. **`build-specterd-windows`** — PyInstaller build on `windows-latest`. Produces `specterd-<version>-win64.zip`. Installs `colorama` (Windows-only transitive dep of click that isn't in the lock file).
|
||||
4. **`build-specterd-macos`** — PyInstaller build on `macos-14` (Apple Silicon). Produces `specterd-<version>-osx_arm64.zip`. **x86_64 macOS build is commented out** — requires a paid runner (`macos-15-large`); will be enabled when the org has a paid plan.
|
||||
5. **`build-electron-linux`** — Needs `build-specterd-linux`. Downloads specterd artifact, wraps in Electron, produces `specter_desktop-<version>-x86_64-linux-gnu.tar.gz`.
|
||||
6. **`build-electron-windows`** — Needs `build-specterd-windows`. Runs in `electronuserland/builder:wine` container on `ubuntu-latest`. Produces `Specter-Setup-<version>.exe`.
|
||||
7. **`build-electron-macos`** — Needs `build-specterd-macos`. Universal-ish build on `macos-14`. **Code signing is conditional**: if `APPLE_CERTIFICATE_BASE64` secret is set, imports cert into a temporary keychain and signs; otherwise builds unsigned. Removes hardcoded provisioning profile from `package.json` on the fly.
|
||||
8. **`create-release`** — Gathers all artifacts, computes combined `SHA256SUMS`, creates the GitHub Release, uploads binaries.
|
||||
9. **`trigger-docker`** — Triggers the `docker-tag.yml` workflow for the release tag (builds and pushes the Docker image).
|
||||
|
||||
**Release artifacts per version:**
|
||||
- `specterd-<version>-win64.zip` (Windows daemon)
|
||||
- `cryptoadvance.specter-<version>.tar.gz` (pip package, published to PyPI)
|
||||
- `specterd-<version>-x86_64-linux-gnu.zip` (Linux daemon)
|
||||
- `Specter-Setup-<version>.exe` (Windows Electron app)
|
||||
- `specterd-<version>-win64.zip` (Windows daemon)
|
||||
- `specterd-<version>-osx_arm64.zip` (macOS daemon, Apple Silicon)
|
||||
- `specter_desktop-<version>-x86_64-linux-gnu.tar.gz` (Linux Electron app)
|
||||
- `cryptoadvance.specter-<version>.tar.gz` (pip package)
|
||||
- `SHA256SUMS` + `SHA256SUMS.asc` (signed checksums)
|
||||
- macOS builds are **not yet automated** in CI
|
||||
- `Specter-Setup-<version>.exe` (Windows Electron app)
|
||||
- macOS Electron app (produced by `build-electron-macos`)
|
||||
- `SHA256SUMS` (combined checksums, created by `create-release`)
|
||||
|
||||
### Private Runner Setup
|
||||
**macOS builds are now automated** (Apple Silicon free tier). x86_64 macOS requires a paid runner and is disabled.
|
||||
|
||||
Releases are built on private runners only (build-only-on-private-hardware policy).
|
||||
### Release-related secrets (GitHub)
|
||||
|
||||
#### GitLab Runner (Linux)
|
||||
| Secret | Purpose | Required? |
|
||||
|----------|---------|---|
|
||||
| `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 |
|
||||
|
||||
GitLab uses a `gitlab-docker-runner` — jobs run inside Docker containers, but also need access to the Docker socket to spin up bitcoind containers.
|
||||
### Testing a release on a fork
|
||||
|
||||
Setup follows the [Docker socket binding](https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#use-docker-socket-binding) approach. Key implications for tests:
|
||||
- bitcoind gets `-rpcallowip=` for the Docker network
|
||||
- Tests use the Docker network IP (not localhost) to talk to bitcoind
|
||||
|
||||
#### GitLab Runner (Windows)
|
||||
|
||||
Required for building Windows binaries. Prerequisites:
|
||||
- Windows 10+ with WSL2 and Docker Desktop
|
||||
- Python 3.7+, Git, Docker
|
||||
- [GitLab Runner for Windows](https://docs.gitlab.com/runner/install/windows.html)
|
||||
|
||||
Setup:
|
||||
```powershell
|
||||
mkdir \Gitlab-Runner
|
||||
# Download gitlab-runner-windows-amd64.exe, rename to gitlab-runner.exe
|
||||
cd \Gitlab-Runner
|
||||
./gitlab-runner.exe register # Use registration token from GitLab CI/CD settings
|
||||
./gitlab-runner.exe install # Install as system service
|
||||
./gitlab-runner.exe start # Start the runner
|
||||
```
|
||||
|
||||
Tag the runner with `windows` so release jobs can find it. Docker Desktop must be running (requires user login on the machine).
|
||||
|
||||
#### CI/CD Dev Environment (for testing releases)
|
||||
|
||||
To test the release pipeline on your own fork:
|
||||
|
||||
1. Fork `cryptoadvance/specter-desktop` on GitHub
|
||||
2. Create a GitLab project mirroring your GitHub fork ([new CI/CD project](https://gitlab.com/projects/new#cicd_for_external_repo)) — use the **same repo name**: `specter-desktop`
|
||||
3. Activate private runners, deactivate public runners (contact maintainers for access)
|
||||
4. Create tokens and set as GitLab CI/CD variables:
|
||||
- `GH_BIN_UPLOAD_PW` — GitHub token for uploading release assets
|
||||
- `TWINE_PASSWORD` — [test.pypi.org](https://test.pypi.org) API token for pip uploads
|
||||
5. Create a tag on your GitHub fork → watch the test release pipeline run
|
||||
|
||||
### GitLab CI Variables (Secrets)
|
||||
|
||||
| Variable | Purpose |
|
||||
|----------|---------|
|
||||
| `GH_BIN_UPLOAD_PW` | GitHub token for uploading release binaries |
|
||||
| `TWINE_PASSWORD` | PyPI/TestPyPI API token for pip package upload |
|
||||
| `GPG_PASSPHRASE` | GPG key passphrase for signing SHA256SUMS |
|
||||
| `SSH_SPECTEREXT_DEPLOY_KEY` | SSH key for tagging specterext-dummy repo |
|
||||
| `SSH_SPECTERSTATIC_DEPLOY_KEY` | SSH key for updating specter-static download page |
|
||||
1. Fork `cryptoadvance/specter-desktop` on GitHub.
|
||||
2. Push a tag matching `v*.*.*` or `v*.*.*-*` on your fork → `release.yml` fires automatically.
|
||||
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.
|
||||
|
||||
## Testing
|
||||
|
||||
|
|
@ -245,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
|
||||
|
||||
|
|
@ -266,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
|
||||
|
|
@ -283,17 +236,18 @@ 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-02)
|
||||
## Current State (as of 2026-04)
|
||||
|
||||
- **~259 open issues**, many from 2023 (potentially stale)
|
||||
- **10 open PRs** (need assessment)
|
||||
- **CI:** Black linter failing on GitHub Actions; Cirrus CI tests and GitLab release pipeline status unknown (need verification)
|
||||
- **Last release:** v2.1.1 (2025-01-03)
|
||||
- **Goal:** Revive with biweekly tested releases, triage issues, fix CI
|
||||
- **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:** 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.
|
||||
- **Goal:** Revive with biweekly tested releases, shake out the new release pipeline, triage issues.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
|
|||
|
|
@ -23,5 +23,9 @@
|
|||
"mochaFile": "cypresstest-output.xml",
|
||||
"toConsole": true
|
||||
},
|
||||
"experimentalSessionAndOrigin": true
|
||||
"experimentalSessionAndOrigin": true,
|
||||
"retries": {
|
||||
"runMode": 1,
|
||||
"openMode": 0
|
||||
}
|
||||
}
|
||||
40
cypress.config.js
Normal file
40
cypress.config.js
Normal 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,
|
||||
},
|
||||
})
|
||||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -67,11 +67,17 @@ describe('Test the actions in UTXO list', () => {
|
|||
cy.get('tx-table').find('tx-row').eq(0).find('.select-tx-img').click( {position: 'top'} )
|
||||
cy.wait(100)
|
||||
cy.get('tx-table').find('.freeze-tx-btn').click()
|
||||
cy.wait(200)
|
||||
// freeze-tx-btn submits a form → full page nav. tx-table's render()
|
||||
// awaits fetchWalletData() before attaching txRowSelected listeners,
|
||||
// so on fast workers a click can land after tx-row creation but
|
||||
// before the listener is live — select-tx-value toggles but the
|
||||
// action box never reveals. Wait long enough for the async gap to close.
|
||||
cy.wait(1000)
|
||||
// Select it
|
||||
cy.get('tx-table').find('tx-row').eq(0).find('.select-tx-img').click( {position: 'top'} );
|
||||
cy.get('tx-table').find('tx-row').eq(0).find('.select-tx-value').invoke('attr', 'value').should('eq', 'true') // Check that the click flow is (still) in order
|
||||
cy.wait(200)
|
||||
// Wait for the tx-table listener to process the selection and reveal the action box.
|
||||
cy.get('tx-table').find('.selected-rows-action-box').should('not.have.class', 'hidden')
|
||||
cy.get('tx-table').find('.compose-tx-btn').click()
|
||||
// Switch to coin selection, check that the right amount of coins are preselected in the coin selection
|
||||
cy.get('.coinselect-hidden').should('have.length', 1);
|
||||
|
|
|
|||
|
|
@ -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('/')
|
||||
})
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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 .
|
||||
|
|
@ -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
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
An image used to run the build on cirrus (tests only, not cypress-tests).
|
||||
|
||||
Create it like this:
|
||||
|
||||
```
|
||||
docker build . -t registry.gitlab.com/cryptoadvance/specter-desktop/cirrus-jammy:20230206
|
||||
docker push registry.gitlab.com/cryptoadvance/specter-desktop/cirrus-jammy:20230206
|
||||
```
|
||||
|
||||
Check the `.cirrus.yml` on how this is used and update the $current_date there.
|
||||
|
|
@ -1,15 +1,20 @@
|
|||
FROM ubuntu:22.04
|
||||
|
||||
# avoid any prompts
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Install Node.js 18.x from NodeSource (Ubuntu apt ships 12.x which lacks optional chaining)
|
||||
RUN apt-get update && \
|
||||
apt-get install -y apt-transport-https curl nodejs npm
|
||||
apt-get install -y apt-transport-https curl ca-certificates gnupg && \
|
||||
curl -fsSL https://deb.nodesource.com/setup_18.x | bash - && \
|
||||
apt-get install -y nodejs
|
||||
|
||||
|
||||
# Install latest NPM and Yarn
|
||||
RUN npm install -g npm@latest
|
||||
# Install latest Yarn
|
||||
RUN npm install -g yarn@latest
|
||||
|
||||
# install additional native dependencies build tools
|
||||
RUN apt install -y build-essential
|
||||
# apt-get update needed again after NodeSource setup script modified sources
|
||||
RUN apt-get update && apt-get install -y build-essential
|
||||
|
||||
# install Git client
|
||||
RUN apt-get install -y git
|
||||
|
|
@ -17,8 +22,6 @@ RUN apt-get install -y git
|
|||
# https://github.com/cypress-io/cypress/releases/tag/v3.8.0
|
||||
RUN apt-get install -y unzip
|
||||
|
||||
# avoid any prompts
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
#install tzdata package
|
||||
RUN apt-get install -y tzdata
|
||||
# set your timezone
|
||||
|
|
@ -40,11 +43,11 @@ RUN apt-get install -y \
|
|||
|
||||
# a few environment variables to make NPM installs easier
|
||||
# good colors for most applications
|
||||
ENV TERM xterm
|
||||
ENV TERM=xterm
|
||||
# avoid million NPM install messages
|
||||
ENV npm_config_loglevel warn
|
||||
ENV npm_config_loglevel=warn
|
||||
# allow installing when the main user is root
|
||||
ENV npm_config_unsafe_perm true
|
||||
ENV npm_config_unsafe_perm=true
|
||||
|
||||
# versions of local tools
|
||||
RUN echo " node version: $(node -v) \n" \
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ A base-image used in cypress-python to use a newer ubuntu jammy rather than a bu
|
|||
Create it like this:
|
||||
|
||||
```
|
||||
docker build . -t registry.gitlab.com/cryptoadvance/specter-desktop/cypress-base-ubuntu-jammy:20220908
|
||||
docker push registry.gitlab.com/cryptoadvance/specter-desktop/cypress-base-ubuntu-jammy:20220908
|
||||
docker build . -t ghcr.io/cryptoadvance/specter-desktop/cypress-base-ubuntu-jammy:20260411
|
||||
docker push ghcr.io/cryptoadvance/specter-desktop/cypress-base-ubuntu-jammy:20260411
|
||||
```
|
||||
|
||||
used in cypress-python
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM registry.gitlab.com/cryptoadvance/specter-desktop/cypress-base-ubuntu-jammy:20220908
|
||||
FROM ghcr.io/cryptoadvance/specter-desktop/cypress-base-ubuntu-jammy:20260411
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
python3-pip python3-virtualenv zip unzip file apt libusb-1.0-0-dev libudev-dev \
|
||||
|
|
@ -17,4 +17,4 @@ RUN DEBIAN_FRONTEND="noninteractive" apt-get install -y --no-install-recommends
|
|||
WORKDIR /test
|
||||
RUN rm -rf node_modules package-lock.json ~/.cache/Cypress
|
||||
RUN npm install --save-dev cypress@9.7.0
|
||||
RUN $(npm bin)/cypress verify
|
||||
RUN npx --no-install cypress verify
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ An image, ready to be used with cypress but also provides all the dependencies w
|
|||
Use versions of cypress as the version part of the tag. So e.g.:
|
||||
|
||||
```
|
||||
docker build . -t registry.gitlab.com/cryptoadvance/specter-desktop/cypress-python-jammy:20230206
|
||||
docker push registry.gitlab.com/cryptoadvance/specter-desktop/cypress-python-jammy:20230206
|
||||
docker build . -t ghcr.io/cryptoadvance/specter-desktop/cypress-python-jammy:20260411
|
||||
docker push ghcr.io/cryptoadvance/specter-desktop/cypress-python-jammy:20260411
|
||||
```
|
||||
|
||||
Search for `cypress-python` on where this is used in the project.
|
||||
|
||||
When editing this Dockerfile, rebuild and push to `ghcr.io/cryptoadvance/specter-desktop/cypress-python-jammy` with a new tag, then update the digest pin in `.github/workflows/test.yml`.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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
|
||||
|
|
|
|||
84
docs/ci-migration-evidence.md
Normal file
84
docs/ci-migration-evidence.md
Normal 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).
|
||||
561
docs/cirrus-replacement-spec.md
Normal file
561
docs/cirrus-replacement-spec.md
Normal file
|
|
@ -0,0 +1,561 @@
|
|||
# Cirrus CI Replacement Spec
|
||||
|
||||
> Incorporates review findings from Winston (architecture), Amelia (impl),
|
||||
> Murat (test arch), Mary (requirements), and Ravi (red team) across two
|
||||
> party-mode rounds plus a security pass. Key decisions: GHCR image
|
||||
> migration is already complete (landed in `kn/update-node-ci`); cutover
|
||||
> split across two PRs; acceptance gate is flake-rate based with 30-day
|
||||
> steady-state budget; branch-protection rename has a runbook; committed
|
||||
> evidence artifact required at gate-close; Codecov is out of scope.
|
||||
> **CRITICAL security regression identified and fixed as PR #1 blocker:**
|
||||
> unverified bitcoind downloads plus `save-always` cache turn a
|
||||
> supply-chain attack into a persistent backdoor vector — PR #1 must
|
||||
> update `tests/install_noded.sh` to GPG-verify releases.
|
||||
|
||||
## Context
|
||||
|
||||
Cirrus CI's free OSS tier shuts down **end of June 2026**. Specter-Desktop
|
||||
depends on Cirrus for PR gating via `.cirrus.yml` (pytest + Cypress +
|
||||
extension smoketest). We must migrate before the shutdown or lose PR gating.
|
||||
|
||||
Today's CI split (see `AGENTS.md` §CI/CD):
|
||||
|
||||
| System | Purpose | Status |
|
||||
|----------------|-----------------------------------------------------|-----------------|
|
||||
| GitHub Actions | Lint, smoke-build, Docker, **full release pipeline** | Active |
|
||||
| Cirrus CI | pytest + Cypress + extension smoketest on PRs | **Sunsetting** |
|
||||
| GitLab CI | `check` job only; release flow already migrated off | Vestigial, dead |
|
||||
|
||||
**Reframe:** the real root cause is **3-system CI sprawl with no owner**.
|
||||
Cirrus shutdown is the forcing function to consolidate on GH Actions.
|
||||
|
||||
## Goals
|
||||
|
||||
1. Preserve PR gating equivalent to Cirrus (pytest, Cypress, extension
|
||||
smoketest) before Cirrus goes dark.
|
||||
2. Zero new private-hardware requirements — stay on public runners.
|
||||
3. Preserve cache hit rate so PR turnaround doesn't regress beyond SLO
|
||||
(see §SLOs).
|
||||
4. Leave the repo in a state where `.cirrus.yml`, `docker/cirrus-jammy/`,
|
||||
`.gitlab-ci.yml`, and `pyinstaller/build-win-ci.bat` can be removed
|
||||
cleanly in a follow-up PR.
|
||||
5. Remove the cross-provider GitLab registry dependency entirely.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Re-architecting the test suite.
|
||||
- Rebalancing the test pyramid (Cypress → pytest migration). Tracked
|
||||
separately in §Deferred.
|
||||
- Moving jobs back to GitLab CI (no PR model).
|
||||
- Adding Cypress Dashboard or other paid observability SaaS.
|
||||
- Coverage upload to Codecov or similar. Coverage stays terminal-only,
|
||||
matching current Cirrus behavior.
|
||||
- Introducing new test matrices (OS/python versions) beyond Cirrus parity.
|
||||
|
||||
## Target: GitHub Actions
|
||||
|
||||
GH Actions already hosts lint, Docker, and the release pipeline, all on
|
||||
public runners. Consolidating reduces CI systems from three to one.
|
||||
|
||||
## Image hosting — already on GHCR
|
||||
|
||||
**Already done** by PR #2602 (merged 2026-04-12, commit `3e277689`). That
|
||||
PR upgraded Node 12→18 and migrated **all three** CI images from GitLab
|
||||
registry to GHCR:
|
||||
|
||||
- `ghcr.io/cryptoadvance/specter-desktop/cirrus-jammy:20260412`
|
||||
- `ghcr.io/cryptoadvance/specter-desktop/cypress-python-jammy:20260411`
|
||||
- `ghcr.io/cryptoadvance/specter-desktop/cypress-base-ubuntu-jammy:20260411`
|
||||
|
||||
The cross-provider GitLab registry dependency is gone. No mirror workflow
|
||||
needed. `.cirrus.yml` already references the GHCR paths, so Cirrus and the
|
||||
new GHA workflow will pull from the same registry during the side-by-side
|
||||
period.
|
||||
|
||||
**Residual ownership gap (small):** the Dockerfiles are built by hand per
|
||||
`docker/cypress-python-jammy/Readme.md`. When they change (historically
|
||||
every ~18 months), whoever edits must remember to `docker build && docker push`
|
||||
to GHCR and bump the tag in `test.yml`. Fix: add one sentence to
|
||||
`docker/cypress-python-jammy/Readme.md` stating that Dockerfile edits
|
||||
require re-push to GHCR and a matching tag/digest bump in
|
||||
`.github/workflows/test.yml`. That's the full remediation — no scheduled
|
||||
liveness check, no CODEOWNERS fight. The image is too static to justify more.
|
||||
|
||||
Deeper hardening (cosign signing, automated build workflow) is tracked in
|
||||
§Security HIGH as a 30-day follow-up.
|
||||
|
||||
## What to port
|
||||
|
||||
Three Cirrus tasks → three GH Actions jobs in a new
|
||||
`.github/workflows/test.yml`. Trigger: `pull_request` + `push` to master.
|
||||
|
||||
### Global workflow-level settings
|
||||
|
||||
```yaml
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
```
|
||||
|
||||
Every job must set:
|
||||
- `timeout-minutes:` (budget below)
|
||||
- `actions/checkout@v4` with **`fetch-depth: 0`** (NOT default `1`) —
|
||||
`tests/test_util_version.py` needs tag history and `git describe` needs
|
||||
annotated tags. No separate `git fetch refs/tags/v1.0.0` dance.
|
||||
- `fail-fast: false` semantics where matrices are used.
|
||||
|
||||
### 1. `test` (pytest)
|
||||
|
||||
Cirrus today: `test_task` on
|
||||
`ghcr.io/cryptoadvance/specter-desktop/cirrus-jammy:20260412`
|
||||
(post-PR-#2602 baseline), runs
|
||||
`pytest --cov=cryptoadvance --junitxml=./testresults.xml` with cached
|
||||
bitcoind and elementsd.
|
||||
|
||||
GH Actions mapping:
|
||||
- `runs-on: ubuntu-22.04`
|
||||
- `timeout-minutes: 45`
|
||||
- System deps installed inline: `libusb-1.0-0-dev libudev-dev
|
||||
python3-virtualenv`. No custom image.
|
||||
- `actions/setup-python@v5` pinned to `3.10`, `cache: pip`, `cache-dependency-path: requirements.txt`.
|
||||
- **bitcoind/elementsd cache** (see §Caching).
|
||||
- Install:
|
||||
```
|
||||
pip install -r requirements.txt --require-hashes
|
||||
pip install -e ".[test]" # intentionally bypasses hashes, documented
|
||||
```
|
||||
- Run: `pytest --cov=cryptoadvance --cov-report=term --junitxml=./testresults.xml -p no:cacheprovider --reruns 0`
|
||||
- `--reruns 0`: fail fast. Flakiness is debt, not a coping mechanism.
|
||||
- Coverage is terminal-only, matching current Cirrus behavior. No upload.
|
||||
- Artifacts: `testresults.xml`.
|
||||
|
||||
### 2. `cypress`
|
||||
|
||||
Cirrus today: `cypress_test_task` on
|
||||
`ghcr.io/cryptoadvance/specter-desktop/cypress-python-jammy:20260411`
|
||||
(post-PR-#2602 baseline), Cirrus-requested `cpu: 6, memory: 6G`,
|
||||
runs `./utils/test-cypress.sh --debug run`.
|
||||
|
||||
GH Actions mapping:
|
||||
- `runs-on: ubuntu-22.04` (4 vCPU / 16 GB free). **Do not pre-escalate** to
|
||||
`ubuntu-22.04-large`. Measure first (see §Cypress measurement).
|
||||
- `timeout-minutes: 30` initially; adjust after measurement.
|
||||
- Container:
|
||||
```yaml
|
||||
container:
|
||||
image: ghcr.io/cryptoadvance/specter-desktop/cypress-python-jammy@sha256:<digest>
|
||||
options: --shm-size=2g # Cypress OOMs on default 64M /dev/shm
|
||||
```
|
||||
Digest captured from the `:20260411` tag at PR #1 time. Pinned by digest,
|
||||
not tag, to make future Dockerfile edits visibly require a workflow bump.
|
||||
- Same bitcoind/elements cache as `test`.
|
||||
- npm cache via `actions/cache` keyed on `package-lock.json` (not
|
||||
`setup-node`; node already ships in the container).
|
||||
- Cypress retries: `{ runMode: 1, openMode: 0 }` in `cypress.json` — one
|
||||
retry max, logged loudly so flakes are visible, not hidden.
|
||||
- Run: `./utils/test-cypress.sh --debug run`.
|
||||
- Artifacts: `cypresstest-output.xml`, `cypress/screenshots/**`,
|
||||
`cypress/videos/**`.
|
||||
|
||||
**Sharding decision:** deferred. If measured wall-clock > 15 min, split
|
||||
specs across a 2-shard matrix. Don't split pre-emptively.
|
||||
|
||||
### 3. `extension-smoketest`
|
||||
|
||||
Straight port of Cirrus's `extension_smoketest_task`. Same runner/deps as
|
||||
`test` job. Preserves the exact bash block: git identity, `ext gen`, server
|
||||
boot, log-line grep, curl assertion. **Contract must stay byte-compatible**
|
||||
— this job is the canary for downstream extension developers.
|
||||
|
||||
## Images
|
||||
|
||||
- **`docker/cirrus-jammy/`** — delete in PR #2. pytest job installs deps
|
||||
inline on `ubuntu-22.04`; no custom image needed. The current GHCR tag
|
||||
(`cirrus-jammy:20260412`) can stay in GHCR indefinitely as a harmless
|
||||
artifact after the Dockerfile is deleted from the repo.
|
||||
- **`cypress-python-jammy`** + **`cypress-base-ubuntu-jammy`** — already
|
||||
on GHCR via PR #2602. Keep Dockerfiles in `docker/`. Add one sentence
|
||||
to `docker/cypress-python-jammy/Readme.md`: *"When editing this
|
||||
Dockerfile, rebuild and push to
|
||||
`ghcr.io/cryptoadvance/specter-desktop/cypress-python-jammy` with a new
|
||||
tag, then update the digest pin in `.github/workflows/test.yml`."*
|
||||
|
||||
## Caching
|
||||
|
||||
GH Actions `actions/cache@v4` keyed on:
|
||||
|
||||
```
|
||||
${{ runner.os }}-${{ runner.arch }}-noded-
|
||||
${{ hashFiles('pyproject.toml', 'tests/elements_gitrev_pinned', 'tests/install_noded.sh', 'tests/bitcoin_SHA256SUMS', 'tests/elements_SHA256SUMS') }}-
|
||||
binary
|
||||
```
|
||||
|
||||
`tests/bitcoin_gitrev_pinned` is not in the repo today (bitcoin version
|
||||
comes from `pyproject.toml`); the cache key hashes the committed
|
||||
SHA256SUMS trust anchors instead, so a version bump invalidates the
|
||||
cache through the trust-anchor files.
|
||||
|
||||
`runner.arch` matters — future ARM runners must not poison x86 caches.
|
||||
|
||||
Paths: `./tests/bitcoin`, `./tests/elements`.
|
||||
|
||||
Populate step: `./tests/install_noded.sh --debug --bitcoin binary` and
|
||||
`--elements binary` on cache miss (match Cirrus `populate_script`).
|
||||
|
||||
Set **`save-always: true`** on the cache step so a mid-job timeout on a
|
||||
cold cache still persists the partial binaries. Otherwise first-PR-after-
|
||||
pin-bump on every branch re-pays the full install cost.
|
||||
|
||||
**Security-critical:** see §Security. `save-always` + unverified
|
||||
downloads was a persistent-backdoor vector. PR #1 must update
|
||||
`install_noded.sh` to GPG-verify releases and re-verify on every cache
|
||||
restore. This is non-negotiable.
|
||||
|
||||
First run after cutover repopulates per branch — one-off ~few-minute cost,
|
||||
accepted.
|
||||
|
||||
## Concurrency, timeouts, retries
|
||||
|
||||
| Setting | Value |
|
||||
|-----------------------|----------------------------------------------|
|
||||
| Concurrency group | `${{ github.workflow }}-${{ github.ref }}` |
|
||||
| Cancel in progress | `true` |
|
||||
| `test` timeout | 45 min |
|
||||
| `cypress` timeout | 30 min (tune after measurement) |
|
||||
| `extension-smoketest` | 15 min |
|
||||
| pytest reruns | **0** (fail fast) |
|
||||
| Cypress `runMode` retries | **1** (log loudly) |
|
||||
|
||||
## Security
|
||||
|
||||
Red-team pass (Ravi, 2026-04-12) surfaced one **CRITICAL** regression
|
||||
introduced by this migration, plus two HIGH findings tracked as follow-ups.
|
||||
|
||||
### CRITICAL — bitcoind/elementsd download verification (PR #1 blocker)
|
||||
|
||||
**Attack chain:** `tests/install_noded.sh:244` does a bare
|
||||
`wget https://bitcoincore.org/bin/bitcoin-core-${version}/${binary_file}`
|
||||
with **no GPG signature verification and no SHA256SUMS check** (same for
|
||||
Elements at line 241). Under Cirrus this was per-run ephemeral. Under GH
|
||||
Actions with `actions/cache@v4 save-always: true` — and a cache key that
|
||||
only hashes the pinned-rev files and the install script, **not** the
|
||||
binary content — a single poisoned fetch (MITM, DNS/BGP hijack, upstream
|
||||
compromise) writes a trojaned `bitcoind` into the cache. Every
|
||||
subsequent run on master and PR branches restores it from cache. The
|
||||
test runner executes it with the repo checkout mounted.
|
||||
|
||||
**Impact:** persistent backdoor across all PR and master runs until the
|
||||
cache key rotates. Blast radius includes the release pipeline
|
||||
(`release.yml`), which shares the repo's `GITHUB_TOKEN` boundary and
|
||||
runs on the same runner pool.
|
||||
|
||||
**Regression status:** the `install_noded.sh` bare-wget predates this
|
||||
migration. Cirrus's per-run ephemerality masked the weakness. Moving to
|
||||
GHA with `save-always` caching **materially worsens** it from transient
|
||||
to persistent. We do not inherit the risk silently — we fix it in PR #1.
|
||||
|
||||
**Fix (must land in PR #1):**
|
||||
1. Update `tests/install_noded.sh` to download `SHA256SUMS` and
|
||||
`SHA256SUMS.asc` alongside the binary tarball.
|
||||
2. Import the Bitcoin Core release signing keys (fanquake, achow101)
|
||||
into a temporary `GNUPGHOME` and verify `SHA256SUMS.asc`.
|
||||
3. Verify the tarball hash matches the entry in `SHA256SUMS`. Abort on
|
||||
mismatch.
|
||||
4. Same treatment for ElementsProject downloads (`install_noded.sh:241`)
|
||||
using Elements release signing keys.
|
||||
5. Run the verification step **on cache hit as well** (not only cache
|
||||
miss), so a tampered cache entry fails closed instead of being
|
||||
trusted on restore.
|
||||
6. Commit the expected `SHA256SUMS` content (or its content hash) to the
|
||||
repo so post-restore verification has a trusted reference. Bump it
|
||||
alongside `tests/bitcoin_gitrev_pinned` / `tests/elements_gitrev_pinned`.
|
||||
|
||||
### HIGH — Cypress image integrity chain (30-day follow-up)
|
||||
|
||||
**Attack chain:** `cypress-python-jammy` is built and pushed by hand.
|
||||
Maintainer-laptop compromise at push time → attacker uploads a trojaned
|
||||
image under legitimate GHCR credentials. Next Dockerfile edit, a
|
||||
maintainer computes the digest from the compromised local build and
|
||||
pins it in `test.yml`. Cypress runs as root inside, with the repo
|
||||
checkout mounted.
|
||||
|
||||
**Not a PR #1 blocker.** Image has been untouched for 18 months; edit
|
||||
frequency bounds exposure. Tracked as a separate hardening issue.
|
||||
|
||||
**Fix (separate PR, target: within 30 days of cutover):**
|
||||
1. Automate image build in a workflow triggered on `docker/**` path
|
||||
filter, using default `GITHUB_TOKEN` with `packages: write`.
|
||||
2. Cosign keyless-sign the pushed image via GitHub OIDC.
|
||||
3. `test.yml` verifies the signature via `cosign verify` before
|
||||
consuming the container. Removes maintainer laptops from the trust
|
||||
path entirely.
|
||||
|
||||
### HIGH (acknowledged) — Release pipeline shares token boundary
|
||||
|
||||
`.github/workflows/release.yml` ships specterd, pip package, and
|
||||
Electron artifacts to real users. It lives in the same repo as
|
||||
`test.yml` and shares the same `GITHUB_TOKEN` boundary and runner pool.
|
||||
Any RCE in a test workflow runs on a runner that can read `release.yml`.
|
||||
This migration does not change that exposure — but **fixing CRITICAL #1
|
||||
above also de-risks the release pipeline**, since the same runner pool
|
||||
consumes the same (now verified) binaries.
|
||||
|
||||
No spec change beyond this acknowledgment. Full release pipeline
|
||||
hardening is out of scope.
|
||||
|
||||
### LOW findings (accepted / backlog)
|
||||
|
||||
- **Branch protection cutover gap:** minutes-wide, runbook adequate,
|
||||
accept.
|
||||
- **`GITHUB_TOKEN` on fork PRs:** default read-only, no
|
||||
`pull_request_target`, no third-party actions, no secrets exposed.
|
||||
Clean.
|
||||
- **First-party actions pinned to major (`@v4`/`@v5`) not SHA:** GitHub
|
||||
org compromise is a tier-1 ecosystem event. Defense-in-depth backlog
|
||||
item, not blocking.
|
||||
|
||||
## Test quality — flake detection, quarantine, steady-state
|
||||
|
||||
Not in Cirrus today. Adding now, at minimum viable.
|
||||
|
||||
- **Flake signal:** parse JUnit XML for retry markers after each run; when
|
||||
a Cypress spec retries-to-green, emit a GH Actions warning annotation
|
||||
and append a line to a tracking gist (`flake-log.md`). No dashboard yet.
|
||||
- **Quarantine policy:** any spec flagged flaky twice in 14 days gets
|
||||
`@skip(reason="flaky", issue="#NNNN")` with a linked GH issue and a
|
||||
**2-week SLA** to fix-or-delete.
|
||||
- **Retry budget:** Cypress `runMode: 1`. Anything needing more is quarantined.
|
||||
- **Steady-state flake SLO (post-cutover):** rolling 30-day rerun rate
|
||||
must stay **≤ 1%** across all jobs. Same JUnit parser feeds a daily cron
|
||||
job that computes the window and auto-opens a P2 issue on breach. This
|
||||
turns flake detection from a one-shot cutover gate into a living quality
|
||||
signal — without it we'll be right back here in six months.
|
||||
|
||||
## Cypress performance — measurement protocol
|
||||
|
||||
**Before** approving any runner upgrade:
|
||||
1. Run the Cypress suite 5× on `ubuntu-22.04` free tier.
|
||||
2. Record wall-clock p50 / p95 via `/usr/bin/time -v` wrapping the script.
|
||||
3. Compare to last 5 Cirrus runs' wall-clock.
|
||||
4. Ship it if p95 is within **Cirrus +20%**.
|
||||
5. Only if p95 exceeds +20% or hits the 30-min timeout, evaluate in order:
|
||||
(a) `--shm-size` bump, (b) spec sharding across 2 jobs,
|
||||
(c) `ubuntu-22.04-large` (paid — requires approval).
|
||||
|
||||
## Migration plan — two PRs
|
||||
|
||||
### PR #1 — Add GH Actions workflow (side-by-side)
|
||||
|
||||
1. **Security blocker:** update `tests/install_noded.sh` per §Security
|
||||
CRITICAL fix (GPG-verify `SHA256SUMS.asc`, verify tarball hash,
|
||||
re-verify on cache restore). Commit trusted reference hashes
|
||||
alongside `tests/bitcoin_gitrev_pinned` /
|
||||
`tests/elements_gitrev_pinned`. This must land in the same PR as the
|
||||
workflow.
|
||||
2. Capture the `cypress-python-jammy` image digest from the `:20260411`
|
||||
GHCR tag. Write it into `test.yml`.
|
||||
3. Add `.github/workflows/test.yml` with all three jobs.
|
||||
4. Add the one-sentence rebuild-and-push note to
|
||||
`docker/cypress-python-jammy/Readme.md`.
|
||||
5. **Do not touch** `.cirrus.yml`, `.gitlab-ci.yml`, or branch protection.
|
||||
6. Land the PR. Both CI systems now run on every PR.
|
||||
7. Iterate on the workflow until the acceptance gate (§Acceptance) passes.
|
||||
8. Commit the evidence artifact to `docs/ci-migration-evidence.md` once
|
||||
the gate is met.
|
||||
|
||||
### PR #2 — Cutover + cleanup
|
||||
|
||||
Only merge when PR #1 meets the acceptance gate.
|
||||
|
||||
1. Delete `.cirrus.yml` and `docker/cirrus-jammy/`.
|
||||
2. Delete `.gitlab-ci.yml` and `pyinstaller/build-win-ci.bat`.
|
||||
3. Audit and prune dead GitLab-only code paths in `utils/release.sh`,
|
||||
`utils/release_helper.py`, `utils/github.py`.
|
||||
4. Drop Cirrus + GitLab sections from `docs/continuous-integration.md`.
|
||||
5. Update `AGENTS.md` CI/CD section.
|
||||
6. **Rename required status checks** in GitHub branch protection (see
|
||||
§Branch protection). This is a separate manual step, documented in the
|
||||
PR body.
|
||||
|
||||
Splitting the cleanup out of PR #1 preserves fast revertability: if
|
||||
anything melts in week 1 of side-by-side, PR #1 is trivially revertable
|
||||
because it touches only additive files.
|
||||
|
||||
## Branch protection & required checks
|
||||
|
||||
**Scariest silent-failure mode.** If the required-check name on master
|
||||
branch protection still says `Cirrus CI / test_task` after cutover,
|
||||
*nothing is gating PRs anymore* and no alert fires.
|
||||
|
||||
Runbook (must execute as part of PR #2 merge):
|
||||
|
||||
1. Before merging PR #2: list current required checks via
|
||||
`gh api repos/cryptoadvance/specter-desktop/branches/master/protection`.
|
||||
2. Record the Cirrus check names.
|
||||
3. Merge PR #2.
|
||||
4. Immediately update branch protection: remove the Cirrus check names,
|
||||
then add the exact GitHub Actions required-check contexts in the form
|
||||
`<workflow name> / <job name>` as reported by the GitHub UI or API.
|
||||
For this migration the workflow `name:` is `Tests` (see
|
||||
`.github/workflows/test.yml`), so the three contexts to require are:
|
||||
- `Tests / test`
|
||||
- `Tests / cypress`
|
||||
- `Tests / extension-smoketest`
|
||||
|
||||
Do NOT add bare `test` / `cypress` / `extension-smoketest` — those are
|
||||
job ids, not emitted check contexts, and will silently fail to gate.
|
||||
5. Open a throwaway test PR and verify those exact three contexts are
|
||||
marked required and are actually gating merge.
|
||||
6. Only then announce cutover complete.
|
||||
|
||||
## Secrets inventory
|
||||
|
||||
No new secrets required. The workflow only uses the default
|
||||
auto-provided `GITHUB_TOKEN` (for `ghcr.io` push during the one-shot image
|
||||
mirror and for any standard action plumbing).
|
||||
|
||||
Historical GitLab secrets (`GH_BIN_UPLOAD_PW`, `TWINE_PASSWORD`,
|
||||
`GPG_PASSPHRASE`, `SSH_SPECTEREXT_DEPLOY_KEY`, `SSH_SPECTERSTATIC_DEPLOY_KEY`)
|
||||
are already unused per AGENTS.md. PR #2 should not touch them (release
|
||||
pipeline lives elsewhere).
|
||||
|
||||
## Stakeholder comms
|
||||
|
||||
This is CI plumbing, not a release. Scope is narrow:
|
||||
|
||||
- **Extension developers:** smoketest contract preserved byte-for-byte.
|
||||
No comms needed unless the job fails post-cutover.
|
||||
- **`docs/continuous-integration.md`:** updated in PR #2 to reflect the
|
||||
new CI topology. That's the extent of external-facing docs.
|
||||
|
||||
## SLOs / acceptance criteria
|
||||
|
||||
### PR wall-clock SLO
|
||||
|
||||
**Baseline captured 2026-04-12** from the Cirrus GraphQL API
|
||||
(`https://api.cirrus-ci.com/graphql`), last 20 successful master-branch
|
||||
builds. Cirrus runs all three tasks in parallel, so total build wall-clock
|
||||
≈ longest task (cypress).
|
||||
|
||||
| Task | n | median | p95 | min | max | GHA ceiling (+20%) |
|
||||
|---------------------------|----|--------|--------|--------|--------|--------------------|
|
||||
| `test_task` | 20 | 4m47s | 5m35s | 4m23s | 5m37s | **5m44s** |
|
||||
| `cypress_test_task` | 20 | 6m02s | 6m55s | 5m27s | 8m15s | **7m14s** |
|
||||
| `extension_smoketest_task`| 20 | 2m10s | 2m27s | 1m55s | 2m39s | **2m36s** |
|
||||
|
||||
- **Tolerance:** per-job wall-clock on GH Actions must not exceed the
|
||||
"GHA ceiling" column (Cirrus median + 20%). This threshold is used
|
||||
uniformly by the Cypress measurement protocol and the acceptance gate
|
||||
— one number, one decision.
|
||||
- **Action on breach:** investigate before cutover; do not ship PR #2 until
|
||||
resolved.
|
||||
|
||||
**Implications for Cypress measurement protocol:** current Cirrus cypress
|
||||
p95 is 6m55s — well under the 30-min timeout and not remotely at risk of
|
||||
hitting paid-runner territory. The "4 vCPU vs 6" concern is almost
|
||||
certainly a non-issue in practice. Measurement protocol stands, but expect
|
||||
it to pass on free runners.
|
||||
|
||||
### Flake rate ceiling
|
||||
|
||||
- **Baseline:** measured during side-by-side (unknown today).
|
||||
- **Ceiling:** post-cutover flake rate ≤ Cirrus baseline. Any regression
|
||||
is a cutover blocker.
|
||||
|
||||
### Cost ceiling
|
||||
|
||||
- GH Actions minutes on public OSS runners are effectively free. No hard
|
||||
cap needed unless we escalate to `-large` runners (then: $X/month ceiling
|
||||
requires explicit approval from project lead).
|
||||
|
||||
### Acceptance gate for cutover (PR #2 merge criteria)
|
||||
|
||||
All four must hold:
|
||||
1. **10 consecutive green runs** of the new workflow on master-branch
|
||||
schedule (nightly) or via manual dispatch.
|
||||
2. **At least 3 green PR runs**, including one that touches `src/cryptoadvance/specter/static/` or frontend templates.
|
||||
3. **Zero new flakes** detected over at least 50 total runs (PR + master).
|
||||
4. Cypress p95 wall-clock within **Cirrus +20%** per measurement protocol.
|
||||
|
||||
"Green on 3 PRs" alone is insufficient — flake rate is the real KPI.
|
||||
|
||||
### Evidence artifact (mandatory)
|
||||
|
||||
Before merging PR #2, a committed **evidence artifact** must exist at
|
||||
`docs/ci-migration-evidence.md` containing, at minimum:
|
||||
|
||||
- Measured wall-clock median + p95 per job over the 10+ master runs
|
||||
- Flake count over the 50+ runs window (expected: 0)
|
||||
- URLs to 3+ sample PR runs including the frontend-touching one
|
||||
- Date and commit SHA at measurement time
|
||||
|
||||
This exists so that six months from now "did we actually hit SLO?" has a
|
||||
grep-able answer instead of a hope. GH Actions logs are GC'd; the evidence
|
||||
doc is not. Ten minutes to write, permanent value.
|
||||
|
||||
## Rollback procedure
|
||||
|
||||
If anything breaks post-cutover:
|
||||
|
||||
1. **Workflow-level breakage in PR #1 phase:** disable the GH Actions
|
||||
workflow via `workflow_dispatch` off-switch or delete the file; Cirrus
|
||||
still gates PRs. Zero user impact.
|
||||
2. **Post-cutover breakage (PR #2 merged):** revert PR #2. This restores
|
||||
`.cirrus.yml` and `docker/cirrus-jammy/`. Re-add Cirrus checks to branch
|
||||
protection. Cirrus is assumed still alive up to shutdown date.
|
||||
3. **Post-Cirrus-shutdown breakage:** no Cirrus fallback exists. Forward
|
||||
fix only. This is why PR #2 must merge **≥ 4 weeks before shutdown**.
|
||||
|
||||
## Timeline
|
||||
|
||||
| Date | Milestone |
|
||||
|-------------|-------------------------------------------------|
|
||||
| 2026-04-12 | Cirrus baseline captured (§SLOs); GHCR migration merged (PR #2602) |
|
||||
| 2026-04-13 | Spec finalized post red-team pass |
|
||||
| 2026-04-14 | Open PR #1 (security fix + workflow + cache) |
|
||||
| 2026-04-21 | PR #1 green on first PR run |
|
||||
| 2026-05-05 | Acceptance gate met (10 green + flake-clean) |
|
||||
| 2026-05-12 | **PR #2 merged + branch protection updated** |
|
||||
| 2026-05-19 | One full release cycle on new CI (if a tag cuts)|
|
||||
| 2026-06-30 | Cirrus shutdown (~7 week buffer after cutover) |
|
||||
|
||||
## Deferred (explicit non-goals for this migration)
|
||||
|
||||
- **Test pyramid rebalance.** Cypress likely owns flows that belong in
|
||||
pytest + a Flask test client. Q3 epic, not this PR. Logged here so it's
|
||||
not lost.
|
||||
- **Cypress Dashboard / paid observability.** Vendor lock, not worth the
|
||||
cost at current scale. JUnit + artifacts cover 90% of the value.
|
||||
- **Coverage upload / Codecov / coverage-delta gating.** Out of scope for
|
||||
this migration. Matches current Cirrus (which also doesn't upload). If
|
||||
we want trend data later, it's a standalone follow-up PR with its own
|
||||
bus-factor discussion and token plumbing.
|
||||
- **Local-contributor reproducibility of the Cypress image.** Nice to
|
||||
have; separate runbook, post-cutover.
|
||||
- **Cypress image cosign signing + build automation.** Tracked in
|
||||
§Security HIGH. Separate PR within 30 days of cutover, not blocking.
|
||||
- **Release pipeline hardening.** Out of scope; §Security HIGH
|
||||
acknowledges shared token boundary.
|
||||
- **First-party action SHA pinning.** Defense-in-depth backlog.
|
||||
|
||||
## Risks (updated)
|
||||
|
||||
1. **Supply-chain backdoor via unverified bitcoind (CRITICAL).** Resolved
|
||||
by the PR #1 blocker fix in §Security. Without that fix, this spec is
|
||||
unmergeable.
|
||||
2. **Cypress perf on 4 vCPU.** Largely resolved by baseline data: Cirrus
|
||||
cypress p95 is 6m55s, so the suite is not heavyweight. Residual risk is
|
||||
that GH Actions 4-vCPU runners could push it past the +25% ceiling
|
||||
(7m32s). Mitigation: measurement protocol in §Cypress measurement.
|
||||
Escape hatch documented, not pre-purchased.
|
||||
2. **Branch protection silent-ungate.** Scariest risk; has a runbook now
|
||||
(§Branch protection).
|
||||
3. **ghcr.io image rebuild drift.** Once mirrored, whoever edits
|
||||
`docker/cypress-python-jammy/Dockerfile` must remember to rebuild and
|
||||
re-push. Mitigation: README runbook + digest pinning makes staleness
|
||||
visible (workflow won't auto-upgrade).
|
||||
4. **First-run cache miss storm.** One-off few-minute cost. Accepted.
|
||||
|
||||
## Unresolved questions
|
||||
|
||||
None. All prior questions resolved or descoped.
|
||||
|
|
@ -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:
|
||||
|
||||

|
||||
|
||||
#### 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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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:
|
||||
|
||||

|
||||
- **`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:
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
|
|
|||
8042
package-lock.json
generated
8042
package-lock.json
generated
File diff suppressed because it is too large
Load diff
19
package.json
19
package.json
|
|
@ -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",
|
||||
|
|
@ -33,5 +41,8 @@
|
|||
"bugs": {
|
||||
"url": "https://github.com/cryptoadvance/specter-desktop/issues"
|
||||
},
|
||||
"homepage": "https://github.com/cryptoadvance/specter-desktop#readme"
|
||||
"homepage": "https://github.com/cryptoadvance/specter-desktop#readme",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -160,7 +160,9 @@ app.whenReady().then(() => {
|
|||
downloadSpecterd(specterdPath)
|
||||
} else {
|
||||
updatingLoaderMsg(
|
||||
'Specterd file could not be validated and no version is configured in the settings<br>Please go to Preferences and set version to fetch or add an executable manually...'
|
||||
'Specterd file could not be validated and no version is configured in the settings<br>Please go to Preferences and set version to fetch or add an executable manually...',
|
||||
false,
|
||||
{ isHtml: true }
|
||||
)
|
||||
updateSpecterdStatus('Failed to locate specterd...')
|
||||
}
|
||||
|
|
@ -169,8 +171,13 @@ app.whenReady().then(() => {
|
|||
if (appSettings.specterdVersion) {
|
||||
downloadSpecterd(specterdPath)
|
||||
} else {
|
||||
// NB: smoketest workflow (.github/workflows/electron-smoketest.yml) greps
|
||||
// for the leading "Specterd was not found and no version is configured"
|
||||
// substring to assert splash reached this branch. Keep both in sync.
|
||||
updatingLoaderMsg(
|
||||
'Specterd was not found and no version is configured in the settings<br>Please go to Preferences and set version to fetch or add an executable manually...'
|
||||
'Specterd was not found and no version is configured in the settings<br>Please go to Preferences and set version to fetch or add an executable manually...',
|
||||
false,
|
||||
{ isHtml: true }
|
||||
)
|
||||
updateSpecterdStatus('Failed to locate specterd...')
|
||||
}
|
||||
|
|
@ -200,6 +207,15 @@ app.on('before-quit', (event) => {
|
|||
}
|
||||
})
|
||||
|
||||
ipcMain.on('open-settings', (event) => {
|
||||
const senderUrl = event.senderFrame?.url || ''
|
||||
if (!senderUrl.startsWith('file://') || !senderUrl.endsWith('/splash.html')) {
|
||||
logger.warn(`Rejected open-settings IPC from untrusted sender: ${senderUrl}`)
|
||||
return
|
||||
}
|
||||
openPreferences()
|
||||
})
|
||||
|
||||
ipcMain.on('request-mainprocess-action', (event, arg) => {
|
||||
switch (arg.message) {
|
||||
case 'save-preferences':
|
||||
|
|
|
|||
5067
pyinstaller/electron/package-lock.json
generated
5067
pyinstaller/electron/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -8,8 +8,6 @@
|
|||
// All of the Node.js APIs are available in the preload process.
|
||||
// It has the same sandbox as a Chrome extension.
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
|
||||
const updateSpinner = (show) => {
|
||||
const spinnerElement = document.getElementById('spinner');
|
||||
if (spinnerElement) {
|
||||
|
|
@ -19,10 +17,23 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||
window.api.receive('update-loader-message', (data) => {
|
||||
const launchTextElement = document.getElementById('launch-text');
|
||||
if (launchTextElement) {
|
||||
launchTextElement.textContent = data.msg;
|
||||
if (data.isHtml) {
|
||||
launchTextElement.innerHTML = data.msg;
|
||||
} else {
|
||||
launchTextElement.textContent = data.msg;
|
||||
}
|
||||
updateSpinner(data.showSpinner);
|
||||
}
|
||||
const settingsBtn = document.getElementById('open-settings-btn');
|
||||
if (settingsBtn) {
|
||||
settingsBtn.classList.toggle('hidden', !data.showSettingsButton);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
})
|
||||
const openSettingsBtn = document.getElementById('open-settings-btn');
|
||||
if (openSettingsBtn) {
|
||||
openSettingsBtn.addEventListener('click', () => {
|
||||
window.api.send('open-settings');
|
||||
});
|
||||
}
|
||||
})
|
||||
|
|
@ -29,11 +29,13 @@ function checkSpecterd(logs, specterdStarted) {
|
|||
}
|
||||
|
||||
let specterIsRunning = false
|
||||
let currentSpecterURL = 'http://localhost:25441'
|
||||
function startSpecterd(specterdPath, automaticWalletImport = false) {
|
||||
if (platformName == 'win64') {
|
||||
specterdPath += '.exe'
|
||||
}
|
||||
let hwiBridgeMode = appSettings.mode == 'hwibridge'
|
||||
currentSpecterURL = hwiBridgeMode ? appSettings.specterURL : 'http://localhost:25441'
|
||||
updatingLoaderMsg('Launching Specter ...', (showSpinner = 'true'))
|
||||
updateSpecterdStatus('Launching Specter ...')
|
||||
let specterdArgs = ['server']
|
||||
|
|
@ -85,7 +87,7 @@ function startSpecterd(specterdPath, automaticWalletImport = false) {
|
|||
}, 3000)
|
||||
} else {
|
||||
logger.info('Normal startup of Specter.')
|
||||
createWindow(appSettings.specterURL)
|
||||
createWindow(currentSpecterURL)
|
||||
}
|
||||
} else if (serverdStatus === 'timeout') {
|
||||
showError('Specter does not seem to start. Check the logs in the menu for more details.')
|
||||
|
|
@ -113,7 +115,7 @@ function startSpecterd(specterdPath, automaticWalletImport = false) {
|
|||
app.on('activate', function () {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow(appSettings.specterURL)
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow(currentSpecterURL)
|
||||
})
|
||||
// since these are streams, you can pipe them elsewhere
|
||||
specterdProcess.on('close', (code) => {
|
||||
|
|
@ -167,7 +169,7 @@ app.on('open-url', (_, url) => {
|
|||
// Only proceed with the import if the importFromWalletSoftwareBtn can be found.
|
||||
// If it is not, users are redirected by specterd to the configure connection screen.
|
||||
function importWallet(walletData) {
|
||||
loadUrl(appSettings.specterURL + '/wallets/new_wallet/')
|
||||
loadUrl(currentSpecterURL + '/wallets/new_wallet/')
|
||||
let code = `
|
||||
const importFromWalletSoftwareBtn = document.getElementById('import-from-wallet-software-btn')
|
||||
if (importFromWalletSoftwareBtn) {
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@
|
|||
<link rel="stylesheet" type="text/css" href="./typography.css">
|
||||
<link rel="stylesheet" type="text/css" href="./output.css">
|
||||
<body class="bg-dark-900">
|
||||
<div width="100%" height="100%" class="flex space-x-2 items-center justify-center h-screen">
|
||||
<div width="100%" height="100%" class="flex flex-col space-y-4 items-center justify-center h-screen">
|
||||
<div class="flex space-x-2 items-center">
|
||||
<svg id="spinner" class="text-white rounded-full bg-dark-700 w-4 h-4 animate-spin hidden" 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="M52.8359,43.6067l1.30463e-08,-1.88843e-08c-0.312506,0.452349 -0.932543,0.565714 -1.38489,0.253208c-0.0990138,-0.0684038 -0.184804,-0.154194 -0.253208,-0.253208l-6.9888,-9.9742l-5.91546e-08,-8.31834e-08c-0.328807,-0.46237 -0.220533,-1.10375 0.241837,-1.43255c0.168889,-0.120103 0.370155,-0.186318 0.577363,-0.189948h4.9835l3.54852e-07,0.000503677c-0.00686545,-9.94223 -8.0722,-17.9964 -18.0144,-17.9896c-4.13508,0.00285541 -8.14312,1.42919 -11.3504,4.03924l4.60883e-08,-3.91242e-08c-0.842285,0.715013 -2.10472,0.611839 -2.81974,-0.230446c-0.715013,-0.842285 -0.611839,-2.10472 0.230446,-2.81974l3.46098e-07,-2.83814e-07c9.39634,-7.70537 23.26,-6.33457 30.9654,3.06177c3.22304,3.93035 4.98589,8.85536 4.98903,13.9382h4.9935l2.44822e-09,4.33431e-11c0.567328,0.0100438 1.0191,0.478096 1.00905,1.04542c-0.0036664,0.207098 -0.0698445,0.408252 -0.189853,0.577075Zm-40.0129,-23.1717l6.9888,9.9742l-2.71216e-09,-3.81325e-09c0.32881,0.4623 0.220594,1.10362 -0.241706,1.43243c-0.168809,0.120065 -0.369975,0.186285 -0.577094,0.189969h-4.983l2.60913e-07,0.000383721c0.0068661,9.94206 8.07207,17.9961 18.0141,17.9893c4.1354,-0.00285595 8.14372,-1.42943 11.3511,-4.03989l-7.47965e-08,6.34738e-08c0.8424,-0.714877 2.10482,-0.6115 2.8197,0.2309c0.714877,0.8424 0.6115,2.10482 -0.2309,2.8197l4.04125e-07,-3.31403e-07c-9.39617,7.70531 -23.2597,6.33461 -30.965,-3.06156c-3.22311,-3.93039 -4.98597,-8.85549 -4.98903,-13.9384h-4.9947l5.45126e-08,9.68797e-10c-0.567328,-0.0100825 -1.01906,-0.478166 -1.00898,-1.04549c0.00367938,-0.207033 0.0698308,-0.408123 0.189782,-0.576906l6.9888,-9.9742l-3.21808e-08,4.66031e-08c0.312407,-0.452417 0.93242,-0.565917 1.38484,-0.25351c0.0990287,0.0683822 0.184838,0.154154 0.253263,0.253153Z" fill="currentColor" fill-rule="evenodd"></path></svg>
|
||||
<p id="launch-text" class="text-lg">Launching Specter Desktop...</p>
|
||||
<p id="launch-text" class="text-lg">Launching Specter Desktop...</p>
|
||||
</div>
|
||||
<button id="open-settings-btn" class="hidden px-4 py-2 bg-dark-700 text-white rounded hover:bg-dark-600 cursor-pointer">Open Settings</button>
|
||||
</div>
|
||||
<script src="renderer.js"></script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -20,16 +20,27 @@ const executeJavaScript = (code) => {
|
|||
mainWindow.webContents.executeJavaScript(code)
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
function showError(error) {
|
||||
updatingLoaderMsg('Specter encountered an error:' + error.toString())
|
||||
}
|
||||
|
||||
function updatingLoaderMsg(msg, showSpinner = false) {
|
||||
function updatingLoaderMsg(msg, showSpinner = false, { isHtml = false, showSettingsButton = false } = {}) {
|
||||
if (mainWindow) {
|
||||
// see preload.js where this is setup
|
||||
mainWindow.webContents.send('update-loader-message', {
|
||||
msg,
|
||||
showSpinner,
|
||||
isHtml,
|
||||
showSettingsButton,
|
||||
})
|
||||
} else {
|
||||
logger.error('mainWindow not initialized in updatingLoaderMsg')
|
||||
|
|
@ -78,11 +89,18 @@ function initMainWindow(dimensions) {
|
|||
return { action: 'deny' }
|
||||
})
|
||||
|
||||
mainWindow.webContents.on('did-fail-load', function () {
|
||||
mainWindow.webContents.on('did-fail-load', function (event, errorCode, errorDescription, validatedURL) {
|
||||
mainWindow.loadURL(`file://${__dirname}/splash.html`)
|
||||
updatingLoaderMsg(
|
||||
`Failed to load: ${appSettings.specterURL}<br>Please make sure the URL is entered correctly in the settings and try again...</b>`
|
||||
)
|
||||
const failedUrl = escapeHtml(validatedURL || appSettings.specterURL || '')
|
||||
let msg
|
||||
if (appSettings.mode === 'hwibridge') {
|
||||
msg = `Failed to load remote Specter at: ${failedUrl}<br><br>` +
|
||||
`You are running in <b>HWI Bridge mode</b>, which requires a remote Specter server.<br>` +
|
||||
`If you want to run Specter locally instead, open <b>Settings</b> (${isMac ? 'Cmd' : 'Ctrl'}+, or via the tray icon) and switch to "Run Specter locally".`
|
||||
} else {
|
||||
msg = `Failed to load: ${failedUrl}<br>Please make sure the URL is entered correctly in the settings and try again...`
|
||||
}
|
||||
updatingLoaderMsg(msg, false, { isHtml: true, showSettingsButton: true })
|
||||
})
|
||||
return mainWindow
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
"pytest==9.0.3",
|
||||
"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"
|
||||
]
|
||||
|
|
@ -1,30 +1,32 @@
|
|||
certifi==2024.6.2
|
||||
certifi==2024.7.4
|
||||
Click==8.1.7
|
||||
Flask==2.2.5
|
||||
# Flask 2.2.x test_client uses werkzeug.__version__, removed in Werkzeug 3.1.
|
||||
Werkzeug==3.0.6
|
||||
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
|
||||
Flask-HTTPAuth==4.8.1
|
||||
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 +35,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
|
||||
|
||||
|
|
|
|||
435
requirements.txt
435
requirements.txt
|
|
@ -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,13 +263,13 @@ 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 \
|
||||
--hash=sha256:a58fedd09989b9975448eef04806b096a3964a7feeebc0a78831ff55685b62b0
|
||||
flask-httpauth==4.8.1 \
|
||||
--hash=sha256:0080393d70e12327781f7509115175ec5e47209816489a620d4fd39e20cea2e8 \
|
||||
--hash=sha256:88499b22f1353893743c3cd68f2ca561c4ad9ef75cd6bcc7f621161cd0e80744
|
||||
# via -r requirements.in
|
||||
flask-login==0.6.3 \
|
||||
--hash=sha256:5e23d14a607ef12806c699590b89d0f0e0d67baeec599d75947bf9c147330333 \
|
||||
|
|
@ -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,17 @@ 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
|
||||
werkzeug==3.0.6 \
|
||||
--hash=sha256:1bc0c2310d2fbb07b1dd1105eba2f7af72f322e1e455f2f93c993bee8c8a5f17 \
|
||||
--hash=sha256:a8dd59d4de28ca70471a34cba79bed5f7ef2e036a76b3ab0835474246eb41f8d
|
||||
# via
|
||||
# -r requirements.in
|
||||
# flask
|
||||
# flask-cors
|
||||
# flask-login
|
||||
wsproto==1.2.0 \
|
||||
--hash=sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065 \
|
||||
|
|
@ -861,5 +925,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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
""" Security Related things for the REST-API """
|
||||
"""Security Related things for the REST-API"""
|
||||
|
||||
import logging, jwt
|
||||
from functools import wraps
|
||||
|
||||
|
|
@ -14,6 +15,18 @@ from flask import current_app as app
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@auth.error_handler
|
||||
@token_auth.error_handler
|
||||
def auth_error(status):
|
||||
"""Return REST API authentication failures as JSON.
|
||||
|
||||
Flask-HTTPAuth 4.8.1 changed the default unauthorized response body to
|
||||
plain text ("Unauthorized Access"). The REST API has historically returned
|
||||
Flask-RESTful JSON error objects, and clients/tests rely on that shape.
|
||||
"""
|
||||
return abort(status)
|
||||
|
||||
|
||||
@auth.verify_password
|
||||
def verify_password(username, password):
|
||||
"""Validate user passwords and store user in the 'g' object"""
|
||||
|
|
@ -40,11 +53,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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ...")
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -1100,6 +1100,10 @@ input[type="number"]::-webkit-outer-spin-button,
|
|||
z-index: 50;
|
||||
}
|
||||
|
||||
.z-20 {
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.z-30 {
|
||||
z-index: 30;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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") }}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
"""
|
||||
|
|
|
|||
39
tests/bitcoin_SHA256SUMS
Normal file
39
tests/bitcoin_SHA256SUMS
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# Trusted reference: verbatim plaintext of https://bitcoincore.org/bin/bitcoin-core-27.2/SHA256SUMS
|
||||
# Provenance:
|
||||
# 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)
|
||||
# - achow101 (primary fingerprint 152812300785C96444D3334D17565732E08E5E41)
|
||||
# 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.
|
||||
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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
22
tests/elements_SHA256SUMS
Normal file
22
tests/elements_SHA256SUMS
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Trusted reference: plaintext hashes extracted from the clearsigned
|
||||
# SHA256SUMS.asc published with elements-0.21.0.2.
|
||||
# Provenance:
|
||||
# Source URL: https://github.com/ElementsProject/elements/releases/download/elements-0.21.0.2/SHA256SUMS.asc
|
||||
# Clearsigned: yes (PGP-SIGNED-MESSAGE, Hash: SHA256)
|
||||
# Verified on: 2026-04-15
|
||||
# Verified by: Good signature from Steven Roose <steven@stevenroose.org>
|
||||
# primary fingerprint 8CC974D9CFD034DCEED213B02A57E0A610D7F19C
|
||||
# signing subkey DE10E82629A8CAD55B700B972F2A88D7F8D68E87
|
||||
# Key sourced from: hkps://keyserver.ubuntu.com (search 0x2F2A88D7F8D68E87)
|
||||
# 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.
|
||||
06de6cd39f00a8b5604f2baa6203c08358762b99560bec6ad90a65bc22fadf6f elements-elements-0.21.0.2-aarch64-linux-gnu.tar.gz
|
||||
d263ed2182f55d0d08013f005f8db3d8d16c5f2a8e954b93f7938ceadd3798e0 elements-elements-0.21.0.2-arm-linux-gnueabihf.tar.gz
|
||||
bbeac447c8947cb6788cc5d3f8e8eade82d9d03b159992fa22f4a0198ebd8a60 elements-elements-0.21.0.2-osx64.tar.gz
|
||||
f7774aba06a4b4a3708be96774cd9e993fc58998f91a1a8cf9b692a9f888f89f elements-elements-0.21.0.2-osx-unsigned.dmg
|
||||
9cdc53b1f81a8c2f3e4b04d80de04ca1cc182d018c0c4fef1aaaf6dcc87f5b42 elements-elements-0.21.0.2-osx-unsigned.tar.gz
|
||||
cf3e2d6fc0811a3dd1e1ace640a62319af35443c76a404caf4f26674197c8059 elements-elements-0.21.0.2-riscv64-linux-gnu.tar.gz
|
||||
a3ec010bd10e8b5947030e6b846069084056418a7a1e111c0d2634a65662e53e elements-elements-0.21.0.2-win64-setup-unsigned.exe
|
||||
7a8f427be448eaa89f87c31cce705069c2d9528cf54fdd838c104f1257f800d4 elements-elements-0.21.0.2-win64.zip
|
||||
4863ee842d13bbb0255c2002ccc07f170b87717589a3493805944579a4af6745 elements-elements-0.21.0.2-win-unsigned.tar.gz
|
||||
3018116794429b77ce0dd7436c2906f8be4eb5d6163b8451c5ce7e7bedad152b elements-elements-0.21.0.2-x86_64-linux-gnu.tar.gz
|
||||
1
tests/fixtures/jade_hardware.psbt
vendored
Normal file
1
tests/fixtures/jade_hardware.psbt
vendored
Normal 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
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
15
tests/fixtures/jade_seedqr_abandon.txt
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
█████████████████████████████
|
||||
██ ▄▄▄▄▄ ██▄▀▀ ▀▄▀▄█ ▄▄▄▄▄ ██
|
||||
██ █ █ █▄ ▀▄▀▄▀▄█ █ █ ██
|
||||
██ █▄▄▄█ ██▀▄██▀▄▀▄█ █▄▄▄█ ██
|
||||
██▄▄▄▄▄▄▄█ ▀ █ ▀▄▀▄█▄▄▄▄▄▄▄██
|
||||
██ ▄▀ ▄▄▄▄█ █▀▄▀▄ ▄▀█▀▄▀███
|
||||
██ ▀██▄▀▄▀▀▄█▀██▄▀▄▀▄▀▄▀▄▀▄██
|
||||
████▀▄▄▄▄▀▀██▀ ▀█▀▄▀▄▀▄▀▄▀▄██
|
||||
██▄▀█ ██▄▀██ ▄ ▀▄▀▄▀▄▀▄▀▄▀▄██
|
||||
██▄██▄▄▄▄▄▀▀▀ ██▄▀ ▄▄▄ ▀▄▀▄██
|
||||
██ ▄▄▄▄▄ █ ▄ ▀█▄█▀ █▄█ ▀▄▀▄██
|
||||
██ █ █ █▄▄ ▀ ▀█▀▄ ▄ ▄▀▄ ███
|
||||
██ █▄▄▄█ █ ▀█▄ ▀ ▀▄▀▄▀▄▀▄█▄██
|
||||
██▄▄▄▄▄▄▄█▄█▄▄██▄█▄█▄█▄█▄█▄██
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
|
|
@ -221,6 +221,188 @@ function sub_compile {
|
|||
echo " --> install_node.sh End $(date) took $DIFF"
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Release signing trust anchors for GPG defense-in-depth verification.
|
||||
#
|
||||
# These fingerprints (and the SHA256 trust anchors committed in
|
||||
# tests/bitcoin_SHA256SUMS / tests/elements_SHA256SUMS) are the reason this
|
||||
# script exists in its hardened form: CI uses `save-always: true` caches, so
|
||||
# a single poisoned fetch would persist across every future run if the
|
||||
# verification path were ever skipped. verify_binary() MUST be invoked on
|
||||
# both cache miss (fresh download) AND cache hit (restore), and MUST abort
|
||||
# non-zero on any mismatch.
|
||||
#
|
||||
# Builder keys for bitcoin-core releases are published at:
|
||||
# https://github.com/bitcoin-core/guix.sigs/tree/main/builder-keys
|
||||
# The elements signing key was fetched from:
|
||||
# hkps://keyserver.ubuntu.com (search 0x2F2A88D7F8D68E87)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Pinned release signing keys (fingerprints). The committed SHA256SUMS files
|
||||
# in tests/ are the primary trust anchor; the GPG step below re-validates the
|
||||
# upstream chain when the network is reachable.
|
||||
BITCOIN_RELEASE_KEYS=(
|
||||
"E777299FC265DD04793070EB944D35F9AC3DB76A" # fanquake <fanquake@gmail.com>
|
||||
"152812300785C96444D3334D17565732E08E5E41" # achow101 / Ava Chow
|
||||
)
|
||||
ELEMENTS_RELEASE_KEYS=(
|
||||
"8CC974D9CFD034DCEED213B02A57E0A610D7F19C" # Steven Roose <steven@stevenroose.org>
|
||||
)
|
||||
|
||||
# Compute the sha256 of a file in a cross-platform way.
|
||||
# Usage: sha256_of <file>
|
||||
function sha256_of {
|
||||
local f=$1
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256sum "$f" | awk '{print $1}'
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
shasum -a 256 "$f" | awk '{print $1}'
|
||||
else
|
||||
echo "ERROR: neither sha256sum nor shasum found" >&2
|
||||
return 127
|
||||
fi
|
||||
}
|
||||
|
||||
# Look up the expected sha256 for ${basename_of_artifact} from the committed
|
||||
# trust anchor file tests/${node_impl}_SHA256SUMS. Comment lines (starting
|
||||
# with '#') in the trust anchor are ignored so the provenance preamble does
|
||||
# not interfere.
|
||||
# Usage: expected_sha_for <node_impl> <artifact_basename>
|
||||
function expected_sha_for {
|
||||
local node_impl=$1
|
||||
local artifact=$2
|
||||
local trust_file=""
|
||||
# tests/install_noded.sh cd's to its own dir at the top of the file, so
|
||||
# the trust anchors live one directory up from the usual CWD
|
||||
# (./${node_impl}) and sometimes in the CWD itself, depending on whether
|
||||
# verify_binary is called before or after we cd into ./${node_impl}.
|
||||
if [ -f "./${node_impl}_SHA256SUMS" ]; then
|
||||
trust_file="./${node_impl}_SHA256SUMS"
|
||||
elif [ -f "../${node_impl}_SHA256SUMS" ]; then
|
||||
trust_file="../${node_impl}_SHA256SUMS"
|
||||
else
|
||||
echo "ERROR: trust anchor tests/${node_impl}_SHA256SUMS not found" >&2
|
||||
return 2
|
||||
fi
|
||||
# Format: <hex> <filename>
|
||||
grep -v '^[[:space:]]*#' "$trust_file" \
|
||||
| awk -v f="$artifact" '$2 == f {print $1; exit}'
|
||||
}
|
||||
|
||||
# Import release signing keys into a throw-away GNUPGHOME and GPG-verify
|
||||
# ${sumsfile} against ${sigfile}. Returns 0 on good signature, non-zero on
|
||||
# anything else (including no network, missing gpg, bad sig). Callers MUST
|
||||
# still compare ${sumsfile} to the committed trust anchor — this GPG step is
|
||||
# defense in depth, not the sole trust anchor.
|
||||
# Usage: gpg_verify_sums <node_impl> <sumsfile> <sigfile>
|
||||
function gpg_verify_sums {
|
||||
local node_impl=$1
|
||||
local sumsfile=$2
|
||||
local sigfile=$3
|
||||
if ! command -v gpg >/dev/null 2>&1; then
|
||||
echo " --> WARNING: gpg not installed; skipping upstream GPG re-verify (committed SHA256SUMS trust anchor still enforced)"
|
||||
return 0
|
||||
fi
|
||||
local tmp_gnupg
|
||||
tmp_gnupg=$(mktemp -d -t specter-gnupg-XXXXXX) || return 1
|
||||
# shellcheck disable=SC2064
|
||||
trap "rm -rf '$tmp_gnupg'" RETURN
|
||||
export GNUPGHOME="$tmp_gnupg"
|
||||
chmod 700 "$tmp_gnupg"
|
||||
local keys=()
|
||||
if [ "$node_impl" = "bitcoin" ]; then
|
||||
keys=("${BITCOIN_RELEASE_KEYS[@]}")
|
||||
elif [ "$node_impl" = "elements" ]; then
|
||||
keys=("${ELEMENTS_RELEASE_KEYS[@]}")
|
||||
fi
|
||||
local imported=0
|
||||
for fpr in "${keys[@]}"; do
|
||||
# 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
|
||||
if [ "$imported" -eq 0 ]; then
|
||||
echo " --> WARNING: could not fetch any ${node_impl} release signing keys; skipping upstream GPG re-verify"
|
||||
unset GNUPGHOME
|
||||
return 0
|
||||
fi
|
||||
# Bitcoin Core SHA256SUMS.asc carries signatures from many maintainers.
|
||||
# gpg --verify exits non-zero when ANY signature's key is missing, even
|
||||
# if others verify successfully. We only import a subset of keys, so
|
||||
# the raw exit code is unreliable. Instead, use --status-fd to check
|
||||
# for at least one VALIDSIG whose primary-key fingerprint we trust.
|
||||
#
|
||||
# VALIDSIG format (last field is always the primary key fingerprint,
|
||||
# even when the signature was made by a subkey):
|
||||
# VALIDSIG <sign_fpr> <date> <ts> ... <primary_fpr>
|
||||
# GOODSIG uses the signing subkey ID, which may differ from the primary
|
||||
# fingerprint we pin — so VALIDSIG is the correct field to match.
|
||||
local status_out
|
||||
if [ -n "$sigfile" ] && [ -f "$sigfile" ]; then
|
||||
status_out=$(gpg --status-fd 1 --verify "$sigfile" "$sumsfile" 2>/dev/null)
|
||||
else
|
||||
# Clearsigned (elements case): verify in-place.
|
||||
status_out=$(gpg --status-fd 1 --verify "$sumsfile" 2>/dev/null)
|
||||
fi
|
||||
local found_good=0
|
||||
for fpr in "${keys[@]}"; do
|
||||
# Match VALIDSIG line whose last field (primary fingerprint) equals ours.
|
||||
if echo "$status_out" | grep -q "VALIDSIG.*${fpr}"; then
|
||||
found_good=1
|
||||
echo " --> GPG: valid signature traced to primary key ${fpr}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
unset GNUPGHOME
|
||||
if [ "$found_good" -eq 0 ]; then
|
||||
echo " --> WARNING: no VALIDSIG from a trusted primary key found in GPG status output"
|
||||
echo " --> Status output: $status_out"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Verify a binary artifact (tarball) against the committed trust anchor.
|
||||
# Aborts the script with exit 2 on any mismatch.
|
||||
# Usage: verify_binary <node_impl> <path_to_binary_file>
|
||||
function verify_binary {
|
||||
local node_impl=$1
|
||||
local binary_path=$2
|
||||
if [[ ! -f "$binary_path" ]]; then
|
||||
echo "ERROR: verify_binary: $binary_path does not exist" >&2
|
||||
exit 2
|
||||
fi
|
||||
local artifact
|
||||
artifact=$(basename "$binary_path")
|
||||
local expected
|
||||
expected=$(expected_sha_for "$node_impl" "$artifact")
|
||||
if [ -z "$expected" ]; then
|
||||
echo "ERROR: verify_binary: no trusted sha256 entry for '$artifact' in tests/${node_impl}_SHA256SUMS" >&2
|
||||
exit 2
|
||||
fi
|
||||
local actual
|
||||
actual=$(sha256_of "$binary_path") || exit 2
|
||||
if [ "$expected" != "$actual" ]; then
|
||||
echo "ERROR: verify_binary: sha256 mismatch for $artifact" >&2
|
||||
echo " expected $expected" >&2
|
||||
echo " actual $actual" >&2
|
||||
echo " This is a trust violation. Aborting." >&2
|
||||
exit 2
|
||||
fi
|
||||
echo " --> verify_binary: OK ($artifact sha256=$expected)"
|
||||
}
|
||||
|
||||
function sub_binary {
|
||||
node_impl=$1
|
||||
echo " --> install_noded.sh Start $(date) (binary) for node_impl $node_impl"
|
||||
|
|
@ -236,16 +418,52 @@ function sub_binary {
|
|||
else
|
||||
binary_file=${node_impl}-${version}-x86_64-linux-gnu.tar.gz
|
||||
fi
|
||||
if [[ ! -f $binary_file ]]; then
|
||||
|
||||
# The tarball lives inside ./${node_impl}/ so that it is captured by the
|
||||
# cache (which covers ./tests/${node_impl}/) and re-verified on every
|
||||
# cache restore. This is the whole point of the hardened flow — see the
|
||||
# "save-always backdoor" rationale at the top of the file.
|
||||
# Clean up a broken symlink from a partial cache restore (the workflow
|
||||
# caches ./tests/${node_impl}-* as well, but be defensive).
|
||||
if [[ -L "./${node_impl}" && ! -e "./${node_impl}" ]]; then
|
||||
echo " --> cleaning up dangling symlink ./${node_impl}"
|
||||
rm -f "./${node_impl}"
|
||||
fi
|
||||
mkdir -p "./${node_impl}"
|
||||
local cached_tarball="./${node_impl}/${binary_file}"
|
||||
|
||||
if [[ -f "$cached_tarball" ]]; then
|
||||
echo " --> cache hit: found existing ${cached_tarball}, re-verifying"
|
||||
verify_binary "$node_impl" "$cached_tarball"
|
||||
else
|
||||
echo " --> cache miss: downloading ${binary_file}"
|
||||
if [ "$node_impl" = "elements" ]; then
|
||||
wget https://github.com/ElementsProject/elements/releases/download/${version}/${binary_file}
|
||||
# elements publishes a clearsigned SHA256SUMS.asc only
|
||||
( cd "./${node_impl}" && \
|
||||
wget -q "https://github.com/ElementsProject/elements/releases/download/${version}/${binary_file}" && \
|
||||
wget -q "https://github.com/ElementsProject/elements/releases/download/${version}/SHA256SUMS.asc" )
|
||||
gpg_verify_sums "$node_impl" "./${node_impl}/SHA256SUMS.asc" "" \
|
||||
|| { echo "ERROR: upstream GPG verification failed for elements SHA256SUMS.asc" >&2; exit 2; }
|
||||
fi
|
||||
if [ "$node_impl" = "bitcoin" ]; then
|
||||
wget https://bitcoincore.org/bin/bitcoin-core-${version}/${binary_file}
|
||||
( cd "./${node_impl}" && \
|
||||
wget -q "https://bitcoincore.org/bin/bitcoin-core-${version}/${binary_file}" && \
|
||||
wget -q "https://bitcoincore.org/bin/bitcoin-core-${version}/SHA256SUMS" && \
|
||||
wget -q "https://bitcoincore.org/bin/bitcoin-core-${version}/SHA256SUMS.asc" )
|
||||
gpg_verify_sums "$node_impl" "./${node_impl}/SHA256SUMS" "./${node_impl}/SHA256SUMS.asc" \
|
||||
|| { echo "ERROR: upstream GPG verification failed for bitcoin SHA256SUMS.asc" >&2; exit 2; }
|
||||
fi
|
||||
# Primary trust anchor: committed tests/${node_impl}_SHA256SUMS.
|
||||
verify_binary "$node_impl" "$cached_tarball"
|
||||
fi
|
||||
|
||||
tar -xzf ${binary_file}
|
||||
# Extract into ./${node_impl}-${version}/ (tarballs ship that layout).
|
||||
# Leave the tarball in place inside ./${node_impl}/ to be reused on
|
||||
# subsequent cache hits (that directory becomes a symlink below, so we
|
||||
# copy the tarball out to a sibling and re-plant it after the symlink).
|
||||
local tarball_basename="${binary_file}"
|
||||
cp "$cached_tarball" "./${tarball_basename}.verified"
|
||||
tar -xzf "$cached_tarball"
|
||||
if [[ -d ./"$node_impl" ]]; then
|
||||
if [[ -d ./"$node_impl"/src ]]; then
|
||||
mv ./"$node_impl" ./"$node_impl"-src
|
||||
|
|
@ -253,8 +471,12 @@ function sub_binary {
|
|||
rm -rf ./"$node_impl"
|
||||
fi
|
||||
fi
|
||||
rm "$node_impl"
|
||||
rm -f "$node_impl"
|
||||
ln -s ./"$node_impl"-${version} "$node_impl"
|
||||
# Re-plant the verified tarball inside the (now-symlinked) cached dir so
|
||||
# the next cache restore can re-verify it.
|
||||
mv "./${tarball_basename}.verified" "./${node_impl}/${binary_file}"
|
||||
|
||||
echo " --> Listing binaries"
|
||||
if [ $(uname) = "Darwin" ]; then
|
||||
find ./"$node_impl"/bin -maxdepth 1 -type f -perm +111 -exec ls -ld {} \;
|
||||
|
|
@ -263,6 +485,11 @@ function sub_binary {
|
|||
fi
|
||||
echo " --> checking for ${node_impl}d"
|
||||
test -x ./${node_impl}/bin/${node_impl}d || exit 2
|
||||
|
||||
# Final defense-in-depth: re-verify the tarball one more time so that
|
||||
# tampering between extract and exit is caught as well.
|
||||
verify_binary "$node_impl" "./${node_impl}/${binary_file}"
|
||||
|
||||
echo " --> Finished installing ${node_impl}d binary"
|
||||
END=$(date +%s)
|
||||
DIFF=$(echo "$END - $START" | bc)
|
||||
|
|
|
|||
|
|
@ -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
180
tests/test_hwi_server.py
Normal 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
140
tests/test_jade_hardware.py
Normal 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"
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
177
tests/test_wallet_create_range.py
Normal file
177
tests/test_wallet_create_range.py
Normal 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
|
||||
|
|
@ -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']}")
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -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
|
||||
532
utils/github.py
532
utils/github.py
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue