1use std::sync::Arc;
4
5use axum::Json;
6use axum::extract::{Extension, State};
7use axum::http::StatusCode;
8use serde_json::Value;
9
10use futu_auth::{CheckCtx, KeyRecord};
11
12use crate::adapter::{self, RestState};
13
14use super::ApiResult;
15use super::card_num::extract_and_resolve_card_num_into_acc_id;
16use super::validation::{authorize_trade_write, validate_header_trd_market_write};
17use super::write_pipeline::limit_reject_response;
18
19struct RestLimitPlan {
20 contexts: Vec<CheckCtx>,
21 acc_id: u64,
22 idempotency_key: Option<String>,
23}
24
25type RestLimitResult = Result<RestLimitPlan, (StatusCode, Json<Value>)>;
26
27fn invalid_group_limit_context(endpoint: &'static str) -> (StatusCode, Json<Value>) {
28 let message = "group order operation or decimal limit context is invalid";
29 (
30 StatusCode::BAD_REQUEST,
31 Json(serde_json::json!({
32 "ret_type": -1,
33 "ret_msg": message,
34 "error": message,
35 "endpoint": endpoint,
36 })),
37 )
38}
39
40fn place_contexts_from_rest_body(body: &Value) -> RestLimitResult {
41 let request = adapter::decode_json_request::<futu_proto::trd_place_order_group::Request>(
42 futu_core::proto_id::TRD_PLACE_ORDER_GROUP,
43 Some(body.clone()),
44 adapter::JsonRequestMode::GenericRest,
45 )?;
46 let contexts = futu_auth_pipeline::body_aware::place_group_check_contexts(&request.c2s)
47 .ok_or_else(|| invalid_group_limit_context("/api/trd/place-order-group"))?;
48 let idempotency_key = match request.c2s.operation.as_ref() {
49 Some(futu_proto::trd_place_order_group::c2s::Operation::Initial(initial)) => {
50 Some(initial.idempotency_key.clone())
51 }
52 _ => None,
53 };
54 Ok(RestLimitPlan {
55 contexts,
56 acc_id: request.c2s.header.acc_id,
57 idempotency_key,
58 })
59}
60
61fn modify_contexts_from_rest_body(body: &Value) -> RestLimitResult {
62 let request = adapter::decode_json_request::<futu_proto::trd_modify_order_group::Request>(
63 futu_core::proto_id::TRD_MODIFY_ORDER_GROUP,
64 Some(body.clone()),
65 adapter::JsonRequestMode::GenericRest,
66 )?;
67 let contexts = futu_auth_pipeline::body_aware::modify_group_check_contexts(&request.c2s)
68 .ok_or_else(|| invalid_group_limit_context("/api/trd/modify-order-group"))?;
69 let idempotency_key = match request.c2s.operation.as_ref() {
70 Some(futu_proto::trd_modify_order_group::c2s::Operation::Initial(initial)) => {
71 Some(initial.idempotency_key.clone())
72 }
73 _ => None,
74 };
75 Ok(RestLimitPlan {
76 contexts,
77 acc_id: request.c2s.header.acc_id,
78 idempotency_key,
79 })
80}
81
82fn cancel_contexts_from_rest_body(body: &Value) -> RestLimitResult {
83 let request = adapter::decode_json_request::<futu_proto::trd_cancel_order_group::Request>(
84 futu_core::proto_id::TRD_CANCEL_ORDER_GROUP,
85 Some(body.clone()),
86 adapter::JsonRequestMode::GenericRest,
87 )?;
88 let contexts = futu_auth_pipeline::body_aware::cancel_group_check_contexts(&request.c2s)
89 .ok_or_else(|| invalid_group_limit_context("/api/trd/cancel-order-group"))?;
90 let idempotency_key = match request.c2s.operation.as_ref() {
91 Some(futu_proto::trd_cancel_order_group::c2s::Operation::Initial(initial)) => {
92 Some(initial.idempotency_key.clone())
93 }
94 _ => None,
95 };
96 Ok(RestLimitPlan {
97 contexts,
98 acc_id: request.c2s.header.acc_id,
99 idempotency_key,
100 })
101}
102
103fn delete_contexts_from_rest_body(body: &Value) -> RestLimitResult {
104 let request = adapter::decode_json_request::<futu_proto::trd_delete_order_group::Request>(
105 futu_core::proto_id::TRD_DELETE_ORDER_GROUP,
106 Some(body.clone()),
107 adapter::JsonRequestMode::GenericRest,
108 )?;
109 let contexts = futu_auth_pipeline::body_aware::delete_group_check_contexts(&request.c2s)
110 .ok_or_else(|| invalid_group_limit_context("/api/trd/delete-order-group"))?;
111 let idempotency_key = match request.c2s.operation.as_ref() {
112 Some(futu_proto::trd_delete_order_group::c2s::Operation::Initial(initial)) => {
113 Some(initial.idempotency_key.clone())
114 }
115 _ => None,
116 };
117 Ok(RestLimitPlan {
118 contexts,
119 acc_id: request.c2s.header.acc_id,
120 idempotency_key,
121 })
122}
123
124fn authorize_group_contexts(
125 state: &RestState,
126 rec: &KeyRecord,
127 endpoint: &'static str,
128 operation_name: &'static str,
129 plan: &RestLimitPlan,
130) -> Result<(), (StatusCode, Json<Value>)> {
131 let now = chrono::Utc::now();
132 let receipt_operation = if plan.contexts.iter().all(|ctx| ctx.mutation_no_exposure) {
133 format!("{operation_name}:transport-preflight")
134 } else {
135 operation_name.to_string()
136 };
137 let receipt_id = futu_auth::group_write_receipt_id(
138 &receipt_operation,
139 Some(&rec.id),
140 plan.acc_id,
141 plan.idempotency_key.as_deref().unwrap_or("confirmation"),
142 );
143 if let Err(outcome) =
144 state
145 .counters
146 .check_batch_limits(&rec.id, rec, &plan.contexts, now, false, &receipt_id)
147 {
148 let reason = outcome.reason().unwrap_or_else(|| "rejected".into());
149 return Err(limit_reject_response(
150 endpoint,
151 rec,
152 &reason,
153 outcome.http_status_code(),
154 ));
155 }
156 Ok(())
157}
158
159macro_rules! route {
160 (
161 $fn_name:ident,
162 $path:literal,
163 $request:path,
164 $response:path,
165 $proto_id:expr,
166 $contexts:path,
167 $operation_name:literal
168 ) => {
169 pub async fn $fn_name(
170 State(state): State<RestState>,
171 rec: Option<Extension<Arc<KeyRecord>>>,
172 Json(mut body): Json<Value>,
173 ) -> ApiResult {
174 crate::adapter::normalize_json_keys_snake_case(&mut body);
175 authorize_trade_write(
176 &state,
177 rec.as_ref().map(|Extension(record)| record.as_ref()),
178 &body,
179 $path,
180 )?;
181 extract_and_resolve_card_num_into_acc_id(
182 &state,
183 rec.as_ref().map(|Extension(record)| record.as_ref()),
184 &mut body,
185 $path,
186 )?;
187 validate_header_trd_market_write(&body, $path)?;
188 if let Some(Extension(record)) = rec.as_ref() {
189 let plan = $contexts(&body)?;
190 authorize_group_contexts(&state, record, $path, $operation_name, &plan)?;
191 }
192 let caller_key_id = rec.as_ref().map(|Extension(record)| record.id.clone());
193 adapter::proto_request_with_idempotency_and_caller::<$request, $response>(
194 &state,
195 $proto_id,
196 Some(body),
197 None,
198 caller_key_id,
199 )
200 .await
201 }
202 };
203}
204
205route!(
206 place_order_group,
207 "/api/trd/place-order-group",
208 futu_proto::trd_place_order_group::Request,
209 futu_proto::trd_place_order_group::Response,
210 futu_core::proto_id::TRD_PLACE_ORDER_GROUP,
211 place_contexts_from_rest_body,
212 "PlaceOrderGroup"
213);
214route!(
215 modify_order_group,
216 "/api/trd/modify-order-group",
217 futu_proto::trd_modify_order_group::Request,
218 futu_proto::trd_modify_order_group::Response,
219 futu_core::proto_id::TRD_MODIFY_ORDER_GROUP,
220 modify_contexts_from_rest_body,
221 "ModifyOrderGroup"
222);
223route!(
224 cancel_order_group,
225 "/api/trd/cancel-order-group",
226 futu_proto::trd_cancel_order_group::Request,
227 futu_proto::trd_cancel_order_group::Response,
228 futu_core::proto_id::TRD_CANCEL_ORDER_GROUP,
229 cancel_contexts_from_rest_body,
230 "CancelOrderGroup"
231);
232route!(
233 delete_order_group,
234 "/api/trd/delete-order-group",
235 futu_proto::trd_delete_order_group::Request,
236 futu_proto::trd_delete_order_group::Response,
237 futu_core::proto_id::TRD_DELETE_ORDER_GROUP,
238 delete_contexts_from_rest_body,
239 "DeleteOrderGroup"
240);
241
242#[cfg(test)]
243#[path = "group_order_tests.rs"]
244mod tests;