Repository Public
1# SP Wallet23Private, single-owner, self-hosted multi-chain wallet — `wallet.spboucher.ai`, hosted on MacLustr.45- **Networks (mainnet):** Ethereum, Base, Arbitrum One, Polygon PoS, Bitcoin, Solana6- **Assets:** native coin of each network + native USDC (Circle) on the five networks that have it7- **Access:** one owner, passkey only (WebAuthn, user verification required). No password, no signup, no recovery by e-mail.8- **UI:** phone-first PWA, French/English, dark/light.910> Sized for a small balance (tens of USDC). It is a software hot wallet on a home cluster, not an HSM. Read11> `docs/adr/0001-custody.md` and `docs/threat-model/threat-model.md` before raising the limits.1213## How a transfer is authorized1415```16phone (passkey) ──HTTPS──▶ API (node A) ──HMAC, LAN──▶ isolated signer (node B, holds the keys)17```18191. The API drafts a transfer, checks balances on-chain, simulates it and pre-checks the signer limits.202. `request-approval` freezes every chain parameter (nonce / UTXOs / blockhash, fees) into an **intent**, serialized21 deterministically and hashed (`SPW-INTENT-v1`, SHA-256).223. The passkey signs a WebAuthn assertion whose **challenge is that hash**.234. The signer — a separate process on a separate machine — verifies the assertion against the passkey public key it pinned at24 setup, re-checks its own limits / expiry / replay cache / halt flag, **rebuilds the transaction from the intent** (only native25 transfers and allowlisted USDC transfers can be expressed) and signs once.265. The API relays the signed bytes and tracks them to finality. A returned hash is never shown as success.2728A stolen session, a compromised API node or a tampered database cannot produce a signature: none of them holds the passkey, and29any changed field changes the hash. What they *can* do is listed in the threat model (mainly: show a misleading review screen).3031## Layout3233| Path | Role |34|---|---|35| `packages/core` | network/asset registry, exact amounts, address validation, canonical intent + hash, transaction builders (EVM, BTC, Solana), policy, audit hash chain, state machine |36| `services/isolated-signer` | keystore (scrypt + AES-256-GCM), key derivation, passkey verification, policy enforcement, signing; CLI |37| `services/api` | Fastify API: passkey auth, sessions, chain adapters with dual-RPC checks, transfer lifecycle, reconciliation worker, audit log; serves the web app |38| `apps/web` | Vite + React + Tailwind PWA |39| `tests/` | software passkey helper, Playwright e2e |40| `docs/` | custody ADR, threat model, runbooks |4142Deviations from the original specification are listed in `docs/security/acceptance.md`.4344## Commands4546```bash47pnpm install48pnpm typecheck49pnpm test # 38 unit/integration/security tests (needs local Postgres db "spw_test")50pnpm build # dist/api.mjs, dist/signer.mjs, dist/web/51scripts/dev-stack.sh # local stack on http://localhost:8650, throwaway keys, broadcast OFF52pnpm e2e # Playwright, phone viewport, virtual passkey (run dev-stack.sh first)53scripts/dev-stop.sh54```5556## Signer CLI (on the signer machine)5758```bash59node signer.mjs status # key id, addresses, limits, journal integrity (no secrets)60node signer.mjs export-recovery # interactive terminal only: prints the 24 words once, for paper backup61node signer.mjs halt | unhalt # emergency stop / resume (then: pm2 restart sp-wallet-signer)62node signer.mjs reset-passkeys && node signer.mjs setup-token # lost passkey: re-run owner setup63```6465Limits live in `~/sp-wallet-signer-data/policy.json` on the signer machine (bump `version` when editing, then restart).66