hypercall_admin/monitoring/
tiers.rs1use axum::{extract::State, http::StatusCode, response::IntoResponse};
4use serde::Deserialize;
5use sonic_rs::json;
6
7use crate::state::AdminState;
8use hypercall_db::UserTierUpdate;
9use hypercall_runtime_api::sonic_json::SonicJson;
10use hypercall_types::WalletAddress;
11
12#[derive(Debug, Deserialize, utoipa::ToSchema)]
13pub struct AdminSetTierRequest {
14 pub wallet: String,
15 pub tier: String,
16}
17
18#[utoipa::path(
25 post,
26 path = "/monitoring/set-tier",
27 request_body = AdminSetTierRequest,
28 responses(
29 (status = 200, description = "Tier set successfully"),
30 (status = 400, description = "Invalid request"),
31 (status = 404, description = "Tier not found in tier_defaults"),
32 (status = 401, description = "Invalid or missing X-Admin-Key header"),
33 (status = 500, description = "Internal error")
34 ),
35 tag = "Monitoring",
36 security(("admin_key" = []))
37)]
38pub async fn set_tier(
39 State(app_state): State<AdminState>,
40 SonicJson(request): SonicJson<AdminSetTierRequest>,
41) -> impl IntoResponse {
42 let wallet: WalletAddress = match request.wallet.parse() {
44 Ok(w) => w,
45 Err(e) => {
46 return (
47 StatusCode::BAD_REQUEST,
48 SonicJson(json!({
49 "error": format!("Invalid wallet address: {}", e),
50 })),
51 )
52 .into_response();
53 }
54 };
55
56 let db_handler = match &app_state.sync_db {
58 Some(h) => h.clone(),
59 None => {
60 return (
61 StatusCode::INTERNAL_SERVER_ERROR,
62 SonicJson(json!({
63 "error": "Diesel handler not available",
64 })),
65 )
66 .into_response();
67 }
68 };
69
70 let tier_name = request.tier.clone();
71 let handler_clone = db_handler.clone();
72 let tier_defaults =
73 match tokio::task::spawn_blocking(move || handler_clone.get_tier_defaults_sync(&tier_name))
74 .await
75 {
76 Ok(Ok(Some(defaults))) => defaults,
77 Ok(Ok(None)) => {
78 return (
79 StatusCode::NOT_FOUND,
80 SonicJson(json!({
81 "error": format!("Tier '{}' not found in tier_defaults", request.tier),
82 })),
83 )
84 .into_response();
85 }
86 Ok(Err(e)) => {
87 return (
88 StatusCode::INTERNAL_SERVER_ERROR,
89 SonicJson(json!({
90 "error": format!("Failed to query tier_defaults: {}", e),
91 })),
92 )
93 .into_response();
94 }
95 Err(e) => {
96 return (
97 StatusCode::INTERNAL_SERVER_ERROR,
98 SonicJson(json!({
99 "error": format!("Failed to query tier_defaults: {}", e),
100 })),
101 )
102 .into_response();
103 }
104 };
105
106 let previous_tier = {
107 let handler_clone = db_handler.clone();
108 tokio::task::spawn_blocking(move || handler_clone.get_user_tier_sync(&wallet)).await
109 };
110 let previous_tier = match previous_tier {
111 Ok(Ok(tier)) => tier,
112 Ok(Err(e)) => {
113 return (
114 StatusCode::INTERNAL_SERVER_ERROR,
115 SonicJson(json!({
116 "error": format!("Failed to load current user tier: {}", e),
117 })),
118 )
119 .into_response();
120 }
121 Err(e) => {
122 return (
123 StatusCode::INTERNAL_SERVER_ERROR,
124 SonicJson(json!({
125 "error": format!("Failed to load current user tier: {}", e),
126 })),
127 )
128 .into_response();
129 }
130 };
131
132 let new_tier = UserTierUpdate {
134 wallet_address: wallet,
135 tier: request.tier.clone(),
136 margin_mode: None,
137 version: None,
138 max_open_orders: Some(tier_defaults.max_open_orders),
139 max_open_positions: Some(tier_defaults.max_open_positions),
140 orders_per_minute: Some(tier_defaults.orders_per_minute),
141 cancels_per_minute: Some(tier_defaults.cancels_per_minute),
142 api_requests_per_minute: Some(tier_defaults.api_requests_per_minute),
143 };
144
145 match app_state.tier_cache.set_tier(new_tier).await {
146 Ok(()) => {
147 if let Err(e) = app_state.submit_tier_update_command(wallet).await {
148 tracing::error!(
149 wallet = %wallet,
150 error = %e,
151 "Failed to apply monitoring tier update in engine"
152 );
153 let rollback_result = app_state
154 .tier_cache
155 .restore_tier_record(&wallet, previous_tier.as_ref())
156 .await;
157 if let Err(rollback_err) = rollback_result {
158 tracing::error!(
159 wallet = %wallet,
160 error = %rollback_err,
161 "Failed to roll back tier after engine apply failure"
162 );
163 }
164 return (
165 StatusCode::INTERNAL_SERVER_ERROR,
166 SonicJson(json!({
167 "error": "Failed to apply tier update in engine",
168 })),
169 )
170 .into_response();
171 }
172 tracing::info!(
173 wallet = %wallet,
174 tier = %request.tier,
175 max_open_orders = tier_defaults.max_open_orders,
176 "User tier set via admin endpoint"
177 );
178 (
179 StatusCode::OK,
180 SonicJson(json!({
181 "success": true,
182 "wallet": wallet.to_string(),
183 "tier": request.tier,
184 "max_open_orders": tier_defaults.max_open_orders,
185 "max_open_positions": tier_defaults.max_open_positions,
186 "orders_per_minute": tier_defaults.orders_per_minute,
187 "cancels_per_minute": tier_defaults.cancels_per_minute,
188 "api_requests_per_minute": tier_defaults.api_requests_per_minute,
189 })),
190 )
191 .into_response()
192 }
193 Err(e) => {
194 tracing::error!(wallet = %wallet, error = %e, "Failed to set user tier");
195 (
196 StatusCode::INTERNAL_SERVER_ERROR,
197 SonicJson(json!({
198 "error": format!("Failed to set tier: {}", e),
199 })),
200 )
201 .into_response()
202 }
203 }
204}