Skip to main content

futu_backend/
crypto_trade.rs

1//! Crypto trade backend wire helpers.
2//!
3//! Crypto trade support added by C++ OpenD 10.5.6508 uses several different
4//! server-side request shapes, but they all start from the same account facts:
5//! public long account id, optional native intra account id, broker id, and
6//! trade cipher. Keep that extraction here so handlers do not re-invent it.
7
8#[cfg(test)]
9mod tests;
10
11use futu_cache::trd_cache::{CachedTrdAcc, TrdCache};
12use futu_core::error::{FutuError, Result};
13
14use crate::{
15    msg_header,
16    proto_internal::{odr_sys_cmn, trade_cmn},
17};
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct CryptoAccountContext {
21    pub acc_id: u64,
22    pub intra_acc_id: Option<u64>,
23    pub broker_id: Option<u32>,
24    pub customer_id: Option<u64>,
25    pub cipher: Vec<u8>,
26}
27
28impl CryptoAccountContext {
29    /// Build the `odr_sys_cmn::MsgHeader` used by crypto asset reads.
30    ///
31    /// C++ `NNProto_Trd_AccCrypto.cpp:197-215` sends
32    /// `asset_pl::AccountInfoReq` through `M_SendProto_SetReqMsgHeader`;
33    /// `NNProtoCenter_Inner_Macro_Send.h:16-24` always sets `cipher` bytes
34    /// even when the cipher length is zero.
35    ///
36    /// v1.4.110 P0-1: delegate to [`crate::msg_header::build_real`].
37    /// `_op` 参数保留作 caller-side 语义标注 (test fixture / log key 可能用),
38    /// 当前实现忽略 (与 v1.4.110 之前 `self.req_id(op)` 的 `_op` 行为一致).
39    pub fn build_asset_msg_header(&self, _op: &str) -> odr_sys_cmn::MsgHeader {
40        msg_header::build_real(self.acc_id, Some(self.cipher.clone()), None, None)
41    }
42
43    /// Build the `trade_cmn::CryptoMsgHeader` used by crypto order paths.
44    ///
45    /// C++ `NNProto_Trd_OrderOpCrypto.cpp:40-48,94-102` sets `req_id` and
46    /// `account_id`, but only writes `cipher` when `GetAccCipher` returned a
47    /// non-empty buffer. Keep that distinction because backend crypto order
48    /// services use this lighter header rather than `odr_sys_cmn::MsgHeader`.
49    ///
50    /// v1.4.110 P0-1: delegate to [`crate::msg_header::build_crypto`].
51    pub fn build_crypto_msg_header(&self, _op: &str) -> trade_cmn::CryptoMsgHeader {
52        msg_header::build_crypto(self.acc_id, self.cipher.clone())
53    }
54
55    pub fn require_intra_acc_id(&self, op: &str) -> Result<u64> {
56        self.intra_acc_id.ok_or_else(|| {
57            crypto_context_error(format!(
58                "Crypto {op}: account {} missing intra_acc_id",
59                self.acc_id
60            ))
61        })
62    }
63
64    pub fn require_broker_id(&self, op: &str) -> Result<u32> {
65        self.broker_id.ok_or_else(|| {
66            crypto_context_error(format!(
67                "Crypto {op}: account {} missing broker_id",
68                self.acc_id
69            ))
70        })
71    }
72
73    pub fn require_customer_id(&self, op: &str) -> Result<u64> {
74        self.customer_id.ok_or_else(|| {
75            crypto_context_error(format!(
76                "Crypto {op}: account {} missing customer_id",
77                self.acc_id
78            ))
79        })
80    }
81}
82
83pub fn lookup_crypto_account_context(
84    cache: &TrdCache,
85    acc_id: u64,
86) -> Result<CryptoAccountContext> {
87    let acc = cache.lookup_account(acc_id).ok_or_else(|| {
88        crypto_context_error(format!("Crypto account {acc_id} not found in trade cache"))
89    })?;
90    crypto_account_context_from_acc(cache, &acc)
91}
92
93pub fn crypto_account_context_from_acc(
94    cache: &TrdCache,
95    acc: &CachedTrdAcc,
96) -> Result<CryptoAccountContext> {
97    if !acc.is_crypto_account() {
98        return Err(crypto_context_error(format!(
99            "Account {} is not a crypto account",
100            acc.acc_id
101        )));
102    }
103    Ok(CryptoAccountContext {
104        acc_id: acc.acc_id,
105        intra_acc_id: acc.intra_acc_id,
106        broker_id: broker_id_from_account(acc),
107        // C++ `NNProto_Trd_MaxQtyCrypto.cpp:49-54` writes `m_nUserID`
108        // into `crypto_risk_comm::Account.cid`. Rust account projection keeps
109        // the same user id in owner_uid/opr_uid.
110        customer_id: acc
111            .owner_uid
112            .filter(|uid| *uid != 0)
113            .or_else(|| acc.opr_uid.filter(|uid| *uid != 0)),
114        cipher: cache.get_cipher(acc.acc_id).unwrap_or_default(),
115    })
116}
117
118/// Crypto native account requests need broker ids such as 1001/1007.
119///
120/// Prefer the C++ sort key `(BrokerID << 48) | (TrdMkt << 32) | IntraAccID`
121/// stored by `bridge/account/real_projection.rs:323-326`. If a historical
122/// cache entry lacks that key, fall back to `Trd_Common.SecurityFirm` mapping
123/// used by C++ `NetCallback::BrokerIDToTcpCategory`.
124pub fn broker_id_from_account(acc: &CachedTrdAcc) -> Option<u32> {
125    let from_sort_key = (acc.sort_key >> 48) as u32;
126    if from_sort_key != 0 {
127        return Some(from_sort_key);
128    }
129    acc.security_firm.and_then(security_firm_to_broker_id)
130}
131
132pub fn security_firm_to_broker_id(sf: i32) -> Option<u32> {
133    match sf {
134        1 => Some(1001),
135        2 => Some(1007),
136        3 => Some(1008),
137        4 => Some(1009),
138        5 => Some(1019),
139        6 => Some(1017),
140        7 => Some(1012),
141        _ => None,
142    }
143}
144
145/// v1.4.110 codex audit P1 #3: 用户唯一已开户 crypto account 的 broker_id.
146///
147/// 对齐 C++ `INNData_Trd_MainBrokerage::GetCryptoSupportedDefaultMainBroker`
148/// (line 70-123) 优先级 1: 如果只开了一个 crypto account, 直接取该 account 的 broker.
149///
150/// 返:
151/// - `Some(broker_id)`: trd_cache 恰好 1 个 `is_crypto_account()`, 取其 broker
152/// - `None`: 0 个或 ≥ 2 个 crypto account, caller 走 9419 crypto_brokers / fallback 路径
153///
154/// 此值作 `resolve_qot_broker_for_request` / `resolve_or_reject_broker` 第 5
155/// 参数注入, QOT handler `securityFirm=Unknown(0)` 时决定 default broker.
156pub fn single_crypto_account_broker(trd_cache: &futu_cache::trd_cache::TrdCache) -> Option<u32> {
157    let crypto_brokers: std::collections::HashSet<u32> = trd_cache
158        .accounts
159        .iter()
160        .filter(|r| r.value().is_crypto_account())
161        .filter_map(|r| broker_id_from_account(r.value()))
162        .collect();
163    if crypto_brokers.len() == 1 {
164        crypto_brokers.into_iter().next()
165    } else {
166        None
167    }
168}
169
170fn crypto_context_error(msg: String) -> FutuError {
171    FutuError::ServerError { ret_type: -1, msg }
172}