Skip to main content

futu_rest/routes/trd/
group_order.rs

1//! REST adapters for guarded v1.8 atomic group orders.
2
3use 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;