Files
smirk-backend-core/README.md
T

161 lines
8.1 KiB
Markdown

# smirk-backend-core
> Status: stable · Updated 2026-09-27 · Applies to: backend v0.3.0 source
Open, self-hostable backend for the [Smirk](https://smirk.cash) non-custodial
multi-chain wallet. Rust + Axum + PostgreSQL.
It gives a wallet chain access (balances, history, UTXOs, spend inputs, fee
estimation, broadcast), Nostr-native identity, a fiat price feed, an async
Grin slatepack relay, and a public social-tips subsystem, all **without ever
holding a spend key or seed**. Run your own; the wallet is backend-agnostic.
The source targets v0.3.0. Release readiness depends on the checks for the exact
candidate and its deployment evidence; the version number is not an audit or a
deployment record.
## Non-custodial by construction
The backend stores public addresses and identity keys. For Monero and Wownero,
it receives private incoming view keys and forwards them to the light wallet
server, which stores them to scan incoming payments. The backend does not persist
those view keys in its own database. Spend keys and seeds remain on the client.
The wallet signs locally, and the backend relays signed transactions to the chain.
## Chains
| Chain | Source | Notes |
|-------|--------|-------|
| Bitcoin, Litecoin | Electrum / Fulcrum | reads, fee estimation, broadcast |
| Monero, Wownero | light-wallet-server (LWS) | stateless view-key forwarding |
| Grin | grin-lws (default) / grin-wallet (view-only) + node | `rewind_hash` scan, broadcast, slatepack relay; scans proxy to grin-lws when configured, falling back to the authoritative grin-wallet scan |
Each chain is independently feature-flagged; a chain whose source isn't configured
is reported `enabled: false` by `/capabilities` rather than failing at call time.
## API
The HTTP contract is generated from the handlers and committed to
[`openapi.json`](openapi.json) — the single source of truth for the API and the
wallet's generated client. The declared workflow checks regeneration for drift;
release evidence must confirm that the workflow actually ran for the candidate.
Public surface (`/api/v1`):
- **Identity & auth** — NIP-98 (Nostr) sign-in + link, wallet-signature auth,
audience-separated JWT sessions, NIP-05 directory (`/.well-known/nostr.json`).
- **Chain access** — per-chain balance / history / UTXOs / spend inputs / fee /
broadcast.
- **Grin relay** — non-custodial store-and-forward mailbox for interactive Grin
transfers (feature-flagged).
- **Social tips** (`/tips/social/*`, feature-flagged): the public send-a-tip
surface across BTC/LTC/XMR/WOW and Grin. Roughly ten endpoints cover the
lifecycle: create (two-phase draft), attach-funding, public metadata for a
share-URL holder, sent, received, claimable, cancel, claim, confirm-sweep, and
clawback. A `TipStatus` state machine (`draft → pending_confirmation → pending →
claiming → claimed`, plus `cancelled` / `clawed_back` / `funding_mismatch`)
governs the row. The backend never holds the spend key: for BTC/LTC/XMR/WOW the
tip key rides the share URL; Grin tips use a voucher/commitment model verified
against the node's `get_outputs` / `get_kernel`. Three background workers keep it
honest, and every one leaves a row untouched on any upstream error (never a
status flip): a funding verifier (confirmation-count + on-chain amount check
before a tip becomes claimable), a sweep reconciler (settles `claiming → claimed`
only after the claim sweep confirms on-chain to the per-asset depth, with reorg
revert), and lifecycle/draft GC janitors for abandoned rows.
- **Capabilities** (`/capabilities`) — what this instance enables, so the wallet
adapts per-instance.
- **Prices** (`/prices`) — cached fiat feed, per-feed operator control.
- **Self-service erasure** (`/account/erasure`, `/account/export`) — action-bound,
two-phase delete-my-data + export.
- **Health** (`/health`) and an optional, default-off public landing (`/`).
## Operator surface
Operator/admin functions live on a **separate loopback listener** (front it with
Tor or an SSH tunnel) — confidentiality by socket, never merged into the public
router or OpenAPI.
- **Operator Console**: an embedded single-page app served at `/admin` on the
loopback admin plane. Sign in with your admin key, then review status, manage
admin keys and invite codes, and edit the runtime config overlay (env plus a
validated DB layer). Guide: [docs/operations/CONSOLE.md](docs/operations/CONSOLE.md).
- **Sign-in-with-Smirk admin auth** — a NIP-98 *signed action* over a single-use
nonce (AUTHN) + a MAC-protected key allowlist (AUTHZ), composed so a route can't
run without both.
- **First-run bootstrap** — an explicit, MAC-protected latch (no trust-on-first-
use); a live deployment adopts as already-bootstrapped; tampering fails closed.
- **`smirk-admin` CLI** — break-glass key management, `create-admin-wallet`
(zeroized), `doctor`. Talks to Postgres directly, bypassing the network plane.
- **Tamper-evident audit** — privileged actions are written to a hash-chained
audit trail, fail-closed (the state change rolls back if the audit write fails).
## Running it
Requires Rust (stable) and PostgreSQL.
```sh
cp .env.example .env # then edit: DATABASE_URL + the required secrets
createdb smirk_backend_core # or point DATABASE_URL at an existing database
cargo run --release # migrations run automatically on startup
```
The server **fails closed**: it refuses to start on a missing/weak secret or an
inconsistent feature configuration rather than booting with a control silently
defeated. Generate secrets with `openssl rand -hex 32`. See
[`.env.example`](.env.example) for the full, documented configuration surface.
First admin (headless bootstrap) — run **one** of these; both latch, so the
other is refused afterward by design:
```sh
smirk-admin setup --pubkey <x-only-hex> # seed YOUR pubkey (active immediately)
smirk-admin create-admin-wallet --out key.hex # generate a key (pending) + latch, no setup needed
```
`<x-only-hex>` is the admin's Sign-in-with-Smirk (Nostr) public key; the admin
then authenticates with the matching key over NIP-98. Full deployment guide:
[docs/operations/OPERATOR_SETUP.md](docs/operations/OPERATOR_SETUP.md).
## Security posture
- **Fail-closed configuration** — the only place that reads the environment;
validates secrets (length + placeholder checks) and feature consistency at boot.
- **Identity at rest is peppered** — `pubkey_hash` / `seed_fingerprint` are stored
as HMACs, so the database is not a seed-existence oracle or a cross-instance
linker.
- **Strict JWTs** — HS256 pinned, zero leeway, audience-separated; admin tokens
are cryptographically distinct from user tokens.
- **Bounded, hardened upstream I/O** — TLS with hostname verification, streaming
size caps, and per-request timeouts on every external call.
- **Verification**: unit tests, database integration tests, schema drift checks,
and dependency checks cover different failure modes. Passing them is not a
claim of an independent security audit.
Report vulnerabilities privately — see [SECURITY.md](SECURITY.md). Please do not
open public issues for security reports.
## Testing
```sh
cargo test # unit + doc tests (no database needed)
cargo test --tests # L1 integration, with TEST_DATABASE_URL supplied privately
```
Database integration tests self-skip when `TEST_DATABASE_URL` is unset. CI's
Postgres job supplies it and runs every integration target. A local pass without
that variable does not verify database behavior. The tests use only
deterministic, non-sensitive test secrets and ephemeral identities — never a
funded or otherwise sensitive wallet seed.
## License
[MIT](LICENSE) © Such Software LLC — run it, embed it, modify it. Built for interop.
## Verification checklist
- [ ] Build and test the exact release candidate with the locked dependencies.
- [ ] Run all database integration tests against a disposable PostgreSQL database.
- [ ] Regenerate OpenAPI and the console bundle from their sources and check drift.
- [ ] Verify each enabled chain against its configured source.
- [ ] Use reviewed Fleet plan/apply procedures for company production deployments.