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#[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 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 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 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 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 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 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
684pub 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 }
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 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}