Third wallet-handler slice. Authenticated, view-only proxies over the
GrinClient — the backend holds no Grin spend key; the wallet exports its
rewind_hash (a public-derived view credential) per scan, the backend forwards
it to grin-wallet's scan_rewind_hash and stores no secret. Spending happens in
the wallet; the backend only broadcasts the finalized tx.
Endpoints (under /api/v1/wallet/grin):
- POST scan {rewind_hash, start_height?} -> {outputs, total_balance,
last_pmmr_index} (incremental resume index)
- GET height -> {height}
- POST broadcast {tx} -> {ok} (node push_transaction)
- JWT-gated. rewind_hash is a view credential: GrinScanRequest omits Debug and
is skipped from every span (never logged), and is validated as 64 hex before
any grin call (applying the wallet-handler review's secret-DTO lesson).
- Feature-gated: grin disabled -> 400.
- Shared validate_hex64 in wallet/mod.rs (view_key + rewind_hash); 3 paths +
schemas + grin tag registered; spec regenerated, drift-clean.
4 L1 integration tests (auth gate on scan/height/broadcast; malformed
rewind_hash 400).
Also: dropped Debug from the xmr_wow view_key-bearing request DTOs
(ViewRequest/RegisterRequest) per the review — defense-in-depth against a
cleartext view_key dump.
First wallet-handler slice. Thin, non-custodial, JWT-gated proxies over the
ElectrumClient — the wallet derives addresses and signs locally; these
endpoints only relay reads and a finalized broadcast.
Endpoints (POST, under /api/v1/wallet/utxo, asset = btc|ltc in body):
- balance -> {confirmed, unconfirmed} (separate integers; client sums)
- utxos -> [{txid, vout, value, height}]
- history -> [{txid, height, fee}]
- tip -> {height}
- fee -> {sat_per_vb}
- broadcast-> {txid}
- JWT-gated for abuse control; the client-supplied address is NOT persisted
against the user (no address<->identity graph stored here).
- Feature-gated dispatch: unknown/disabled asset -> 400; the Electrum client
validates the address + network before any network call.
- broadcast validates tx_hex (non-empty, even-length, hex, <=200 KB) before
reaching the node.
- Registered in build_router + openapi.json (6 paths, btc_ltc tag); spec
regenerated, drift-clean.
5 L1 integration tests (auth gate on balance/broadcast/fee; unknown-asset and
malformed-tx-hex 400s — all deterministic, no network) + tx_hex unit test.
Add ChainClients (infra/chains.rs): per-chain data-source handles built once at
startup from config and shared via AppState. Each client is present only when
its chain feature flag is on, and a malformed chain endpoint now fails startup
(fail-closed) rather than the first request. Cloneable (grin's session state is
shared via Arc) so it lives in the cloneable AppState.
Foundation for the wallet-handlers phase — handlers will read these clients
from State. No endpoints yet (openapi unchanged).
- shared build_router() (lib) so main + tests exercise identical wiring
- tests/common: spins the real router + AppState against TEST_DATABASE_URL;
self-skips when unset (cargo test stays green without a DB). Deterministic,
non-secret test JWT/peppers; ephemeral generated identities only — no funded
or sensitive wallet seeds (those stay in a separate gitignored L3 harness).
- tests/db_queries: validates migrations + db layer against real Postgres
(peppered identity lookups, UNIQUE->409, session create/lookup/revoke,
key upsert/list/delete, restore-attempt counts).
- tests/auth_flow: HTTP flows through the real router (health, auth gate,
refresh rotation + old-token revocation, username set+lookup, key register+
fetch, NIP-05 directory).
- CI: new integration job with an ephemeral postgres:16 service.
- 82 tests total (69 unit + 13 integration), all green against live PG.
Complements the monorepo's client-side suite (it tests the wire format the
client sends; the client tests the wallet side); mirrors its secret-handling
(env-driven, gitignored). Mirrors the L0-L5 tiering from docs/engineering.