Skip to main content

strata_sdk/
order_stream.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3use std::time::Duration;
4
5use futures_util::{SinkExt, StreamExt};
6use tokio::sync::{broadcast, mpsc, oneshot};
7use tokio::task::JoinHandle;
8use tokio_tungstenite::tungstenite::Message;
9
10use super::*;
11
12pub const ORDER_STREAM_AUTH_DOMAIN: &str = "strata:order-command-stream:v2";
13const COMMAND_TIMEOUT: Duration = Duration::from_secs(10);
14const EVENT_BUFFER: usize = 1_024;
15const MAX_CLIENT_COMMANDS_PER_FRAME: usize = 64;
16const MAX_SERVER_EVENTS_PER_FRAME: usize = 64;
17
18#[derive(Clone, Debug)]
19pub struct OrderChallengeResult {
20    pub self_trade_prevention: PlatformSelfTradePrevention,
21    pub prevented_order_ids: Vec<String>,
22    pub effective_request: PlatformOrderChallengeRequest,
23    pub response: PlatformOrderChallengeResponse,
24}
25
26struct ActorRequest {
27    command: PlatformOrderCommand,
28    response: oneshot::Sender<Result<PlatformOrderCommandEvent, SdkError>>,
29}
30
31/// Cloneable handle to one authenticated persistent order-command socket.
32/// Commands from clones are sequenced by a single writer task, so concurrent
33/// strategies cannot produce sequence gaps on the wire.
34#[derive(Clone)]
35pub struct OrderCommandStream {
36    market_id: String,
37    owner_wallet: String,
38    session_public_key: String,
39    commands: mpsc::Sender<ActorRequest>,
40    events: broadcast::Sender<PlatformOrderCommandEvent>,
41}
42
43impl std::fmt::Debug for OrderCommandStream {
44    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        formatter
46            .debug_struct("OrderCommandStream")
47            .field("market_id", &self.market_id)
48            .field("owner_wallet", &self.owner_wallet)
49            .field("session_public_key", &self.session_public_key)
50            .finish_non_exhaustive()
51    }
52}
53
54impl OrderCommandStream {
55    pub(crate) async fn connect<S: SessionSigner + ?Sized>(
56        client: &StrataClient,
57        market_id: &str,
58        owner_wallet: &str,
59        signer: &S,
60    ) -> Result<Self, SdkError> {
61        let market_id = validate_platform_market_id(market_id)?;
62        let owner_wallet = canonical_public_key(owner_wallet, "owner_wallet")?;
63        let session_public_key = canonical_public_key(signer.public_key(), "session_public_key")?;
64        if owner_wallet == session_public_key {
65            return Err(SdkError::InvalidRequest(
66                "session_public_key must be distinct from owner_wallet".to_owned(),
67            ));
68        }
69        let mut url = client.base_url.clone();
70        let scheme = match url.scheme() {
71            "https" => "wss",
72            "http" => "ws",
73            _ => {
74                return Err(SdkError::InvalidBaseUrl(
75                    "order command URL must use http or https".to_owned(),
76                ))
77            }
78        };
79        url.set_scheme(scheme).map_err(|_| {
80            SdkError::InvalidBaseUrl("could not select WebSocket scheme".to_owned())
81        })?;
82        let base_path = url.path().trim_end_matches('/');
83        url.set_path(&format!("{base_path}/v2/markets/{market_id}/orders/stream"));
84        url.set_query(None);
85        url.set_fragment(None);
86
87        let (mut socket, _) = tokio_tungstenite::connect_async(url.as_str())
88            .await
89            .map_err(|error| SdkError::Stream(error.to_string()))?;
90        let auth = tokio::time::timeout(COMMAND_TIMEOUT, socket.next())
91            .await
92            .map_err(|_| SdkError::Stream("authentication challenge timed out".to_owned()))?
93            .ok_or_else(|| SdkError::Stream("socket closed before authentication".to_owned()))?
94            .map_err(|error| SdkError::Stream(error.to_string()))?;
95        let Message::Text(auth) = auth else {
96            return Err(SdkError::Stream(
97                "expected a text authentication challenge".to_owned(),
98            ));
99        };
100        let event: PlatformOrderCommandEvent = serde_json::from_str(&auth)
101            .map_err(|error| SdkError::InvalidResponse(error.to_string()))?;
102        let (challenge, server_time_ms, expires_at_ms) = match event {
103            PlatformOrderCommandEvent::AuthChallenge {
104                schema_version,
105                contract_version,
106                market_id: response_market,
107                challenge,
108                server_time_ms,
109                expires_at_ms,
110            } => {
111                validate_platform_version(schema_version, &contract_version)?;
112                if response_market != market_id
113                    || challenge.len() != 64
114                    || !challenge.bytes().all(|byte| byte.is_ascii_hexdigit())
115                    || challenge.bytes().any(|byte| byte.is_ascii_uppercase())
116                {
117                    return Err(SdkError::InvalidResponse(
118                        "order stream authentication bindings are invalid".to_owned(),
119                    ));
120                }
121                (challenge, server_time_ms, expires_at_ms)
122            }
123            _ => {
124                return Err(SdkError::InvalidResponse(
125                    "order stream did not begin with authentication".to_owned(),
126                ))
127            }
128        };
129        if expires_at_ms <= server_time_ms {
130            return Err(SdkError::InvalidResponse(
131                "order stream authentication challenge is expired".to_owned(),
132            ));
133        }
134        let auth_message =
135            order_stream_auth_message(&market_id, &owner_wallet, &session_public_key, &challenge);
136        let signature = signer
137            .sign_message(&auth_message)
138            .await
139            .map_err(SdkError::Signer)?;
140        if signature.len() != 64 {
141            return Err(SdkError::Signer(
142                "stream authentication signature must contain 64 bytes".to_owned(),
143            ));
144        }
145        socket
146            .send(Message::Text(
147                serde_json::to_string(&PlatformOrderCommandClientFrame::Authenticate {
148                    owner_wallet: owner_wallet.clone(),
149                    session_public_key: session_public_key.clone(),
150                    signature: bs58::encode(signature).into_string(),
151                    batch_format: Some(PlatformOrderCommandBatchFormat::CompactV1),
152                })
153                .map_err(|error| SdkError::InvalidRequest(error.to_string()))?
154                .into(),
155            ))
156            .await
157            .map_err(|error| SdkError::Stream(error.to_string()))?;
158
159        let ready = tokio::time::timeout(COMMAND_TIMEOUT, socket.next())
160            .await
161            .map_err(|_| SdkError::Stream("signed authentication timed out".to_owned()))?
162            .ok_or_else(|| SdkError::Stream("socket closed during authentication".to_owned()))?
163            .map_err(|error| SdkError::Stream(error.to_string()))?;
164        let Message::Text(ready) = ready else {
165            return Err(SdkError::Stream(
166                "expected a text authentication result".to_owned(),
167            ));
168        };
169        let mut ready_events = parse_order_command_events(&ready)?;
170        if ready_events.len() != 1 {
171            return Err(SdkError::InvalidResponse(
172                "order stream authentication returned an invalid event batch".to_owned(),
173            ));
174        }
175        let ready = ready_events.pop().expect("one ready event");
176        let (stream_id, sequence) = match &ready {
177            PlatformOrderCommandEvent::Ready {
178                schema_version,
179                contract_version,
180                market_id: response_market,
181                stream_id,
182                sequence,
183                ..
184            } => {
185                validate_platform_version(*schema_version, contract_version)?;
186                if response_market != &market_id
187                    || !valid_handle(stream_id, "order_command_stream_")
188                    || sequence != "1"
189                {
190                    return Err(SdkError::InvalidResponse(
191                        "order stream ready bindings are invalid".to_owned(),
192                    ));
193                }
194                (stream_id.clone(), 1u64)
195            }
196            _ => {
197                return Err(SdkError::InvalidResponse(
198                    "order stream authentication was not accepted".to_owned(),
199                ))
200            }
201        };
202
203        let (commands, receiver) = mpsc::channel(512);
204        let (events, _) = broadcast::channel(EVENT_BUFFER);
205        let _ = events.send(ready);
206        tokio::spawn(run_actor(
207            socket,
208            receiver,
209            events.clone(),
210            market_id.clone(),
211            stream_id,
212            sequence,
213        ));
214        Ok(Self {
215            market_id,
216            owner_wallet,
217            session_public_key,
218            commands,
219            events,
220        })
221    }
222
223    pub fn market_id(&self) -> &str {
224        &self.market_id
225    }
226
227    pub fn owner_wallet(&self) -> &str {
228        &self.owner_wallet
229    }
230
231    pub fn session_public_key(&self) -> &str {
232        &self.session_public_key
233    }
234
235    /// Subscribe to heartbeats, correlated command results, and pushed chain
236    /// confirmations. Lag is explicit through `broadcast::RecvError::Lagged`.
237    pub fn subscribe(&self) -> broadcast::Receiver<PlatformOrderCommandEvent> {
238        self.events.subscribe()
239    }
240
241    pub async fn command(
242        &self,
243        command: PlatformOrderCommand,
244    ) -> Result<PlatformOrderCommandEvent, SdkError> {
245        let (response, receiver) = oneshot::channel();
246        self.commands
247            .send(ActorRequest { command, response })
248            .await
249            .map_err(|_| SdkError::Stream("order command socket is closed".to_owned()))?;
250        tokio::time::timeout(COMMAND_TIMEOUT, receiver)
251            .await
252            .map_err(|_| SdkError::Stream("order command timed out".to_owned()))?
253            .map_err(|_| SdkError::Stream("order command actor stopped".to_owned()))?
254    }
255
256    /// Authenticated non-trading round trip for health and latency measurement.
257    pub async fn probe(&self, nonce: &str) -> Result<(), SdkError> {
258        if nonce.is_empty()
259            || nonce.len() > 64
260            || !nonce
261                .bytes()
262                .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'))
263        {
264            return Err(SdkError::InvalidRequest(
265                "order command probe nonce is invalid".to_owned(),
266            ));
267        }
268        match self
269            .command(PlatformOrderCommand::Probe {
270                nonce: nonce.to_owned(),
271            })
272            .await?
273        {
274            PlatformOrderCommandEvent::ProbeResult {
275                nonce: returned, ..
276            } if returned == nonce => Ok(()),
277            _ => Err(SdkError::InvalidResponse(
278                "order command probe result is invalid".to_owned(),
279            )),
280        }
281    }
282
283    pub async fn challenge(
284        &self,
285        request: PlatformOrderChallengeRequest,
286        self_trade_prevention: PlatformSelfTradePrevention,
287    ) -> Result<OrderChallengeResult, SdkError> {
288        let request = normalize_order_challenge_request(request)?;
289        self.ensure_request_identity(&request)?;
290        match self
291            .command(PlatformOrderCommand::Challenge {
292                request,
293                self_trade_prevention,
294            })
295            .await?
296        {
297            PlatformOrderCommandEvent::ChallengeResult {
298                self_trade_prevention,
299                prevented_order_ids,
300                effective_request,
301                response,
302                ..
303            } => {
304                self.ensure_request_identity(&effective_request)?;
305                validate_challenge_result(&self.market_id, &effective_request, &response)?;
306                Ok(OrderChallengeResult {
307                    self_trade_prevention,
308                    prevented_order_ids,
309                    effective_request,
310                    response,
311                })
312            }
313            _ => Err(SdkError::InvalidResponse(
314                "expected an order challenge result".to_owned(),
315            )),
316        }
317    }
318
319    /// Prepare an order-control transaction over the socket. `Authorized`
320    /// hands back a challenge; because this socket already authenticated the
321    /// session and the challenge is bound to it, `authorization_signature` may
322    /// be `None` (one signature: the session signs only the transaction).
323    /// `Direct` sends the operation itself.
324    pub async fn prepare(
325        &self,
326        request: PlatformOrderPrepareRequest,
327    ) -> Result<PlatformOrderPrepareResponse, SdkError> {
328        let request = match request {
329            PlatformOrderPrepareRequest::Authorized(authorization) => {
330                PlatformOrderPrepareRequest::Authorized(normalize_order_prepare_authorization(
331                    authorization,
332                )?)
333            }
334            PlatformOrderPrepareRequest::Direct(operation) => {
335                let operation = normalize_order_challenge_request(operation)?;
336                self.ensure_request_identity(&operation)?;
337                PlatformOrderPrepareRequest::Direct(operation)
338            }
339        };
340        match self
341            .command(PlatformOrderCommand::Prepare {
342                request: request.clone(),
343            })
344            .await?
345        {
346            PlatformOrderCommandEvent::PrepareResult { response, .. } => {
347                validate_prepared(&self.market_id, &response)?;
348                if let PlatformOrderPrepareRequest::Direct(operation) = &request {
349                    if response.action != order_request_action(operation) {
350                        return Err(SdkError::InvalidResponse(
351                            "prepared order action does not match request".to_owned(),
352                        ));
353                    }
354                }
355                Ok(response)
356            }
357            _ => Err(SdkError::InvalidResponse(
358                "expected an order prepare result".to_owned(),
359            )),
360        }
361    }
362
363    pub async fn submit(
364        &self,
365        request: PlatformOrderSubmitRequest,
366    ) -> Result<PlatformOrderSubmitResponse, SdkError> {
367        let request = normalize_submit_request(request)?;
368        let expected_control = request.order_control_id.clone();
369        match self
370            .command(PlatformOrderCommand::Submit { request })
371            .await?
372        {
373            PlatformOrderCommandEvent::SubmitResult { response, .. } => {
374                validate_submit(&self.market_id, &expected_control, &response)?;
375                Ok(response)
376            }
377            _ => Err(SdkError::InvalidResponse(
378                "expected an order submit result".to_owned(),
379            )),
380        }
381    }
382
383    pub async fn status(
384        &self,
385        request: PlatformOrderStatusRequest,
386    ) -> Result<PlatformOrderStatusResponse, SdkError> {
387        let request = normalize_status_request(request)?;
388        let expected_control = request.order_control_id.clone();
389        match self
390            .command(PlatformOrderCommand::Status { request })
391            .await?
392        {
393            PlatformOrderCommandEvent::StatusResult { response, .. } => {
394                validate_status(&self.market_id, &expected_control, &response)?;
395                Ok(response)
396            }
397            _ => Err(SdkError::InvalidResponse(
398                "expected an order status result".to_owned(),
399            )),
400        }
401    }
402
403    pub async fn execute_order<S, V>(
404        &self,
405        operation: &OrderExecuteOperation,
406        signer: &S,
407        verifier: &V,
408        idempotency_key: Option<&str>,
409        self_trade_prevention: PlatformSelfTradePrevention,
410    ) -> Result<PlatformOrderSubmitResponse, SdkError>
411    where
412        S: SessionSigner + ?Sized,
413        V: OrderVerifier + ?Sized,
414    {
415        self.ensure_signer(signer)?;
416        let challenged = self
417            .challenge(
418                operation.challenge_request(self.session_public_key.clone()),
419                self_trade_prevention,
420            )
421            .await?;
422        let (prepared, signed_transaction_base64) = self
423            .authorize_and_prepare(&challenged, signer, verifier)
424            .await?;
425        self.submit(PlatformOrderSubmitRequest {
426            order_control_id: prepared.order_control_id.clone(),
427            signed_transaction_base64,
428            idempotency_key: normalize_idempotency_key(
429                idempotency_key.unwrap_or(&prepared.order_control_id),
430            )?,
431        })
432        .await
433    }
434
435    /// Prepare and arm a fail-closed cancel-all, then maintain its heartbeat
436    /// for this transaction's lifetime. For indefinite unattended exposure,
437    /// use [`Self::maintain_dead_man`] so the blockhash is refreshed too.
438    pub async fn arm_dead_man<S, V>(
439        &self,
440        timeout: Duration,
441        signer: &S,
442        verifier: &V,
443        idempotency_key: Option<&str>,
444    ) -> Result<DeadManGuard, SdkError>
445    where
446        S: SessionSigner + ?Sized,
447        V: OrderVerifier + ?Sized,
448    {
449        let timeout_ms = checked_dead_man_timeout(timeout)?;
450        let (state, _) = self
451            .arm_dead_man_once(timeout_ms, signer, verifier, idempotency_key)
452            .await?;
453        let stream = self.clone();
454        let task = tokio::spawn(async move {
455            let mut interval =
456                tokio::time::interval(Duration::from_millis((timeout_ms / 3).max(250)));
457            interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
458            interval.tick().await;
459            loop {
460                interval.tick().await;
461                match stream.heartbeat_dead_man().await {
462                    Ok(state) if state.status == PlatformDeadManStatus::Armed => {}
463                    _ => break,
464                }
465            }
466        });
467        Ok(DeadManGuard {
468            initial_state: state,
469            stream: self.clone(),
470            task,
471            active: true,
472        })
473    }
474
475    /// Maintain a dead-man indefinitely by heartbeating the current ticket and
476    /// externally signing a fresh exact cancel-all before its blockhash expires.
477    /// The caller supplies owner-controlled signer/verifier adapters in `Arc`s
478    /// solely because the maintenance task must outlive this method call.
479    pub async fn maintain_dead_man<S, V>(
480        &self,
481        timeout: Duration,
482        signer: Arc<S>,
483        verifier: Arc<V>,
484    ) -> Result<DeadManGuard, SdkError>
485    where
486        S: SessionSigner + 'static,
487        V: OrderVerifier + 'static,
488    {
489        let timeout_ms = checked_dead_man_timeout(timeout)?;
490        let (state, mut transaction_expires_at_ms) = self
491            .arm_dead_man_once(timeout_ms, signer.as_ref(), verifier.as_ref(), None)
492            .await?;
493        let stream = self.clone();
494        let maintained_stream = stream.clone();
495        let task = tokio::spawn(async move {
496            let mut interval =
497                tokio::time::interval(Duration::from_millis((timeout_ms / 3).max(250)));
498            interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
499            interval.tick().await;
500            let refresh_lead_ms = timeout_ms.saturating_mul(2).max(5_000);
501            loop {
502                interval.tick().await;
503                let now = unix_ms().unwrap_or(u64::MAX);
504                if now.saturating_add(refresh_lead_ms) >= transaction_expires_at_ms {
505                    match maintained_stream
506                        .arm_dead_man_once(timeout_ms, signer.as_ref(), verifier.as_ref(), None)
507                        .await
508                    {
509                        Ok((next, expires)) if next.status == PlatformDeadManStatus::Armed => {
510                            transaction_expires_at_ms = expires;
511                        }
512                        _ => break,
513                    }
514                } else {
515                    match maintained_stream.heartbeat_dead_man().await {
516                        Ok(next) if next.status == PlatformDeadManStatus::Armed => {}
517                        _ => break,
518                    }
519                }
520            }
521        });
522        Ok(DeadManGuard {
523            initial_state: state,
524            stream,
525            task,
526            active: true,
527        })
528    }
529
530    pub async fn heartbeat_dead_man(&self) -> Result<PlatformDeadManState, SdkError> {
531        self.dead_man_command(PlatformOrderCommand::DeadManHeartbeat)
532            .await
533    }
534
535    pub async fn dead_man_status(&self) -> Result<PlatformDeadManState, SdkError> {
536        self.dead_man_command(PlatformOrderCommand::DeadManStatus)
537            .await
538    }
539
540    pub async fn disarm_dead_man(&self) -> Result<PlatformDeadManState, SdkError> {
541        self.dead_man_command(PlatformOrderCommand::DeadManDisarm)
542            .await
543    }
544
545    async fn dead_man_command(
546        &self,
547        command: PlatformOrderCommand,
548    ) -> Result<PlatformDeadManState, SdkError> {
549        match self.command(command).await? {
550            PlatformOrderCommandEvent::DeadManResult { state, .. } => Ok(state),
551            _ => Err(SdkError::InvalidResponse(
552                "expected a dead-man result".to_owned(),
553            )),
554        }
555    }
556
557    async fn arm_dead_man_once<S, V>(
558        &self,
559        timeout_ms: u64,
560        signer: &S,
561        verifier: &V,
562        idempotency_key: Option<&str>,
563    ) -> Result<(PlatformDeadManState, u64), SdkError>
564    where
565        S: SessionSigner + ?Sized,
566        V: OrderVerifier + ?Sized,
567    {
568        self.ensure_signer(signer)?;
569        let challenged = self
570            .challenge(
571                PlatformOrderChallengeRequest::CancelAll {
572                    owner_wallet: self.owner_wallet.clone(),
573                    session_public_key: self.session_public_key.clone(),
574                },
575                PlatformSelfTradePrevention::CancelTaker,
576            )
577            .await?;
578        let (prepared, signed_transaction_base64) = self
579            .authorize_and_prepare(&challenged, signer, verifier)
580            .await?;
581        let transaction_expires_at_ms = prepared.expires_at_ms;
582        let request = PlatformOrderSubmitRequest {
583            order_control_id: prepared.order_control_id.clone(),
584            signed_transaction_base64,
585            idempotency_key: normalize_idempotency_key(
586                idempotency_key.unwrap_or(&prepared.order_control_id),
587            )?,
588        };
589        let state = match self
590            .command(PlatformOrderCommand::DeadManArm {
591                timeout_ms,
592                request,
593            })
594            .await?
595        {
596            PlatformOrderCommandEvent::DeadManResult { state, .. } => state,
597            _ => {
598                return Err(SdkError::InvalidResponse(
599                    "expected a dead-man result".to_owned(),
600                ))
601            }
602        };
603        if state.status != PlatformDeadManStatus::Armed {
604            return Err(SdkError::InvalidResponse(
605                "dead-man ticket was not armed".to_owned(),
606            ));
607        }
608        Ok((state, transaction_expires_at_ms))
609    }
610
611    /// One signature: this socket already authenticated the session and the
612    /// challenge is bound to it, so no message signature is needed — the
613    /// challenge's authorization payload is still parsed to bind the prepared
614    /// blockhash and order set, then the session signs only the transaction,
615    /// after it has been verified.
616    async fn authorize_and_prepare<S, V>(
617        &self,
618        challenged: &OrderChallengeResult,
619        signer: &S,
620        verifier: &V,
621    ) -> Result<(PlatformOrderPrepareResponse, String), SdkError>
622    where
623        S: SessionSigner + ?Sized,
624        V: OrderVerifier + ?Sized,
625    {
626        let authorization =
627            validate_order_authorization(&challenged.response, &challenged.effective_request)?;
628        let prepared = self
629            .prepare(PlatformOrderPrepareRequest::Authorized(
630                PlatformOrderPrepareAuthorization {
631                    challenge_id: challenged.response.challenge_id.clone(),
632                    authorization_signature: None,
633                },
634            ))
635            .await?;
636        validate_order_prepare_binding(&prepared, &challenged.response, &authorization)?;
637        verifier
638            .verify(&OrderVerificationContext {
639                challenge: Some(&challenged.response),
640                operation: &challenged.effective_request,
641                market_id: &self.market_id,
642                prepared: &prepared,
643                owner_wallet: &self.owner_wallet,
644                session_public_key: &self.session_public_key,
645            })
646            .await
647            .map_err(SdkError::Verification)?;
648        let transaction = signer
649            .sign_transaction(&prepared.transaction_base64)
650            .await
651            .map_err(SdkError::Signer)?;
652        let transaction = canonical_base64(&transaction, "signed_transaction_base64")?;
653        verify_signed_transaction_message(&prepared.transaction_base64, &transaction)
654            .map_err(SdkError::Verification)?;
655        Ok((prepared, transaction))
656    }
657
658    fn ensure_request_identity(
659        &self,
660        request: &PlatformOrderChallengeRequest,
661    ) -> Result<(), SdkError> {
662        if order_request_owner(request) != self.owner_wallet
663            || order_request_session(request) != self.session_public_key
664        {
665            return Err(SdkError::InvalidRequest(
666                "order command identity does not match the authenticated socket".to_owned(),
667            ));
668        }
669        Ok(())
670    }
671
672    fn ensure_signer<S: SessionSigner + ?Sized>(&self, signer: &S) -> Result<(), SdkError> {
673        if canonical_public_key(signer.public_key(), "session_public_key")?
674            != self.session_public_key
675        {
676            return Err(SdkError::InvalidRequest(
677                "signer does not match the authenticated order command session".to_owned(),
678            ));
679        }
680        Ok(())
681    }
682}
683
684/// Keeps the durable dead-man deadline alive. Drop is deliberately fail-closed:
685/// it stops heartbeats and leaves the pre-signed cancel-all armed.
686pub struct DeadManGuard {
687    pub initial_state: PlatformDeadManState,
688    stream: OrderCommandStream,
689    task: JoinHandle<()>,
690    active: bool,
691}
692
693impl DeadManGuard {
694    pub async fn disarm(&mut self) -> Result<PlatformDeadManState, SdkError> {
695        self.task.abort();
696        let state = self.stream.disarm_dead_man().await?;
697        self.active = false;
698        Ok(state)
699    }
700}
701
702impl Drop for DeadManGuard {
703    fn drop(&mut self) {
704        self.task.abort();
705        if self.active {
706            // Intentionally no disarm. A crashed/dropped agent must fail closed.
707        }
708    }
709}
710
711pub fn order_stream_auth_message(
712    market_id: &str,
713    owner_wallet: &str,
714    session_public_key: &str,
715    challenge: &str,
716) -> Vec<u8> {
717    format!(
718        "{ORDER_STREAM_AUTH_DOMAIN}\n{market_id}\n{owner_wallet}\n{session_public_key}\n{challenge}"
719    )
720    .into_bytes()
721}
722
723async fn run_actor<S>(
724    mut socket: tokio_tungstenite::WebSocketStream<S>,
725    mut commands: mpsc::Receiver<ActorRequest>,
726    events: broadcast::Sender<PlatformOrderCommandEvent>,
727    market_id: String,
728    stream_id: String,
729    mut server_sequence: u64,
730) where
731    S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
732{
733    let mut client_sequence = 0u64;
734    let mut request_counter = 0u64;
735    let mut pending =
736        HashMap::<String, oneshot::Sender<Result<PlatformOrderCommandEvent, SdkError>>>::new();
737    let failure = 'actor: loop {
738        tokio::select! {
739            request = commands.recv() => {
740                let Some(request) = request else {
741                    let _ = socket.close(None).await;
742                    break "order command handle closed".to_owned();
743                };
744                let mut requests = Vec::with_capacity(MAX_CLIENT_COMMANDS_PER_FRAME);
745                requests.push(request);
746                // Give concurrently queued callers one scheduler turn to join
747                // this transport batch. Each command keeps its own sequence,
748                // request ID and response channel.
749                tokio::task::yield_now().await;
750                while requests.len() < MAX_CLIENT_COMMANDS_PER_FRAME {
751                    match commands.try_recv() {
752                        Ok(request) => requests.push(request),
753                        Err(mpsc::error::TryRecvError::Empty | mpsc::error::TryRecvError::Disconnected) => break,
754                    }
755                }
756                let mut frames = Vec::with_capacity(requests.len());
757                let mut responses = Vec::with_capacity(requests.len());
758                for request in requests {
759                    client_sequence = client_sequence.saturating_add(1);
760                    request_counter = request_counter.saturating_add(1);
761                    let request_id = format!("rust-{request_counter:x}");
762                    frames.push(PlatformOrderCommandClientFrame::Command {
763                        request_id: request_id.clone(),
764                        sequence: client_sequence.to_string(),
765                        command: request.command,
766                    });
767                    responses.push((request_id, request.response));
768                }
769                let message = match encode_order_command_frames(&frames) {
770                    Ok(message) => message,
771                    Err(error) => {
772                        let text = error.to_string();
773                        for (_, response) in responses {
774                            let _ = response.send(Err(SdkError::InvalidRequest(text.clone())));
775                        }
776                        break text;
777                    }
778                };
779                if let Err(error) = socket.send(Message::Text(message.into())).await {
780                    let text = error.to_string();
781                    for (_, response) in responses {
782                        let _ = response.send(Err(SdkError::Stream(text.clone())));
783                    }
784                    break text;
785                }
786                for (request_id, response) in responses {
787                    pending.insert(request_id, response);
788                }
789            }
790            incoming = socket.next() => {
791                let Some(incoming) = incoming else {
792                    break "order command socket closed".to_owned();
793                };
794                let message = match incoming {
795                    Ok(Message::Text(message)) => message,
796                    Ok(Message::Ping(payload)) => {
797                        if let Err(error) = socket.send(Message::Pong(payload)).await {
798                            break error.to_string();
799                        }
800                        continue;
801                    }
802                    Ok(Message::Pong(_)) => continue,
803                    Ok(Message::Close(_)) => break "order command socket closed".to_owned(),
804                    Ok(_) => continue,
805                    Err(error) => break error.to_string(),
806                };
807                let frame_events = match parse_order_command_events(&message) {
808                    Ok(events) => events,
809                    Err(error) => break error.to_string(),
810                };
811                for event in frame_events {
812                    if let Err(error) = validate_event_sequence(
813                        &event,
814                        &market_id,
815                        &stream_id,
816                        &mut server_sequence,
817                    ) {
818                        break 'actor error.to_string();
819                    }
820                    let request_id = event_request_id(&event).map(str::to_owned);
821                    let _ = events.send(event.clone());
822                    if let Some(request_id) = request_id {
823                        if let Some(response) = pending.remove(&request_id) {
824                            let result = match &event {
825                                PlatformOrderCommandEvent::CommandError { error, .. } => {
826                                    Err(SdkError::Command {
827                                        code: serde_json::to_value(error.code)
828                                            .ok()
829                                            .and_then(|value| value.as_str().map(str::to_owned))
830                                            .unwrap_or_else(|| "command_rejected".to_owned()),
831                                        message: error.message.clone(),
832                                        retryable: error.retryable,
833                                    })
834                                }
835                                _ => Ok(event),
836                            };
837                            let _ = response.send(result);
838                        }
839                    }
840                }
841            }
842        }
843    };
844    for response in pending.into_values() {
845        let _ = response.send(Err(SdkError::Stream(failure.clone())));
846    }
847}
848
849fn encode_order_command_frames(
850    frames: &[PlatformOrderCommandClientFrame],
851) -> Result<String, serde_json::Error> {
852    if frames.len() == 1 {
853        serde_json::to_string(&frames[0])
854    } else {
855        serde_json::to_string(frames)
856    }
857}
858
859fn parse_order_command_events(message: &str) -> Result<Vec<PlatformOrderCommandEvent>, SdkError> {
860    let value: serde_json::Value = serde_json::from_str(message).map_err(|error| {
861        SdkError::InvalidResponse(format!("invalid order command frame: {error}"))
862    })?;
863    let events = if value.is_array() {
864        serde_json::from_value::<Vec<PlatformOrderCommandEvent>>(value)
865    } else if value.get("type").and_then(serde_json::Value::as_str) == Some("event_batch") {
866        return serde_json::from_value::<PlatformOrderCommandServerFrame>(value)
867            .map_err(|error| {
868                SdkError::InvalidResponse(format!("invalid order command event batch: {error}"))
869            })
870            .and_then(expand_order_command_event_batch);
871    } else {
872        serde_json::from_value::<PlatformOrderCommandEvent>(value).map(|event| vec![event])
873    }
874    .map_err(|error| SdkError::InvalidResponse(format!("invalid order command event: {error}")))?;
875    if events.is_empty() || events.len() > MAX_SERVER_EVENTS_PER_FRAME {
876        return Err(SdkError::InvalidResponse(
877            "order command event batch is invalid".to_owned(),
878        ));
879    }
880    Ok(events)
881}
882
883fn expand_order_command_event_batch(
884    frame: PlatformOrderCommandServerFrame,
885) -> Result<Vec<PlatformOrderCommandEvent>, SdkError> {
886    let PlatformOrderCommandServerFrame::EventBatch {
887        schema_version,
888        contract_version,
889        market_id,
890        stream_id,
891        first_sequence,
892        previous_sequence,
893        server_time_ms,
894        events,
895    } = frame;
896    if events.is_empty() || events.len() > MAX_SERVER_EVENTS_PER_FRAME {
897        return Err(SdkError::InvalidResponse(
898            "order command event batch is invalid".to_owned(),
899        ));
900    }
901    let first = parse_wire_sequence(&first_sequence)?;
902    let previous = parse_wire_sequence(&previous_sequence)?;
903    if first
904        != previous.checked_add(1).ok_or_else(|| {
905            SdkError::InvalidResponse("order command event batch sequence overflowed".to_owned())
906        })?
907    {
908        return Err(SdkError::InvalidResponse(
909            "order command event batch sequence is invalid".to_owned(),
910        ));
911    }
912    events
913        .into_iter()
914        .enumerate()
915        .map(|(index, event)| {
916            let sequence = first
917                .checked_add(u64::try_from(index).map_err(|_| {
918                    SdkError::InvalidResponse("order command event batch is too large".to_owned())
919                })?)
920                .ok_or_else(|| {
921                    SdkError::InvalidResponse(
922                        "order command event batch sequence overflowed".to_owned(),
923                    )
924                })?;
925            let previous_sequence = sequence.saturating_sub(1).to_string();
926            let sequence = sequence.to_string();
927            let common = || {
928                (
929                    schema_version,
930                    contract_version.clone(),
931                    market_id.clone(),
932                    stream_id.clone(),
933                    sequence.clone(),
934                    previous_sequence.clone(),
935                    server_time_ms,
936                )
937            };
938            Ok(match event {
939                PlatformOrderCommandBatchEvent::ProbeResult { request_id, nonce } => {
940                    let (
941                        schema_version,
942                        contract_version,
943                        market_id,
944                        stream_id,
945                        sequence,
946                        previous_sequence,
947                        server_time_ms,
948                    ) = common();
949                    PlatformOrderCommandEvent::ProbeResult {
950                        schema_version,
951                        contract_version,
952                        market_id,
953                        stream_id,
954                        sequence,
955                        previous_sequence,
956                        request_id,
957                        nonce,
958                        server_time_ms,
959                    }
960                }
961                PlatformOrderCommandBatchEvent::ChallengeResult {
962                    request_id,
963                    self_trade_prevention,
964                    prevented_order_ids,
965                    effective_request,
966                    response,
967                } => {
968                    let (
969                        schema_version,
970                        contract_version,
971                        market_id,
972                        stream_id,
973                        sequence,
974                        previous_sequence,
975                        server_time_ms,
976                    ) = common();
977                    PlatformOrderCommandEvent::ChallengeResult {
978                        schema_version,
979                        contract_version,
980                        market_id,
981                        stream_id,
982                        sequence,
983                        previous_sequence,
984                        request_id,
985                        self_trade_prevention,
986                        prevented_order_ids,
987                        effective_request,
988                        response,
989                        server_time_ms,
990                    }
991                }
992                PlatformOrderCommandBatchEvent::PrepareResult {
993                    request_id,
994                    response,
995                } => {
996                    let (
997                        schema_version,
998                        contract_version,
999                        market_id,
1000                        stream_id,
1001                        sequence,
1002                        previous_sequence,
1003                        server_time_ms,
1004                    ) = common();
1005                    PlatformOrderCommandEvent::PrepareResult {
1006                        schema_version,
1007                        contract_version,
1008                        market_id,
1009                        stream_id,
1010                        sequence,
1011                        previous_sequence,
1012                        request_id,
1013                        response,
1014                        server_time_ms,
1015                    }
1016                }
1017                PlatformOrderCommandBatchEvent::SubmitResult {
1018                    request_id,
1019                    response,
1020                } => {
1021                    let (
1022                        schema_version,
1023                        contract_version,
1024                        market_id,
1025                        stream_id,
1026                        sequence,
1027                        previous_sequence,
1028                        server_time_ms,
1029                    ) = common();
1030                    PlatformOrderCommandEvent::SubmitResult {
1031                        schema_version,
1032                        contract_version,
1033                        market_id,
1034                        stream_id,
1035                        sequence,
1036                        previous_sequence,
1037                        request_id,
1038                        response,
1039                        server_time_ms,
1040                    }
1041                }
1042                PlatformOrderCommandBatchEvent::StatusResult {
1043                    request_id,
1044                    response,
1045                } => {
1046                    let (
1047                        schema_version,
1048                        contract_version,
1049                        market_id,
1050                        stream_id,
1051                        sequence,
1052                        previous_sequence,
1053                        server_time_ms,
1054                    ) = common();
1055                    PlatformOrderCommandEvent::StatusResult {
1056                        schema_version,
1057                        contract_version,
1058                        market_id,
1059                        stream_id,
1060                        sequence,
1061                        previous_sequence,
1062                        request_id,
1063                        response,
1064                        server_time_ms,
1065                    }
1066                }
1067                PlatformOrderCommandBatchEvent::DeadManResult { request_id, state } => {
1068                    let (
1069                        schema_version,
1070                        contract_version,
1071                        market_id,
1072                        stream_id,
1073                        sequence,
1074                        previous_sequence,
1075                        server_time_ms,
1076                    ) = common();
1077                    PlatformOrderCommandEvent::DeadManResult {
1078                        schema_version,
1079                        contract_version,
1080                        market_id,
1081                        stream_id,
1082                        sequence,
1083                        previous_sequence,
1084                        request_id,
1085                        state,
1086                        server_time_ms,
1087                    }
1088                }
1089                PlatformOrderCommandBatchEvent::CommandError { request_id, error } => {
1090                    let (
1091                        schema_version,
1092                        contract_version,
1093                        market_id,
1094                        stream_id,
1095                        sequence,
1096                        previous_sequence,
1097                        server_time_ms,
1098                    ) = common();
1099                    PlatformOrderCommandEvent::CommandError {
1100                        schema_version,
1101                        contract_version,
1102                        market_id,
1103                        stream_id,
1104                        sequence,
1105                        previous_sequence,
1106                        request_id,
1107                        error,
1108                        server_time_ms,
1109                    }
1110                }
1111                PlatformOrderCommandBatchEvent::Heartbeat => {
1112                    let (
1113                        schema_version,
1114                        contract_version,
1115                        market_id,
1116                        stream_id,
1117                        sequence,
1118                        previous_sequence,
1119                        server_time_ms,
1120                    ) = common();
1121                    PlatformOrderCommandEvent::Heartbeat {
1122                        schema_version,
1123                        contract_version,
1124                        market_id,
1125                        stream_id,
1126                        sequence,
1127                        previous_sequence,
1128                        server_time_ms,
1129                    }
1130                }
1131            })
1132        })
1133        .collect()
1134}
1135
1136fn validate_event_sequence(
1137    event: &PlatformOrderCommandEvent,
1138    market_id: &str,
1139    stream_id: &str,
1140    server_sequence: &mut u64,
1141) -> Result<(), SdkError> {
1142    let Some((schema, contract, market, stream, sequence, previous)) = event_sequence(event) else {
1143        return Err(SdkError::InvalidResponse(
1144            "order stream restarted without signed authentication".to_owned(),
1145        ));
1146    };
1147    validate_platform_version(schema, contract)?;
1148    let sequence = parse_wire_sequence(sequence)?;
1149    let previous = parse_wire_sequence(previous)?;
1150    if market != market_id
1151        || stream != stream_id
1152        || previous != *server_sequence
1153        || sequence != server_sequence.saturating_add(1)
1154    {
1155        return Err(SdkError::InvalidResponse(
1156            "order command event sequence is not contiguous".to_owned(),
1157        ));
1158    }
1159    *server_sequence = sequence;
1160    Ok(())
1161}
1162
1163fn event_sequence(
1164    event: &PlatformOrderCommandEvent,
1165) -> Option<(u16, &str, &str, &str, &str, &str)> {
1166    match event {
1167        PlatformOrderCommandEvent::ProbeResult {
1168            schema_version,
1169            contract_version,
1170            market_id,
1171            stream_id,
1172            sequence,
1173            previous_sequence,
1174            ..
1175        }
1176        | PlatformOrderCommandEvent::ChallengeResult {
1177            schema_version,
1178            contract_version,
1179            market_id,
1180            stream_id,
1181            sequence,
1182            previous_sequence,
1183            ..
1184        }
1185        | PlatformOrderCommandEvent::PrepareResult {
1186            schema_version,
1187            contract_version,
1188            market_id,
1189            stream_id,
1190            sequence,
1191            previous_sequence,
1192            ..
1193        }
1194        | PlatformOrderCommandEvent::SubmitResult {
1195            schema_version,
1196            contract_version,
1197            market_id,
1198            stream_id,
1199            sequence,
1200            previous_sequence,
1201            ..
1202        }
1203        | PlatformOrderCommandEvent::StatusResult {
1204            schema_version,
1205            contract_version,
1206            market_id,
1207            stream_id,
1208            sequence,
1209            previous_sequence,
1210            ..
1211        }
1212        | PlatformOrderCommandEvent::DeadManResult {
1213            schema_version,
1214            contract_version,
1215            market_id,
1216            stream_id,
1217            sequence,
1218            previous_sequence,
1219            ..
1220        }
1221        | PlatformOrderCommandEvent::CommandError {
1222            schema_version,
1223            contract_version,
1224            market_id,
1225            stream_id,
1226            sequence,
1227            previous_sequence,
1228            ..
1229        }
1230        | PlatformOrderCommandEvent::Heartbeat {
1231            schema_version,
1232            contract_version,
1233            market_id,
1234            stream_id,
1235            sequence,
1236            previous_sequence,
1237            ..
1238        } => Some((
1239            *schema_version,
1240            contract_version,
1241            market_id,
1242            stream_id,
1243            sequence,
1244            previous_sequence,
1245        )),
1246        PlatformOrderCommandEvent::AuthChallenge { .. }
1247        | PlatformOrderCommandEvent::Ready { .. } => None,
1248    }
1249}
1250
1251fn event_request_id(event: &PlatformOrderCommandEvent) -> Option<&str> {
1252    match event {
1253        PlatformOrderCommandEvent::ProbeResult { request_id, .. }
1254        | PlatformOrderCommandEvent::ChallengeResult { request_id, .. }
1255        | PlatformOrderCommandEvent::PrepareResult { request_id, .. }
1256        | PlatformOrderCommandEvent::SubmitResult { request_id, .. }
1257        | PlatformOrderCommandEvent::StatusResult { request_id, .. }
1258        | PlatformOrderCommandEvent::DeadManResult { request_id, .. }
1259        | PlatformOrderCommandEvent::CommandError { request_id, .. } => Some(request_id),
1260        _ => None,
1261    }
1262}
1263
1264fn parse_wire_sequence(value: &str) -> Result<u64, SdkError> {
1265    if value.is_empty()
1266        || !value.bytes().all(|byte| byte.is_ascii_digit())
1267        || (value.len() > 1 && value.starts_with('0'))
1268    {
1269        return Err(SdkError::InvalidResponse(
1270            "order command sequence is not canonical".to_owned(),
1271        ));
1272    }
1273    value
1274        .parse()
1275        .map_err(|_| SdkError::InvalidResponse("order command sequence exceeds u64".to_owned()))
1276}
1277
1278fn checked_dead_man_timeout(timeout: Duration) -> Result<u64, SdkError> {
1279    let timeout_ms = u64::try_from(timeout.as_millis()).unwrap_or(u64::MAX);
1280    if !(1_000..=30_000).contains(&timeout_ms) {
1281        return Err(SdkError::InvalidRequest(
1282            "dead-man timeout must be between one and thirty seconds".to_owned(),
1283        ));
1284    }
1285    Ok(timeout_ms)
1286}
1287
1288fn validate_challenge_result(
1289    market_id: &str,
1290    request: &PlatformOrderChallengeRequest,
1291    response: &PlatformOrderChallengeResponse,
1292) -> Result<(), SdkError> {
1293    validate_platform_version(response.schema_version, &response.contract_version)?;
1294    if response.market_id != market_id
1295        || response.action != order_request_action(request)
1296        || !valid_handle(&response.challenge_id, "oc_")
1297        || response.order_ids.is_empty()
1298        || response.order_ids.len() > 12
1299        || response.expires_at_ms <= response.server_time_ms
1300        || response
1301            .order_ids
1302            .iter()
1303            .any(|id| !valid_handle(id, "order_"))
1304    {
1305        return Err(SdkError::InvalidResponse(
1306            "order challenge bindings are invalid".to_owned(),
1307        ));
1308    }
1309    canonical_base64(
1310        &response.authorization_payload_base64,
1311        "authorization_payload_base64",
1312    )?;
1313    Ok(())
1314}
1315
1316fn validate_prepared(
1317    market_id: &str,
1318    response: &PlatformOrderPrepareResponse,
1319) -> Result<(), SdkError> {
1320    validate_platform_version(response.schema_version, &response.contract_version)?;
1321    if response.market_id != market_id
1322        || !valid_handle(&response.order_control_id, "or_")
1323        || response.order_ids.is_empty()
1324        || response.order_ids.len() > 12
1325        || response.expires_at_ms == 0
1326    {
1327        return Err(SdkError::InvalidResponse(
1328            "prepared order control is invalid".to_owned(),
1329        ));
1330    }
1331    canonical_base64(&response.transaction_base64, "transaction_base64")?;
1332    canonical_base58_32(&response.recent_blockhash, "recent_blockhash")?;
1333    Ok(())
1334}
1335
1336fn normalize_submit_request(
1337    request: PlatformOrderSubmitRequest,
1338) -> Result<PlatformOrderSubmitRequest, SdkError> {
1339    if !valid_handle(&request.order_control_id, "or_") {
1340        return Err(SdkError::InvalidRequest(
1341            "order_control_id is invalid".to_owned(),
1342        ));
1343    }
1344    Ok(PlatformOrderSubmitRequest {
1345        order_control_id: request.order_control_id,
1346        signed_transaction_base64: canonical_base64(
1347            &request.signed_transaction_base64,
1348            "signed_transaction_base64",
1349        )?,
1350        idempotency_key: normalize_idempotency_key(&request.idempotency_key)?,
1351    })
1352}
1353
1354fn normalize_status_request(
1355    request: PlatformOrderStatusRequest,
1356) -> Result<PlatformOrderStatusRequest, SdkError> {
1357    if !valid_handle(&request.order_control_id, "or_") {
1358        return Err(SdkError::InvalidRequest(
1359            "order_control_id is invalid".to_owned(),
1360        ));
1361    }
1362    Ok(PlatformOrderStatusRequest {
1363        order_control_id: request.order_control_id,
1364        idempotency_key: normalize_idempotency_key(&request.idempotency_key)?,
1365    })
1366}
1367
1368fn validate_submit(
1369    market_id: &str,
1370    control_id: &str,
1371    response: &PlatformOrderSubmitResponse,
1372) -> Result<(), SdkError> {
1373    validate_platform_version(response.schema_version, &response.contract_version)?;
1374    if response.market_id != market_id
1375        || response.order_control_id != control_id
1376        || response.status != PlatformOrderSubmissionStatus::Submitted
1377    {
1378        return Err(SdkError::InvalidResponse(
1379            "order submit bindings are invalid".to_owned(),
1380        ));
1381    }
1382    canonical_signature(&response.signature, "signature")?;
1383    Ok(())
1384}
1385
1386fn validate_status(
1387    market_id: &str,
1388    control_id: &str,
1389    response: &PlatformOrderStatusResponse,
1390) -> Result<(), SdkError> {
1391    validate_platform_version(response.schema_version, &response.contract_version)?;
1392    if response.market_id != market_id
1393        || response.order_control_id != control_id
1394        || response.order_ids.is_empty()
1395        || response.order_ids.len() > 12
1396        || response
1397            .order_ids
1398            .iter()
1399            .any(|id| !valid_handle(id, "order_"))
1400        || (response.status == PlatformOrderControlStatus::Failed)
1401            != response
1402                .failure_code
1403                .as_deref()
1404                .is_some_and(|code| !code.is_empty())
1405    {
1406        return Err(SdkError::InvalidResponse(
1407            "order status bindings are invalid".to_owned(),
1408        ));
1409    }
1410    canonical_signature(&response.signature, "signature")?;
1411    Ok(())
1412}
1413
1414#[cfg(test)]
1415mod tests {
1416    use super::*;
1417
1418    #[test]
1419    fn authentication_domain_binds_every_identity() {
1420        let message = String::from_utf8(order_stream_auth_message(
1421            "market_11111111111111111111111111111111",
1422            "owner",
1423            "session",
1424            &"ab".repeat(32),
1425        ))
1426        .unwrap();
1427        assert_eq!(
1428            message,
1429            format!(
1430                "{ORDER_STREAM_AUTH_DOMAIN}\nmarket_11111111111111111111111111111111\nowner\nsession\n{}",
1431                "ab".repeat(32)
1432            )
1433        );
1434    }
1435
1436    #[test]
1437    fn canonical_sequence_rejects_gaps_and_leading_zeroes() {
1438        assert_eq!(parse_wire_sequence("8").unwrap(), 8);
1439        assert!(parse_wire_sequence("08").is_err());
1440        assert!(parse_wire_sequence("-1").is_err());
1441    }
1442
1443    #[test]
1444    fn parses_ordered_event_batches_and_single_frame_fallback() {
1445        let event = serde_json::json!({
1446            "type": "heartbeat",
1447            "schema_version": 2,
1448            "contract_version": "2.0",
1449            "market_id": "market_11111111111111111111111111111111",
1450            "stream_id": "order_command_stream_11111111111111111111111111111111",
1451            "sequence": "2",
1452            "previous_sequence": "1",
1453            "server_time_ms": 1_786_810_000_000u64
1454        });
1455        let single = parse_order_command_events(&event.to_string()).unwrap();
1456        assert_eq!(single.len(), 1);
1457        let batch = parse_order_command_events(
1458            &serde_json::Value::Array(vec![event.clone(), event]).to_string(),
1459        )
1460        .unwrap();
1461        assert_eq!(batch.len(), 2);
1462        let compact = serde_json::json!({
1463            "type": "event_batch",
1464            "schema_version": 2,
1465            "contract_version": "2.0",
1466            "market_id": "market_11111111111111111111111111111111",
1467            "stream_id": "order_command_stream_11111111111111111111111111111111",
1468            "first_sequence": "2",
1469            "previous_sequence": "1",
1470            "server_time_ms": 1_786_810_000_000u64,
1471            "events": [
1472                {
1473                    "type": "probe_result",
1474                    "request_id": "probe-1",
1475                    "nonce": "health-1"
1476                },
1477                {"type": "heartbeat"}
1478            ]
1479        });
1480        let compact = parse_order_command_events(&compact.to_string()).unwrap();
1481        assert_eq!(compact.len(), 2);
1482        assert!(matches!(
1483            &compact[0],
1484            PlatformOrderCommandEvent::ProbeResult {
1485                sequence,
1486                previous_sequence,
1487                request_id,
1488                nonce,
1489                ..
1490            } if sequence == "2"
1491                && previous_sequence == "1"
1492                && request_id == "probe-1"
1493                && nonce == "health-1"
1494        ));
1495        assert!(matches!(
1496            &compact[1],
1497            PlatformOrderCommandEvent::Heartbeat { sequence, previous_sequence, .. }
1498                if sequence == "3" && previous_sequence == "2"
1499        ));
1500        assert!(parse_order_command_events("[]").is_err());
1501    }
1502
1503    #[test]
1504    fn encodes_single_commands_compatibly_and_concurrent_commands_as_one_batch() {
1505        let frame = |request_id: &str, sequence: &str| PlatformOrderCommandClientFrame::Command {
1506            request_id: request_id.to_owned(),
1507            sequence: sequence.to_owned(),
1508            command: PlatformOrderCommand::DeadManStatus,
1509        };
1510        let singleton = encode_order_command_frames(&[frame("one", "1")]).unwrap();
1511        assert!(serde_json::from_str::<serde_json::Value>(&singleton)
1512            .unwrap()
1513            .is_object());
1514        let batch = encode_order_command_frames(&[frame("two", "2"), frame("three", "3")]).unwrap();
1515        let decoded = serde_json::from_str::<Vec<PlatformOrderCommandClientFrame>>(&batch).unwrap();
1516        assert_eq!(decoded.len(), 2);
1517    }
1518}