Beta software — read
DISCLAIMER.mdbefore use. No warranty. Non-custodial: your funds are always recoverable from any BIP39-compatible wallet using your seed phrase; PiWalletSV is not required. Report security issues perSECURITY.md.
Air-gapped Bitcoin SV wallet built on a Raspberry Pi, a TFT bonnet (joystick + buttons), and an ArduCam OV5647 camera. Signing is performed entirely offline; a companion web app on your phone or laptop handles all on-chain work.
User-facing site: piwalletsv.com · Live wallet: app.piwalletsv.com
The wire format, QR transport, derivation rules, and SPV requirements are documented as an open spec in docs/protocol/ so any project can build a compatible companion (or a compatible signer) against PiWalletSV. Canonical test vectors live in tests/fixtures/.
Python 3.11+ (Python 3.14 is not yet supported by all transitive wheels; use 3.13 on macOS).
python3.13 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
pytest- CLI:
piwallet --help— vault, sign, decode, multipart QR (piwallet qr join,piwallet qr scan-cameraon Pi), USB backup (piwallet backup export|import). - Pi bring-up:
./scripts/sync-to-pi.sh user@host --bootstraporbash scripts/bootstrap-pi-dev.shon the Pi. Smoke tests:./scripts/run_display_demo.sh,./scripts/run_bonnet.sh. See docs/build.md.
Vanilla TypeScript + Vite, in companion/. The shared PW1 multipart-QR module is byte-for-byte compatible with piwallet.qr on the Pi.
cd companion
npm install
npm run dev # https://localhost:5173/ + https://<lan-ip>:5173/
npm test # vitest: round-trip vs tests/fixtures/proposal_01.cbor
npm run build # tsc --noEmit + vite build to companion/distRoutes:
/#/wallets— paired-wallet list (IndexedDB). Rename, copy xpub, remove. Default landing surface; the Pi side is unaffected by removals./#/wallets/<id>— wallet detail with Balance, Send, Receive, and Recent addresses sections. Balance does a gap-limit-20 UTXO scan via WhatsOnChain acrossm/0/iandm/1/i. Send runs greedy coin selection, fetches per-input TSC Merkle proofs + block headers, builds a BSV BEEF via@bsv/sdk, and emits anunsigned_proposalanimated as PW1 frames. Receive derivesm/0/<index>children, addresses computed via@scure/bip32+@noble/hashesand cross-checked againstpiwallet.core.derivationbyte-for-byte./#/scan—getUserMedia+jsqr+MultipartAssemblerreassembles a PW1 stream. Recognises real envelopes (xpub_export,unsigned_proposal,signed_tx) and renders the right action card.signed_txPOSTs raw hex to WhatsOnChain/tx/raw, surfaces the returned txid + awhatsonchain.com/tx/<txid>link, and warns if the broadcaster echoes a different txid than the Pi signed./#/loop(dev only) — round-trips every envelope kind (build → CBOR + gzip → PW1 split → reassemble → gunzip + CBOR decode) on page load. Tree-shaken out of production builds.
Cross-domain links (footer, "Why is this safe?", terms-modal pointers) are driven by VITE_DOCS_BASE_URL at build time so dev mirrors stay self-contained. See companion/.env and companion/src/lib/config.ts.
Until the bonnet display (Phase 2) drives QR rendering directly, you can pipe the envelope through qrencode and scan the terminal from the phone:
sudo apt install qrencode
piwallet xpub-export --wallet-id <id> -o /tmp/xpub.bin
piwallet qr split --chunk-chars 48 /tmp/xpub.bin |
while IFS= read -r line; do
clear
qrencode -t UTF8 -- "$line"
sleep 0.4
doneThen on the phone, open https://<pi-or-laptop-lan-ip>:5173/#/scan, hit Start camera, and point it at the Pi terminal. When the assembler completes, the Save as paired wallet card appears; the wallet shows up under /#/wallets. The reverse direction (laptop/phone PW1 → Pi camera) already works via piwallet qr scan-camera.
piwallet decode /tmp/xpub.bin prints the same human-readable summary the web app shows for sanity checks.
getUserMedia is only exposed over a secure origin, so npm run dev serves HTTPS by default with a throwaway self-signed cert (via @vitejs/plugin-basic-ssl).
- Connect your phone to the same Wi-Fi.
- Open the Network URL Vite logs (e.g.
https://192.168.x.y:5173/). - iOS Safari → "This Connection Is Not Private" → Show Details → visit this website → Visit Website. After that the Scan tab can request the camera.
- To skip the warning entirely, generate a real-looking cert with
mkcert, install its root CA on the phone, and wire the cert files intovite.config.ts(server.https = { key, cert }).
Set PIWALLET_HTTP=1 npm run dev to disable HTTPS for plain localhost work.
PiWalletSV is designed so that anyone can build a compatible companion or signer against it. The full wire-format spec is in docs/protocol/:
derivation.md— BIP39/32/44 layout, P2PKH address encoding, wallet fingerprint.envelopes.md— CBOR + gzip shapes forxpub_export,unsigned_proposal,signed_tx.qr-transport.md— thePW1multipart QR framing.spv.md— what a v1 signer verifies before producing signatures (BEEF + Merkle path + header anchors + change re-derivation + value conservation).conformance.md— canonical test vectors intests/fixtures/you can diff your implementation against.
The reference Python signer in piwallet/ and the reference TypeScript companion in companion/ both validate themselves against these fixtures on every test run, so they cannot silently drift from the spec.