Skip to main content

futu_backend/
command_runtime.rs

1//! Compatibility exports and backend transport adapter for `futu-command-runtime`.
2//!
3//! Pure command execution contracts live in `futu-command-runtime`; this module
4//! keeps the stable `futu_backend::command_runtime` import path and wires the
5//! production backend connection into the new transport trait.
6
7use async_trait::async_trait;
8use bytes::Bytes;
9use futu_command_runtime::{
10    CommandAuthorizationEvidence, CommandRequest, CommandResponse, CommandTransport,
11};
12use futu_command_spec::trade::{TradeBackendChannel, trade_account_discovery_command};
13use futu_command_spec::{
14    AuthRequirement, BackendChannelKind, BrokerDiscoveryOperation, CommandSpecId,
15    ConnectionDiscoveryOperation, HeartbeatOperation, backend_extension_command,
16    broker_discovery_command, command_spec_by_id, connection_discovery_command, heartbeat_command,
17    indicator_command, static_data_read_command, system_read_command, user_cloud_command,
18};
19pub use futu_command_spec::{
20    BackendExtensionOperation, CMD_ALGO_ORDER_LOGS, CMD_BATCH_CLOSE_POSITIONS,
21    CMD_BFF_ORDER_CANCEL, CMD_BFF_ORDER_CONFIRM, CMD_BFF_ORDER_DELETE, CMD_BFF_ORDER_DETAIL,
22    CMD_BFF_ORDER_NEW_V2, CMD_BFF_ORDER_REPLACE, CMD_CLEAR_FUTURES_POSITIONS, CMD_CUSTOMER_BCAN,
23    CMD_CUSTOMER_COMPANY_INFO, CMD_GET_BIZ_GROUP, CMD_GET_BOND_ANSWER_STATE,
24    CMD_GET_BOND_POSITION_LIST, CMD_GET_BOND_SINGLE_ASSET, CMD_GET_BOND_TOTAL_ASSET,
25    CMD_GET_BOND_TRADE_REMINDER, CMD_GET_CASH_DETAIL, CMD_GET_CASH_LOG, CMD_GET_CN_AH_MARGIN_INFO,
26    CMD_GET_HK_MARGIN_INFO, CMD_GET_US_MARGIN_INFO, CMD_POSITION_ACTION_RESULT,
27    CMD_POSITION_CORPORATE_ACTION_DERIVATIVE, CMD_POSITION_CORPORATE_ACTION_STOCK,
28    CMD_PULL_ACCOUNT_FLAG, CMD_REVERSE_POSITION, CMD_ROLL_POSITION,
29    IndicatorOperation as IndicatorCommandOperation, MarketEventOperation, QotReadOperation,
30    QotWriteOperation, StaticDataReadOperation, SystemReadOperation,
31    TradeAccountDiscoveryOperation, TradeAuthOperation, TradeQueryEnvironment, TradeQueryOperation,
32    UserCloudOperation,
33    trade::{CryptoTradeOperation, TradeWriteOperation},
34};
35use futu_core::error::{FutuError, Result as FutuResult};
36
37use crate::conn::{
38    BackendConn, BackendWriterAdmission, RequestTimeoutPolicy, WriterAdmissionObserver,
39};
40use crate::quote_sub::{
41    DispatchAccepted, NormalSubscriptionDispatchHooks, QotSubscriptionWriterAdmission,
42};
43
44#[path = "command_runtime_policy.rs"]
45mod policy;
46
47#[path = "command_runtime_qot.rs"]
48mod qot;
49
50pub(crate) use qot::execute_qot_subscription_set_with_dispatch;
51pub use qot::{
52    execute_market_event, execute_qot_read, execute_qot_read_with_reserved,
53    execute_qot_subscription_set,
54};
55
56use policy::{BACKEND_COMMAND_TIMEOUT, BackendCommandPolicyPorts};
57
58#[cfg(test)]
59#[path = "command_runtime_tests.rs"]
60mod tests;
61
62#[cfg(test)]
63#[path = "command_runtime_crypto_tests.rs"]
64mod crypto_tests;
65
66#[cfg(test)]
67#[path = "command_runtime_ordinary_tests.rs"]
68mod ordinary_tests;
69
70#[cfg(test)]
71pub(crate) use crate::command_runtime_test_support::{
72    decode_internal_test_request, decode_internal_test_request_body, encode_internal_test_response,
73    execute_internal_test_command,
74};
75
76pub use futu_command_runtime::{
77    ChannelId, ChannelLifecycleState, ChannelRuntimeAction, CommandExecution,
78    CommandExecutionContext, CommandExecutionOutcome, CommandOutcome, CommandRuntime,
79    CommandRuntimeAction, CommandRuntimeDecision, CommandRuntimeError, decide_command_outcome,
80};
81
82#[derive(Clone)]
83pub(crate) struct SubscriptionDispatchContext {
84    pub(crate) quote_market_type: u8,
85    pub(crate) exact_normal_wire: Vec<u8>,
86    pub(crate) hooks: NormalSubscriptionDispatchHooks,
87    pub(crate) accepted: std::sync::Arc<parking_lot::Mutex<Option<DispatchAccepted>>>,
88}
89
90pub type QotWritePublishTerminal = dyn Fn(u64, &mut dyn FnMut()) -> bool + Send + Sync;
91
92pub struct ChannelBoundBackendTransport<'a> {
93    backend: &'a BackendConn,
94    channel: BackendChannelKind,
95    request_timeout: std::time::Duration,
96    timeout_policy: RequestTimeoutPolicy,
97    subscription_dispatch: Option<SubscriptionDispatchContext>,
98    writer_admission_guard: Option<std::sync::Arc<dyn Fn(u64) -> bool + Send + Sync>>,
99    writer_admission_publish_terminal: Option<std::sync::Arc<QotWritePublishTerminal>>,
100    writer_admission_accepted: Option<std::sync::Arc<dyn Fn(u64) + Send + Sync>>,
101}
102
103impl<'a> ChannelBoundBackendTransport<'a> {
104    #[must_use]
105    pub fn new(backend: &'a BackendConn, channel: BackendChannelKind) -> Self {
106        Self {
107            backend,
108            channel,
109            request_timeout: BACKEND_COMMAND_TIMEOUT,
110            timeout_policy: RequestTimeoutPolicy::Disconnect,
111            subscription_dispatch: None,
112            writer_admission_guard: None,
113            writer_admission_publish_terminal: None,
114            writer_admission_accepted: None,
115        }
116    }
117
118    fn keep_connection_on_timeout(mut self) -> Self {
119        self.timeout_policy = RequestTimeoutPolicy::KeepConnection;
120        self
121    }
122
123    fn with_subscription_dispatch(mut self, context: SubscriptionDispatchContext) -> Self {
124        self.subscription_dispatch = Some(context);
125        self
126    }
127
128    fn with_writer_admission_guard(
129        mut self,
130        guard: std::sync::Arc<dyn Fn(u64) -> bool + Send + Sync>,
131    ) -> Self {
132        self.writer_admission_guard = Some(guard);
133        self
134    }
135
136    fn with_writer_admission_accepted(
137        mut self,
138        accepted: std::sync::Arc<dyn Fn(u64) + Send + Sync>,
139    ) -> Self {
140        self.writer_admission_accepted = Some(accepted);
141        self
142    }
143
144    fn with_writer_admission_publish_terminal(
145        mut self,
146        terminal: std::sync::Arc<QotWritePublishTerminal>,
147    ) -> Self {
148        self.writer_admission_publish_terminal = Some(terminal);
149        self
150    }
151}
152
153#[async_trait]
154impl CommandTransport for ChannelBoundBackendTransport<'_> {
155    fn channel_kind(&self) -> Option<BackendChannelKind> {
156        Some(self.channel)
157    }
158
159    async fn execute(&self, request: CommandRequest) -> FutuResult<CommandResponse> {
160        let context = self.subscription_dispatch.as_ref();
161        let observer = match request.spec_id {
162            CommandSpecId::QotSubscriptionSet => context.map(|context| {
163                let quote_market_type = context.quote_market_type;
164                let exact_normal_wire = context.exact_normal_wire.clone();
165                let hooks = context.hooks.clone();
166                let admission_hooks = hooks.clone();
167                let accepted = std::sync::Arc::clone(&context.accepted);
168                WriterAdmissionObserver {
169                    try_admit: std::sync::Arc::new(move |admission: BackendWriterAdmission| {
170                        (admission_hooks.try_admit)(QotSubscriptionWriterAdmission {
171                            connection_generation: admission.connection_generation,
172                            serial_no: admission.serial_no,
173                        })
174                    }),
175                    publish_terminal: None,
176                    on_accepted: std::sync::Arc::new(move |admission: BackendWriterAdmission| {
177                        let receipt = (hooks.on_accepted)(
178                            QotSubscriptionWriterAdmission {
179                                connection_generation: admission.connection_generation,
180                                serial_no: admission.serial_no,
181                            },
182                            quote_market_type,
183                            exact_normal_wire.clone(),
184                        );
185                        *accepted.lock() = Some(receipt);
186                    }),
187                }
188            }),
189            _ if context.is_some() => {
190                return Err(FutuError::Codec(
191                    "subscription dispatch context used by non-subscription command".into(),
192                ));
193            }
194            _ if self.writer_admission_guard.is_some()
195                || self.writer_admission_publish_terminal.is_some() =>
196            {
197                let guard = self.writer_admission_guard.clone();
198                let terminal = self.writer_admission_publish_terminal.clone();
199                let accepted = self.writer_admission_accepted.clone();
200                Some(WriterAdmissionObserver {
201                    try_admit: std::sync::Arc::new(move |admission| {
202                        guard
203                            .as_ref()
204                            .is_none_or(|guard| guard(admission.connection_generation))
205                    }),
206                    publish_terminal: terminal.map(|terminal| {
207                        std::sync::Arc::new(
208                            move |admission: BackendWriterAdmission, publish: &mut dyn FnMut()| {
209                                terminal(admission.connection_generation, publish)
210                            },
211                        )
212                            as std::sync::Arc<
213                                dyn Fn(BackendWriterAdmission, &mut dyn FnMut()) -> bool
214                                    + Send
215                                    + Sync,
216                            >
217                    }),
218                    on_accepted: std::sync::Arc::new(move |admission| {
219                        if let Some(accepted) = accepted.as_ref() {
220                            accepted(admission.connection_generation);
221                        }
222                    }),
223                })
224            }
225            _ => None,
226        };
227        execute_on_backend(
228            self.backend,
229            request,
230            self.request_timeout,
231            self.timeout_policy,
232            observer,
233        )
234        .await
235    }
236}
237
238fn runtime_with_transport(
239    transport: ChannelBoundBackendTransport<'_>,
240    authorization: CommandAuthorizationEvidence,
241) -> CommandRuntime<ChannelBoundBackendTransport<'_>, BackendCommandPolicyPorts<'_>> {
242    let policy = BackendCommandPolicyPorts::new(transport.backend, authorization);
243    CommandRuntime::with_policy(transport, policy)
244}
245
246fn login_runtime_with_transport(
247    transport: ChannelBoundBackendTransport<'_>,
248) -> CommandRuntime<ChannelBoundBackendTransport<'_>, BackendCommandPolicyPorts<'_>> {
249    runtime_with_transport(transport, CommandAuthorizationEvidence::LoginSession)
250}
251
252fn authorization_for_command(
253    spec_id: CommandSpecId,
254    trade_cipher: Option<&[u8]>,
255) -> FutuResult<CommandAuthorizationEvidence> {
256    let spec = command_spec_by_id(spec_id)
257        .ok_or_else(|| FutuError::Codec(format!("unknown command spec id: {spec_id:?}")))?;
258    match spec.auth {
259        AuthRequirement::TradeUnlocked => trade_cipher
260            .ok_or_else(|| {
261                FutuError::Codec(
262                    "command authorization evidence: trade cipher was not provided".to_owned(),
263                )
264            })
265            .and_then(|cipher| {
266                CommandAuthorizationEvidence::from_trade_cipher(cipher).map_err(|error| {
267                    FutuError::Codec(format!("command authorization evidence: {error}"))
268                })
269            }),
270        AuthRequirement::None | AuthRequirement::Login => {
271            Ok(CommandAuthorizationEvidence::LoginSession)
272        }
273    }
274}
275
276pub async fn execute_trade_read(
277    backend: &BackendConn,
278    operation: TradeQueryOperation,
279    environment: TradeQueryEnvironment,
280    trade_cipher: Option<&[u8]>,
281    body: Bytes,
282) -> FutuResult<CommandResponse> {
283    let channel = match environment {
284        TradeQueryEnvironment::Real => BackendChannelKind::Broker,
285        TradeQueryEnvironment::Sim => BackendChannelKind::Platform,
286    };
287    let spec_id = CommandSpecId::TradeRead {
288        operation,
289        environment,
290    };
291    let runtime = runtime_with_transport(
292        ChannelBoundBackendTransport::new(backend, channel),
293        authorization_for_command(spec_id, trade_cipher)?,
294    );
295    let execution = runtime
296        .execute(CommandExecutionContext::new(spec_id, body))
297        .await
298        .map_err(|error| FutuError::Codec(format!("trade-read command spec error: {error}")))?;
299    let report = &execution.report;
300    tracing::trace!(
301        command = report.command_name,
302        cmd_id = report.cmd_id,
303        channel = ?report.channel,
304        outcome = ?report.outcome,
305        response_body_len = report.response_body_len,
306        "command runtime executed trade read"
307    );
308    execution.into_response()
309}
310
311pub async fn execute_backend_extension(
312    backend: &BackendConn,
313    operation: BackendExtensionOperation,
314    body: Bytes,
315) -> FutuResult<CommandResponse> {
316    let spec = backend_extension_command(operation);
317    let runtime = login_runtime_with_transport(ChannelBoundBackendTransport::new(
318        backend,
319        spec.runtime.channel,
320    ));
321    let execution = runtime
322        .execute(CommandExecutionContext::new(
323            CommandSpecId::BackendExtension(operation),
324            body,
325        ))
326        .await
327        .map_err(|error| {
328            FutuError::Codec(format!("backend-extension command spec error: {error}"))
329        })?;
330    let report = &execution.report;
331    tracing::trace!(
332        command = report.command_name,
333        cmd_id = report.cmd_id,
334        channel = ?report.channel,
335        evidence_kind = ?report.command_evidence.kind,
336        outcome = ?report.outcome,
337        response_body_len = report.response_body_len,
338        "command runtime executed backend extension"
339    );
340    execution.into_response()
341}
342
343pub async fn execute_backend_extension_guarded(
344    backend: &BackendConn,
345    operation: BackendExtensionOperation,
346    body: Bytes,
347    guard: std::sync::Arc<dyn Fn(u64) -> bool + Send + Sync>,
348) -> FutuResult<CommandResponse> {
349    let spec = backend_extension_command(operation);
350    let transport = ChannelBoundBackendTransport::new(backend, spec.runtime.channel)
351        .with_writer_admission_guard(guard);
352    let runtime = login_runtime_with_transport(transport);
353    let execution = runtime
354        .execute(CommandExecutionContext::new(
355            CommandSpecId::BackendExtension(operation),
356            body,
357        ))
358        .await
359        .map_err(|error| {
360            FutuError::Codec(format!(
361                "guarded backend-extension command spec error: {error}"
362            ))
363        })?;
364    execution.into_response()
365}
366
367pub async fn execute_trade_auth(
368    backend: &BackendConn,
369    operation: TradeAuthOperation,
370    body: Bytes,
371) -> FutuResult<CommandResponse> {
372    let runtime = login_runtime_with_transport(ChannelBoundBackendTransport::new(
373        backend,
374        BackendChannelKind::Broker,
375    ));
376    let execution = runtime
377        .execute(CommandExecutionContext::new(
378            CommandSpecId::TradeAuth(operation),
379            body,
380        ))
381        .await
382        .map_err(|error| FutuError::Codec(format!("trade-auth command spec error: {error}")))?;
383    let report = &execution.report;
384    tracing::trace!(
385        command = report.command_name,
386        cmd_id = report.cmd_id,
387        channel = ?report.channel,
388        outcome = ?report.outcome,
389        response_body_len = report.response_body_len,
390        "command runtime executed trade auth"
391    );
392    execution.into_response()
393}
394
395pub async fn execute_trade_account_discovery(
396    backend: &BackendConn,
397    operation: TradeAccountDiscoveryOperation,
398    body: Bytes,
399) -> FutuResult<CommandResponse> {
400    let channel = match trade_account_discovery_command(operation).channel {
401        TradeBackendChannel::Broker => BackendChannelKind::Broker,
402        TradeBackendChannel::Platform => BackendChannelKind::Platform,
403    };
404    let runtime = login_runtime_with_transport(ChannelBoundBackendTransport::new(backend, channel));
405    let execution = runtime
406        .execute(CommandExecutionContext::new(
407            CommandSpecId::TradeAccountDiscovery(operation),
408            body,
409        ))
410        .await
411        .map_err(|error| {
412            FutuError::Codec(format!(
413                "trade-account-discovery command spec error: {error}"
414            ))
415        })?;
416    let report = &execution.report;
417    tracing::trace!(
418        command = report.command_name,
419        cmd_id = report.cmd_id,
420        channel = ?report.channel,
421        outcome = ?report.outcome,
422        response_body_len = report.response_body_len,
423        "command runtime executed trade account discovery"
424    );
425    execution.into_response()
426}
427
428pub async fn execute_connection_discovery(
429    backend: &BackendConn,
430    operation: ConnectionDiscoveryOperation,
431    body: Bytes,
432) -> FutuResult<CommandResponse> {
433    let spec = connection_discovery_command(operation);
434    let runtime =
435        login_runtime_with_transport(ChannelBoundBackendTransport::new(backend, spec.channel));
436    let execution = runtime
437        .execute(CommandExecutionContext::new(
438            CommandSpecId::ConnectionDiscovery(operation),
439            body,
440        ))
441        .await
442        .map_err(|error| {
443            FutuError::Codec(format!("connection-discovery command spec error: {error}"))
444        })?;
445    let report = &execution.report;
446    tracing::trace!(
447        command = report.command_name,
448        cmd_id = report.cmd_id,
449        channel = ?report.channel,
450        outcome = ?report.outcome,
451        response_body_len = report.response_body_len,
452        "command runtime executed connection discovery"
453    );
454    execution.into_response()
455}
456
457pub async fn execute_broker_discovery(
458    backend: &BackendConn,
459    operation: BrokerDiscoveryOperation,
460    body: Bytes,
461) -> FutuResult<CommandResponse> {
462    if operation == BrokerDiscoveryOperation::ValidBrokerListChangedPush {
463        return Err(FutuError::Codec(
464            "CMD20177 is push-only and cannot be executed as a request".into(),
465        ));
466    }
467    let spec = broker_discovery_command(operation);
468    let mut transport = ChannelBoundBackendTransport::new(backend, spec.runtime.channel);
469    if operation == BrokerDiscoveryOperation::ValidBrokerList {
470        transport = transport.keep_connection_on_timeout();
471    }
472    let runtime = login_runtime_with_transport(transport);
473    let execution = runtime
474        .execute(CommandExecutionContext::new(
475            CommandSpecId::BrokerDiscovery(operation),
476            body,
477        ))
478        .await
479        .map_err(|error| {
480            FutuError::Codec(format!("broker-discovery command spec error: {error}"))
481        })?;
482    let report = &execution.report;
483    tracing::trace!(
484        command = report.command_name,
485        cmd_id = report.cmd_id,
486        channel = ?report.channel,
487        outcome = ?report.outcome,
488        response_body_len = report.response_body_len,
489        "command runtime executed broker discovery"
490    );
491    execution.into_response()
492}
493
494pub async fn execute_heartbeat(
495    backend: &BackendConn,
496    operation: HeartbeatOperation,
497    body: Bytes,
498) -> FutuResult<CommandResponse> {
499    let spec = heartbeat_command(operation);
500    let runtime = login_runtime_with_transport(ChannelBoundBackendTransport::new(
501        backend,
502        spec.runtime.channel,
503    ));
504    let execution = runtime
505        .execute(CommandExecutionContext::new(
506            CommandSpecId::Heartbeat(operation),
507            body,
508        ))
509        .await
510        .map_err(|error| FutuError::Codec(format!("heartbeat command spec error: {error}")))?;
511    let report = &execution.report;
512    tracing::trace!(
513        command = report.command_name,
514        cmd_id = report.cmd_id,
515        channel = ?report.channel,
516        outcome = ?report.outcome,
517        response_body_len = report.response_body_len,
518        "command runtime executed heartbeat"
519    );
520    execution.into_response()
521}
522
523pub async fn execute_trade_write(
524    backend: &BackendConn,
525    operation: TradeWriteOperation,
526    environment: TradeQueryEnvironment,
527    trade_cipher: Option<&[u8]>,
528    body: Bytes,
529) -> FutuResult<CommandResponse> {
530    let channel = match environment {
531        TradeQueryEnvironment::Real => BackendChannelKind::Broker,
532        TradeQueryEnvironment::Sim => BackendChannelKind::Platform,
533    };
534    let spec_id = CommandSpecId::TradeWrite {
535        operation,
536        environment,
537    };
538    let runtime = runtime_with_transport(
539        ChannelBoundBackendTransport::new(backend, channel),
540        authorization_for_command(spec_id, trade_cipher)?,
541    );
542    let execution = runtime
543        .execute(CommandExecutionContext::new(spec_id, body))
544        .await
545        .map_err(|error| FutuError::Codec(format!("trade-write command spec error: {error}")))?;
546    let report = &execution.report;
547    tracing::trace!(
548        command = report.command_name,
549        cmd_id = report.cmd_id,
550        channel = ?report.channel,
551        outcome = ?report.outcome,
552        response_body_len = report.response_body_len,
553        "command runtime executed trade write"
554    );
555    execution.into_response()
556}
557
558pub async fn execute_crypto_trade_command(
559    backend: &BackendConn,
560    operation: CryptoTradeOperation,
561    trade_cipher: Option<&[u8]>,
562    body: Bytes,
563) -> FutuResult<CommandResponse> {
564    let spec_id = CommandSpecId::CryptoTrade(operation);
565    let runtime = runtime_with_transport(
566        ChannelBoundBackendTransport::new(backend, BackendChannelKind::Broker),
567        authorization_for_command(spec_id, trade_cipher)?,
568    );
569    let execution = runtime
570        .execute(CommandExecutionContext::new(spec_id, body))
571        .await
572        .map_err(|error| FutuError::Codec(format!("crypto-trade command spec error: {error}")))?;
573    let report = &execution.report;
574    tracing::trace!(
575        command = report.command_name,
576        cmd_id = report.cmd_id,
577        channel = ?report.channel,
578        outcome = ?report.outcome,
579        response_body_len = report.response_body_len,
580        "command runtime executed crypto trade command"
581    );
582    execution.into_response()
583}
584
585pub async fn execute_system_read(
586    backend: &BackendConn,
587    operation: SystemReadOperation,
588    body: Bytes,
589) -> FutuResult<CommandResponse> {
590    let spec = system_read_command(operation);
591    let runtime = login_runtime_with_transport(ChannelBoundBackendTransport::new(
592        backend,
593        spec.runtime.channel,
594    ));
595    let execution = runtime
596        .execute(CommandExecutionContext::new(
597            CommandSpecId::SystemRead(operation),
598            body,
599        ))
600        .await
601        .map_err(|error| FutuError::Codec(format!("system-read command spec error: {error}")))?;
602    let report = &execution.report;
603    tracing::trace!(
604        command = report.command_name,
605        cmd_id = report.cmd_id,
606        channel = ?report.channel,
607        evidence_kind = ?report.command_evidence.kind,
608        outcome = ?report.outcome,
609        response_body_len = report.response_body_len,
610        "command runtime executed system read"
611    );
612    execution.into_response()
613}
614
615pub async fn execute_indicator_read(
616    backend: &BackendConn,
617    operation: IndicatorCommandOperation,
618    body: Bytes,
619) -> FutuResult<CommandResponse> {
620    let spec = indicator_command(operation);
621    let runtime = login_runtime_with_transport(ChannelBoundBackendTransport::new(
622        backend,
623        spec.runtime.channel,
624    ));
625    let execution = runtime
626        .execute(CommandExecutionContext::new(
627            CommandSpecId::Indicator(operation),
628            body,
629        ))
630        .await
631        .map_err(|error| FutuError::Codec(format!("indicator command spec error: {error}")))?;
632    execution.into_response()
633}
634
635pub async fn execute_static_data_read(
636    backend: &BackendConn,
637    operation: StaticDataReadOperation,
638    body: Bytes,
639) -> FutuResult<CommandResponse> {
640    let spec = static_data_read_command(operation);
641    let runtime = login_runtime_with_transport(ChannelBoundBackendTransport::new(
642        backend,
643        spec.runtime.channel,
644    ));
645    let execution = runtime
646        .execute(CommandExecutionContext::new(
647            CommandSpecId::StaticDataRead(operation),
648            body,
649        ))
650        .await
651        .map_err(|error| {
652            FutuError::Codec(format!("static-data-read command spec error: {error}"))
653        })?;
654    let report = &execution.report;
655    tracing::trace!(
656        command = report.command_name,
657        cmd_id = report.cmd_id,
658        channel = ?report.channel,
659        outcome = ?report.outcome,
660        response_body_len = report.response_body_len,
661        "command runtime executed static-data read"
662    );
663    execution.into_response()
664}
665
666pub async fn execute_user_cloud(
667    backend: &BackendConn,
668    operation: UserCloudOperation,
669    body: Bytes,
670) -> FutuResult<CommandResponse> {
671    if operation == UserCloudOperation::UpdatePush {
672        return Err(FutuError::Codec(
673            "user-cloud CMD20175 is push-only and cannot be executed as a request".to_string(),
674        ));
675    }
676    let spec = user_cloud_command(operation);
677    if spec.runtime.channel != BackendChannelKind::Platform {
678        return Err(FutuError::Codec(format!(
679            "user-cloud command {} is bound to {:?}",
680            spec.runtime.name, spec.runtime.channel
681        )));
682    }
683    let runtime = login_runtime_with_transport(ChannelBoundBackendTransport::new(
684        backend,
685        spec.runtime.channel,
686    ));
687    let execution = runtime
688        .execute(CommandExecutionContext::new(
689            CommandSpecId::UserCloud(operation),
690            body,
691        ))
692        .await
693        .map_err(|error| FutuError::Codec(format!("user-cloud command spec error: {error}")))?;
694    let report = &execution.report;
695    tracing::trace!(
696        command = report.command_name,
697        cmd_id = report.cmd_id,
698        channel = ?report.channel,
699        outcome = ?report.outcome,
700        response_body_len = report.response_body_len,
701        "command runtime executed user cloud command"
702    );
703    execution.into_response()
704}
705
706pub async fn execute_qot_plaintext(
707    backend: &BackendConn,
708    cmd_id: u16,
709    body: Bytes,
710    reserved: [u8; 10],
711) -> FutuResult<CommandResponse> {
712    execute_qot_command(backend, CommandSpecId::QotPlaintext(cmd_id), body, reserved).await
713}
714
715pub async fn execute_qot_write(
716    backend: &BackendConn,
717    operation: QotWriteOperation,
718    body: Bytes,
719    reserved: [u8; 10],
720) -> FutuResult<CommandResponse> {
721    let id = CommandSpecId::QotWrite(operation);
722    let spec = futu_command_spec::command_spec_by_id(id).ok_or_else(|| {
723        FutuError::Codec(format!("qot-write command spec missing for {operation:?}"))
724    })?;
725    let runtime =
726        login_runtime_with_transport(ChannelBoundBackendTransport::new(backend, spec.channel));
727    let execution = runtime
728        .execute(CommandExecutionContext::new(id, body).with_reserved(reserved))
729        .await
730        .map_err(|error| FutuError::Codec(format!("qot-write command spec error: {error}")))?;
731    execution.into_response()
732}
733
734pub async fn execute_qot_write_guarded(
735    backend: &BackendConn,
736    operation: QotWriteOperation,
737    body: Bytes,
738    reserved: [u8; 10],
739    publish_terminal: std::sync::Arc<QotWritePublishTerminal>,
740    accepted: std::sync::Arc<dyn Fn(u64) + Send + Sync>,
741) -> FutuResult<CommandResponse> {
742    let id = CommandSpecId::QotWrite(operation);
743    let spec = futu_command_spec::command_spec_by_id(id).ok_or_else(|| {
744        FutuError::Codec(format!("qot-write command spec missing for {operation:?}"))
745    })?;
746    let transport = ChannelBoundBackendTransport::new(backend, spec.channel)
747        .with_writer_admission_publish_terminal(publish_terminal)
748        .with_writer_admission_accepted(accepted);
749    let runtime = login_runtime_with_transport(transport);
750    let execution = runtime
751        .execute(CommandExecutionContext::new(id, body).with_reserved(reserved))
752        .await
753        .map_err(|error| {
754            FutuError::Codec(format!("guarded qot-write command spec error: {error}"))
755        })?;
756    execution.into_response()
757}
758
759async fn execute_qot_command(
760    backend: &BackendConn,
761    spec_id: CommandSpecId,
762    body: Bytes,
763    reserved: [u8; 10],
764) -> FutuResult<CommandResponse> {
765    let runtime = login_runtime_with_transport(ChannelBoundBackendTransport::new(
766        backend,
767        BackendChannelKind::Qot,
768    ));
769    let execution = runtime
770        .execute(CommandExecutionContext::new(spec_id, body).with_reserved(reserved))
771        .await
772        .map_err(|error| FutuError::Codec(format!("qot command spec error: {error}")))?;
773    let report = &execution.report;
774    tracing::trace!(
775        command = report.command_name,
776        cmd_id = report.cmd_id,
777        channel = ?report.channel,
778        outcome = ?report.outcome,
779        response_body_len = report.response_body_len,
780        "command runtime executed QOT command"
781    );
782    execution.into_response()
783}
784
785async fn execute_on_backend(
786    backend: &BackendConn,
787    request: CommandRequest,
788    request_timeout: std::time::Duration,
789    timeout_policy: RequestTimeoutPolicy,
790    observer: Option<WriterAdmissionObserver>,
791) -> FutuResult<CommandResponse> {
792    let frame = backend
793        .request_with_reserved_timeout_policy_observed(
794            request.cmd_id,
795            request.body.to_vec(),
796            request.reserved,
797            request_timeout,
798            timeout_policy,
799            observer,
800        )
801        .await?;
802    Ok(CommandResponse {
803        request_serial_no: frame.header.serial_no,
804        cmd_id: frame.header.cmd_id,
805        body: frame.body,
806        ex_head: frame.ex_head,
807    })
808}