1use 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#[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#[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#[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#[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#[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#[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 pub price_atoms: String,
277 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 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#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
389#[serde(rename_all = "snake_case")]
390pub enum PlatformOrderAction {
391 Place,
392 Cancel,
393 CancelAll,
394 Replace,
397 Batch,
400}
401
402#[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#[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 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 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 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#[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#[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#[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 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#[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}