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_user_security_group;
67use futu_proto::qot_get_valuation_detail;
68use futu_proto::qot_get_valuation_plate_stock_list;
69use futu_proto::qot_get_warrant;
70use futu_proto::qot_modify_user_security;
71use futu_proto::qot_option_screen;
72use futu_proto::qot_request_history_kl;
73use futu_proto::qot_request_history_kl_quota;
74use futu_proto::qot_request_rehab;
75use futu_proto::qot_request_trade_date;
76use futu_proto::qot_set_price_reminder;
77use futu_proto::qot_stock_filter;
78use futu_proto::qot_stock_screen;
79use futu_proto::qot_sub;
80use futu_proto::qot_warrant_screen;
81use futu_proto::skill_wrap_api;
82use futu_proto::used_quota;
83use futu_server::conn::IncomingRequest;
84
85use crate::adapter::{self, JsonRequestMode, RestState, decode_json_request};
86
87type ApiResult = Result<Json<Value>, (StatusCode, Json<Value>)>;
88type RawApiResult = Result<adapter::RawJson, (StatusCode, Json<Value>)>;
89
90pub const REST_SHARED_CONN: u64 = 0xFFFF_FFFE;
120
121async fn proto_request_shared_conn<Req, Rsp>(
138 state: &RestState,
139 proto_id: u32,
140 json_body: Option<Value>,
141 ctx: Option<&crate::caller_context::CallerContext>,
142) -> ApiResult
143where
144 Req: Message + Default + serde::de::DeserializeOwned,
145 Rsp: Message + Default + serde::Serialize,
146{
147 let req_msg: Req = decode_json_request(proto_id, json_body, JsonRequestMode::QotSharedConn)?;
151
152 let body = Bytes::from(req_msg.encode_to_vec());
154
155 let incoming = IncomingRequest::builder(
160 REST_SHARED_CONN,
161 proto_id,
162 state.next_serial(),
163 ProtoFmtType::Protobuf,
164 body,
165 )
166 .with_caller_scope(
167 ctx.and_then(|c| c.caller_allowed_acc_ids_arc()),
168 ctx.and_then(|c| c.caller_key_id()),
169 )
170 .build();
171 let resp_bytes = state
172 .router
173 .dispatch(REST_SHARED_CONN, &incoming)
174 .await
175 .ok_or_else(|| {
176 (
177 StatusCode::INTERNAL_SERVER_ERROR,
178 Json(serde_json::json!({
179 "error": "handler returned no response"
180 })),
181 )
182 })?;
183
184 let rsp_msg = Rsp::decode(Bytes::from(resp_bytes)).map_err(|e| {
186 (
187 StatusCode::INTERNAL_SERVER_ERROR,
188 Json(serde_json::json!({
189 "error": format!("failed to decode response: {e}")
190 })),
191 )
192 })?;
193
194 let mut json_rsp = serde_json::to_value(&rsp_msg).map_err(|e| {
196 (
197 StatusCode::INTERNAL_SERVER_ERROR,
198 Json(serde_json::json!({
199 "error": format!("failed to serialize response: {e}")
200 })),
201 )
202 })?;
203
204 adapter::maybe_wrap_err_code_prefix(&mut json_rsp);
207
208 Ok(Json(json_rsp))
209}
210
211async fn proto_request_shared_conn_raw<Req, Rsp>(
212 state: &RestState,
213 proto_id: u32,
214 json_body: Option<Value>,
215 ctx: Option<&crate::caller_context::CallerContext>,
216) -> RawApiResult
217where
218 Req: Message + Default + serde::de::DeserializeOwned,
219 Rsp: Message + Default + serde::Serialize,
220{
221 let req_msg: Req = decode_json_request(proto_id, json_body, JsonRequestMode::QotSharedConn)?;
222 let body = Bytes::from(req_msg.encode_to_vec());
223
224 let incoming = IncomingRequest::builder(
225 REST_SHARED_CONN,
226 proto_id,
227 state.next_serial(),
228 ProtoFmtType::Protobuf,
229 body,
230 )
231 .with_caller_scope(
232 ctx.and_then(|c| c.caller_allowed_acc_ids_arc()),
233 ctx.and_then(|c| c.caller_key_id()),
234 )
235 .build();
236 let resp_bytes = state
237 .router
238 .dispatch(REST_SHARED_CONN, &incoming)
239 .await
240 .ok_or_else(|| {
241 (
242 StatusCode::INTERNAL_SERVER_ERROR,
243 Json(serde_json::json!({
244 "error": "handler returned no response"
245 })),
246 )
247 })?;
248
249 let rsp_msg = Rsp::decode(Bytes::from(resp_bytes)).map_err(|e| {
250 (
251 StatusCode::INTERNAL_SERVER_ERROR,
252 Json(serde_json::json!({
253 "error": format!("failed to decode response: {e}")
254 })),
255 )
256 })?;
257
258 adapter::raw_json_from_proto_response(&rsp_msg)
259}
260
261#[cfg(test)]
262fn map_surface_spec_error(
263 spec: &'static futu_surface_spec::EndpointSpec,
264 err: futu_surface_spec::DispatchError,
265) -> (StatusCode, Json<Value>) {
266 let proto_id = spec
267 .proto_id()
268 .map(|id| id.to_string())
269 .unwrap_or_else(|| "daemon-local".to_string());
270 let ret_msg = format!(
271 "{} (endpoint: {}, proto_id: {})",
272 err, spec.canonical_name, proto_id
273 );
274 let mut body = validation_error_body(ret_msg);
275 let machine_error_field = spec.runtime.error.machine_error_field;
276 if let Some(obj) = body.as_object_mut() {
277 obj.insert(
278 machine_error_field.to_string(),
279 serde_json::json!({
280 "kind": "validation_error",
281 "message": err.to_string(),
282 "endpoint": spec.canonical_name,
283 "proto_id": proto_id,
284 }),
285 );
286 }
287 (StatusCode::BAD_REQUEST, Json(body))
288}
289
290#[cfg(test)]
291fn validation_error_body(message: impl Into<String>) -> Value {
292 let message = message.into();
293 serde_json::json!({
294 "ret_type": -1,
295 "ret_msg": message,
296 "error": message,
297 })
298}
299
300mod misc;
330mod qot_3401_plus;
331mod quotes;
332mod reference;
333mod snapshot;
334mod subscribe;
335
336#[cfg(test)]
337mod tests;
338
339#[cfg(test)]
340use misc::{
341 inject_default_is_req_all_conn, normalize_financial_calendar_rest_body_for_internal_proto,
342};
343#[cfg(test)]
344use quotes::{annotate_quote_cache_miss, orderbook_loud_unsub_hint};
345#[cfg(test)]
346use snapshot::{
347 augment_snapshot_with_exchange_code, augment_static_info_with_exchange_code,
348 check_static_info_input,
349};
350#[cfg(test)]
351use subscribe::body_has_sub_or_unsub_flag;
352
353pub use misc::{
354 get_financial_calendar, get_risk_free_rate, get_spread_table, get_ticker_statistic,
355 get_ticker_statistic_detail, list_plates, query_subscription, search_target_financial_calendar,
356 unsubscribe,
357};
358pub use qot_3401_plus::{
359 get_ark_active_transaction, get_ark_fund_holding, get_ark_stock_dynamic, get_dividend_calendar,
360 get_dividend_rank, get_earnings_beat_rank, get_earnings_calendar, get_economic_calendar,
361 get_fed_watch_dot_plot, get_fed_watch_target_rate, get_heat_map_data,
362 get_high_dividend_soe_rank, get_hot_list, get_industrial_chain_by_plate,
363 get_industrial_chain_detail, get_industrial_chain_list, get_industrial_plate_info,
364 get_industrial_plate_stock, get_institution_distribution, get_institution_holding_change,
365 get_institution_holding_list, get_institution_list, get_institution_profile,
366 get_macro_indicator_history, get_macro_indicator_list, get_period_change_rank,
367 get_rating_change, get_rise_fall_distribution, get_short_selling_rank, get_top_movers_rank,
368 get_us_after_hours_rank, get_us_overnight_rank, get_us_pre_market_rank,
369};
370pub use quotes::{get_basic_qot, get_broker, get_kl, get_order_book, get_rt, get_ticker};
371pub use reference::{
372 get_capital_distribution, get_capital_flow, get_code_change, get_company_executive_background,
373 get_company_executives, get_company_operational_efficiency, get_company_profile,
374 get_corporate_actions_buybacks, get_corporate_actions_dividends,
375 get_corporate_actions_stock_splits, get_daily_short_volume, get_derivative_unusual,
376 get_financial_unusual, get_financials_earnings_price_history,
377 get_financials_earnings_price_move, get_financials_revenue_breakdown,
378 get_financials_statements, get_future_info, get_holding_change, get_insider_holder_list,
379 get_insider_trade_list, get_ipo_calendar, get_ipo_list, get_market_state, get_option_chain,
380 get_option_exercise_probability, get_option_expiration_date, get_option_quote,
381 get_option_strategy, get_option_strategy_analysis, get_option_strategy_spread,
382 get_option_volatility, get_owner_plate, get_plate_security, get_plate_set, get_price_reminder,
383 get_reference, get_research_analyst_consensus, get_research_morningstar_report,
384 get_research_rating_summary, get_shareholders_holder_detail, get_shareholders_holding_changes,
385 get_shareholders_institutional, get_shareholders_overview, get_short_interest, get_suspend,
386 get_technical_unusual, get_top_ten_buy_sell_brokers, get_used_quota, get_user_security,
387 get_user_security_group, get_valuation_detail, get_valuation_plate_stock_list, get_warrant,
388 modify_user_security, option_screen, request_history_kl, request_history_kl_quota,
389 request_rehab, request_trading_days, set_price_reminder, stock_filter, stock_screen,
390 warrant_screen,
391};
392pub use snapshot::{get_snapshot, get_static_info};
393pub use subscribe::{get_sub_info, subscribe};