Skip to main content

hypercall_admin/monitoring/
engine.rs

1//! Engine state digest + balance ledger follower admin endpoints.
2
3use axum::{extract::State, http::StatusCode, response::IntoResponse};
4use serde::Serialize;
5use sonic_rs::json;
6
7use crate::state::AdminState;
8use hypercall_runtime_api::sonic_json::SonicJson;
9
10/// GET /monitoring/engine-state-digest - Deterministic engine state digest
11///
12/// **Authentication**: Requires `X-Admin-Key` header (performance protection).
13///
14/// This returns the latest engine-published digest of matching-critical state.
15/// It intentionally excludes wall-clock freshness fields so primary and standby
16/// can be compared when they report the same sequence.
17#[utoipa::path(
18    get,
19    path = "/monitoring/engine-state-digest",
20    responses(
21        (status = 200, description = "Engine state digest", body = hypercall_runtime_api::boundary::engine::EngineStateDigest),
22        (status = 401, description = "Invalid or missing X-Admin-Key header")
23    ),
24    tag = "Monitoring",
25    security(("admin_key" = []))
26)]
27pub async fn engine_state_digest(State(app_state): State<AdminState>) -> impl IntoResponse {
28    SonicJson(app_state.engine_state_digest_provider.engine_state_digest())
29}
30
31#[derive(Debug, Serialize, utoipa::ToSchema)]
32pub struct BalanceLedgerSnapshotEntry {
33    #[schema(value_type = String)]
34    pub wallet: hypercall_types::WalletAddress,
35    pub balance: String,
36}
37
38#[derive(Debug, Serialize, utoipa::ToSchema)]
39pub struct BalanceLedgerSyncResponse {
40    pub balance_update_seq: u64,
41    pub balance_update_stream_required: bool,
42    pub latest_acked_balance_stream_sequence: Option<u64>,
43    pub latest_acked_balance_update_seq: Option<u64>,
44    pub balance_count: usize,
45    pub balances: Vec<BalanceLedgerSnapshotEntry>,
46}
47
48/// GET /monitoring/engine/balance-ledger/snapshot - Engine balance follower snapshot
49#[utoipa::path(
50    get,
51    path = "/monitoring/engine/balance-ledger/snapshot",
52    responses(
53        (status = 200, description = "Engine balance ledger sync snapshot", body = BalanceLedgerSyncResponse),
54        (status = 401, description = "Invalid or missing X-Admin-Key header"),
55        (status = 503, description = "Balance update stream has not acknowledged current balance updates")
56    ),
57    tag = "Monitoring",
58    security(("admin_key" = []))
59)]
60pub async fn balance_ledger_sync_snapshot(
61    State(app_state): State<AdminState>,
62) -> impl IntoResponse {
63    let snapshot = app_state
64        .balance_snapshot_provider
65        .balance_ledger_sync_snapshot();
66    if snapshot.balance_update_stream_required
67        && snapshot.balance_update_seq > 0
68        && snapshot
69            .latest_acked_balance_update_seq
70            .map(|acked_seq| acked_seq < snapshot.balance_update_seq)
71            .unwrap_or(true)
72    {
73        return (
74            StatusCode::SERVICE_UNAVAILABLE,
75            SonicJson(json!({
76                "error": "balance update stream has not acknowledged current balance updates",
77                "balance_update_seq": snapshot.balance_update_seq,
78                "balance_update_stream_required": snapshot.balance_update_stream_required,
79                "latest_acked_balance_stream_sequence": snapshot.latest_acked_balance_stream_sequence,
80                "latest_acked_balance_update_seq": snapshot.latest_acked_balance_update_seq,
81            })),
82        )
83            .into_response();
84    }
85
86    let mut balances: Vec<_> = snapshot
87        .balances
88        .into_iter()
89        .map(|(wallet, balance)| BalanceLedgerSnapshotEntry {
90            wallet,
91            balance: balance.to_string(),
92        })
93        .collect();
94    balances.sort_by_key(|entry| entry.wallet);
95
96    (
97        StatusCode::OK,
98        SonicJson(BalanceLedgerSyncResponse {
99            balance_update_seq: snapshot.balance_update_seq,
100            balance_update_stream_required: snapshot.balance_update_stream_required,
101            latest_acked_balance_stream_sequence: snapshot.latest_acked_balance_stream_sequence,
102            latest_acked_balance_update_seq: snapshot.latest_acked_balance_update_seq,
103            balance_count: balances.len(),
104            balances,
105        }),
106    )
107        .into_response()
108}