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    SelfTradePrevented,
134    DeadManExpired,
135    RateLimited,
136    TemporarilyUnavailable,
137    SubmissionAmbiguous,
138    SettlementPending,
139    SettlementFailed,
140}
141
142/// Exact asset amount. Public money never crosses the contract as a float.
143#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
144#[serde(deny_unknown_fields)]
145pub struct ExactAmount {
146    pub asset_id: String,
147    pub atoms: String,
148}
149
150/// Sequence metadata shared by all recoverable state streams.
151#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
152#[serde(deny_unknown_fields)]
153pub struct SequenceEnvelope {
154    pub stream_id: String,
155    pub sequence: String,
156    pub previous_sequence: Option<String>,
157    pub server_time_ms: u64,
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub snapshot_id: Option<String>,
160}
161
162#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
163#[serde(deny_unknown_fields)]
164pub struct PageRequest {
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub cursor: Option<String>,
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub limit: Option<u32>,
169}
170
171#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
172#[serde(deny_unknown_fields)]
173pub struct PageInfo {
174    pub next_cursor: Option<String>,
175    pub has_more: bool,
176}
177
178#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
179#[serde(deny_unknown_fields)]
180pub struct PublicOperationError {
181    pub code: PlatformPublicErrorCode,
182    pub message: String,
183    pub retryable: bool,
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub retry_after_ms: Option<u64>,
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub operation_id: Option<String>,
188}
189
190/// One operation currently callable through the live v2 gateway.
191#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
192#[serde(deny_unknown_fields)]
193pub struct LivePlatformCapability {
194    pub id: String,
195    pub risk: CapabilityRisk,
196    pub required_scope: String,
197    pub transports: Vec<PlatformTransport>,
198    pub mcp_exposure: McpExposure,
199}
200
201/// Operations currently available to the client.
202#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
203#[serde(deny_unknown_fields)]
204pub struct PlatformDiscoveryResponse {
205    pub schema_version: u16,
206    pub contract_version: String,
207    pub server_time_ms: u64,
208    pub authority: PlatformAuthority,
209    pub capabilities: Vec<LivePlatformCapability>,
210}
211
212#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
213#[serde(rename_all = "snake_case")]
214pub enum PlatformNetwork {
215    Solana,
216}
217
218/// Asset identity used by ordinary SDK operations.
219#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
220#[serde(deny_unknown_fields)]
221pub struct PlatformAsset {
222    pub asset_id: String,
223    pub symbol: String,
224    pub name: String,
225    pub decimals: u8,
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub logo_url: Option<String>,
228    pub network: PlatformNetwork,
229}
230
231#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
232#[serde(deny_unknown_fields)]
233pub struct PlatformAssetsResponse {
234    pub schema_version: u16,
235    pub contract_version: String,
236    pub server_time_ms: u64,
237    pub assets: Vec<PlatformAsset>,
238    pub page: PageInfo,
239}
240
241#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
242#[serde(rename_all = "snake_case")]
243pub enum PlatformMarketAction {
244    Quote,
245    ExecuteImmediate,
246    PlaceOrder,
247    ScheduleTwap,
248}
249
250/// Stable market metadata for public SDK operations.
251#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
252#[serde(deny_unknown_fields)]
253pub struct PlatformMarket {
254    pub market_id: String,
255    pub label: String,
256    pub base_asset_id: String,
257    pub quote_asset_id: String,
258    pub status: PlatformMarketState,
259    pub available_actions: Vec<PlatformMarketAction>,
260}
261
262#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
263#[serde(deny_unknown_fields)]
264pub struct PlatformMarketsResponse {
265    pub schema_version: u16,
266    pub contract_version: String,
267    pub server_time_ms: u64,
268    pub markets: Vec<PlatformMarket>,
269    pub page: PageInfo,
270}
271
272#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
273#[serde(deny_unknown_fields)]
274pub struct PlatformBookLevel {
275    /// Quote atoms per whole base unit, encoded as an unsigned decimal string.
276    pub price_atoms: String,
277    /// Available base quantity in base atoms, encoded as an unsigned decimal string.
278    pub size_atoms: String,
279}
280
281#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
282#[serde(rename_all = "snake_case")]
283pub enum PlatformBookSide {
284    Bid,
285    Ask,
286}
287
288#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
289#[serde(deny_unknown_fields)]
290pub struct PlatformBookChange {
291    pub side: PlatformBookSide,
292    pub price_atoms: String,
293    /// Zero removes the price level.
294    pub size_atoms: String,
295}
296
297#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
298#[serde(deny_unknown_fields)]
299pub struct PlatformBookSnapshotResponse {
300    pub schema_version: u16,
301    pub contract_version: String,
302    pub market_id: String,
303    pub stream_id: String,
304    pub sequence: String,
305    pub server_time_ms: u64,
306    pub snapshot_id: String,
307    pub bids: Vec<PlatformBookLevel>,
308    pub asks: Vec<PlatformBookLevel>,
309}
310
311#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
312#[serde(deny_unknown_fields)]
313pub struct PlatformBestBidAskResponse {
314    pub schema_version: u16,
315    pub contract_version: String,
316    pub market_id: String,
317    pub stream_id: String,
318    pub sequence: String,
319    pub server_time_ms: u64,
320    pub best_bid: Option<PlatformBookLevel>,
321    pub best_ask: Option<PlatformBookLevel>,
322}
323
324#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
325#[serde(deny_unknown_fields)]
326pub struct PlatformFeeScheduleResponse {
327    pub schema_version: u16,
328    pub contract_version: String,
329    pub market_id: String,
330    pub server_time_ms: u64,
331    pub passive_maker_fee_bps: u16,
332    pub maximum_immediate_execution_fee_bps: u16,
333    pub book_prices_include_trading_fees: bool,
334    pub exact_fee_returned_by_quote: bool,
335}
336
337#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
338#[serde(deny_unknown_fields)]
339pub struct PlatformMarketStatusResponse {
340    pub schema_version: u16,
341    pub contract_version: String,
342    pub market_id: String,
343    pub server_time_ms: u64,
344    pub status: PlatformMarketState,
345    pub tick_size_atoms: String,
346    pub minimum_order_size_atoms: String,
347}
348
349#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
350#[serde(rename_all = "snake_case")]
351pub enum PlatformTradeSide {
352    Buy,
353    Sell,
354}
355
356#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
357#[serde(deny_unknown_fields)]
358pub struct PlatformTrade {
359    pub trade_id: String,
360    pub side: PlatformTradeSide,
361    pub price_atoms: String,
362    pub size_atoms: String,
363    pub executed_at_ms: u64,
364}
365
366#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
367#[serde(deny_unknown_fields)]
368pub struct PlatformTradesResponse {
369    pub schema_version: u16,
370    pub contract_version: String,
371    pub market_id: String,
372    pub server_time_ms: u64,
373    pub trades: Vec<PlatformTrade>,
374}
375
376#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
377#[serde(rename_all = "snake_case")]
378pub enum PlatformOrderType {
379    GoodUntilCancelled,
380    ImmediateOrCancel,
381    FillOrKill,
382    PostOnly,
383}
384
385/// Externally authorized resting-order operation. The public contract exposes
386/// product intent only; private construction details never cross the SDK
387/// boundary.
388#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
389#[serde(rename_all = "snake_case")]
390pub enum PlatformOrderAction {
391    Place,
392    Cancel,
393    CancelAll,
394    /// Atomically cancel one existing order and place its explicitly bound
395    /// successor in the same transaction.
396    Replace,
397    /// Atomically execute a bounded heterogeneous set of place, cancel, and
398    /// replace operations in one transaction.
399    Batch,
400}
401
402/// One operation inside an atomic order-control batch. Owner and session
403/// identity live on the enclosing challenge so no item can widen authority.
404#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
405#[serde(tag = "action", rename_all = "snake_case", deny_unknown_fields)]
406pub enum PlatformOrderBatchOperation {
407    Place {
408        account_sequence: String,
409        client_order_id: String,
410        side: PlatformTradeSide,
411        order_type: PlatformOrderType,
412        limit_price_atoms: String,
413        size_atoms: String,
414    },
415    Cancel {
416        order_id: String,
417    },
418    Replace {
419        order_id: String,
420        account_sequence: String,
421        client_order_id: String,
422        side: PlatformTradeSide,
423        order_type: PlatformOrderType,
424        limit_price_atoms: String,
425        size_atoms: String,
426    },
427}
428
429/// Request canonical bytes for one externally signed order-control operation.
430/// Variant-specific fields are sealed so an authorization cannot be widened
431/// between challenge and transaction preparation.
432#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
433#[serde(tag = "action", rename_all = "snake_case", deny_unknown_fields)]
434pub enum PlatformOrderChallengeRequest {
435    Place {
436        owner_wallet: String,
437        session_public_key: String,
438        account_sequence: String,
439        client_order_id: String,
440        side: PlatformTradeSide,
441        order_type: PlatformOrderType,
442        limit_price_atoms: String,
443        size_atoms: String,
444    },
445    Cancel {
446        owner_wallet: String,
447        session_public_key: String,
448        order_id: String,
449    },
450    CancelAll {
451        owner_wallet: String,
452        session_public_key: String,
453    },
454    Replace {
455        owner_wallet: String,
456        session_public_key: String,
457        order_id: String,
458        account_sequence: String,
459        client_order_id: String,
460        side: PlatformTradeSide,
461        order_type: PlatformOrderType,
462        limit_price_atoms: String,
463        size_atoms: String,
464    },
465    Batch {
466        owner_wallet: String,
467        session_public_key: String,
468        operations: Vec<PlatformOrderBatchOperation>,
469    },
470}
471
472#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
473#[serde(deny_unknown_fields)]
474pub struct PlatformOrderChallengeResponse {
475    pub schema_version: u16,
476    pub contract_version: String,
477    pub challenge_id: String,
478    pub market_id: String,
479    pub action: PlatformOrderAction,
480    /// Exact opaque order set bound by the authorization. Replace returns the
481    /// old then new ID. Batch flattens item IDs in request order, with replace
482    /// contributing old then new. A batch contains at most six operations.
483    pub order_ids: Vec<String>,
484    pub authorization_payload_base64: String,
485    pub server_time_ms: u64,
486    pub expires_at_ms: u64,
487}
488
489#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
490#[serde(deny_unknown_fields)]
491pub struct PlatformOrderPrepareRequest {
492    pub challenge_id: String,
493    /// Base58 Ed25519 signature over `authorization_payload_base64`.
494    pub authorization_signature: String,
495}
496
497#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
498#[serde(deny_unknown_fields)]
499pub struct PlatformOrderPrepareResponse {
500    pub schema_version: u16,
501    pub contract_version: String,
502    pub order_control_id: String,
503    pub market_id: String,
504    pub action: PlatformOrderAction,
505    pub order_ids: Vec<String>,
506    /// Backend-partially-signed Solana v0 transaction. The external session
507    /// signer verifies and fills only its signature slot.
508    pub transaction_base64: String,
509    pub recent_blockhash: String,
510    pub last_valid_block_height: u64,
511    pub expires_at_ms: u64,
512}
513
514#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
515#[serde(deny_unknown_fields)]
516pub struct PlatformOrderSubmitRequest {
517    pub order_control_id: String,
518    pub signed_transaction_base64: String,
519    pub idempotency_key: String,
520}
521
522#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
523#[serde(rename_all = "snake_case")]
524pub enum PlatformOrderSubmissionStatus {
525    Submitted,
526}
527
528#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
529#[serde(deny_unknown_fields)]
530pub struct PlatformOrderSubmitResponse {
531    pub schema_version: u16,
532    pub contract_version: String,
533    pub order_control_id: String,
534    pub market_id: String,
535    pub action: PlatformOrderAction,
536    pub order_ids: Vec<String>,
537    pub signature: String,
538    pub status: PlatformOrderSubmissionStatus,
539}
540
541#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
542#[serde(deny_unknown_fields)]
543pub struct PlatformOrderStatusRequest {
544    pub order_control_id: String,
545    pub idempotency_key: String,
546}
547
548#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
549#[serde(rename_all = "snake_case")]
550pub enum PlatformOrderControlStatus {
551    Submitting,
552    Submitted,
553    Failed,
554}
555
556#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
557#[serde(deny_unknown_fields)]
558pub struct PlatformOrderStatusResponse {
559    pub schema_version: u16,
560    pub contract_version: String,
561    pub order_control_id: String,
562    pub market_id: String,
563    pub action: PlatformOrderAction,
564    pub order_ids: Vec<String>,
565    pub signature: String,
566    pub status: PlatformOrderControlStatus,
567    pub failure_code: Option<String>,
568    pub updated_at_ms: u64,
569}
570
571/// Collision policy for an incoming order that would cross the owner's own
572/// resting liquidity. Every mode still preserves Strata's matcher and on-chain
573/// self-fill prohibition; this only controls which order is cancelled first.
574#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
575#[serde(rename_all = "snake_case")]
576pub enum PlatformSelfTradePrevention {
577    CancelTaker,
578    CancelMaker,
579    CancelBoth,
580    SkipOwnLiquidity,
581}
582
583/// One command on the persistent order-control connection. Challenge results
584/// may contain an effective request that differs from the requested one only
585/// by the explicitly selected self-trade prevention transformation.
586#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
587#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
588pub enum PlatformOrderCommand {
589    Challenge {
590        request: PlatformOrderChallengeRequest,
591        self_trade_prevention: PlatformSelfTradePrevention,
592    },
593    Prepare {
594        request: PlatformOrderPrepareRequest,
595    },
596    Submit {
597        request: PlatformOrderSubmitRequest,
598    },
599    Status {
600        request: PlatformOrderStatusRequest,
601    },
602    DeadManArm {
603        timeout_ms: u64,
604        request: PlatformOrderSubmitRequest,
605    },
606    DeadManStatus,
607    DeadManHeartbeat,
608    DeadManDisarm,
609}
610
611/// Frames sent by an external agent. Authentication proves possession of the
612/// declared session key; individual order authorizations and transactions keep
613/// their existing exact external-signing boundaries.
614#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
615#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
616pub enum PlatformOrderCommandClientFrame {
617    Authenticate {
618        owner_wallet: String,
619        session_public_key: String,
620        /// Base58 Ed25519 signature over the stream authentication payload.
621        signature: String,
622    },
623    Command {
624        request_id: String,
625        sequence: String,
626        command: PlatformOrderCommand,
627    },
628}
629
630#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
631#[serde(rename_all = "snake_case")]
632pub enum PlatformDeadManStatus {
633    Armed,
634    Triggering,
635    Triggered,
636    Disarmed,
637    Expired,
638    Failed,
639}
640
641#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
642#[serde(deny_unknown_fields)]
643pub struct PlatformDeadManState {
644    pub status: PlatformDeadManStatus,
645    pub timeout_ms: u64,
646    pub heartbeat_deadline_ms: u64,
647    pub order_control_id: Option<String>,
648    pub signature: Option<String>,
649    pub failure_code: Option<String>,
650    pub updated_at_ms: u64,
651}
652
653/// Sequenced frames emitted by the persistent order-control connection. Every
654/// command result is correlated by the caller's request ID; terminal chain
655/// status may arrive later without blocking command submission.
656#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
657#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
658pub enum PlatformOrderCommandEvent {
659    AuthChallenge {
660        schema_version: u16,
661        contract_version: String,
662        market_id: String,
663        challenge: String,
664        server_time_ms: u64,
665        expires_at_ms: u64,
666    },
667    Ready {
668        schema_version: u16,
669        contract_version: String,
670        market_id: String,
671        stream_id: String,
672        sequence: String,
673        server_time_ms: u64,
674    },
675    ChallengeResult {
676        schema_version: u16,
677        contract_version: String,
678        market_id: String,
679        stream_id: String,
680        sequence: String,
681        previous_sequence: String,
682        request_id: String,
683        self_trade_prevention: PlatformSelfTradePrevention,
684        prevented_order_ids: Vec<String>,
685        effective_request: PlatformOrderChallengeRequest,
686        response: PlatformOrderChallengeResponse,
687        server_time_ms: u64,
688    },
689    PrepareResult {
690        schema_version: u16,
691        contract_version: String,
692        market_id: String,
693        stream_id: String,
694        sequence: String,
695        previous_sequence: String,
696        request_id: String,
697        response: PlatformOrderPrepareResponse,
698        server_time_ms: u64,
699    },
700    SubmitResult {
701        schema_version: u16,
702        contract_version: String,
703        market_id: String,
704        stream_id: String,
705        sequence: String,
706        previous_sequence: String,
707        request_id: String,
708        response: PlatformOrderSubmitResponse,
709        server_time_ms: u64,
710    },
711    StatusResult {
712        schema_version: u16,
713        contract_version: String,
714        market_id: String,
715        stream_id: String,
716        sequence: String,
717        previous_sequence: String,
718        request_id: String,
719        response: PlatformOrderStatusResponse,
720        server_time_ms: u64,
721    },
722    DeadManResult {
723        schema_version: u16,
724        contract_version: String,
725        market_id: String,
726        stream_id: String,
727        sequence: String,
728        previous_sequence: String,
729        request_id: String,
730        state: PlatformDeadManState,
731        server_time_ms: u64,
732    },
733    CommandError {
734        schema_version: u16,
735        contract_version: String,
736        market_id: String,
737        stream_id: String,
738        sequence: String,
739        previous_sequence: String,
740        request_id: String,
741        error: PublicOperationError,
742        server_time_ms: u64,
743    },
744    Heartbeat {
745        schema_version: u16,
746        contract_version: String,
747        market_id: String,
748        stream_id: String,
749        sequence: String,
750        previous_sequence: String,
751        server_time_ms: u64,
752    },
753}
754
755#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
756#[serde(deny_unknown_fields)]
757pub struct PlatformAccountOrder {
758    pub order_id: String,
759    pub side: PlatformTradeSide,
760    pub order_type: PlatformOrderType,
761    pub state: PlatformOrderState,
762    pub limit_price_atoms: String,
763    pub original_size_atoms: String,
764    pub remaining_size_atoms: String,
765}
766
767#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
768#[serde(deny_unknown_fields)]
769pub struct PlatformAccountFill {
770    pub fill_id: String,
771    pub side: PlatformTradeSide,
772    pub price_atoms: String,
773    pub size_atoms: String,
774    pub fee_quote_atoms: String,
775    pub fee_is_final: bool,
776    pub settlement: PlatformSettlementState,
777    pub executed_at_ms: u64,
778    pub confirmed_at_ms: Option<u64>,
779    pub transaction_id: Option<String>,
780    pub realized_pnl_quote_atoms: String,
781}
782
783#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
784#[serde(deny_unknown_fields)]
785pub struct PlatformAccountSnapshotResponse {
786    pub schema_version: u16,
787    pub contract_version: String,
788    pub market_id: String,
789    pub wallet_address: String,
790    pub server_time_ms: u64,
791    pub orders: Vec<PlatformAccountOrder>,
792    pub fills: Vec<PlatformAccountFill>,
793}
794
795#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
796#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
797pub enum PlatformAccountEvent {
798    AuthChallenge {
799        schema_version: u16,
800        contract_version: String,
801        market_id: String,
802        wallet_address: String,
803        challenge: String,
804        server_time_ms: u64,
805        expires_at_ms: u64,
806    },
807    AccountSnapshot {
808        schema_version: u16,
809        contract_version: String,
810        market_id: String,
811        wallet_address: String,
812        stream_id: String,
813        sequence: String,
814        server_time_ms: u64,
815        orders: Vec<PlatformAccountOrder>,
816        fills: Vec<PlatformAccountFill>,
817    },
818    OrdersSnapshot {
819        schema_version: u16,
820        contract_version: String,
821        market_id: String,
822        wallet_address: String,
823        stream_id: String,
824        sequence: String,
825        previous_sequence: String,
826        server_time_ms: u64,
827        orders: Vec<PlatformAccountOrder>,
828    },
829    Fill {
830        schema_version: u16,
831        contract_version: String,
832        market_id: String,
833        wallet_address: String,
834        stream_id: String,
835        sequence: String,
836        previous_sequence: String,
837        server_time_ms: u64,
838        fill: PlatformAccountFill,
839    },
840    Heartbeat {
841        schema_version: u16,
842        contract_version: String,
843        market_id: String,
844        wallet_address: String,
845        stream_id: String,
846        sequence: String,
847        previous_sequence: String,
848        server_time_ms: u64,
849    },
850}
851
852#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
853#[serde(tag = "type", rename_all = "snake_case")]
854pub enum PlatformMarketDataEvent {
855    BookSnapshot {
856        schema_version: u16,
857        contract_version: String,
858        market_id: String,
859        stream_id: String,
860        sequence: String,
861        server_time_ms: u64,
862        snapshot_id: String,
863        bids: Vec<PlatformBookLevel>,
864        asks: Vec<PlatformBookLevel>,
865    },
866    BookDelta {
867        schema_version: u16,
868        contract_version: String,
869        market_id: String,
870        stream_id: String,
871        sequence: String,
872        previous_sequence: String,
873        server_time_ms: u64,
874        changes: Vec<PlatformBookChange>,
875    },
876    BestBidAsk {
877        schema_version: u16,
878        contract_version: String,
879        market_id: String,
880        stream_id: String,
881        sequence: String,
882        server_time_ms: u64,
883        best_bid: Option<PlatformBookLevel>,
884        best_ask: Option<PlatformBookLevel>,
885    },
886    Trade {
887        schema_version: u16,
888        contract_version: String,
889        market_id: String,
890        server_time_ms: u64,
891        trade: PlatformTrade,
892    },
893    MarketStatus {
894        schema_version: u16,
895        contract_version: String,
896        market_id: String,
897        server_time_ms: u64,
898        status: PlatformMarketState,
899    },
900    Heartbeat {
901        schema_version: u16,
902        contract_version: String,
903        market_id: String,
904        server_time_ms: u64,
905    },
906}
907
908#[cfg(test)]
909mod tests {
910    use super::*;
911
912    #[test]
913    fn public_platform_fixtures_decode_strictly() {
914        let discovery: PlatformDiscoveryResponse =
915            serde_json::from_str(PLATFORM_CAPABILITIES_FIXTURE).unwrap();
916        let assets: PlatformAssetsResponse = serde_json::from_str(PLATFORM_ASSETS_FIXTURE).unwrap();
917        let markets: PlatformMarketsResponse =
918            serde_json::from_str(PLATFORM_MARKETS_FIXTURE).unwrap();
919        let book: PlatformBookSnapshotResponse =
920            serde_json::from_str(PLATFORM_BOOK_FIXTURE).unwrap();
921        let bbo: PlatformBestBidAskResponse = serde_json::from_str(PLATFORM_BBO_FIXTURE).unwrap();
922        let fees: PlatformFeeScheduleResponse =
923            serde_json::from_str(PLATFORM_FEES_FIXTURE).unwrap();
924        let status: PlatformMarketStatusResponse =
925            serde_json::from_str(PLATFORM_STATUS_FIXTURE).unwrap();
926        let trades: PlatformTradesResponse = serde_json::from_str(PLATFORM_TRADES_FIXTURE).unwrap();
927        let account: PlatformAccountSnapshotResponse =
928            serde_json::from_str(PLATFORM_ACCOUNT_FIXTURE).unwrap();
929        let order_challenge: PlatformOrderChallengeResponse =
930            serde_json::from_str(PLATFORM_ORDER_CHALLENGE_FIXTURE).unwrap();
931        let order_prepare: PlatformOrderPrepareResponse =
932            serde_json::from_str(PLATFORM_ORDER_PREPARE_FIXTURE).unwrap();
933        let order_submit: PlatformOrderSubmitResponse =
934            serde_json::from_str(PLATFORM_ORDER_SUBMIT_FIXTURE).unwrap();
935        let order_status: PlatformOrderStatusResponse =
936            serde_json::from_str(PLATFORM_ORDER_STATUS_FIXTURE).unwrap();
937
938        assert_eq!(discovery.schema_version, PLATFORM_SCHEMA_VERSION);
939        assert_eq!(discovery.capabilities.len(), 5);
940        assert!(!discovery.authority.accepts_private_keys);
941        assert_eq!(assets.assets.len(), 2);
942        assert_eq!(markets.markets.len(), 1);
943        assert_eq!(markets.markets[0].base_asset_id, assets.assets[0].asset_id);
944        assert_eq!(markets.markets[0].quote_asset_id, assets.assets[1].asset_id);
945        assert_eq!(book.sequence, "42");
946        assert_eq!(bbo.best_bid.unwrap().price_atoms, "149990000");
947        assert_eq!(fees.maximum_immediate_execution_fee_bps, 10);
948        assert_eq!(status.status, PlatformMarketState::Active);
949        assert_eq!(trades.trades.len(), 1);
950        assert_eq!(account.orders.len(), 1);
951        assert_eq!(account.fills.len(), 1);
952        assert_eq!(order_challenge.action, PlatformOrderAction::Place);
953        assert_eq!(order_prepare.order_ids, order_challenge.order_ids);
954        assert_eq!(order_submit.order_ids, order_challenge.order_ids);
955        assert_eq!(order_status.order_control_id, order_submit.order_control_id);
956        assert_eq!(order_status.status, PlatformOrderControlStatus::Submitting);
957    }
958
959    #[test]
960    fn public_platform_response_rejects_unreviewed_fields() {
961        let mut value: serde_json::Value =
962            serde_json::from_str(PLATFORM_CAPABILITIES_FIXTURE).unwrap();
963        value
964            .as_object_mut()
965            .unwrap()
966            .insert("unexpected_field".to_owned(), serde_json::Value::Bool(true));
967        assert!(serde_json::from_value::<PlatformDiscoveryResponse>(value).is_err());
968
969        let mut account_event: serde_json::Value =
970            serde_json::from_str(PLATFORM_ACCOUNT_FIXTURE).unwrap();
971        let event = account_event.as_object_mut().unwrap();
972        event.insert("type".to_owned(), serde_json::json!("account_snapshot"));
973        event.insert(
974            "stream_id".to_owned(),
975            serde_json::json!("account_stream_66666666666666666666666666666666"),
976        );
977        event.insert("sequence".to_owned(), serde_json::json!("1"));
978        event.insert("unexpected_field".to_owned(), serde_json::json!(true));
979        assert!(serde_json::from_value::<PlatformAccountEvent>(account_event).is_err());
980    }
981
982    #[test]
983    fn atomic_order_batch_request_is_strict_and_typed() {
984        let request: PlatformOrderChallengeRequest = serde_json::from_value(serde_json::json!({
985            "action": "batch",
986            "owner_wallet": "11111111111111111111111111111111",
987            "session_public_key": "22222222222222222222222222222222",
988            "operations": [
989                {
990                    "action": "cancel",
991                    "order_id": "order_11111111111111111111111111111111"
992                },
993                {
994                    "action": "replace",
995                    "order_id": "order_22222222222222222222222222222222",
996                    "account_sequence": "8",
997                    "client_order_id": "replacement-8",
998                    "side": "sell",
999                    "order_type": "post_only",
1000                    "limit_price_atoms": "151000000",
1001                    "size_atoms": "2000000"
1002                }
1003            ]
1004        }))
1005        .unwrap();
1006        let PlatformOrderChallengeRequest::Batch { operations, .. } = request else {
1007            panic!("expected batch request");
1008        };
1009        assert_eq!(operations.len(), 2);
1010        assert!(matches!(
1011            operations[1],
1012            PlatformOrderBatchOperation::Replace { .. }
1013        ));
1014
1015        assert!(
1016            serde_json::from_value::<PlatformOrderChallengeRequest>(serde_json::json!({
1017                "action": "batch",
1018                "owner_wallet": "11111111111111111111111111111111",
1019                "session_public_key": "22222222222222222222222222222222",
1020                "operations": [{
1021                    "action": "cancel",
1022                    "order_id": "order_11111111111111111111111111111111",
1023                    "implementation": "hidden"
1024                }]
1025            }))
1026            .is_err()
1027        );
1028    }
1029
1030    #[test]
1031    fn persistent_order_commands_are_strict_and_explicit_about_self_trade_policy() {
1032        let frame: PlatformOrderCommandClientFrame = serde_json::from_value(serde_json::json!({
1033            "type": "command",
1034            "request_id": "agent-1",
1035            "sequence": "1",
1036            "command": {
1037                "type": "challenge",
1038                "self_trade_prevention": "cancel_maker",
1039                "request": {
1040                    "action": "cancel_all",
1041                    "owner_wallet": "11111111111111111111111111111111",
1042                    "session_public_key": "22222222222222222222222222222222"
1043                }
1044            }
1045        }))
1046        .unwrap();
1047        assert!(matches!(
1048            frame,
1049            PlatformOrderCommandClientFrame::Command {
1050                command: PlatformOrderCommand::Challenge {
1051                    self_trade_prevention: PlatformSelfTradePrevention::CancelMaker,
1052                    ..
1053                },
1054                ..
1055            }
1056        ));
1057        assert!(
1058            serde_json::from_value::<PlatformOrderCommandClientFrame>(serde_json::json!({
1059                "type": "command",
1060                "request_id": "agent-1",
1061                "sequence": "1",
1062                "command": {
1063                    "type": "challenge",
1064                    "request": {
1065                        "action": "cancel_all",
1066                        "owner_wallet": "11111111111111111111111111111111",
1067                        "session_public_key": "22222222222222222222222222222222"
1068                    }
1069                }
1070            }))
1071            .is_err()
1072        );
1073    }
1074}