hypercall_admin/monitoring/
halts.rs1use axum::{extract::State, http::StatusCode, response::IntoResponse};
4use serde::Serialize;
5use sonic_rs::json;
6use std::collections::HashMap;
7use tracing::warn;
8
9use crate::state::AdminState;
10use hypercall_runtime_api::sonic_json::SonicJson;
11use hypercall_runtime_api::trading_halt::{
12 normalize_symbol, TradingHaltActivation, TradingHaltState,
13};
14
15#[derive(Debug, Serialize, utoipa::ToSchema)]
16pub struct TradingHaltsResponse {
17 pub global_halt: Option<TradingHaltActivation>,
18 pub halted_markets: HashMap<String, TradingHaltActivation>,
19 pub audit_log: Vec<TradingHaltActivation>,
20}
21
22#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
23pub struct SetGlobalTradingHaltRequest {
24 pub halted: bool,
25 pub reason: String,
26}
27
28#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
29pub struct SetMarketTradingHaltRequest {
30 pub symbol: String,
31 pub halted: bool,
32 pub reason: String,
33}
34
35fn trading_halts_response(state: &TradingHaltState) -> TradingHaltsResponse {
36 TradingHaltsResponse {
37 global_halt: state.global_halt.clone(),
38 halted_markets: state.halted_markets.clone(),
39 audit_log: state.audit_log.clone(),
40 }
41}
42
43#[utoipa::path(
45 get,
46 path = "/monitoring/trading-halts",
47 responses(
48 (status = 200, description = "Trading halt state", body = TradingHaltsResponse),
49 (status = 401, description = "Invalid or missing X-Admin-Key header")
50 ),
51 tag = "Monitoring",
52 security(("admin_key" = []))
53)]
54pub async fn get_trading_halts(State(app_state): State<AdminState>) -> impl IntoResponse {
55 let state = app_state.trading_halt.read().await;
56 SonicJson(trading_halts_response(&state))
57}
58
59#[utoipa::path(
61 post,
62 path = "/monitoring/trading-halts/global",
63 request_body = SetGlobalTradingHaltRequest,
64 responses(
65 (status = 200, description = "Updated trading halt state", body = TradingHaltsResponse),
66 (status = 400, description = "Invalid request"),
67 (status = 401, description = "Invalid or missing X-Admin-Key header")
68 ),
69 tag = "Monitoring",
70 security(("admin_key" = []))
71)]
72pub async fn set_global_trading_halt(
73 State(app_state): State<AdminState>,
74 SonicJson(request): SonicJson<SetGlobalTradingHaltRequest>,
75) -> impl IntoResponse {
76 if request.reason.trim().is_empty() {
77 return (
78 StatusCode::BAD_REQUEST,
79 SonicJson(json!({
80 "error": "Reason is required",
81 })),
82 )
83 .into_response();
84 }
85
86 let actor = "admin_api".to_string();
87 {
88 let mut state = app_state.trading_halt.write().await;
89 state.set_global_halt(request.halted, request.reason, actor.clone());
90 }
91
92 warn!(
93 halted = request.halted,
94 actor = %actor,
95 "Global trading halt updated"
96 );
97
98 let state = app_state.trading_halt.read().await;
99 (StatusCode::OK, SonicJson(trading_halts_response(&state))).into_response()
100}
101
102#[utoipa::path(
104 post,
105 path = "/monitoring/trading-halts/market",
106 request_body = SetMarketTradingHaltRequest,
107 responses(
108 (status = 200, description = "Updated trading halt state", body = TradingHaltsResponse),
109 (status = 400, description = "Invalid request"),
110 (status = 401, description = "Invalid or missing X-Admin-Key header")
111 ),
112 tag = "Monitoring",
113 security(("admin_key" = []))
114)]
115pub async fn set_market_trading_halt(
116 State(app_state): State<AdminState>,
117 SonicJson(request): SonicJson<SetMarketTradingHaltRequest>,
118) -> impl IntoResponse {
119 if request.reason.trim().is_empty() {
120 return (
121 StatusCode::BAD_REQUEST,
122 SonicJson(json!({
123 "error": "Reason is required",
124 })),
125 )
126 .into_response();
127 }
128 if request.symbol.trim().is_empty() {
129 return (
130 StatusCode::BAD_REQUEST,
131 SonicJson(json!({
132 "error": "Symbol is required",
133 })),
134 )
135 .into_response();
136 }
137
138 let actor = "admin_api".to_string();
139 let normalized_symbol = normalize_symbol(&request.symbol);
140 {
141 let mut state = app_state.trading_halt.write().await;
142 state.set_market_halt(
143 &normalized_symbol,
144 request.halted,
145 request.reason,
146 actor.clone(),
147 );
148 }
149
150 warn!(
151 halted = request.halted,
152 symbol = %normalized_symbol,
153 actor = %actor,
154 "Market trading halt updated"
155 );
156
157 let state = app_state.trading_halt.read().await;
158 (StatusCode::OK, SonicJson(trading_halts_response(&state))).into_response()
159}