Files
smirk-backend-core/tests/wallet_grin.rs
T
jwinterm e62f1bec8e 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.
2026-06-23 16:02:44 -04:00

65 lines
1.7 KiB
Rust

//! L1 integration: Grin (view-only) wallet endpoints through the real router.
//!
//! Deterministic, no-network paths: the JWT gate and rewind_hash validation,
//! resolved before any grin-wallet call. The networked happy paths live in the
//! separate gitignored L3 harness.
mod common;
use axum::http::StatusCode;
use serde_json::json;
#[tokio::test]
async fn scan_requires_a_token() {
let app = require_app!();
let (status, _) = app
.request(
"POST",
"/api/v1/wallet/grin/scan",
None,
Some(json!({ "rewind_hash": "00" })),
)
.await;
assert_eq!(status, StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn scan_rejects_malformed_rewind_hash() {
let app = require_app!();
let (_uid, access, _r) = app.mint_session().await;
// Authed, but the rewind_hash is not 64 hex -> 400 before any grin call.
let (status, body) = app
.request(
"POST",
"/api/v1/wallet/grin/scan",
Some(&access),
Some(json!({ "rewind_hash": "tooshort" })),
)
.await;
assert_eq!(status, StatusCode::BAD_REQUEST);
assert_eq!(body["code"], "VALIDATION_ERROR");
}
#[tokio::test]
async fn height_requires_a_token() {
let app = require_app!();
let (status, _) = app
.request("GET", "/api/v1/wallet/grin/height", None, None)
.await;
assert_eq!(status, StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn broadcast_requires_a_token() {
let app = require_app!();
let (status, _) = app
.request(
"POST",
"/api/v1/wallet/grin/broadcast",
None,
Some(json!({ "tx": {} })),
)
.await;
assert_eq!(status, StatusCode::UNAUTHORIZED);
}