mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-17 15:31:38 +02:00
feat: GET /api/v1/capabilities (client feature discovery)
Public, unauthenticated description of what this deployment offers, so the
wallet can adapt to the instance it's pointed at — grey out disabled chains and
features, and pick the right network. This is the client-facing half of the
open-core/feature-flag design: every capability is a config switch, surfaced
here. No secrets — only on/off flags and public network names.
Response: { version, chains: { btc/ltc/xmr/wow/grin: { enabled, network } },
features: { grin_relay, prices, nostr_identity, tips } }. UTXO chains carry
their configured network; CryptoNote/Grin report null. On the normal rate-limit
tier. Registered in openapi (system tag); 1 L1 test (public, well-shaped).
This commit is contained in:
+113
@@ -348,6 +348,27 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/capabilities": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"system"
|
||||
],
|
||||
"summary": "Describe this instance's enabled chains and features.",
|
||||
"operationId": "capabilities",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Enabled chains and features",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CapabilitiesResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/health": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -1670,6 +1691,71 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CapabilitiesResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"version",
|
||||
"chains",
|
||||
"features"
|
||||
],
|
||||
"properties": {
|
||||
"chains": {
|
||||
"$ref": "#/components/schemas/ChainCapabilities"
|
||||
},
|
||||
"features": {
|
||||
"$ref": "#/components/schemas/FeatureCapabilities"
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "Backend version (Cargo package version)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"ChainCapabilities": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"btc",
|
||||
"ltc",
|
||||
"xmr",
|
||||
"wow",
|
||||
"grin"
|
||||
],
|
||||
"properties": {
|
||||
"btc": {
|
||||
"$ref": "#/components/schemas/ChainCapability"
|
||||
},
|
||||
"grin": {
|
||||
"$ref": "#/components/schemas/ChainCapability"
|
||||
},
|
||||
"ltc": {
|
||||
"$ref": "#/components/schemas/ChainCapability"
|
||||
},
|
||||
"wow": {
|
||||
"$ref": "#/components/schemas/ChainCapability"
|
||||
},
|
||||
"xmr": {
|
||||
"$ref": "#/components/schemas/ChainCapability"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ChainCapability": {
|
||||
"type": "object",
|
||||
"description": "Per-chain availability. `network` is the configured network for UTXO chains\n(so the wallet derives addresses for the right one); `null` for chains whose\nnetwork isn't a backend setting.",
|
||||
"required": [
|
||||
"enabled"
|
||||
],
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"network": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"CheckRestoreRequest": {
|
||||
"type": "object",
|
||||
"description": "Ask whether a wallet (by seed fingerprint) was created on this backend, and\nwhether the submitted keys match.",
|
||||
@@ -1868,6 +1954,33 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"FeatureCapabilities": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"grin_relay",
|
||||
"prices",
|
||||
"nostr_identity",
|
||||
"tips"
|
||||
],
|
||||
"properties": {
|
||||
"grin_relay": {
|
||||
"type": "boolean",
|
||||
"description": "Grin async slatepack relay mailbox."
|
||||
},
|
||||
"nostr_identity": {
|
||||
"type": "boolean",
|
||||
"description": "Nostr-native identity (NIP-98 login/link, NIP-05 directory)."
|
||||
},
|
||||
"prices": {
|
||||
"type": "boolean",
|
||||
"description": "Fiat price feed."
|
||||
},
|
||||
"tips": {
|
||||
"type": "boolean",
|
||||
"description": "Tipping (parked)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"FeeRequest": {
|
||||
"type": "object",
|
||||
"description": "A fee-estimate request.",
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
//! Capability discovery.
|
||||
//!
|
||||
//! A public, unauthenticated description of what *this* deployment offers, so the
|
||||
//! wallet can adapt to the instance it's pointed at (grey out disabled chains and
|
||||
//! features, pick the right network). This is the client-facing half of the
|
||||
//! open-core/feature-flag design: every capability is a config switch, surfaced
|
||||
//! here. No secrets — only on/off flags and the public network names.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{extract::State, routing::get, Json, Router};
|
||||
use serde::Serialize;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
/// Per-chain availability. `network` is the configured network for UTXO chains
|
||||
/// (so the wallet derives addresses for the right one); `null` for chains whose
|
||||
/// network isn't a backend setting.
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct ChainCapability {
|
||||
pub enabled: bool,
|
||||
pub network: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct ChainCapabilities {
|
||||
pub btc: ChainCapability,
|
||||
pub ltc: ChainCapability,
|
||||
pub xmr: ChainCapability,
|
||||
pub wow: ChainCapability,
|
||||
pub grin: ChainCapability,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct FeatureCapabilities {
|
||||
/// Grin async slatepack relay mailbox.
|
||||
pub grin_relay: bool,
|
||||
/// Fiat price feed.
|
||||
pub prices: bool,
|
||||
/// Nostr-native identity (NIP-98 login/link, NIP-05 directory).
|
||||
pub nostr_identity: bool,
|
||||
/// Tipping (parked).
|
||||
pub tips: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct CapabilitiesResponse {
|
||||
/// Backend version (Cargo package version).
|
||||
pub version: String,
|
||||
pub chains: ChainCapabilities,
|
||||
pub features: FeatureCapabilities,
|
||||
}
|
||||
|
||||
/// Describe this instance's enabled chains and features.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/capabilities",
|
||||
responses((status = 200, description = "Enabled chains and features", body = CapabilitiesResponse)),
|
||||
tag = "system"
|
||||
)]
|
||||
#[instrument(skip(state))]
|
||||
pub async fn capabilities(State(state): State<Arc<AppState>>) -> Json<CapabilitiesResponse> {
|
||||
let cfg = &state.config;
|
||||
let chains = &cfg.features.chains;
|
||||
Json(CapabilitiesResponse {
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
chains: ChainCapabilities {
|
||||
btc: ChainCapability {
|
||||
enabled: chains.btc,
|
||||
network: Some(cfg.chains.btc.network.clone()),
|
||||
},
|
||||
ltc: ChainCapability {
|
||||
enabled: chains.ltc,
|
||||
network: Some(cfg.chains.ltc.network.clone()),
|
||||
},
|
||||
xmr: ChainCapability {
|
||||
enabled: chains.xmr,
|
||||
network: None,
|
||||
},
|
||||
wow: ChainCapability {
|
||||
enabled: chains.wow,
|
||||
network: None,
|
||||
},
|
||||
grin: ChainCapability {
|
||||
enabled: chains.grin,
|
||||
network: None,
|
||||
},
|
||||
},
|
||||
features: FeatureCapabilities {
|
||||
grin_relay: cfg.features.grin_relay,
|
||||
prices: cfg.features.prices,
|
||||
nostr_identity: cfg.features.nostr_identity,
|
||||
tips: cfg.features.tips,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Capability route, RELATIVE to the `/api/v1` mount point. Public (no auth).
|
||||
pub fn routes() -> Router<Arc<AppState>> {
|
||||
Router::new().route("/capabilities", get(capabilities))
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
//! HTTP API layer: handlers and the OpenAPI assembly.
|
||||
|
||||
pub mod auth;
|
||||
pub mod capabilities;
|
||||
pub mod health;
|
||||
pub mod middleware;
|
||||
pub mod nip05;
|
||||
|
||||
@@ -16,6 +16,7 @@ use utoipa::OpenApi;
|
||||
),
|
||||
paths(
|
||||
crate::api::health::health,
|
||||
crate::api::capabilities::capabilities,
|
||||
crate::api::auth::pow_challenge,
|
||||
crate::api::auth::extension_register,
|
||||
crate::api::auth::check_restore,
|
||||
@@ -59,6 +60,10 @@ use utoipa::OpenApi;
|
||||
),
|
||||
components(schemas(
|
||||
crate::api::health::HealthResponse,
|
||||
crate::api::capabilities::CapabilitiesResponse,
|
||||
crate::api::capabilities::ChainCapabilities,
|
||||
crate::api::capabilities::ChainCapability,
|
||||
crate::api::capabilities::FeatureCapabilities,
|
||||
crate::api::auth::AuthResponse,
|
||||
crate::api::auth::UserInfo,
|
||||
crate::api::auth::AssetPublicKey,
|
||||
|
||||
@@ -95,6 +95,7 @@ pub fn build_router(state: Arc<AppState>) -> Router {
|
||||
.merge(
|
||||
api::users::routes()
|
||||
.merge(api::wallet::routes())
|
||||
.merge(api::capabilities::routes())
|
||||
.layer(normal),
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
//! L1 integration: the public capabilities endpoint.
|
||||
|
||||
mod common;
|
||||
|
||||
use axum::http::StatusCode;
|
||||
|
||||
#[tokio::test]
|
||||
async fn capabilities_is_public_and_well_shaped() {
|
||||
let app = require_app!();
|
||||
// No token required — the wallet reads this before/without login.
|
||||
let (status, body) = app.request("GET", "/api/v1/capabilities", None, None).await;
|
||||
assert_eq!(status, StatusCode::OK);
|
||||
|
||||
assert!(body["version"].as_str().is_some(), "version present");
|
||||
// Every chain reports an `enabled` boolean.
|
||||
for chain in ["btc", "ltc", "xmr", "wow", "grin"] {
|
||||
assert!(
|
||||
body["chains"][chain]["enabled"].is_boolean(),
|
||||
"chain {chain} reports enabled"
|
||||
);
|
||||
}
|
||||
// UTXO chains carry a network; CryptoNote/Grin don't.
|
||||
assert!(body["chains"]["btc"]["network"].as_str().is_some());
|
||||
assert!(body["chains"]["xmr"]["network"].is_null());
|
||||
// Feature flags are present.
|
||||
for feat in ["grin_relay", "prices", "nostr_identity", "tips"] {
|
||||
assert!(
|
||||
body["features"][feat].is_boolean(),
|
||||
"feature {feat} reports a boolean"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user