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/// One sequenced event emitted by the persistent order-control connection.
654/// After authentication, the transport carries bounded arrays of these events
655/// so concurrent results share frame overhead without weakening per-event
656/// sequence or request correlation. Terminal chain status may arrive later
657/// without blocking command submission.
658#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
659#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
660pub enum PlatformOrderCommandEvent {
661    AuthChallenge {
662        schema_version: u16,
663        contract_version: String,
664        market_id: String,
665        challenge: String,
666        server_time_ms: u64,
667        expires_at_ms: u64,
668    },
669    Ready {
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    },
677    ChallengeResult {
678        schema_version: u16,
679        contract_version: String,
680        market_id: String,
681        stream_id: String,
682        sequence: String,
683        previous_sequence: String,
684        request_id: String,
685        self_trade_prevention: PlatformSelfTradePrevention,
686        prevented_order_ids: Vec<String>,
687        effective_request: PlatformOrderChallengeRequest,
688        response: PlatformOrderChallengeResponse,
689        server_time_ms: u64,
690    },
691    PrepareResult {
692        schema_version: u16,
693        contract_version: String,
694        market_id: String,
695        stream_id: String,
696        sequence: String,
697        previous_sequence: String,
698        request_id: String,
699        response: PlatformOrderPrepareResponse,
700        server_time_ms: u64,
701    },
702    SubmitResult {
703        schema_version: u16,
704        contract_version: String,
705        market_id: String,
706        stream_id: String,
707        sequence: String,
708        previous_sequence: String,
709        request_id: String,
710        response: PlatformOrderSubmitResponse,
711        server_time_ms: u64,
712    },
713    StatusResult {
714        schema_version: u16,
715        contract_version: String,
716        market_id: String,
717        stream_id: String,
718        sequence: String,
719        previous_sequence: String,
720        request_id: String,
721        response: PlatformOrderStatusResponse,
722        server_time_ms: u64,
723    },
724    DeadManResult {
725        schema_version: u16,
726        contract_version: String,
727        market_id: String,
728        stream_id: String,
729        sequence: String,
730        previous_sequence: String,
731        request_id: String,
732        state: PlatformDeadManState,
733        server_time_ms: u64,
734    },
735    CommandError {
736        schema_version: u16,
737        contract_version: String,
738        market_id: String,
739        stream_id: String,
740        sequence: String,
741        previous_sequence: String,
742        request_id: String,
743        error: PublicOperationError,
744        server_time_ms: u64,
745    },
746    Heartbeat {
747        schema_version: u16,
748        contract_version: String,
749        market_id: String,
750        stream_id: String,
751        sequence: String,
752        previous_sequence: String,
753        server_time_ms: u64,
754    },
755}
756
757#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
758#[serde(deny_unknown_fields)]
759pub struct PlatformAccountOrder {
760    pub order_id: String,
761    pub side: PlatformTradeSide,
762    pub order_type: PlatformOrderType,
763    pub state: PlatformOrderState,
764    pub limit_price_atoms: String,
765    pub original_size_atoms: String,
766    pub remaining_size_atoms: String,
767}
768
769#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
770#[serde(deny_unknown_fields)]
771pub struct PlatformAccountFill {
772    pub fill_id: String,
773    pub side: PlatformTradeSide,
774    pub price_atoms: String,
775    pub size_atoms: String,
776    pub fee_quote_atoms: String,
777    pub fee_is_final: bool,
778    pub settlement: PlatformSettlementState,
779    pub executed_at_ms: u64,
780    pub confirmed_at_ms: Option<u64>,
781    pub transaction_id: Option<String>,
782    pub realized_pnl_quote_atoms: String,
783}
784
785#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
786#[serde(deny_unknown_fields)]
787pub struct PlatformAccountSnapshotResponse {
788    pub schema_version: u16,
789    pub contract_version: String,
790    pub market_id: String,
791    pub wallet_address: String,
792    pub server_time_ms: u64,
793    pub orders: Vec<PlatformAccountOrder>,
794    pub fills: Vec<PlatformAccountFill>,
795}
796
797#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
798#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
799pub enum PlatformAccountEvent {
800    AuthChallenge {
801        schema_version: u16,
802        contract_version: String,
803        market_id: String,
804        wallet_address: String,
805        challenge: String,
806        server_time_ms: u64,
807        expires_at_ms: u64,
808    },
809    AccountSnapshot {
810        schema_version: u16,
811        contract_version: String,
812        market_id: String,
813        wallet_address: String,
814        stream_id: String,
815        sequence: String,
816        server_time_ms: u64,
817        orders: Vec<PlatformAccountOrder>,
818        fills: Vec<PlatformAccountFill>,
819    },
820    OrdersSnapshot {
821        schema_version: u16,
822        contract_version: String,
823        market_id: String,
824        wallet_address: String,
825        stream_id: String,
826        sequence: String,
827        previous_sequence: String,
828        server_time_ms: u64,
829        orders: Vec<PlatformAccountOrder>,
830    },
831    Fill {
832        schema_version: u16,
833        contract_version: String,
834        market_id: String,
835        wallet_address: String,
836        stream_id: String,
837        sequence: String,
838        previous_sequence: String,
839        server_time_ms: u64,
840        fill: PlatformAccountFill,
841    },
842    Heartbeat {
843        schema_version: u16,
844        contract_version: String,
845        market_id: String,
846        wallet_address: String,
847        stream_id: String,
848        sequence: String,
849        previous_sequence: String,
850        server_time_ms: u64,
851    },
852}
853
854#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
855#[serde(tag = "type", rename_all = "snake_case")]
856pub enum PlatformMarketDataEvent {
857    BookSnapshot {
858        schema_version: u16,
859        contract_version: String,
860        market_id: String,
861        stream_id: String,
862        sequence: String,
863        server_time_ms: u64,
864        snapshot_id: String,
865        bids: Vec<PlatformBookLevel>,
866        asks: Vec<PlatformBookLevel>,
867    },
868    BookDelta {
869        schema_version: u16,
870        contract_version: String,
871        market_id: String,
872        stream_id: String,
873        sequence: String,
874        previous_sequence: String,
875        server_time_ms: u64,
876        changes: Vec<PlatformBookChange>,
877    },
878    BestBidAsk {
879        schema_version: u16,
880        contract_version: String,
881        market_id: String,
882        stream_id: String,
883        sequence: String,
884        server_time_ms: u64,
885        best_bid: Option<PlatformBookLevel>,
886        best_ask: Option<PlatformBookLevel>,
887    },
888    Trade {
889        schema_version: u16,
890        contract_version: String,
891        market_id: String,
892        server_time_ms: u64,
893        trade: PlatformTrade,
894    },
895    MarketStatus {
896        schema_version: u16,
897        contract_version: String,
898        market_id: String,
899        server_time_ms: u64,
900        status: PlatformMarketState,
901    },
902    Heartbeat {
903        schema_version: u16,
904        contract_version: String,
905        market_id: String,
906        server_time_ms: u64,
907    },
908}
909
910#[cfg(test)]
911mod tests {
912    use super::*;
913
914    #[test]
915    fn public_platform_fixtures_decode_strictly() {
916        let discovery: PlatformDiscoveryResponse =
917            serde_json::from_str(PLATFORM_CAPABILITIES_FIXTURE).unwrap();
918        let assets: PlatformAssetsResponse = serde_json::from_str(PLATFORM_ASSETS_FIXTURE).unwrap();
919        let markets: PlatformMarketsResponse =
920            serde_json::from_str(PLATFORM_MARKETS_FIXTURE).unwrap();
921        let book: PlatformBookSnapshotResponse =
922            serde_json::from_str(PLATFORM_BOOK_FIXTURE).unwrap();
923        let bbo: PlatformBestBidAskResponse = serde_json::from_str(PLATFORM_BBO_FIXTURE).unwrap();
924        let fees: PlatformFeeScheduleResponse =
925            serde_json::from_str(PLATFORM_FEES_FIXTURE).unwrap();
926        let status: PlatformMarketStatusResponse =
927            serde_json::from_str(PLATFORM_STATUS_FIXTURE).unwrap();
928        let trades: PlatformTradesResponse = serde_json::from_str(PLATFORM_TRADES_FIXTURE).unwrap();
929        let account: PlatformAccountSnapshotResponse =
930            serde_json::from_str(PLATFORM_ACCOUNT_FIXTURE).unwrap();
931        let order_challenge: PlatformOrderChallengeResponse =
932            serde_json::from_str(PLATFORM_ORDER_CHALLENGE_FIXTURE).unwrap();
933        let order_prepare: PlatformOrderPrepareResponse =
934            serde_json::from_str(PLATFORM_ORDER_PREPARE_FIXTURE).unwrap();
935        let order_submit: PlatformOrderSubmitResponse =
936            serde_json::from_str(PLATFORM_ORDER_SUBMIT_FIXTURE).unwrap();
937        let order_status: PlatformOrderStatusResponse =
938            serde_json::from_str(PLATFORM_ORDER_STATUS_FIXTURE).unwrap();
939
940        assert_eq!(discovery.schema_version, PLATFORM_SCHEMA_VERSION);
941        assert_eq!(discovery.capabilities.len(), 5);
942        assert!(!discovery.authority.accepts_private_keys);
943        assert_eq!(assets.assets.len(), 2);
944        assert_eq!(markets.markets.len(), 1);
945        assert_eq!(markets.markets[0].base_asset_id, assets.assets[0].asset_id);
946        assert_eq!(markets.markets[0].quote_asset_id, assets.assets[1].asset_id);
947        assert_eq!(book.sequence, "42");
948        assert_eq!(bbo.best_bid.unwrap().price_atoms, "149990000");
949        assert_eq!(fees.maximum_immediate_execution_fee_bps, 10);
950        assert_eq!(status.status, PlatformMarketState::Active);
951        assert_eq!(trades.trades.len(), 1);
952        assert_eq!(account.orders.len(), 1);
953        assert_eq!(account.fills.len(), 1);
954        assert_eq!(order_challenge.action, PlatformOrderAction::Place);
955        assert_eq!(order_prepare.order_ids, order_challenge.order_ids);
956        assert_eq!(order_submit.order_ids, order_challenge.order_ids);
957        assert_eq!(order_status.order_control_id, order_submit.order_control_id);
958        assert_eq!(order_status.status, PlatformOrderControlStatus::Submitting);
959    }
960
961    #[test]
962    fn public_platform_response_rejects_unreviewed_fields() {
963        let mut value: serde_json::Value =
964            serde_json::from_str(PLATFORM_CAPABILITIES_FIXTURE).unwrap();
965        value
966            .as_object_mut()
967            .unwrap()
968            .insert("unexpected_field".to_owned(), serde_json::Value::Bool(true));
969        assert!(serde_json::from_value::<PlatformDiscoveryResponse>(value).is_err());
970
971        let mut account_event: serde_json::Value =
972            serde_json::from_str(PLATFORM_ACCOUNT_FIXTURE).unwrap();
973        let event = account_event.as_object_mut().unwrap();
974        event.insert("type".to_owned(), serde_json::json!("account_snapshot"));
975        event.insert(
976            "stream_id".to_owned(),
977            serde_json::json!("account_stream_66666666666666666666666666666666"),
978        );
979        event.insert("sequence".to_owned(), serde_json::json!("1"));
980        event.insert("unexpected_field".to_owned(), serde_json::json!(true));
981        assert!(serde_json::from_value::<PlatformAccountEvent>(account_event).is_err());
982    }
983
984    #[test]
985    fn atomic_order_batch_request_is_strict_and_typed() {
986        let request: PlatformOrderChallengeRequest = serde_json::from_value(serde_json::json!({
987            "action": "batch",
988            "owner_wallet": "11111111111111111111111111111111",
989            "session_public_key": "22222222222222222222222222222222",
990            "operations": [
991                {
992                    "action": "cancel",
993                    "order_id": "order_11111111111111111111111111111111"
994                },
995                {
996                    "action": "replace",
997                    "order_id": "order_22222222222222222222222222222222",
998                    "account_sequence": "8",
999                    "client_order_id": "replacement-8",
1000                    "side": "sell",
1001                    "order_type": "post_only",
1002                    "limit_price_atoms": "151000000",
1003                    "size_atoms": "2000000"
1004                }
1005            ]
1006        }))
1007        .unwrap();
1008        let PlatformOrderChallengeRequest::Batch { operations, .. } = request else {
1009            panic!("expected batch request");
1010        };
1011        assert_eq!(operations.len(), 2);
1012        assert!(matches!(
1013            operations[1],
1014            PlatformOrderBatchOperation::Replace { .. }
1015        ));
1016
1017        assert!(
1018            serde_json::from_value::<PlatformOrderChallengeRequest>(serde_json::json!({
1019                "action": "batch",
1020                "owner_wallet": "11111111111111111111111111111111",
1021                "session_public_key": "22222222222222222222222222222222",
1022                "operations": [{
1023                    "action": "cancel",
1024                    "order_id": "order_11111111111111111111111111111111",
1025                    "implementation": "hidden"
1026                }]
1027            }))
1028            .is_err()
1029        );
1030    }
1031
1032    #[test]
1033    fn persistent_order_commands_are_strict_and_explicit_about_self_trade_policy() {
1034        let frame: PlatformOrderCommandClientFrame = serde_json::from_value(serde_json::json!({
1035            "type": "command",
1036            "request_id": "agent-1",
1037            "sequence": "1",
1038            "command": {
1039                "type": "challenge",
1040                "self_trade_prevention": "cancel_maker",
1041                "request": {
1042                    "action": "cancel_all",
1043                    "owner_wallet": "11111111111111111111111111111111",
1044                    "session_public_key": "22222222222222222222222222222222"
1045                }
1046            }
1047        }))
1048        .unwrap();
1049        assert!(matches!(
1050            frame,
1051            PlatformOrderCommandClientFrame::Command {
1052                command: PlatformOrderCommand::Challenge {
1053                    self_trade_prevention: PlatformSelfTradePrevention::CancelMaker,
1054                    ..
1055                },
1056                ..
1057            }
1058        ));
1059        assert!(
1060            serde_json::from_value::<PlatformOrderCommandClientFrame>(serde_json::json!({
1061                "type": "command",
1062                "request_id": "agent-1",
1063                "sequence": "1",
1064                "command": {
1065                    "type": "challenge",
1066                    "request": {
1067                        "action": "cancel_all",
1068                        "owner_wallet": "11111111111111111111111111111111",
1069                        "session_public_key": "22222222222222222222222222222222"
1070                    }
1071                }
1072            }))
1073            .is_err()
1074        );
1075    }
1076}