mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-17 15:31:38 +02:00
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:
@@ -0,0 +1,64 @@
|
||||
//! 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);
|
||||
}
|
||||
Reference in New Issue
Block a user