1use axum::extract::Json;
6use axum::http::StatusCode;
7use bytes::Bytes;
8use prost::Message;
9use serde_json::Value;
10
11use futu_codec::header::ProtoFmtType;
12use futu_proto::qot_get_basic_qot;
13use futu_proto::qot_get_broker;
14use futu_proto::qot_get_capital_distribution;
15use futu_proto::qot_get_capital_flow;
16use futu_proto::qot_get_code_change;
17use futu_proto::qot_get_company_executive_background;
18use futu_proto::qot_get_company_executives;
19use futu_proto::qot_get_company_operational_efficiency;
20use futu_proto::qot_get_company_profile;
21use futu_proto::qot_get_corporate_actions_buybacks;
22use futu_proto::qot_get_corporate_actions_dividends;
23use futu_proto::qot_get_corporate_actions_stock_splits;
24use futu_proto::qot_get_daily_short_volume;
25use futu_proto::qot_get_financials_earnings_price_history;
26use futu_proto::qot_get_financials_earnings_price_move;
27use futu_proto::qot_get_financials_revenue_breakdown;
28use futu_proto::qot_get_financials_statements;
29use futu_proto::qot_get_future_info;
30use futu_proto::qot_get_holding_change_list;
31use futu_proto::qot_get_insider_holder_list;
32use futu_proto::qot_get_insider_trade_list;
33use futu_proto::qot_get_ipo_list;
34use futu_proto::qot_get_kl;
35use futu_proto::qot_get_market_state;
36use futu_proto::qot_get_option_chain;
37use futu_proto::qot_get_option_exercise_probability;
38use futu_proto::qot_get_option_expiration_date;
39use futu_proto::qot_get_option_quote;
40use futu_proto::qot_get_option_strategy;
41use futu_proto::qot_get_option_strategy_analysis;
42use futu_proto::qot_get_option_strategy_spread;
43use futu_proto::qot_get_option_volatility;
44use futu_proto::qot_get_order_book;
45use futu_proto::qot_get_owner_plate;
46use futu_proto::qot_get_plate_security;
47use futu_proto::qot_get_plate_set;
48use futu_proto::qot_get_price_reminder;
49use futu_proto::qot_get_reference;
50use futu_proto::qot_get_research_analyst_consensus;
51use futu_proto::qot_get_research_morningstar_report;
52use futu_proto::qot_get_research_rating_summary;
53use futu_proto::qot_get_rt;
54use futu_proto::qot_get_security_snapshot;
55use futu_proto::qot_get_shareholders_holder_detail;
56use futu_proto::qot_get_shareholders_holding_changes;
57use futu_proto::qot_get_shareholders_institutional;
58use futu_proto::qot_get_shareholders_overview;
59use futu_proto::qot_get_short_interest;
60use futu_proto::qot_get_static_info;
61use futu_proto::qot_get_sub_info;
62use futu_proto::qot_get_suspend;
63use futu_proto::qot_get_ticker;
64use futu_proto::qot_get_top_ten_buy_sell_brokers;
65use futu_proto::qot_get_user_security;
66use futu_proto::qot_get_valuation_detail;
67use futu_proto::qot_get_valuation_plate_stock_list;
68use futu_proto::qot_get_warrant;
69use futu_proto::qot_modify_user_security;
70use futu_proto::qot_option_screen;
71use futu_proto::qot_request_history_kl;
72use futu_proto::qot_request_history_kl_quota;
73use futu_proto::qot_request_rehab;
74use futu_proto::qot_request_trade_date;
75use futu_proto::qot_set_price_reminder;
76use futu_proto::qot_stock_filter;
77use futu_proto::qot_stock_screen;
78use futu_proto::qot_sub;
79use futu_proto::qot_warrant_screen;
80use futu_proto::skill_wrap_api;
81use futu_proto::used_quota;
82use futu_server::conn::IncomingRequest;
83
84use crate::adapter::{self, JsonRequestMode, RestState, decode_json_request};
85
86type ApiResult = Result<Json<Value>, (StatusCode, Json<Value>)>;
87type RawApiResult = Result<adapter::RawJson, (StatusCode, Json<Value>)>;
88
89pub const REST_SHARED_CONN: u64 = 0xFFFF_FFFE;
119
120async fn proto_request_shared_conn<Req, Rsp>(
137 state: &RestState,
138 proto_id: u32,
139 json_body: Option<Value>,
140 ctx: Option<&crate::caller_context::CallerContext>,
141) -> ApiResult
142where
143 Req: Message + Default + serde::de::DeserializeOwned,
144 Rsp: Message + Default + serde::Serialize,
145{
146 let req_msg: Req = decode_json_request(proto_id, json_body, JsonRequestMode::QotSharedConn)?;
150
151 let body = Bytes::from(req_msg.encode_to_vec());
153
154 let incoming = IncomingRequest::builder(
159 REST_SHARED_CONN,
160 proto_id,
161 state.next_serial(),
162 ProtoFmtType::Protobuf,
163 body,
164 )
165 .with_caller_scope(
166 ctx.and_then(|c| c.caller_allowed_acc_ids_arc()),
167 ctx.and_then(|c| c.caller_key_id()),
168 )
169 .build();
170 let resp_bytes = state
171 .router
172 .dispatch(REST_SHARED_CONN, &incoming)
173 .await
174 .ok_or_else(|| {
175 (
176 StatusCode::INTERNAL_SERVER_ERROR,
177 Json(serde_json::json!({
178 "error": "handler returned no response"
179 })),
180 )
181 })?;
182
183 let rsp_msg = Rsp::decode(Bytes::from(resp_bytes)).map_err(|e| {
185 (
186 StatusCode::INTERNAL_SERVER_ERROR,
187 Json(serde_json::json!({
188 "error": format!("failed to decode response: {e}")
189 })),
190 )
191 })?;
192
193 let mut json_rsp = serde_json::to_value(&rsp_msg).map_err(|e| {
195 (
196 StatusCode::INTERNAL_SERVER_ERROR,
197 Json(serde_json::json!({
198 "error": format!("failed to serialize response: {e}")
199 })),
200 )
201 })?;
202
203 adapter::maybe_wrap_err_code_prefix(&mut json_rsp);
206
207 Ok(Json(json_rsp))
208}
209
210async fn proto_request_shared_conn_raw<Req, Rsp>(
211 state: &RestState,
212 proto_id: u32,
213 json_body: Option<Value>,
214 ctx: Option<&crate::caller_context::CallerContext>,
215) -> RawApiResult
216where
217 Req: Message + Default + serde::de::DeserializeOwned,
218 Rsp: Message + Default + serde::Serialize,
219{
220 let req_msg: Req = decode_json_request(proto_id, json_body, JsonRequestMode::QotSharedConn)?;
221 let body = Bytes::from(req_msg.encode_to_vec());
222
223 let incoming = IncomingRequest::builder(
224 REST_SHARED_CONN,
225 proto_id,
226 state.next_serial(),
227 ProtoFmtType::Protobuf,
228 body,
229 )
230 .with_caller_scope(
231 ctx.and_then(|c| c.caller_allowed_acc_ids_arc()),
232 ctx.and_then(|c| c.caller_key_id()),
233 )
234 .build();
235 let resp_bytes = state
236 .router
237 .dispatch(REST_SHARED_CONN, &incoming)
238 .await
239 .ok_or_else(|| {
240 (
241 StatusCode::INTERNAL_SERVER_ERROR,
242 Json(serde_json::json!({
243 "error": "handler returned no response"
244 })),
245 )
246 })?;
247
248 let rsp_msg = Rsp::decode(Bytes::from(resp_bytes)).map_err(|e| {
249 (
250 StatusCode::INTERNAL_SERVER_ERROR,
251 Json(serde_json::json!({
252 "error": format!("failed to decode response: {e}")
253 })),
254 )
255 })?;
256
257 adapter::raw_json_from_proto_response(&rsp_msg)
258}
259
260#[cfg(test)]
261fn map_surface_spec_error(
262 spec: &'static futu_surface_spec::EndpointSpec,
263 err: futu_surface_spec::DispatchError,
264) -> (StatusCode, Json<Value>) {
265 let proto_id = spec
266 .proto_id()
267 .map(|id| id.to_string())
268 .unwrap_or_else(|| "daemon-local".to_string());
269 let ret_msg = format!(
270 "{} (endpoint: {}, proto_id: {})",
271 err, spec.canonical_name, proto_id
272 );
273 let mut body = validation_error_body(ret_msg);
274 let machine_error_field = spec.runtime.error.machine_error_field;
275 if let Some(obj) = body.as_object_mut() {
276 obj.insert(
277 machine_error_field.to_string(),
278 serde_json::json!({
279 "kind": "validation_error",
280 "message": err.to_string(),
281 "endpoint": spec.canonical_name,
282 "proto_id": proto_id,
283 }),
284 );
285 }
286 (StatusCode::BAD_REQUEST, Json(body))
287}
288
289#[cfg(test)]
290fn validation_error_body(message: impl Into<String>) -> Value {
291 let message = message.into();
292 serde_json::json!({
293 "ret_type": -1,
294 "ret_msg": message,
295 "error": message,
296 })
297}
298
299mod misc;
329mod quotes;
330mod reference;
331mod snapshot;
332mod subscribe;
333
334#[cfg(test)]
335mod tests;
336
337#[cfg(test)]
338use misc::inject_default_is_req_all_conn;
339#[cfg(test)]
340use quotes::{annotate_quote_cache_miss, orderbook_loud_unsub_hint};
341#[cfg(test)]
342use snapshot::{
343 augment_snapshot_with_exchange_code, augment_static_info_with_exchange_code,
344 check_static_info_input,
345};
346#[cfg(test)]
347use subscribe::body_has_sub_or_unsub_flag;
348
349pub use misc::{
350 get_risk_free_rate, get_spread_table, get_ticker_statistic, get_ticker_statistic_detail,
351 list_plates, query_subscription, unsubscribe,
352};
353pub use quotes::{get_basic_qot, get_broker, get_kl, get_order_book, get_rt, get_ticker};
354pub use reference::{
355 get_capital_distribution, get_capital_flow, get_code_change, get_company_executive_background,
356 get_company_executives, get_company_operational_efficiency, get_company_profile,
357 get_corporate_actions_buybacks, get_corporate_actions_dividends,
358 get_corporate_actions_stock_splits, get_daily_short_volume, get_derivative_unusual,
359 get_financial_unusual, get_financials_earnings_price_history,
360 get_financials_earnings_price_move, get_financials_revenue_breakdown,
361 get_financials_statements, get_future_info, get_holding_change, get_insider_holder_list,
362 get_insider_trade_list, get_ipo_list, get_market_state, get_option_chain,
363 get_option_exercise_probability, get_option_expiration_date, get_option_quote,
364 get_option_strategy, get_option_strategy_analysis, get_option_strategy_spread,
365 get_option_volatility, get_owner_plate, get_plate_security, get_plate_set, get_price_reminder,
366 get_reference, get_research_analyst_consensus, get_research_morningstar_report,
367 get_research_rating_summary, get_shareholders_holder_detail, get_shareholders_holding_changes,
368 get_shareholders_institutional, get_shareholders_overview, get_short_interest, get_suspend,
369 get_technical_unusual, get_top_ten_buy_sell_brokers, get_used_quota, get_user_security,
370 get_valuation_detail, get_valuation_plate_stock_list, get_warrant, modify_user_security,
371 option_screen, request_history_kl, request_history_kl_quota, request_rehab,
372 request_trading_days, set_price_reminder, stock_filter, stock_screen, warrant_screen,
373};
374pub use snapshot::{get_snapshot, get_static_info};
375pub use subscribe::{get_sub_info, subscribe};