Skip to main content

strata_public_contract/
platform.rs

1//! Public SDK 2.0 request and response primitives.
2
3use serde::{Deserialize, Serialize};
4
5use crate::{CapabilityRisk, McpExposure};
6
7pub const PLATFORM_SCHEMA_VERSION: u16 = 2;
8pub const PLATFORM_CONTRACT_VERSION: &str = "2.0";
9#[cfg(any(test, feature = "fixtures"))]
10#[doc(hidden)]
11pub const PLATFORM_CAPABILITIES_FIXTURE: &str =
12    include_str!("../fixtures/v2/platform-capabilities.json");
13#[cfg(any(test, feature = "fixtures"))]
14#[doc(hidden)]
15pub const PLATFORM_ASSETS_FIXTURE: &str = include_str!("../fixtures/v2/assets.json");
16#[cfg(any(test, feature = "fixtures"))]
17#[doc(hidden)]
18pub const PLATFORM_MARKETS_FIXTURE: &str = include_str!("../fixtures/v2/markets.json");
19#[cfg(any(test, feature = "fixtures"))]
20#[doc(hidden)]
21pub const PLATFORM_BOOK_FIXTURE: &str = include_str!("../fixtures/v2/book.json");
22#[cfg(any(test, feature = "fixtures"))]
23#[doc(hidden)]
24pub const PLATFORM_BBO_FIXTURE: &str = include_str!("../fixtures/v2/bbo.json");
25#[cfg(any(test, feature = "fixtures"))]
26#[doc(hidden)]
27pub const PLATFORM_FEES_FIXTURE: &str = include_str!("../fixtures/v2/fees.json");
28#[cfg(any(test, feature = "fixtures"))]
29#[doc(hidden)]
30pub const PLATFORM_STATUS_FIXTURE: &str = include_str!("../fixtures/v2/status.json");
31#[cfg(any(test, feature = "fixtures"))]
32#[doc(hidden)]
33pub const PLATFORM_TRADES_FIXTURE: &str = include_str!("../fixtures/v2/trades.json");
34#[cfg(any(test, feature = "fixtures"))]
35#[doc(hidden)]
36pub const PLATFORM_ACCOUNT_FIXTURE: &str = include_str!("../fixtures/v2/account.json");
37#[cfg(any(test, feature = "fixtures"))]
38#[doc(hidden)]
39pub const PLATFORM_ORDER_CHALLENGE_FIXTURE: &str =
40    include_str!("../fixtures/v2/order-challenge.json");
41#[cfg(any(test, feature = "fixtures"))]
42#[doc(hidden)]
43pub const PLATFORM_ORDER_PREPARE_FIXTURE: &str = include_str!("../fixtures/v2/order-prepare.json");
44#[cfg(any(test, feature = "fixtures"))]
45#[doc(hidden)]
46pub const PLATFORM_ORDER_SUBMIT_FIXTURE: &str = include_str!("../fixtures/v2/order-submit.json");
47#[cfg(any(test, feature = "fixtures"))]
48#[doc(hidden)]
49pub const PLATFORM_ORDER_STATUS_FIXTURE: &str = include_str!("../fixtures/v2/order-status.json");
50
51#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
52#[serde(rename_all = "snake_case")]
53pub enum PermissionSource {
54    ExternalAgentOwner,
55}
56
57#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
58#[serde(rename_all = "snake_case")]
59pub enum SigningLocation {
60    External,
61}
62
63#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
64#[serde(deny_unknown_fields)]
65pub struct PlatformAuthority {
66    pub permission_source: PermissionSource,
67    pub signing_location: SigningLocation,
68    pub accepts_private_keys: bool,
69}
70
71#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
72#[serde(rename_all = "snake_case")]
73pub enum PlatformTransport {
74    Http,
75    Websocket,
76    Mcp,
77}
78
79#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
80#[serde(rename_all = "snake_case")]
81pub enum PlatformMarketState {
82    Active,
83    ReadOnly,
84    QuoteOnly,
85    CancelOnly,
86    Paused,
87    Warming,
88    Degraded,
89    Unavailable,
90}
91
92#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
93#[serde(rename_all = "snake_case")]
94pub enum PlatformOrderState {
95    Created,
96    Accepted,
97    Open,
98    PartiallyFilled,
99    Filled,
100    CancelPending,
101    Cancelled,
102    Expired,
103    Rejected,
104}
105
106#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
107#[serde(rename_all = "snake_case")]
108pub enum PlatformSettlementState {
109    NotApplicable,
110    Pending,
111    Confirmed,
112    Failed,
113}
114
115#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
116#[serde(rename_all = "snake_case")]
117pub enum PlatformPublicErrorCode {
118    InvalidRequest,
119    UnsupportedCapability,
120    MarketUnavailable,
121    MarketWarming,
122    QuoteUnavailable,
123    QuoteExpired,
124    PriceBoundFailed,
125    InsufficientBalance,
126    PolicyRejected,
127    SessionExpired,
128    SequenceConflict,
129    DuplicateClientId,
130    OrderRejected,
131    OrderNotFound,
132    CancelTooLate,
133    RateLimited,
134    TemporarilyUnavailable,
135    SubmissionAmbiguous,
136    SettlementPending,
137    SettlementFailed,
138}
139
140/// Exact asset amount. Public money never crosses the contract as a float.
141#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
142#[serde(deny_unknown_fields)]
143pub struct ExactAmount {
144    pub asset_id: String,
145    pub atoms: String,
146}
147
148/// Sequence metadata shared by all recoverable state streams.
149#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
150#[serde(deny_unknown_fields)]
151pub struct SequenceEnvelope {
152    pub stream_id: String,
153    pub sequence: String,
154    pub previous_sequence: Option<String>,
155    pub server_time_ms: u64,
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub snapshot_id: Option<String>,
158}
159
160#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
161#[serde(deny_unknown_fields)]
162pub struct PageRequest {
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub cursor: Option<String>,
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub limit: Option<u32>,
167}
168
169#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
170#[serde(deny_unknown_fields)]
171pub struct PageInfo {
172    pub next_cursor: Option<String>,
173    pub has_more: bool,
174}
175
176#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
177#[serde(deny_unknown_fields)]
178pub struct PublicOperationError {
179    pub code: PlatformPublicErrorCode,
180    pub message: String,
181    pub retryable: bool,
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub retry_after_ms: Option<u64>,
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub operation_id: Option<String>,
186}
187
188/// One operation currently callable through the live v2 gateway.
189#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
190#[serde(deny_unknown_fields)]
191pub struct LivePlatformCapability {
192    pub id: String,
193    pub risk: CapabilityRisk,
194    pub required_scope: String,
195    pub transports: Vec<PlatformTransport>,
196    pub mcp_exposure: McpExposure,
197}
198
199/// Operations currently available to the client.
200#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
201#[serde(deny_unknown_fields)]
202pub struct PlatformDiscoveryResponse {
203    pub schema_version: u16,
204    pub contract_version: String,
205    pub server_time_ms: u64,
206    pub authority: PlatformAuthority,
207    pub capabilities: Vec<LivePlatformCapability>,
208}
209
210#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
211#[serde(rename_all = "snake_case")]
212pub enum PlatformNetwork {
213    Solana,
214}
215
216/// Asset identity used by ordinary SDK operations.
217#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
218#[serde(deny_unknown_fields)]
219pub struct PlatformAsset {
220    pub asset_id: String,
221    pub symbol: String,
222    pub name: String,
223    pub decimals: u8,
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub logo_url: Option<String>,
226    pub network: PlatformNetwork,
227}
228
229#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
230#[serde(deny_unknown_fields)]
231pub struct PlatformAssetsResponse {
232    pub schema_version: u16,
233    pub contract_version: String,
234    pub server_time_ms: u64,
235    pub assets: Vec<PlatformAsset>,
236    pub page: PageInfo,
237}
238
239#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
240#[serde(rename_all = "snake_case")]
241pub enum PlatformMarketAction {
242    Quote,
243    ExecuteImmediate,
244    PlaceOrder,
245    ScheduleTwap,
246}
247
248/// Stable market metadata for public SDK operations.
249#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
250#[serde(deny_unknown_fields)]
251pub struct PlatformMarket {
252    pub market_id: String,
253    pub label: String,
254    pub base_asset_id: String,
255    pub quote_asset_id: String,
256    pub status: PlatformMarketState,
257    pub available_actions: Vec<PlatformMarketAction>,
258}
259
260#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
261#[serde(deny_unknown_fields)]
262pub struct PlatformMarketsResponse {
263    pub schema_version: u16,
264    pub contract_version: String,
265    pub server_time_ms: u64,
266    pub markets: Vec<PlatformMarket>,
267    pub page: PageInfo,
268}
269
270#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
271#[serde(deny_unknown_fields)]
272pub struct PlatformBookLevel {
273    /// Quote atoms per whole base unit, encoded as an unsigned decimal string.
274    pub price_atoms: String,
275    /// Available base quantity in base atoms, encoded as an unsigned decimal string.
276    pub size_atoms: String,
277}
278
279#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
280#[serde(rename_all = "snake_case")]
281pub enum PlatformBookSide {
282    Bid,
283    Ask,
284}
285
286#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
287#[serde(deny_unknown_fields)]
288pub struct PlatformBookChange {
289    pub side: PlatformBookSide,
290    pub price_atoms: String,
291    /// Zero removes the price level.
292    pub size_atoms: String,
293}
294
295#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
296#[serde(deny_unknown_fields)]
297pub struct PlatformBookSnapshotResponse {
298    pub schema_version: u16,
299    pub contract_version: String,
300    pub market_id: String,
301    pub stream_id: String,
302    pub sequence: String,
303    pub server_time_ms: u64,
304    pub snapshot_id: String,
305    pub bids: Vec<PlatformBookLevel>,
306    pub asks: Vec<PlatformBookLevel>,
307}
308
309#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
310#[serde(deny_unknown_fields)]
311pub struct PlatformBestBidAskResponse {
312    pub schema_version: u16,
313    pub contract_version: String,
314    pub market_id: String,
315    pub stream_id: String,
316    pub sequence: String,
317    pub server_time_ms: u64,
318    pub best_bid: Option<PlatformBookLevel>,
319    pub best_ask: Option<PlatformBookLevel>,
320}
321
322#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
323#[serde(deny_unknown_fields)]
324pub struct PlatformFeeScheduleResponse {
325    pub schema_version: u16,
326    pub contract_version: String,
327    pub market_id: String,
328    pub server_time_ms: u64,
329    pub passive_maker_fee_bps: u16,
330    pub maximum_immediate_execution_fee_bps: u16,
331    pub book_prices_include_trading_fees: bool,
332    pub exact_fee_returned_by_quote: bool,
333}
334
335#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
336#[serde(deny_unknown_fields)]
337pub struct PlatformMarketStatusResponse {
338    pub schema_version: u16,
339    pub contract_version: String,
340    pub market_id: String,
341    pub server_time_ms: u64,
342    pub status: PlatformMarketState,
343    pub tick_size_atoms: String,
344    pub minimum_order_size_atoms: String,
345}
346
347#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
348#[serde(rename_all = "snake_case")]
349pub enum PlatformTradeSide {
350    Buy,
351    Sell,
352}
353
354#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
355#[serde(deny_unknown_fields)]
356pub struct PlatformTrade {
357    pub trade_id: String,
358    pub side: PlatformTradeSide,
359    pub price_atoms: String,
360    pub size_atoms: String,
361    pub executed_at_ms: u64,
362}
363
364#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
365#[serde(deny_unknown_fields)]
366pub struct PlatformTradesResponse {
367    pub schema_version: u16,
368    pub contract_version: String,
369    pub market_id: String,
370    pub server_time_ms: u64,
371    pub trades: Vec<PlatformTrade>,
372}
373
374#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
375#[serde(rename_all = "snake_case")]
376pub enum PlatformOrderType {
377    GoodUntilCancelled,
378    ImmediateOrCancel,
379    FillOrKill,
380    PostOnly,
381}
382
383/// Externally authorized resting-order operation. The public contract exposes
384/// product intent only; private construction details never cross the SDK
385/// boundary.
386#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
387#[serde(rename_all = "snake_case")]
388pub enum PlatformOrderAction {
389    Place,
390    Cancel,
391    CancelAll,
392    /// Atomically cancel one existing order and place its explicitly bound
393    /// successor in the same transaction.
394    Replace,
395    /// Atomically execute a bounded heterogeneous set of place, cancel, and
396    /// replace operations in one transaction.
397    Batch,
398}
399
400/// One operation inside an atomic order-control batch. Owner and session
401/// identity live on the enclosing challenge so no item can widen authority.
402#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
403#[serde(tag = "action", rename_all = "snake_case", deny_unknown_fields)]
404pub enum PlatformOrderBatchOperation {
405    Place {
406        account_sequence: String,
407        client_order_id: String,
408        side: PlatformTradeSide,
409        order_type: PlatformOrderType,
410        limit_price_atoms: String,
411        size_atoms: String,
412    },
413    Cancel {
414        order_id: String,
415    },
416    Replace {
417        order_id: String,
418        account_sequence: String,
419        client_order_id: String,
420        side: PlatformTradeSide,
421        order_type: PlatformOrderType,
422        limit_price_atoms: String,
423        size_atoms: String,
424    },
425}
426
427/// Request canonical bytes for one externally signed order-control operation.
428/// Variant-specific fields are sealed so an authorization cannot be widened
429/// between challenge and transaction preparation.
430#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
431#[serde(tag = "action", rename_all = "snake_case", deny_unknown_fields)]
432pub enum PlatformOrderChallengeRequest {
433    Place {
434        owner_wallet: String,
435        session_public_key: String,
436        account_sequence: String,
437        client_order_id: String,
438        side: PlatformTradeSide,
439        order_type: PlatformOrderType,
440        limit_price_atoms: String,
441        size_atoms: String,
442    },
443    Cancel {
444        owner_wallet: String,
445        session_public_key: String,
446        order_id: String,
447    },
448    CancelAll {
449        owner_wallet: String,
450        session_public_key: String,
451    },
452    Replace {
453        owner_wallet: String,
454        session_public_key: String,
455        order_id: String,
456        account_sequence: String,
457        client_order_id: String,
458        side: PlatformTradeSide,
459        order_type: PlatformOrderType,
460        limit_price_atoms: String,
461        size_atoms: String,
462    },
463    Batch {
464        owner_wallet: String,
465        session_public_key: String,
466        operations: Vec<PlatformOrderBatchOperation>,
467    },
468}
469
470#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
471#[serde(deny_unknown_fields)]
472pub struct PlatformOrderChallengeResponse {
473    pub schema_version: u16,
474    pub contract_version: String,
475    pub challenge_id: String,
476    pub market_id: String,
477    pub action: PlatformOrderAction,
478    /// Exact opaque order set bound by the authorization. Replace returns the
479    /// old then new ID. Batch flattens item IDs in request order, with replace
480    /// contributing old then new. A batch contains at most six operations.
481    pub order_ids: Vec<String>,
482    pub authorization_payload_base64: String,
483    pub server_time_ms: u64,
484    pub expires_at_ms: u64,
485}
486
487#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
488#[serde(deny_unknown_fields)]
489pub struct PlatformOrderPrepareRequest {
490    pub challenge_id: String,
491    /// Base58 Ed25519 signature over `authorization_payload_base64`.
492    pub authorization_signature: String,
493}
494
495#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
496#[serde(deny_unknown_fields)]
497pub struct PlatformOrderPrepareResponse {
498    pub schema_version: u16,
499    pub contract_version: String,
500    pub order_control_id: String,
501    pub market_id: String,
502    pub action: PlatformOrderAction,
503    pub order_ids: Vec<String>,
504    /// Backend-partially-signed Solana v0 transaction. The external session
505    /// signer verifies and fills only its signature slot.
506    pub transaction_base64: String,
507    pub recent_blockhash: String,
508    pub last_valid_block_height: u64,
509    pub expires_at_ms: u64,
510}
511
512#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
513#[serde(deny_unknown_fields)]
514pub struct PlatformOrderSubmitRequest {
515    pub order_control_id: String,
516    pub signed_transaction_base64: String,
517    pub idempotency_key: String,
518}
519
520#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
521#[serde(rename_all = "snake_case")]
522pub enum PlatformOrderSubmissionStatus {
523    Submitted,
524}
525
526#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
527#[serde(deny_unknown_fields)]
528pub struct PlatformOrderSubmitResponse {
529    pub schema_version: u16,
530    pub contract_version: String,
531    pub order_control_id: String,
532    pub market_id: String,
533    pub action: PlatformOrderAction,
534    pub order_ids: Vec<String>,
535    pub signature: String,
536    pub status: PlatformOrderSubmissionStatus,
537}
538
539#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
540#[serde(deny_unknown_fields)]
541pub struct PlatformOrderStatusRequest {
542    pub order_control_id: String,
543    pub idempotency_key: String,
544}
545
546#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
547#[serde(rename_all = "snake_case")]
548pub enum PlatformOrderControlStatus {
549    Submitting,
550    Submitted,
551    Failed,
552}
553
554#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
555#[serde(deny_unknown_fields)]
556pub struct PlatformOrderStatusResponse {
557    pub schema_version: u16,
558    pub contract_version: String,
559    pub order_control_id: String,
560    pub market_id: String,
561    pub action: PlatformOrderAction,
562    pub order_ids: Vec<String>,
563    pub signature: String,
564    pub status: PlatformOrderControlStatus,
565    pub failure_code: Option<String>,
566    pub updated_at_ms: u64,
567}
568
569#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
570#[serde(deny_unknown_fields)]
571pub struct PlatformAccountOrder {
572    pub order_id: String,
573    pub side: PlatformTradeSide,
574    pub order_type: PlatformOrderType,
575    pub state: PlatformOrderState,
576    pub limit_price_atoms: String,
577    pub original_size_atoms: String,
578    pub remaining_size_atoms: String,
579}
580
581#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
582#[serde(deny_unknown_fields)]
583pub struct PlatformAccountFill {
584    pub fill_id: String,
585    pub side: PlatformTradeSide,
586    pub price_atoms: String,
587    pub size_atoms: String,
588    pub fee_quote_atoms: String,
589    pub fee_is_final: bool,
590    pub settlement: PlatformSettlementState,
591    pub executed_at_ms: u64,
592    pub confirmed_at_ms: Option<u64>,
593    pub transaction_id: Option<String>,
594    pub realized_pnl_quote_atoms: String,
595}
596
597#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
598#[serde(deny_unknown_fields)]
599pub struct PlatformAccountSnapshotResponse {
600    pub schema_version: u16,
601    pub contract_version: String,
602    pub market_id: String,
603    pub wallet_address: String,
604    pub server_time_ms: u64,
605    pub orders: Vec<PlatformAccountOrder>,
606    pub fills: Vec<PlatformAccountFill>,
607}
608
609#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
610#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
611pub enum PlatformAccountEvent {
612    AuthChallenge {
613        schema_version: u16,
614        contract_version: String,
615        market_id: String,
616        wallet_address: String,
617        challenge: String,
618        server_time_ms: u64,
619        expires_at_ms: u64,
620    },
621    AccountSnapshot {
622        schema_version: u16,
623        contract_version: String,
624        market_id: String,
625        wallet_address: String,
626        stream_id: String,
627        sequence: String,
628        server_time_ms: u64,
629        orders: Vec<PlatformAccountOrder>,
630        fills: Vec<PlatformAccountFill>,
631    },
632    OrdersSnapshot {
633        schema_version: u16,
634        contract_version: String,
635        market_id: String,
636        wallet_address: String,
637        stream_id: String,
638        sequence: String,
639        previous_sequence: String,
640        server_time_ms: u64,
641        orders: Vec<PlatformAccountOrder>,
642    },
643    Fill {
644        schema_version: u16,
645        contract_version: String,
646        market_id: String,
647        wallet_address: String,
648        stream_id: String,
649        sequence: String,
650        previous_sequence: String,
651        server_time_ms: u64,
652        fill: PlatformAccountFill,
653    },
654    Heartbeat {
655        schema_version: u16,
656        contract_version: String,
657        market_id: String,
658        wallet_address: String,
659        stream_id: String,
660        sequence: String,
661        previous_sequence: String,
662        server_time_ms: u64,
663    },
664}
665
666#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
667#[serde(tag = "type", rename_all = "snake_case")]
668pub enum PlatformMarketDataEvent {
669    BookSnapshot {
670        schema_version: u16,
671        contract_version: String,
672        market_id: String,
673        stream_id: String,
674        sequence: String,
675        server_time_ms: u64,
676        snapshot_id: String,
677        bids: Vec<PlatformBookLevel>,
678        asks: Vec<PlatformBookLevel>,
679    },
680    BookDelta {
681        schema_version: u16,
682        contract_version: String,
683        market_id: String,
684        stream_id: String,
685        sequence: String,
686        previous_sequence: String,
687        server_time_ms: u64,
688        changes: Vec<PlatformBookChange>,
689    },
690    BestBidAsk {
691        schema_version: u16,
692        contract_version: String,
693        market_id: String,
694        stream_id: String,
695        sequence: String,
696        server_time_ms: u64,
697        best_bid: Option<PlatformBookLevel>,
698        best_ask: Option<PlatformBookLevel>,
699    },
700    Trade {
701        schema_version: u16,
702        contract_version: String,
703        market_id: String,
704        server_time_ms: u64,
705        trade: PlatformTrade,
706    },
707    MarketStatus {
708        schema_version: u16,
709        contract_version: String,
710        market_id: String,
711        server_time_ms: u64,
712        status: PlatformMarketState,
713    },
714    Heartbeat {
715        schema_version: u16,
716        contract_version: String,
717        market_id: String,
718        server_time_ms: u64,
719    },
720}
721
722#[cfg(test)]
723mod tests {
724    use super::*;
725
726    #[test]
727    fn public_platform_fixtures_decode_strictly() {
728        let discovery: PlatformDiscoveryResponse =
729            serde_json::from_str(PLATFORM_CAPABILITIES_FIXTURE).unwrap();
730        let assets: PlatformAssetsResponse = serde_json::from_str(PLATFORM_ASSETS_FIXTURE).unwrap();
731        let markets: PlatformMarketsResponse =
732            serde_json::from_str(PLATFORM_MARKETS_FIXTURE).unwrap();
733        let book: PlatformBookSnapshotResponse =
734            serde_json::from_str(PLATFORM_BOOK_FIXTURE).unwrap();
735        let bbo: PlatformBestBidAskResponse = serde_json::from_str(PLATFORM_BBO_FIXTURE).unwrap();
736        let fees: PlatformFeeScheduleResponse =
737            serde_json::from_str(PLATFORM_FEES_FIXTURE).unwrap();
738        let status: PlatformMarketStatusResponse =
739            serde_json::from_str(PLATFORM_STATUS_FIXTURE).unwrap();
740        let trades: PlatformTradesResponse = serde_json::from_str(PLATFORM_TRADES_FIXTURE).unwrap();
741        let account: PlatformAccountSnapshotResponse =
742            serde_json::from_str(PLATFORM_ACCOUNT_FIXTURE).unwrap();
743        let order_challenge: PlatformOrderChallengeResponse =
744            serde_json::from_str(PLATFORM_ORDER_CHALLENGE_FIXTURE).unwrap();
745        let order_prepare: PlatformOrderPrepareResponse =
746            serde_json::from_str(PLATFORM_ORDER_PREPARE_FIXTURE).unwrap();
747        let order_submit: PlatformOrderSubmitResponse =
748            serde_json::from_str(PLATFORM_ORDER_SUBMIT_FIXTURE).unwrap();
749        let order_status: PlatformOrderStatusResponse =
750            serde_json::from_str(PLATFORM_ORDER_STATUS_FIXTURE).unwrap();
751
752        assert_eq!(discovery.schema_version, PLATFORM_SCHEMA_VERSION);
753        assert_eq!(discovery.capabilities.len(), 5);
754        assert!(!discovery.authority.accepts_private_keys);
755        assert_eq!(assets.assets.len(), 2);
756        assert_eq!(markets.markets.len(), 1);
757        assert_eq!(markets.markets[0].base_asset_id, assets.assets[0].asset_id);
758        assert_eq!(markets.markets[0].quote_asset_id, assets.assets[1].asset_id);
759        assert_eq!(book.sequence, "42");
760        assert_eq!(bbo.best_bid.unwrap().price_atoms, "149990000");
761        assert_eq!(fees.maximum_immediate_execution_fee_bps, 10);
762        assert_eq!(status.status, PlatformMarketState::Active);
763        assert_eq!(trades.trades.len(), 1);
764        assert_eq!(account.orders.len(), 1);
765        assert_eq!(account.fills.len(), 1);
766        assert_eq!(order_challenge.action, PlatformOrderAction::Place);
767        assert_eq!(order_prepare.order_ids, order_challenge.order_ids);
768        assert_eq!(order_submit.order_ids, order_challenge.order_ids);
769        assert_eq!(order_status.order_control_id, order_submit.order_control_id);
770        assert_eq!(order_status.status, PlatformOrderControlStatus::Submitting);
771    }
772
773    #[test]
774    fn public_platform_response_rejects_unreviewed_fields() {
775        let mut value: serde_json::Value =
776            serde_json::from_str(PLATFORM_CAPABILITIES_FIXTURE).unwrap();
777        value
778            .as_object_mut()
779            .unwrap()
780            .insert("unexpected_field".to_owned(), serde_json::Value::Bool(true));
781        assert!(serde_json::from_value::<PlatformDiscoveryResponse>(value).is_err());
782
783        let mut account_event: serde_json::Value =
784            serde_json::from_str(PLATFORM_ACCOUNT_FIXTURE).unwrap();
785        let event = account_event.as_object_mut().unwrap();
786        event.insert("type".to_owned(), serde_json::json!("account_snapshot"));
787        event.insert(
788            "stream_id".to_owned(),
789            serde_json::json!("account_stream_66666666666666666666666666666666"),
790        );
791        event.insert("sequence".to_owned(), serde_json::json!("1"));
792        event.insert("unexpected_field".to_owned(), serde_json::json!(true));
793        assert!(serde_json::from_value::<PlatformAccountEvent>(account_event).is_err());
794    }
795
796    #[test]
797    fn atomic_order_batch_request_is_strict_and_typed() {
798        let request: PlatformOrderChallengeRequest = serde_json::from_value(serde_json::json!({
799            "action": "batch",
800            "owner_wallet": "11111111111111111111111111111111",
801            "session_public_key": "22222222222222222222222222222222",
802            "operations": [
803                {
804                    "action": "cancel",
805                    "order_id": "order_11111111111111111111111111111111"
806                },
807                {
808                    "action": "replace",
809                    "order_id": "order_22222222222222222222222222222222",
810                    "account_sequence": "8",
811                    "client_order_id": "replacement-8",
812                    "side": "sell",
813                    "order_type": "post_only",
814                    "limit_price_atoms": "151000000",
815                    "size_atoms": "2000000"
816                }
817            ]
818        }))
819        .unwrap();
820        let PlatformOrderChallengeRequest::Batch { operations, .. } = request else {
821            panic!("expected batch request");
822        };
823        assert_eq!(operations.len(), 2);
824        assert!(matches!(
825            operations[1],
826            PlatformOrderBatchOperation::Replace { .. }
827        ));
828
829        assert!(
830            serde_json::from_value::<PlatformOrderChallengeRequest>(serde_json::json!({
831                "action": "batch",
832                "owner_wallet": "11111111111111111111111111111111",
833                "session_public_key": "22222222222222222222222222222222",
834                "operations": [{
835                    "action": "cancel",
836                    "order_id": "order_11111111111111111111111111111111",
837                    "implementation": "hidden"
838                }]
839            }))
840            .is_err()
841        );
842    }
843}