mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-17 07:21:32 +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:
+241
@@ -584,6 +584,116 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/wallet/grin/broadcast": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"grin"
|
||||
],
|
||||
"summary": "Broadcast a finalized, signed Grin transaction.",
|
||||
"operationId": "broadcast",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GrinBroadcastRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Transaction broadcast",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GrinBroadcastResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Grin disabled or malformed transaction"
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing or invalid token"
|
||||
},
|
||||
"503": {
|
||||
"description": "Upstream node unavailable"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/wallet/grin/height": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"grin"
|
||||
],
|
||||
"summary": "Current Grin chain tip height (for confirmation counting).",
|
||||
"operationId": "height",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Chain tip height",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GrinHeightResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Grin disabled"
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing or invalid token"
|
||||
},
|
||||
"503": {
|
||||
"description": "Upstream node unavailable"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/wallet/grin/scan": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"grin"
|
||||
],
|
||||
"summary": "View-only scan for the outputs a `rewind_hash` recognizes.",
|
||||
"operationId": "scan",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GrinScanRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Recognized outputs + balance + resume index",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GrinScanResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Grin disabled or malformed rewind_hash"
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing or invalid token"
|
||||
},
|
||||
"503": {
|
||||
"description": "Upstream node unavailable"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/wallet/lws/balance": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@@ -1537,6 +1647,133 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"GrinBroadcastRequest": {
|
||||
"type": "object",
|
||||
"description": "Broadcast a finalized, signed Grin transaction (built + signed by the wallet).",
|
||||
"required": [
|
||||
"tx"
|
||||
],
|
||||
"properties": {
|
||||
"tx": {
|
||||
"type": "object",
|
||||
"description": "The finalized transaction object (grin node `push_transaction` input)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"GrinBroadcastResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ok"
|
||||
],
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"GrinHeightResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"height"
|
||||
],
|
||||
"properties": {
|
||||
"height": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"GrinOutput": {
|
||||
"type": "object",
|
||||
"description": "A single output recovered by a view-only scan. Amounts are nanogrin.",
|
||||
"required": [
|
||||
"commit",
|
||||
"value",
|
||||
"height",
|
||||
"mmr_index",
|
||||
"is_coinbase",
|
||||
"lock_height"
|
||||
],
|
||||
"properties": {
|
||||
"commit": {
|
||||
"type": "string"
|
||||
},
|
||||
"height": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"minimum": 0
|
||||
},
|
||||
"is_coinbase": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"lock_height": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"minimum": 0
|
||||
},
|
||||
"mmr_index": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"minimum": 0
|
||||
},
|
||||
"value": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"GrinScanRequest": {
|
||||
"type": "object",
|
||||
"description": "A view-only scan request. Carries the `rewind_hash` view credential, so it\ndeliberately omits `Debug` (never logged).",
|
||||
"required": [
|
||||
"rewind_hash"
|
||||
],
|
||||
"properties": {
|
||||
"rewind_hash": {
|
||||
"type": "string",
|
||||
"description": "The wallet's `rewind_hash` (64 hex). Forwarded to grin-wallet; not stored."
|
||||
},
|
||||
"start_height": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "int64",
|
||||
"description": "Scan from this block height (wallet birthday / last scanned). Omit for full.",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"GrinScanResponse": {
|
||||
"type": "object",
|
||||
"description": "Result of a view-only scan: recognized outputs, total, and the resume index.",
|
||||
"required": [
|
||||
"outputs",
|
||||
"total_balance",
|
||||
"last_pmmr_index"
|
||||
],
|
||||
"properties": {
|
||||
"last_pmmr_index": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "Resume point (`last_pmmr_index`) for the next incremental scan.",
|
||||
"minimum": 0
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/GrinOutput"
|
||||
}
|
||||
},
|
||||
"total_balance": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"HealthResponse": {
|
||||
"type": "object",
|
||||
"description": "Liveness response.",
|
||||
@@ -2467,6 +2704,10 @@
|
||||
{
|
||||
"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)."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+11
-1
@@ -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;
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
//! Grin wallet handlers — authenticated, **view-only** proxies over the
|
||||
//! [`GrinClient`](crate::infra::grin::GrinClient).
|
||||
//!
|
||||
//! The backend holds no Grin spend key. The wallet exports its `rewind_hash` (a
|
||||
//! view credential derived from its *public* root key — it can recognize the
|
||||
//! wallet's outputs and read amounts, but cannot spend) and sends it per scan;
|
||||
//! the backend forwards it to grin-wallet's `scan_rewind_hash` and stores no
|
||||
//! secret. Spending (input selection, kernel signing) happens in the wallet; the
|
||||
//! backend only broadcasts the finalized transaction. See
|
||||
//! `docs/private/GRIN_LWS_DESIGN.md`.
|
||||
//!
|
||||
//! Conventions (matching [`super::btc_ltc`] / [`super::xmr_wow`]):
|
||||
//! * JWT-gated; the `rewind_hash` is a view credential — its request struct omits
|
||||
//! `Debug` and is `skip`-ped from every span, so it is never logged.
|
||||
//! * snake_case wire fields; every DTO derives `utoipa::ToSchema`.
|
||||
//! * Routes are RELATIVE to the `/api/v1` mount point; see [`routes`].
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::HeaderMap,
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::instrument;
|
||||
|
||||
use super::validate_hex64;
|
||||
use crate::api::middleware::extract_user_id_from_token;
|
||||
use crate::error::AppError;
|
||||
use crate::infra::grin::{GrinClient, ViewWalletOutputResult};
|
||||
use crate::AppState;
|
||||
|
||||
/// Resolve the Grin client, or a 400 if Grin support is disabled.
|
||||
fn grin_client(state: &AppState) -> Result<&GrinClient, AppError> {
|
||||
state.chains.grin.as_deref().ok_or_else(|| {
|
||||
AppError::ValidationError("grin support is not enabled on this server".into())
|
||||
})
|
||||
}
|
||||
|
||||
// ── DTOs ────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// A view-only scan request. Carries the `rewind_hash` view credential, so it
|
||||
/// deliberately omits `Debug` (never logged).
|
||||
#[derive(Deserialize, utoipa::ToSchema)]
|
||||
pub struct GrinScanRequest {
|
||||
/// The wallet's `rewind_hash` (64 hex). Forwarded to grin-wallet; not stored.
|
||||
pub rewind_hash: String,
|
||||
/// Scan from this block height (wallet birthday / last scanned). Omit for full.
|
||||
pub start_height: Option<u64>,
|
||||
}
|
||||
|
||||
/// A single output recovered by a view-only scan. Amounts are nanogrin.
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct GrinOutput {
|
||||
pub commit: String,
|
||||
pub value: u64,
|
||||
pub height: u64,
|
||||
pub mmr_index: u64,
|
||||
pub is_coinbase: bool,
|
||||
pub lock_height: u64,
|
||||
}
|
||||
|
||||
/// Result of a view-only scan: recognized outputs, total, and the resume index.
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct GrinScanResponse {
|
||||
pub outputs: Vec<GrinOutput>,
|
||||
pub total_balance: u64,
|
||||
/// Resume point (`last_pmmr_index`) for the next incremental scan.
|
||||
pub last_pmmr_index: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct GrinHeightResponse {
|
||||
pub height: u64,
|
||||
}
|
||||
|
||||
/// Broadcast a finalized, signed Grin transaction (built + signed by the wallet).
|
||||
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
||||
pub struct GrinBroadcastRequest {
|
||||
/// The finalized transaction object (grin node `push_transaction` input).
|
||||
#[schema(value_type = Object)]
|
||||
pub tx: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct GrinBroadcastResponse {
|
||||
pub ok: bool,
|
||||
}
|
||||
|
||||
fn output_dto(o: ViewWalletOutputResult) -> GrinOutput {
|
||||
GrinOutput {
|
||||
commit: o.commit,
|
||||
value: o.value,
|
||||
height: o.height,
|
||||
mmr_index: o.mmr_index,
|
||||
is_coinbase: o.is_coinbase,
|
||||
lock_height: o.lock_height,
|
||||
}
|
||||
}
|
||||
|
||||
// ── handlers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/// View-only scan for the outputs a `rewind_hash` recognizes.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/wallet/grin/scan",
|
||||
request_body = GrinScanRequest,
|
||||
responses(
|
||||
(status = 200, description = "Recognized outputs + balance + resume index", body = GrinScanResponse),
|
||||
(status = 400, description = "Grin disabled or malformed rewind_hash"),
|
||||
(status = 401, description = "Missing or invalid token"),
|
||||
(status = 503, description = "Upstream node unavailable")
|
||||
),
|
||||
tag = "grin"
|
||||
)]
|
||||
#[instrument(skip(state, headers, req))]
|
||||
pub async fn scan(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
Json(req): Json<GrinScanRequest>,
|
||||
) -> Result<Json<GrinScanResponse>, AppError> {
|
||||
extract_user_id_from_token(&state, &headers).await?;
|
||||
validate_hex64(&req.rewind_hash, "rewind_hash")?;
|
||||
|
||||
let view = grin_client(&state)?
|
||||
.scan_rewind_hash(&req.rewind_hash, req.start_height)
|
||||
.await?;
|
||||
Ok(Json(GrinScanResponse {
|
||||
outputs: view.output_result.into_iter().map(output_dto).collect(),
|
||||
total_balance: view.total_balance,
|
||||
last_pmmr_index: view.last_pmmr_index,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Current Grin chain tip height (for confirmation counting).
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/wallet/grin/height",
|
||||
responses(
|
||||
(status = 200, description = "Chain tip height", body = GrinHeightResponse),
|
||||
(status = 400, description = "Grin disabled"),
|
||||
(status = 401, description = "Missing or invalid token"),
|
||||
(status = 503, description = "Upstream node unavailable")
|
||||
),
|
||||
tag = "grin"
|
||||
)]
|
||||
#[instrument(skip(state, headers))]
|
||||
pub async fn height(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Json<GrinHeightResponse>, AppError> {
|
||||
extract_user_id_from_token(&state, &headers).await?;
|
||||
let height = grin_client(&state)?.get_height().await?;
|
||||
Ok(Json(GrinHeightResponse { height }))
|
||||
}
|
||||
|
||||
/// Broadcast a finalized, signed Grin transaction.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/wallet/grin/broadcast",
|
||||
request_body = GrinBroadcastRequest,
|
||||
responses(
|
||||
(status = 200, description = "Transaction broadcast", body = GrinBroadcastResponse),
|
||||
(status = 400, description = "Grin disabled or malformed transaction"),
|
||||
(status = 401, description = "Missing or invalid token"),
|
||||
(status = 503, description = "Upstream node unavailable")
|
||||
),
|
||||
tag = "grin"
|
||||
)]
|
||||
#[instrument(skip(state, headers, req))]
|
||||
pub async fn broadcast(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
Json(req): Json<GrinBroadcastRequest>,
|
||||
) -> Result<Json<GrinBroadcastResponse>, AppError> {
|
||||
extract_user_id_from_token(&state, &headers).await?;
|
||||
grin_client(&state)?.broadcast(&req.tx).await?;
|
||||
Ok(Json(GrinBroadcastResponse { ok: true }))
|
||||
}
|
||||
|
||||
// ── router ────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Grin routes, RELATIVE to the `/api/v1` mount point.
|
||||
pub fn routes() -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
.route("/wallet/grin/scan", post(scan))
|
||||
.route("/wallet/grin/height", get(height))
|
||||
.route("/wallet/grin/broadcast", post(broadcast))
|
||||
}
|
||||
+17
-8
@@ -12,11 +12,14 @@ use crate::error::AppError;
|
||||
use crate::AppState;
|
||||
|
||||
pub mod btc_ltc;
|
||||
pub mod grin;
|
||||
pub mod xmr_wow;
|
||||
|
||||
/// All wallet routes, RELATIVE to the `/api/v1` mount point.
|
||||
pub fn routes() -> Router<Arc<AppState>> {
|
||||
btc_ltc::routes().merge(xmr_wow::routes())
|
||||
btc_ltc::routes()
|
||||
.merge(xmr_wow::routes())
|
||||
.merge(grin::routes())
|
||||
}
|
||||
|
||||
// ── shared input validators ───────────────────────────────────────────────────
|
||||
@@ -40,17 +43,23 @@ pub(crate) fn validate_hex(value: &str, field: &str, max: usize) -> Result<(), A
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A Monero/Wownero private view key: 32 bytes = 64 hex chars. Validated only by
|
||||
/// shape; never logged.
|
||||
pub(crate) fn validate_view_key(view_key: &str) -> Result<(), AppError> {
|
||||
if view_key.len() != 64 || !view_key.bytes().all(|b| b.is_ascii_hexdigit()) {
|
||||
return Err(AppError::ValidationError(
|
||||
"view_key must be 64 hexadecimal characters".into(),
|
||||
));
|
||||
/// Exactly 32 bytes as 64 hex chars — the shape of the view credentials this
|
||||
/// backend forwards (Monero/Wownero private view key; Grin `rewind_hash`).
|
||||
/// `field` names the value for the error; the value itself is never logged.
|
||||
pub(crate) fn validate_hex64(value: &str, field: &str) -> Result<(), AppError> {
|
||||
if value.len() != 64 || !value.bytes().all(|b| b.is_ascii_hexdigit()) {
|
||||
return Err(AppError::ValidationError(format!(
|
||||
"{field} must be 64 hexadecimal characters"
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A Monero/Wownero private view key (64 hex). Validated by shape; never logged.
|
||||
pub(crate) fn validate_view_key(view_key: &str) -> Result<(), AppError> {
|
||||
validate_hex64(view_key, "view_key")
|
||||
}
|
||||
|
||||
/// A CryptoNote address — base58, ~95 (standard) to ~106 (integrated) chars.
|
||||
/// Bounded generously; the LWS does full validation.
|
||||
pub(crate) fn validate_cn_address(address: &str) -> Result<(), AppError> {
|
||||
|
||||
@@ -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