15 KiB
AGENTS.md — Specter Desktop
Guidelines for AI contributors working on specter-desktop.
Project Overview
Specter Desktop is a GUI for Bitcoin Core & Electrum optimized for airgapped hardware wallets. It's a Flask web app that manages multisig wallets, coordinates hardware wallet signing via HWI, and handles PSBT-based transaction workflows.
License: MIT Stack: Python 3.9-3.10, Flask, Jinja2 templates, plain JavaScript (no frameworks), JSON file persistence, PyInstaller for desktop builds 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)
src/cryptoadvance/specter/
├── server.py # Flask app factory, create_and_init()
├── cli/ # CLI entry point (specter command)
├── managers/ # Core business logic
│ ├── device_manager.py # Hardware wallet device registry
│ ├── wallet_manager.py # Wallet CRUD + balance/tx tracking
│ ├── node_manager.py # Bitcoin Core / Electrum connections
│ └── user_manager.py # Auth + RBAC
├── rpc.py # Bitcoin Core JSON-RPC client
├── persistence.py # JSON file storage (no SQL)
├── device.py # Device domain model + HWI glue
├── wallet/ # Wallet domain logic, descriptors, address/tx helpers
├── util/
│ └── psbt.py # PSBT utilities (parse, analyze, finalize)
├── devices/
│ └── hwi/ # HWI-based hardware wallet drivers (Jade, KeepKey, DIY)
├── hwi_rpc.py # HWI JSON-RPC wrapper
├── hwi_server.py # HWI bridge subprocess (for GUI builds)
├── templates/ # Jinja2 HTML templates
├── static/ # CSS/JS/images
└── services/ # Extension system (specterext namespace packages)
Key patterns:
- Manager pattern: each domain has a
*Managerclass that owns the lifecycle - JSON file persistence in
~/.specter/(devices, wallets, nodes as JSON files) - Extensions via
specterextnamespace packages with their own Flask blueprints - HWI bridge for hardware wallet communication (Trezor, Ledger, ColdCard, etc.)
- PSBT workflow: construct → sign (hardware) → broadcast
Running from Source
# Prerequisites (Ubuntu/Debian)
sudo apt install libusb-1.0-0-dev libudev-dev libffi-dev libssl-dev build-essential
# Clone and set up
git clone https://github.com/cryptoadvance/specter-desktop.git
cd specter-desktop
pip3 install virtualenv
virtualenv --python=python3.10 .env
source .env/bin/activate
pip3 install -r requirements.txt --require-hashes
pip3 install -e .
# Run dev server
python3 -m cryptoadvance.specter server --config DevelopmentConfig --debug
# → http://127.0.0.1:25441/
Python version: 3.9 or 3.10 required. 3.11+ will break on some dependencies.
CI/CD
The project uses GitHub Actions for linting, testing, Docker images, and the full release pipeline, plus Cirrus CI for the heavyweight test suite (pytest + Cypress). GitLab CI is retained in .gitlab-ci.yml but effectively dead — the release flow was migrated to GitHub Actions.
Overview
| Provider | Purpose | Config File | Trigger |
|---|---|---|---|
| GitHub Actions | Lint, smoke-build, Docker images, releases (pip + specterd + Electron for Linux/Win/macOS) | .github/workflows/ |
Push, PR, tags |
| Cirrus CI | Full test suite (pytest + Cypress + extension smoketest) | .cirrus.yml |
PR |
| GitLab CI | Dead — config retained; check job only waits on GH master status. Release jobs no longer run. |
.gitlab-ci.yml |
(vestigial) |
GitHub Actions (7 workflows)
- Black Python Linter (
zblack.yml) — Runs on every push and PR. Usespsf/black@26.3.0action pinned to Black version22.3.0, on python-3.12 (pinned to avoid 3.14 incompatibility with Black 22.3.0). Checks./src. - TOC Generator (
toc.yml) — Auto-generates TOCs forREADME.md,docs/faq.md,docs/development.mdon push. - Docker Push (
docker-push.yml) — Builds multi-arch (amd64 + arm64) image on every push. Pushes toghcr.io/<owner>/<repo>:<branch>(upstream:ghcr.io/cryptoadvance/specter-desktop:<branch>). - Docker Tag (
docker-tag.yml) — Builds multi-arch image on version tags. Pushes toghcr.io/<owner>/<repo>:<tag>. - Extension Compatibility Check (
extension-compat.yml) — On changes torequirements.*orpyproject.toml: installs the full lock file, imports every bundled extension (Swan, LiquidIssuer, DevHelp, Notifications, ExFund, Faucet, Electrum, Spectrum, StackTrack, TimelockRecovery), runspip check, and best-effort runs extension test suites. Catches dep conflicts before they break downstream extensions. - Test specterd build (
test-specterd-build.yml) — PR smoke test on changes topyinstaller/,requirements*,src/**, or packaging files. Builds specterd on Linux and runs--helpsmoke test. - Release (
release.yml) — The release pipeline. See next section.
All GitHub Actions use public runners (ubuntu-latest / ubuntu-24.04 / windows-latest / macos-14). No private runners.
Cirrus CI (Testing)
Cirrus CI runs the full test suite on PRs. Config: .cirrus.yml.
Three tasks:
test_task— Full pytest suite with bitcoind + elementsd in regtest mode. Uses cached binary downloads. Produces JUnit XML results.cypress_test_task— Frontend tests with Cypress. Requires 6 CPU, 6GB RAM. Produces screenshots and video artifacts.extension_smoketest_task— Generates a test extension, starts the server, verifies the extension loads and responds.
Docker images used:
registry.gitlab.com/cryptoadvance/specter-desktop/cirrus-jammy:20230206(pytest)registry.gitlab.com/cryptoadvance/specter-desktop/cypress-python-jammy:20230206(Cypress)
Both images are pre-built and hosted on the GitLab container registry. They include Python, virtualenv, and other dependencies. The docker/ directory in the repo contains Dockerfiles for building them.
Caching: bitcoind and elementsd binaries are cached by Cirrus based on the version pinned in pyproject.toml. The tests/install_noded.sh script handles downloading or compiling them.
Release pipeline — .github/workflows/release.yml
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.
Python version: pinned to 3.10 via the PYTHON_VERSION env var at the top of release.yml.
Jobs (10 total):
release-pip— Builds the pip package and publishes to PyPI via trusted publishing (noTWINE_PASSWORDsecret needed when configured on PyPI; falls back to token auth otherwise). Only publishes whengithub.repository == 'cryptoadvance/specter-desktop'. Version derived from tag with-pre→rcPEP 440 mapping.build-specterd-linux— PyInstaller build onubuntu-latest. Producesspecterd-<version>-x86_64-linux-gnu.zip.build-specterd-windows— PyInstaller build onwindows-latest. Producesspecterd-<version>-win64.zip. Installscolorama(Windows-only transitive dep of click that isn't in the lock file).build-specterd-macos— PyInstaller build onmacos-14(Apple Silicon). Producesspecterd-<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.build-electron-linux— Needsbuild-specterd-linux. Downloads specterd artifact, wraps in Electron, producesspecter_desktop-<version>-x86_64-linux-gnu.tar.gz.build-electron-windows— Needsbuild-specterd-windows. Runs inelectronuserland/builder:winecontainer onubuntu-latest. ProducesSpecter-Setup-<version>.exe.build-electron-macos— Needsbuild-specterd-macos. Universal-ish build onmacos-14. Code signing is conditional: ifAPPLE_CERTIFICATE_BASE64secret is set, imports cert into a temporary keychain and signs; otherwise builds unsigned. Removes hardcoded provisioning profile frompackage.jsonon the fly.create-release— Gathers all artifacts, computes combinedSHA256SUMS, creates the GitHub Release, uploads binaries.trigger-docker— Triggers thedocker-tag.ymlworkflow for the release tag (builds and pushes the Docker image).
Release artifacts per version:
cryptoadvance.specter-<version>.tar.gz(pip package, published to PyPI)specterd-<version>-x86_64-linux-gnu.zip(Linux daemon)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)Specter-Setup-<version>.exe(Windows Electron app)- macOS Electron app (produced by
build-electron-macos) SHA256SUMS(combined checksums, created bycreate-release)
macOS builds are now automated (Apple Silicon free tier). x86_64 macOS requires a paid runner and is disabled.
Release-related secrets (GitHub)
| Secret | Purpose | Required? |
|---|---|---|
APPLE_CERTIFICATE_BASE64 |
Base64-encoded .p12 Apple signing certificate |
Optional — unsigned build if missing |
APPLE_CERTIFICATE_PASSWORD |
Password for the .p12 |
With APPLE_CERTIFICATE_BASE64 |
APPLE_PROVISIONING_PROFILE_BASE64 |
Base64-encoded provisioning profile | Optional |
| PyPI trusted publisher | Configured on PyPI side, not a GH secret | Required for release-pip in upstream |
Historical GitLab secrets (GH_BIN_UPLOAD_PW, TWINE_PASSWORD, GPG_PASSPHRASE, SSH_SPECTEREXT_DEPLOY_KEY, SSH_SPECTERSTATIC_DEPLOY_KEY) are no longer used. .gitlab-ci.yml still references them but the pipeline is dead.
Testing a release on a fork
- Fork
cryptoadvance/specter-desktopon GitHub. - Push a tag matching
v*.*.*orv*.*.*-*on your fork →release.ymlfires automatically. - The
release-pipPyPI publish step is gated ongithub.repository == 'cryptoadvance/specter-desktop', so forks build the pip package but don't publish. - Unsigned macOS builds work out of the box; signing requires you to add your own Apple secrets.
Dead config
.gitlab-ci.yml— still in the repo but release jobs no longer run. Safe to remove in a cleanup pass.pyinstaller/build-win-ci.bat— former GitLab Windows runner entry point; no longer invoked.utils/release.sh/utils/release_helper.py/utils/github.py— may contain dead code paths now that GitLab isn't uploading artifacts. Audit before changes.
Testing
Tests require a bitcoind binary (regtest mode). No tests run without it.
# Install bitcoind for tests
./tests/install_noded.sh --bitcoin binary # downloads binary
# OR
./tests/install_noded.sh --bitcoin compile # compiles from source
# For Elements/Liquid tests (optional):
./tests/install_noded.sh --elements binary
# Install test dependencies
pip3 install -e ".[test]"
# Run tests
pytest # all tests (needs bitcoind)
pytest -m "not slow" # skip slow tests
pytest -m "not elm" # skip Elements/Liquid tests
pytest -m "not elm and not slow" # skip both
pytest tests/test_specter.py # specific file
pytest tests/test_specter.py -k Manager # match test name
pytest --capture=no --log-cli-level=DEBUG # verbose output
pytest --bitcoind-log-stdout # include bitcoind logs
Cypress (frontend tests):
npm ci # install JS dependencies first
./utils/test-cypress.sh run # run all
./utils/test-cypress.sh open # interactive mode
./utils/test-cypress.sh snapshot spec_empty_specter_home.js # create baseline
./utils/test-cypress.sh dev spec_empty_specter_home.js # dev against snapshot
Important test quirks:
- bitcoind starts once for all tests — blockchain state accumulates
- Halving interval is 150 blocks in regtest; 100 blocks needed for spendable coins
- Transaction IDs change depending on whether you run tests alone or together
- Don't hardcode txids in assertions across test suites
- You need Python 3.10 virtualenv for tests (
ignore_cleanup_errorskwarg fails on 3.9)
Code Style
- Black for Python formatting (v22.3.0). Pre-commit hook:
pre-commit install - CI runs a Black linter check (
.github/workflows/zblack.yml) — currently failing - Plain JavaScript, no frameworks. Material Icons for UI.
- Colors: orange
#F5A623, blue#4A90E2 - Minimize dependencies — security-conscious project
Building Releases
Desktop builds use PyInstaller + Electron:
- specterd (daemon binary):
pyinstaller specterd.specfrompyinstaller/dir - Electron app: wraps specterd, downloads it on first launch with SHA256 + GPG verification
- Platform scripts:
utils/build-osx.sh,utils/build-unix.sh,pyinstaller/build-win-ci.bat - pip package:
python3 -m build
See docs/build-instructions.md for step-by-step manual build instructions.
Dependencies
# After changing requirements.in:
pip-compile --generate-hashes requirements.in
Hash-pinned requirements for reproducibility and security. Don't bypass --require-hashes.
Extension System
Extensions live in specterext namespace packages. Each extension:
- Has its own Flask blueprint, templates, and static files
- 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) - See
docs/extensions/for the extension development guide
Key Files for Navigation
| What | Where |
|---|---|
| App factory | src/cryptoadvance/specter/server.py |
| CLI entry | src/cryptoadvance/specter/cli/ |
| Config classes | src/cryptoadvance/specter/config.py |
| RPC client | src/cryptoadvance/specter/rpc.py |
| Wallet model | src/cryptoadvance/specter/wallet/ |
| Device model | src/cryptoadvance/specter/device.py |
| PSBT handling | src/cryptoadvance/specter/util/psbt.py |
| All managers | src/cryptoadvance/specter/managers/ |
| Templates | src/cryptoadvance/specter/templates/ |
| Tests | tests/ |
| CI config | .github/workflows/, .cirrus.yml, .gitlab-ci.yml |
| Build scripts | pyinstaller/, utils/, electron/ |
| CI Docker images | docker/ |
Current State (as of 2026-04)
- Last release: v2.1.1 (2025-01-03) — no release on the new GH Actions pipeline yet; first tagged run will exercise it end-to-end.
- CI migration complete: release pipeline moved from GitLab to
.github/workflows/release.yml..gitlab-ci.ymlretained but dead. - 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.0action + 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 listfor current state. - Goal: Revive with biweekly tested releases, shake out the new release pipeline, triage issues.
Contributing
- Fork → branch from
master→ PR - Run
pre-commit installfor Black formatting - Reference issues in commits:
Fixes #123 - See
CONTRIBUTING.mdfor full guidelines