mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-17 07:21:32 +02:00
feat(wallet): unified GET /wallet/heights (tip per chain)
The wallet's ChainProvider seam fetches all chain tips in one call
(getBlockchainHeights) and selects per asset; the backend exposed per-chain
height endpoints but no aggregate, so the client's call 404'd. Add a
best-effort /wallet/heights returning {btc,ltc,xmr,wow,grin} concurrently — a
disabled or unreachable chain is null, never failing the others. JWT-gated,
OpenAPI-annotated (drift clean).
This commit is contained in:
@@ -1040,6 +1040,35 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/wallet/heights": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"system"
|
||||||
|
],
|
||||||
|
"summary": "Current tip height for every enabled chain (one round-trip).",
|
||||||
|
"operationId": "heights",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Tip height per chain (null if disabled/unreachable)",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HeightsResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Missing or invalid token"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer_auth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/wallet/lws/balance": {
|
"/wallet/lws/balance": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -2366,6 +2395,48 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"HeightsResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Current tip height per chain. `null` = the chain is disabled on this instance\nor its source was unreachable for this request.",
|
||||||
|
"properties": {
|
||||||
|
"btc": {
|
||||||
|
"type": [
|
||||||
|
"integer",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "int64",
|
||||||
|
"example": 955860
|
||||||
|
},
|
||||||
|
"grin": {
|
||||||
|
"type": [
|
||||||
|
"integer",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"ltc": {
|
||||||
|
"type": [
|
||||||
|
"integer",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"wow": {
|
||||||
|
"type": [
|
||||||
|
"integer",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"xmr": {
|
||||||
|
"type": [
|
||||||
|
"integer",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "int64"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"HistoryEntry": {
|
"HistoryEntry": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "A transaction-history entry.",
|
"description": "A transaction-history entry.",
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ use utoipa::OpenApi;
|
|||||||
crate::api::wallet::grin_relay::respond,
|
crate::api::wallet::grin_relay::respond,
|
||||||
crate::api::wallet::grin_relay::finalize,
|
crate::api::wallet::grin_relay::finalize,
|
||||||
crate::api::wallet::grin_relay::cancel,
|
crate::api::wallet::grin_relay::cancel,
|
||||||
|
crate::api::wallet::heights::heights,
|
||||||
crate::api::nip05::well_known_nostr,
|
crate::api::nip05::well_known_nostr,
|
||||||
),
|
),
|
||||||
components(schemas(
|
components(schemas(
|
||||||
@@ -132,6 +133,7 @@ use utoipa::OpenApi;
|
|||||||
crate::api::wallet::grin_relay::SlateIdRequest,
|
crate::api::wallet::grin_relay::SlateIdRequest,
|
||||||
crate::api::wallet::grin_relay::RelayEntry,
|
crate::api::wallet::grin_relay::RelayEntry,
|
||||||
crate::api::wallet::grin_relay::PendingRelaysResponse,
|
crate::api::wallet::grin_relay::PendingRelaysResponse,
|
||||||
|
crate::api::wallet::heights::HeightsResponse,
|
||||||
crate::api::nip05::WellKnownResponse,
|
crate::api::nip05::WellKnownResponse,
|
||||||
)),
|
)),
|
||||||
tags(
|
tags(
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
//! Unified blockchain-tip heights across all enabled chains.
|
||||||
|
//!
|
||||||
|
//! The wallet's ChainProvider seam fetches every chain's tip in a single call
|
||||||
|
//! and selects per asset, so one `GET /wallet/heights` returns them together.
|
||||||
|
//! Best-effort: a disabled chain (or a momentarily-unreachable source) is
|
||||||
|
//! reported `null` rather than failing the whole response.
|
||||||
|
//!
|
||||||
|
//! JWT-gated like the rest of the wallet surface (abuse-gating only; tip height
|
||||||
|
//! is public chain data and is not user-specific).
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::{extract::State, http::HeaderMap, routing::get, Json, Router};
|
||||||
|
use serde::Serialize;
|
||||||
|
use tracing::instrument;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
use crate::api::middleware::extract_user_id_from_token;
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::AppState;
|
||||||
|
|
||||||
|
/// Current tip height per chain. `null` = the chain is disabled on this instance
|
||||||
|
/// or its source was unreachable for this request.
|
||||||
|
#[derive(Debug, Serialize, ToSchema)]
|
||||||
|
pub struct HeightsResponse {
|
||||||
|
#[schema(example = 955860)]
|
||||||
|
pub btc: Option<i64>,
|
||||||
|
pub ltc: Option<i64>,
|
||||||
|
pub xmr: Option<i64>,
|
||||||
|
pub wow: Option<i64>,
|
||||||
|
pub grin: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Current tip height for every enabled chain (one round-trip).
|
||||||
|
#[utoipa::path(
|
||||||
|
security(("bearer_auth" = [])),
|
||||||
|
get,
|
||||||
|
path = "/wallet/heights",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Tip height per chain (null if disabled/unreachable)", body = HeightsResponse),
|
||||||
|
(status = 401, description = "Missing or invalid token")
|
||||||
|
),
|
||||||
|
tag = "system"
|
||||||
|
)]
|
||||||
|
#[instrument(skip(state, headers))]
|
||||||
|
pub async fn heights(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
) -> Result<Json<HeightsResponse>, AppError> {
|
||||||
|
extract_user_id_from_token(&state, &headers).await?;
|
||||||
|
let c = &state.chains;
|
||||||
|
// Concurrent best-effort: each chain's failure collapses to `None`, never
|
||||||
|
// failing the others. Disabled chains short-circuit without a network call.
|
||||||
|
let (btc, ltc, xmr, wow, grin) = tokio::join!(
|
||||||
|
async {
|
||||||
|
match &c.btc {
|
||||||
|
Some(e) => e.get_tip_height().await.ok(),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async {
|
||||||
|
match &c.ltc {
|
||||||
|
Some(e) => e.get_tip_height().await.ok(),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async {
|
||||||
|
match &c.xmr {
|
||||||
|
Some(l) => l.get_blockchain_height().await.ok().map(|h| h as i64),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async {
|
||||||
|
match &c.wow {
|
||||||
|
Some(l) => l.get_blockchain_height().await.ok().map(|h| h as i64),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async {
|
||||||
|
match &c.grin {
|
||||||
|
Some(g) => g.get_height().await.ok().map(|h| h as i64),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Ok(Json(HeightsResponse {
|
||||||
|
btc,
|
||||||
|
ltc,
|
||||||
|
xmr,
|
||||||
|
wow,
|
||||||
|
grin,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `GET /wallet/heights`, RELATIVE to the `/api/v1` mount point.
|
||||||
|
pub fn routes() -> Router<Arc<AppState>> {
|
||||||
|
Router::new().route("/wallet/heights", get(heights))
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ use crate::AppState;
|
|||||||
pub mod btc_ltc;
|
pub mod btc_ltc;
|
||||||
pub mod grin;
|
pub mod grin;
|
||||||
pub mod grin_relay;
|
pub mod grin_relay;
|
||||||
|
pub mod heights;
|
||||||
pub mod xmr_wow;
|
pub mod xmr_wow;
|
||||||
|
|
||||||
/// All wallet routes, RELATIVE to the `/api/v1` mount point.
|
/// All wallet routes, RELATIVE to the `/api/v1` mount point.
|
||||||
@@ -22,6 +23,7 @@ pub fn routes() -> Router<Arc<AppState>> {
|
|||||||
.merge(xmr_wow::routes())
|
.merge(xmr_wow::routes())
|
||||||
.merge(grin::routes())
|
.merge(grin::routes())
|
||||||
.merge(grin_relay::routes())
|
.merge(grin_relay::routes())
|
||||||
|
.merge(heights::routes())
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── shared input validators ───────────────────────────────────────────────────
|
// ── shared input validators ───────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user