feat: Grin wallet handlers (view-only, non-custodial)

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.
This commit is contained in:
jwinterm
2026-06-23 16:02:44 -04:00
parent 10b508fa8e
commit e62f1bec8e
5 changed files with 524 additions and 9 deletions
+11 -1
View File
@@ -46,6 +46,9 @@ use utoipa::OpenApi;
crate::api::wallet::xmr_wow::submit_tx,
crate::api::wallet::xmr_wow::height,
crate::api::wallet::xmr_wow::confirmations,
crate::api::wallet::grin::scan,
crate::api::wallet::grin::height,
crate::api::wallet::grin::broadcast,
crate::api::nip05::well_known_nostr,
),
components(schemas(
@@ -103,13 +106,20 @@ use utoipa::OpenApi;
crate::api::wallet::xmr_wow::HeightResponse,
crate::api::wallet::xmr_wow::ConfirmationsResponse,
crate::api::wallet::xmr_wow::OkResponse,
crate::api::wallet::grin::GrinScanRequest,
crate::api::wallet::grin::GrinOutput,
crate::api::wallet::grin::GrinScanResponse,
crate::api::wallet::grin::GrinHeightResponse,
crate::api::wallet::grin::GrinBroadcastRequest,
crate::api::wallet::grin::GrinBroadcastResponse,
crate::api::nip05::WellKnownResponse,
)),
tags(
(name = "system", description = "Service health and metadata."),
(name = "auth", description = "Wallet authentication: registration, restore, refresh, sessions, Nostr identity."),
(name = "btc_ltc", description = "Bitcoin/Litecoin chain access via Electrum (balance, UTXOs, history, tip, fee, broadcast)."),
(name = "xmr_wow", description = "Monero/Wownero chain access via LWS (register, balance, history, unspent/random outs, submit, height, confirmations).")
(name = "xmr_wow", description = "Monero/Wownero chain access via LWS (register, balance, history, unspent/random outs, submit, height, confirmations)."),
(name = "grin", description = "Grin view-only chain access (rewind_hash scan, tip height, broadcast).")
)
)]
pub struct ApiDoc;