futu_backend/msg_header.rs
1//! 集中 `MsgHeader` builder (v1.4.110 P0-1)
2//!
3//! 取代 v1.4.110 之前散落 15+ 处直接 `odr_sys_cmn::MsgHeader { ... }` /
4//! `sim_odr_sys_cmn::MsgHeader { ... }` / `trade_cmn::CryptoMsgHeader { ... }`
5//! 构造模式. 普通 real/sim/crypto `req_id` 走
6//! [`crate::trade_query::create_backend_req_id`] 派生;FIN BFF/position-action
7//! 命令则必须保留 durable intent owner 提供的 `req_id`. 两类路径都防止
8//! v1.4.109 P0 类 dedupe bug (req_id 固定 → backend 幂等 cache → 误返 stale
9//! order echo). 见
10//! `essentials/2026-05-16-0725-v1.4.109-place-order-msgheader-req-id-not-unique-root-cause-codex-handoff-zh.md`.
11//!
12//! 配套 pre-push Check 6 (`.githooks/pre-push`, v1.4.110 P0-4) 用 grep 拦截
13//! 任何新加的直接构造, 强制走本 module 的 builder.
14//!
15//! # 四个 shape 差异
16//!
17//! - [`build_real`] → `odr_sys_cmn::MsgHeader` (real account, 7 fields,
18//! 含 `sub_account_id`)
19//! - [`build_sim`] → `sim_odr_sys_cmn::MsgHeader` (sim account, 7 fields,
20//! 含 `market` 替代 `sub_account_id`)
21//! - [`build_crypto`] → `trade_cmn::CryptoMsgHeader` (crypto, 仅 3 fields)
22//! - [`build_fin_real`] → `finpb_odr_sys_cmn::MsgHeader` (BFF/position action,
23//! caller-owned durable request id + optional JP sub-account)
24
25#[cfg(test)]
26mod tests;
27
28use crate::proto_internal::{finpb_odr_sys_cmn, odr_sys_cmn, sim_odr_sys_cmn, trade_cmn};
29use crate::trade_query::create_backend_req_id;
30
31/// 构造 real-account `odr_sys_cmn::MsgHeader`.
32///
33/// # Args
34///
35/// - `acc_id`: 账户 id (填 `account_id` 字段; 同时作为 [`create_backend_req_id`]
36/// 的派生种子).
37/// - `cipher`: trade cipher.
38/// - `Some(vec![])` = cache-only query (wire 上 field-present 但为空 bytes).
39/// - 非空 `Some` = 持 unlock token 的写路径 (PlaceOrder / ModifyOrder /
40/// CancelOrder / OrderFillInfoReq w/ security_type).
41/// - `None` = 无 trade 上下文 (如 sim_acc_list / login-time queries).
42/// - `security_type`: PlaceOrder / ModifyOrder / OrderFillInfoReq 等需要;
43/// 纯 list / asset 查询填 `None`.
44/// - `input_source`: **仅 PlaceOrder 填** (C++ 行为, see
45/// `crates/futu-gateway-trd/src/handlers/trd/translate.rs` 的
46/// `PLACE_ORDER_MSG_INPUT_SOURCE`); modify / cancel / query 都 `None`.
47pub fn build_real(
48 acc_id: u64,
49 cipher: Option<Vec<u8>>,
50 security_type: Option<u32>,
51 input_source: Option<u32>,
52) -> odr_sys_cmn::MsgHeader {
53 odr_sys_cmn::MsgHeader {
54 req_id: Some(create_backend_req_id(acc_id)),
55 account_id: Some(acc_id),
56 cipher,
57 security_type,
58 exchange_code: None,
59 input_source,
60 sub_account_id: None,
61 }
62}
63
64/// Construct the FIN service real-account header used by Desktop BFF and
65/// position-action commands. These commands own a durable request id, so the
66/// caller supplies it instead of generating a transport-local id here.
67pub fn build_fin_real(
68 req_id: String,
69 acc_id: u64,
70 cipher: Vec<u8>,
71 security_type: Option<u32>,
72 sub_account_id: Option<u64>,
73) -> finpb_odr_sys_cmn::MsgHeader {
74 finpb_odr_sys_cmn::MsgHeader {
75 req_id: Some(req_id),
76 account_id: Some(acc_id),
77 cipher: Some(cipher),
78 security_type,
79 exchange_code: None,
80 input_source: None,
81 sub_account_id,
82 }
83}
84
85/// 构造 sim-account `sim_odr_sys_cmn::MsgHeader`.
86///
87/// Sim shape 与 real shape 差异:
88/// - 多 `market` 字段 (sim 账户跨市场必填, see
89/// `futu_domain_trade_account::sim_account_header_market_like_cpp`).
90/// - 无 `sub_account_id`.
91///
92/// # Args
93///
94/// - `market`: C++ raw `NN_TrdMarket` (`Sim_US_Margin=100` 等也可出现)。
95/// `None` 仅用于确认为空 header 的测试/兼容路径。
96pub fn build_sim(
97 acc_id: u64,
98 cipher: Option<Vec<u8>>,
99 market: Option<u32>,
100 security_type: Option<u32>,
101) -> sim_odr_sys_cmn::MsgHeader {
102 sim_odr_sys_cmn::MsgHeader {
103 req_id: Some(create_backend_req_id(acc_id)),
104 account_id: Some(acc_id),
105 cipher,
106 security_type,
107 exchange_code: None,
108 input_source: None,
109 market,
110 }
111}
112
113/// 构造 crypto `trade_cmn::CryptoMsgHeader` (仅 3 字段).
114///
115/// 空 cipher 处理为 `None` 而非 `Some(vec![])` (对齐 v1.4.110 之前
116/// `crypto_trade.rs::build_crypto_msg_header` 原行为, C++ 参照见
117/// `NNProto_Trd_OrderOpCrypto.cpp:40-48`).
118pub fn build_crypto(acc_id: u64, cipher: Vec<u8>) -> trade_cmn::CryptoMsgHeader {
119 trade_cmn::CryptoMsgHeader {
120 req_id: Some(create_backend_req_id(acc_id)),
121 account_id: Some(acc_id),
122 cipher: if cipher.is_empty() {
123 None
124 } else {
125 Some(cipher)
126 },
127 }
128}
129
130/// Build the global account-list heartbeat header.
131///
132/// C++ deliberately writes only `req_id`, seeded with `1` for real trading;
133/// account id and cipher remain absent because the repeated account list owns
134/// the subscription identity.
135/// Ref: `NNProto_Trd_KeepAlive.cpp:84-95`.
136pub fn build_real_trade_account_heartbeat() -> odr_sys_cmn::MsgHeader {
137 odr_sys_cmn::MsgHeader {
138 req_id: Some(create_backend_req_id(1)),
139 account_id: None,
140 cipher: None,
141 security_type: None,
142 exchange_code: None,
143 input_source: None,
144 sub_account_id: None,
145 }
146}