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)]
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}